在 Redis 6.0版本之后开始引入了多线程处理网络请求,将读取网络数据到输入缓冲区、协议解析和将执行结果写入输出缓冲区的过程变为了多线程,执行命令仍然是单线程。之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。多线程 IO 的读(请求)和写(响应)在实现流程是一样的,只是执行读还是写操作的差异并且这些 IO 线程在同一时刻只能全部是读或者写。
Redis 6.0版本的流程就会变为如下:
Redis 是基于 Reactor 模式开发了自己的网络事件处理器, 这个处理器被称为文件事件处理器。这个事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,文件事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
Redis 6.0版本之后将连接应答处理器及命令回复处理器修改为了多线程,只有命令请求处理器仍然是单线程的。
简单描述:
具体原理可以查看下面这篇博客:
https://blog.csdn.net/armlinuxww/article/details/92803381
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈是机器内存的大小或者网络带宽。
我们可以通过 Redis 自带的测试脚本 redis-benchmark(一般在/usr/local/bin目录下)进行测试
redis-benchmark -h 127.0.0.1 -c 500 -n 1000000 -q -t set
Redis 单机的一秒钟处理请求数可以达到10万以上,随着跨服务器、跨局域网导致网络IO的消耗每秒钟可以处理的请求数逐步降低。
常用的五种数据类型:
不常用的4种数据类型:
每个类型都有属于自己的应用场景。
当 Zset 存储的元素数量小于 128 个并且所有元素的长度都小于 64 字节时使用 Zlist(有序链表)来存储数据,任何一个条件不满足就会进化成跳跃表进行存储。
跳跃表(skiplist)是一种随机化的数据结构,是一种可以与平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成。
跳跃表 skiplist 受到多层链表结构的启发而设计出来的。按照生成链表的方式,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个新增节点随机出一个层数(level,默认最大值为64)。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn)。
通过 setNx(set if not exist) 方法实现,如果不存在则插入,其可以很好的用来实现我们的分布式锁。对于某个资源加锁我们只需要
setNx resourceName value
这里有个问题,加锁了之后如果机器宕机那么这个锁就不会得到释放。所以会加入过期时间,加入过期时间需要和setNx同一个原子操作,在 Redis2.8 之前我们需要使用 Lua 脚本达到我们的目的,但是 Redis2.8 之后支持 nx 和 ex 操作是同一原子操作。
set key value ex 5 nx
ex:设置键的过期时间为 second 秒
nx:只在键不存在时,才对键进行设置操作。
RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间、不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
RDB 优势
RDB 劣势
AOF 优势
AOF 劣势
注意:当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。
Redis 会通过创建子进程来进行 RDB 操作,子进程创建后,父子进程共享数据段。直到父进程试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给父进程,而子进程所见到的最初的资源仍然保持不变。
RDB 默认有以下三个规则,满足其一就会进行磁盘写入
save 900 1 # 900 秒内至少有一个 key 被修改(包括添加)
save 300 10 # 400 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改
AOF 持久化策略(硬盘缓存到磁盘),默认 everysec
因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子:如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。
Reids 所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回收。内存回收主要分为两类,一类是 key 过期,一类是内存使用达到上限(max_memory)触发内存淘汰。
Redis 中同时使用了惰性过期和定期过期两种过期策略。
Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算决定清理掉哪些数据,以保证新数据的存入。
redis.conf 淘汰策略设置:maxmemory-policy noeviction
策略 | 含义 |
---|---|
volatile-lru | 根据 LRU 算法删除设置了超时属性(expire)的键,直到腾出足够内存为止。如果没有可删除的键对象,回退到 noeviction 策略。 |
allkeys-lru | 根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够内存为止。 |
volatile-lfu | 在带有过期时间的键中选择最不常用的。 |
allkeys-lfu | 在所有的键中选择最不常用的,不管数据有没有设置超时属性。 |
volatile-random | 在带有过期时间的键中随机选择。 |
allkeys-random | 随机删除所有键,直到腾出足够内存为止。 |
volatile-ttl | 根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。 |
noeviction | 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时 Redis 只响应读操作。 |
原因:指 Redis 缓存在短时间内大面积失效,所有请求都直接访问数据库。可能导致数据库 CPU 瞬间飙升甚至宕机,由于大量的应用服务依赖数据库和 Redis 服务,这个时候很快会演变成各服务器集群的雪崩,最后网站彻底崩溃。
解决:
原因:缓存穿透是指查询一个一定不存在的数据。一个请求查询缓存没有命中,就需要从数据库查询。因为查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决:
原因:某个 Key 非常热点、访问非常频繁,处于集中式高并发访问的情况。当这个 Key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决:
我们应该什么时候去更新缓存,保证数据是实时、有效
指系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,直接查询数据库,然后再将数据缓存的问题。
如果选择更新缓存,可能在多次修改数据的情况下都没有一次读取会导致缓存被频繁更新降低性能。一般建议删除缓存,用户在读取的时候直接查询数据库保存最新值到缓存。
LRU 是一个数据淘汰算法,淘汰掉最近最久未使用的数据。可以基于 Java 的 LinkedHashMap 进行实现
class LRULinkedHashMap<K,V> extends LinkedHashMap<K,V> {
private int capacity;
LRULinkedHashMap(int capacity) {
// AccessOrder 表示是否按照访问顺序进行排序
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
//插入值的时候会判断是否超过指定的最大容量,是则删除最近最久未使用的元素
return size() > capacity;
}
}
启动一个 slave 的时候执行 slaveof 命令,会在本地保存 master 节点的信息。发送一个 psync(同步) 命令给 master,如果是这个 slave 第一次连接到 master,就会触发一个全量复制。master 就会启动一个线程去生成 RDB 快照,并使用一个缓冲区记录从现在开始执行的所有写命令。RDB 文件生成后,master 会将这个 RDB 文件发送给 slave 的,slave 拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,最后 master 会把内存里面缓存的那些写命令都发给 slave。
slave 节点上会记录一个 master_repl_offset 值,表示记录的偏移量。重新连接上的时候,只需要从偏移量的位置开始继续同步即可。
Redis-Sentinel 是 Redis 官方推荐的高可用性(HA)解决方案,当用 Redis 做 Master-slave 的高可用方案时,假如 master 宕机了,Redis 本身(包括它的很多客户端)都没有实现自动进行主备切换,而 Redis-sentinel 本身也是一个独立运行的进程,它能监控多个 master-slave 集群,发现 master 宕机后能进行自懂切换。具有以下功能:
Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令。如果在规定时间内都没有收到有效回复,Sentinel 会将该服务器标记为下线 (主观下线)。这个时候 Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,只有超过半数(集群半数加一,所以集群个数一般为奇数) Sentinel 节点都认为 master 下线,master 才真正确认被下线(客观下线),这个时候就需要重新选举 master。
假设集群只有一主一从,对应两个 Sentinel。如果 master 节点所在的服务器挂了,会导致 Sentinel 也只剩下一个。一个节点是不满足客观下线的条件,所以当 Sentinel 节点只剩下一个的情况下是不会执行故障转移的。
一共有四个因素影响选举的结果,按优先级从上往下
Redis Cluster 可以看成是由多个 Redis 实例组成的数据集合,自动将数据进行分片,每个master 上放一部分数据。支撑N个 master node,每个 master node 都可以挂载多个 slave node。因为每个 master 都有 salve 节点,那么如果 mater 挂掉,redis cluster 这套机制,就会自动将某个 slave 切换成 master。
客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。节点之间互相交互,共享数据分片、节点状态等信息。
Redis Cluster 实现了针对海量数据+高并发+高可用的场景。
例如,hash(key)%N,根据余数,决定映射到那一个节点。这种方式比较简单,属于静态的分片规则。但是一旦节点数量变化,新增或者减少,由于取模的 N 发生变化,数据需要重新分布。
将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
Redis 既没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现的。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个 固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,Redis Cluster 槽范围是 0 ~ 16383。槽 是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽,比如 Node1 负责 0-5460,Node2 负责 5461-10922,Node3 负责 10923-16383。
对象分布到 Redis 节点上时,对 key 用 CRC16 算法计算再对 16384 取模,得到一个 slot 的值,数据落到负责这个 slot 的 Redis 节点上。
增加一个 master,就将其他 master 的部分 slot 移动过去。减少一个 master,就将它的 slot 移动到其他 master 上去,然后将没有任何槽的节点从集群中移除即可。
每个节点都会记录哪些槽指派给了自己,哪些槽指派给了其他节点。客户端向节点发送键命令,节点要计算这个键属于哪个槽。如果是自己负责这个槽,那么直接执行命令。如果不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。
Jedis 等客户端会在本地维护一份 slot-node 的映射关系,大部分时候不需要重定向,所以叫做 smart jedis(需要客户端支持)。
有些数据可能是不能跨节点存储,需要存储在同一个节点上。在 key 里面加入 {hash tag} 即可。Redis 在计算槽编号的时候只会获取 {} 之间的字符串进行槽编号计算,这样由于上面两个不同的键 {} 里面的字符串是相同的,因此他们可以被计算出相同的槽。
当 slave 发现自己的 master 变为 FAIL 状态时,便尝试进行故障转移,以期成为新的 master。由于挂掉的 master 可能会有多个 slave,从而存在多个 slave 竞争成为 master 节点的过程。整体流程和哨兵机制类似,其过程如下:
Redis Cluster 既能够实现主从的角色分配,又能够实现主从切换,相当于集成了 Replication 和 Sentinal 的功能。
检查每个 slave 与 master 断开连接的时间,如果超过了 cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成 master。
每个从节点都根据自己对 master 复制数据偏移量,来设置一个选举时间。复制数据偏移量越大,选举时间越靠前,优先进行选举。
所有的 master node 给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。
彻底搞懂epoll高效运行的原理
Redis面试题(一): Redis到底是多线程还是单线程?
redis的 rdb 和 aof 持久化的区别
JavaFamily
阿里JAVA面试题剖析:Redis 集群模式的工作原理能说一下么?