通辽做网站制作,创建一个网站需要什么,国内免费saas crm正在,餐饮网站设计公司redis缓存击穿和缓存穿透的封装 一、首先是互斥锁二、封装为工具类三、调用四、数据预热五、缓存更新的CacheAside方案 #xff08;来源黑马redis#xff09; 一、首先是互斥锁 //拿到锁private boolean tryLock(String key) {Boolean flag stringRedisTemplate.opsForValue… redis缓存击穿和缓存穿透的封装 一、首先是互斥锁二、封装为工具类三、调用四、数据预热五、缓存更新的CacheAside方案 来源黑马redis 一、首先是互斥锁 //拿到锁private boolean tryLock(String key) {Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);//相当于setnxreturn BooleanUtil.isTrue(flag);//判断是否成功,因为直接返回可能会导致拆箱}//释放锁private void unlock(String key){stringRedisTemplate.delete(key);}解释 tryLock 方法 这个方法尝试获取一个分布式锁使用 Redis 的 setIfAbsent 方法来实现。
1.方法签名: private boolean tryLock(String key)
key: 锁的键名用于在 Redis 中标识这个锁。 返回值: 如果成功获取锁则返回 true否则返回 false。
2.方法内部逻辑:
使用 stringRedisTemplate.opsForValue().setIfAbsent(key, “1”, 10, TimeUnit.SECONDS) 尝试在 Redis 中设置一个键值对。 key: 锁的键名。 “1”: 锁的值这里只是一个占位符表示该键已被锁定。 10, TimeUnit.SECONDS: 设置锁的过期时间为 10 秒。这是为了防止死锁即某个进程获取了锁但未能正确释放导致其他进程无法获取锁。 setIfAbsent 方法相当于 Redis 的 SETNX 命令它会在键不存在时设置键值对并返回 true如果键已存在则不做任何操作并返回 false。 BooleanUtil.isTrue(flag) 用于判断 setIfAbsent 的返回值。这里进行了安全的布尔值判断避免了自动拆箱可能引发的 NullPointerException。
unlock 方法 这个方法用于释放之前获取的分布式锁。 方法签名: private void unlock(String key) key: 需要释放的锁的键名。 方法内部逻辑: 使用 stringRedisTemplate.delete(key) 来删除 Redis 中的锁键。这相当于释放了锁使得其他进程可以尝试获取该锁。 这个方法没有返回值因为它只是简单地执行删除操作。 注意事项 在实际的生产环境中你可能需要处理更多的边界情况和异常例如网络错误、Redis 服务器故障等。 为了防止误删其他进程的锁你可能需要在删除前验证锁的值是否与你设置的值相匹配。 在高并发的场景下你可能需要考虑使用更复杂的锁机制例如 RedLock 算法以提高锁的可靠性和安全性。 在某些情况下你可能需要处理锁续期的问题特别是当锁的持有时间可能超过你最初设置的过期时间时。这可以通过定时任务或后台线程来实现。
二、封装为工具类
Slf4j
Component
public class CaCheClient {private StringRedisTemplate stringRedisTemplate;//注入操作redis。/*** 构造函数用于初始化StringRedisTemplate。*/public CaCheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate stringRedisTemplate;}/*** 向Redis中设置键值对并指定过期时间。** param key 键* param value 值* param time 过期时间* param unit 时间单位*/public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value) , time, unit);}/*** 使用逻辑过期方式向Redis中设置键值对。** param key 键* param value 值* param time 过期时间* param unit 时间单位*/public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {//设置逻辑过期RedisData redisDatanew RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));//存入redis中stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}/*** 使用互斥锁实现缓存穿透处理的逻辑。** param keyPreFix 键前缀* param id 唯一标识符* param type 返回对象的类型* param dbFallback 当缓存不存在时从数据库获取数据的函数因为有参数有返回值* param time 缓存过期时间* param unit 时间单位* param R 返回对象的类型* param ID 唯一标识符的类型* return 返回查询到的对象*/public R,ID R queryWithPassThrough(String keyPreFix, ID id, ClassR type, FunctionID,R dbFallback,Long time,TimeUnit unit){//我们不知道返回什么类型所以定义泛型R RString key keyPreFix id; //定义一个key包装id和一个字段名//1.从redis中查询商铺缓存String Json stringRedisTemplate.opsForValue().get(key);//存的是一个对象可以用哈希也可以用String这里用hash演示//2.判断是否存在if (StrUtil.isNotBlank(Json)) {//isNotBlank只有里面有字符的时候才是truenull和空或者/n都为空false//3.存在直接返回//把JSON对象转化为shop对象return JSONUtil.toBean(Json, type);}//判断命中的是否是空值if (Json ! null) {//返回一个错误信息return null;}//防止缓存穿透缓存穿透是指恶意请求或者不存在的数据请求导致大量的查询直接访问数据库而绕过了缓存层。在这段代码中如果 Json 不为空即缓存中存在值但其实际内容为null则这可能是一个早前缓存的结果数据库中确实没有对应数据。在这种情况下直接返回 null避免继续查询数据库从而节省资源。//4.不存在根据id查询数据库// R r getById(id);//因为我们这里需要去查询一个有参有返回值的函数所以我们在上面定义Function难点R r dbFallback.apply(id);//FunctionID,R dbFallback这里传入id返回R//5.不存在返回错误if (r null) {//将空值写入redisstringRedisTemplate.opsForValue().set(key,null,2L, TimeUnit.MINUTES);//返回错误信息return null;}//6.存在把数据写入redisthis.set(key, r, time, unit);//7.然后返回。return r;}/*** 创建一个线程池*/private static final ExecutorService CACHE_BUILDER_EXECUTOR Executors.newFixedThreadPool(10);//获得十个线程/*** 使用逻辑过期解决缓存击穿问题的查询方法。** param keyPreFix 键前缀* param id 唯一标识符* param type 返回对象的类型* param dbFallback 当缓存不存在或过期时从数据库获取数据的函数* param time 缓存过期时间* param unit 时间单位* param R 返回对象的类型* param ID 唯一标识符的类型* return 返回查询到的对象*/public R,ID R queryWithLogicalExpire(String keyPreFix, ID id, ClassR type, FunctionID,R dbFallback,Long time,TimeUnit unit){String key keyPreFix id; //定义一个key包装id和一个字段名//1.从redis中查询商铺缓存String Json stringRedisTemplate.opsForValue().get(key);//存的是一个对象可以用哈希也可以用String这里用hash演示//2.判断是否存在if (StrUtil.isBlank(Json)) {//isNotBlank只有里面有字符的时候才是truenull和空或者/n都为空false//3.存在直接返回// 如果缓存中的值为空包括 null、空字符串或者只包含空白字符则直接返回 null。// 这是为了避免缓存穿透即使缓存中有值但实际上没有有效数据时也不去访问数据库而是直接返回空结果。//把JSON对象转化为shop对象return null;}//RedisData里面有两个参数 private LocalDateTime expireTime;private Object data;data用来存储数据//4.命中需要先把json反序列化为对象RedisData redisData JSONUtil.toBean(Json, RedisData.class);Object data redisData.getData();R r JSONUtil.toBean((JSONObject) data,type);LocalDateTime expireTime redisData.getExpireTime();//5.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {//意思是过期时间expireTime..是不是在当前LocalDateTime.now()时间之后.isAfter//5.1 未过期直接返回店铺信息return r;}//5.2 过期了需要缓存重建//6 缓存重建//6.1获取互斥锁String lockKey lock:shop: id; //定义一个key包装id和一个字段名boolean isLock tryLock(lockKey);//6.2判断是否获取锁成功if (isLock){//6.3 成功开启独立线程实现缓存重建CACHE_BUILDER_EXECUTOR.submit(() - {try {//重建缓存//1.R r1 dbFallback.apply(id);//apply(id) 方法: dbFallback.apply(id) 调用了函数式接口 dbFallback 的 apply 方法传入了参数 id这个方法的作用是根据 id 从数据库中获取数据并返回。//2.写入redisthis.setWithLogicalExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {//释放锁unlock(lockKey);}});}return r;}//拿到锁private boolean tryLock(String key) {Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);//相当于setnxreturn BooleanUtil.isTrue(flag);//判断是否成功,因为直接返回可能会导致拆箱}//释放锁private void unlock(String key){stringRedisTemplate.delete(key);}
}
三、调用
Service
public class ShopServiceImpl extends ServiceImplShopMapper, Shop implements IShopService {Resourceprivate StringRedisTemplate stringRedisTemplate;Resourceprivate CaCheClient caCheClient;Overridepublic Result queryById(Long id) {//首先根据id在redis中查询店铺缓存//缓存穿透访问不存在的id来测试// Shop shop caCheClient// .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);//用互斥锁解决缓存击穿// Shop shop queryWithMutex(id);//用逻辑过期解决缓存击穿用jmt快速访问测试Shop shop caCheClient.queryWithLogicalExpire(CACHE_SHOP_KEY,id,Shop.class,this::getById, 20L, TimeUnit.SECONDS);if (shop null) {return Result.fail(店铺不存在);}//7.然后返回。return Result.ok(shop);}四、数据预热
/*** 数据的预热就是在很多活动开始前会提前导入数据方便访问* param id* param expireSeconds*/public void saveShop2Redis(Long id,Long expireSeconds){//我们传入的这两个数据一个是用来查询的一个是用来设置过期时间的都是自己定义的//1.查询店铺数据Shop shop getById(id);//用过Mp来查询id获得店铺信息//2.封装逻辑过期时间RedisData redisData new RedisData();//创建一个对象用来接受数据和过期时间然后一起传进去我觉得这个和手动设置也没啥区别啊redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//3.写入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id,JSONUtil.toJsonStr(redisData));//通过string类型写入}
五、缓存更新的CacheAside方案
CacheAside缓存调用者在更新数据库的同时完成对缓存的更新先操作数据库后缓存 这里就有面试题了 (使用事务保证数据库与缓存的操作原子性 OverrideTransactionalpublic Result update(Shop shop) {Long id shop.getId();if (id null) {return Result.fail(店铺id不能为空);}//1.更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY id);//3.返回结果return Result.ok();}