15人参与 • 2025-05-07 • Redis
redis作为当今最流行的内存数据库和缓存系统,被广泛应用于各类应用场景。然而,即使redis本身性能卓越,在高并发场景下,应用与redis服务器之间的网络通信仍可能成为性能瓶颈。
这时,客户端缓存技术便显得尤为重要。
客户端缓存是指在应用程序内存中维护一份redis数据的本地副本,以减少网络请求次数,降低延迟,并减轻redis服务器负担。
本文将分享redis客户端缓存的四种实现方式,分析其原理、优缺点、适用场景及最佳实践.
本地内存缓存是最直接的客户端缓存实现方式,它在应用程序内存中使用数据结构(如hashmap、concurrenthashmap或专业缓存库如caffeine、guava cache等)存储从redis获取的数据。这种方式完全由应用程序自己管理,与redis服务器无关。
以下是使用spring boot和caffeine实现的简单本地缓存示例:
@service public class redislocalcacheservice { private final stringredistemplate redistemplate; private final cache<string, string> localcache; public redislocalcacheservice(stringredistemplate redistemplate) { this.redistemplate = redistemplate; // 配置caffeine缓存 this.localcache = caffeine.newbuilder() .maximumsize(10_000) // 最大缓存条目数 .expireafterwrite(duration.ofminutes(5)) // 写入后过期时间 .recordstats() // 记录统计信息 .build(); } public string get(string key) { // 首先尝试从本地缓存获取 string value = localcache.getifpresent(key); if (value != null) { // 本地缓存命中 return value; } // 本地缓存未命中,从redis获取 value = redistemplate.opsforvalue().get(key); if (value != null) { // 将从redis获取的值放入本地缓存 localcache.put(key, value); } return value; } public void set(string key, string value) { // 更新redis redistemplate.opsforvalue().set(key, value); // 更新本地缓存 localcache.put(key, value); } public void delete(string key) { // 从redis中删除 redistemplate.delete(key); // 从本地缓存中删除 localcache.invalidate(key); } // 获取缓存统计信息 public map<string, object> getcachestats() { cachestats stats = localcache.stats(); map<string, object> statsmap = new hashmap<>(); statsmap.put("hitcount", stats.hitcount()); statsmap.put("misscount", stats.misscount()); statsmap.put("hitrate", stats.hitrate()); statsmap.put("evictioncount", stats.evictioncount()); return statsmap; } }
优点
缺点
redis 6.0引入了服务器辅助的客户端缓存功能,也称为跟踪模式(tracking)。
在这种模式下,redis服务器会跟踪客户端请求的键,当这些键被修改时,服务器会向客户端发送失效通知。这种机制确保了客户端缓存与redis服务器之间的数据一致性。
redis提供了两种跟踪模式:
使用lettuce(spring boot redis的默认客户端)实现服务器辅助的客户端缓存:
@service public class redistrackingcacheservice { private final statefulredisconnection<string, string> connection; private final rediscommands<string, string> commands; private final map<string, string> localcache = new concurrenthashmap<>(); private final set<string> trackedkeys = concurrenthashmap.newkeyset(); public redistrackingcacheservice(redisclient redisclient) { this.connection = redisclient.connect(); this.commands = connection.sync(); // 配置客户端缓存失效监听器 connection.addlistener(message -> { if (message instanceof pushmessage) { pushmessage pushmessage = (pushmessage) message; if ("invalidate".equals(pushmessage.gettype())) { list<object> invalidations = pushmessage.getcontent(); handleinvalidations(invalidations); } } }); // 启用客户端缓存跟踪 commands.clienttracking(clienttrackingargs.builder.enabled()); } public string get(string key) { // 首先尝试从本地缓存获取 string value = localcache.get(key); if (value != null) { return value; } // 本地缓存未命中,从redis获取 value = commands.get(key); if (value != null) { // 启用跟踪后,redis服务器会记录这个客户端正在跟踪这个键 localcache.put(key, value); trackedkeys.add(key); } return value; } public void set(string key, string value) { // 更新redis commands.set(key, value); // 更新本地缓存 localcache.put(key, value); trackedkeys.add(key); } private void handleinvalidations(list<object> invalidations) { if (invalidations != null && invalidations.size() >= 2) { // 解析失效消息 string invalidationtype = new string((byte[]) invalidations.get(0)); if ("key".equals(invalidationtype)) { // 单个键失效 string invalidatedkey = new string((byte[]) invalidations.get(1)); localcache.remove(invalidatedkey); trackedkeys.remove(invalidatedkey); } else if ("prefix".equals(invalidationtype)) { // 前缀失效 string prefix = new string((byte[]) invalidations.get(1)); iterator<map.entry<string, string>> it = localcache.entryset().iterator(); while (it.hasnext()) { string key = it.next().getkey(); if (key.startswith(prefix)) { it.remove(); trackedkeys.remove(key); } } } } } // 获取缓存统计信息 public map<string, object> getcachestats() { map<string, object> stats = new hashmap<>(); stats.put("cachesize", localcache.size()); stats.put("trackedkeys", trackedkeys.size()); return stats; } // 清除本地缓存但保持跟踪 public void clearlocalcache() { localcache.clear(); } // 关闭连接并清理资源 @predestroy public void cleanup() { if (connection != null) { connection.close(); } } }
优点
缺点
选择合适的跟踪模式:
使用前缀跟踪:按键前缀组织数据并跟踪,减少跟踪开销
合理设置redirect参数:在多个客户端共享跟踪连接时
主动重连策略:连接断开后尽快重建连接和缓存
设置合理的本地缓存大小:避免过度占用应用内存
基于过期时间(time-to-live,ttl)的缓存失效策略是一种简单有效的客户端缓存方案。
它为本地缓存中的每个条目设置一个过期时间,过期后自动删除或刷新。
这种方式不依赖服务器通知,而是通过预设的时间窗口来控制缓存的新鲜度,平衡了数据一致性和系统复杂度。
使用spring cache和caffeine实现ttl缓存:
@configuration public class cacheconfig { @bean public cachemanager cachemanager() { caffeinecachemanager cachemanager = new caffeinecachemanager(); cachemanager.setcaffeinespec(caffeinespec.parse( "maximumsize=10000,expireafterwrite=300s,recordstats")); return cachemanager; } } @service public class redisttlcacheservice { private final stringredistemplate redistemplate; @autowired public redisttlcacheservice(stringredistemplate redistemplate) { this.redistemplate = redistemplate; } @cacheable(value = "rediscache", key = "#key") public string get(string key) { return redistemplate.opsforvalue().get(key); } @cacheput(value = "rediscache", key = "#key") public string set(string key, string value) { redistemplate.opsforvalue().set(key, value); return value; } @cacheevict(value = "rediscache", key = "#key") public void delete(string key) { redistemplate.delete(key); } // 分层缓存 - 不同过期时间的缓存 @cacheable(value = "shorttermcache", key = "#key") public string getwithshortttl(string key) { return redistemplate.opsforvalue().get(key); } @cacheable(value = "longtermcache", key = "#key") public string getwithlongttl(string key) { return redistemplate.opsforvalue().get(key); } // 在程序逻辑中手动控制过期时间 public string getwithdynamicttl(string key, duration ttl) { // 使用loadingcache,可以动态设置过期时间 cache<string, string> dynamiccache = caffeine.newbuilder() .expireafterwrite(ttl) .build(); return dynamiccache.get(key, k -> redistemplate.opsforvalue().get(k)); } // 定期刷新缓存 @scheduled(fixedrate = 60000) // 每分钟执行 public void refreshcache() { // 获取需要刷新的键列表 list<string> keystorefresh = getkeystorefresh(); for (string key : keystorefresh) { // 触发重新加载,会调用被@cacheable注解的方法 this.get(key); } } private list<string> getkeystorefresh() { // 实际应用中,可能从配置系统或特定的redis set中获取 return arrays.aslist("config:app", "config:features", "daily:stats"); } // 使用二级缓存模式,对热点数据使用更长的ttl public string getwithtwolevelcache(string key) { // 首先查询本地一级缓存(短ttl) cache<string, string> l1cache = caffeine.newbuilder() .maximumsize(1000) .expireafterwrite(duration.ofseconds(10)) .build(); string value = l1cache.getifpresent(key); if (value != null) { return value; } // 查询本地二级缓存(长ttl) cache<string, string> l2cache = caffeine.newbuilder() .maximumsize(10000) .expireafterwrite(duration.ofminutes(5)) .build(); value = l2cache.getifpresent(key); if (value != null) { // 提升到一级缓存 l1cache.put(key, value); return value; } // 查询redis value = redistemplate.opsforvalue().get(key); if (value != null) { // 更新两级缓存 l1cache.put(key, value); l2cache.put(key, value); } return value; } }
优点
缺点
基于数据特性设置不同ttl:
添加随机因子:ttl加上随机偏移量,避免缓存同时过期
实现缓存预热机制:应用启动时主动加载热点数据
结合后台刷新:对关键数据使用定时任务在过期前主动刷新
监控缓存效率:跟踪命中率、过期率等指标,动态调整ttl策略
基于发布/订阅(pub/sub)的缓存失效通知利用redis的发布/订阅功能来协调分布式系统中的缓存一致性。
当数据发生变更时,应用程序通过redis发布一条失效消息到特定频道,所有订阅该频道的客户端收到消息后清除对应的本地缓存。
这种方式实现了主动的缓存失效通知,而不依赖于redis 6.0以上版本的跟踪功能。
@service public class redispubsubcacheservice { private final stringredistemplate redistemplate; private final map<string, string> localcache = new concurrenthashmap<>(); @autowired public redispubsubcacheservice(stringredistemplate redistemplate) { this.redistemplate = redistemplate; // 订阅缓存失效通知 subscribetoinvalidations(); } private void subscribetoinvalidations() { // 使用独立的redis连接订阅缓存失效通知 redisconnectionfactory connectionfactory = redistemplate.getconnectionfactory(); if (connectionfactory != null) { // 创建消息监听容器 redismessagelistenercontainer container = new redismessagelistenercontainer(); container.setconnectionfactory(connectionfactory); // 消息监听器,处理缓存失效通知 messagelistener invalidationlistener = (message, pattern) -> { string invalidationmessage = new string(message.getbody()); handlecacheinvalidation(invalidationmessage); }; // 订阅缓存失效通知频道 container.addmessagelistener(invalidationlistener, new patterntopic("cache:invalidations")); container.start(); } } private void handlecacheinvalidation(string invalidationmessage) { try { // 解析失效消息 map<string, object> invalidation = new objectmapper().readvalue( invalidationmessage, new typereference<map<string, object>>() {}); string type = (string) invalidation.get("type"); if ("key".equals(type)) { // 单个键失效 string key = (string) invalidation.get("key"); localcache.remove(key); } else if ("prefix".equals(type)) { // 前缀失效 string prefix = (string) invalidation.get("prefix"); localcache.keyset().removeif(key -> key.startswith(prefix)); } else if ("all".equals(type)) { // 清空整个缓存 localcache.clear(); } } catch (exception e) { // 处理解析错误 } } public string get(string key) { // 首先尝试从本地缓存获取 string value = localcache.get(key); if (value != null) { return value; } // 本地缓存未命中,从redis获取 value = redistemplate.opsforvalue().get(key); if (value != null) { // 存入本地缓存 localcache.put(key, value); } return value; } public void set(string key, string value) { // 更新redis redistemplate.opsforvalue().set(key, value); // 更新本地缓存 localcache.put(key, value); // 发布缓存更新通知 publishinvalidation("key", key); } public void delete(string key) { // 从redis中删除 redistemplate.delete(key); // 从本地缓存中删除 localcache.remove(key); // 发布缓存失效通知 publishinvalidation("key", key); } public void deletebyprefix(string prefix) { // 获取并删除指定前缀的键 set<string> keys = redistemplate.keys(prefix + "*"); if (keys != null && !keys.isempty()) { redistemplate.delete(keys); } // 清除本地缓存中匹配的键 localcache.keyset().removeif(key -> key.startswith(prefix)); // 发布前缀失效通知 publishinvalidation("prefix", prefix); } public void clearallcache() { // 清空本地缓存 localcache.clear(); // 发布全局失效通知 publishinvalidation("all", null); } private void publishinvalidation(string type, string key) { try { // 创建失效消息 map<string, object> invalidation = new hashmap<>(); invalidation.put("type", type); if (key != null) { invalidation.put(type.equals("key") ? "key" : "prefix", key); } invalidation.put("timestamp", system.currenttimemillis()); // 添加来源标识,防止自己接收自己发出的消息 invalidation.put("source", getapplicationinstanceid()); // 序列化并发布消息 string message = new objectmapper().writevalueasstring(invalidation); redistemplate.convertandsend("cache:invalidations", message); } catch (exception e) { // 处理序列化错误 } } private string getapplicationinstanceid() { // 返回应用实例唯一标识,避免处理自己发出的消息 return "app-instance-" + uuid.randomuuid().tostring(); } // 获取缓存统计信息 public map<string, object> getcachestats() { map<string, object> stats = new hashmap<>(); stats.put("cachesize", localcache.size()); return stats; } }
优点
缺点
各种缓存策略的性能对比:
实现方式 | 实时性 | 复杂度 | 内存占用 | 网络开销 | 一致性保证 | redis版本要求 |
---|---|---|---|---|---|---|
本地内存缓存 | 低 | 低 | 高 | 低 | 弱 | 任意 |
服务器辅助缓存 | 高 | 高 | 中 | 中 | 强 | 6.0+ |
ttl过期策略 | 中 | 低 | 中 | 中 | 中 | 任意 |
pub/sub通知 | 高 | 中 | 中 | 高 | 中强 | 任意 |
根据以下因素选择合适的缓存策略:
数据一致性要求
应用架构
redis版本
读写比例
资源限制
redis客户端缓存是提升应用性能的强大工具,通过减少网络请求和数据库访问,可以显著降低延迟并提高吞吐量。
在实际应用中,这些策略往往不是相互排斥的,而是可以组合使用,针对不同类型的数据采用不同的缓存策略,以获得最佳性能和数据一致性平衡。
无论选择哪种缓存策略,关键是理解自己应用的数据访问模式和一致性需求,并据此设计最合适的缓存解决方案。
通过正确应用客户端缓存技术,可以在保持数据一致性的同时,显著提升系统性能和用户体验。
到此这篇关于redis实现客户端缓存的4种方式的文章就介绍到这了,更多相关redis客户端缓存内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论