项目笔记---秒杀商城

秒杀商城项目:
(1)首先做了登录系统,用户传入的密码会经过两次MD5加密,目的是加强数据安全,然后将用户信息与数据库进行匹配实现一个登录功能;若登录成功则跳转到商品页面,若登录失败提示登录失败并重新跳转到登录页面。
(2)登录成功后,展示商品详情页,


前端技术:thymeleaf、Bootstrap、JQuery
后端技术:SpringBoot、JSR303(服务端的验证框架)、Mybatis
中间件 RabbitMQ(消息队列)、Redis(缓存)、Druid(数据连接池)

项目笔记---秒杀商城_第1张图片

如何应对大并发:1、利用缓存,并发的瓶颈在于数据库,因此通过减少数据库的访问可以有效解决高并发,因此通过缓存可以有效应对;2、使用异步



项目步骤:
1、搭建项目框架:SpringBoot环境搭建、集成Thymeleaf、集成Mybatis+Druid、集成Jedis+Redis安装+通用缓存key封装
2、实现登录功能:数据库设计、明文密码两次MD5处理、JSR303参数检验+全局异常处理、分布式Session
3、实现秒杀功能:数据库设计、商品列表页、商品页详情、订单页详情
4、JMeter压测:JMeter入门、自定义变量模拟多用户、JMeter命令行使用、SpringBoot打war包
5、页面优化技术:页面缓存+URL缓存+对象缓存、页面静态化+前后端分离、静态资源优化、CDN优化
6、接口优化:Redis预减库存减少对数据库的访问、内存标记减少Redis访问、RabbitMQ队列缓冲,异步下单,增强用户体验、RabbitMQ安装与SpringBoot集成、访问Nginx水平扩展、压测
7、安全优化:秒杀接口隐藏、数学公式验证码、接口防刷



两次MD5加密:
第一次:在用户端输入明文密码传入到后端要进行一次MD5加密,原因是因为明文密码传入到后端要进行网络传输有可能会被截获,通过加密防止用户密码在网络中以明文进行传输。
第二次:后端接到已经经过第一次加密的密码之后存到数据库之前在进行一次MD5加密,原因MD5加密本身的安全性不是非常高,通过第二次加密使得存入数据库的密码更为安全,防止数据库被盗后出现的数据安全问题。

MD5加密操作示例

@Component
public class MD5Utils {
    public static String md5(String src){
        //MD5加密
        return DigestUtils.md5Hex(src);
    }

    //准备盐,和前端的盐进行统一
    private static final String salt="1a2b3c4d";

    //第一次加密
    public static String inputPassToFormPass(String inputPass){
        //使用盐对传到后端的密码进行加密,
        String str = salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
        //返回第一次加密结果
        return md5(str);
    }

    //第二次加密,第二次加密的盐是要存到数据库里面的
    public static String formPassToDBPass(String formPass,String salt){
        String str = salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
        return md5(str);
    }

    public static String inputPassToDBPass(String inputPass,String salt){
        //获取第一次加密数据
        String formPass = inputPassToFormPass(inputPass);
        //获取第二次加密数据,第二次加密传入参数的是第一次加密数据
        String dbPass = formPassToDBPass(formPass, salt);
        //返回第二次加密结果
        return dbPass;
    }

    public static void main(String[] args) {
        //对123456第一次加密后:ce21b747de5af71ab5c2e20ff0a60eea
        System.out.println(inputPassToFormPass("123456"));
        //对第一次加密结果进行第二次加密:0687f9701bca74827fcefcd7e743d179
        System.out.println(formPassToDBPass("ce21b747de5af71ab5c2e20ff0a60eea","1a2b3c4d"));
        //两次加密后的结果测试:0687f9701bca74827fcefcd7e743d179,后端真正被调用的方法
        System.out.println(inputPassToDBPass("123456","1a2b3c4d"));
    }

}


JSR303进行参数校验,简化代码

1、引入对应依赖

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-validationartifactId>
        dependency>

在需要进行登录页面的参数类中添加@Valid注解

@RequestMapping("/doLogin")
    @ResponseBody
    public RespBean doLogin(@Valid LoginVo loginVo){
//        log.info("{}",loginVo);
        return userService.doLogin(loginVo);
    }

然后就可以通过注解的方式对参数是否存在、长度等信息进行校验

public class LoginVo {
    @NotNull //参数非空注解
    @IsMobile
    private String mobile;

    @NotNull
    @Length(min=32)//密码最小长度限制
    private String password;

}

自定义参数校验注解:判定手机号是否符合规范为例
创建验证手机号注解,@Constraint(validatedBy = {IsMobileValidator.class})为指定验证规则的类

/*
* 验证手机号
* */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

    boolean required() default true;

    String message() default "手机号码格式错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

编写注解中验证规则IsMobileValidator ,然后就可以使用@IsMobile注解来验证手机号

/*
* 手机号码校验规则
* */
public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {

    private boolean required = false;
    @Override
    public void initialize(IsMobile constraintAnnotation) {
        required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (required){
            return ValidatorUtil.isMobile(value);
        }else {
            if (StringUtils.isEmpty(value)){
                return true;
            }else {
                return ValidatorUtil.isMobile(value);
            }
        }
    }
}


页面登录功能总结:
1、使用MD5对密码进行两次加密,防止传输过程和数据库中的密码泄露
2、使用JSR303进行参数校验,简化代码,对需要校验的传入参数添加@Valid注解,在对属性根据要求添加对应的校验注解


分布式session:

解决方案1:session复制
优点:无需修改代码,只需要修改Tomcat配置
缺点:Session同步传输占用内网带宽、多台Tomcat同步性能指数级下降、Session占用内存,无法有效水平扩展


解决方案2:前端存储
优点:不占用服务端内存
缺点:存在安全风险、数据大小受cookie限制、占用外网带宽

解决方案3:Session粘滞
优点:无需修改代码、服务端可以水平扩展
缺点:增加新机器,会重新Hash,导致重新登录、应用重启,需要重新登录

解决方案4:后端集中存储
优点:安全、容易水平扩展
缺点:增加复杂度、需要修改代码

项目笔记---秒杀商城_第2张图片

分布式session解决方案:使用Redis实现分布式session

方案一、使用SpringSession实现
1)导入依赖spring-boot-starter-data-redis、commons-pool2、spring-session-data-redis


        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.sessiongroupId>
            <artifactId>spring-session-data-redisartifactId>
        dependency>


2)配置redis(properties.yml)

spring:
  redis:
    #服务器地址
    host: 119.xxx.xxx.xxx
    #端口
    port: 6379
    #连接密码
    password: xxxxxxx
    #默认操作的数据库
    database: 6
    #连接超时时间
    timeout: 10000ms
    #配置连接池
    lettuce:
      pool:
        #最大连接数
        max-active: 8
        #最大连接阻塞等待时间,默认-1
        max-wait: 10000ms
        #最大空闲连接,默认8
        max-idle: 200
        #最小空闲连接,默认0
        min-idle: 5

测试登录会发现,session以及存储在redis上
项目笔记---秒杀商城_第3张图片

方案二,将用户信息存入Redis
1、去掉spring-session-data-redis依赖,其余配置不变

编写redis配置类,由于redis中是以二进制呈现的,为了更好的人机交互,需要对其进行序列化

/*
* Redis配置类,实现redis的序列化(redis的数据是二进制的需要对其序列化来使得人机交互更为简便)
* */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //设置redis key的序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置redis value的序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //设置redis hash类型key的序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //设置redis hash类型value的序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());


        //注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

无论是使用SpringSession实现分布式缓存还是将用户信息存入redis,其原理都是将数据存入redis,从而使得信息同步,解决分布式session



登录功能总结:做了两次MD5加密,第一次防止明文密码在网络传输时泄露,第二次是防止数据库信息被盗窃从而导致第一次的MD5的盐和密码会被破解;使用Validation组件实现参数校验,实现手机号码的验证规则;分布式session问题,若运行后遇到大并发会使用多台服务器主机群,在tomcat进行登录,Nginx反向代理将用户信息放在tomcat1的session,跳转到另一个页面时,Nginx反向代理到tomcat2,此时tomcat2的session中并没有用于信息,从而需要让用户重新登录,分布式session的解决方案就是使用Springsession或将用户信息存入redis.

秒杀功能总结:该功能准备了四个表:商品表、秒杀商品表、订单表、秒杀订单表,同时准备了商品列表页,通过商品列表页的详情按钮转跳到商品详情页面,在秒杀时间内可以通过点击秒杀转跳到订单详情页面,如果秒杀失败则转跳到秒杀失败页面。

压力测试总结:Jmeter是Apache下的一款压力测试工具,可以用来进行压了测试。
QPS:每秒的查询率,即一台服务器每秒能够相应的查询次数,是在规定时间内所处理流量多少的衡量标准
TPS:事务/s,每秒能够完成的事务量
Jmeter





第一个优化:添加缓存
使用redis做缓存,缓存主要包含三个方面,页面缓存、URL缓存和对象缓存;优点是:通过缓存技术缩短系统的响应时间,减少网络传输时间和应用延迟时间,提高系统的吞吐量,增加系统的并发用户数,提高了数据库资源的利用率;
缺点是:在高并发场景下会出现缓存失效(缓存穿透、缓存雪崩、缓存击穿),造成瞬间数据库访问量增大,甚至崩溃,另外缓存无法做到与数据库数据实时同步,因此在实际的业务场景中需要考虑到数据库与缓存相关数据的一致性。
例如:用户的信息做了变更,如果redis里面的缓存不做处理就会出现问题,因此需要保持数据库与缓存的一致性





第二个优化:页面静态化。页面缓存缓存的是整个页面,传输给前端还是整个页面,数据量还是比较大,通过页面静态化将页面和专门的数据进行拆分,静态化后的页面利用浏览器缓存做缓存,减少数据量的传输。




解决库存超卖的问题:秒杀业务逻辑,先判定库存是否满足秒杀条件,同时判定是否为重复抢购,满足条件后进行秒杀,秒杀时限减库存操作,然后生成订单,在生成秒杀订单。要解决库存超卖关键在于秒杀商品减库存的操作。
第一步,在更新操作的时候,判定商品库存是否大于0,大于0才能做减少库存操作
解决同一用户同时秒杀多件商品的方法:可以通过数据库建立唯一索引避免




数据库的并发瓶颈较低,远远不如缓存,通过redis缓存减少数据库的访问,当知识查询时直接从redis获取,减少与数据库的交互,当进行更新操作时,将数据库与缓存同时进行更新;在秒杀下单时,将请求存入队列进行缓冲,通过队列进行异步下单增强用户体验,使用RabbitMQ进行消息队列缓冲。
减少数据库访问的思路:在redis里面扣减库存,在系统初始化时将商品库存信息加载到redis;当收到订单请求信息时,通过redis预减库存。库存不足秒杀失败,如果还有库存,将下单请求进入rabbitMQ消息队列,并且立即返回客户端正在排队中。请求入队后进入异步操作,异步生成订单,从而真正减少数据库的库存,确保数据库的库存是正确的。





Springboot集成RabbitMQ
1)添加依赖


        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>


Springboot集成RabbitMQ
2)配置rabbitmq

spring:
  rabbitmq:
    #服务器
    host: xxx.xxx.xxx.xxx
    #用户名(默认的用户名密码)
    username: guest
    #密码
    password: guest
    #虚拟主机
    virtual-host: /
    #端口
    port: 5672
    listener:
      simple:
        #消费者最小数量
        concurrency: 10
        #消费者最大数量
        max-concurrency: 10
        #限制消费者每次只能处理一条消息,处理完毕在继续下一条消息
        prefetch: 1
        #启动时是否默认启动容器(默认true)
        auto-startup: true
        #被拒绝时重新进入队列
        default-requeue-rejected: true
    #配置模板
    template:
      retry:
        #发布重试,默认false
        enabled: true
        #重试时间默认1000ms
        initial-interval: 1000ms
        #重试最大次数,默认3次
        max-attempts: 3
        #重试最大间隔时间,默认10000ms
        max-interval: 10000ms
        #重试的间隔乘数,若配2.0,则第一次等10s,第二次20s,第三次40s,配置为1间隔时间就是一致的
        multiplier: 1

Springboot集成RabbitMQ
3)准备队列用于存放消息,所有消息都需要经过队列

@Configuration //定义配置类注解@Configuration
public class RabbitMQConfig {

    //生成一个queue队列,并设置为持久化
    @Bean
    public Queue queue(){
        return new Queue("queue",true);
    }
}

Springboot集成RabbitMQ
4)消息生产者与消息消费者进行测试

/*
* 消息发送者
* */
@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object msg){
        log.info("发送消息"+msg);
        //指定往queue队列法msg消息
        rabbitTemplate.convertAndSend("queue",msg);
    }
}
/*
*
* 消息消费者
* */
@Service
@Slf4j
public class MQReceiver {
    @RabbitListener(queues = "queue")//该注解用于监听队列
    public void receive(Object msg){
        log.info("接收消息:"+msg);
    }
}

    /*
    * 功能描述:测试发送rabbitMQ消息
    * */
    @RequestMapping("/mq")
    @ResponseBody
    public void mq(){
        mqSender.send("Hello");
    }




生产者发送消息给用户;队列是存储消息的缓冲区;消费者是接收消息的用户。RabbitMQ核心思想是生成者从不直接向队列发送任何消息,而是只向交换机发送消息。
交换机一边接收来自生产者的消息,另一边将消息推送到队列。交换机必须确切的知道如何处理它接收到的消息。处理接收消息的规则由交换类型定义。

Fanout模式(广播模式):消息可以被多个队列同时接受,多个队列接受到的是同一个生产者发送的同一个消息。
项目笔记---秒杀商城_第4张图片

准备交换机—准备两个队列—绑定队列–测试

/*
* RabbitMQ配置类
* */
@Configuration //定义配置类注解@Configuration
public class RabbitMQConfig {

    private static final String QUEUE01 = "queue_fanout01";
    private static final String QUEUE02 = "queue_fanout02";
    private static final String EXCHANGE = "fanoutExchange";

    @Bean
    public Queue queue01(){
        return new Queue(QUEUE01);
    }

    @Bean
    public Queue queue02(){
        return new Queue(QUEUE02);
    }

    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange(EXCHANGE);
    }

    //将队列绑定到交换机
    @Bean
    public Binding binding01(){
        return BindingBuilder.bind(queue01()).to(fanoutExchange());
    }
    @Bean
    public Binding binding02(){
        return BindingBuilder.bind(queue02()).to(fanoutExchange());
    }
}
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
//消息发送
/*
* 消息发送者
* */
@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object msg){
        log.info("发送消息"+msg);
        //指定往queue队列法msg消息
        rabbitTemplate.convertAndSend("fanoutExchange","",msg);
    }
}
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
//消息接收
@Service
@Slf4j
public class MQReceiver {

    @RabbitListener(queues = "queue_fanout01")
    public void receive01(Object msg){
        log.info("QUEUE01接收消息:"+msg);
    }
    @RabbitListener(queues = "queue_fanout02")
    public void receive02(Object msg){
        log.info("QUEUE02接收消息:"+msg);
    }
}
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
//测试fanout模式
    /*
    * fanout模式
    * */
    @RequestMapping("/mq/fanout")
    @ResponseBody
    public void mq01(){
        mqSender.send("hello");
    }

Direct模式:涉及到路由键(routing key),将队列与路由键进行绑定(如图),如果发送的消息中携带路由键,该消息就会根据对应的路由键发送到匹配的队列。direct模式可以使用RabbitMQ自带的交换机,消息传递时路由键必须去匹配才能够被队列接受,否则消息就会被抛弃
项目笔记---秒杀商城_第5张图片

@Configuration
public class RabbitMQDirectConfig {

    //队列名
    private static final String QUEUE01 = "queue_direct01";
    private static final String QUEUE02 = "queue_direct02";
    //交换机名
    private static final String EXCHANGE = "directExchange";
    //路由键
    private static final String ROUTINGKEY01 = "queue.red";
    private static final String ROUTINGKEY02 = "queue.green";

    //创建队列
    @Bean
    public Queue queue01(){
        return new Queue(QUEUE01);
    }

    @Bean
    public Queue queue02(){
        return new Queue(QUEUE02);
    }

    //创建交换机
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(EXCHANGE);
    }

    //绑定交换机和队列,同时绑定路由键
    @Bean
    public Binding binding01(){
        return BindingBuilder.bind(queue01()).to(directExchange()).with(ROUTINGKEY01);
    }

    @Bean
    public Binding binding02(){
        return BindingBuilder.bind(queue02()).to(directExchange()).with(ROUTINGKEY02);
    }
}
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
/*
* 消息发送者
* */
@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(Object msg){
        log.info("发送消息"+msg);
        //指定往queue队列法msg消息
        rabbitTemplate.convertAndSend("fanoutExchange","",msg);
    }

    public void directSend01(Object msg){
        log.info("发送red消息:"+msg);
        rabbitTemplate.convertAndSend("directExchange","queue.red",msg);
    }

    public void directSend02(Object msg){
        log.info("发送green消息:"+msg);
        rabbitTemplate.convertAndSend("directExchange","queue.green",msg);
    }
}
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
/*
*
* 消息消费者
* */
@Service
@Slf4j
public class MQReceiver {
    @RabbitListener(queues = "queue")//该注解用于监听队列
    public void receive(Object msg){
        log.info("接收消息:"+msg);
    }

    @RabbitListener(queues = "queue_fanout01")
    public void receive01(Object msg){
        log.info("QUEUE01接收消息:"+msg);
    }
    @RabbitListener(queues = "queue_fanout02")
    public void receive02(Object msg){
        log.info("QUEUE02接收消息:"+msg);
    }

    //接收direct模式消息
    @RabbitListener(queues = "queue_direct01")
    public void receive03(Object msg){
        log.info("queue_direct01接收消息:"+msg);
    }
    @RabbitListener(queues = "queue_direct02")
    public void receive04(Object msg){
        log.info("queue_direct02接收消息:"+msg);
    }
}
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
    /*
     * direct模式
     * */
    @RequestMapping("/mq/direct01")
    @ResponseBody
    public void mq02(){
        mqSender.directSend01("Hello,RED");
    }

    @RequestMapping("/mq/direct02")
    @ResponseBody
    public void mq03(){
        mqSender.directSend02("Hello,GREEN");
    }

Topic模式:使用通配符*或#,*匹配一个词汇,#匹配0个或多个词汇。
以下图为例:
*.orange.*表示只要是xx.orange.yy的消息都会传入该队列,xx、yy任意一个词
*.*.rabbit表示路由键为xx.yy.rabbit
lazy.#表示路由键为lazy.xx.yy,后面可以跟任意个词也可以为空
Topic模式下,消息传递时路由键必须去匹配才能够被队列接受,否则消息就会被抛弃

项目笔记---秒杀商城_第6张图片

@Configuration
public class RabbitMQTopicConfig {

    private static final String QUEUE01 = "queue_topic01";
    private static final String QUEUE02 = "queue_topic02";
    private static final String EXCHANGE="topicExchange";
    private static final String ROUTINGKEY01 = "#.queue.#";
    private static final String ROUTINGKEY02 = "*.queue.#";

    @Bean
    public Queue queue01(){
        return new Queue(QUEUE01);
    }
    @Bean
    public Queue queue02(){
        return new Queue(QUEUE02);
    }
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(EXCHANGE);
    }
    @Bean
    public Binding binding01(){
        return BindingBuilder.bind(queue01()).to(topicExchange()).with(ROUTINGKEY01);
    }
    @Bean
    public Binding binding02(){
        return BindingBuilder.bind(queue02()).to(topicExchange()).with(ROUTINGKEY02);
    }
}
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
@Service
@Slf4j
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void topicSend01(Object msg){
        log.info("发送queue1消息:"+msg);
        rabbitTemplate.convertAndSend("topicExchange","queue.red.message",msg);
    }

    public void topicSend02(Object msg){
        log.info("发送消息被两个queue接收:"+msg);
        rabbitTemplate.convertAndSend("topicExchange","green.queue.red.message",msg);
    }
}


---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
/*
*
* 消息消费者
* */
@Service
@Slf4j
public class MQReceiver {

    //topic模式接收消息
    @RabbitListener(queues = "queue_topic01")
    public void receive05(Object msg){
        log.info("queue_direct01接收消息:"+msg);
    }
    @RabbitListener(queues = "queue_topic02")
    public void receive06(Object msg){
        log.info("queue_direct02接收消息:"+msg);
    }
}

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
    /*
    * topic模式
    * */
    @RequestMapping("/mq/topic01")
    @ResponseBody
    public void mq04(){
        mqSender.topicSend01("Hello,red");
    }

    @RequestMapping("/mq/topic02")
    @ResponseBody
    public void mq05(){
        mqSender.topicSend02("Hello,green,red");
    }

Headers模式:Headers类型的exchange使用的比较少,以至于官方文档貌似都没提到,它是忽略routingKey的一种路由方式。是使用Headers来匹配的。Headers是一个键值对,可以定义成Hashtable。发送者在发送的时候定义一些键值对,接收者也可以再绑定时候传入一些键值对,两者匹配的话,则对应的队列就可以收到消息





项目笔记---秒杀商城_第7张图片

预减库存:实现InitializingBean接口的afterPropertiesSet方法将库存加载到redis,然后通过redis进行判定扣减库存;

使用Redis实现分布式锁,
锁:当线程进入后先占位,当别的线程进入操作时发现已经有线程占位就会放弃或稍后再试,线程执行完成之后会去删除这个锁。

优化Redis操作库存
上面代码实际演示会发现Redis的库存有问题,原因在于Redis没有做到原子性。我们采用锁去解决
分布式锁:进来一个线程先占位,当别的线程进来操作时,发现已经有人占位了,就会放弃或者稍后再试线程操作执行完成后,需要调用del指令释放位子

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void textLock01() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //占位,setIfAbsent()方法表示如果key不存在才可以设置成功
        Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
        //如果占位成功进行正常操作
        if (isLock){
            valueOperations.set("name","xxxx");
            String name = (String) valueOperations.get("name");
            System.out.println("name="+name);
            Integer.parseInt("xxxx");
            //操作成功删除锁
            redisTemplate.delete("k1");
        }else {
            System.out.println("有线程在使用,请稍后");
        }

    }--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
//为了防止业务执行过程中抛异常或者挂机导致del指定没法调用形成死锁,可以添加超时时间
    /*
    * 解决死锁方式:当出现异常时就可能导致锁无法删除,从而导致死锁
    * 通过设定锁的时间,使得当执行出现异常时锁可以进行销毁
    * 设定
    * */
    @Test
    public void textLock02() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //设定锁的时间为5秒
        Boolean isLock = valueOperations.setIfAbsent("k1", "v1", 5, TimeUnit.SECONDS);
        if (isLock){
            valueOperations.set("name","xxxx");
            String name = (String) valueOperations.get("name");
            Integer.parseInt("xxxx");//出现异常
            //操作成功删除锁
            redisTemplate.delete("k1");
        }else {
            System.out.println("有线程在使用,请稍后");
        }
    }

上面例子,如果业务非常耗时会紊乱。举例:第一个线程首先获得锁,然后执行业务代码,但是业务代码耗时8秒,这样会在第一个线程的任务还未执行成功锁就会被释放,这时第二个线程会获取到锁开始执行,在第二个线程开执行了3秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,他释放的第二个现成的锁,释放之后,第三个线程进来。

解决方案:

  • 尽量避免在获取锁之后,执行耗时操作
  • 将锁的value设置为一个随机字符串,每次释放锁的时候,都去比较随机字符串是否一致,如果一致,再去释放,否则不释放
  • 释放锁时要去查看所得value,比较value是否正确,释放锁总共三个步骤,这三个步骤不具备原子性。


Lua脚本
Lua脚本优势:
1)使用方便,Redis内置了对Lua脚本的支持
2)Lua脚本可以在Rdis服务端原子的执行多个Redis命令
3)由于网络在很大程度上会影响到Redis性能,使用Lua脚本可以让多个命令一次执行,可以有效解决网络给Redis带的性能问题
使用Lua脚本思路:
1)提前在Redis服务端写好Lua脚本,然后在java客户端去调用脚本
2)可以在java客户端写Lua脚本,写好之后,去执行。需要执行时,每次将脚本发送到Redis上去执行
3)创建Lua脚本(放在resources目录下)

Lua脚本

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
    @Test
    public void textLock03() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String value = UUID.randomUUID().toString();
        Boolean isLock = valueOperations.setIfAbsent("k1", value, 120, TimeUnit.SECONDS);
        if (isLock){
            valueOperations.set("name","xxxx");
            String name = (String) valueOperations.get("name");
            System.out.println("name="+name);
            System.out.println(valueOperations.get("k1"));
            //执行lua脚本
            Boolean result = (Boolean) redisTemplate.execute(script, Collections.singletonList("k1"), value);
            System.out.println(result);
        }else {
            System.out.println("有线程在使用,请稍后");
        }
    }


安全优化:
(1)隐藏接口地址,当秒杀开始时,不会直接调用秒杀的接口,而是获取真正秒杀接口的地址,所获取的秒杀地址是根据不同用户秒杀不同的商品所生成的唯一地址,再根据这个地址进行秒杀。
(2)验证码防护:使用验证码过滤脚本秒杀,使用复杂的验证码进行验证,如数学公式成语等,通过验证码可以有效减轻服务器压力
(3)接口防刷:使用计数器算法进行限流,在一定的时间内请求一定的次数,到达这个时间就重置计数器,没到达时间就进行添加;在一定时间内达到计数器阈值时就会限制访问。缺点:资源浪费


总结:


项目框架搭建:1、Springboot环境搭建;2、集成Thymeleaf,RespBean;3、Mybatis
分布式会话:
1、用户登录:设计数据库,明文密码二次加密,参数校验+全区异常处理
2、共享session:使用redis缓存将用户数据存入redis,使得信息得以共享,解决分布式会话问题
功能开发:
1、商品列表
2、商品详情
3、秒杀
4、订单详情
系统压测:使用JMeter对项目进行压测,通过自定义变量模拟多个用户,利用模拟的用户对商品进行秒杀压测,主要压测两部分:商品列表的访问,秒杀功能
优化:
1、页面缓存+URL缓存+对象缓存
2、静态资源优化
接口优化:
1、redis预减库存,减少数据库的访问
2、内存标记减少redis的访问
3、RabbitMQ异步下单,将秒杀先存入队列,在利用队列里的秒杀请求进行判定,通过判定的请求会实现数据库更新以及下单(SpringBoot整合RabbitMQ+交换机)
安全优化:
1、秒杀接口的隐藏
2、算术验证码

你可能感兴趣的:(面试整理,java,开发语言)