Redis缓存策略之缓存击穿

一、缓存击穿解决方案 

        缓存击穿也称热点Key问题,是指缓存中的某个数据过期或者被删除,而此时有大量的并发请求查询这个数据,导致这些请求直接落到后端存储上,增加了存储的负载。为了解决缓存击穿问题,可以考虑以下两个方案:

        (1)互斥锁:在缓存失效的瞬间,使用互斥锁(也称为分布式锁)来保证只有一个线程去查询数据库,其他线程等待获取锁。只有当第一个线程查询数据库后,其他线程才能获取缓存的数据。

        其优点是确保只有一个线程能够执行数据库查询操作,避免了多个线程同时查询导致的缓存击穿问题。通过互斥锁,可以控制并发访问的数量,避免瞬时的大量请求直接访问数据库。

        其缺点是使用互斥锁可能带来一定的性能开销,特别是在高并发场景下。还有不当使用锁可能导致死锁的问题,需要慎重设计锁的使用方式。而且其他线程会一直循环操作,直到解锁。

        (2)逻辑过期:设置过期时间,在Redis中,为缓存数据的值(value)中添加了一个过期时间标识。这个过期时间并不会直接作用于Redis,而是后续通过业务逻辑去处理。

        假如当线程一发起查询请求时,首先从缓存中获取数据。通过检查value中的过期时间标识,此时假设线程一可以判断出当前的数据已经过期了。由于数据已经过期,线程一尝试获取互斥锁。如果成功获取锁,说明线程一是第一个发现数据过期的线程。

        获取锁后,线程一开启一个新的线程(线程二),该线程负责进行以前的重构数据的逻辑。这个重构过程可能包括从数据库中获取最新的数据,并将其写入缓存。

        如果此时有其他线程(例如线程三)发起查询请求,由于线程二持有着锁,线程三无法获得锁。因此,线程三也会直接返回当前已过期的数据,直到新开的线程二把重建数据构建完毕。

        这种方法的优点在于,通过使用互斥锁和异步构建缓存的方式,系统可以在后台进行数据的重构,而不会阻塞其他线程的查询请求。然而,缺点是在构建完缓存之前,返回的都是过期的数据,可能会导致一段时间内返回的是不准确的结果。

二、缓存击穿案例 

 本文以互斥锁方法为例解决缓存击穿问题。

(1)首先通过命令熟悉互斥锁的基本使用

        如下图所示,在命令行中直接登录Redis客户端,使用setnx lock 1模拟上锁,该命令尝试将键“lock”的值设置为1,但仅当该键不存在时才执行。由于键“lock”之前不存在,该命令执行成功,返回值为 1,表示设置成功。

        使用get lock命令用于检索键“lock”的当前值。由于在前一步中成功执行了setnx命令,键 “lock”的值现在为 "1"。

        使用setnx lock 2再次尝试使用 setnx 将键“lock”的值设置为 2。由于键“lock”已经存在,该命令执行失败,返回值为 0,表示设置未成功。

        使用 del 命令删除键“lock”。返回值为 1,表示成功删除了键 "lock"。

       使用setnx lock 2命令,再次尝试使用 setnx 将键“lock”的值设置为 2。由于键“lock”之前已被删除,该命令执行成功,返回值为 1,表示设置成功。

        这几行命令简单模拟了互斥锁的使用。    

(2)将互斥锁应用到Web项目中

        下方代码是一个简单的分布式锁的实现,使用了Redis的setIfAbsent和delete操作来获取锁和释放锁。

//获取锁
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

//释放锁
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

        ①获取锁tryLock方法:

        使用 setIfAbsent 操作,尝试在 Redis 中设置键值对,如果该键不存在(即成功获取锁),则将键的值设置为 "1",并设置过期时间为 10 秒。

        返回一个Boolean类型的标志,表示是否成功获取锁。在代码中,使用 BooleanUtil.isTrue(flag) 来转换为布尔值。

        ②释放锁unlock方法:

         使用 delete 操作,将 Redis 中对应的键删除,即释放锁。

        编写queryWithMutex方法,实现了缓存击穿和缓存穿透的问题

        首次某一个线程使用tryLock方法尝试获取互斥锁,如果获取失败,则进行休眠重试。对应一下部分:

//4.3 失败,则休眠重试
    Thread.sleep(50);
    return queryWithMutex(id);

        在获取互斥锁成功后,根据商店ID查询数据库。为了模拟实际应用中的一些耗时操作,这里使用 Thread.sleep(2000) 模拟延迟。

        判断数据库中是否存在该商店信息,不存在则将空值写入 Redis 缓存,然后返回空。如果数据库中存在该商店,将商铺数据写入 Redis 缓存。最后,在 finally 块中释放互斥锁,这时别的线程才能读取到缓存中的信息。互斥锁解决了缓存击穿的问题。

        源代码如下:

//互斥锁解决击穿
public Shop queryWithMutex(Long id) {
    String key = CACHE_SHOP_KEY + id;
    // 1、从redis中查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2、判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 存在,直接返回
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    //判断命中的值是否是空值
    if (shopJson != null) {
        //返回一个错误信息
        return null;
    }
    // 4.实现缓存重构
    //4.1 获取互斥锁
    String lockKey = "lock:shop:" + id;
    Shop shop = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 4.2 判断否获取成功
        if (!isLock) {
            //4.3 失败,则休眠重试
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        //4.4 成功,根据id查询数据库
        shop = getById(id);
        //模拟延迟
        Thread.sleep(2000);
        // 5.不存在,返回错误
        if (shop == null) {
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //6.写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_NULL_TTL, TimeUnit.MINUTES);

    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        //7.释放互斥锁
        unlock(lockKey);
    }
    return shop;
}

        重启项目,这里为了模拟大量线程对一个失效Key的值的访问,使用Jmeter性能测试工具,模拟1000个线程对URL(localhost/shop/1)进行请求。如图,设置线程组里面有1000个线程,如下图,设置请求的URL。

        运行该线程组,如下图,查看结果,所有HTTP请求成功。 

         如下图,1000个线程请求该URL,最终只执行一次数据库的查询语句。

  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无所谓_我会出丑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值