【黑马点评-2店铺查询】四、缓存穿透(1. 存空字符串)的逻辑 + 代码实现(public Shop querywithchuantou(Long id))

什么是缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,
这样缓存永远不会生效,这些请求都会打到数据库。

shop实体对应的数据库中表tb_shop

字段有

  • id
  • name
  • type等
  • typeid(有索引)

public Shop querywithchuantou(Long id) 代码逻辑

1. 查询Redis, 若有数据,直接返回shop实体。

  1. 从Redis中查询对应的json字符串, key为"cache:shop:"+id
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    
  2. 如果shopJson不为null,也不是空字符串,说明Redis中有有效数据,直接转为shop实体返回。
    if (StrUtil.isNotBlank(shopJson)) {
    	Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    	return shop;
    }
    

2. 若缓存存储的空字符串,返回null,失败。

  1. 如果shopJson为空字符串(说明数据库中也没有数据),直接返回null,失败
    if (shopJson != null) {
    	/*
    	2.2.1 缓存中为空字符串:
    	如果 shopJson 不为 null,但前面的 isNotBlank 判断未通过,
    	说明 shopJson 是一个空字符串或仅包含空白字符。
    	表明该店铺不存在。因此,直接返回 null,避免再次访问数据库。
    	*/
    	return null;
    }
    

3. 缓存未命中,查询数据库,数据库也没,则向Redis存空字符串,返回null,失败。

  1. 如果shopJson为null,说明Redis未命中。
  2. 借助MyBatis-plus,由id获取shop实体
    Shop shop = getById(id);
    
  3. 若shop也为null,说明数据库也没,向Redis中存空字符串
  • key为"cache:shop:"+id
  • value为空字符串
  • 加一个key的过期时间2min(只短期)
    if (shop == null) {
    	stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
    	return null;
    }
    

4. 数据库中有,将数据重建到Redis,并返回shop实体。

  1. 将数据库中的shop实体专为Json字符串
  2. 存入Redis中
  3. 返回shop实体给用户
    String jsonStr = JSONUtil.toJsonStr(shop);
    // 并存入redis,并设置TTL,防止存了错的缓存
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
    // 4. 最终把查询到的商户信息返回给前端
    return shop;
    

示例代码

/*
    querywithchuantou 方法旨在解决缓存穿透问题。缓存穿透是指对一个在缓存和数据库中都不存在的数据进行频繁查询,
    导致每次请求都直接访问数据库,增加了数据库的压力。
     */
public Shop querywithchuantou(Long id) {
    /*
     * 1. 从 Redis 查询缓存:
     * 首先,通过 stringRedisTemplate 从 Redis 中获取键为 CACHE_SHOP_KEY + id 的缓存数据。
     * 这里,CACHE_SHOP_KEY 是缓存键的前缀,id 是店铺的唯一标识符。
     */
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    /*
     * 2. 判断缓存中是否存在数据:
     * 2.1 缓存命中的话,直接返回店铺数据
     * 如果 shopJson 不为空(字符串不是 null、""、全空格),说明缓存中有数据。
     * 则将其反序列化为 Shop 对象并返回,避免了对数据库的访问。
     * 
     * StrUtil.isNotBlank(shopJson) 用于判断 shopJson 是否不为 null、不为空字符串,
     * 且不全是空白字符。
     */
    if (StrUtil.isNotBlank(shopJson)) {
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return shop;
    }

    /*
     * 2.2 缓存未命中
     */
    if (shopJson != null) {
        /*
         * 2.2.1 缓存中为空字符串:
         * 如果 shopJson 不为 null,但前面的 isNotBlank 判断未通过,
         * 说明 shopJson 是一个空字符串或仅包含空白字符。
         * 表明该店铺不存在。因此,直接返回 null,避免再次访问数据库。
         */
        return null;
    }

    /*
     * 2.2.2 当前数据是null,查询数据库:
     * 如果缓存中没有相关数据(即 shopJson 为 null),则查询数据库获取店铺信息。
     */
    Shop shop = getById(id);

    /*
     * 3. 判断数据库中是否存在该数据:
     */
    if (shop == null) {
        // 这里的常量值是2分钟
        /*
         * 3.1 数据库中也没,则缓存空对象
         * 如果数据库中也没有该店铺的信息,则将空字符串存入 Redis,并设置一个较短的过期时间(CACHE_NULL_TTL,例如 2 分钟)。
         * 这样可以在短时间内避免对同一不存在的数据频繁查询数据库,减轻数据库压力。
         */
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return null;
    }

    /*
     * 3.2 数据库中存在,缓存数据库中存在的数据:
     * 如果数据库中存在该店铺的信息,则将其序列化为 JSON 字符串,存入 Redis,
     * 并设置一个较长的过期时间(CACHE_SHOP_TTL,例如 30 分钟)。这样可以在后续相同的查询中直接从缓存获取数据,提高查询效率。
     */
    String jsonStr = JSONUtil.toJsonStr(shop);
    // 并存入redis,并设置TTL,防止存了错的缓存
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
    // 4. 最终把查询到的商户信息返回给前端
    return shop;
}
posted @ 2025-04-12 23:06  kuki'  阅读(60)  评论(0)    收藏  举报