🎉🎉欢迎光临,终于等到你啦🎉🎉
🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀
🌟持续更新的专栏Redis实战与进阶
本专栏讲解Redis从原理到实践
这是苏泽的个人主页可以看到我其他的内容哦👇👇
努力的苏泽http://suzee.blog.csdn.net
最近超级无敌忙 就断更好久了 实在是抽不出时间来 没办法 这篇文章也只是整理我以前学习的资料 目前还有一整套企业级的Redis处理方案没写哈 敬请期待朋友们
下面是正文
目录
首先我们要明白什么是缓存击穿
分析有什么办法能解决
业务解析
编辑
代码实现:
然后我们将缓存穿透的函数给封装起来
原函数
我们将这部分逻辑 封装到 queryWithPassThrough中
我们再写一个函数queryWithMutex来用互斥锁解决缓存穿透的问题
于是原本的查询函数的结构就变成了这样
这样做的优缺点
优点:
缺点:
下一篇我们来讲解如何使用另一个方案解决这个问题
首先我们要明白什么是缓存击穿
Redis缓存击穿是指在高并发的情况下,当某个热点数据的缓存过期或不存在时,大量的请求同时涌入数据库或后端服务,导致数据库或后端服务负载过高,甚至崩溃的情况。
分析有什么办法能解决
当涉及到并发访问共享资源时,互斥锁和逻辑过期是两种常用的技术手段。
- 互斥锁(Mutex):
互斥锁是一种并发控制机制,用于在多个线程或进程之间保证共享资源的互斥访问。它通过在关键代码段前后设置锁来确保同一时间只有一个线程或进程可以执行关键代码段。当某个线程或进程获取到互斥锁时,其他线程或进程需要等待锁的释放才能继续执行。
业务解析
代码实现:
先写两个函数 一个加锁 一个释放锁
private boolean tryLock(String key){
//自定义互斥锁 将申请锁的结果返回
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
然后我们将缓存穿透的函数给封装起来
原函数
public Result queryById(Long id) {
//1.从Redis查询id 这里使用的数据结构可以是String也可以是hash 若是查询不到就为空了 CACHE_SHOP_KEY就是"cache:shop:"
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在 在字符串意义上是否为空
if (StrUtil.isNotBlank(shopJson)) {
//3.存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//3.判断是否命中写入redis中的null
if(shopJson != null){
return Result.fail("店铺信息不能为空!");
}
//4.不存在 查询数据库
Shop shop = getById(id);
//5.数据库中不存在 返回报错
if (shop == null){
//空值写入redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, null,CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
//6.数据库中存在 写入Redis 并返回
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
return Result.ok(shop);
}
我们将这部分逻辑 封装到 queryWithPassThrough中
//封装缓存穿透函数
private Shop queryWithPassThrough(Long id){
//1.从Redis查询id 这里使用的数据结构可以是String也可以是hash 若是查询不到就为空了 CACHE_SHOP_KEY就是"cache:shop:"
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否 在字符串意义上是否为空
if (StrUtil.isNotBlank(shopJson)) {
//3.存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//3.判断是否命中写入redis中的null
if(shopJson != null){
return null;
}
Shop shop = getById(id);
//5.数据库中不存在 返回报错
if (shop == null){
//空值null写入redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, null,CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.数据库中存在 写入Redis 并返回
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
return shop;
}
我们再写一个函数queryWithMutex来用互斥锁解决缓存穿透的问题
于是原本的查询函数的结构就变成了这样
public Result queryById(Long id) {
//缓存穿透
// Shop shop = queryWithPassThrough(id);
//互斥锁解决缓存穿透
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在");
}
//返回
return Result.ok(shop);
}
//解决缓存穿透的问题
private Shop queryWithMutex(Long id) throws InterruptedException{
String lockKey = "lockKey" + id;
while (true) {
//1.从Redis查询id 这里使用的数据结构可以是String也可以是hash 若是查询不到就为空了 CACHE_SHOP_KEY就是"cache:shop:"
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否 在字符串意义上是否为空
if (StrUtil.isNotBlank(shopJson)) {
//3.存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//3.判断是否命中写入redis中的null
if(shopJson != null){
return null;
}
//4重建缓存
//4.1申请互斥锁
boolean flag = tryLock(lockKey);
//4.2判断是否成功
if (!flag){
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
else//成功了就跳出循环 这里不敢用递归 怕栈溢出了
break;
}
//4.4 成功 查询数据库
Shop shop = getById(id);
//5.数据库中不存在 返回报错
if (shop == null){
//空值null写入redis
stringRedisTemplate.opsForValue().set("cache:shop:" + id, null,CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.数据库中存在 写入Redis 并返回
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
//7.释放互斥锁
unLock(lockKey);
return shop;
}
这样做的优缺点
互斥锁作为一种并发控制机制,在解决缓存击穿问题时具有以下优点和缺点:
优点:
-
确保数据一致性:互斥锁可以确保同一时间只有一个线程或进程可以访问共享资源,避免了并发访问导致的数据不一致性问题。
-
避免竞态条件:互斥锁可以防止多个线程或进程同时执行关键代码段,避免了竞态条件的发生。竞态条件是指多个线程或进程对共享资源的访问顺序不确定,导致结果的不可预测性。
-
简单易用:互斥锁的使用相对简单,可以通过加锁和解锁操作来控制对共享资源的访问。
缺点:
-
性能开销:互斥锁在多线程环境下会引入一定的性能开销。当多个线程竞争同一个锁时,其他线程需要等待锁的释放,这会导致一些线程的阻塞和等待,降低系统的并发性能。
-
可能引发死锁:如果在使用互斥锁时处理不当,可能会发生死锁的情况。死锁是指多个线程或进程相互等待对方持有的资源,导致所有线程都无法继续执行。
-
容易导致线程饥饿:当某个线程持有互斥锁并长时间不释放时,其他线程可能会一直等待锁的释放,导致线程饥饿现象出现。