首頁 >Java >java教程 >緩存擊穿!竟然不知道怎麼寫程式碼? ? ?

緩存擊穿!竟然不知道怎麼寫程式碼? ? ?

Java后端技术全栈
Java后端技术全栈轉載
2023-08-24 15:59:02919瀏覽


Redis有三大問題:快取雪崩快取擊穿快取穿透,今天我們來聊聊快取擊穿

關於快取擊穿相關理論文章,相信大家已經看過不少,但是具體程式碼中是怎麼實現的,怎麼解決的等問題,可能就一臉懵逼了。

今天,老田就帶大家來看看,快取擊穿解決和程式碼實作。

場景

請看下面這段程式碼:

/** 
 * @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,怎么办呢?

第一步、缓存中不存在

第二步、查询数据库

第三、由于数据库中不存在,直接返回了,并没有操作缓存

第四步、再次执行第一步.....死循环了吧

方案1:设置空对象

就是当缓存中和数据库中都不存在的情况下,以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);
    }
}

方案2 布隆过滤器

布隆过滤器(Bloom Filter):是一种空间效率极高的概率型算法和数据结构,用于判断一个元素是否在集合中(类似Hashset)。它的核心一个很长的二进制向量和一系列hash函数,数组长度以及hash函数的个数都是动态确定的。

Hash函数:SHA1SHA256MD5..

布隆过滤器的用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:

  • 網頁爬蟲對URL的去重,避免爬取相同的URL位址
  • ##反垃圾郵件,從數十億個垃圾郵件清單中判斷某郵箱是否垃圾信箱(垃圾簡訊)
  • 快取擊穿,將已存在的快取放到布隆過濾器中,當駭客存取不存在的快取時迅速回傳避免快取及DB掛掉。
其內部維護一個全為0的

bit數組,需要說明的是,布隆過濾器有一個誤判率的概念,誤判率越低,則數組越長,所佔空間越大。誤判率越高則數組越小,所佔的空間越小。布隆過濾器的相關理論和演算法這裡就不聊了,有興趣的可以自行研究。

優勢與劣勢

  • #全量儲存但是不儲存元素本身,在某些對保密要求非常嚴格的場合有優勢;
  • 空間高效率
  • 插入/查詢時間都是常數
    O(k),遠超過一般的演算法

#

  • 存在误算率(False Positive),默认0.03,随着存入的元素数量增加,误算率随之增加;
  • 一般情况下不能从布隆过滤器中删除元素;
  • 数组长度以及hash函数个数确定过程复杂;

代码实现:

/** 
 * @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);
    }
}

方案3 互斥锁

使用Redis实现分布式的时候,有用到setnx,这里大家可以想象,我们是否可以使用这个分布式锁来解决缓存击穿的问题?

这个方案留给大家去实现,只要掌握了Redis的分布式锁,那这个实现起来就非常简单了。

总结

搞定缓存击穿、使用双重检查锁的方式来解决,看到双重检查锁,大家肯定第一印象就会想到单例模式,这里也算是给大家复习一把双重检查锁的使用。

由於惡意攻擊導致的快取擊穿,解決方案我們也實現了兩種,至少在工作和麵試中,肯定是能應付了。

另外,使用鎖定的時候注意鎖定的力道,這裡建議換成分散式鎖定(RedisZookeeper實作),因為我們既然引入緩存,大部分情況下都會是部署多個節點的,同時,引入分散式鎖了,我們就可以使用方法入參id用起來,這樣是不是更爽!

希望大家能領悟到的是文中的一些思路,並不是死記硬背技術。

#

以上是緩存擊穿!竟然不知道怎麼寫程式碼? ? ?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:Java后端技术全栈。如有侵權,請聯絡admin@php.cn刪除