徐州网站推广优化,品牌网站设计公司哪家好,请人代做谷歌外贸网站,wordpress 免费版什么是缓存#xff1f;
缓存是一种提高数据访问效率的技术#xff0c;通过在内存中存储数据的副本来减少对数据库或其他慢速存储设备的频繁访问。缓存通常用于存储热点数据或计算代价高的结果#xff0c;以加快响应速度。
添加Redis缓存有什么好处#xff1f;
Redis 基…什么是缓存
缓存是一种提高数据访问效率的技术通过在内存中存储数据的副本来减少对数据库或其他慢速存储设备的频繁访问。缓存通常用于存储热点数据或计算代价高的结果以加快响应速度。
添加Redis缓存有什么好处
Redis 基于内存存储读写速度极快相比于传统的磁盘存储方式能够显著提高系统的响应速度。
缓存了高频访问的数据后可以减少对数据库的访问次数从而减轻数据库的负载。 简单的将数据存入Redis之后存在什么问题
缓存一致性问题
当数据库中的数据发生变化时缓存中的数据可能没有及时更新导致数据不一致。
解决办法使用缓存更新策略如主动更新推荐、延迟双删、或设置缓存失效时间。
缓存穿透
如果客户端频繁请求数据库中不存在的数据而这些数据不会被缓存最终所有请求都会直接打到数据库增加负载。
解决办法为不存在的数据设置一个短时间的空值缓存。
缓存雪崩
当大量缓存数据同时过期或 Redis 宕机时所有请求涌向数据库可能导致系统崩溃。
解决办法为不同缓存设置不同的过期时间分布式过期时间并配置缓存服务的高可用性如主从、集群。
缓存击穿
某个热点数据突然失效时大量请求直接打到数据库可能导致数据库压力骤增。
解决办法使用互斥锁机制确保缓存重新加载时只有一个请求访问数据库。或者使用逻辑过期方式。这两种需要根据情况来选择。如果需要确保数据的一致性推荐使用互斥锁机制。如果短暂的数据不一致不要紧追求的是访问速度推荐使用逻辑过期方式
缓存更新策略 为什么读操作未命中Redis读数据库存入Redis还需要设置超时时间呢
通过设置超时时间即使缓存更新失败缓存数据会在过期时间后自动失效重新加载最新数据确保数据最终一致性。也就是兜底方案。
为什么写的操作是先写入数据库再删除缓存呢
1. 避免读写竞争问题
如果先删除缓存再写数据库可能出现以下情况
线程 A 删除缓存。线程 B 查询数据时发现缓存为空去数据库查询旧数据并将其重新写入缓存。线程 A 写入新的数据到数据库。
这样缓存中保存的就会是旧数据导致数据不一致。
为什么先写入数据库就不存在这种读写竞争问题
因为对Redis的读写时间耗时很短在很短的时间内CPU的执行权被抢夺的概率很小但是更新数据库的操作比较久尤其涉及到多表查询更新的时候。被抢夺CPU执行权的概率比较大。
2. 确保缓存数据最终一致
先写入数据库再删除缓存的顺序可以保证以下情况
数据库中总是有最新的数据。即使缓存被删除后有线程查询触发缓存更新时查询到的也是最新的数据。
package com.hmdp.service.impl;import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;/*** p* 服务实现类* /p** author 虎哥* since 2021-12-22*/
Service
RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements IShopService {final StringRedisTemplate stringRedisTemplate;/*** 实现Redis缓存店铺信息* param id* return*/Overridepublic Result queryById(Long id) {// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String shopStr stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY id);// 存在直接返回Redis中的信息if (StrUtil.isNotBlank(shopStr)) {// 将string类型转换为对象Shop shop JSONUtil.toBean(shopStr, Shop.class);return Result.ok(shop);}// 不存在则查询数据库Shop shop getById(id);if (shop null) {// 数据库不存在返回404return Result.fail(商铺不存在);}// 数据库中存在商铺信息则将商铺信息存入Redis 并且设置超时时间作为兜底方案stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);// 返回商铺信息return Result.ok(shop);}OverrideTransactionalpublic Result updateShop(Shop shop) {Long id shop.getId();if (id null ){return Result.fail(店铺id不能为空!);}// 先更新数据库updateById(shop);// 删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY id );return Result.ok();}
}缓存穿透访问不存在的数据 也就是说我们在之前的查询的代码上还需要添加如果Redis当中存在值且不为空直接返回Redis中的数据。还需要判断Redis存储的数据是否wield空 如果为空直接返回错误信息不需要再查询数据库导致数据库崩溃。如果Redis当中的值为null我们需要查询数据库 如果数据库查询不到我们需要将这个不存在的数据保存在Redis值为空防止缓存击穿并且设置TTL
判断 Redis 是否存储了空值
if (shopStr ! null) 判断了 Redis 中的值是否为 空如果为 或空值标记表示这是防止缓存穿透写入的特殊数据直接返回错误信息。
数据库查询为空时缓存空值
使用 或其他标记作为空值写入 Redis并设置一个较短的过期时间例如 2 分钟。这样可以避免频繁查询数据库防止缓存穿透。
正常数据的缓存设置 TTL
如果数据库中有数据写入 Redis 时设置长时间的 TTL如 CACHE_SHOP_TTL确保数据一致性。
更新数据后删除缓存
确保先更新数据库再删除缓存避免并发情况下的数据不一致问题。
package com.hmdp.service.impl;import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;/*** p* 服务实现类* /p** author 虎哥* since 2021-12-22*/
Service
RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements IShopService {final StringRedisTemplate stringRedisTemplate;/*** 实现Redis缓存店铺信息** param id* return*/Overridepublic Result queryById(Long id) {// 1. 从Redis中查询商铺信息String shopStr stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY id);// 2. 如果Redis中存在值且不为空字符串直接返回if (StrUtil.isNotBlank(shopStr)) {Shop shop JSONUtil.toBean(shopStr, Shop.class);return Result.ok(shop);}// 3. 判断Redis中是否存储了空值防止缓存穿透if (shopStr ! null) {return Result.fail(商铺不存在);}// 4. Redis中不存在数据从数据库查询Shop shop getById(id);// 5. 如果数据库中也不存在则写入一个空值到Redis并设置短TTL防止缓存穿透if (shop null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id, , // 空值标记2, // 短时间TTL如2分钟TimeUnit.MINUTES);return Result.fail(商铺不存在);}// 6. 如果数据库中存在将商铺信息写入Redis并设置TTLstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);// 7. 返回商铺信息return Result.ok(shop);}/*** 更新商铺信息** param shop* return*/OverrideTransactionalpublic Result updateShop(Shop shop) {Long id shop.getId();if (id null) {return Result.fail(店铺id不能为空!);}// 1. 先更新数据库updateById(shop);// 2. 再删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY id);return Result.ok();}
}缓存雪崩 缓存击穿问题数据过期同时有大量请求 缓存击穿问题发生在某些热点数据在缓存失效的瞬间同时有大量请求涌入从而导致这些请求直接访问数据库可能会引发数据库压力过大甚至宕机的情况。
1. 给线程加锁互斥锁
原理
通过对热点数据的访问加互斥锁如分布式锁或本地锁保证在缓存失效后只有一个线程能去查询数据库并更新缓存其余线程等待锁释放后从缓存读取数据。
优点
简单易实现逻辑清晰只需引入锁机制即可。保护数据库有效防止多线程同时查询数据库降低数据库压力。通用性强适用于所有数据场景尤其是并发量高的热点数据。
缺点
性能问题 在高并发情况下锁的排队等待会增加响应时间。如果锁竞争激烈会导致线程阻塞。单点问题 如果锁是本地实现可能会出现分布式环境中的一致性问题。使用分布式锁如 Redis 分布式锁会增加实现复杂度。潜在死锁风险如果锁机制设计不当可能会导致死锁。 使用互斥锁的代码逻辑
查询 Redis
首先查询 Redis 是否存在目标数据。如果命中直接返回。如果未命中Redis 中没有目标数据进入下一步。 尝试获取锁
如果获取锁成功
再次查询 Redis 是否有数据防止其他线程已加载数据。如果 Redis 中仍然没有数据查询数据库加载数据到 Redis。释放锁返回数据。
如果未获取到锁
等待一段时间后重复尝试获取锁。在每次尝试获取锁时先查询 Redis 是否已存在数据防止无意义的锁竞争。
为什么获取锁之后还需要再次检查Redis中是否存在数据
核心原因 避免重复查询数据库和重复写入 Redis从而减少资源浪费。
多线程场景下的问题
假设线程 A 获取到锁并完成了缓存重建从数据库查询数据并写入 Redis。线程 B 在等待锁的过程中其实线程 A 已经完成了数据的缓存。线程 B 获取到锁时如果不再次检查 Redis就会重复从数据库查询并覆盖 Redis 中的已有数据导致不必要的开销和延迟。
这个锁应该是什么样的锁是平常的吗 使用 Redis 实现分布式锁
Redis 是实现分布式锁的常用工具。通过 SETNXset if not exists命令以及锁的过期时间可以实现高效且可靠的分布式锁。
如果键不存在SETNX 会创建这个键并返回成功true。如果键已经存在SETNX 会直接返回失败false。
/*** 尝试获取锁* param key 锁的唯一标识* return 是否获取成功*/
private boolean tryLock(String key) {Boolean success stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(success);
}/*** 释放锁* param key 锁的唯一标识*/
private void unlock(String key) {stringRedisTemplate.delete(key);
}Service
RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements IShopService {final StringRedisTemplate stringRedisTemplate;/*** 查询商铺信息包含缓存击穿的解决方案*/Overridepublic Result queryById(Long id) {// 通过互斥锁解决缓存击穿问题Shop shop queryWithMutex(id);if (shop null) {return Result.fail(店铺不存在);}return Result.ok(shop);}/*** 互斥锁解决缓存击穿*/public Shop queryWithMutex(Long id) {String key CACHE_SHOP_KEY id;String lockKey LOCK_SHOP_KEY id;// 1. 查询缓存String shopJson stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}if (shopJson ! null) {return null; // 空值}Shop shop null;boolean isLockAcquired false;try {// 2. 尝试获取锁while (!(isLockAcquired tryLock(lockKey))) {Thread.sleep(50); // 未获取锁等待并重试}// 3. 再次检查缓存防止重复查询数据库String shopJsonAfterLock stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJsonAfterLock)) {return JSONUtil.toBean(shopJsonAfterLock, Shop.class);}if (shopJsonAfterLock ! null) {return null; // 空值}// 4. 查询数据库shop getById(id);if (shop null) {// 数据库不存在写入空值防止缓存穿透stringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 5. 写入缓存stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复线程中断状态throw new RuntimeException(线程被中断, e);} finally {// 6. 释放锁if (isLockAcquired) {unlock(lockKey);}}return shop;}/*** 获取锁*/private boolean tryLock(String key) {Boolean success stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(success);}/*** 释放锁防止误删其他线程的锁*/private void unlock(String key) {stringRedisTemplate.delete(key);}/*** 更新商铺信息并清除缓存*/OverrideTransactionalpublic Result updateShop(Shop shop) {Long id shop.getId();if (id null) {return Result.fail(店铺id不能为空);}// 1. 更新数据库updateById(shop);// 2. 删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY id);return Result.ok();}
}2. 逻辑过期
原理
将缓存数据设置为逻辑过期时间同时保留旧值。当缓存过期时请求仍然返回旧数据同时后台异步更新缓存通过任务队列或线程池刷新数据。
优点
高性能 由于返回旧数据请求不会直接打到数据库避免了数据库的压力。不会阻塞用户线程用户体验更好。无锁机制 通过异步任务更新缓存避免了加锁的复杂性和性能问题。支持高并发 用户请求不会因缓存失效而阻塞。
缺点
数据一致性问题 在缓存逻辑过期的时间段内可能返回的是旧数据适合对一致性要求不高的场景。实现复杂度高 需要设计异步更新逻辑增加系统复杂性。需要引入额外的定时任务或异步线程池处理刷新逻辑。资源占用 异步更新的任务可能会带来额外的资源消耗尤其是在数据量较大时。
选择建议
互斥锁适合对数据一致性要求高的业务场景如订单、库存等核心数据且并发量较低。逻辑过期更适合高并发、热点数据且对数据一致性要求不高的场景如新闻热点、排行榜等。 逻辑过期方式解决问题的逻辑
查询缓存数据
通过Redis的key查询数据结果可能是 缓存未命中直接返回null。缓存命中继续处理逻辑。
判断逻辑过期
未过期直接返回缓存中的数据。已过期需要重建缓存。 重建缓存防止缓存穿透
获取分布式锁互斥锁。
如果获取不到锁 直接返回旧的数据避免阻塞等待。如果获取到锁 再次检查是否过期双重检查锁机制防止锁释放期间其他线程已更新数据。开启一个独立线程完成缓存重建释放锁后返回旧数据。
缓存重建逻辑
查询数据库获取最新数据。写入Redis并设置逻辑过期时间。释放锁确保其他线程可以继续执行。
public Shop queryWithLogicExpire(Long id){String key CACHE_SHOP_KEYid;// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String shopStr stringRedisTemplate.opsForValue().get(key);// Redis中不存在该信息if(StrUtil.isBlank(shopStr)){return null;}RedisData redisData JSONUtil.toBean(shopStr, RedisData.class);
// Shop data (Shop) redisData.getData();Shop data JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);// 存在 需要判断缓存是否逻辑过期// 未过期 直接返回数据if (redisData.getExpireTime().isAfter(LocalDateTime.now())){return data;}// 过期 缓存重建// 尝试获取互斥锁String lockKey LOCK_SHOP_KEY id;if (tryLock(lockKey)) {//如果线程b检查完过期时间后线程a刚好重建完成并释放锁// 此时线程b可以拿到锁并再次重建所以需要进行二次校验过期时间// 获取锁之后还需要再次检测Redis缓存是否过期 如果未过期就不需要再开启新的线程shopStr stringRedisTemplate.opsForValue().get(key);RedisData redisDataAfterGetLock JSONUtil.toBean(shopStr, RedisData.class);if (redisDataAfterGetLock.getExpireTime().isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) redisDataAfterGetLock.getData(),Shop.class);}//TODO 获取到了就开启独立的线程,CATHE_REBUILD_EXECUTOR.submit(()-{try {// 重建缓存this.saveShop2Redis(id,CACHE_SHOP_TTL);} catch (Exception e) {log.error(缓存重建失败, e);}finally {// 释放锁unlock(lockKey);}});}// 还是返回旧的数据return data;
为什么需要进行二次校验
以下是可能的线程执行时间线
线程A发现缓存过期获取锁进入重建逻辑。线程B也发现缓存过期但未获取到锁进入等待状态。线程A完成缓存重建更新Redis数据释放锁。线程B获取到锁继续执行认为缓存仍然过期。 如果没有二次校验线程B会再次重建缓存。如果有二次校验线程B会发现缓存数据已经更新不需要重复重建。
此时如果线程A完成缓存重建并释放锁后线程B再次获取锁直接重建缓存会造成
重复的缓存重建浪费资源。数据被多次写入Redis增加Redis的负担。
二次校验通过在获取锁后再次检查缓存数据是否过期或已经被其他线程更新可以避免这种重复操作。
为什么没有查到数据的时候直接返回null
因为对于热点key来说我们会先将数据存储到Redis当中并且设置逻辑过期时间。如果根据key在Redis当中查询不到该数据只能说明这个数据不在热点当中直接返回空即可。
未命中说明
该key不属于热点数据或从未被缓存。逻辑过期方案主要针对热点数据的缓存维护对非热点数据无需增加负担。
如何设置逻辑超时时间
引入一个RedisData类包含以下字段
expireTime逻辑过期时间LocalDateTime类型。data具体的业务数据对象如Shop类。
package com.hmdp.utils;import lombok.Data;import java.time.LocalDateTime;Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}缓存数据存储到Redis
RedisData redisData new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(CACHE_SHOP_TTL));
redisData.setData(shop); // shop 是业务对象
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));使用JSON序列化将RedisData对象保存到Redis中
封装成工具类 set普通的缓存设置方法。
setWithLogicExpire设置逻辑过期缓存。
queryWithPassThrough解决缓存穿透问题。
queryWithLogicExpire解决缓存击穿问题使用逻辑过期方案。
需要注意两个点
提高通用性面向多个类封装工具包的设计原则
使用泛型
当工具包需要支持多个类时提高通用性是关键。可以通过以下方式实现
1.1 使用泛型
为什么使用泛型不同的调用者可能会涉及不同的数据模型和返回类型。通过泛型可以让工具方法适配任意类型的返回值而不需要为每个类重复编写代码。如何使用泛型 定义返回值类型 R适配不同的对象类型如 Shop、User 等。定义主键类型 ID适配不同的数据主键如 Long 或 String。
public R, ID R queryWithPassThrough(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit
) {// 逻辑与实现
}R 代表返回的实体类类型如 Shop、User 等。ID 代表主键的类型如 Long数字型 ID或 StringUUID。
2. 如何根据调用者需求获取数据库数据
关键点调用者可能有不同的需求例如
查询逻辑不同按 ID、按名称等。数据返回类型不同单个对象、列表等。数据库表不同。
解决方案 使用 FunctionID, R 获取数据
FunctionID, R 的优势
灵活性调用者可以动态传递 Lambda 表达式实现灵活的查询逻辑。解耦工具类不需要依赖具体的服务或 DAO 实现而是将查询逻辑交给调用者。简单易用调用者可以用 Lambda 或方法引用直接定义查询逻辑。
工具类的实现
public R, ID R queryWithPassThrough(String keyPrefix, ID id, ClassR type,FunctionID, R dbFallback, Long time, TimeUnit unit
) {String key keyPrefix id;String json stringRedisTemplate.opsForValue().get(key);// 判断缓存中是否有数据if (StrUtil.isNotBlank(json)) {return JSONUtil.toBean(json, type); // 缓存命中返回数据}// 查询数据库R result dbFallback.apply(id);if (result null) {// 防止缓存穿透缓存空值stringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 数据库有数据写入缓存this.set(key, result, time, unit);return result;
}调用示例
使用 Lambda 表达式或方法引用定义数据库查询逻辑
// Lambda 表达式
queryWithPassThrough(shop:, 1L, Shop.class,id - shopService.findById(id),10L, TimeUnit.MINUTES
);// 方法引用
queryWithPassThrough(user:, abc123, User.class,userService::findById,10L, TimeUnit.MINUTES
);package com.hmdp.utils;import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;import static com.hmdp.utils.RedisConstants.*;Slf4j
Component
RequiredArgsConstructor
public class CacheClient {final StringRedisTemplate stringRedisTemplate;public void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);}public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit){RedisData redisData new RedisData();redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));redisData.setData(value);stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}public R,ID R queryWithPassThrough(String keyPrefix, ID id,ClassR type, FunctionID,R dbFallback,Long time, TimeUnit unit){// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String key keyPrefix id;String Json stringRedisTemplate.opsForValue().get(key);// 存在直接返回Redis中的信息if (StrUtil.isNotBlank(Json)) {// 将string类型转换为对象return JSONUtil.toBean(Json,type);}// 判断命中的是否是空值if (Json ! null) {// 是空值返回一个错误信息return null;}// 不存在则查询数据库R r dbFallback.apply(id);if (r null) {// 数据库不存在返回404// 要将空值写入Redis 防止缓存穿透stringRedisTemplate.opsForValue().set(keyPrefix id,,CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}// 数据库中存在商铺信息则将商铺信息存入Redis 并且设置超时时间作为兜底方案this.set(key,r,time,unit);// 返回商铺信息return r;}// 线程池// 不知道怎么根据id查询数据库那么就需要面向函数式接口让调用者传递函数private static final ExecutorService CATHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10);public R,ID R queryWithLogicExpire(String prefixKey, String PrefixLock,ID id,ClassR type,FunctionID,R dbFallback, Long time, TimeUnit unit){String key prefixKeyid;// 1. 首先从Redis当中查询是否存在商铺// 存储对象可以使用HashMap 也可以使用string类型String json stringRedisTemplate.opsForValue().get(key);// Redis中不存在该信息if(StrUtil.isBlank(json)){return null;}RedisData redisData JSONUtil.toBean(json, RedisData.class);
// Shop data (Shop) redisData.getData();R data JSONUtil.toBean((JSONObject) redisData.getData(), type);// 存在 需要判断缓存是否逻辑过期// 未过期 直接返回数据if (redisData.getExpireTime().isAfter(LocalDateTime.now())){return data;}// 过期 缓存重建// 尝试获取互斥锁String lockKey PrefixLock id;if (tryLock(lockKey)) {//如果线程b检查完过期时间后线程a刚好重建完成并释放锁// 此时线程b可以拿到锁并再次重建所以需要进行二次校验过期时间// 获取锁之后还需要再次检测Redis缓存是否过期 如果未过期就不需要再开启新的线程json stringRedisTemplate.opsForValue().get(key);RedisData redisDataAfterGetLock JSONUtil.toBean(json, RedisData.class);if (redisDataAfterGetLock.getExpireTime().isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) redisDataAfterGetLock.getData(),type);}//TODO 获取到了就开启独立的线程,CATHE_REBUILD_EXECUTOR.submit(()-{try {// 重建缓存// 查询数据库R r dbFallback.apply(id);this.setWithLogicExpire(key,r,time,unit);} catch (Exception e) {log.error(缓存重建失败, e);}finally {// 释放锁unlock(lockKey);}});}// 还是返回旧的数据return data;}/*** 获取锁*/private boolean tryLock(String key) {Boolean success stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(success);}/*** 释放锁防止误删其他线程的锁*/private void unlock(String key) {stringRedisTemplate.delete(key);}
}