1. 主动更新-4种核心缓存更新策略
核心原则:根据业务的 “读写比例”“一致性要求”“性能要求” 选择策略,优先保证数据一致性,其次优化性能。
1.1. cache-aside(旁路缓存)
这是最常用、最经典的策略,也叫 “先查缓存,再查数据库,更新时先更库再删缓存”。
- 读取数据:先查询缓存,命中则直接返回;未命中则查询数据库,将结果写入缓存并返回。
- 更新数据:先更新数据库,再删除缓存(而非更新缓存)。
@service
public class userservicecacheaside {
@resource
private usermapper usermapper;
@resource
private redistemplate<string, object> 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);
}
}- 优点
- 逻辑简单,易于实现;适合读多写少的业务场景;
- 避免 “更新缓存” 带来的并发数据不一致问题(比如两个线程同时更新,缓存可能存旧值)。
- 缺点
- 缓存未命中时会有 “缓存穿透” 的风险(可通过布隆过滤器解决);
- 数据库更新后、缓存删除前,若有读请求,可能读到旧值(概率极低,可接受)。
1.2. write-through(写穿透)
更新时 “先更缓存,再更数据库”,读取时只查缓存(缓存一定有最新数据)。
- 读取数据:直接从缓存读取,缓存必然命中(因为更新时同步写缓存);
- 更新数据:先更新缓存;再由缓存同步更新数据库(通常是缓存框架自动完成)。
@service
public class userwritethroughservice {
@autowired
private redistemplate<string, object> 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;
}
}- 优点
- 读取性能极高,无需访问数据库;数据一致性强,缓存与数据库同步更新。
- 缺点
- 写入性能低(每次写都要操作缓存 + 数据库);
- 数据库写入失败会导致缓存与数据库不一致(需加事务 / 重试)。
1.3. write-behind(写回)
也叫 “延迟更新”,更新时只更缓存,不立即更数据库,而是等缓存过期 / 淘汰时,再批量同步到数据库。
- 更新流程:更新缓存,并标记缓存为 “脏数据”;缓存过期 / 被淘汰时,异步将 “脏数据” 批量写入数据库。
- 读取流程:与 cache aside 一致(先查缓存,未命中查库)。
/**
* 业务场景:用户点赞数(写多读少,允许短时间缓存与数据库不一致)
*/
@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<string, object> 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<string, object> zsetops = redistemplate.opsforzset();
// 1. 批量获取脏数据集中的用户id(最多取1000条,避免单次同步过多)
set<object> dirtyuserids = zsetops.range(dirty_data_set_key, 0, 999);
if (dirtyuserids == null || dirtyuserids.isempty()) {
return;
}
// 2. 遍历脏数据,同步到数据库
list<long> failuserids = new arraylist<>(); // 记录同步失败的用户id
for (object useridobj : dirtyuserids) {
long userid = long.parselong(useridobj.tostring());
string cachekey = cache_like_count_key + userid;
try {
// 2.1 获取缓存中的最新点赞数
object cachecountobj = redistemplate.opsforvalue().get(cachekey);
if (cachecountobj == null) {
zsetops.remove(dirty_data_set_key, userid); // 移除脏数据标记
continue;
}
long cachecount = long.parselong(cachecountobj.tostring());
// 2.2 更新数据库
likecountmapper.updatelikecountbyuserid(userid, cachecount);
// 2.3 同步成功:移除脏数据标记
zsetops.remove(dirty_data_set_key, userid);
} catch (exception e) {
failuserids.add(userid); // 记录失败id,后续重试
}
}
// 3. 处理同步失败的用户id(简单重试:重新加入脏数据集)
if (!failuserids.isempty()) {
for (long failuserid : failuserids) {
zsetops.add(dirty_data_set_key, failuserid, system.currenttimemillis());
}
}
}
/**
* 降级策略:缓存更新失败时,直接更新数据库
*/
private void fallbackupdatedb(long userid, int increment) {
try {
long currentcount = likecountmapper.selectlikecountbyuserid(userid);
if (currentcount == null) {
currentcount = 0l;
}
likecountmapper.updatelikecountbyuserid(userid, currentcount + increment);
} catch (exception e) {
// 可进一步接入消息队列/告警,保证数据不丢失
}
}
}- 优点:
- 写入性能极高(只需操作缓存,数据库异步批量更新);
- 适合写多读少的场景(如计数器、点赞数)。
- 缺点
- 数据一致性差(缓存未同步到数据库时,服务宕机会丢失数据);
- 实现复杂(需处理脏数据标记、异步同步、数据恢复)。
1.4. 刷新过期(refresh-ahead)
本质是 cache-aside(旁路缓存)的优化 / 增强版
- 更新流程:与 cache aside 一致(先更新数据库,再删除缓存)。
- 读取流程:先查询缓存,未命中则查询数据库,将结果写入缓存并返回;命中则检查缓存剩余过期时间,若剩余过期时间 ≥ 阈值:直接返回缓存中的旧值;若剩余过期时间 < 阈值:异步触发缓存刷新(后台查数据库最新数据 → 重写缓存并重置 ttl),当前请求仍返回缓存旧值。
/**
* 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<string, object> redistemplate;
@resource
private productcategorymapper productcategorymapper;
/**
* 获取商品分类数据(核心refresh-ahead逻辑)
*/
public productcategory getcategorywithrefreshahead(long categoryid) {
string cachekey = "category:" + categoryid;
valueoperations<string, object> 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;
}
}- 优点
- 保证数据一致性;避免 “缓存穿透” 的风险。
- 仅在 “缓存快过期且被访问” 时刷新,比定时刷新更精准,避免无意义的全量刷新,节省资源;
- 缺点
- 需额外开发 “过期时间预判”“异步刷新”“线程池管理” 逻辑,增加开发和维护成本;
- 触发刷新后、异步更新完成前,客户端仍会读取到旧值(窗口极短,通常可接受);
- 异步线程池耗尽、数据库查询失败等,可能导致刷新失败,需增加重试 / 日志监控机制;
- 刷新阈值(如总 ttl 的 10%)设置不合理时:过大→频繁刷新浪费资源,过小→刷新完成前缓存已过期。
2. 3种补充策略
2.1. read-through(读穿透)
read-through是cache aside的 “封装版 / 框架版”,核心是封装缓存读取逻辑,让业务层聚焦业务而非缓存操作。
只需要将cache aside是缓存的逻辑封装,所有业务复用即可。
- 优点 :
- 封装性好,应用代码无需关心缓存逻辑
- 集中处理缓存加载,减少冗余代码
- 适合只读或读多写少的数据
- 缺点:
- 缓存未命中时引发数据库请求,可能导致数据库负载增加
- 无法直接处理写操作,需要与其他策略结合使用
- 需要额外维护一个缓存管理层
- 适用场景
- 读操作频繁的业务系统
- 需要集中管理缓存加载逻辑的应用
- 复杂的缓存预热和加载场景
2.2. 最终一致性(eventual consistency)
最终一致性策略基于分布式事件系统实现数据同步:
- 数据变更时发布事件到消息队列
- 缓存服务订阅相关事件并更新缓存
- 即使某些操作暂时失败,最终系统也会达到一致状态 首先定义数据变更事件:
@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<string, datachangeevent> 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<string, object> 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 saveduser = userrepository.save(user);
// 2. 发布创建事件
publisher.publishchange("user", saveduser.getid().tostring(), "create", saveduser);
return saveduser;
}
public user updateuser(user user) {
// 1. 更新用户到数据库
user updateduser = userrepository.save(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);
}
}- 优点 :
- 支持分布式系统中的数据一致性
- 削峰填谷,减轻系统负载峰值
- 服务解耦,提高系统弹性和可扩展性
- 缺点:
- 一致性延迟,只能保证最终一致性
- 实现和维护更复杂,需要消息队列基础设施
- 可能需要处理消息重复和乱序问题
- 适用场景
- 大型分布式系统
- 可以接受短暂不一致的业务场景
- 需要解耦数据源和缓存更新逻辑的系统
2.3. 过期淘汰(被动更新)
- 本质:依赖 redis 自身的过期策略(如 ttl 过期、lru 淘汰)被动更新缓存,配合核心策略使用(比如 cache aside 中给缓存设 ttl,到期自动淘汰旧数据)。
- 特点:不主动更新,而是 “被动清理旧数据”,是所有策略的基础保障(避免缓存永久有效)。
简单说:过期淘汰是 “策略目标”(让过期缓存被清理),惰性删除 + 定期删除是 “技术手段”。
2.3.1. 惰性删除(lazy delete)
- 逻辑:当用户访问某个 key 时,redis 先检查该键是否过期,若过期则立即删除,不返回值;
- 定位:过期淘汰的核心实现手段之一,被动触发,节省 cpu 资源(不用轮询所有 key)。
2.3.2. 定期删除(periodic delete)
- 逻辑:redis 会启动一个后台线程,每隔一段时间(默认 100ms) 随机抽取一部分过期 key 检查,删除其中已过期的;为了不阻塞主线程,每次检查的时间和数量都有限制。
- 定位:补充惰性删除的不足(避免过期 key 长期不被访问,一直占用内存),主动但轻量化。
2.3.3. 两者结合的原因
- 只靠惰性删除:过期 key 若长期不被访问,会一直占内存;
- 只靠定期删除:轮询所有 key 会消耗大量 cpu,影响性能;
- 结合使用:既保证了过期 key 最终会被清理(定期删除兜底),又避免了过度消耗 cpu(惰性删除减少检查),是 redis 平衡性能和内存的最优方案。
3. 内存淘汰
内存淘汰属于「兜底型缓存清理机制」,是指 redis 达到最大内存(maxmemory)时,按照预设规则(如 lru、lfu、随机等)自动淘汰部分缓存数据,本质是 “内存管理手段”,而非 “保证数据一致性的更新策略”。
- 典型行为:redis 内存占满后,淘汰最少使用的 key(lru 策略);
- 核心目标:当 redis 内存达到
maxmemory上限时,主动淘汰部分键,释放内存以保证 redis 能继续接收新写入; - 核心定位:目的是避免 redis 内存溢出,而非保证缓存与数据库的一致性 —— 淘汰的可能是最新的、也可能是旧的缓存数据,完全不考虑业务逻辑;
- 常见策略:
volatile-lru:淘汰设置了过期时间的键中,最近最少使用的;allkeys-lru:淘汰所有键中最近最少使用的;volatile-ttl:淘汰设置了过期时间的键中,剩余过期时间最短的;noeviction(默认):不淘汰任何键,内存满时拒绝新写入并返回错误;
- 是否属于:❌ 严格来说,不算 “业务层面的缓存更新策略”,而是 redis 底层的内存保护机制;但广义上可视为 “被动清理缓存的补充手段”。
简单记:主动更新是 “主动做事”,过期淘汰是 “被动兜底做事”,内存淘汰是 “实在没内存了才清理”,前两者属于缓存更新策略范畴,后者是底层机制。
到此这篇关于redis缓存更新策略的文章就介绍到这了,更多相关redis缓存更新策略内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论