Redis学习记录1
缓存穿透
定义概念 :
缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。
比如http://localhost:15080/vat-admin-gateway/rc/rcModel/selectById/id=0
显然id不能等于0,从1开始
解决方案 :
(1)业务层校验,先对参数进行校验,id小于等于0的直接返回错误请求。
(2)缓存空对象:在没有的数据中存一个null,而这些空的对象会设置一个有效期)
(3) 布隆过滤器。 先访问布隆过滤器,再访问Redis,再访问数据库


缓存击穿
定义概念:
缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案:
1.粗暴的设置永不过期 2.定时更新过期时间 3.设置互斥锁 4.逻辑过期
①、设置热点数据永不过期
对于某个需要频繁获取的信息,缓存在Redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景是不适合的。
②、定时更新
比如这个热点数据的过期时间是1h,那么每到59minutes时,通过定时任务去更新这个热点key,并重新设置其过期时间。
③、互斥锁
这是解决缓存穿透比较常用的方法。
互斥锁简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。
④、逻辑过期
1):在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
2):当查询的时候,从redis取出数据后判断时间是否过期
3):如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新

缓存雪崩:
定义概念:
缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
解决方案:
主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
当然也可以利用Redis的集群提高服务的可用性。(哨兵模式、集群模式);给缓存业务添加降级限流策略(nginx或spring cloud gateway);给业务添加多级缓存
双写一致性
redis做为缓存,mysql的数据如何与redis进行同步呢? 要根据业务场景来回答
强一致性
我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。


排他锁是如何保证读写、读读互斥的呢?
排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法
延时双删(被抛弃)
面试官:你听说过延时双删吗?为什么不用它呢?
候选人:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。
数据工作的大致流程:
- 服务节点删除 redis 主库数据。
- 服务节点修改 mysql 主库数据。
- 服务节点使得当前业务处理
等待一段时间,等 redis 和 mysql 主从节点数据同步成功。 - 服务节点从 redis 主库删除数据。
- 当前或其它服务节点读取 redis 从库数据,发现 redis 从库没有数据,从 mysql 从库读取数据,并写入 redis 主库。

数据同步允许延时
我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。

数据持久化
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
RDB[全称Redis Database Backup file(Redis数据备份文件)]是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。

AOF[全称为Append Only File(追加文件)]的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令

数据过期策略
惰性删除和定期删除结合使用
惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key [缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放]
定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。[缺点:难以确定删除操作执行的时长和频率]
定期清理有两种模式:
SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数
FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
数据淘汰策略
假如缓存过多,内存是有限的,内存被占满了怎么办?
Redis支持8种不同策略来选择要删除的key:(可以在redis的配置文件中配置)
- noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
- allkeys-random:对全体key ,随机进行淘汰。
- volatile-random:对设置了TTL的key ,随机进行淘汰。
- allkeys-lru: 对全体key,基于LRU算法进行淘汰
- volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
- allkeys-lfu: 对全体key,基于LFU算法进行淘汰
- volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
LRU(Least Recently Used)最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU(Least Frequently Used)最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
使用建议:
1.优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
2.如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。
3.如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
4.如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。
分布式锁
为什么要用分布式锁? 因为一份代码会被nginx反向代理,负载均衡到多台服务器。这样一般的锁(JVM监控的锁)就不可以了。
Redisson实现的分布式锁。底层是setnx和Lua脚本
在redisson的分布式锁中,为了控制锁的有效时长,提供了一个WatchDog(看门狗),一个线程获取锁成功以后, WatchDog会给持有锁的线程续期(默认是每隔10秒续期一次)

- watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
- watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
- 从2得出,如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;
tryLock的第一个参数是尝试时间,第二个参数是预计运行业务时间。如果有第二个参数,则不会触发看门狗机制。如果没有第二个参数,默认续30s,每10s续一次。
lock的参数是预计运行业务时间(也就是叫自动释放时间),有此参数则不会触发看门狗机制,没有则会触发。还是直接用tryLock吧。lock() 一直等锁释放;tryLock() 获取到锁返回true,获取不到锁并直接返回false。
private void redissonDoc() throws InterruptedException {
//1. 普通的可重入锁
RLock lock = redissonClient.getLock("generalLock");
// 拿锁失败时会不停的重试
// 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
lock.lock();
// 尝试拿锁10s后停止重试,返回false
// 具有Watch Dog 自动延期机制 默认续30s
boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);
// 拿锁失败时会不停的重试
// 没有Watch Dog ,10s后自动释放
lock.lock(10, TimeUnit.SECONDS);
// 尝试拿锁100s后停止重试,返回false
// 没有Watch Dog ,10s后自动释放
boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
//2. 公平锁 保证 Redisson 客户端线程将以其请求的顺序获得锁
RLock fairLock = redissonClient.getFairLock("fairLock");
//3. 读写锁 没错与JDK中ReentrantLock的读写锁效果一样
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
readWriteLock.readLock().lock();
readWriteLock.writeLock().lock();
}
Redisson实现的分布式锁。可重入。这样做是为了避免死锁的产生,判断是否是当前线程持有的锁。如果是当前线程,就会计数。释放锁就会减一。在存储此数据的时候用的hash.大key是业务,小key是当前线程的唯一标识(比如线程Id),value是当前重入的次数。
redisson实现的分布式锁不能解决主从一致问题。用redisson提供的红锁(锁一半以上从节点)效率太低了。如果要保证强一致性,建议使用zookeeper实现的分布式锁。
主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
一般都是一主多从,主节点负责写数据,从节点负责读数据

全量同步:
1.从节点请求主节点同步数据(replication id、 offset )
2.主节点判断是否是第一次请求(判断replication id是不是一样),是第一次就与从节点同步版本信息(replication id和offset)
3.主节点执行bgsave,生成rdb文件后,发送给从节点去执行
4.在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
5.把生成之后的命令日志文件发送给从节点进行同步

增量同步:
1.从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
2.主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
哨兵模式
基本概念

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
•主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
•客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选主规则
•首先判断主与从节点断开时间长短,如超过指定值就排该从节点
•然后判断从节点的slave-priority值,越小优先级越高
•如果slave-prority一样,则判断slave节点的offset值,越大优先级越高(这个代表的是数据是否是最新的版本)
•最后是判断slave节点的运行id大小,越小优先级越高。
redis脑裂解决:
Redis 中有两个关键的配置项可以解决这个问题,分别是 min-slaves-to-write(最小从服务器数) 和 min-slaves-max-lag(从连接的最大延迟时间)。
min-slaves-to-write 是指主库最少得有 N 个健康的从库存活才能执行写命令。
min-slaves-max-lag 设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
你们项目用的是单点还是集群,哪种集群
主从(1主1从)+哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群,因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量网络带宽,也不能使用lua脚本和事务。
分片集群

Redis分片集群中数据是怎么存储和读取的?
lRedis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽
l将16384个插槽分配到不同的实例
l读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为插槽,寻找插槽所在的实例
Redis是单线程的,但是为什么还那么快
- Redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO
能解释一下I/O多路复用模型?
Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度, I/O多路复用模型主要就是实现了高效的网络请求
IO多路复用监听的是socket集合,而普通的阻塞IO和非阻塞IO都是只监听一个socket.阻塞IO就是在第一步等待,非阻塞就是不停重试,非阻塞IO感觉好笨。

候选人:嗯~~,I/O多路复用是指利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
StringRedisTemplate 与 RedisTemplate的区别
前者继承后者,一般用前者。
最大不同在于序列化器不同。所以不能混用。如在RDM客户端中,redisTemplate存储的东西为乱码,而stringRedisTemplate存储的东西为可见的字符串。
StringRedisTemplate的常用方法
https://blog.csdn.net/weixin_43835717/article/details/92802040