在这一节中,我们将利用 Redis 极大地提升商户详情的查询性能,并探讨企业级开发中非常重要的“缓存一致性”问题。
1. 认识缓存及其作用
缓存(Cache)就像是越野车的“避震器”,它的数据通常存储在内存中,读写性能远高于磁盘。
- 为什么要使用缓存? 主要是为了降低后端数据库的负载,提高读写效率,降低响应时间。面对海量的高并发请求,如果没有缓存作为缓冲,数据库极易崩溃。
- 使用缓存的成本:虽然速度快,但也会带来额外的成本,包括数据一致性成本、代码维护成本和运维成本。
2. 添加商户缓存的核心流程
标准的操作方式是:先查询缓存,缓存没有再查询数据库。 具体业务流转如下:
-
提交商铺 id。
-
从 Redis 查询商铺缓存。
-
判断缓存是否命中:
- 如果命中,直接将 JSON 数据反序列化后返回给前端。
- 如果未命中,则根据 id 去 MySQL 数据库查询。
-
判断数据库中是否存在:
- 如果不存在,返回错误提示(例如 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 常见的三种更新策略
- 内存淘汰:不用自己维护,利用 Redis 内存达到上限时自动淘汰部分数据(一致性差)。
- 超时剔除:给缓存添加 TTL 有效期,到期自动删除,下次查询时更新(一致性一般,作为兜底方案)。
- 主动更新(Cache Aside Pattern) :编写业务逻辑,在修改数据库的同时,更新缓存(一致性好,企业常用)。
3.2 主动更新的两个致命拷问(面试高频)
当我们决定采用“主动更新”时,面临两个技术难点:
拷问一:是删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都去更新缓存,如果中间一直没人查询,就会产生大量无效写操作。
- 删除缓存(推荐) :更新数据库时让缓存失效(删除),下一次查询时自然会重新拉取最新数据。这属于延迟加载的思想,胜出。
拷问二:如何保证数据库与缓存操作的原子性,且先操作谁?
- 先删除缓存,再操作数据库:如果此时有并发,线程1删除了缓存还没来得及写数据库,线程2来查询发现没缓存,就会把数据库的旧数据查出来并写回缓存,导致数据永久不一致。
- 先操作数据库,再删除缓存(推荐) :我们应当先更新数据库,再删除缓存。虽然在极极端情况下依然有并发冲突的可能,但概率极低,是企业最主流的做法。同时为了保证原子性,可以利用 Spring 的
@Transactional将两个操作放在一个事务中。
3.3 完善缓存双写一致性代码
根据上述理论,我们需要对原有代码进行两处修改:
-
查询逻辑中(兜底方案) :将商铺数据写入 Redis 时,必须加上超时时间(例如 30 分钟)。
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES); -
修改商户逻辑中(主动更新) :先修改数据库,再删除缓存。
@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(); }
(以上逻辑参考自源文档)