引言
redis作为一款高性能的内存数据库,已经成为缓存层的首选解决方案。然而,使用缓存时最大的挑战在于保证缓存数据与底层数据源的一致性。缓存更新策略直接影响系统的性能、可靠性和数据一致性,选择合适的策略至关重要。
本文将介绍redis中6种缓存更新策略。
策略一:cache-aside(旁路缓存)策略
工作原理
cache-aside是最常用的缓存模式,由应用层负责缓存和数据库的交互逻辑:
- 读取数据:先查询缓存,命中则直接返回;未命中则查询数据库,将结果写入缓存并返回
- 更新数据:先更新数据库,再删除缓存(或更新缓存)
代码示例
@service
public class userservicecacheaside {
@autowired
private redistemplate<string, user> redistemplate;
@autowired
private userrepository userrepository;
private static final string cache_key_prefix = "user:";
private static final long cache_expiration = 30; // 缓存过期时间(分钟)
public user getuserbyid(long userid) {
string cachekey = cache_key_prefix + userid;
// 1. 查询缓存
user user = redistemplate.opsforvalue().get(cachekey);
// 2. 缓存命中,直接返回
if (user != null) {
return user;
}
// 3. 缓存未命中,查询数据库
user = userrepository.findbyid(userid).orelse(null);
// 4. 将数据库结果写入缓存(设置过期时间)
if (user != null) {
redistemplate.opsforvalue().set(cachekey, user, cache_expiration, timeunit.minutes);
}
return user;
}
public void updateuser(user user) {
// 1. 先更新数据库
userrepository.save(user);
// 2. 再删除缓存
string cachekey = cache_key_prefix + user.getid();
redistemplate.delete(cachekey);
// 或者选择更新缓存
// redistemplate.opsforvalue().set(cachekey, user, cache_expiration, timeunit.minutes);
}
}
优缺点分析
优点
- 实现简单,控制灵活
- 适合读多写少的业务场景
- 只缓存必要的数据,节省内存空间
缺点
- 首次访问会有一定延迟(缓存未命中)
- 存在并发问题:如果先删除缓存后更新数据库,可能导致数据不一致
- 需要应用代码维护缓存一致性,增加了开发复杂度
适用场景
- 读多写少的业务场景
- 对数据一致性要求不是特别高的应用
- 分布式系统中需要灵活控制缓存策略的场景
策略二:read-through(读穿透)策略
工作原理
read-through策略将缓存作为主要数据源的代理,由缓存层负责数据加载:
- 应用程序只与缓存层交互
- 当缓存未命中时,由缓存管理器负责从数据库加载数据并存入缓存
- 应用程序无需关心缓存是否存在,缓存层自动处理加载逻辑
代码示例
首先定义缓存加载器接口:
public interface cacheloader<k, v> {
v load(k key);
}
实现read-through缓存管理器:
@component
public class readthroughcachemanager<k, v> {
@autowired
private redistemplate<string, v> redistemplate;
private final concurrenthashmap<string, cacheloader<k, v>> loaders = new concurrenthashmap<>();
public void registerloader(string cacheprefix, cacheloader<k, v> loader) {
loaders.put(cacheprefix, loader);
}
public v get(string cacheprefix, k key, long expiration, timeunit timeunit) {
string cachekey = cacheprefix + key;
// 1. 查询缓存
v value = redistemplate.opsforvalue().get(cachekey);
// 2. 缓存命中,直接返回
if (value != null) {
return value;
}
// 3. 缓存未命中,通过加载器获取数据
cacheloader<k, v> loader = loaders.get(cacheprefix);
if (loader == null) {
throw new illegalstateexception("no cache loader registered for prefix: " + cacheprefix);
}
// 使用加载器从数据源加载数据
value = loader.load(key);
// 4. 将加载的数据存入缓存
if (value != null) {
redistemplate.opsforvalue().set(cachekey, value, expiration, timeunit);
}
return value;
}
}
使用示例:
@service
public class userservicereadthrough {
private static final string cache_prefix = "user:";
private static final long cache_expiration = 30;
@autowired
private readthroughcachemanager<long, user> cachemanager;
@autowired
private userrepository userrepository;
@postconstruct
public void init() {
// 注册用户数据加载器
cachemanager.registerloader(cache_prefix, this::loaduserfromdb);
}
private user loaduserfromdb(long userid) {
return userrepository.findbyid(userid).orelse(null);
}
public user getuserbyid(long userid) {
// 直接通过缓存管理器获取数据,缓存逻辑由管理器处理
return cachemanager.get(cache_prefix, userid, cache_expiration, timeunit.minutes);
}
}
优缺点分析
优点
- 封装性好,应用代码无需关心缓存逻辑
- 集中处理缓存加载,减少冗余代码
- 适合只读或读多写少的数据
缺点
- 缓存未命中时引发数据库请求,可能导致数据库负载增加
- 无法直接处理写操作,需要与其他策略结合使用
- 需要额外维护一个缓存管理层
适用场景
- 读操作频繁的业务系统
- 需要集中管理缓存加载逻辑的应用
- 复杂的缓存预热和加载场景
策略三:write-through(写穿透)策略
工作原理
write-through策略由缓存层同步更新底层数据源:
- 应用程序更新数据时先写入缓存
- 然后由缓存层负责同步写入数据库
- 只有当数据成功写入数据库后才视为更新成功
代码示例
首先定义写入接口:
public interface cachewriter<k, v> {
void write(k key, v value);
}
实现write-through缓存管理器:
@component
public class writethroughcachemanager<k, v> {
@autowired
private redistemplate<string, v> redistemplate;
private final concurrenthashmap<string, cachewriter<k, v>> writers = new concurrenthashmap<>();
public void registerwriter(string cacheprefix, cachewriter<k, v> writer) {
writers.put(cacheprefix, writer);
}
public void put(string cacheprefix, k key, v value, long expiration, timeunit timeunit) {
string cachekey = cacheprefix + key;
// 1. 获取对应的缓存写入器
cachewriter<k, v> writer = writers.get(cacheprefix);
if (writer == null) {
throw new illegalstateexception("no cache writer registered for prefix: " + cacheprefix);
}
// 2. 同步写入数据库
writer.write(key, value);
// 3. 更新缓存
redistemplate.opsforvalue().set(cachekey, value, expiration, timeunit);
}
}
使用示例:
@service
public class userservicewritethrough {
private static final string cache_prefix = "user:";
private static final long cache_expiration = 30;
@autowired
private writethroughcachemanager<long, user> cachemanager;
@autowired
private userrepository userrepository;
@postconstruct
public void init() {
// 注册用户数据写入器
cachemanager.registerwriter(cache_prefix, this::saveusertodb);
}
private void saveusertodb(long userid, user user) {
userrepository.save(user);
}
public void updateuser(user user) {
// 通过缓存管理器更新数据,会同步更新数据库和缓存
cachemanager.put(cache_prefix, user.getid(), user, cache_expiration, timeunit.minutes);
}
}
优缺点分析
优点
- 保证数据库与缓存的强一致性
- 将缓存更新逻辑封装在缓存层,简化应用代码
- 读取缓存时命中率高,无需回源到数据库
缺点
- 实时写入数据库增加了写操作延迟
- 增加系统复杂度,需要处理事务一致性
- 对数据库写入压力大的场景可能成为性能瓶颈
适用场景
- 对数据一致性要求高的系统
- 写操作不是性能瓶颈的应用
- 需要保证缓存与数据库实时同步的场景
策略四:write-behind(写回)策略
工作原理
write-behind策略将写操作异步化处理:
- 应用程序更新数据时只更新缓存
- 缓存维护一个写入队列,将更新异步批量写入数据库
- 通过批量操作减轻数据库压力
代码示例
实现异步写入队列和处理器:
@component
public class writebehindcachemanager<k, v> {
@autowired
private redistemplate<string, v> redistemplate;
private final blockingqueue<cacheupdate<k, v>> updatequeue = new linkedblockingqueue<>();
private final concurrenthashmap<string, cachewriter<k, v>> writers = new concurrenthashmap<>();
public void registerwriter(string cacheprefix, cachewriter<k, v> writer) {
writers.put(cacheprefix, writer);
}
@postconstruct
public void init() {
// 启动异步写入线程
thread writerthread = new thread(this::processwritebehindqueue);
writerthread.setdaemon(true);
writerthread.start();
}
public void put(string cacheprefix, k key, v value, long expiration, timeunit timeunit) {
string cachekey = cacheprefix + key;
// 1. 更新缓存
redistemplate.opsforvalue().set(cachekey, value, expiration, timeunit);
// 2. 将更新放入队列,等待异步写入数据库
updatequeue.offer(new cacheupdate<>(cacheprefix, key, value));
}
private void processwritebehindqueue() {
list<cacheupdate<k, v>> batch = new arraylist<>(100);
while (true) {
try {
// 获取队列中的更新,最多等待100ms
cacheupdate<k, v> update = updatequeue.poll(100, timeunit.milliseconds);
if (update != null) {
batch.add(update);
}
// 继续收集队列中可用的更新,最多收集100个或等待200ms
updatequeue.drainto(batch, 100 - batch.size());
if (!batch.isempty()) {
// 按缓存前缀分组批量处理
map<string, list<cacheupdate<k, v>>> groupedupdates = batch.stream()
.collect(collectors.groupingby(cacheupdate::getcacheprefix));
for (map.entry<string, list<cacheupdate<k, v>>> entry : groupedupdates.entryset()) {
string cacheprefix = entry.getkey();
list<cacheupdate<k, v>> updates = entry.getvalue();
cachewriter<k, v> writer = writers.get(cacheprefix);
if (writer != null) {
// 批量写入数据库
for (cacheupdate<k, v> u : updates) {
try {
writer.write(u.getkey(), u.getvalue());
} catch (exception e) {
// 处理异常,可以重试或记录日志
log.error("failed to write-behind for key {}: {}", u.getkey(), e.getmessage());
}
}
}
}
batch.clear();
}
} catch (interruptedexception e) {
thread.currentthread().interrupt();
break;
} catch (exception e) {
log.error("error in write-behind process", e);
}
}
}
@data
@allargsconstructor
private static class cacheupdate<k, v> {
private string cacheprefix;
private k key;
private v value;
}
}
使用示例:
@service
public class userservicewritebehind {
private static final string cache_prefix = "user:";
private static final long cache_expiration = 30;
@autowired
private writebehindcachemanager<long, user> cachemanager;
@autowired
private userrepository userrepository;
@postconstruct
public void init() {
// 注册用户数据写入器
cachemanager.registerwriter(cache_prefix, this::saveusertodb);
}
private void saveusertodb(long userid, user user) {
userrepository.save(user);
}
public void updateuser(user user) {
// 更新仅写入缓存,异步写入数据库
cachemanager.put(cache_prefix, user.getid(), user, cache_expiration, timeunit.minutes);
}
}
优缺点分析
优点
- 显著提高写操作性能,减少响应延迟
- 通过批量操作减轻数据库压力
- 平滑处理写入峰值,提高系统吞吐量
缺点
- 存在数据一致性窗口期,不适合强一致性要求的场景
- 系统崩溃可能导致未写入的数据丢失
- 实现复杂,需要处理失败重试和冲突解决
适用场景
- 高并发写入场景,如日志记录、统计数据
- 对写操作延迟敏感但对一致性要求不高的应用
- 数据库写入是系统瓶颈的场景
策略五:刷新过期(refresh-ahead)策略
工作原理
refresh-ahead策略预测性地在缓存过期前进行更新:
- 缓存设置正常的过期时间
- 当访问接近过期的缓存项时,触发异步刷新
- 用户始终访问的是已缓存的数据,避免直接查询数据库的延迟
代码示例
@component
public class refreshaheadcachemanager<k, v> {
@autowired
private redistemplate<string, object> redistemplate;
@autowired
private threadpooltaskexecutor refreshexecutor;
private final concurrenthashmap<string, cacheloader<k, v>> loaders = new concurrenthashmap<>();
// 刷新阈值,当过期时间剩余不足阈值比例时触发刷新
private final double refreshthreshold = 0.75; // 75%
public void registerloader(string cacheprefix, cacheloader<k, v> loader) {
loaders.put(cacheprefix, loader);
}
@suppresswarnings("unchecked")
public v get(string cacheprefix, k key, long expiration, timeunit timeunit) {
string cachekey = cacheprefix + key;
// 1. 获取缓存项和其ttl
v value = (v) redistemplate.opsforvalue().get(cachekey);
long ttl = redistemplate.getexpire(cachekey, timeunit.milliseconds);
if (value != null) {
// 2. 如果缓存存在但接近过期,触发异步刷新
if (ttl != null && ttl > 0) {
long expirationms = timeunit.tomillis(expiration);
if (ttl < expirationms * (1 - refreshthreshold)) {
refreshasync(cacheprefix, key, cachekey, expiration, timeunit);
}
}
return value;
}
// 3. 缓存不存在,同步加载
return loadandcache(cacheprefix, key, cachekey, expiration, timeunit);
}
private void refreshasync(string cacheprefix, k key, string cachekey, long expiration, timeunit timeunit) {
refreshexecutor.execute(() -> {
try {
loadandcache(cacheprefix, key, cachekey, expiration, timeunit);
} catch (exception e) {
// 异步刷新失败,记录日志但不影响当前请求
log.error("failed to refresh cache for key {}: {}", cachekey, e.getmessage());
}
});
}
private v loadandcache(string cacheprefix, k key, string cachekey, long expiration, timeunit timeunit) {
cacheloader<k, v> loader = loaders.get(cacheprefix);
if (loader == null) {
throw new illegalstateexception("no cache loader registered for prefix: " + cacheprefix);
}
// 从数据源加载
v value = loader.load(key);
// 更新缓存
if (value != null) {
redistemplate.opsforvalue().set(cachekey, value, expiration, timeunit);
}
return value;
}
}
使用示例:
@service
public class productservicerefreshahead {
private static final string cache_prefix = "product:";
private static final long cache_expiration = 60; // 1小时
@autowired
private refreshaheadcachemanager<string, product> cachemanager;
@autowired
private productrepository productrepository;
@postconstruct
public void init() {
// 注册产品数据加载器
cachemanager.registerloader(cache_prefix, this::loadproductfromdb);
}
private product loadproductfromdb(string productid) {
return productrepository.findbyid(productid).orelse(null);
}
public product getproduct(string productid) {
return cachemanager.get(cache_prefix, productid, cache_expiration, timeunit.minutes);
}
}
线程池配置
@configuration
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;
}
}
优缺点分析
优点
- 用户始终访问缓存数据,避免因缓存过期导致的延迟
- 异步刷新减轻了数据库负载峰值
- 缓存命中率高,用户体验更好
缺点
- 实现复杂度高,需要额外的线程池管理
- 预测算法可能不准确,导致不必要的刷新
- 对于很少访问的数据,刷新可能是浪费
适用场景
- 对响应时间要求苛刻的高流量系统
- 数据更新频率可预测的场景
- 数据库资源有限但缓存容量充足的系统
策略六:最终一致性(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);
}
}
优缺点分析
优点
- 支持分布式系统中的数据一致性
- 削峰填谷,减轻系统负载峰值
- 服务解耦,提高系统弹性和可扩展性
缺点
- 一致性延迟,只能保证最终一致性
- 实现和维护更复杂,需要消息队列基础设施
- 可能需要处理消息重复和乱序问题
适用场景
- 大型分布式系统
- 可以接受短暂不一致的业务场景
- 需要解耦数据源和缓存更新逻辑的系统
缓存更新策略选择指南
选择合适的缓存更新策略需要考虑以下因素:
1. 业务特性考量
| 业务特征 | 推荐策略 |
|---|---|
| 读多写少 | cache-aside 或 read-through |
| 写密集型 | write-behind |
| 高一致性需求 | write-through |
| 响应时间敏感 | refresh-ahead |
| 分布式系统 | 最终一致性 |
2. 资源限制考量
| 资源约束 | 推荐策略 |
|---|---|
| 内存限制 | cache-aside(按需缓存) |
| 数据库负载高 | write-behind(减轻写压力) |
| 网络带宽受限 | write-behind 或 refresh-ahead |
3. 开发复杂度考量
| 复杂度要求 | 推荐策略 |
|---|---|
| 简单实现 | cache-aside |
| 中等复杂度 | read-through 或 write-through |
| 高复杂度但高性能 | write-behind 或 最终一致性 |
结论
缓存更新是redis应用设计中的核心挑战,没有万能的策略适用于所有场景。根据业务需求、数据特性和系统资源,选择合适的缓存更新策略或组合多种策略才是最佳实践。
在实际应用中,可以根据不同数据的特性选择不同的缓存策略,甚至在同一个系统中组合多种策略,以达到性能和一致性的最佳平衡。
以上就是redis中6种缓存更新策略详解的详细内容,更多关于redis缓存更新的资料请关注代码网其它相关文章!
发表评论