黑马点评-Redis缓存更新/穿透/雪崩/击穿
文章目录
- 商户查询缓存
- 缓存更新
- 内存淘汰
- 超时剔除
- 主动更新
- 缓存穿透
- 缓存空对象
- 布隆过滤
- 缓存雪崩
- 缓存击穿
- 互斥锁
- 逻辑过期
商户查询缓存
缓存更新
根据商铺id查询商铺信息,
在我们之前的想法就是,根据key查redis,redis里有的话直接返回其中的数据,如果没有,就查数据库的数据返回,并同时将数据库里查到的数据保存在redis里,这样下一次查询的速度就会更快。
但是我们要考虑到一个问题:redis里的数据并不是一直和数据库里的数据同步。当我们修改了数据库数据,但是redis里保存的还是原来的,这是就有错误了。
这里使用缓存更新策略
内存淘汰
不用自己维护,redis内部的淘汰机制会在内存不足时自动淘汰部分数据。
超时剔除
给缓存数据加上TTL时间,到期后自动删除缓存。
主动更新
编写业务逻辑,修改数据库同时更新缓存。
这里我们使用主动更新。
1.根据id查询店铺,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置缓存时间。
2.根据id修改店铺,先修改数据库,再删除缓存。
这里先修改数据库,再删缓存的顺序是怎么确定的呢?
我们这里直接看会出现问题的场景。
- 假设先删缓存,再操作数据库(线程A)
A删除了缓存,B这时候来查没有,查到数据库(假设是20),然后写入缓存(20)。然后A继续它的步骤,更新数据库(假设是10),这个时候,缓存(20)和数据库(10)不一致。
- 假设先操作数据库,再删缓存(线程B)
A先查询缓存,恰好失效了没命中,查数据库(10),但此时立刻来了B来更新数据库(改为20),然后删除缓存,最后A将之前查到的数据库(10)写入缓存,最后导致缓存(10)和数据库(20)不一致。
两种情况都会出现问题,但是由于第二种情况出现的概率较低,所以采用第二种方案。
代码:
@Override@Transactionalpublic 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);return Result.ok();}
缓存穿透
缓存穿透是指请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
如果恶意请求无数次,会使数据库崩溃,我们的解决方案:
缓存空对象
就是再次来请求时,直接返回null,这样就不会再次访问数据库。
简单粗暴,但造成额外的内存消耗和造成短期的不一致(请求的id加入此时真的插入了一条商铺数据,但我们已经给人家返回null导致不一致,只有当TTL过期后用户才能查到数据)(均可设置TTL尽量解决)
布隆过滤
一种算法思想,由一个二进制向量(或者说位数组)和多个哈希函数组成。
请求时先请求过滤器,发现数据不存在直接拒绝,存在后,则按照寻常步骤进行。
当然,这里过滤器里存的不是那些数据,而是将数据进行哈希处理后得到二进制位(如果所有位置都是1,就认为元素可能存在;如果有任何一个位置是0,则元素肯定不存在)以此判断。
但是仍有问题:不存在则是真不存在,存在的话但数据不一定真的存在(哈希冲突)
缺点:实现复杂,存在误判
所以,我们这里选择缓存空对象方案。
String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值,是也不能去查数据库if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);return r;
这里需要说明:
StrUtil.isNotBlank(json)
从redis里查数据,返回的结果只要不是字符串,其他均为false.
所以可能返回空值‘’‘’和null。
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
-
不同key添加一个随机TTL值
避免同时失效
-
利用redis集群提高服务可用性
-
添加降级限流策略
-
添加多级缓存
缓存击穿
缓存击穿也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大冲击。
解决方案:
互斥锁
当线程1第一个发现未命中时,获取互斥锁,查询数据库数据写入缓存后释放锁,而在这过程中,其他线程来也发现未命中,但他们获取锁失败,所以它们不能执行接下来的操作,而是一直重试等待,直到命中缓存。
优点:无额外内存消耗,保证数据一致性;实现简单
缺点:线程需要等待,影响性能;可能产生死锁
逻辑过期
因为问题出现在过期时间,所以我们这里就不设置TTL。这样我们可以看作key”永不过期“
物理过期:Redis自带的TTL机制,到了时间自动删除
逻辑过期:业务层面的控制,数据在Redis里不设置过期时间,但是存储的数据结构中包含一个过期时间字段
由于没有TTL,所有线程来都会命中(没命中说明数据是真的不存在)。当线程未命中时,它获取到锁,然后由它产生一个新线程负责去查询写入操作,而线程1和其他线程则在锁释放前直接返回旧数据。
优点:线程无需等待,性能好
缺点:不保证数据一致性;额外内存消耗;实现复杂
基于互斥锁来解决根据ID修改商铺问题:
这里我们的互斥锁采用的是redis里的setnx。
SETNX key value
指定的key不存在时,将key的值设为value,如果设置成功返回1,如果key已经存在,就不做任何操作,返回0。
多个客户端同时尝试设置同一个key,只有一个能成功,从而获得锁
public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson != null) {// 返回一个错误信息return null;}// 4.实现缓存重建// 4.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功,根据id查询数据库r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}
private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}
基于逻辑过期来解决根据ID修改商铺问题:
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return r;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){// 6.3.成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R newR = dbFallback.apply(id);// 重建缓存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的商铺信息return r;}
public class RedisData {private LocalDateTime expireTime;//逻辑过期时间private Object data;
}
>W<