网站建设好的乡镇,做关键词优化需要修改网站标题,邯郸哪里有做网站的李,徐汇区网站建设目录 一、为什么要使用缓存
二、添加商户缓存
1.缓存的模型和思路
2.代码
3.缓存更新策略
Redis内存淘汰机制#xff1a;
3.1 被动淘汰策略#xff08;不主动淘汰#xff0c;仅在查询时触发#xff09;
3.2 主动淘汰策略#xff08;主动扫描内存#xff0c;按规则…目录 一、为什么要使用缓存
二、添加商户缓存
1.缓存的模型和思路
2.代码
3.缓存更新策略
Redis内存淘汰机制
3.1 被动淘汰策略不主动淘汰仅在查询时触发
3.2 主动淘汰策略主动扫描内存按规则淘汰数据
Redis 过期删除策略
3.3三大过期删除策略详解
1. 被动删除惰性删除
编辑
2. 主动删除定期删除
3. 内存淘汰策略的补充作用
三、数据库缓存不一致解决方案
1.发生原因及相关解决方案
2.实现商铺和缓存与数据库双写一致
四、缓存穿透
1.定义
2.编码解决商品查询的缓存穿透问题
3.小总结
五、缓存雪崩
1.定义
2.解决方案
六、缓存击穿
1. 定义
2.解决方案
2.1互斥锁
2.2逻辑过期
3.互斥锁 vs 逻辑过期场景选择与组合方案
4.利用互斥锁解决缓存击穿问题
5.利用逻辑过期解决缓存击穿问题
七、封装Redis工具类 一、为什么要使用缓存
一句话:因为速度快,好用
缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低**用户访问并发量带来的**服务器读写压力
实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为避震器,系统是几乎撑不住的,所以企业会大量运用到缓存技术;
但是缓存也会增加代码复杂度和运营的成本: 实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用 浏览器缓存主要是存在于浏览器端的缓存 应用层缓存可以分为tomcat本地缓存比如之前提到的map或者是使用redis作为缓存 数据库缓存在数据库中有一片空间是 buffer pool增改查数据都会先加载到mysql的缓存中 CPU缓存当代计算机最大的问题是 cpu性能提升了但内存读写速度没有跟上所以为了适应当下的情况增加了cpu的L1L2L3级的缓存 二、添加商户缓存 在我们查询商户信息时我们是直接操作从数据库中去进行查询的大致逻辑是这样直接查询数据库那肯定慢咯所以我们需要增加缓存
GetMapping(/{id})
public Result queryShopById(PathVariable(id) Long id) {//这里是直接查询数据库return shopService.queryById(id);
}
1.缓存的模型和思路 标准的操作方式就是查询数据库之前先查询缓存如果缓存数据存在则直接从缓存中返回如果缓存数据不存在再查询数据库然后将数据存入redis。
2.代码
代码思路如果缓存有则直接返回如果缓存不存在则查询数据库然后存入redis。 3.缓存更新策略 Redis内存淘汰机制
Redis 内存淘汰策略
│
├─ 被动淘汰策略noeviction
│ └─ 机制内存不足时拒绝写操作读操作正常
│ └─ 适用不允许数据丢失的场景
│
└─ 主动淘汰策略│├─ 基于过期时间仅淘汰设置过期时间的键│ ││ ├─ volatile-lru│ │ └─ 机制淘汰过期键中最久未使用的数据│ │ └─ 适用热点数据缓存│ ││ ├─ volatile-ttl│ │ └─ 机制淘汰过期键中剩余时间最短的数据│ │ └─ 适用时效性强的数据如限时活动│ ││ └─ volatile-random│ └─ 机制随机淘汰过期键│ └─ 适用数据访问无规律的场景│└─ 基于数据热度/大小淘汰所有键│├─ allkeys-lru│ └─ 机制淘汰所有键中最久未使用的数据│ └─ 适用通用缓存场景热点数据优先│├─ allkeys-random│ └─ 机制随机淘汰所有键│ └─ 适用性能优先、访问无规律的场景│├─ allkeys-lfu│ └─ 机制淘汰所有键中访问频率最低的数据│ └─ 适用长期高频访问数据如常用功能缓存│└─ volatile-lfu└─ 机制淘汰过期键中访问频率最低的数据└─ 适用需保留高频访问的过期数据场景
Redis 提供了 8 种内存淘汰策略可分为被动淘汰和主动淘汰两类
3.1 被动淘汰策略不主动淘汰仅在查询时触发 noeviction默认策略 机制当内存不足时拒绝执行所有会导致内存增加的命令如 set、lpush 等但读命令如 get仍可正常执行。 应用场景适用于不允许丢失数据的场景如缓存与数据库强一致的场景但需确保业务能处理写失败的情况。 3.2 主动淘汰策略主动扫描内存按规则淘汰数据
主动淘汰策略又分为基于过期时间和基于数据热度 / 大小两类
①基于过期时间的淘汰策略
此类策略仅淘汰设置了过期时间的键适合缓存场景 volatile-lruLeast Recently Used 机制在过期键中淘汰最长时间未被访问的键。原理通过维护 “最近使用” 顺序淘汰不活跃数据适合热点数据场景如用户行为缓存。示例电商首页商品缓存频繁访问的商品保留冷门商品被淘汰。 volatile-ttl 机制在过期键中优先淘汰剩余过期时间最短的键。原理根据 TTLTime To Live值判断适合对时效性要求高的数据如限时活动缓存。示例秒杀活动倒计时缓存剩余时间短的先淘汰。 volatile-random 机制在过期键中随机淘汰数据。特点实现简单但缺乏针对性适用于数据访问无明显规律的场景。 ②基于数据热度 / 大小的淘汰策略
此类策略对所有键无论是否设置过期时间生效 allkeys-lru 机制在所有键中淘汰最长时间未被访问的键。应用场景最常用的策略之一适合缓存场景如热点文章、用户会话缓存能有效保留活跃数据。优化Redis 通过 “近似 LRU” 算法采样少量数据而非全量扫描平衡性能与准确性。 allkeys-random 机制在所有键中随机淘汰数据。特点性能开销小但可能淘汰活跃数据适用于数据访问无规律且对缓存命中率要求不高的场景。 volatile-lfuLeast Frequently Used 机制在过期键中淘汰访问频率最低的键。原理通过记录访问次数区分 “偶然访问” 和 “高频访问” 数据避免 LRU 淘汰高频但近期未访问的键。示例新闻类应用中高频访问的热点新闻即使近期未被访问也会被保留。 allkeys-lfu 机制在所有键中淘汰访问频率最低的键。应用场景适合长期保留高频访问数据例如用户高频使用的功能缓存。 策略淘汰范围淘汰依据适用场景命中率性能开销noeviction所有键不淘汰拒绝写操作不允许数据丢失的场景无低allkeys-lru所有键最近最少使用通用缓存场景热点数据明显高中volatile-lru过期键最近最少使用仅缓存过期数据的场景高中allkeys-lfu所有键访问频率最低长期高频访问数据的场景最高高volatile-ttl过期键剩余过期时间最短时效性强的数据如限时活动中低random 策略对应范围键随机数据访问无规律或性能优先的场景低低
Redis 过期删除策略
Redis 作为内存型数据库需要高效处理过期键的删除避免无效数据占用内存。其过期删除策略采用被动删除 主动删除的混合模式平衡内存占用与 CPU 开销
3.3三大过期删除策略详解
1. 被动删除惰性删除 机制当客户端访问某个键时Redis 会检查该键是否过期若过期则删除并返回 nil空。优点不主动消耗 CPU 资源仅在访问时触发对性能影响最小。缺点可能导致过期键长时间滞留内存尤其当键未被访问时。示例 # 设定期限为 10 秒的键
redis.setex(key1, 10, value)
# 10 秒后未访问key1 仍存在于内存中
# 当执行 redis.get(key1) 时才会触发删除并返回 nil图片来源于小林Coding
2. 主动删除定期删除 机制Redis 周期性地随机抽取一部分键检查是否过期并删除过期键。具体规则 每个 Redis 服务器每秒执行 hz 次默认 hz10即每秒 10 次过期扫描。每次扫描随机选取 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个键默认 20 个若过期键比例超过 25%则继续扫描直到过期键比例降至 25% 以下或扫描次数达到上限。 优点主动清理过期键减少内存浪费。缺点扫描频率和范围需平衡频率过高会占用 CPU过低则清理不及时。 图片来源于小林Coding
3. 内存淘汰策略的补充作用 当 Redis 内存达到 maxmemory 阈值时会触发内存淘汰策略如 lru/lfu 等此时即使键未过期也可能被淘汰作为过期删除的补充机制。 三、数据库缓存不一致解决方案
1.发生原因及相关解决方案
由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:
用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等;怎么解决呢有如下几种方案 Cache Aside Pattern 人工编码方式缓存调用者在更新完数据库后再去更新缓存也称之为双写方案 Read/Write Through Pattern : 由系统本身完成数据库与缓存的问题交由系统本身去处理 Write Behind Caching Pattern 调用者只操作缓存其他线程去异步处理数据库实现最终一致 对比维度Cache Aside Pattern (双写方案)Read/Write Through PatternWrite Behind Caching Pattern数据一致性最终一致性可能存在短暂不一致窗口强一致性系统保证缓存与数据库同步最终一致性异步同步延迟更大性能表现写操作需两次IO数据库缓存写操作由系统优化处理最佳写性能仅操作缓存实现复杂性需应用层维护双写逻辑需实现底层存储系统抽象层需处理异步队列和失败重试机制适用场景读多写少场景如商品详情页对一致性要求高的场景如金融账户写密集场景如日志记录主要风险并发写可能引发脏数据br需配合失效机制系统设计缺陷会导致全局故障数据丢失风险异步未完成时宕机典型应用电商系统商品信息缓存分布式文件系统元数据管理社交平台点赞数统计维护成本中需处理双写异常场景高需维护存储抽象层高需维护可靠消息队列数据流动方向应用层双向控制brDB↔缓存单向流动brDB→缓存单向流动br缓存→DB
综合考虑使用方案一但是方案一调用者如何处理呢这里有几个问题
操作缓存和数据库时有三个问题需要考虑
如果采用第一个方案那么假设我们每次操作数据库后都操作缓存但是中间如果没有人查询那么这个更新动作实际上只有最后一次生效中间的更新动作意义并不大我们可以把缓存删除等待再次查询时将缓存中的数据加载出来 1.删除缓存还是更新缓存 更新缓存每次更新数据库都更新缓存无效写操作较多 删除缓存更新数据库时让缓存失效查询时再更新缓存 2.如何保证缓存与数据库的操作的同时成功或失败 单体系统将缓存与数据库操作放在一个事务 分布式系统利用TCC等分布式事务方案 应该具体操作缓存还是操作数据库我们应当是先操作数据库再删除缓存原因在于如果你选择第一种方案在两个线程并发来访问时假设线程1先来他先把缓存删了此时线程2过来他查询缓存数据并不存在此时他写入缓存当他写入缓存后线程1再执行更新动作时实际上写入的就是旧的数据新的数据被旧数据覆盖了。 先操作缓存还是先操作数据库 先删除缓存再操作数据库 先操作数据库再删除缓存 2.实现商铺和缓存与数据库双写一致
核心思路如下
修改ShopController中的业务逻辑满足下面的需求
根据id查询店铺时如果缓存未命中则查询数据库将数据库结果写入缓存并设置超时时间
根据id修改店铺时先修改数据库再删除缓存
修改重点代码1修改ShopServiceImpl的queryById方法
设置redis缓存时添加过期时间 修改重点代码2
代码分析通过之前的淘汰我们确定了采用删除策略来解决双写问题当我们修改了数据之后然后把缓存中的数据进行删除查询时发现缓存中没有数据则会从mysql中加载最新的数据从而避免数据库和缓存不一致的问题 四、缓存穿透
1.定义
指请求查询一个不存在的数据缓存和数据库中均无该数据导致请求每次都穿透到数据库造成资源浪费。若被恶意攻击如批量请求不存在的 ID可能导致数据库崩溃。
典型场景恶意用户通过脚本批量请求user_id-1等不存在的参数。
例如请求店铺为0的信息 常见的解决方案有两种
方案实现方式优缺点布隆过滤器Bloom Filter在请求进入数据库前先用布隆过滤器判断数据是否存在不存在则直接拒绝。- 优点空间效率高过滤速度快 - 缺点存在误判率误报存在漏报不存在空值缓存当查询结果为 null 时仍将 null 存入缓存设置短过期时间如 5 分钟避免重复查询数据库。- 优点简单易实现 - 缺点可能存储无效空值占用少量内存 2.编码解决商品查询的缓存穿透问题
核心思路如下
在原来的逻辑中我们如果发现这个数据在mysql中不存在直接就返回404了这样是会存在缓存穿透问题的
现在的逻辑中如果这个数据不存在我们不会返回404 还是会把这个数据写入到Redis中并且将value设置为空欧当再次发起查询时我们如果发现命中之后判断这个value是否是null如果是null则是之前写入的数据证明是缓存穿透数据如果不是则直接返回数据。 3.小总结
缓存穿透产生的原因是什么
用户请求的数据在缓存中和数据库中都不存在不断发起这样的请求给数据库带来巨大压力
缓存穿透的解决方案有哪些 * 缓存null值 * 布隆过滤 * 增强id的复杂度避免被猜测id规律 * 做好数据的基础格式校验 * 加强用户权限校验 * 做好热点参数的限流 五、缓存雪崩
1.定义 指大量缓存 key 在同一时间段失效或缓存服务整体不可用导致海量请求直接访问数据库造成数据库雪崩甚至宕机。典型场景缓存服务器重启、大量 key 设置相同过期时间如凌晨零点失效。 2.解决方案
方案实现方式优缺点过期时间分散化为不同 key 的过期时间添加随机值如TTL3600random(0,1800)避免集体失效。- 优点简单有效成本低 - 缺点无法应对缓存服务整体故障多级缓存架构部署多层缓存如本地缓存 Redis 缓存当 Redis 失效时本地缓存仍可响应请求。- 优点提高可用性降低数据库压力 - 缺点维护成本高需处理缓存一致性缓存集群与熔断- 缓存采用集群部署避免单点故障 - 引入熔断机制如 Hystrix当数据库压力过高时暂时拒绝部分请求。- 优点高可用性保护数据库 - 缺点需要额外组件支持增加架构复杂度持久化与热重启启用 Redis 持久化RDB/AOF服务器重启时快速加载缓存减少全量失效窗口。- 优点恢复速度快 - 缺点依赖持久化文件的完整性 六、缓存击穿
1. 定义 指热点数据的缓存过期瞬间大量请求同时穿透缓存直达数据库导致数据库负载激增的现象。典型场景秒杀活动中某商品的缓存 key 过期瞬间数万请求访问数据库查询库存。 假设线程1在查询缓存之后本来应该去查询数据库然后把这个数据重新加载到缓存的此时只要线程1走完这个逻辑其他线程就都能从缓存中加载这些数据了但是假设在线程1没有走完的时候后续的线程2线程3线程4同时过来访问当前这个方法 那么这些线程都不能从缓存中查询到数据那么他们就会同一时刻来访问查询缓存都没查到接着同一时间去访问数据库同时的去执行数据库代码对数据库访问压力过大 2.解决方案
方案实现方式优缺点互斥锁Mutex当缓存失效时先通过 Redis 的SETNX获取锁成功的线程查询数据库并更新缓存其他线程等待锁释放。- 优点简单有效避免大量请求击穿 - 缺点存在锁竞争可能阻塞请求热点数据永不过期逻辑过期不设置过期时间通过后台线程异步更新数据如定时任务。- 优点完全避免过期击穿 - 缺点数据一致性略有延迟需配合版本号或消息队列更新缓存时间随机打散给热点 key 的过期时间添加随机偏移如expire3600random(0,600)避免同时过期。- 优点轻量级方案适合非强一致场景 - 缺点无法完全杜绝击穿但可大幅降低概率
这里我们重点讲解互斥锁和逻辑过期
2.1互斥锁
因为锁能实现互斥性。假设线程过来只能一个人一个人的来访问数据库从而避免对于数据库访问压力过大但这也会影响查询的性能因为此时会让查询的性能从并行变成了串行我们可以采用tryLock方法 double check来解决这样的问题。
假设现在线程1过来访问他查询缓存没有命中但是此时他获得到了锁的资源那么线程1就会一个人去执行逻辑假设现在线程2过来线程2在执行过程中并没有获得到锁那么线程2就可以进行到休眠直到线程1把锁释放后线程2获得到锁然后再来执行逻辑此时就能够从缓存中拿到数据了。 优点缺点1. 强一致性确保缓存与数据库实时同步 2. 实现简单适用于中小并发场景。1. 高并发下存在锁竞争可能导致请求延迟 2. 极端情况下如数据库慢查询可能引发线程饥饿。
2.2逻辑过期 物理过期缓存不设置expire永久存储逻辑过期在缓存值中附加过期时间戳由应用层判断是否需要更新。异步更新发现逻辑过期时启动后台线程异步更新缓存前台请求返回旧数据避免阻塞。 流程
我们把过期时间设置在 redis的value中注意这个过期时间并不会直接作用于redis而是我们后续通过逻辑去处理。假设线程1去查询缓存然后从value中判断出来当前的数据已经过期了此时线程1去获得互斥锁那么其他线程会进行阻塞获得了锁的线程他会开启一个线程去进行 以前的重构数据的逻辑直到新开的线程完成这个逻辑后才释放锁 而线程1直接进行返回假设现在线程3过来访问由于线程线程2持有着锁所以线程3无法获得锁线程3也直接返回数据只有等到新开的线程2把重建数据构建完后其他线程才能走返回正确的数据。
这种方案巧妙在于异步的构建缓存缺点在于在构建完缓存之前返回的都是脏数据。 优点缺点1. 无锁竞争响应速度快适合高并发读场景 2. 避免缓存击穿的同时保证服务可用性。1. 存在数据不一致窗口旧数据可能被读取 2. 后台更新失败时需额外容错机制如重试队列。
3.互斥锁 vs 逻辑过期场景选择与组合方案
维度互斥锁方案逻辑过期方案一致性要求强一致性实时同步最终一致性允许短期延迟并发场景中小并发、写操作较频繁高并发读、写操作较少典型应用库存扣减、订单状态查询商品详情页、首页轮播图组合优化互斥锁 逻辑过期 1. 逻辑过期作为主方案提升读性能 2. 后台更新时添加互斥锁避免并发脏写。
互斥锁方案由于保证了互斥性所以数据一致且实现简单因为仅仅只需要加一把锁而已也没其他的事情需要操心所以没有额外的内存消耗缺点在于有锁就有死锁问题的发生且只能串行执行性能肯定受到影响
逻辑过期方案线程读取过程中不需要等待性能好有一个额外的线程持有锁去进行重构数据但是在重构数据完成前其他的线程只能返回之前的数据且实现起来麻烦 4.利用互斥锁解决缓存击穿问题
核心思路相较于原来从缓存中查询不到数据后直接查询数据库而言现在的方案是 进行查询之后如果从缓存没有查询到数据则进行互斥锁的获取获取互斥锁后判断是否获得到了锁如果没有获得到则休眠过一会再进行尝试直到获取到锁为止才能进行查询
如果获取到了锁的线程再去进行查询查询后将数据写入redis再释放锁返回数据利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑防止缓存击穿 操作锁的代码
核心思路就是利用redis的setnx方法来表示获取锁该方法含义是redis中如果没有这个key则插入成功返回1在stringRedisTemplate中返回true 如果有这个key则插入失败则返回0在stringRedisTemplate返回false我们可以通过true或者是false来表示是否有线程成功插入key成功插入的key的线程我们认为他就是获得到锁的线程。
//获取锁
private boolean tryLock(String key) {Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);
}//释放锁
private void unlock(String key) {stringRedisTemplate.delete(key);
}
操作代码 public Shop queryWithMutex(Long id) {String key CACHE_SHOP_KEY id;// 1、从redis中查询商铺缓存String shopJson stringRedisTemplate.opsForValue().get(key);// 2、判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的值是否是空值if (shopJson ! null) {//返回一个错误信息return null;}// 4.实现缓存重构//4.1 获取互斥锁String lockKey lock:shop: id;Shop shop null;try {boolean isLock tryLock(lockKey);// 4.2 判断否获取成功if(!isLock){//4.3 失败则休眠重试Thread.sleep(50);return queryWithMutex(id);}//4.4 成功根据id查询数据库shop getById(id);// 5.不存在返回错误if(shop null){//将空值写入redisstringRedisTemplate.opsForValue().set(key,,CACHE_NULL_TTL,TimeUnit.MINUTES);//返回错误信息return null;}//6.写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.释放互斥锁unlock(lockKey);}return shop;}5.利用逻辑过期解决缓存击穿问题
需求修改根据id查询商铺的业务基于逻辑过期方式来解决缓存击穿问题
思路分析当用户开始查询redis时判断是否命中如果没有命中则直接返回空数据不查询数据库而一旦命中后将value取出判断value中的过期时间是否满足如果没有过期则直接返回redis中的数据如果过期则在开启独立线程后直接返回之前的数据独立线程去重构数据重构完成后释放互斥锁。 如果封装数据因为现在redis中存储的数据的value需要带上过期时间此时要么你去修改原来的实体类要么你
步骤一、
新建一个实体类我们采用第二个方案这个方案对原来代码没有侵入性。
package com.hmdp.utils;import lombok.Builder;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;Data
Builder
public class RedisData implements Serializable {private LocalDateTime expireTime;private Object data;
}步骤二、
在ShopServiceImpl新增此方法利用单元测试进行缓存预热 在测试类中 步骤三正式代码
ShopServiceImpl
private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key CACHE_SHOP_KEY id;// 1.从redis查询商铺缓存String json stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在直接返回return null;}// 4.命中需要先把json反序列化为对象RedisData redisData JSONUtil.toBean(json, RedisData.class);Shop shop JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期直接返回店铺信息return shop;}// 5.2.已过期需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey LOCK_SHOP_KEY id;boolean isLock tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()-{try{//重建缓存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回过期的商铺信息return shop;
}
七、封装Redis工具类
基于StringRedisTemplate封装一个缓存工具类满足下列需求 * 方法1将任意Java对象序列化为json并存储在string类型的key中并且可以设置TTL过期时间 * 方法2将任意Java对象序列化为json并存储在string类型的key中并且可以设置逻辑过期时间用于处理缓存击穿问题 * 方法3根据指定的key查询缓存并反序列化为指定类型利用缓存空值的方式解决缓存穿透问题 * 方法4根据指定的key查询缓存并反序列化为指定类型需要利用逻辑过期解决缓存击穿问题 将逻辑进行封装
Slf4j
Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10);public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate stringRedisTemplate;}public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 设置逻辑过期RedisData redisData new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}public R,ID R queryWithPassThrough(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit){String key keyPrefix id;// 1.从redis查询商铺缓存String json stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if (json ! null) {// 返回一个错误信息return null;}// 4.不存在根据id查询数据库R r dbFallback.apply(id);// 5.不存在返回错误if (r null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在写入redisthis.set(key, r, time, unit);return r;}public R, ID R queryWithLogicalExpire(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit) {String key keyPrefix id;// 1.从redis查询商铺缓存String json stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在直接返回return null;}// 4.命中需要先把json反序列化为对象RedisData redisData JSONUtil.toBean(json, RedisData.class);R r JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期直接返回店铺信息return r;}// 5.2.已过期需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey LOCK_SHOP_KEY id;boolean isLock tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){// 6.3.成功开启独立线程实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() - {try {// 查询数据库R newR dbFallback.apply(id);// 重建缓存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的商铺信息return r;}public R, ID R queryWithMutex(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit) {String key keyPrefix id;// 1.从redis查询商铺缓存String shopJson stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson ! null) {// 返回一个错误信息return null;}// 4.实现缓存重建// 4.1.获取互斥锁String lockKey LOCK_SHOP_KEY id;R r null;try {boolean isLock tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败休眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功根据id查询数据库r dbFallback.apply(id);// 5.不存在返回错误if (r null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}private boolean tryLock(String key) {Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}
}
看到这如果有用的话记得点赞关注哦后续会更新更多内容的