前言
最近在学Redis 想象一下,当你的应用程序需要处理大量的请求时,Redis就像是一位快递小哥,把数据飞快地送到用户手中。
用Redis不学其原理,就像四大名著不看红楼梦,说明这个人文学造诣和自我修养不足,他理解不了这种内在的阳春白雪的高雅艺术,他只能看到外表的辞藻堆砌,参不透其中深奥的精神内核,他整个人的层次就卡在这里了,只能度过一个相对失败的人生。
一、什么是缓存击穿?
在我们的业务中,经常会出现一些数据被频繁地访问 的情况,例如在秒杀活动中。这些被频繁访问的数据被称为热点数据。当缓存中的某个热点数据过期 时,如果大量的请求 访问该热点数据,由于无法从缓存中读取,这些请求将直接访问 数据库。然而,由于高并发的请求量,数据库很容易被冲垮,这就是缓存击穿。
二、解决方案
有两种常见的解决方案:
- 互斥锁 :重视数据一致性 ,在后续学习分布式中能更深有体会。
- 逻辑过期 :(从前有个火之国的木叶隐村有个叫"火影"的大牛,懒得做的事会让一众手下的忍者去处理,多好啊!)
三、实现
互斥锁(Mutex) :引用Redis官方文档:https://redis.io/commands/setnx/的Example (和React一样体验感拉满了)来简单说明一下互斥锁:
c
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis>
可谓一山不容二虎 SETNX 我个人喜欢把它作为互斥锁的抽象概念(抽象的概念在《深入理解计算机系统》中被反复强调!!!)。 好吧,我是在拿它挡刀。不过可以确定的是:mykey在已经有value的情况下,禁止你再把新值放进去了!!! 直接上代码:
java
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 获取锁
* @param key
* @return
*/
public boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "任意值", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*
* @param key
*/
public void unlock(String key) {
stringRedisTemplate.delete(key);
}
@Override
public Result queryById(Long id) {
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺不存在");
}
return Result.ok();
}
/**
* 互斥锁
*
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
//1.获取缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在则直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否为空字符串
if ("".equals(shopJson)) {
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);
queryWithMutex(id);
}
//进行DoubleCheck,再次检查换缓存是否存在,若存在则无需构建缓存(重复1 2步即可)
key = CACHE_SHOP_KEY + id;
shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
//3.存在则直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//4.4成功,根据id查询数据库
shop = getById(id);
//模拟重建的演示
Thread.sleep(200);
//5.不存在,返回错误
if (shop == null) {
//将空值写入Redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回错误信息
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7.释放互斥锁(不管缓存构建过程中有无异常,都得解锁)
unlock(lockKey);
}
//8.返回
return shop;
}
}
压测成功!2k 次请求只查了一次数据库,锁生效了! 查看redis缓存确实存进去了,
由于互斥锁适合处理高并发的场景,逻辑过期适合处理逻辑较为复杂的业务场景,等找到了一个合适的复杂业务再来介绍o(╥﹏╥)o
扯皮:
最近听到S.H.E的<<你曾是少年>>,哎呦不错哦~
你我來自湖北 四川 廣西 寧夏 河南 山東 貴州 雲南的小鎮鄉村 曾經發誓 要做了不起的人 卻在北京 上海 廣州 深圳 某天夜半忽然醒來 像被命運叫醒了 它說你不能就這樣過完一生