【黑马点评-2店铺查询】三、缓存击穿(1. 互斥锁解决)的逻辑 + 代码实现(public Shop querywithjichuan_mutex(Long id))

什么是缓存击穿

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

店铺查询 + 互斥锁的逻辑图

居中图片

public Shop querywithjichuan_mutex(Long id) 逻辑

1. 先根据key查询Redis

  1. "cache:shop:"+id为key,从Redis中提取
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    

2. 若有数据,直接返回shop

  1. 若shopJson非null也非空字符串,则说明Redis中有数据,直接转换为shop实体返回。
    if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
    }
    

3. 查到空字符串,则返回失败

  1. 若为空字符串,则(数据库中也没),直接返回null,失败。
    if (shopJson != null) {
            return null;
    }
    

4. 获取互斥锁,再去查询数据库

4.0 锁的实现

  1. 加锁:在Redis中设置key,value为字符串1,过期时间为10s(尝试设置键 "myLock",如果该键不存在,则设置成功并在 10 秒后过期;如果已存在,则设置失败。)
  2. 释放锁,把Redis中该key删了。
/*
    下面用来解决热点高并发访问中的缓存击穿问题
    实现了一个基于 Redis 的分布式锁机制,包含获取锁和释放锁的逻辑。
     */
/*
1. 获取锁的逻辑:
 */
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 避免返回值为null,我们这里使用了BooleanUtil工具类
    return BooleanUtil.isTrue(flag);
}

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

4.1 尝试获取锁

  1. 调用基于Redis的加锁操作,
  • key为 "lock:shop:"+id
  • 若该key不存在,则获取成功
    boolean flag = tryLock(LOCK_SHOP_KEY + id);
    
  1. 若获取失败,则sleep 50ms再尝试
    while (!flag) {
    	Thread.sleep(50);
    	return querywithjichuan_mutex(id);
    }
    

5.1 数据库也没有,就Redis中存空字符串

  1. 根据id获取shop实体
  2. 若shop为null,即数据库也无,则在缓存中存空字符串
    • key为"cache:shop:"+id
    • value为空字符串
    • 过期时间为2min
    if (shop == null) {
    	stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
    	return null;
    }
    

5.2 数据库中有,就重建Redis

  1. shop转为Json字符串存入Redis
    • key为"cache:shop:"+id
    • value就是shop的json字符串,也要有个过期时间,30min
String jsonStr = JSONUtil.toJsonStr(shop);
// 并存入redis,设置TTL
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);

6 释放锁

unlock(LOCK_SHOP_KEY + id);

示例代码

/*
    二、缓存击穿的解决:法一,互斥锁
    引入“互斥锁”来确保只有一个线程去查询数据库和重建缓存,其他线程等待或重试。
     */
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;
}
posted @ 2025-04-12 22:51  kuki'  阅读(122)  评论(0)    收藏  举报