Redis
には 3 つの大きな問題があります: キャッシュなだれ
、キャッシュの故障
、 キャッシュペネトレーション
、今日はキャッシュペネトレーション
について話します。
キャッシュ ブレークダウンに関連する理論的な記事をたくさん読んだと思いますが、特定のコードでそれを実装する方法や解決方法について混乱しているかもしれません。
今日は、Lao Tian がキャッシュ ブレークダウン ソリューションとコードの実装について説明します。
次のコードを見てください:
/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource private RedisTemplate<Long, String> redisTemplate; @Override public UserInfo findById(Long id) { //查询缓存 String userInfoStr = redisTemplate.opsForValue().get(id); //如果缓存中不存在,查询数据库 //1 if (isEmpty(userInfoStr)) { UserInfo userInfo = userMapper.findById(id); //数据库中不存在 if(userInfo == null){ return null; } userInfoStr = JSON.toJSONString(userInfo); //2 //放入缓存 redisTemplate.opsForValue().set(id, userInfoStr); } return JSON.parseObject(userInfoStr, UserInfo.class); } private boolean isEmpty(String string) { return !StringUtils.hasText(string); } }
プロセス全体:
如果,在//1
到//2
之间耗时1.5秒,那就代表着在这1.5秒时间内所有的查询都会走查询数据库。这也就是我们所说的缓存中的“缓存击穿
”。
其实,你们项目如果并发量不是很高,也不用怕,并且我见过很多项目也就差不多是这么写的,也没那么多事,毕竟只是第一次的时候可能会发生缓存击穿。
但,我们也不要抱着一个侥幸的心态去写代码,既然是多线程导致的,估计很多人会想到锁,下面我们使用锁来解决。
既然使用到锁,那么我们第一时间应该关心的是锁的粒度。
如果我们放在方法findById
上,那就是所有查询都会有锁的竞争,这里我相信大家都知道我们为什么不放在方法上。
/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource private RedisTemplate<Long, String> redisTemplate; @Override public UserInfo findById(Long id) { //查询缓存 String userInfoStr = redisTemplate.opsForValue().get(id); if (isEmpty(userInfoStr)) { //只有不存的情况存在锁 synchronized (UserInfoServiceImpl.class){ UserInfo userInfo = userMapper.findById(id); //数据库中不存在 if(userInfo == null){ return null; } userInfoStr = JSON.toJSONString(userInfo); //放入缓存 redisTemplate.opsForValue().set(id, userInfoStr); } } return JSON.parseObject(userInfoStr, UserInfo.class); } private boolean isEmpty(String string) { return !StringUtils.hasText(string); } }
看似解决问题了,其实,问题还是没得到解决,还是会缓存击穿,因为排队获取到锁后,还是会执行同步块代码,也就是还会查询数据库,完全没有解决缓存击穿。
由此,我们引入双重检查锁
,我们在上的版本中进行稍微改变,在同步模块中再次校验缓存中是否存在。
/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource private RedisTemplate<Long, String> redisTemplate; @Override public UserInfo findById(Long id) { //查缓存 String userInfoStr = redisTemplate.opsForValue().get(id); //第一次校验缓存是否存在 if (isEmpty(userInfoStr)) { //上锁 synchronized (UserInfoServiceImpl.class){ //再次查询缓存,目的是判断是否前面的线程已经set过了 userInfoStr = redisTemplate.opsForValue().get(id); //第二次校验缓存是否存在 if (isEmpty(userInfoStr)) { UserInfo userInfo = userMapper.findById(id); //数据库中不存在 if(userInfo == null){ return null; } userInfoStr = JSON.toJSONString(userInfo); //放入缓存 redisTemplate.opsForValue().set(id, userInfoStr); } } } return JSON.parseObject(userInfoStr, UserInfo.class); } private boolean isEmpty(String string) { return !StringUtils.hasText(string); } }
这样,看起来我们就解决了缓存击穿问题,大家觉得解决了吗?
回顾上面的案例,在正常的情况下是没问题,但是一旦有人恶意攻击呢?
比如说:入参id=10000000,在数据库里并没有这个id,怎么办呢?
第一步、缓存中不存在
第二步、查询数据库
第三步、由于数据库中不存在,直接返回了,并没有操作缓存
第四步、再次执行第一步.....死循环了吧
就是当缓存中和数据库中都不存在的情况下,以id为key,空对象为value。
set(id,空对象);
回到上面的四步,就变成了。
比如说:入参
id=10000000
,在数据库里并没有这个id,怎么办呢?第一步、缓存中不存在
第二步、查询数据库
第三步、由于数据库中不存在,以id为
key
,空对象为value
放入缓存中第四步、执行第一步,此时,缓存就存在了,只是这时候只是一个空对象。
代码实现部分:
/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource private RedisTemplate<Long, String> redisTemplate; @Override public UserInfo findById(Long id) { String userInfoStr = redisTemplate.opsForValue().get(id); //判断缓存是否存在,是否为空对象 if (isEmpty(userInfoStr)) { synchronized (UserInfoServiceImpl.class){ userInfoStr = redisTemplate.opsForValue().get(id); if (isEmpty(userInfoStr)) { UserInfo userInfo = userMapper.findById(id); if(userInfo == null){ //构建一个空对象 userInfo= new UserInfo(); } userInfoStr = JSON.toJSONString(userInfo); redisTemplate.opsForValue().set(id, userInfoStr); } } } UserInfo userInfo = JSON.parseObject(userInfoStr, UserInfo.class); //空对象处理 if(userInfo.getId() == null){ return null; } return JSON.parseObject(userInfoStr, UserInfo.class); } private boolean isEmpty(String string) { return !StringUtils.hasText(string); } }
布隆过滤器(Bloom Filter
):是一种空间效率极高的概率型算法和数据结构,用于判断一个元素是否在集合中(类似Hashset
)。它的核心一个很长的二进制向量和一系列hash函数
,数组长度以及hash函数
的个数都是动态确定的。
Hash函数:
SHA1
,SHA256
,MD5
..
布隆过滤器的用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:
URL
アドレスのクロールを避けるために URL
の重複を排除します。すべて 0 である bit 配列
を内部的に維持します。ブルーム フィルターには偽陽性率の概念があることに注意してください。偽陽性率が高くなるほど、偽陽性率が高くなります。率が高いほど誤検知率が高く、低い場合は配列が長くなり、より多くのスペースを占めることになります。偽陽性率が高くなるほど、アレイが小さくなり、占有スペースも小さくなります。ブルームフィルターの理論やアルゴリズムについてはここでは説明しませんので、興味のある方はご自身で勉強してください。
利点
O(k)
、一般的なアルゴリズムよりもはるかに優れています欠点
False Positive
),默认0.03
,随着存入的元素数量增加,误算率随之增加;代码实现:
/** * @author 田维常 * @公众号 java后端技术全栈 * @date 2021/6/27 15:59 */ @Service public class UserInfoServiceImpl implements UserInfoService { @Resource private UserMapper userMapper; @Resource private RedisTemplate<Long, String> redisTemplate; private static Long size = 1000000000L; private static BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), size); @Override public UserInfo findById(Long id) { String userInfoStr = redisTemplate.opsForValue().get(id); if (isEmpty(userInfoStr)) { //校验是否在布隆过滤器中 if(bloomFilter.mightContain(id)){ return null; } synchronized (UserInfoServiceImpl.class){ userInfoStr = redisTemplate.opsForValue().get(id); if (isEmpty(userInfoStr) ) { if(bloomFilter.mightContain(id)){ return null; } UserInfo userInfo = userMapper.findById(id); if(userInfo == null){ //放入布隆过滤器中 bloomFilter.put(id); return null; } userInfoStr = JSON.toJSONString(userInfo); redisTemplate.opsForValue().set(id, userInfoStr); } } } return JSON.parseObject(userInfoStr, UserInfo.class); } private boolean isEmpty(String string) { return !StringUtils.hasText(string); } }
使用Redis
实现分布式的时候,有用到setnx
,这里大家可以想象,我们是否可以使用这个分布式锁来解决缓存击穿的问题?
这个方案留给大家去实现,只要掌握了Redis
的分布式锁,那这个实现起来就非常简单了。
搞定缓存击穿
、使用双重检查锁
的方式来解决,看到双重检查锁
,大家肯定第一印象就会想到单例模式
,这里也算是给大家复习一把双重检查锁的使用。
悪意のある攻撃によるキャッシュ破壊については、弊社でも2つの対策を実施しておりますが、少なくとも仕事や面接では確実に対処できます。
また、ロックを使用する場合は、ロック強度
に注意してください。分散ロック
(Redis
またはZookeeper
実装)、キャッシュを導入したため、ほとんどの場合、複数のノードをデプロイし、同時に分散ロックを導入するため、メソッド 入力パラメータ id
を使用できます。使ってみたら、これはもっとワクワクするじゃないですか!
皆さんがテクノロジーを丸暗記するのではなく、この記事にあるアイデアの一部を理解できることを願っています。
以上がキャッシュ故障!コードの書き方も知りませんか? ? ?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。