高并发项目(其二)

RabbitMQ软件

一个流行的消息中间件,实现高级队列协议,为分布式应用程序提供可靠,异步消息传递机制,可以在多个进程,多个主机间传递消息,核心概念:生产者,消费者和队列

生产者:将消息发布到队列

消费者:从队列获取消息并处理

PS:这个软件安装在Linux而且需要Erlang语言环境,再安装RabiitMQ

配置RabbitMQ

配置防火墙,开放15627端口

firewall-cmd --zone=public --add-port=15672/tcp --permanent

//重启防火墙
firewall-cmd --reload

修改RabbitMQ登录配置

cd /etc/rabbitmq/        //mq安装目录

vi rabbitmq.config        //修改配置

加入 [{rabbit,[{loopback_users,[]}]}].

/sbin/service rabbittmq-server stop        //重启rabbit
/sbin/service rabbittmq-server start

进入RabbitMQ网页

192.168.198.135:15672

账号密码均为guest

现在在SpringBoot中集成RabbitMQ

P      ||||||       C    


    P:消息发送者

    C:消息接收者

    中间表示队列

pom.xml引入rabbitMQ需要的依赖

org.springframework.boot
spring-boot-starter-amqp

在application.yml配置

rabbitmq:
  host:192.168.198.135        //ip地址
  username: guest
  password: guest
  virtual-host: /            //要操作的虚拟主机
  port: 5672                //端口 15672是RabbitMQ网页的端口

  listener:
    simple:    
     concurrency: 10                 //消费者最小数量

     max-concurrency:10              //     最大

     perfetch: 1                     //限制消费者每次只能处理一条消息,处理完才能继续下一条

     auto-startup:true               //默认启动容器

     default-requeue-rejected: true    //消息被拒绝后是否重新进入队列

     template:
       retry:
         enabled:true                  //消息处理失败是否重新尝试默认false
         initial-interval: 1000ms      //初始化重试时间间隔,即第一次处理消息失败后1s后重试
         max-attempts: 3                //最多重试3次
         max-interval: 10000ms          //重试最大时间间隔10s
         motiplier: 1                   //重试时间间隔乘数2,第一次等待1s,第二次就是1*2s

创建RabbitMQConfig类(可以创建队列,交换机)

@Configuration
public class RabbitMQConfig{

  private static final String QUEUE = "queue";     //定义队列名

  @Bean
  public Queue queue(){                        //创建队列

    return new Queue(QUEUE,true);    //true表示队列持久化

  }

}
简单模拟下

创建消息生产者

@Service
public class MQSender{
    
    @Resource
    private RabbitTemplate rabbitTemplate;    //装配它操作RabbitMQ

    //方法:发送消息
    public void send(Object msg){

        rabbitTemplate.convertAndSend("queue",msg);    //将消息加入队列

    }

}

创建消息消费者

@Service
public class MQReceiver{

@Resource
    private RabbitTemplate rabbitTemplate;

    //方法:接收消息
    @RabbitListener(queues="queue")      //监听那些队列
    public void receive(Object msg){

        log.info("接收消息",msg);
    
    }    

}

控制层控制

@Controller
RabbitMQHandler{

  @Resource
  private MQSender mqSender;


  @RequestMapping("/mq")
  @ResoponseBody
  public void mq(){
    
     mqSender.send("hello");

  }
}

RabbitMQ使用模式
Fanout模式

广播模式,就是把交换机(Exchange)里消息发送给所有绑定该交换机的队列,忽略路由

                                    消费者

生产者        交换机              
    
                                    消费者

例:生产者将消息发送到交换机,交换机发送到两个绑定了他的队列

修改RabbitMQConfig

RabbitMQConfig{

     private static final String QUEUE1 = "queue1";        //两个队列名称
     private static final String QUEUE2 = "queue2";
     private static final String EXCHANGE = "exchange";    //交换机名称

     @Bean
     public Queue queue1(){                        

        return new Queue(QUEUE1,true);   

     }
 
     @Bean
     public Queue queue2(){                        

        return new Queue(QUEUE2,true);    

     }
     
     @Bean
     public FanoutExchange exchange(){                //创建交换机

        return new FanoutExchange(EXCHANGE);
     }

     //然后将两个队列绑定到交换机
    @Bean
    public Binding binding01(){

      return BindingBuilder.bind(queue1()).to(EXCHANGE);

    }

    @Bean
    public Binding binding02(){

      return BindingBuilder.bind(queue2()).to(EXCHANGE);

    }

}

修改MQSender

MQSender{
    
    public void sendFanout(Object msg){

        rabbitTemplate.convertAndSend("exchange","",msg)    //消息传到交换机
                                                              空格是忽略路由


    }
}

修改MQRecevier

MQRecevier{

    @RabbitListener(queues="queue1")      //监听队列1
    public void receive(Object msg){

        log.info("接收消息",msg);
    
    }    

    @RabbitListener(queues="queue2")      //监听队列2
    public void receive(Object msg){

        log.info("接收消息",msg);
    
    }    
    


}

修改Controller

RabbitMQHandler{

  @RequestMapping("/mq/findout")
  @ResoponseBody
  public void findout(){            //现在就是发送到交换机
    
     mqSender.send("hello");

  }
    

}
Direct模式

路由模式,在使用交换机的同时,生产者指定路由发送数据,消费者绑定路由接收数据。生产者向交换机发送数据时,会声明发送给交换机下那个路由,只有当消费者队列绑定了交换机且声明了路由,才会收到数据

                               |||||    queue3   queue.red路由

P         X    info/error/     |||||    queue4   queue.green路由

生产者                           队列
        交换机                           消费者
                这就是路由

例:交换机在发送消息时指定不同路由发送到不同消费者

修改RabbitMQConfig

private static final String QUEUE3 = "queue3";        //两个队列名称
private static final String QUEUE4 = "queue4";
private static final String EXCHANGE_DIRECT = "exchange_direct";    //交换机名称

private static final String ROUNTING1 = "queue.red";        //定义路由,内容自己写
private static final String ROUNTING2 = "queue.green";

...
return new Queue(QUEUE3,true);  //同样创建两个队列
return new Queue(QUEUE4,true);  
return BindingBuilder.bind(queue3()).to(EXCHANGE_DIRECT).wiht(ROUNTING1); //关联路由
return BindingBuilder.bind(queue4()).to(EXCHANGE_DIRECT).with(ROUNTING2);

 修改MQSender

MQSender{
 
    //方法:发送到交换机并指定路由
    public void sendDirect1(Object msg){

      rabbitTemplate.convertAndSend("exchange","queue.red",msg);    //此时就指定路由

    }

    public void sendDirect2(Object msg){

      rabbitTemplate.convertAndSend("exchange","queue.green",msg);    //此时就指定路由

    }


}
MQRecevier{

    @RabbitListener(queues="queue3")      //监听队列1
    public void queue_direct1(Object msg){

        log.info("接收消息",msg);
    
    }    


     @RabbitListener(queues="queue4")      //监听队列1
    public void queue_direct2(Object msg){

        log.info("接收消息",msg);
    
    }  

}

 修改RabbitMQHandler

@RequestMapping("/mq/direct03")
  @ResoponseBody
  public void direct03(){            //现在就是发送到交换机
    
     mqSender.sendDirect1("hello03");

  }

@RequestMapping("/mq/direct04")
  @ResoponseBody
  public void direct04(){            //现在就是发送到交换机
    
     mqSender.sendDirect2("hello04");

  }
Topic模式

direct模式可能会造成路由RoutingKey太多,实际上往往按某个规则进行路由匹配,Topic就是direct模式的一种拓展/叠加,模糊的路由匹配模式

*:可以(只能)匹配一个单词

#:可以匹配多个单词(或零个)

例如
              *.orange.*          Q1            C1
    
P      X  
        
             *.*>rabbit           Q2            C2
    
      
        来个quick.orange.rabbit--HEllo    就会同时发送到Q1,Q2

            lazy.orange.elephant          就只会发送到C1

            

            lazy.orange.elephant能匹配lazy.#

例:发送red只有Q1接收到,发送greenQ1,Q2都能接收到

新建RabbitMQTopicConfig(因为之前那个代码太多)

private static final String QUEUE1 = "queue_topic1";        //两个队列名称
private static final String QUEUE2 = "queue_topic2";
private static final String EXCHANGE = "TopicExchanget";    //交换机名称

private static final String ROUNTING1 = "#.queue.#";        //定义路由,内容自己写
private static final String ROUNTING2 = "*.queue.#";

...
return new Queue(QUEUE1,true);  //同样创建两个队列
return new Queue(QUEUE2,true);  

return BindingBuilder.bind(queue_topic1).to(EXCHANGE).wiht(ROUNTING1); //关联路由
return BindingBuilder.bind(queue_topic2).to(EXCHANGE).with(ROUNTING2);

修改MQSender

 public void sendTotic3(Object msg){

      rabbitTemplate.convertAndSend
                ("topicExchange","queue.red.message",msg);    //此时就指定路由
                                                                只能匹配到Q1

    }

 public void sendTopic4(Object msg){

      rabbitTemplate.convertAndSend
                ("topicExchange","green.queue.green.message",msg);    //此时就指定路由
                                                                        匹配到Q1,Q2

    }

修改MQReceiver

@RabbitListener(queues="queue_topic1")      //监听队列1
    public void queue_topic1(Object msg){

        log.info("接收消息",msg);
    
    }

@RabbitListener(queues="queue_topic2")      //监听队列1
    public void queue_topic2(Object msg){

        log.info("接收消息",msg);
    
    }

修改MQHandler

@RequestMapping("/mq/topic1")
  @ResoponseBody
  public void topic1(){            //现在就是发送到交换机
    
     mqSender.sendTopic3("hello red");

  }

@RequestMapping("/mq/topic2")
  @ResoponseBody
  public void topic2(){            //现在就是发送到交换机
    
     mqSender.sendTopic4("hello green");

  }
Headers模式

使用较少,比较少见且复杂,不关心路由key是否匹配,只关心header的key-value对是否匹配

现在解决之前的问题

  当秒杀系统开始时候,如果一大堆线程都来请求,那么对数据库压力很大,故需要Redis分担,在过滤环节就预减库存,这样调用seckill方法的线程变少了,对数据库解压

修改SeckillController

public class SeckillController implements InitilaizingBean{
    
    ...
    //该方法是在SeckillController类所有属性都初始化后自动执行,就可以将秒杀商品数量加载到Redis
    @Override
    public void afterPropertiesSet() throws Exception{
        
        //查询所有的秒杀商品
        List<GoodsVo> list = goodsService.findGoodsVo();
        //遍历List,将秒杀商品库存量放到Redis
        if(CollectionUtils.isEmpty(list)){    //判断是否为空
                return;
        }
        list.forEach(goodsVo -> {

            //秒杀商品库存量对应key:seckillGoods:商品id
            redisTemplate.opsForValue().
                set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());

        });
    }
}

继而修改SeckillController的doSeckill方法

doSeckill(...){
    ...

    //库存预减,如果在Redis中预减库存,发现商品没了,就直接返回
    // 从而减少执行orderService.seckill()请求,防止线程堆积

  //derement具有原子性,当执行decrement方法是一个一个进行的不是一下冲进许多线程
   Long decrement = redisTemplate.opsForValue().
                        decrement("seckillGoods:"+goodsId);
    
    if(derement < 0){        //说明商品没库存了

        redisTemplate.opsForValue.
                increment("seckillGoods:"+goodsId);//恢复看起舒服

        return "secKillFail";    //返回错误页面    
    }

}

继续优化

在预减Redis库存时,可以判断库存量是否为0.是则不再减1,免得0和-1一直循环浪费内存,就是直接在本机jvm操作了,快于在Redis操作

SeckillController{

//定义map记录秒杀商品是否还有库存
private HashMap<Long,Boolean> entryStockMap = new HashMap();

@Override
    public void afterPropertiesSet() throws Exception{
        

        List<GoodsVo> list = goodsService.findGoodsVo();
    
        if(CollectionUtils.isEmpty(list)){    
                return;
        }
        list.forEach(goodsVo -> {
            redisTemplate.opsForValue().
                set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());

            //初始化map,false表示还有库存,true表示没有库存
            entryStockMap.put(goodsVo.getId(),false);

        });
    }

   doSeckill(...){
    ...
        //对Map判断,如果已经无了,直接返回,无需再Redis预减
        if(entryStockMap.get(goodsId)){
            return "secKillFail";        //返回错误信息
        }

   Long decrement = redisTemplate.opsForValue().
                        decrement("seckillGoods:"+goodsId);
    
    if(derement < 0){        

        //这里表示秒杀商品数量已经无了
        entryStockMap.put(goodsId,true);
        
        

        redisTemplate.opsForValue.
                increment("seckillGoods:"+goodsId);

        return "secKillFail";      
    }

}
}
加入消息队列,实现秒杀的异步请求

前面秒杀,没有实现异步机制,是完成下订单后再返回,当有大并发请求下订单操作时,数据库来不及响应,容易造成线程堆积,可通过消息队列实现秒杀异步请求

新建一个秒杀消息SeckillMessage

//秒杀消息对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage{

    private User user;
    private Long goodsId;

}

创建RabbitMQSeckillConfig

@Configuration
public class RabbitMQSeckillConfig{

    //定义消息队列和交换机名
    private static final String QUEUE = "seckillQueue";
    private static final String EXCHANGE = "seckillExchange";

    //创建队列
    @Bean
    public Queue queue_skill(){

        return new Queue(QUEUE);    

    }
    
    //创建交换机
    @Bean
    public TopicExchange topicExchange_seckill(){

        return new TopicExchange(EXCHANGE);
    }
    
    //将队列绑定到交换机,并指定路由
    @Bean
    public Binding bingding_seckill(){
    
        return BingdingBuilder.bind(queue_seckill()).
                to(topicExchange_seckill)).with("seckill.#");
    }


}

创建消息消费者和消息生产者

MQSenderMessage

@Service
MQSenderMessage{
    
    @Resource
    private rabbitTemplate;

    //方法:发送秒杀消息
    public void sendSeckillMessage(String message){
    
        rabbitTemplate.convertAndSend
            ("seckillExchange","seckill.message",message);
    
    }

}

MQReceiverMessage

@Service
MQReceiverMessage{

    @Resoucre
    goodsService;
    orderService;

    //接收消息,并完成下单
    @RabbitListener(queues="seckillQueue")
    public void queue(String message){
    
        //这里从队列取出的是String,但需要SeckillMessage(获取参数来秒杀)
    
        JSONUtil.toBean(message,SeckillMessage.class).var;

        User user = seckillMessage.getUser();            //得到用户

        Long goodsId = seckillMessage.getGoodsId();        //秒杀的商品id
    
        goodService.findGoodsVoByGoodsId(goodsId);        //通过商品id得GoodsVo

        //进行下单操作
        orderService.seckill(user,goodsVo);
        

    }

}

再次修改SeckillController

@Resource
MQSenderMessage;        //装配消息生产者

doSeckill{


    //抢购,向消息队列发送秒杀请求,实现秒杀异步请求
    //我们发送描述消息后立即返回结果(临时)如:排队中

    new SeckillMessage(user,goodid).var
    
    将seckillMessage转为字符串发送出去
    mqSenderMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));
    
    model.addAttribute("errmsg","排队中");
    return "seckillFail";        
    

}

前端再根据后端返回信息(有兴趣可以写,毕竟前端)

秒杀安全

前面我们处理高并发,都是按照正常程序逻辑处理的,即用户正常抢购,还要考虑抢购安全性,当前抢购接口是固定的,如果泄露会有安全隐患,比如还未开启抢购或结束有人用脚本发起抢购

故要隐藏抢购接口

用户抢购时候先生成唯一一个抢购路径,返回给客户端,客户端抢购时会携带这个抢购路径,服务端做校验,成功才继续走下一步,否则直接返回

先在ResponseBeanEnum新增错误信息

REQUEST_ILLEGAL(500502,"请求非法"),
SESSION_ERROR(500503,"用户信息有误")
SEK_KILL_WAIT(500504,"排除中");

在OrderService中加入方法

interface {

    //方法:生成秒杀路径(唯一)
    String createPat(User user,Long goodsId);

    //方法:对秒杀路径进行校验
    boolean cherckPath(User user,Long goodsId,String path);


}

在OrderServiceImpl实现


    String createPat(User user,Long goodsId){

       String path =  MD5Util.md5(UUIDUtil.uuid());    //生成一个随机唯一路径

       RedisTemplate.opsForValue().set
            ("seckillPath:"+user.getId()
            +":"+goodsId,path,60.TimeUnitSECONDS);    //保存到Redis,并设置过期时间60s

       return Path;

    }




    boolean cherckPath(User user,Long goodsId,String path){

        if(user != null || goodId <0 || StringUtils.hasText(path)){    //校验
                    return false;
        }
    
         //取出用户路径再校验
   
       String redisPath = (String) redisTemplate.opsForValue().       
            get("seckillPath:"+user.getId()
                        +":"+goodsId,path);

        return path.equals(redisPath);
        
    }

@RequestMaping("/path")
@ResponseBody
public RespBean getPath(User user,LOng goodsId){            //获取秒杀路径
    
    if(user == null || goodId <0 ){    //校验
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
    }
    String path = orderService.createPath(user,goodsId);
    return RespBean.success(path);

 }

修改SeckillController的doSeckill方法

@RequestMapping("/{path}/doSeckill")        //直接带入path进行验证
@ResponseBody
public RespBean doSeckill(User user,Long goodsId,@PathVariable String path){

    if(user == null){
        reutrn RespBean.error(RespBeanEnum.SESSION_ERROR);   
    }

       boolean b = orderService.checkPath(user,goodsId,path);  //这里校验路径

       if(!b){            //校验失败
            return RespBean.error(RespBeanEnum.REQUST_ILLEGAL);
        }
   

    ...//将之前的返回页面 改为 返回错误信息(RespoBeanEnum)


    
 }

改进前端页面

验证码防脚本攻击

在一些抢购活动中,可以通过验证码的方式,防止脚本攻击,如12306

使用验证码happy Captcha

网站: https://gitee.com/ramostear/Happy-Captcha

 验证码的代码

在SeckillController中加入

@RequestMapping("/captcha")
public void happyCaptcha(request,response,user){

    ...//生成验证码的代码,官网有

   //ps:该验证码默认存到session中了,key是happy-captcha

   
    redisTemplate.opsForValue().set("captcha:"+user.getId()+":"+goodsId,
        (String) request.getSession().getAttribute("happy-captcha"),30,Tim..);

    //从session中取出验证码放入到Redis


}

在OrderService加入验证方法

boolean checkCaptcha(User user,Long goodsId,String captcha);

在impl中实现

boolean checkCaptcha(User user,Long goodsId,String captcha){

    if(user == null || goodsId <0 || !StringUtils.hasText(captcha)){
    
            return false;    
    }

    //从Redis取出验证码
    String redisCaptcha= (String) redisTemplate.opsForValue().
                        get("captcha:"+user.getId()+":"+goodsId);

    return captcha.equals(redisCaptch);

}

//因为在获取秒杀路径(上面讲的)的时候就是要秒杀了,所以可以将验证码方法加在那儿

在SeckillController中

getPath(...String captcha){

    if(user == null || goodsId <0){
    
                ...
    }

    //增加一个业务逻辑-校验用户输入的验证码是否正确
    boolean check = orderService.checkCaptcha(user,goodsId,captcha);

     if(!check){    //验证码校验失败
        return RespBean.error(RespBeanEnum.CAPTCHA_ERROR);
    }

    //验证码验证成功则继续往下走验证秒杀路径
    String path = ...       


}

改进前端页面

秒杀接口限流-防刷

即一直点击 抢购 提示信息"访问过于频繁,请重新访问"

思路分析

因为秒杀时要先调用秒杀路径(getPath),所以对此方法限流即可

修改SeckillController的getPath方法

getPath(...){

    //增加Redis计数器,完成对用户的限流
    如 5s 内访问次数超过5次,则是刷接口

    String uri = request.getRequestURI(); //这里就是localhost:8080/sekill/path的/path

    ValueOperation valueOperations = redisTemplate.opsForValue();

    Integer count = (Integer) valueOperations.get(uri+":"+user.getId());

    if(count == null){    //说明没有这个key

        //初始化key且值为1,过期时间5秒
        valueOperations.set(uri+":"+user.getId(),1,5,TimeUnit...);  

    }else if(count < 5){
    
        valueOperations.increment(uri+":"+user.getId());    //有且小于5就加1

    }else{

        return RepBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);    //刷接口就报错

    }

}

但这限流通用性不强,得改进,可将改方法封装成注解,哪里需要就来个注解

自定义注解@AccessLimit

@Retention(RetentionPolicy.RUNTIME)    //这个注解指定了被修饰的注解在运行时保留
@Target(ElementType.METHOD)            //这个注解指定了被修饰的注解可以应用在方法上
public @interface AccessLimit{
        
    int second();                        //时间范围
    int maxCount();                     //访问最大值
    boolean needLogin() default true    //是否登录

}

然后可放到getPath()使用

@RequestMapping("/path")
@AccessLimit(second = 5,maxCount = 5,needLogin = true)
getPath(...){}

//使用注解防刷可提高通用性和灵活性

单光这样只是单纯一个注解,本身没有任何作用

新增UserContext类似工具类

UserContext{

    //每个线程都有自己的ThreadLocal,把共享数据放到这里,保证线程安全
    private static ThreadLocal<User> userHolder = new ThreadLocal();

    pulic static void setUser(User user){

        userHolder.set(user);
    }

    pulic static User getUser(){

        return userHolder.get();
    }

}

最后还要个自定义拦截器AccessLimitInterceptor

@Compoent
AccessLimitInterceptor  implements HandlerInterceptor {

    //先装配组件
    userService,redisTemplate

    //这方法要得到User对象并放入到ThreadLocal,去处理@Accesslimit

    @Override
    public boolean preHandle(request,response){
        if(handler instanceof HandlerMethod){
            User user = getUser(request,response);
            UserContext.setUser(user);        //存入到Threadlocal,上面写了

            //现在处理@Accesslimit
            HandlerMethod hm = (HandlerMethod) handler; //handler转为HandlerMethod

            AccessLimit accessLimit =
               hm.getMethodAnnotation(AccessLimit.class); //获取到目标方法的注解

            if(accessLimit == null){    //如果目标方法没有@A..说明该接口没处理限流
                return true;
            }
            
            int second =  accessLimit.second();                //获取注解的值
            int maxCount = accessLimit.maxCount();
            boolean needLogin = accessLimit.needLogin();

            if(needLogin){        //说明用户必须得登录才能访问目标方法
                if(user == null){
                    return false;    //流程走到此不走了
                }                  
            }

            //这里就是之前getPath里的方法了
    
            String uri = request.getRequestURI(); 
            //这里就是localhost:8080/sekill/path的/path
            
            String key = uri+":"+userId();

            ValueOperation valueOperations = redisTemplate.opsForValue();

            Integer count = (Integer) valueOperations.get(key);

            if(count == null){    //说明没有这个key

            //初始化key且值为1,过期时间5秒
            valueOperations.set(key,1,second,TimeUnit...);  

            }else if(count < maxCount){
    
            valueOperations.increment(uri+":"+user.getId());    //有且小于5就加1

            }else{

             return false    //刷接口就报错

            } 


        }

    }
    
        //单独写方法得到user对象的userTicket(此东西放在Cookie),方便上面用
        private User getUser(request,response){
          String ticket = CookieUtil.getCookieValue(request,"userTicket");
        
            if(!StringUtils.hasText(ticket)){    //说明没有用户登录直接返回null
                    return null;
            }
            return userService.getUserByCookie(ticket,request,response);

        }

}

装配到Webconfig

WebConfig{

    @Resource
    private AccessLimitInterceptor accessLimitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){

        registry/addInterceptor(accessLimitInterceptor);    //注册拦截器才生效
    }

}

Redis分布式锁(Redis章节讲过)

本项目的Redis的decrement方法具有原子性和隔离性并且Mysql的update方法也具有锁行(一个一个执行)功能,所以有效控制抢购不会超卖,但如果项目复杂;假如需要进行Redis的set操作甚至修改DB,文件那么这时候就需要Redis分布式锁扩大隔离性范围

假如没有decrement方法

SeckillController{

    //得到一个uuid,作为锁的值
    String uuid = UUID.randomUUID().toString();
    boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,3,Time..);

    if(lock){    //获取锁成功
    
        //进行减1的业务操作
        Long decrement = redisTemplate.opsForValue().decrement(...);
        ...
        
        //释放锁redis+lua脚本(下面有写)
        redistTemplate.execute(redisScript,Arrays.asList("lock"),uuid);

    } else{        //获取失败
    
        model.addAttribute("errmsg",RespBeanEnum.SEC_KILL_RETRY.getMessage());

        return "secKillFail";        //返回错误页面

    }

}    

lock.lua脚本文件

if redis.call('get',KEYS[1]) == ARGV[1] then    //比较传进来的UUID是否一致
 return redis.call('del',KEYS[1])
else return 0
end

RedisConfig

//增加执行脚本
@Bean
public DefaultRedisScript<long> script(){


    DefaultRedisScript<long> redisScript = new DefaultRedisScript();

    //设置要执行的lua脚本位置,把lock.lua文件放在resources目录下
    redisScript.setLocation(new ClassPathResource("lock.lua"));
    redisScript.setResultType(Long.class);
    return redisScript;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值