1. 项目背景与核心需求
在本地生活服务类应用中,店铺信息展示是最基础也最频繁的业务场景之一。以"黑马点评"这类应用为例,当用户搜索附近餐厅时,系统需要快速返回店铺列表,包含名称、评分、人均消费等关键信息。这类数据的特点是读多写少(每天可能有数万次查询,但店铺信息可能几天才更新一次),天然适合使用缓存来减轻数据库压力。
但缓存机制如果设计不当,会导致两类典型问题:
- 数据不一致:店铺信息已更新(如修改了营业时间),但用户看到的仍是缓存中的旧数据
- 内存溢出:长期不活跃的店铺缓存持续占用内存,影响系统整体性能
这正是我们需要实现"超时剔除"和"主动更新"双重策略的根本原因。前者通过TTL(Time To Live)机制自动清理过期数据,后者确保关键数据变更时能及时同步到缓存层。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis缓存策略设计
2.1 基础缓存方案选型
对于店铺信息这类结构化数据,通常有两种缓存方案:
- 序列化存储:将整个店铺对象转为JSON/String存入Redis
- 优点:一次查询即可获取完整信息
- 缺点:更新时需要反序列化-修改-再序列化
- Hash存储:利用Redis Hash结构存储字段
- 优点:可单独更新某个字段(如只修改评分)
- 缺点:查询所有字段时需要HGETALL操作
考虑到店铺信息的字段数量适中(通常10-20个字段),且需要频繁展示完整信息,本方案选择JSON序列化存储方式。示例数据结构如下:
java复制// Java实体类
public class Shop {
private Long id;
private String name;
private String address;
private Double score;
private String businessHours;
// 其他字段...
}
// 存储格式
{
"id": 1,
"name": "川湘居",
"address": "朝阳区建国路88号",
"score": 4.8,
"businessHours": "10:00-22:00"
}
2.2 超时剔除策略实现
Redis原生支持通过EXPIRE命令设置键的存活时间。我们需要考虑两个关键参数:
-
基础TTL时间:根据业务特点设置合理的过期时间
- 高频变更数据(如秒杀库存):设置较短TTL(如30秒)
- 低频变更数据(如店铺基础信息):设置较长TTL(如30分钟)
-
抖动因子:避免缓存雪崩
- 直接设置固定TTL可能导致大量缓存同时失效
- 解决方案:基础TTL + 随机抖动值(如±10%)
Java实现示例:
java复制public void setShopCache(Shop shop) {
// 基础TTL 30分钟 + 随机抖动(±3分钟)
int baseTtl = 30 * 60;
int randomTtl = ThreadLocalRandom.current().nextInt(-180, 180);
int finalTtl = baseTtl + randomTtl;
String key = "cache:shop:" + shop.getId();
String value = JSON.toJSONString(shop);
redisTemplate.opsForValue().set(key, value, finalTtl, TimeUnit.SECONDS);
}
2.3 主动更新策略设计
主动更新需要建立数据库与缓存的联动机制,常见模式有:
-
双写模式:在更新DB的同时直接更新缓存
- 优点:实现简单
- 缺点:非原子操作,可能产生不一致
-
消息队列:通过变更日志触发缓存更新
- 优点:解耦可靠
- 缺点:系统复杂度高
-
延迟双删:先删缓存→更新DB→延迟再删缓存
- 优点:平衡实现难度与一致性
- 缺点:需要合理设置延迟时间
考虑到"黑马点评"这类中型应用的实际情况,我们选择双写+异常重试的方案:
java复制@Transactional
public void updateShop(Shop shop) {
// 1. 更新数据库
shopMapper.updateB
