Redis缓存更新策略包括主动更新(如Cache-Aside先更新数据库再删缓存)、事件驱动更新和过期淘汰(惰性删除与定期删除结合)。内存淘汰是底层机制,用于内存不足时清理数据,非一致性策略。
选择合适的缓存更新策略,主要依据三个指标:业务场景是读多写少还是写多读少、对数据一致性的要求级别、性能压力的集中位置。优先保证数据一致性,再优化性能,这一顺序不可颠倒。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
这是最经典且应用最广泛的策略,核心逻辑为“先查缓存,再查数据库,更新时先改数据库再删缓存”。具体流程如下:
@Service
public class UserServiceCacheAside {
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
// 1. 查询缓存
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user; //缓存命中,直接返回
}
// 2. 缓存未命中,查询数据库
user = userMapper.selectById(id);
if (user != null) {
// 3. 将数据库结果写入缓存(设置过期时间)
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
}
return user;
}
public void updateUser(User user) {
// 1. 先更新数据库
userMapper.updateById(user);
// 2. 再删除缓存(而非更新缓存,避免并发问题)
String cacheKey = "user:" + user.getId();
redisTemplate.delete(cacheKey);
}
} 该策略的思路是“先更新缓存,再更新数据库”,读取时仅查询缓存,因为缓存中始终存储最新数据。
@Service
public class UserWriteThroughService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
/**
* 新增用户(Write Through:先写缓存,再写数据库)
* 加事务保证缓存和数据库要么都成功,要么都失败
*/
@Transactional(rollbackFor = Exception.class)
public void addUser(User user) {
// 1. 先写缓存(设置过期时间,兜底)
String cacheKey = "user:write_through:" + user.getId();
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
// 2. 同步写数据库(若数据库写入失败,事务回滚,缓存也会被删除)
int insertCount = userMapper.insertUser(user);
if (insertCount <= 0) {
// 数据库写入失败,主动删除缓存,避免脏数据
redisTemplate.delete(cacheKey);
throw new RuntimeException("新增用户到数据库失败");
}
}
/**
* 更新用户(Write Through:先更新缓存,再更新数据库)
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(User user) {
String cacheKey = "user:write_through:" + user.getId();
// 1. 先更新缓存(若缓存不存在,先查数据库再更新,保证缓存有数据)
User oldUser = (User) redisTemplate.opsForValue().get(cacheKey);
if (oldUser == null) {
oldUser = userMapper.selectUserById(user.getId());
if (oldUser == null) {
throw new RuntimeException("用户不存在,ID: " + user.getId());
}
}
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
// 2. 同步更新数据库
int updateCount = userMapper.updateUser(user);
if (updateCount <= 0) {
// 数据库更新失败,回滚缓存(恢复旧值)
redisTemplate.opsForValue().set(cacheKey, oldUser, 1, TimeUnit.HOURS);
throw new RuntimeException("更新用户到数据库失败,ID: " + user.getId());
}
}
/**
* 读取用户(Write Through:只查缓存,不查数据库)
*/
public User getUserById(Long userId) {
String cacheKey = "user:write_through:" + userId;
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user == null) {
// 理论上 Write Through 策略下缓存一定有数据,此处仅做异常兜底
user = userMapper.selectUserById(userId);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
}
}
return user;
}
} 该策略又称“延迟更新”,核心思想是更新时仅修改缓存,不立即写入数据库,待缓存过期或被淘汰时再批量同步到数据库。
/**
* 业务场景:用户点赞数(写多读少,允许短时间缓存与数据库不一致)
*/
@Service
public class LikeCountWriteBackService {
// Redis Key前缀:用户点赞数缓存
private static final String CACHE_LIKE_COUNT_KEY = "like:count:";
// Redis Key:脏数据标记(记录需要同步到数据库的用户ID)
private static final String DIRTY_DATA_SET_KEY = "like:dirty:user:ids";
// 缓存过期时间(兜底,避免脏数据永久不刷新)
private static final long CACHE_EXPIRE_TIME = 24 * 60 * 60;
@Resource
private RedisTemplate redisTemplate;
@Resource
private LikeCountMapper likeCountMapper;
/**
* 核心操作:更新用户点赞数(只更缓存,标记脏数据)
* @param userId 用户ID
* @param increment 增加的点赞数(正数)
*/
public void updateLikeCount(Long userId, int increment) {
String cacheKey = CACHE_LIKE_COUNT_KEY + userId;
try {
// 1. 原子更新Redis缓存中的点赞数(避免并发问题)
redisTemplate.opsForValue().increment(cacheKey, increment);
// 设置缓存过期时间(兜底)
redisTemplate.expire(cacheKey, CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
// 2. 将用户ID加入脏数据集(标记为需要同步到数据库)
// 使用ZSet存储,score为当前时间戳,便于后续按时间筛选
redisTemplate.opsForZSet().add(DIRTY_DATA_SET_KEY, userId, System.currentTimeMillis());
} catch (Exception e) {
// 异常时降级:直接更新数据库(避免数据丢失)
fallbackUpdateDb(userId, increment);
}
}
/**
* 读取用户点赞数(先查缓存,未命中查库并回填缓存)
* @param userId 用户ID
* @return 最新点赞数
*/
public Long getLikeCount(Long userId) {
String cacheKey = CACHE_LIKE_COUNT_KEY + userId;
// 1. 先查缓存
Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (cacheValue != null) {
return Long.parseLong(cacheValue.toString());
}
// 2. 缓存未命中:查数据库
Long dbCount = likeCountMapper.selectLikeCountByUserId(userId);
if (dbCount == null) {
dbCount = 0L;
}
// 3. 回填缓存(并标记为脏数据,避免后续同步时覆盖)
redisTemplate.opsForValue().set(cacheKey, dbCount);
redisTemplate.expire(cacheKey, CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
redisTemplate.opsForZSet().add(DIRTY_DATA_SET_KEY, userId, System.currentTimeMillis());
return dbCount;
}
/**
* 核心异步任务:定时将脏数据同步到数据库(Write Back核心)
* 定时规则:每5分钟执行一次(可根据业务调整)
*/
@Scheduled(cron = "0 */5 * * * ")
@Transactional(rollbackFor = Exception.class)
public void syncDirtyDataToDb() {
ZSetOperations zSetOps = redisTemplate.opsForZSet();
// 1. 批量获取脏数据集中的用户ID(最多取1000条,避免单次同步过多)
Set 该策略本质上是Cache-Aside的优化版本,主要解决“缓存过期瞬间的穿透”问题。
/**
* Refresh-Ahead(提前刷新)策略实现示例
* 核心逻辑:访问缓存时检查剩余过期时间,若小于阈值则异步刷新缓存,当前请求仍返回旧值
*/
@Service
public class RefreshAheadCacheService {
// 缓存过期时间(示例:30分钟)
private static final long CACHE_TTL_SECONDS = 30 * 60;
// Refresh-Ahead 触发阈值(过期时间剩余10%时触发,示例:3分钟)
private static final long REFRESH_THRESHOLD_SECONDS = CACHE_TTL_SECONDS / 10;
@Resource
private RedisTemplate redisTemplate;
@Resource
private ProductCategoryMapper productCategoryMapper;
/**
* 获取商品分类数据(核心Refresh-Ahead逻辑)
*/
public ProductCategory getCategoryWithRefreshAhead(Long categoryId) {
String cacheKey = "category:" + categoryId;
ValueOperations valueOps = redisTemplate.opsForValue();
// 1. 先查缓存
ProductCategory category = (ProductCategory) valueOps.get(cacheKey);
if (category == null) {
// 缓存未命中:查库 + 写入缓存(常规Cache Aside逻辑)
category = productCategoryMapper.selectById(categoryId);
if (category != null) {
redisTemplate.opsForValue().set(cacheKey, category, CACHE_TTL_SECONDS, TimeUnit.SECONDS);
}
return category;
}
// 2. 缓存命中:检查剩余过期时间,判断是否触发Refresh-Ahead
Long remainExpireSeconds = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
// 剩余时间小于阈值 且 缓存未过期(避免已过期的情况)
if (remainExpireSeconds != null && remainExpireSeconds > 0
&& remainExpireSeconds < REFRESH_THRESHOLD_SECONDS) {
// 3. 异步刷新缓存(不阻塞当前请求)
asyncRefreshCategoryCache(categoryId, cacheKey);
}
// 当前请求仍返回旧值,异步刷新不影响响应速度
return category;
}
/**
* 异步刷新缓存(核心:不阻塞主线程)
*/
@Async("refreshExecutor") // 指定自定义异步线程池(避免用默认线程池)
public void asyncRefreshCategoryCache(Long categoryId, String cacheKey) {
try {
// 1. 从数据库查询最新数据
ProductCategory latestCategory = productCategoryMapper.selectById(categoryId);
if (latestCategory != null) {
// 2. 重新设置缓存(覆盖旧值 + 重置过期时间)
redisTemplate.opsForValue().set(cacheKey, latestCategory, CACHE_TTL_SECONDS, TimeUnit.SECONDS);
}
} catch (Exception e) {
System.err.println("Refresh-Ahead刷新缓存失败:Key=" + cacheKey + ",原因:" + e.getMessage());
}
}
} 线程池配置:
@Configuration
@EnableAsync // 开启异步功能
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor refreshExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("cache-refresh-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}Read-Through可理解为Cache Aside的“封装版”或“框架版”。核心思想是将缓存读取逻辑封装起来,业务层只需关注业务本身,无需处理缓存操作。
简言之,将Cache Aside中的缓存逻辑抽取封装,供所有业务复用。
最终一致性策略基于分布式事件系统实现数据同步:
首先定义数据变更事件:
@Data
@AllArgsConstructor
public class DataChangeEvent {
private String entityType;
private String entityId;
private String operation; // CREATE, UPDATE, DELETE
private String payload; // JSON格式的实体数据
}实现事件发布者:
@Component
public class DataChangePublisher {
@Autowired
private KafkaTemplate kafkaTemplate;
private static final String TOPIC = "data-changes";
public void publishChange(String entityType, String entityId, String operation, Object entity) {
try {
// 将实体序列化为JSON
String payload = new ObjectMapper().writeValueAsString(entity);
// 创建事件
DataChangeEvent event = new DataChangeEvent(entityType, entityId, operation, payload);
// 发布到Kafka
kafkaTemplate.send(TOPIC, entityId, event);
} catch (Exception e) {
log.error("Failed to publish data change event", e);
throw new RuntimeException("Failed to publish event", e);
}
}
} 实现事件消费者更新缓存:
@Component
@Slf4j
public class CacheUpdateConsumer {
@Autowired
private RedisTemplate redisTemplate;
private static final long CACHE_EXPIRATION = 30;
@KafkaListener(topics = "data-changes")
public void handleDataChangeEvent(DataChangeEvent event) {
try {
String cacheKey = buildCacheKey(event.getEntityType(), event.getEntityId());
switch (event.getOperation()) {
case "CREATE":
case "UPDATE":
// 解析JSON数据
Object entity = parseEntity(event.getPayload(), event.getEntityType());
// 更新缓存
redisTemplate.opsForValue().set(
cacheKey, entity, CACHE_EXPIRATION, TimeUnit.MINUTES);
log.info("Updated cache for {}: {}", cacheKey, event.getOperation());
break;
case "DELETE":
// 删除缓存
redisTemplate.delete(cacheKey);
log.info("Deleted cache for {}", cacheKey);
break;
default:
log.warn("Unknown operation: {}", event.getOperation());
}
} catch (Exception e) {
log.error("Error handling data change event: {}", e.getMessage(), e);
// 失败处理:可以将失败事件放入死信队列等
}
}
private String buildCacheKey(String entityType, String entityId) {
return entityType.toLowerCase() + ":" + entityId;
}
private Object parseEntity(String payload, String entityType) throws JsonProcessingException {
// 根据实体类型选择反序列化目标类
Class> targetClass = getClassForEntityType(entityType);
return new ObjectMapper().readValue(payload, targetClass);
}
private Class> getClassForEntityType(String entityType) {
switch (entityType) {
case "User": return User.class;
case "Product": return Product.class;
// 其他实体类型
default: throw new IllegalArgumentException("Unknown entity type: " + entityType);
}
}
} 使用示例:
@Service
@Transactional
public class UserServiceEventDriven {
@Autowired
private UserRepository userRepository;
@Autowired
private DataChangePublisher publisher;
public User createUser(User user) {
// 1. 保存用户到数据库
User sa vedUser = userRepository.sa ve(user);
// 2. 发布创建事件
publisher.publishChange("User", sa vedUser.getId().toString(), "CREATE", sa vedUser);
return sa vedUser;
}
public User updateUser(User user) {
// 1. 更新用户到数据库
User updatedUser = userRepository.sa ve(user);
// 2. 发布更新事件
publisher.publishChange("User", updatedUser.getId().toString(), "UPDATE", updatedUser);
return updatedUser;
}
public void deleteUser(Long userId) {
// 1. 从数据库删除用户
userRepository.deleteById(userId);
// 2. 发布删除事件
publisher.publishChange("User", userId.toString(), "DELETE", null);
}
}简言之,过期淘汰是“策略目标”(让过期缓存被清理),惰性删除与定期删除是“技术手段”。
内存淘汰属于“兜底型缓存清理机制”,指Redis达到最大内存(maxmemory)时,按照预设规则(如LRU、LFU、随机等)自动淘汰部分缓存数据。本质上这是“内存管理手段”,而非“保证数据一致性的更新策略”。
maxmemory上限时,主动淘汰部分键,释放内存,保证Redis能继续接收新写入;volatile-lru:淘汰设置了过期时间的键中最近最少使用的;allkeys-lru:淘汰所有键中最近最少使用的;volatile-ttl:淘汰设置了过期时间的键中剩余过期时间最短的;noeviction(默认):不淘汰任何键,内存满时拒绝新写入并返回错误。总结而言:主动更新是“主动做事”,过期淘汰是“被动兜底做事”,内存淘汰是“实在没内存了才清理”。前两种属于缓存更新策略范畴,最后一种是底层机制。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述