基于互斥锁 解决 缓存击穿的解决方案

什么是缓存击穿

缓存击穿问题也叫热点Key问题,
就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,
无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案

  1. 互斥锁(时间换空间)
  • 优点:内存占用小,一致性高,实现简单
  • 缺点:性能较低,容易出现死锁
  1. 逻辑过期(空间换时间)
  • 优点:性能高
  • 缺点:内存占用较大,容易出现脏读

两者相比较,互斥锁更加易于实现,但是容易发生死锁,且锁导致并行变成串行,导致系统性能下降。
逻辑过期实现起来相较复杂,且需要耗费额外的内存,但是通过开启子线程重建缓存,使原来的同步阻塞变成异步,提高系统的响应速度,但是容易出现脏读

逻辑图

image

请求 -> Redis 有数据? -> 有:返回
                     ↓
                没有数据 -> 是空字符串? -> 是:返回null
                                       ↓
                              否:尝试加锁 -> 加锁失败:休眠重试
                                            ↓
                                      加锁成功 -> 查数据库
                                                ↓
                                       数据为空 -> 缓存空字符串
                                       有数据  -> 缓存结果
                                                ↓
                                           返回数据

实现

/*
    二、缓存击穿的解决:法一,互斥锁
    引入“互斥锁”来确保只有一个线程去查询数据库和重建缓存,其他线程等待或重试。
     */
public Shop querywithjichuan_mutex(Long id) {
	// 1. 先从Redis中查,这里的常量值是固定的前缀 + 店铺id
	String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
	/*
        2. 判断缓存是否命中
        2.1 缓存命中了
        2.1.1 如果 shopJson 不为空(字符串不是 null、""、全空格),说明缓存中有数据。
        把 json 字符串转为 Shop 对象直接返回,不需要访问数据库。
         */
	if (StrUtil.isNotBlank(shopJson)) {
		Shop shop = JSONUtil.toBean(shopJson, Shop.class);
		return shop;
	}

	/* 2.1.2 如果查询到的是空字符串“”,则说明是我们缓存的空数据
        之前数据库查不到这条数据,我们会把一个空字符串放进缓存。
        这样后续请求查到这个空字符串就不会再去访问数据库,避免缓存穿透。
         */
	if (shopJson != null) {
		return null;
	}
	/*
        2.2 缓存未命中,开始加锁重建缓存
        实现在高并发的情况下缓存重建
        调用 tryLock() 尝试获取互斥锁(通常是 Redis 的 SETNX 实现)。
         */

	Shop shop = null;
	try {
		// 获取互斥锁
		boolean flag = tryLock(LOCK_SHOP_KEY + id);
		/* 2.2.1 加锁失败就休眠重试(自旋)
            如果拿不到锁,说明其他线程正在重建缓存。
            当前线程休眠 50 毫秒后递归调用自己重新尝试查询缓存。
            这个过程称为“自旋锁”或“延迟重试”。
             */
		while (!flag) {
			Thread.sleep(50);
			return querywithjichuan_mutex(id);
		}
		// 2.2.2 获取成功->读取数据库,重建缓存
		// 2.2.2.1 查不到,则将空值写入Redis
		shop = getById(id);
		if (shop == null) {
			stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
			return null;
		}
		/*
            2.2.2.2 数据库中有数据,写入缓存
            查到了则转为json字符串
             */
		String jsonStr = JSONUtil.toJsonStr(shop);
		// 并存入redis,设置TTL
		stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
		// 最终把查询到的商户信息返回给前端
	} catch (InterruptedException e) {
		throw new RuntimeException(e);
	} finally {
		// 2.2.3 最后:释放锁
		unlock(LOCK_SHOP_KEY + id);
	}
	return shop;
}
  1. 使用Redis中的setnx指令实现互斥锁,只有当值不存在时才能进行set操作
  2. 锁的有效期更具体业务有关,需要灵活变动,一般锁的有效期是业务处理时长10~20倍
  3. 线程获取锁后,还需要查询缓存(也就是所谓的双检),这样才能够真正有效保障缓存不被击穿
posted @ 2025-04-09 11:31  kuki'  阅读(94)  评论(0)    收藏  举报