场景设定
假设你是一家24小时超市的店长,货架上的商品就是缓存数据,仓库是数据库。你要保证:
- 顾客随时能拿到商品(快速响应)
- 过期商品及时下架(数据更新)
- 补货时不影响顾客购物(避免数据库压力)
核心流程分步解析
第一步:商品包装(数据结构设计)
每个商品包装盒上有两个标签:
- 商品本身(真实数据)
- 建议补货时间(逻辑过期时间)
|---------------------|
| iPhone 15 |
| 生产日期:5月1日 |
| 建议补货:5月30日 | ← 这个就是逻辑过期时间
|---------------------|
第二步:顾客购物(读取流程)
- 顾客拿起商品(读取缓存)
- 检查补货时间:
- 如果没过期 → 直接购买(返回数据)
- 如果已过期 →
a. 顾客先拿走旧商品(返回旧数据)
b. 悄悄通知店员补货(异步更新)
c. 其他顾客不受影响(避免集体挤仓库)
第三步:店员补货(更新流程)
- 收到补货通知(异步线程触发)
- 锁住货架(分布式锁)
- 防止多个店员同时补货(并发更新)
- 去仓库拿新货(查数据库)
- 更换商品+更新补货时间(更新缓存)
- 解锁货架
关键设计要点
1. 为什么用逻辑过期?
- 物理过期:就像每天定点清空货架,时间一到所有顾客都得等补货
- 逻辑过期:允许顾客先拿旧商品,后台悄悄换新货,购物体验更流畅
2. 如何防止多人同时补货?
- 货架锁:只有一个店员能操作货架(Redisson分布式锁)
- 锁的超时:设置30秒自动解锁(防止死锁)
3. 极端情况处理
- 补货失败:保留旧商品,但贴个纸条"此商品可能过时"(记录日志)
- 时间错乱:所有店员用同一个钟表(Redis服务器时间)
对比传统方案
场景 | 传统方案(物理过期) | 逻辑过期方案 |
---|---|---|
缓存突然失效 | 所有顾客堵在仓库门口 | 顾客先拿旧商品,店员悄悄补货 |
补货期间 | 货架空空,顾客抱怨 | 购物流程不受影响 |
店员工作量 | 高峰期要疯狂补货 | 闲时慢慢补货就行 |
一句话总结逻辑过期
"用旧数据顶住流量洪峰,后台偷偷换新数据"
就像饭店在用餐高峰期:
- 先给客人上昨天的靓汤(旧缓存)
- 后厨赶紧熬新汤(异步更新)
- 新汤熬好立刻替换(不影响正在吃饭的客人)
一、核心数据结构设计
1.1 Key设计规范
// 统一前缀:业务模块:主键
String key = "product:" + productId; // 示例:product:123
1.2 Value数据结构
// 逻辑过期包装类
public class LogicExpireWrapper<T> implements Serializable {
private T data; // 业务数据
private Long expireAt; // 逻辑过期时间戳(秒级)
// 示例构造方法
public LogicExpireWrapper(T data, int expireSeconds) {
this.data = data;
this.expireAt = System.currentTimeMillis() / 1000 + expireSeconds;
}
// 检查是否过期
public boolean isExpired() {
return System.currentTimeMillis() / 1000 > expireAt;
}
}
1.3 Redis存储示例
{
"data": {
"id": 123,
"name": "iPhone 15",
"price": 6999
},
"expireAt": 1717027200 // 2024-05-30 00:00:00
}
二、Spring Boot集成实现
2.1 配置RedisTemplate
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用JSON序列化
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
return template;
}
}
2.2 核心服务实现
@Service
@Slf4j
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redisson;
// 逻辑过期时间(30分钟)
private static final int LOGIC_EXPIRE_SECONDS = 1800;
// 线程池用于异步更新
private final ExecutorService refreshExecutor =
Executors.newFixedThreadPool(5);
public Product getProduct(Long productId) {
String key = "product:" + productId;
// 1. 查询Redis
LogicExpireWrapper<Product> wrapper =
(LogicExpireWrapper<Product>) redisTemplate.opsForValue().get(key);
// 2. 缓存不存在
if (wrapper == null) {
return loadAndSetProduct(productId, key);
}
// 3. 检查逻辑过期
if (wrapper.isExpired()) {
// 异步刷新
refreshExecutor.execute(() -> refreshProduct(key, productId));
}
return wrapper.getData();
}
private Product loadAndSetProduct(Long productId, String key) {
RLock lock = redisson.getLock(key + ":lock");
try {
// 尝试获取锁(等待1秒,持有30秒)
if (lock.tryLock(1, 30, TimeUnit.SECONDS)) {
// 二次检查
LogicExpireWrapper<Product> existWrapper =
(LogicExpireWrapper<Product>) redisTemplate.opsForValue().get(key);
if (existWrapper != null) return existWrapper.getData();
// 数据库查询
Product product = productDao.findById(productId);
// 设置逻辑过期
LogicExpireWrapper<Product> newWrapper =
new LogicExpireWrapper<>(product, LOGIC_EXPIRE_SECONDS);
redisTemplate.opsForValue().set(key, newWrapper);
return product;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return productDao.findById(productId); // 降级方案
}
private void refreshProduct(String key, Long productId) {
RLock lock = redisson.getLock(key + ":lock");
try {
if (lock.tryLock()) {
Product product = productDao.findById(productId);
LogicExpireWrapper<Product> newWrapper =
new LogicExpireWrapper<>(product, LOGIC_EXPIRE_SECONDS);
redisTemplate.opsForValue().set(key, newWrapper);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
三、方案优势分析
3.1 核心优势
graph TD
A[高可用性] --> B[始终快速响应]
A --> C[避免缓存击穿]
D[灵活性] --> E[动态调整过期时间]
D --> F[差异化过期策略]
G[资源优化] --> H[减少DB压力]
G --> I[避免大量重建]
3.2 适用场景
- 读多写少的热点数据(如商品详情)
- 允许短暂数据延迟(如资讯类内容)
- 重建成本高的复杂查询(涉及多表关联)
四、潜在问题与解决方案
4.1 数据不一致窗口
现象:异步更新完成前返回旧数据
优化方案:
// 在Wrapper中添加版本号
public class LogicExpireWrapper<T> {
private Long version; // 数据版本号
// ...
}
// 前端携带版本号请求
public Product getProduct(Long productId, Long clientVersion) {
LogicExpireWrapper<Product> wrapper = getFromRedis(productId);
if (wrapper.getVersion() > clientVersion) {
return wrapper.getData();
} else {
return loadFromDB(productId);
}
}
4.2 时钟同步问题
解决方案:
// 使用Redis服务器时间
Long currentTime = redisTemplate.execute(
(RedisCallback<Long>) connection -> connection.time()
);
4.3 缓存污染风险
防御措施:
// 在Wrapper中添加创建时间
private Long createTime = System.currentTimeMillis();
// 定期清理陈旧数据
@Scheduled(fixedRate = 3600000)
public void cleanExpiredData() {
// SCAN遍历所有key
Cursor<String> cursor = redisTemplate.scan(
ScanOptions.scanOptions().match("product:*").build()
);
while (cursor.hasNext()) {
String key = cursor.next();
LogicExpireWrapper wrapper =
(LogicExpireWrapper) redisTemplate.opsForValue().get(key);
if (wrapper.isExpired() &&
System.currentTimeMillis() - wrapper.getCreateTime() > 86400000) {
redisTemplate.delete(key);
}
}
}
五、方案对比与选型建议
维度 | 逻辑过期方案 | 物理过期方案 |
---|---|---|
实现复杂度 | 高(需维护逻辑时间) | 低(依赖Redis TTL) |
数据一致性 | 最终一致性 | 强一致性 |
性能影响 | 异步更新对主流程无影响 | 缓存穿透时影响明显 |
内存占用 | 需要存储额外字段 | 仅存储业务数据 |
适用场景 | 高并发读场景 | 常规业务场景 |
六、生产环境最佳实践
6.1 监控指标配置
# Prometheus监控配置示例
metrics:
redis:
hit_rate:
type: gauge
description: "缓存命中率"
expire_ratio:
type: counter
description: "逻辑过期触发次数"
6.2 动态参数调整
// 通过配置中心动态修改
@RefreshScope
@Configuration
public class ExpireConfig {
@Value("${cache.logic.expire:1800}")
private int logicExpireSeconds;
// Getter/Setter
}
6.3 降级策略
// 当异步更新失败时
private void refreshProduct(String key, Long productId) {
try {
// ...原有逻辑
} catch (Exception e) {
// 设置短期物理过期兜底
redisTemplate.expire(key, 60, TimeUnit.SECONDS);
}
}
结语:优雅与风险的平衡
逻辑过期方案就像走钢丝——在性能与一致性之间寻找平衡点。建议:
- 核心业务配合版本号校验
- 非核心业务允许一定延迟
- 必须配套完善的监控体系
记住:没有完美的方案,只有适合场景的解决方案。通过合理设计,逻辑过期可以成为高并发系统的利器,但永远要为最坏情况准备好降级策略。