SpringBoot AOP + Redis 实现延时双删实战:解决高并发下的缓存一致性难题
今天我们聊聊一个在高并发场景下经常遇到的问题——缓存与数据库一致性问题,以及如何用SpringBoot AOP + Redis实现延时双删来解决这个问题。
问题背景:缓存与数据库一致性
在高并发系统中,我们通常会引入缓存来提升系统的响应速度。但在数据更新时,经常会遇到缓存与数据库不一致的问题。
举个例子:当一个商品的价格发生变化时,如果只更新了数据库而没有清除缓存,那么用户在一段时间内看到的还是旧价格;如果先删除缓存再更新数据库,在高并发场景下可能会出现以下情况:
- 线程A删除缓存
- 线程B查询数据,发现缓存为空,从数据库读取旧数据并写入缓存
- 线程A更新数据库
- 最终缓存中仍然是旧数据
延时双删:一种巧妙的解决方案
延时双删(Delayed Double Delete)是一种解决缓存一致性问题的策略,其核心思想是在更新数据库后,先删除一次缓存,等待一段合适的时间后,再次删除缓存。
具体步骤如下:
- 删除缓存
- 更新数据库
- 延时一段时间(如500ms)
- 再次删除缓存
为什么要延时后再删除一次缓存呢?这是为了防止在第1步和第2步之间,有其他线程读取到旧数据并重新写入缓存的情况。通过延时再删除一次,可以确保这段时间内的脏数据被清除。
实现思路:AOP + 注解驱动
我们可以利用Spring AOP的特性,通过自定义注解的方式来实现延时双删,这样既不影响业务代码的逻辑,又能统一处理缓存一致性问题。
核心实现包含以下几个部分:
- 自定义@DelayDoubleDelete注解
- AOP切面拦截注解方法
- 异步延时删除缓存的逻辑
核心代码实现
1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DelayDoubleDelete {
String key(); // Redis中缓存的key
long delay() default 500L; // 延时时间
String prefix() default ""; // 缓存key的前缀
boolean enable() default true; // 是否启用延时双删
}
2. AOP切面实现
@Aspect
@Component
@Slf4j
public class DelayDoubleDeleteAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Around("@annotation(delayDoubleDelete)")
public Object around(ProceedingJoinPoint joinPoint, DelayDoubleDelete delayDoubleDelete) throws Throwable {
if (!delayDoubleDelete.enable()) {
return joinPoint.proceed();
}
String key = delayDoubleDelete.key();
String prefix = delayDoubleDelete.prefix();
long delay = delayDoubleDelete.delay();
// 构造完整的缓存key
String cacheKey = prefix.isEmpty() ? key : prefix + ":" + key;
try {
// 第一次删除缓存
stringRedisTemplate.delete(cacheKey);
log.info("第一次删除缓存: {}", cacheKey);
// 执行业务方法(更新数据库)
Object result = joinPoint.proceed();
// 延时后第二次删除缓存
scheduleSecondDelete(cacheKey, delay);
return result;
} catch (Exception e) {
log.error("延时双删处理异常", e);
throw e;
}
}
/**
* 异步调度第二次删除操作
*/
private void scheduleSecondDelete(String key, long delay) {
CompletableFuture.runAsync(() -> {
try {
// 等待指定延时时间
Thread.sleep(delay);
// 第二次删除缓存
stringRedisTemplate.delete(key);
log.info("第二次删除缓存: {}", key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("延时双删线程被中断", e);
} catch (Exception e) {
log.error("第二次删除缓存失败: {}", key, e);
}
});
}
}
3. 业务方法使用注解
@Service
public class ProductServiceImpl implements ProductService {
@Override
@DelayDoubleDelete(key = "'product:' + #product.id", delay = 500L, prefix = "")
public Product updateProduct(Product product) {
log.info("更新商品信息: {}", product.getId());
// 更新数据库
productDatabase.put(product.getId(), product);
// 返回更新后的商品信息
return product;
}
}
延时时间的选择
延时时间的选择是一个关键因素,需要考虑以下几点:
- 数据库主从同步延迟:如果使用主从架构,需要考虑主库到从库的同步时间
- 网络延迟:数据在网络传输中的延迟
- 业务场景:不同业务对一致性的要求不同
一般情况下,500ms是一个比较合适的初始值,后续可根据实际业务情况进行调整。
适用场景
延时双删适用于以下场景:
- 读多写少的业务场景
- 对数据一致性要求较高但可以容忍短暂不一致的场景
- 缓存穿透风险较高的场景
注意事项
- 延时双删并不是强一致性解决方案,在极端高并发情况下仍可能存在极小概率的数据不一致
- 延时时间需要根据实际业务场景进行调整
- 需要合理设置Redis连接池参数,避免连接泄露
- 在分布式环境下,需要考虑时钟同步问题
总结
通过SpringBoot AOP + Redis实现延时双删,是一种优雅解决缓存一致性问题的方式。它将缓存处理逻辑与业务逻辑分离,提高了代码的可维护性。当然,延时双删也有其局限性,需要根据具体的业务场景选择最适合的解决方案。
在实际应用中,我们还可以结合其他策略,如分布式锁、消息队列等,进一步提高系统的数据一致性保障。
服务端技术精选
标题:SpringBoot AOP + Redis 实现延时双删实战:解决高并发下的缓存一致性难题
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/02/15/1770968182714.html
评论
0 评论