点评--day03--2. 2 商户查询缓存

4 阅读4分钟

在这一节中,我们将利用 Redis 极大地提升商户详情的查询性能,并探讨企业级开发中非常重要的“缓存一致性”问题。

1. 认识缓存及其作用

缓存(Cache)就像是越野车的“避震器”,它的数据通常存储在内存中,读写性能远高于磁盘。

  • 为什么要使用缓存? 主要是为了降低后端数据库的负载,提高读写效率,降低响应时间。面对海量的高并发请求,如果没有缓存作为缓冲,数据库极易崩溃。
  • 使用缓存的成本:虽然速度快,但也会带来额外的成本,包括数据一致性成本、代码维护成本和运维成本。

2. 添加商户缓存的核心流程

标准的操作方式是:先查询缓存,缓存没有再查询数据库。 具体业务流转如下:

  1. 提交商铺 id。

  2. 从 Redis 查询商铺缓存。

  3. 判断缓存是否命中

    • 如果命中,直接将 JSON 数据反序列化后返回给前端。
    • 如果未命中,则根据 id 去 MySQL 数据库查询。
  4. 判断数据库中是否存在

    • 如果不存在,返回错误提示(例如 404)。
    • 如果存在,先将商铺数据写入 Redis,然后再返回给前端。

核心代码演示:

@Override
public Result queryById(Long id) {
    String key = "cache:shop:" + id;
    // 1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    // 4.不存在,根据id查询数据库
    Shop shop = getById(id);
    // 5.不存在,返回错误
    if (shop == null) {
        return Result.fail("店铺不存在!");
    }
    // 6.存在,写入redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
    // 7.返回
    return Result.ok(shop);
}

(以上逻辑参考自源文档)


3. 核心进阶:缓存更新策略(双写一致性问题)

只要使用了缓存,就一定会面临数据库与缓存数据不一致的问题。那么如何选择更新策略呢?

3.1 常见的三种更新策略

  1. 内存淘汰:不用自己维护,利用 Redis 内存达到上限时自动淘汰部分数据(一致性差)。
  2. 超时剔除:给缓存添加 TTL 有效期,到期自动删除,下次查询时更新(一致性一般,作为兜底方案)。
  3. 主动更新(Cache Aside Pattern) :编写业务逻辑,在修改数据库的同时,更新缓存(一致性好,企业常用)。

3.2 主动更新的两个致命拷问(面试高频)

当我们决定采用“主动更新”时,面临两个技术难点:

拷问一:是删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都去更新缓存,如果中间一直没人查询,就会产生大量无效写操作。
  • 删除缓存(推荐) :更新数据库时让缓存失效(删除),下一次查询时自然会重新拉取最新数据。这属于延迟加载的思想,胜出

拷问二:如何保证数据库与缓存操作的原子性,且先操作谁?

  • 先删除缓存,再操作数据库:如果此时有并发,线程1删除了缓存还没来得及写数据库,线程2来查询发现没缓存,就会把数据库的旧数据查出来并写回缓存,导致数据永久不一致。
  • 先操作数据库,再删除缓存(推荐) :我们应当先更新数据库,再删除缓存。虽然在极极端情况下依然有并发冲突的可能,但概率极低,是企业最主流的做法。同时为了保证原子性,可以利用 Spring 的 @Transactional 将两个操作放在一个事务中。

3.3 完善缓存双写一致性代码

根据上述理论,我们需要对原有代码进行两处修改:

  1. 查询逻辑中(兜底方案) :将商铺数据写入 Redis 时,必须加上超时时间(例如 30 分钟)。

    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
    
  2. 修改商户逻辑中(主动更新) :先修改数据库,再删除缓存。

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空");
        }
        // 1. 更新数据库
        updateById(shop);
        // 2. 删除缓存
        stringRedisTemplate.delete("cache:shop:" + id);
        return Result.ok();
    }
    

(以上逻辑参考自源文档)