面试知识点总结之redis

redis数据结构类型

string,hash,list,set,zset

 

 

redis持久化方式:(挂了重启恢复)

  1. RDB:RDB持久化机制,是对redis中的数据执行周期性的持久化(快照)

  2. AOF:AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集

 

redis的6中数据淘汰策略:

redis内存数据集大小上升到一定大小,实行数据淘汰策略

  1. volatile-lru:从一设置过期时间的数据集(server,db[i].expires)中挑选最近最少使用的数据淘汰

  2. volatile-ttl:挑选将要过期的数据淘汰

  3. volatile-random:任意选择数据淘汰

  4. allkeys-lru:从数据集(server.db[].dict)中挑选最近最少使用的数据淘汰

  5. allkeys-random:从数据集(server.db[].dict)中挑选任意选择数据淘汰

  6. no-enviction():禁止驱逐数据(默认配置)

 

redis缓存穿透,缓存击穿,缓存雪崩:

缓存系统过程:前台请求,后台先从缓存中取出数据,渠道直接返回结果,取不到从数据库中取,数据库取到更新缓存,并返回结果,数据库没取到,直接返回空结果;

 

缓存穿透:

指缓存和数据库中都没有数据,用户不断发起请求。流量大时,DB可能挂掉,要是有人用不存在的key频繁攻击,就是漏洞,如发起’id = -1‘或者id特别大

解决方案:

接口增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;从缓存取不到的数据在数据库也没有取到,可将key-value对写成key-null,缓存有效时间设置短,如30秒,这样可以防止同一个id暴力攻击。

 

缓存击穿:

缓存击穿指缓存中没有但数据库中有的数据(一般时缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时取数据库取数据,引起数据库压力瞬间增大,造成过大压力。

解决方案:

1. 设置热点数据永远不过期(并不是很好的变法)

2. 接口限流与降级,熔断;重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要做好降级准备,当接口中某些服务不可用时候,进行熔断,失败快速返回机制

3. 布隆过滤器。bloomfilter类似于一个hashset,用于快速判断某个元素是否存在于集合中,其典型应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键在于hash算法和容器大小

4. 加互斥锁:缓存中有数据直接返回,缓存中没数据,第一个进入的线程,获取锁并从数据库获取数据,没释放锁之前其他并行进入的线程会等待100MS,重新去缓存取数据,这样就防止都去数据库重复取数据,重复往缓存中更新数据的情况出现。

 

缓存雪崩:

是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机,和缓存穿透不同的是,缓存击穿指并发查同一条数据,缓存雪崩式不同数据都过期了,很多数据都查不到从而查数据库

解决方案:

1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生

2. 如果缓存数据库式分布式部署,将热点数据均匀分布在不同缓存数据库中

3. 设置热点数据永远不过期

 

redis与mysql如何保证数据的一致性

如果不是严格要求“缓存和数据库”必须保持一致性的话,最好不要做这个方案,即读请求和写请求串行化,串到一个内存队列里面去,串行化可以保证一定不会出现不一致的情况,但会导致系统吞吐量大幅度降低。

Cache Aside Pattern

  1. 读的时候先读缓存,如果缓存不存在的话就读数据库,取出数据库后更新缓存;如果存在的话直接读取缓存的信息。

  2. 写的时候,先更新数据库,再删除缓存,说到这个问题,会出现①为什么是删除缓存,而不是更新缓存②为什么是先更新数据库,再删除缓存,不是先删除缓存,再更新数据库?

写的时候为什么是删除缓存而不是更新缓存?

很多时候复杂的缓存场景,缓存不是仅仅从数据库中取出来值。可能是关联多张表的数据并通过计算才是缓存需要的值。并且,更新缓存的代价有时候很高。对于需要频繁写操作,而读操作很少的时候,每次进行数据库的修改,缓存也要随之更新,会造成系统吞吐的下降,但此时缓存并不会频繁访问到,用到的缓存才去算缓存。

  删除缓存而不是更新缓存时一种懒加载的思想,不是每次都重复更新缓存,只有用到的时候才去更新缓存,同时即使有大量的读请求,实际也就更新了一次,后面的请求不会重复读。

Cache Aside Pattern存在的问题

问题:先更新数据库,再删除缓存,如果更新缓存失败了,导致数据库中是新数据,缓存中是旧数据,就出现数据不一致的问题。

 

解决思路:先删除缓存,再更新数据库

缓存删除失败:如果缓存删除失败,那么数据库信息没有被修改,保持了数据一致性

缓存删除成功,数据库更新失败,此时数据库里的是旧数据,缓存是空的,查询时发现缓存不存在,就查数据库并更新缓存,数据保持一致性

问题:上面的方案存在不足,如删除完缓存更新数据库时,如果一个请求过来查询数据,缓存不存在,就查询数据库的旧数据,更新旧数据到缓存中。随后数据更新完成,修改了数据库的数据,此时不采用给缓存设置过期时间策略,该数据永远都是脏数据。

 

解决方案:采用双删除策略。写请求先删除缓存,再去更新数据库,等待一段时间后异步删除缓存,这样可以保证在读取错误数据时能及时被修正过来。

还有一种策略就是:写请求先修改缓存为指定值,然后再去更新数据库,再更新缓存。读请求过来后,先读缓存,判断是指定值后就进入循环读取状态,等到写请求更新缓存。如果循环超时就取数据库读取数据,更新缓存。这种方案保证了读写的一致性,但由于读请求等待写请求的完成,会降低系统的吞吐量。

缓存穿透击穿与雪崩实现代码:

(1)使用的是Google的Bloom Filter
//引入依赖
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
</dependency>

(2)使用双重验证锁解决高并发环境下的缓存穿透问题
@Service
public class StudentServiceImpl implements StudentService {
    @Autowired
    private StudentMapper studentMapper;
    //springboot自动配置的,直接注入到类中即可使用
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    /**
     * 查询所有学生信息,带有缓存
     * @return
     */
    public List<Student> getAllStudent() {
        //在高并发条件下,会出现缓存穿透
        List<Student> studentList = (List<Student>)redisTemplate.opsForValue().get("allStudent");
        if (null == studentList) {
            //5个人, 4个等,1个进入
            synchronized (this) {
                //双重检测锁,假使同时有5个请求进入了上一个if(null == studentList),
                //加了锁之后one by one 的访问,这里再次对缓存进行检测,尽一切可能防止缓存穿透的产生,但是性能会有所损失
                studentList = (List<Student>)redisTemplate.opsForValue().get("allStudent");
                if (null == studentList) {
                    studentList = studentMapper.getAllStudent();
                    redisTemplate.opsForValue().set("allStudent", studentList);System.out.println("请求的数据库。。。。。。");} else {//System.out.println("请求的缓存。。。。。。");
                }}} else {System.out.println("请求的缓存。。。。。。");
        }return studentList;}} 
(1)加互斥锁,互斥锁参考代码如下:
static Lock reenLock = new ReentrantLock();
public List<String> getData04() throws InterruptedException {
        List<String> result = new ArrayList<String>();
        // 从缓存读取数据
        result = getDataFromCache();
        if (result.isEmpty()) {
            if (reenLock.tryLock()) {
                try {
                   System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
                     // 从数据库查询数据
                       result = getDataFromDB();
                    // 将查询到的数据写入缓存
                    setDataToCache(result);
                } finally {
                reenLock.unlock();// 释放锁
                }
            } else {
            result = getDataFromCache();// 先查一下缓存
            if (result.isEmpty()) {
                System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
                Thread.sleep(100);// 小憩一会儿
            return getData04();// 重试
            }
        }
        }
        return result;
}

 

posted @ 2021-03-12 19:35  白玉神驹  阅读(84)  评论(0编辑  收藏  举报