干货满满的redis实现分布式锁(过年还是在家学习比较安全)

先貼上 测试用的controller service代码

controller:

 ```java       
 @RestController
 @RequestMapping("/skill")
 @Slf4j
 public class SecKillController {

    @Autowired
    private SecKillService secKillService;

    /**
     * 查询秒杀活动特价商品的信息
     * @param productId
     * @return
     */
    @GetMapping("/query/{productId}")
    public String query(@PathVariable String productId)throws Exception
    {
        return secKillService.querySecKillProductInfo(productId);
    }


    /**
     * 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
     * @param productId
     * @return
     * @throws Exception
     */
    @GetMapping("/order/{productId}")
    public String skill(@PathVariable String productId)throws Exception
    {
        log.info("@skill request, productId:" + productId);
        secKillService.orderProductMockDiffUser(productId);
        return secKillService.querySecKillProductInfo(productId);
    }
 }

serviceimpl:

    ```java
    @Service
    public class SecKillServiceImpl implements SecKillService {
        /*超时时间 10s*/
        private static final int TIMEOUT = 10 * 1000; 



        /**
         * 国庆活动,皮蛋粥特价,限量100000份
         */
        static Map<String,Integer> products;
        static Map<String,Integer> stock;
        static Map<String,String> orders;
        static
        {
            /**
             * 模拟多个表,商品信息表,库存表,秒杀成功订单表
             */
            products = new HashMap<>();
            stock = new HashMap<>();
            orders = new HashMap<>();
            products.put("123456", 100000);
            stock.put("123456", 100000);
        }

        private String queryMap(String productId)
        {
            return "国庆活动,皮蛋粥特价,限量份"
                    + products.get(productId)
                    +" 还剩:" + stock.get(productId)+" 份"
                    +" 该商品成功下单用户数目:"
                    +  orders.size() +" 人" ;
        }

        @Override
        public String querySecKillProductInfo(String productId)
        {
            return this.queryMap(productId);
        }

        @Override
        public void orderProductMockDiffUser(String productId)
        {
            /*加锁*/

            /*1.查询该商品库存,为0则活动结束。*/
            int stockNum = stock.get(productId);
            if(stockNum == 0) {
                throw new SellException(100,"活动结束");
            }else {
                /*2.下单(模拟不同用户openid不同)*/
                orders.put(KeyUtil.genUniqueKey(),productId);
                /*3.减库存*/
                stockNum =stockNum-1;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stock.put(productId,stockNum);
            }

            /*解锁*/

        }
    }


使用ab压测过后(100次请求 50个线程同时进行)明显数据已经不对了

多线程的问题一般会想到使用 synchronized 关键字

测试之后发现synchronized 确实能解决现有问题但是也有缺点
1.他会使方法编程单线程的 一次只有一个线程能访问 大大的降低了效率 但是同时也保持了准确度 可以说勉强是一种解决办法但是不是最优的
2.控制的不够细腻 比如上面的方法 需要传订单号 假如我有两个商品 s1和s2 s1抢的人多 s2抢的人少 但是他们都是用这一个下单接口 一次只有一个人能访问 就会导致非常不效率
3.只能单点控制 加入我把这个抢单接口做了集群 通过负载均衡过后 还是会出现之前的情况
这三个缺点说明synchronized关键字并不能完美解决问题接下来就引出用redis实现分布式锁

 ```java    
 package com.imooc.sell.service;

 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;

 @Component
 @Slf4j
 public class RedisLock {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     *
     * @param key(商品id)
     * @param value(当前时间加上超时时间)
     * @return
     */
    public   boolean lock(String key ,String value){


        /*
         redis setnx命令
         如果返回true 说明没有加锁 可以正常运行 返回false相反
         */
        if(stringRedisTemplate.opsForValue().setIfAbsent(key, value)){
            return true;
       }else{
            /*
            第一个if主要防止后面逻辑代码报错 导致没有正常解锁
            运用超时时间控制 如果锁的时间超过了当前时间说明锁已经超时了
            锁超时之后允许覆盖旧锁
             */
            String nowTime = stringRedisTemplate.opsForValue().get(key);
            if(!StringUtils.isEmpty(nowTime)&& Long.parseLong(nowTime)<System.currentTimeMillis()){

                /*
                第二个if防止有两个线程同时检测到锁超时 而且两个value相同
                但是执行getset总有个先后顺序
                拿到旧锁的时间与上面获取的旧锁时间一致说明是先获取到旧锁的一个
                第二个人再次执行getset之后 获取到的旧时间是上一个线程的value
                与之前获取的时间不同所以就不会成功
                 */
                String oldTime = stringRedisTemplate.opsForValue().getAndSet(key, value);
                if (!StringUtils.isEmpty(oldTime)&&oldTime.equals(nowTime)){
                    return true;
                }
            }
            return false;
        }
    }

    public void unlock(String key,String value){
        try {
            String currentTime = stringRedisTemplate.opsForValue().get(key);
            if (!StringUtils.isEmpty(currentTime)&&currentTime.equals(value)){
                stringRedisTemplate.opsForValue().getOperations().delete(key);
            }
        }catch (Exception e){
            log.error("【redis分布式锁】解锁异常, {}", e);
        }

    }

 } 
加上redis锁之后使用ab压测测试 测试结果如下     
![](cg.jpg)
可以看到和之前的完全不一样 不会出现超卖的情况        

代码注释写的非常清楚了低下就不再写了     
上面使用synchronized关键字的遇到的问题也都完美解决了     
1.效率问题,当没有拿到锁的时候可以返回常用的抢单提示语,及时响应不会出现卡在那不懂的情况,用户体验提升效率提升。     
2.控制细腻的问题  redis存储以商品id为key,每个商品一把锁,完美解决a商品抢的人多b商品抢的人少的情况。      
3.单点问题,使用redis锁 不管业务代码做不做集群,都是从一个redis或者redis集群中取锁,这样就也完美处理了synchronized关键字只能处理单点的问题






转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。邮件至 wcfinyourheart@163.com

文章标题:干货满满的redis实现分布式锁(过年还是在家学习比较安全)

本文作者:wcf

发布时间:2020-01-26, 10:05:02

最后更新:2020-02-05, 10:14:34

原始链接:http://1007638786.github.io/2020/01/26/%E5%B9%B2%E8%B4%A7%E6%BB%A1%E6%BB%A1%E7%9A%84redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%88%E8%BF%87%E5%B9%B4%E8%BF%98%E6%98%AF%E5%9C%A8%E5%AE%B6%E5%AD%A6%E4%B9%A0%E6%AF%94%E8%BE%83%E5%AE%89%E5%85%A8%EF%BC%89/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏