Heim  >  Artikel  >  Java  >  Cache-Panne! Sie wissen nicht einmal, wie man Code schreibt? ? ?

Cache-Panne! Sie wissen nicht einmal, wie man Code schreibt? ? ?

Java后端技术全栈
Java后端技术全栈nach vorne
2023-08-24 15:59:02843Durchsuche


inRedis中有三大问题:缓存雪崩缓存击穿缓存穿透,今天我们来聊聊缓存击穿.

Ich glaube, Sie haben viele theoretische Artikel zum Thema Cache-Aufschlüsselung gelesen, aber Sie sind möglicherweise verwirrt, wie Sie es in den spezifischen Code implementieren und wie Sie es lösen können.

Heute zeigt Ihnen Lao Tian die Cache-Aufschlüsselungslösung und die Code-Implementierung.

Szenario

Bitte schauen Sie sich den Code unten an:

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

Der gesamte Prozess:

Cache-Panne! Sie wissen nicht einmal, wie man Code schreibt? ? ?

如果,在//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..

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

  • Webcrawler URL Adresse
  • URL的去重,避免爬取相同的URL地址
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(垃圾短信)
  • 缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。
  • 其内部维护一个全为0的bit数组,需要说明的是,布隆过滤器有一个误判率的概念,误判率越低,则数组越长,所占空间越大。误判率越高则数组越小,所占的空间越小。布隆过滤器的相关理论和算法这里就不聊了,感兴趣的可以自行研究。

    优势和劣势

    优势

    • 全量存储但是不存储元素本身,在某些对保密要求非常严格的场合有优势;
    • 空间高效率
    • 插入/查询时间都是常数O(k),远远超过一般的算法

    劣势

    Anti-Spam, ermitteln Sie aus Milliarden von Spam-Listen, ob es sich bei einem bestimmten Postfach um Spam (Spam-Textnachricht) handelt. 🎜🎜Cache-Aufschlüsselung, wird bereits vorhanden sein. Der Cache wird in einem Bloom platziert Wenn ein Hacker auf einen Cache zugreift, der nicht existiert, kehrt er schnell zurück, um Cache- und DB-Aufhänger zu vermeiden.

    Es behält einen internen Vor- und Nachteile

    Vorteile🎜🎜🎜🎜Speicherung vollständig, aber nicht des Elements selbst, was in manchen Situationen, in denen die Vertraulichkeitsanforderungen sehr streng sind, Vorteile hat;🎜 🎜 Hohe Platzeffizienz🎜🎜Einfügungs-/Abfragezeit ist konstantO(k), weit über den allgemeinen Algorithmus hinaus

    Nachteile🎜

    • 存在误算率(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的分布式锁,那这个实现起来就非常简单了。

    总结

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

    Aufgrund des durch böswillige Angriffe verursachten Cache-Ausfalls haben wir auch zwei Lösungen implementiert. Zumindest bei der Arbeit und bei Interviews können wir definitiv damit umgehen.

    Achten Sie bei der Verwendung des Schlosses auch darauf, es zu verwenden, macht es auf diese Weise nicht mehr Spaß? 锁的力度,这里建议换成分布式锁(Redis或者Zookeeper实现),因为我们既然引入缓存,大部分情况下都会是部署多个节点的,同时,引入分布式锁了,我们就可以使用方法入参id

    Ich hoffe, dass jeder einige der Ideen im Artikel verstehen kann und sich die Technologie nicht auswendig merken kann.

    Das obige ist der detaillierte Inhalt vonCache-Panne! Sie wissen nicht einmal, wie man Code schreibt? ? ?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

    Stellungnahme:
    Dieser Artikel ist reproduziert unter:Java后端技术全栈. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen