Redis( 缓存篇 ==> 互斥锁解决缓存击穿

我们可以在查询缓存未命中的时候添加一个互斥锁。这样一来,在面对高并发的情况下,只有第一个进来的线程才可以拿到锁然后操作数据库,待操作结束后释放锁,未拿到锁的用户则等待一段时间重新查询缓存,直到缓存重建完毕后拿到数据后方可结束。

Redis( 缓存篇 ==> 互斥锁解决缓存击穿_第1张图片

 关于互斥锁这一部分,我们可以使用Redis里的setnx命令来模拟实现。

Redis( 缓存篇 ==> 互斥锁解决缓存击穿_第2张图片

setnx命令对应java里的setIfAbsent,代码如下:

Redis( 缓存篇 ==> 互斥锁解决缓存击穿_第3张图片

这样一来,只有第一个进来的线程才可以添加key并返回true,后面来的线程无法完成添加且返回false。

我们在写一个释放锁的方法,在拿到锁的进程操作数据库结束后,把锁释放掉(防止死锁)

总体实现流程大致如下:

  1. 根据key查询Redis缓存
  2. 命中了数据直接返回即可
  3. 命中了空值直接返回错误信息(防止缓存穿透)
  4. 获取锁
  5. 获取锁失败则等待一段时间后重新查询缓存
  6. 获取锁成功则查询数据库
  7. 数据库查询为null则缓存一个空值到Redis里(防止缓存穿透),过期时间设置短一点
  8. 数据库查询成功则直接添加缓存到Redis里
  9. 释放锁

Redis( 缓存篇 ==> 互斥锁解决缓存击穿_第4张图片 

代码实现如下(带详细注释):

    public Shop cacheBreakDown(Long id) {
        //定义一个ID
        String cacheID = CACHE_SHOP_KEY + id;
        //1、查询Redis
        String shopJson = stringRedisTemplate.opsForValue().get(cacheID);
        //2、判断是否命中
        if(StrUtil.isNotBlank(shopJson)){
            //命中了直接返回
            return BeanUtil.toBean(shopJson,Shop.class);
        }
        //3、判断是否命中空值
        if(shopJson != null){
            //命中了空值,直接返回
            return null;
        }
        //提前定义一个shop,要不在try里跨作用域了最后return不了
        Shop shop = null;
        try{
            //4、缓存未命中,获取锁,判断是否成功拿到锁
            if(!getLock(id)){
                //没有拿到锁,说明已有别的线程进来了,正在添加缓存
                //休眠一会,重新查询缓存
                Thread.sleep(200);
                //一定要return!递归出口
                return cacheBreakDown(id);
            }
            //5、只有拿到锁的线程才可以查询数据库
            shop = getById(id);
            //模拟缓存重建过程所需时间
            Thread.sleep(500);
            //5、判断是否查询到数据
            if(shop == null){
                //没数据,返回空值缓存,防止缓存击穿
                stringRedisTemplate.opsForValue().set(cacheID,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //6、查询成功,添加缓存
            stringRedisTemplate.opsForValue().set(cacheID,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
        }catch (Exception e){
            //统一异常处理
            throw new RuntimeException(e);
        }finally {
            //7、释放锁
            unLock(id);
        }
        //8、返回
        return shop;
    }

总结一下互斥锁的优缺点:

优点:

  • 不需要消耗额外内存
  • 实现简单
  • 保证了一致性

缺点:

  • 没拿到锁的线程需要等待重试,效率不高
  • 加锁和解锁的过程没有保证原子性,可能面临死锁的风险

你可能感兴趣的:(Redis,缓存,redis,数据库,spring,boot)