延迟双删是解决缓存与数据库一致性的常用方案,主要流程是:
先删除缓存
更新数据库
延迟一段时间后再次删除缓存
下面是基于 Spring Boot 3 + MyBatis Plus + Redis 的核心实现代码:
缓存服务类 CacheService
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 缓存服务类,封装Redis缓存的基本操作
*/
@Component
@RequiredArgsConstructor
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 从缓存获取数据
*
* @param key 缓存键
* @return 缓存值,如果不存在则返回null
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 向缓存设置数据
*
* @param key 缓存键
* @param value 缓存值
* @param timeout 过期时间
* @param unit 时间单位
*/
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 删除缓存
*
* @param key 缓存键
* @return 是否删除成功
*/
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
}
延迟双删服务类 DelayDoubleDeleteService
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 延迟双删服务类,提供延迟删除缓存的能力
*/
@Component
@RequiredArgsConstructor
public class DelayDoubleDeleteService {
private final CacheService cacheService;
private final ThreadPoolTaskScheduler taskScheduler;
/**
* 延迟删除缓存
*
* @param key 要删除的缓存键
* @param delay 延迟时间
* @param unit 时间单位
*/
public void delayDelete(String key, long delay, TimeUnit unit) {
taskScheduler.schedule(() -> {
// 执行第二次删除
boolean result = cacheService.delete(key);
// 可以根据需要记录日志
// log.info("延迟删除缓存 {},结果: {}", key, result);
}, System.currentTimeMillis() + unit.toMillis(delay));
}
}
Redis配置类 RedisConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类,配置RedisTemplate和相关序列化方式
* 确保Redis数据正确序列化和反序列化
*/
@Configuration
public class RedisConfig {
/**
* 配置RedisTemplate,设置key和value的序列化方式
*
* @param connectionFactory Redis连接工厂
* @return 配置好的RedisTemplate实例
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用StringRedisSerializer序列化key
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 使用Jackson2JsonRedisSerializer序列化value
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
线程池配置类 ThreadPoolConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置类,配置定时任务线程池
*/
@Configuration
public class ThreadPoolConfig {
/**
* 配置定时任务线程池,用于执行延迟删除操作
*
* @return 配置好的ThreadPoolTaskScheduler实例
*/
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 核心线程数
scheduler.setCorePoolSize(5);
// 最大线程数
scheduler.setMaxPoolSize(10);
// 队列容量
scheduler.setQueueCapacity(20);
// 线程名前缀
scheduler.setThreadNamePrefix("delay-delete-");
// 线程空闲时间
scheduler.setKeepAliveSeconds(60);
// 拒绝策略:当线程池和队列都满了,让提交任务的线程执行任务
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
scheduler.initialize();
return scheduler;
}
}
用户服务类 UserService
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* 用户服务类,实现用户相关业务逻辑,包含延迟双删缓存策略
*/
@Service
@RequiredArgsConstructor
public class UserService extends ServiceImpl<UserMapper, User> {
private final CacheService cacheService;
private final DelayDoubleDeleteService delayDoubleDeleteService;
/**
* 缓存键前缀
*/
private static final String CACHE_KEY_PREFIX = "user:";
/**
* 根据ID获取用户信息(先查缓存,再查数据库)
*
* @param id 用户ID
* @return 用户信息,如果不存在则返回null
*/
public User getUserById(Long id) {
// 1. 先查询缓存
String cacheKey = getCacheKey(id);
User user = (User) cacheService.get(cacheKey);
if (user != null) {
return user;
}
// 2. 缓存不存在,查询数据库
user = getById(id);
// 3. 将查询结果存入缓存,设置过期时间
if (user != null) {
cacheService.set(cacheKey, user, 30, TimeUnit.MINUTES);
}
return user;
}
/**
* 更新用户信息(实现延迟双删策略)
*
* @param user 要更新的用户信息
* @return 是否更新成功
*/
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(User user) {
if (user == null || user.getId() == null) {
return false;
}
String cacheKey = getCacheKey(user.getId());
// 1. 第一次删除缓存
cacheService.delete(cacheKey);
// 2. 更新数据库
boolean result = updateById(user);
// 3. 延迟一段时间后再次删除缓存(根据业务场景调整延迟时间)
// 这里设置500毫秒延迟,确保数据库更新完成后再删除可能存在的脏数据
delayDoubleDeleteService.delayDelete(cacheKey, 500, TimeUnit.MILLISECONDS);
return result;
}
/**
* 构建缓存键
*
* @param id 用户ID
* @return 缓存键字符串
*/
private String getCacheKey(Long id) {
return CACHE_KEY_PREFIX + id;
}
}
实现说明
核心原理:延迟双删通过两次删除缓存来保证数据一致性,第一次删除避免脏读,第二次删除解决可能存在的并发问题。
关键配置:
RedisTemplate 配置确保序列化正确
线程池配置用于执行延迟删除任务
缓存键设计采用前缀 + ID 的方式,便于管理
业务流程:
查询:先查缓存,缓存未命中再查数据库并更新缓存
更新:先删缓存,再更新数据库,最后延迟删缓存
延迟时间设置:
延迟时间应根据实际业务场景设置,通常为几百毫秒到几秒
确保能覆盖数据库事务提交的时间,避免二次删除时数据库更新尚未完成
如果文章对你有一点点帮助,欢迎【点赞、留言、+ 关注】,
您的关注是我持续创作的重要动力!有问题欢迎随时交流!多一个朋友多一条路!