使用 Spring Boot AOP + Redis 实现延时双删功能

时间:2025-03-13 17:12:01

1. 引言

在高并发环境下,数据库和 Redis 缓存的一致性是一个核心问题。通常的做法是 删除缓存后更新数据库,但如果在这两步之间有请求访问,就可能导致缓存穿透或脏数据。延时双删策略(First Delete -> Update DB -> Sleep -> Second Delete)是一种优化方案。

2. 延时双删原理

  1. 删除缓存: 先删除 Redis 中的缓存数据。
  2. 更新数据库: 执行数据库操作(新增、修改、删除)。
  3. 延时删除: 休眠一定时间(如 500ms),再次删除缓存,确保并发请求不会读到旧数据。

示意图:

+------------+      +------------+      +------------+      +------------+
|  Client    | ---> |  Delete    | ---> |  Update    | ---> |  Sleep     |
|  Request   |      |  Cache     |      |  Database  |      |  & Delete  |
+------------+      +------------+      +------------+      +------------+

3. 使用 Spring Boot AOP + Redis 实现

3.1 引入依赖

pom.xml 中添加 Redis 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3.2 配置 Redis 连接

application.yml 中配置 Redis:

spring:
  redis:
    host: localhost
    port: 6379
    timeout: 5000ms

3.3 业务逻辑 Service

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
public class ProductService {
    private final StringRedisTemplate redisTemplate;

    public ProductService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void updateProduct(Long productId, String newData) {
        // 第一次删除缓存
        redisTemplate.delete("product:" + productId);
        
        // 更新数据库(模拟)
        System.out.println("更新数据库中的数据: " + newData);

        // 延时双删
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
                redisTemplate.delete("product:" + productId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
}

3.4 AOP 切面自动管理

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class CacheEvictAspect {
    private final StringRedisTemplate redisTemplate;

    public CacheEvictAspect(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @AfterReturning("execution(* com.example.service.ProductService.updateProduct(..))")
    public void delayedCacheEviction() {
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
                redisTemplate.delete("product:" + 123L);
                System.out.println("二次删除缓存成功");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
}

4. 测试效果

调用 updateProduct(123L, "newData"):

更新数据库中的数据: newData
二次删除缓存成功

5. 总结

  • 为什么需要延时双删?
  • 避免数据库更新后,旧数据被并发请求读取。
  • 为什么使用 AOP?
  • 让业务逻辑更加清晰,减少代码侵入。
  • 延时双删的风险?
  • 延时时间要合理,过长影响性能,过短可能无法覆盖所有请求。

改进方案:

  • 使用 消息队列(如 Kafka/RabbitMQ)通知删除缓存。
  • 结合 布隆过滤器分布式锁 进一步优化。

希望这篇文章能帮到你!????