在电商类应用中,商户信息和商铺列表是最频繁被访问的数据之一。传统模式下每次请求都直接查询数据库会给系统带来巨大压力,而引入Redis缓存能显著提升系统响应速度并降低数据库负载。下面我将结合实战经验,详细解析如何为商户信息和商铺列表添加缓存层。
缓存的核心思想是"空间换时间",通过将热点数据存储在内存中,减少对磁盘数据库的访问。在商户信息场景中,我们采用标准的Cache-Aside模式:
这种模式特别适合读多写少的场景,比如商户信息这类变更频率较低的数据。
注意:不要采用先删除缓存再更新数据库的策略,这会导致缓存击穿问题。我们采用先更新DB再删除缓存的策略,虽然可能存在极短时间的数据不一致,但对商户信息这类数据是可以接受的。
对于商户信息缓存,我们使用String类型存储JSON序列化后的对象,原因在于:
对应的Redis key设计为:
code复制cache:shop:{id}
这种命名方式清晰表达了业务含义,冒号分隔形成逻辑命名空间,避免key冲突。
让我们深入分析商户信息查询的缓存实现:
java复制public Result queryById(Long id) {
// 1. 构建Redis key
String key = CACHE_SHOP_KEY + id;
// 2. 尝试从Redis获取缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 3. 缓存命中处理
if(StrUtil.isNotBlank(shopJson)){
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4. 缓存未命中,查询数据库
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);
}
缓存穿透防护:当查询不存在的商铺ID时,我们仍然会缓存空结果(步骤5返回前可以添加空缓存),避免恶意攻击反复查询不存在的ID。
序列化选择:使用JSON序列化而非Java原生序列化,因为:
缓存更新策略:商户信息变更时,应采用"先更新数据库,再删除缓存"的策略,确保下次读取时获取最新数据。
设置合理的过期时间:即使是商户信息这类相对稳定的数据,也应设置TTL(如24小时),防止长期不更新导致的数据陈旧。
热点数据预热:可以在系统启动时或低峰期,主动加载热点商户数据到缓存。
批量查询支持:当前实现是单条查询,对于批量查询场景可以扩展为pipeline或mget操作。
商铺列表缓存与单条商户缓存有所不同,它处理的是集合数据,且通常需要保持特定的排序。
java复制public Result queryByList() {
// 1. 构建固定key(列表缓存通常使用固定key)
String key = "cache:shop:type";
// 2. 尝试从Redis获取缓存
String shopTypeJson = stringRedisTemplate.opsForValue().get(key);
// 3. 缓存命中处理
if(StrUtil.isNotBlank(shopTypeJson)){
List<ShopType> shopTypeList = JSONUtil.toList(shopTypeJson, ShopType.class);
return Result.ok(shopTypeList);
}
// 4. 缓存未命中,查询数据库(带排序)
List<ShopType> shopTypeList = query().orderByAsc("sort").list();
// 5. 数据库不存在处理
if(shopTypeList == null){
return Result.fail("店铺类型不存在");
}
// 6. 数据库存在,写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopTypeList));
// 7. 返回结果
return Result.ok(shopTypeList);
}
Key设计:列表缓存使用固定key而非动态ID,因为列表通常是全局唯一的。
排序处理:数据库查询时明确指定了排序规则(orderByAsc("sort")),确保每次返回一致的顺序。
更新频率:列表数据变更频率可能更高,需要设置较短的TTL或通过消息机制及时更新。
大列表性能问题:当商铺数量很多时,JSON序列化和网络传输可能成为瓶颈。解决方案:
一致性挑战:列表数据变更时,所有客户端可能同时失效缓存。解决方案:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 定时过期 | 实现简单 | 可能读到过期数据 | 数据一致性要求不高 |
| 写时删除 | 数据新鲜度高 | 写操作开销大 | 读多写少 |
| 写时更新 | 一致性最好 | 实现复杂 | 数据一致性要求高 |
对于商户信息,推荐组合使用"写时删除+定时过期"策略,既保证基本一致性,又防止长期不更新的问题。
监控指标:
调优参数:
properties复制# Redis连接池配置示例
spring.redis.lettuce.pool.max-active=16
spring.redis.lettuce.pool.max-wait=200ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=4
缓存穿透:
缓存雪崩:
缓存击穿:
对于高并发场景,可以考虑以下进阶优化:
多级缓存架构:
code复制客户端 → CDN → 网关缓存 → 应用本地缓存 → Redis → DB
热点数据发现:
异步缓存更新:
在实际项目中,我们通过引入Caffeine作为本地二级缓存,将核心商户信息的读取QPS从2000提升到了8000+,同时平均响应时间从15ms降低到3ms。关键配置如下:
java复制@Bean
public CaffeineCacheManager cacheManager() {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats();
return new CaffeineCacheManager("shopCache", caffeine);
}
缓存技术看似简单,但在高并发系统中需要精心设计和不断调优。建议在项目初期就建立完善的缓存监控体系,记录命中率、响应时间等关键指标,为后续优化提供数据支持。