SpringBoot AOP + Redis 实现延时双删实战:解决高并发下的缓存一致性难题

今天我们聊聊一个在高并发场景下经常遇到的问题——缓存与数据库一致性问题,以及如何用SpringBoot AOP + Redis实现延时双删来解决这个问题。

问题背景:缓存与数据库一致性

在高并发系统中,我们通常会引入缓存来提升系统的响应速度。但在数据更新时,经常会遇到缓存与数据库不一致的问题。

举个例子:当一个商品的价格发生变化时,如果只更新了数据库而没有清除缓存,那么用户在一段时间内看到的还是旧价格;如果先删除缓存再更新数据库,在高并发场景下可能会出现以下情况:

  1. 线程A删除缓存
  2. 线程B查询数据,发现缓存为空,从数据库读取旧数据并写入缓存
  3. 线程A更新数据库
  4. 最终缓存中仍然是旧数据

延时双删:一种巧妙的解决方案

延时双删(Delayed Double Delete)是一种解决缓存一致性问题的策略,其核心思想是在更新数据库后,先删除一次缓存,等待一段合适的时间后,再次删除缓存。

具体步骤如下:

  1. 删除缓存
  2. 更新数据库
  3. 延时一段时间(如500ms)
  4. 再次删除缓存

为什么要延时后再删除一次缓存呢?这是为了防止在第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是一个比较合适的初始值,后续可根据实际业务情况进行调整。

适用场景

延时双删适用于以下场景:

  • 读多写少的业务场景
  • 对数据一致性要求较高但可以容忍短暂不一致的场景
  • 缓存穿透风险较高的场景

注意事项

  1. 延时双删并不是强一致性解决方案,在极端高并发情况下仍可能存在极小概率的数据不一致
  2. 延时时间需要根据实际业务场景进行调整
  3. 需要合理设置Redis连接池参数,避免连接泄露
  4. 在分布式环境下,需要考虑时钟同步问题

总结

通过SpringBoot AOP + Redis实现延时双删,是一种优雅解决缓存一致性问题的方式。它将缓存处理逻辑与业务逻辑分离,提高了代码的可维护性。当然,延时双删也有其局限性,需要根据具体的业务场景选择最适合的解决方案。

在实际应用中,我们还可以结合其他策略,如分布式锁、消息队列等,进一步提高系统的数据一致性保障。


服务端技术精选


标题:SpringBoot AOP + Redis 实现延时双删实战:解决高并发下的缓存一致性难题
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/02/15/1770968182714.html

    评论
    0 评论
avatar

取消