Redis是单线程,为什么还那么快?
redis是一个基于内存实现的key-value数据格式的nosql。
- 它完全基于内存操作,执行速度非常快
- 降低了CPU的消耗。采用单线程,避免不必要的上下文切换和竞争条件,也不用考虑多线程带来的线程安全问题
采用更高效的非阻塞I/O。redis采用epoll作为I/O多路复用技术的实现,使得Redis在网络I/O操作中能并发处理大量的客户端请求,实现高吞吐率。
什么是I/O多路复用?
I/O多路复用是一种同步的I/O模型,利用I/O多路复用模型可以实现一个线程监视多个文件句柄。一旦某个文件句柄就绪,就通知对应的应用程序进行相应的读写操作;没有文件句柄就绪时,就会阻塞应用程序,从而释放出CPU资源。
简单来说,就是单个线程处理多个TCP连接,尽可能的减少系统开销,无需创建和维护过多的线程或进程
实现I/O多路复用模型有三种:分别是select,poll,epoll
- select模型基本实现原理:采用轮询和遍历的方式实现I/O多路复用,使用数组结构来存储文件句柄(FD),最大连接是为1024;优点是跨平台支持性好,几乎支持所有平台,缺点由于使用轮询的方式全盘扫描,会随着FD的数量增多和导致性能下降,时间复杂度是O(n)。
- poll模型也是是轮询加遍历的方式,采用链表的方式来存储FD;优点没有最大FD的限制,缺点和select一样,使用轮询的方式全盘扫描,会随着FD的数量增多和导致性能下降,时间复杂度是O(n)。
- epoll模式使用时间通知机制来触发相关的I/O操作,将轮询改为回调,提高CPU的执行效率,使用B+数来存储FD,没有FD数量的限制,缺点只能在Linux下工作
缓存雪崩:指在缓存中的大量数据,在同一时刻全部过期,到达数据库,导致数据库压力增加,造成数据库服务器 崩溃的现象。
产生原因与解决:
- 缓存中间件宕机,可以通过对缓存做高可用集群来避免。
- 对缓存失效时间增加1-5min的随机值
缓存穿透:短时间内有大量不存在缓存中的数据请求到应用中,而这些key在缓存中找不到,穿透到了数据库。
解决:
- 把无效的key也保存到缓存中,并设置一个特殊的值,比如null
- 如果攻击者不断用随机的不存在key访问数据库,可以用布隆过滤器来实现,在系统启动的时候把目标数据全部缓存到布隆过滤器里面。
缓存击穿:指存在缓存中的热点数据过期时,仍有大量的请求进来到数据库,导致数据库压力增大
解决:
- 设置缓存永不过期。将某些热点数据的缓存设置保持长期有效,避免因过期导致的击穿问题
- 使用互斥锁或分布式锁。当缓存失效时,使用锁机制控制并发请求,避免同一资源的并发读写竞争
Redis持久化机制RDB和AOF
RDB是通过快照的方式来进行持久化的,也就是说,Redis根据快照的触发条件,把内存里面的数据快照写入磁盘,以二进制的压缩文件进行存储
RDB快照触发条件:
- 执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行命令
- 根据redis.conf文件里面的配置自动触发bgsave (conf文件内 # save 3600 1 300 100 60 10000)
- 主从复制时触发
RDB执行原理:
bgsave执行时会fork(浅拷贝)主进程得到子进程,子进程会共享主进程的内存数据(页表:记录虚拟地址与物理地址的映射关系),完成fork后读取内存数据并写入RDB文件中;这个速度比较快,纳秒级别的
fork采用的copy-on-write技术:
当主进程执行读操作时,共享内存
当主进程执行写操作时,则会拷贝一份数据,执行写操作
AOF持久化是一种近乎实时的方式,把Redis Server执行的事务命令进行追加存储,简单来说就是客户端执行一个数据变更的操作,Redis Server就会把这个命令追加到AOF缓冲区的末尾,再把缓冲区的数据写入磁盘的AOF文件,至于最终什么时候真正持久到磁盘,是根据刷胖策略决定的。AOF默认是关闭的,需要把redis.conf中的appendonly修改为yes进行开启。
AOF刷盘三种策略:
- appendfsync always #表示每执行一次写操作,立即写入AOF文件
- appendfsync everysec #写命令执行完先放到AOF缓冲区,每隔一秒写入AOF文件,默认策略
- appendfsync no #写命令执行完先放到AOF缓冲区,由操作系统决定什么时候写入AOF文件
区别:
配置项 | 刷盘时机 | 优点 | 缺点 |
always | 同步刷盘 | 可靠性高,几乎不丢数据 | 性能影响大 |
everysec | 每秒刷盘 | 性能适中 | 最多丢失一秒数据 |
no | 操作系统刷盘 | 性能最好 | 可靠性较差,可能丢失大量数据 |
AOF重写机制:
因为是记录命令,AOF文件要比RDB文件大的多。而且AOF会记录对同一个key的多条命令,但只有最后一条有意义。通过bgrewriteaof 命令可以让aof执行重写功能,用最少的命令达到相同的效果
例如set num 12
set name jack ====> bgrewriteaof ===> mset name jack num 666
set num 666
Redis也会在触发阈值的时候自动去重写AOF文件,可在redis.conf中配置
#AOF文件比上次文件,增长多少百分比才会触发重写
auto-aof-rewrite-percentage 100
#AOF文件体积最小多大以上可以触发
auto-aof-rewrite-min-size 64mb
RDB和AOF各有优缺点,如果对数据安全性要求较高,在实际开发中可以结合两者来使用
RDB | AOF | |
持久化方式 | 定时对整个内存做快照 | 记录每次执行的命令 |
数据完整性 | 不完整,两次备份之间有缺少 | 相对完整,取决了刷盘策略 |
文件大小 | 会有压缩,体积相对小 | 记录命令,文件体积大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为没有AOF完整 | 高,数据完整性更高 |
系统资源占用 | 高,大量CPU和资源消耗 | 低,主要是磁盘IO资源,但AOF重写时会占有CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高 |
Redis 数据删除策略 - 惰性删除、定期删除两者配合使用
惰性删除:设置该key过期时间后,不需要管它,当需要该key时,在检查它是否过期,如果过期就删除它, 没有就返回它所在的key
优点:对CPU友好,只会在使用该key时进行过期检查,对于许多用不到的key不用浪费时间去检查
缺点:对内存不友好,如果一个key过期了,但是一直没有去使用,那么该key就一直存在,不会释放
定期删除:每隔一段时间,就对一部分key进行检查,删除里面过期的key;随着时间推移,它会遍历redis 所有的key,直到都检查一遍,可以确保假如一个key过期了,就一定会被删除
定期删除有两种模式:
SLOW模式:定时任务,执行频率默认是10hz(每秒执行10次),每次不超过25ms,通过redis.conf中hz 选项来配置
FAST模式:执行频率不固定,但两次间隔不超过2ms,每次耗时不超过1ms
优点:可以通过限制删除操作执行时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效的释 放过期键占用的内存
缺点:难以确定删除操作执行的时长和频率
Redis淘汰策略:
当redis中的内存不够用时,此时在向Redis中添加新的key时,那么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的可以,基于LFU算法进行淘汰
LRU算法:最近最少使用,用当前时间减去最后一次访问的时间,这个值越大则淘汰优先级越高
(在Redis中只是使用了简单的队列或链表去缓存数据,而mysql中Buffer_Poll也用到了LRU,只不过 mysql中设计了冷热数据分离的机构)
LFU算法:最少使用频率,会统计每个key的访问频率,值越小则淘汰优先级越高
(使用两个双向链表形成一个二维双向链表,横向保存访问频率,竖向保存访问频率相同的所有元素,新添加 的元素,访问次数默认为1,并添加到相同频率节点对应双向链表头部。当元素被访问时,就会增加对应key 的访问频次,并把当前访问的节点移动到下一个频次节点。)
数据淘汰策略-使用建议:
- 优先使用allkeys - lru策略,充分利用lru算法的优势,把最近最常使用的数据留在缓存中,如果业务有冷 热数据区分,建议使用
- 如果业务中数据访问频率不大,可以使用allkeys - random, 随机选择淘汰
- 如果业务中有置顶的需求,可以使用 volatile - lfu ,把置顶的数据不设置过期时间,这些时间就不会被淘 汰,会淘汰其他具有过期时间的数据
- 如果业务中有短时高频的数据,可以用 allkeys - lfu 或 volatile - lfu
Redis提供的集群方案有三种:
主从复制:读写分离,解决高并发读
哨兵模式:高可用
分片集群:高并发写
1、主从数据同步原理:
主从全量同步:
- slave(从节点)执行replicaof命令与master(主节点)建立连接
- 请求数据同步,slave会把自己的repId和offset发送给master
- master判断是不是第一次连接,是第一次,返回master的版本信息,slave保存版本信息;
(每个master都有唯一的Replication id简称repId,如果是第一次连接,master和slave的repId是不相等的,slave会保存master发送过来的repId和offset)
- matser执行bgsave命令,生成RDB文件,发送给slave节点RDB文件
- slave清空本地数据,写进RDB文件
- 若master生成RDB文件时,有其他数据在写入,master记录其中的所有数据,并写入repl_baklog日志文件
- master发送repl_baklog给slave
- slave执行接受到的文件命令
- 主从复制完成后,主节点每接收一个写操作都会通过复制缓冲区(replication_buffer)发送给从节点,保证主从节点数据一致
主从增量同步:
- slave发起psync命令携带repId和offset参数
- master判断请求repId是否一致,若不一致返回continue
- mater去repl_baking文件中获取offset之后的数据返回给slave
2、Redis哨兵作用:
redis提供哨兵(sentinel)机制来实现对主从集群的自动故障恢复
哨兵的结构和作用:
1、监控:sentinel会不断检查你的master和slave是否按预期工作
2、自动故障恢复:如果master故障了,sentinel会将一个slave提升为master,当故障实例恢复后也以新的 master为主
3、通知:Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新的消息推送给redis 的客户端
服务状态监控:
Sentinel基于心跳机制监控服务状态,每隔一秒向集群中的实例发生ping命令
·主观下线:如果某Sentinel发现某实例未在规定时间响应,则认为该实例是主观下线
·客观下线:若超过指定数量(quorum)的Sentinel都认为该实例主观下线,则该实例客观下线,quorum值最好超过Sentinel实例数量的一半。
哨兵选举规则:
·首先判断主与从节点断开时间长短,如超过指定值则排除该节点
·然后判断从节点的slave-priority值(redis.conf中配置),越小优先级越高
·如果slave-priority的值一样,则判断slave的offset值,值越大优先级越高
·最后判断slave节点的运行id大小,越小优先级越高
哨兵脑裂问题
·由于哨兵和集群中的主从节点可能处于不同的网络分区,哨兵只能监测到从节点们,这个时候哨兵会发现监测的节点中没有主节点,那么他会经过选举产生一个新的主节点,但是客户端这个时候还是会持续的向老的主节点发送数据,新的主节点此时时没有新的数据写入的,这样就造成了类似大脑分裂的情况。
·而当网络恢复后,哨兵会将老的主节点降为从节点,这时候再从新的主节点master中同步数据,老master会把自己节点中的数据清空,从而导致数据丢失
解决方案
修改Redis的两个参数来解决问题
min-replicas-to-write 1
#表示最少的slave节点为1个,这样就能保证如果出现网络问题,如果主节点没有从节点了,那么 服务端拒绝写入数据,这样老的主节点是没有新数据产生的
min-replicas-max-lag 5
#表示数据复制和同步的延迟不能超过5秒
3、分片集群
作用:
·集群中有多个master,每个master保存不同的数据(解决高并发写)
·每个master有多个slave(解决高并发读)
·master之间通过ping检测彼此的健康(类似哨兵)
·客户端可以访问集群中的任意节点,最终都会转发到正确的节点(路由规则)
redis分片集群中数据是怎么存储和读取的(路由规则)
·Redis分片集群引入哈希槽的概念,Redis有16384个哈希槽
·将16384个槽分配到不同的实例上
·读写数据:根据key的有效部分计算哈希值,通过CRC16校验后对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key作为有效部分)余数作为插槽,寻找插槽的实例