1. 引言
在高并发环境下,数据库和 Redis 缓存的一致性是一个核心问题。通常的做法是 删除缓存后更新数据库,但如果在这两步之间有请求访问,就可能导致缓存穿透或脏数据。延时双删策略(First Delete -> Update DB -> Sleep -> Second Delete)是一种优化方案。
2. 延时双删原理
- 删除缓存: 先删除 Redis 中的缓存数据。
- 更新数据库: 执行数据库操作(新增、修改、删除)。
- 延时删除: 休眠一定时间(如 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)通知删除缓存。
- 结合 布隆过滤器 或 分布式锁 进一步优化。
希望这篇文章能帮到你!????