场景
也是热点key问题,就是一个高并发访问并且缓存重建较为复杂的key突然失效了,这里的key失效可以理解为某电商平台在节日大促,同时段大量请求访问一个商品,这个商品key会存在一个固定TTL,若TTL到时了,key消失,仍有大量请求访问该商品,这个key的重建业务复杂,耗时又高。于是,请求都来到数据库拿数据,瞬间给数据库造成了巨大的压力。
解决方案
互斥锁
在发起请求未命中redis缓存时,表示此信息不存在,或过期,尝试获取锁。
若没拿到锁,表示此数据正在被更新,线程进行休眠再递归重新从缓存获取数据。
若拿到了锁,根据id查数据库,将数据信息写入redis并释放互斥锁,返回数据。
ps:图片来自B站黑马程序员
redis中有命令setnx key value
表示key不存在就set,存在就无法赋值,这就可以应用到锁中
对应方法setIfAbsent(),再设置过期时间,避免死锁
1 2 3 4 5 6 7
| private boolean tryLock(String key) { Boolean flag = stringRedisTemplate .opsForValue() .setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); }
|
模拟:封装在了queryWithMutex()中,开始店铺信息不存在redis中,当请求需要获取店铺信息时,redis没命中,那么获取锁,
进行缓存写入操作,成功后释放锁并返回数据。然后其他请求就能从redis中拿到数据了。
ps:其中有部分缓存穿透操作,请前往redis缓存穿透
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| @Resource private StringRedisTemplate stringRedisTemplate;
@Override public 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 shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)) { return JSONUtil.toBean(shopJson, Shop.class); } if (shopJson != null) { return null; } String lockKey = LOCK_SHOP_KEY + id; Shop shop = null; try { boolean isLock = tryLock(lockKey); if (!isLock) { Thread.sleep(50); return queryWithMutex(id); } shop = getById(id); Thread.sleep(200); if (shop == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { unLock(lockKey); }
return shop; }
private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); }
private void unLock(String key) { stringRedisTemplate.delete(key); }
|
锁最终需要被释放,所以try的finally需要释放锁。
理论预期,只会进行一次数据库查询,需要在高并发条件模拟大量请求到来,只有一个线程获取锁,写缓存,其他请求递归查询,等待,直到写入缓存成功,理论全部请求会通过,只是耗时问题)
redis无缓存,大量请求到来,数据库查询次数一次,并存入redis,释放锁
逻辑过期