目录
缓存与数据库同步问题
低一致性:使用Redis自带的内存淘汰机制;高一致性:主动更新,以超时作为兜底方案。
删除缓存比更新缓存更有性价比:
每次更新数据的同时都更新缓存,如果没有经常查询该数据,则是总在写缓存,浪费。
更新数据时让缓存失效,查询时再更新缓存更好。
先操作数据库,再操作缓存,线程更安全。
双写一致性(mysql和redis数据同步)
修改了数据库的数据,要同时更新缓存的数据,数据保持一致
一致性要求高
可以用读写锁,例如Redission提供的读写锁
允许延时一致
可选方案:
先删除缓存,再更新数据库
先更新数据库,再删除缓存(较好)
延迟双删(更新数据库后删除缓存,休眠一会,再次删除缓存)
重点保证两步都执行成功,才能解决数据一致性问题
即:异步重试
引入消息队列
订阅数据库更新日志,再操作缓存,例如阿里的canal
1、2两种方案仍然不能解决并发场景下和读写分离+主从复制延迟情况下的数据一致性问题,3延迟删除的时间要大于其他线程查询到旧值写入缓存的时间和主从复制延迟的时间,并不好评估。
缓存三兄弟
缓存穿透
请求的数据在缓存和数据库中都不存在,则缓存永远不会生效,请求会直接打到数据库,如果大量线程都请求不存在的数据,会损坏数据库。(缓存失效)
解决
1.缓存空对象
实现简单,维护方便。但有额外的内存消耗,且若没有在新增时插入缓存,可能会存在短期数据不一致的问题(要等到缓存自动删除)。
2.布隆过滤
内存占用少,但存在误判的可能(判断数据不存在则一定不存在,判断数据存在却可能数据不存在,此时会发生缓存穿透问题)
3.设定更规范的用户权限,更复杂的校验方法等
缓存雪崩
同一时段大量的缓存key同时失效或Redis服务宕机,导致大量请求到达数据库。(缓存出问题)
解决
key失效:
给不同key的TTL(过期时间)添加随机值
服务宕机:
1.Redis集群提高服务可用性(哨兵模式、集群模式)
2.缓存业务降级限流(nginx,springcloud gateway)
2.给业务添加多级缓存
缓存击穿(热点key问题)
一个被高并发访问且缓存重建业务较复杂的key突然失效了,大量的请求的访问会在瞬间给数据库带来巨大冲击。
解决
1.互斥锁
例如未能访问到数据则获取互斥锁,直至数据库重建缓存数据,写入缓存才释放锁,其余线程检测到上锁(未能获取互斥锁)则等待并重试获取锁,直至锁释放,缓存命中。
保证数据一致性,实现简单,没有额外内存消耗;但是性能受影响,因为线程需要等待,会有死锁风险。
2.逻辑过期
不设置过期时间,增加逻辑时间,若逻辑时间过期,获取互斥锁,并将重建缓存数据交给另一个新线程,自己直接返回过期数据,不等待,而其余查询缓存的线程获取锁失败,也直接返回过期数据,不等待;释放锁后,后来的线程即可返回正确的数据。
线程不等待,性能好;但是不保证一致性,有额外消耗内存,实现复杂。
缓存持久化
RDB数据快照
将内存中的所有数据都记录到磁盘中,当Redis实列故障重启后,从磁盘读取快照文件,恢复数据。
RDB触发
手动执行RDB主动备份
启动redis程序,输入命令:
save 主进程执行RDB,会阻塞所有命令。
bgsave 子进程执行RDB,避免主进程受影响。(一般用这个)
Redis内部自动触发RDB
redis.conf文件中找
例如:
save 900 1(900s内,如果至少有一个key被修改,则执行bgsave)
RDB执行原理
bgsave会fork一个与主进程相同的子进程,子进程共享主进程的内存数据(复制页表,页表记录了虚拟地址与物理地址的映射关系,即相当于得到了数据的地址),由子进程写新的RDB文件替换旧的RDB文件。
fork采用的是copy-on-write技术:
主进程执行读操作时,访问共享内存;
主进程执行写操作时,会拷贝数据,在数据副本执行写操作,并且,之后读取数据也会读取该数据副本。
AOF
Append Only File(追加文件)。Redis处理的每一个写命令都会记录到AOF文件中。
AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
对比
缓存过期策略
惰性删除和定期删除配合使用
惰性删除
数据过期后,先不去删除;需要该key时,检查是否过期;过期则删除,反之返回key。
优点:对CPU友好,只在使用key时检查,不会浪费时间进行过期检查。
缺点:对内存不友好,key过期但不使用,会一直存放在内存中,不会释放。
定期删除
每隔一段时间,就从数据库中取出一定量的随机key进行检查,并删除其中的过期key。
定期清理的两种模式:
SLOW模式 定时任务,执行频率默认为10hz,每次不超过25ms,通过修改redis.conf的hz选项调整参数
FAST模式 执行频率不固定,两次间隔不低于2ms,每次耗时不超过1ms。
优点:有效释放过期key占用的内存;通过限制删除操作的时长和频率减少对CPU的影响
缺点:不确定删除操作的执行时长和频率
数据淘汰策略(Redis内存不够怎么办?)
Redis支持8种不同策略来选择要删除的key:
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算法进行淘汰
(其实就四类1.默认不删除 2.TTL过期删除 3.TTL/全体+LRU 4.TTL/全体+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 策略。
Redis分布式锁
获取和释放锁
如何合理设置有效时长?
1.根据业务执行时间预估,续期时长
另用一个线程监控业务执行时长,但实现麻烦
2.redission实现的分布式锁
合理控制有效时长:
watchDog实现,创建锁后,会有一个线程监控业务执行,默认在锁有效时长/3的时候(10s),重置有效时长,一旦业务执行完,需要手动释放锁,会给watchDog终止重置有效时长的信号。
可重入:
底层采用哈希结构,记录该分布式锁和所属线程,以及重入次数(用于判断何时释放锁)。
判断锁属于该线程,则线程内需要多次获取锁时(线程内可能有多个代码块需要获得锁),可以直接使用,而不用等待释放再获取锁,有效减少死锁的情况。
重入锁,则将次数+1,释放锁则-1,次数为0时,线程释放锁
实现代码
主从数据一致:
redission的分布式锁不能解决主从数据一致问题,但提供了红锁解决。
红锁 在至少过半的redis实例上加锁,而不是只在一个redis实例上加锁。实现复杂且性能差
-->要保持数据的强一致性,用zookeeper实现的分布式锁
Redis的集群方案
三种:主从复制、哨兵模式、分片集群
主从一致复制、同步流程
搭建主从集群
master负责写,slave负责读
全量同步
流程
从节点向主节点请求数据同步(带参数replid、offset)---->判断是否是第一次同步(replid不相同则是第一次)----->是第一次,返回master(主节点)的数据版本信息(replid,offset)---->执行bgsave,生成RDB文件并发送 -----> 从节点清空本地文件,加载RDB文件 ----->在生成和传送RDB文件时,若主节点接收了请求,对数据进行了操作,会将命令记载在repl_backlog,将repl_backlog里的命令发送给从节点,从节点执行命令实现同步。
Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid。
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
主从增量同步(slave重启或后期数据变化)
流程
从节点向主节点请求数据同步(带参数replid、offset)---->判断是否是第一次同步------>不是第一次,主节点根据offset在repl_backlog中获取offset之后的命令,发送给从节点------>从节点执行命令
如何保证Redis的高并发高可用?(哨兵模式)
哨兵(Sentinel)的作用:
监控:Sentinel 会不断检查您的master和slave是否按预期工作
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态检测
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令: 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选主规则
1.首先判断主与从节点断开时间长短,如超过指定值就排该从节点
2.然后判断从节点的slave-priority值,越小优先级越高
3.如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
4.最后是判断slave节点的运行id大小,越小优先级越高。
redis集群(哨兵模式)脑裂?
分片集群
Redis为什么这么快?
- 基于内存,内存读写比磁盘快
- 是单线程,没有线程上下文切换,减少了锁和竞争
- 使用了非阻塞I/O复用多路技术
I/O复用多路技术
指用一个线程同时监听多个socket,哪个socket就绪了(可以读写),就直接响应,避免无效等待(一个线程监听一个socket,没有就绪则一直阻塞等待)。