基于Redis分布式锁的实现

uk8692 2019-11-03

首先介绍下在高并发的情况下会给我们的缓存造成什么样的问题?

1、缓存击穿:在高并发的情况下某一个热点key失效,就会造成大量的请求去访问我们的数据,比如秒杀某一商品的时候。
2、缓存雪崩:是指某一个时刻大量的key过期,解决可以通过设置不同的过期时间解决。
3、缓存穿透:利用缓存中没有就去查询数据库的这一特点恶意的进行攻击,可以通过当我们查询数据库发现数据库中没有数据时可以设置一个key为该key,但是值为空字符串或者null的值并且设置短暂的过期时间。

解决缓存穿透:这里需要配置缓存击穿的分布式锁解决

if (pmsSkuInfo != null) {
    /*4.mysql将想要查询的结果返回给redis*/
    System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "mysql将数据结果返回给redis缓
    存中");
    jedis.set("sku:" + skuId + ":info", JSON.toJSONString(pmsSkuInfo));
} else {
    /*数据库中不存在该sku*/
    /*为了防止缓存穿透(大量请求访问数据库mysql),将null值或者空串给
    redis , 并且给null值或者空串设置3分钟的自动销毁*/
    jedis.setex("sku:" + skuId + ":info", 60 * 3, JSON.toJSONString(""));
}

这里主要介绍缓存击穿的解决方案(基于Redis的分布式锁的实现)。

Redis的分布式锁需要解决三个问题:
A、    设置锁和设置锁超时必须要原子性操作。通过setex命令。
B、    解决误删锁问题:通过value(设置一个唯一的token)判断是不是自己的锁。
C、    如果通过判断是否是自己所拥有的锁又要解决判断和删除锁是两个独立操作,不是原子性(可以使用lua脚本)。
@Override
    public PmsSkuInfo getSkuById(String skuId, String ip) {
        System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "进入了商品详情的请求");
        PmsSkuInfo pmsSkuInfo = new PmsSkuInfo();
        /*1.连接缓存*/
        Jedis jedis = redisUtil.getJedis();
        /*2.查询缓存*/
        String skuKey = "sku:" + skuId + ":info";
        String skuJson = jedis.get(skuKey);
        /*StringUtils.isEmpty(skuJson)*/
        if (skuJson != null && !skuJson.equals("")) {
            /*把json转化为java对象类  , 但是skuJson不能为空*/
            pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);
            System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "从redis缓存中获取商品详情");
        } else {
            /*3.如果缓存中没有,查询mysql*/
            System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "发现缓存中没有申请缓存分布式锁:" + "sku:" + skuId + ":lock");
            /**
             * 缓存穿透问题:(1).基于redis的nx分布式锁
             */
            /*设置分布式锁 返回值是OK*/
            String token = UUID.randomUUID().toString();
            String OK = jedis.set("sku:" + skuId + ":lock", token, "nx", "px", 10 * 1000);
            if (OK != null && !OK.equals("") && OK.equals("OK")) {
                /*设置成功,有权利在10秒的过期时间内访问数据库*/
                System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "成功拿到分布式锁,有权利在10秒的过期时间内访问数据库" + "sku:" + skuId + ":lock");
                pmsSkuInfo = getSkuByIdFromDb(skuId);

                /*测试代码:*/
                /*try {
                    Thread.sleep(5*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/

                if (pmsSkuInfo != null) {
                    /*4.mysql将想要查询的结果返回给redis*/
                    System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "mysql将数据结果返回给redis缓存中");
                    jedis.set("sku:" + skuId + ":info", JSON.toJSONString(pmsSkuInfo));
                } else {
                    /*数据库中不存在该sku*/
                    /*为了防止缓存穿透(大量请求访问数据库mysql),将null值或者空串给redis , 并且给null值或者空串设置3分钟的自动销毁*/
                    jedis.setex("sku:" + skuId + ":info", 60 * 3, JSON.toJSONString(""));
                }
                /*在访问MySQL后,将手中的mysql锁释放掉*/
                /*问题一:为了避免当拿到锁的用户在过期时间后依旧没有从数据库中获取到数据,锁已经自动销毁(过期时间已满)
                  这时下一个用户拿到锁,开始访问数据库,但是,上一个拿到锁的用户访问完毕,会回来执行“删除锁”
                  jedis.del("sku:" + skuId + ":lock");命令,这时删除的锁是当前正在访问的用户的锁,造成当前用户访问失败。
                  所以在当前用户拿到锁情况下,上一个用户回来做“删除锁”jedis.del("sku:" + skuId + ":lock");命令时
                  上一个用户应该将此时的newToken和自己拿到锁分配的随机token做比较,相同则是同一个用户,可以删除锁
                  确认删除的是自己的锁    "sku:" + skuId + ":info"=token  k , v 结构 */
                String newToken = jedis.get("sku:" + skuId + ":lock");
                if (newToken != null && !newToken.equals("") && newToken.equals(token)) {
                    /*问题二:当刚好代码执行到这里时,自己的token过期了,然后在if里面又把下一个用户的token删除了,怎么解决?
                      可以用lua脚本,在查询到key的同时删除该key,防止高并发下的意外发生!
                      将:String newToken = jedis.get("sku:" + skuId + ":info");
                         if(newToken!=null&&!newToken.equals("")&&newToken.equals(token)){
                             jedis.del("sku:" + skuId + ":lock");
                         }改为下面的代码:lua脚本,在查询到key的同时删除该key!
                       String script = "if redis.call('get' , KEYS[1]==ARGV[1] then return redis.call('del',KEYS[1]))
                                        else return o end";
                       jedis.eval(script,Collections.singletonList("lock"),Conllections.singletonList(token));
                    */
                    jedis.del("sku:" + skuId + ":lock");
                    System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "在访问MySQL后,将手中的mysql锁释放掉" + "sku:" + skuId + ":lock");
                }
            } else {
                /*设置失败 ,自旋(该线程在睡眠几秒后重新尝试访问getSkuById方法)*/
                System.out.println("ip为:" + ip + "的用户:" + Thread.currentThread().getName() + "没有拿到分布式锁 ,自旋(该线程在睡眠几秒后重新尝试访问getSkuById方法)");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                /*return不会产生新的线程,这才是自旋。如果不加return,则会产生新的getSkuById()“孤儿”线程。*/
                return getSkuById(skuId, ip);
            }
        }

        jedis.close();/*最后一步一定要关闭jidis*/

        return pmsSkuInfo;
    }

相关推荐

DiamondTao / 0评论 2020-08-30