1.简介
redis官网首页明确的表达了它的各种功能:
- Redis 是一个开源的、内存的key-value数据存储系统,它可以用作数据库、缓存和消息中间件
- 支持的数据结构:string、hash、list、sets、zset、bitmap、pub/sub、hyperloglogs 、geospatial indexes(地理空间)和streams流
- 持久化机制:RDB(快照)、AOF(log)
- 高可用:Sentinel、Cluster
- 扩展:Lua
redis的设计目标就是一个字:快
2.Redis数据类型和内存
Redis是二进制安全的,是因为它对存储数据没有任何编码或格式限制,能够接受和处理任意二进制数据,非常灵活通用。
redis使用对象类型表示k-v,相当于每次创建一个键值对,至少会创建两个对象,一个是用来保存键的对象,另一个是保存值的对象
redis的对象中有两个field分别为 type(类型,对应redis的五大种数据类型,是type命令的实现方式),encoding(编码,对应当前key的type类型的底层实现方式,是object encoding命令实现)
redis的数据类型指的都是Value的类型,Key的类型只有一种 string
string
redis的string类型的key的value值最大不能超过512MB,string的底层有两种实现:简单动态字符串(simple dynamic string. SDS)和 C原生字符串,C字符串一般只会用在不需要对字符串值改变的场合使用(比如打印日志),其他大多数场景都是SDS。
在Java中,我们可以把SDS比作一个class对象,它和Java中的容器比较类似,SDS对象拥有三个基本的field:char buf[](保存字符串的字节数组)、 int len(SDS保存字符串的长度,同样也是buf字符串中已使用的字节长度)、int free(buf中空余的字节空间)
C字符串和SDS的区别:
- C字符串获取长度的时间复杂度为O(n),SDS为O(1)
- C字符串使用时,需要注意内存溢出,SDS有自动内存分配的机制 (类似Java容器的扩容)
- 减少每次修改字符串值时所需要的内存重分配次数
- SDS字符串是二进制安全的
共享字符串:
redis在初始化服务器时,会创建一万个字符串对象,包含0-9999的所有整数值,举个例子:
使用string的命令,可以看 redis命令入门(stirng)
RedisJSON
RedisJSON是基于string扩展,所以它的api包含了大多数string的命令,它作为一个module加载到redis-server中:redis-server --loadmodule /path/to/module/src/rejson.so
主要功能:
- 完全支持JSON协议。
- 支持JSONPath语法。
- 数据在redis中以树型结构存储,并且是二进制安全。
- 对整个JSON的更新都是原子操作。
限制:整个JSON树的深度最多支持到128,超过会报错。
命令:JSON命令
Bitmap
bitmap也是基于string扩展,底层是bit数组,1byte=8bit,表示1byte能记录8个数据。每个bit能表示两种状态0和1。
举个栗子:bitmap实现打卡
命令:Bitmap
HyperLogLog(HLL)
HLL用于在Redis中进行大规模唯一计数的近似统计(日活,月活)。HLL能够在使用固定内存的情况下,对非常大的数据集进行近似的基数计数(集合中不重复元素的数量),同时提供较低的存储成本和计算复杂度。底层是基于位数组(bit array)扩展,位数组的长度是redis在创建HLL时使用自适应的算法来初始化位数组长度,并且决定了HLL的精确度,位数组中的每个位被用于表示某个哈希函数(MurmurHash)的输出。
HLL的基数估计有一定的误差率,数组越长误差越小,内存占用越高。它单个HLL结构最大只占用12KB,最多能存储2^64个元素。
主要命令:
- PFADD KEY element1 element2 ... 往集合中添加元素,时间复杂度O(1)。
- PFCOUNT KEY1 KEY2 ... 统计key中的元素个数,时间复杂度是O(1),如果统计是多个key则是O(n),多个key的统计会进行merge。
- PFMERGE destKey sourceKey sourceKey ... 把多个key合并成一个destkey,复杂度O(n)。
问题:
-
HLL的误差无法通过用户可配置的选项或命令来直接调整。只能全局配置最大长度:
CONFIG SET hll-sparse-max-bytes <max_bytes> -
HLL的误差无法在创建某个key时指定。
list
list的底层有两种实现:ziplist(压缩列表)和 quicklist(双端链表)
压缩列表比双端链表更省内存,而且压缩列表在内存中是连续存放的,元素量比较少的时候载入压缩列表的数据会比双端链表快,反之如果元素量比较多的时候,则使用双端链表
使用list可以实现数组,栈,队列等数据结构,具体可以看 redis命令入门(list)
set
set的底层有两种实现:intset 和 hashtable
当set集合的所有元素都是整数,并且元素个数不超过512(可通过配置修改)个,set使用intset编码的整数集合作为底层实现,其他情况均使用hashtable(字典)作为底层实现
使用set命令文档,具体可以看 redis命令入门 (set)
zset
zset的底层有两种实现:skiplist(跳跃表) 和 ziplist(压缩列表)
zset集合保存的元素数量小于128个并且所有元素成员的长度都小于64字节,则使用ziplist,其他情况使用skiplist。
ziplist 的每个集合元素会使用两个紧挨的节点来保存,一个节点保存member,另一个节点保存score,默认元素是按照score从小到大排序,skiplist 编码对象底层使用zset结构实现,zset结构由一个字典和一个skiplist实现,每个skiplist节点都保存了一个集合元素,每个skiplist节点的层高都是1-32之间的随机数,且在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的,节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序
使用zset相关命令可以实现延时队列
hash
hash的底层有两种实现:ziplist(跳跃表) 和 hashtable (字典)
每个字典带有两个hash表,一个正常使用,另一个在进行扩容rehash时(类似Hashmap)使用,hash表使用拉链法解决冲突,当某个字典上的值越来越多时,服务器会采用渐进式的rehash方式来进行扩容,字典内的值不是一次性全部重新分配到新的hash表上。
Streams(5.0)
Streams用于处理有序的、不断更新的消息流,它提供了一种高效的、可持久化的、实时的数据结构,用于处理发布/订阅、消息队列、事件驱动等场景。
主要特性和概念(官方文档):
-
消息和消息组:
- 消息(Message)是 Streams 的基本单元,它包含一个唯一的 ID 和一组键值对(Fields-Values)。
- 消息组(Message Group)是一组相关的消息,它们共享相同的组 ID。
-
消费者和消费者组:
- 消费者(Consumer)是订阅 Streams 中消息的实体。每个消费者都有一个唯一的名称。
- 消费者组(Consumer Group)是多个消费者的集合,它们共同处理消息,并实现负载均衡和故障恢复。
-
消费流程:
- 消费者通过消费者组订阅 Streams 中的消息。
- 每个消费者在消费消息时都有一个当前位置,表示其已读取的消息范围。
- 消费者可以使用
XREAD命令从 Streams 中读取消息,以及使用XACK命令确认已处理的消息。
-
消息发布和添加:
- 使用
XADD命令可以向 Streams 中发布新的消息。 - 消息的 ID 可以由 Redis 自动生成,也可以手动指定。
- 使用
-
消息消费和处理:
- 消费者可以使用
XREADGROUP命令从 Streams 中读取消息,并使用消费者组的方式进行协作消费。 - 消费者组可以实现负载均衡和故障恢复,确保消息被所有消费者均匀地处理,并能恢复消费者失败后的状态。
- 消费者可以使用
3.持久化
Redis持久化分为RDB、AOF、混合模式、关闭持久化四种,官方文档
RDB:在某个时间点会把当前数据库状态保存为一个压缩的二进制RDB文件到磁盘,可以通过执行命令SAVE或BGSAVE命令来生成,或者通过配置文件配置定时自定生成,当进程意外退出时,丢失的数据量取决于配置,如果配置为每三十分钟生成,则最坏情况下会丢失三十分钟的数据。
配置文件(默认):
save 900 1:server在900秒内,对数据库修改1次
save 300 10:server在300秒内,对数据库修改10次
save 60 10000:server在60秒内,对数据库修改1000次
只要满足任意一个条件,server就会执行save,且会打印日志:
它的特点是:保存和加载的过程会很快,但是数据实时性会略低(通过配置)
-
SAVE:此命令会阻塞redis主线程,知道RDB文件生成完成
-
BGSAVE:此命令redis服务会fork一个子进程来单独执行生成RDB文件操作,主进程继续对外提供服务;它和BGREWRITEAOF命令不能同时执行,如果在BGSAVE命令执行期间,客户端执行BGREWRITEAOF命令,服务器会等待BGSAVE命令执行完成之后,延迟执行BGREWRITEAOF命令;如果BGREWRITEAOF正在执行,客户端执行了BGSAVE,则服务器端会拒绝本次BGSAVE命令。
AOF:将每次执行的命令保存到硬盘(类似于MySQL的binlog),当进程意外退出时丢失的数据更少(理论上最多丢失一秒内的数据,因为操作系统默认1秒刷新一次内核缓存区的数据到磁盘上)。
它的特点是:保存和加载的过程会很慢,生成的数据文件会比较大(可以通过rewrite来重写减少文件大小),但是数据实时性会比较高。
因为AOF文件的更新频率一般比RDB文件的更新频率高,所以如果开启了AOF方式,服务器会默认使用AOF方式来恢复数据库状态,除非配置文件中显式的关闭了AOF方式,开启RDB,才会使用RDB持久化方式。
配置文件: appendonly [yes|no]
关于AOF有一个很重要的命令:BGREWRITEAOF
作用如下:
-
重写 AOF 日志文件:BGREWRITEAOF 命令会在后台异步地对 AOF 日志文件进行重写,生成一个新的 AOF 文件。重写过程中,Redis 服务器会重新执行当前数据集的写操作,将其记录到新的 AOF 文件中,而旧的 AOF 文件则保持不变。
-
压缩 AOF 文件:重写 AOF 文件会删除多余的写指令(比如两条指令:set k 1, set k 2,会被压缩成一条 set k 2),相当于压缩 AOF 大小。
-
优化加载时间:AOF 重写后的新文件可以在加载时更快地恢复。因为新文件是经过优化的,加载时间更短。
在执行 BGREWRITEAOF 命令期间,如果有新的命令执行,Redis 会将这些新的命令同时记录到原有的 AOF 日志文件和命令缓存区中,当BGREWRITEAOF执行完成,Redis会把命令缓冲区中的数据继续写入到新的AOF文件中。
混合模式:4.0之后的版本是可以配置RDB+AOP两种方式搭配使用的,通过配置文件开启,当开启混合持久化后
- 子进程会把内存中的数据以RDB的方式写入AOF中,
- 把缓冲区中的增量命令以AOF方式写入到文件
- 将含有RDB个数和AOF个数的AOF数据覆盖旧的AOF文件
- 新的AOF文件中,一部分数据来自RDB文件,一部分来自Redis运行过程时的增量数据
配置文件:aof-use-rdb-preamble yes
关闭持久化:适用纯缓存场景。
配置文件:save 0 0 ,appendonly no,要修改两个配置。
内存回收机制
如果服务器配置了 maxmemory(32位系统最大设置3GB) ,当服务器占用的内存数超过maxmemory的值时,redis会根据配置文件中的以下配置,采用对应的策略来回收数据
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-lfu:从已设置过期时间的数据集中淘汰最不经常使用的数据
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
- allkeys-lru:从所有数据中淘汰最长时间没有被使用的数据(最后一次时间)
- allkeys-lfu:从所有数据中淘汰最不经常使用的数据
- allkeys-random:从所有数据集中任意选择数据进行淘汰
- noeviction:禁止驱逐数据
如果配置volatile-lru或allkeys-lru,当server的内存数超过maxmemory的值,key的空转时长最高的部分key会被优先删除,可以通过 object idletime命令查看最近一次使用该key的时间,单位是秒。
在配置文件中使用maxmemory-policy 配置,默认是noeviction
过期删除策略
redis使用惰性删除和定期删除来清理过期的key
惰性删除:只在使用key的时候,对key进行过期检查,如果key已经过期,则删除该key;如果没有过期,则正常返回
定期删除:每隔一段时间,迭代所有database,取出一定数量的key,删除其中的过期key
RDB和AOF对过期key的处理:
- RDB:如果key已过期,save和bgsave会忽略过期建,如果server以master启动时载入RDB文件,期间检查到key已过期,也会直接忽略,如果是slave启动,不管是否已过期都会被加载,但是因为slave启动之后还会跟master同步数据,所以过期key还是会被删除掉,不会有影响。
- AOF:不管key是否过期,AOF都会被加载,如果执行BGREWRITEAOF命令会重写日志并清理已经过期的key,但是当client访问过期key,server会删除过期key并追加del key到AOF中。如果是主从模式,slave不会主动删除过期key,而是会等master删除后向slave发送一个del命令再删除。假设在这期间,有client访问了slave节点的过期key,slave会按照未过期key正常处理请求。
复制
redis的复制分为两种情况(官方文档):
- 将从服务器的数据库状态同步到主服务器状态
- 在主服务器的数据被修改时,通过命令传播的方式发送给从服务器,从服务器同样也执行该命令和主服务器保持数据同步
sync命令的执行过程:
- 从服务器向主服务器发送sync命令
- 主服务器收到sync命令之后,会先执行bgsave命令在后台生成一个RDB文件,并使用一个缓冲区来记录生成RDB文件期间的命令
- 当主服务器的RDB文件生成完成时,会将RDB文件发送给从服务器,从服务器接收并load这个RDB,将自己的数据库更新
- 主服务器将记录在缓冲区中的所有命令发给从服务器,从服务器接收之后,开始执行命令更新到与主服务器数据库的状态,这时主从数据库的状态达到一致
- 每次主服务器接收到新的命令请求时,都会对从服务器做一次命令传播,从服务器执行后,再次与主服务数据库状态达到一致
sync的执行过程中,如果主服务器在向从服务器传播命令的过程中,如果从服务器还没来得及接收主服务器的命令就宕机,这种情况下,从服务器数据就会不一致,除非上线后再次手动执行sync进行全量同步,但是sync实际上是很重,很耗费资源的操作,如果从服务器在宕机后立刻重新上线,期间与主服务器数据的差值很小,完全没必要做全量同步操作,所以之后的psync命令解决了这个问题
psync命令有两种模式:
- 完整同步:用于处理首次复制,和sync命令的执行步骤是一样的,都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区中的增量命令来进行同步
- 部分重同步:用于处理断线后重新连接主服务器时,主服务器可以在从服务器断线期间执行的命令发送给从服务器,从服务器只需要执行这一部分的命令就可以把当前服务器数据和主服务器同步到最新状态
psync采用了复制积压缓冲区(一个固定大小的队列)和偏移量(offset)概念(类似kafka),主从双方都会维护一个偏移量值,主服务器每次向从服务器传播数据时,就将自己的偏移量增加,同时向复制积压缓冲区中添加一份数据并且也记录下数据的偏移量值,从服务器每次收到主服务器的数据时,也会将自己的偏移量增加,双方偏移量增加的数值就是发送和接受数据量的字节大小,如果主从服务器的数据一致,那么双方的偏移量大小一定是相等的,如果从服务器中途宕机下线,之后再次上线的时候,会向主服务器发送一个psync命令,并且上报自己的偏移量,主服务器在接收到从服务器发来的psync命令时,会先判断从服务器的偏移量大小是否在主服务器的复制积压缓冲区中,如果不在,则说明从服务器与主服务器数据之间的差值过大,则主服务器会向从服务器进行一次全量复制(完整同步),如果在,则把复制积压缓冲区中的数据传播给从服务器,从服务器接受后,进行偏移量增加,达到数据一致。
高可用
Sentinel:哨兵
Redis官方文档对于哨兵功能的描述:
监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
通知(Notification):哨兵可以将故障转移的结果发送给客户端。
- Sentinel是一个运行在特殊模式下的Redis服务器,且只能使用它特有的命令
- Sentinel会以每十秒一次的频率向被监视的主服务器和从服务器发送info命令,当主服务器处于下线状态,或Sentinel正在对主服务器进行故障转移时,Sentinel向从服务器发送info命令的频率会改为每秒一次
- Sentinel会以每秒一次的频率向所有redis实例(主,从,Sentinel)发送ping命令,根据回复判断对方是否下线
- 当Sentinel将一个主服务器判断为主观下线时,它会向其他的Sentinel进行询问,看其他Sentinel是否同意主服务器进入主观下线状态
- 当Sentinel收集到足够多的(过半)下线投票之后,它会向主服务器判断为客观下线,并发起故障转移操作
- 每个Sentinel会向每个redis服务器创建两个链接,一个订阅链接用户订阅服务器的__sentinel__:hello频道;另一个是命令链接,用于向主服务器发送/接收命令
- 两个Sentinel之间也会互相创建命令链接来交换信息
Sentinel主从选举(基于raft算法,详细可以点击这里)
- sentinel会以每秒一次的频率向所有redis实例(主,从,Sentinel)发送ping命令,根据回复判断对方是否下线
- 服务器对ping命令的响应有两种情况:有效回复(pong、loading、masterdown)和无效回复(超时),如果一个实例在down-after-milliseconds毫秒内对sentinel的回复都是无效,那么sentinel会认为此实例进入主观下线状态
- 当sentinel认为leader处于主观下线后,会向其它的sentinel进行确认,当sentinel从其它的sentinel接收到在配置文件中配置的数量的客观下线判断后,所有sentinel服务器将进行领头sentinel的选举
领头sentinel的选举规则:
- 所有sentinel都会参加选举
- 每个sentinel都会要求其它sentinel将自己设置为局部领头sentinel
- 在一次选举中,每个sentinel都只会把首次收到的选举消息的sentinel作为自己的局部领头sentinel,并且会给自己的局部领头sentinel回复消息(先到先得)
- 如果某个sentinel被超过半数的sentinel设置成局部领头sentinel,那么这个sentinel成为领头sentinel(过半)
- 如果在给定时间内,没有选出领头sentinel,那么过一段时间后,会重新选举
领头sentinel选举完成后,开始leader的故障转移与leader选举:
- 领头sentinel会把所有从服务器信息放到一个列表中
- 过滤掉列表中所有已下线或断线的从服务器
- 过滤掉列表中所有最近5秒内没有回复过领头sentinel消息的从服务器(保证最近通信)
- 过滤掉列表中所有与已下线主服务器连接断开超过down-after-miliseconds*10毫秒的从服务器
- 选择优先级
slave-priority最大的从服务器做为leader,如果没有最大或者有多个最大的,继续 - 按照从服务器的复制偏移量进行排序,选择最大的,如果没有最大或者有多个最大的,继续
- 最后按照从服务器的运行id排序,选择最大的作为leader
leader选出来后,领头sentinel会向从服务器发送 slaveof no one命令,将从服务器提升为leader服务器,然后开始故障转移:
-
领头sentinel会给其它所有从服务器发送slaveof 命令,让他们复制新的主服务器
-
将已下线的主服务器设置为新的主服务器的从服务器
Cluster:集群
Sentinel的写操作无法负载均衡,存储能力会有单机限制。redis3.0开始引入分布式存储方案,集群由多个Node组成,Redis的数据分布在这些Node中。集群中的Node分为master和slave:master负责读写请求和集群信息的维护;slave只进行主节点数据和状态信息的复制
cluster的主要作用有两点:数据分区和高可用;数据分区是为了解决存储能力受到单机限制,而高可用则是为了解决操作无法负载均衡以及实现故障恢复自动化。
数据分区有顺序分区,哈希分区等,而哈希分区具有天然的随机性,集群使用的分区方案便是哈希分区的一种
-
所有节点通过握手的方式加入集群(类似TCP三次握手),通过cluster meet ip port命令
-
集群中一共有16384个槽进行数据的拆分,每个节点都会记录每个槽的所在节点信息
-
节点在接到一个命令请求时,会先检查要处理的key是否在自己负责的范围内,如果不是,则会向客户端返回此key所在的节点信息,客户端会redirct返回的节点信息进行请求
-
计算key属于哪个槽:crc16(key) & 16383,命令cluster keyslot key 查看key属于哪个槽
-
集群节点只能使用0号数据库
-
cluster replicate nodeId 让接收命令的节点成为nodeId置顶的节点的从节点
脑裂问题(Split-Brain)
脑裂是指一个分布式系统中的节点之间出现网络分区,导致系统分裂成多个独立的子集,每个子集都认为自己是系统的正常部分。在 Redis Cluster 中,如果网络分区发生,可能会导致一些问题:
-
数据一致性问题:当发生网络分区时,不同的子集可能会修改相同的键,并在分区合并后导致数据不一致。例如,一个客户端在分区 A 修改了键 X 的值,同时另一个客户端在分区 B 修改了相同键 X 的值。在分区合并后,Redis Cluster 需要解决冲突并决定保留哪个修改。
-
主从复制问题:Redis Cluster 使用主从复制来实现数据的冗余备份和高可用性。当发生网络分区时,可能会导致主节点和从节点在不同的子集中。如果分区合并后,两个节点都试图成为主节点,可能会导致主节点冲突和数据不一致。
为了尽量避免脑裂问题,Redis Cluster 采用了一些机制:
-
主节点投票:当网络分区恢复时,Redis Cluster 使用基于投票的机制来决定主节点。只有获得大多数节点(过半)的支持的节点才能成为主节点,防止脑裂情况下产生多个主节点。
-
心跳检测和故障转移:Redis Cluster 使用心跳检测来监测节点的可用性。如果一个节点被判断为不可用,Redis Cluster 会将该节点上的槽迁移到其他可用节点上,并重新分配主从节点关系,以确保系统的可用性和一致性。
举个栗子: 假设一个 Redis Cluster 包含 6 个节点, A、B、C、D、E 、F。
-
网络分区:由于网络故障或其他原因,集群中的节点被分为两个子集:A、B 、C 在子集 1 中,D、E 、F 在子集 2 中。子集之间因为网络问题无法相互通信。
-
数据修改:clent1 连接到子集 1 中的节点 A,并修改键 X 的值为 100。同时,client2 连接到子集 2 中的节点 D,并修改同一个键 X 的值为 200。
-
分区恢复:网络故障修复,子集 1 和子集 2 之间的网络连接恢复。
在这个例子中,发生了脑裂。子集 1 和子集 2 都认为自己是整个 Redis Cluster 的正常部分。但是因为网络问题,子集 1 中的节点 A 无法感知到子集 2 中的节点 D 的修改,并相信键 X 的值仍然是 100。同样地,子集 2 中的节点 D 也无法感知到子集 1 中的节点 A 的修改,并认为键 X 的值是 200。
当分区合并时,Redis Cluster 需要解决这个冲突并决定键 X 的最终值。可能的情况是,某些节点保留键 X 的值为 100,而其他节点保留值为 200。这导致了数据的不一致性。
此外,由于脑裂情况下可能存在多个主节点,还可能导致主节点冲突和数据复制的不一致性。这取决于投票和领导者选举机制的结果。
扩展
Pipelining
众所周知redis是基于C/S架构的TCP server,意味着client每发送一个请求到server,server都需要先从socket中读取数据,然后执行命令,最后响应给client,假设redis每秒能处理1万个请求,在网络环境较差的情况下,如果一次RTT是50ms,redis相当于每秒最多处理20个请求,完全不能充分利用redis的性能。
使用pipelining管道能批量执行多个命令,redis会在内存中,为每个pipelining创建一个FIFO的queue,执行命令就相当于顺序pop这个队列,然后执行,能最大限度的提高redis处理命令的速度,并且因为每次I/O请求都会执行系统调用read()和write(),pipelining相当于减少了大量的用户态和内核态切换的开销,提高了整个redis服务的性能,举个栗子吧:
上图是分别使用pipeline和不使用pipeline的基准测试代码,再贴上结果:
使用pipeline的方法:
Result "test.RedisTest.withPipelining":
11.215 ±(99.9%) 0.169 ms/op [Average]
(min, avg, max) = (11.044, 11.215, 11.377), stdev = 0.112
CI (99.9%): [11.046, 11.384] (assumes normal distribution)
不使用pipeline的方法:
Result "test.RedisTest.withoutPipelining":
6172.159 ±(99.9%) 2716.869 ms/op [Average]
(min, avg, max) = (3313.240, 6172.159, 8310.269), stdev = 1797.041
CI (99.9%): [3455.290, 8889.028] (assumes normal distribution)
对比:
Benchmark Mode Cnt Score Error Units
RedisTest.withPipelining avgt 10 11.215 ± 0.169 ms/op
RedisTest.withoutPipelining avgt 10 6172.159 ± 2716.869 ms/op
实测性能相差两个数量级,window10,redis3,关闭RDB
注意Pipeline并不是原子性,因为redis在执行queue中的命中时,也会接收来自其它客户端的命令。
数据库通知
redis中有两种通知
1. 键事件通知:某个命令被什么键执行
subscribe__keyevent@0__:command key
2. 键空间通知:某个键执行了什么命令
subscribe__keyspace@0__:key command
开启通知有两种方式
1.配置文件:notify-keyspace-events "AKE",默认关闭
2.命令行:CONFIG SET notify-keyspace-events AKE
解释一下AKE:(截图取自官网)
需要注意的:
-
如果要开启事件通知,K或E是必须的。
-
所有的event都是基于key是真实存在的并且被修改,举个栗子,执行srem k1 v1,如果v1不存在k1中,是不会发出事件的。
-
expired events:因为redis中key过期策略的实现是 定时任务+客户端主动触发(访问时检测),只有当server端真实删除key之后,才会发送event,所以不保证expired events是实时的。
-
如果订阅设置AKE,每个key会收到两个event。
-
在cluster中,每个节点都会发送event,所以如果在生产中使用,必须订阅所有节点。
Pub/Sub
redis的pub/sub消息是at-most-once,即消息最多只会被消费一次,且当一个client订阅某个channel之后,只能受到当前时间以后的消息,不能收到历史消息,如果要实现可靠性,可以使用Streams。
基本用法:SUBSCRIBE channel1 channel2,表示订阅多个channel,并支持使用正则匹配订阅命令PSUBSCRIBE,且与database无关。取消订阅使用UNSUBSCRIBE或PUNSUBSCRIBE也可以取消一个或多个订阅。
需要注意的:
如果一个client执行 SUBSCRIBE book,再执行PSUBSCRIBE boo*,即两次订阅的channel有交集,会受到多条消息。
Sharded Pub/Sub(redis7)
Sharded Pub/Sub是为了解决Cluster模式下Pub/Sub的网络风暴问题,假设一个Cluster有100个节点,clent1对其中某个节点publish消息,这个节点会把这条消息广播给其它99个节点,为了避免浪费资源,Shareded 会把channel进行分发,一个shared节点只处理属于自己的channel而不会进行广播。
基本用法:SSUBSCRIBE, SUNSUBSCRIBE ,SPUBLISH,举个栗子:SSUBSCRIBE channel message
Lua脚本
redis在2.6版本开始支持lua脚本,redis客户端使用lua脚本直接在服务器端执行多个redis命令,而且一次lua脚本的执行是一次原子操作
- 服务器在执行Lua脚本之前,会为lua环境设置一个超时的hook函数,当脚本运行超时,可以在客户端执行script kill命令来执行hook函数终止脚本的执行,或者执行shutdown nosave命令关闭整个服务器
- 在redis中使用Lua脚本,大多数时候是用来实现分布式锁或组合执行原子操作
举个栗子:Redis+Lua实现令牌桶
慢日志
向mysql一样,redis也有自身用来记录慢指令的功能,用户可以通过这个功能产生的日志来监视和优化查询速度,通过使用slowlog get命令可以查看慢日志
相关的配置有两个:
- slowlog-log-slower-than:时间单位为微秒,每个指令在执行时如果超过这个时间,则会被redis记录下来
- slowlog-max-len:设置服务器最多保存多少条慢查询日志
如果服务器产生的慢日志数量超过slowlog-max-len的设定值,则会舍弃最早记录的日志,把当前的日志保存
事务
redis的事务提供了一种将多个命令打包放到一个FIFO队列中,然后一次性,按顺序的执行机制
redis通过五个核心命令来实现事务
- multi:开启事务
- exec:执行事务
- watch:监视keys,如果在事务执行之前(exec),监视的key被其他命令改动,事务将取消执行(乐观锁),在redis6.0.9之前,被watch的key如果过期,是不会影响事务的,之后的版本则会取消事务制修订。
- discard:取消事务
- unwatch:取消监视
redis的事务有两点可以保证:
-
在执行事务期间,Redis会按照顺序依次执行事务中的命令,而不会中途插入执行其他命令。
-
如果client在执行exec之前断开连接,则整个事务不会被执行,当事务被执行,并且开启了AOF,redis会保证整个事务中的数据通过一次write系统调用来落盘,但是假如在落盘过程中服务crash或被kill掉,那整个事务可能会有一个中间态(部分落盘),在重启服务时,redis会报错并退出,可以使用redis-check-aof工具来修复这种异常数据(删除),再重新启动服务。
在redis执行事务期间,可能会有几种错误:
-
在事务执行过程中,某个命令执行出错,比如使用了错误的命令(对一个string类型的key使用hash或list类型的命令)或参数,或者对不存在的键执行操作等。这种错误不会中断事务的执行,而是记录错误信息,继续执行后续的命令。在执行EXEC命令时,Redis会返回每个命令的执行结果,所以需要根据结果判断是否有执行错误发生。
-
事务执行过程中,可能redis可能会因为语法错误或服务器配置的参数报错(maxmemory限制内存),redis会检测语法错误,如果有语法错误会放弃执行整个事务。
Redis的事务不支持回滚,但是它会检查事务中的每一个命令使用是否错误,如果有错误便不会执行事务,只有通过检查才会开启事务执行并且会全部执行。
监视器
客户端通过执行monitor命令,可以实时的接收并打印服务器当前处理的请求信息,redis服务器在接收命令请求之外,还会将当前命令请求的信息发送给所有监视器,服务器端使用链表来保存所有的监视器信息。
Client-side Caching(redis6):
redis使用_Tracking_来实现,有两种模式
1. default-mode:server端会记住每个client所访问过的keys,当前某个key被修改或过期删除,server会发送过期消息来通知客户端清除本地缓存,这种模式会额外占用server端的内存
举个栗子:
- client1 -> server:client tracking on
- client1 -> server:get k1
- (这里server会记住client1已经缓存了k1,而client1会保存k1到本地缓存)
- client2 -> server:set k1 xxx
- server -> client1:invalidate k1(给client1发过期消息,client1会清除本地缓存的k1)
这种模式下,redis也会尽量控制内存使用量
使用过期表:这个过期表里保存了开启了客户端缓存且被访问到的keys和clientId,当达到容量上限时,会把表内最老的key做虚假修改处理,先从过期表里删除掉最老的key,然后server端会假装这个key被修改了,再通知client,让client做本地缓存过期(类似LRU)。如果期间client断开连接,那对应的keys也会被清除。
2. broadcasting:这种模式server不会记住每个client访问过的keys,而是每个client会subscribe自己访问过的key的变化(类似键空间通知),这种模式下只会少量使用server端内存,client使用这种模式时,会指定key的一个或多个prefix发送给server端,server端只保存prefix和对应的clientId(前缀表),如果客户端传的prefix是空字符串就表示所有key,server端同样也是用过期消息来通知client过期本地缓存,但是这种模式会增加CPU消耗,因为要匹配key的prefix。
RedLock
红锁 是 Redis官方 的一种分布式锁实现(它的实现是有争议的,感兴趣的可以看这里,作者对此的回复在这里)
RedLock 是基于 Redis 的单实例或多实例部署模式下实现的。它使用 Redis 的原子操作和 Lua 脚本,通过互斥锁的方式实现分布式锁。工作原理:
-
获取锁:
- 客户端请求获取锁时,会生成一个唯一的标识符(例如 UUID)作为锁的拥有者标识。
- 客户端依次尝试在多个 Redis 实例上(过半)使用 SETNX(SET if Not eXists)命令创建相同的锁键,并设置过期时间,保证只有一个客户端能够成功获取到锁。
-
验证锁:
- 客户端在获取锁后,会通过 GET 命令检查锁的值是否与自己生成的标识符一致,以验证锁是否有效。
- 如果锁的值与标识符一致,则表示客户端成功获取到了锁。
-
续约锁:
- 在获取到锁后,客户端需要定期发送 RENEW 命令来续约锁的过期时间,防止锁自动过期而导致其他客户端获取到锁。
-
释放锁:
- 客户端释放锁时,会使用 DEL 命令删除锁键,将锁释放,并且确保只有拥有相同标识符的客户端才能成功删除锁键。
RedLock 的优势和问题:
- 分布式:RedLock 可以在多个 Redis 实例之间实现分布式锁,避免单点故障和数据不一致的问题。
- 容错性:即使部分 Redis 实例出现故障,只要大多数 Redis 实例仍然可用,RedLock 仍然能够正常工作。
- 高可用性:使用 RedLock 可以实现高可用的分布式锁服务,防止锁服务成为单点故障。
需要注意的是,RedLock 仍然存在一些限制和潜在的问题:
- 时钟漂移:RedLock要求每个实例的系统时间必须同步,如果时钟差距大,可能会导致锁的过期时间不一致,会引发并发问题。
- 锁的持有时间:避免长时间持有锁导致其他请求阻塞。
- 可靠性:RedLock 强依赖redis的高可用,如果加锁成功后,某些持有锁的实例出现故障或网络分区,可能导致锁无法正常释放或丢失锁的一致性。
- 网络延迟:Redlock 算法需要在多个 Redis 实例之间进行通信,如果网络延迟过高,可能会导致锁的竞争条件,从而降低性能或导致并发问题。
总的来说,RedLock 是一种使用 Redis 实现的分布式锁方案,可以在分布式系统中实现并发控制和数据一致性,但在使用时需要注意其限制和潜在的问题。
Commend Hooks(6.2,实验功能)
命令钩子,它允许用户在server执行命令之前和之后注入自定义逻辑(文档)。
使用方式:可执行文件、脚本文件或者其它语言的库实现。
配置文件:
command-hooks-enabled yes (启用命令钩子功能)
command-hook-dir /path/to/hooks (钩子脚本所在的目录)
如果我们想在java层面使用类似的功能,可以使用jedis3.x版本,通过jedis.addListener添加监听器实现。
网络模型
在 Redis 中,通过使用 Reactor 模型来实现非阻塞 I/O 和事件驱动。
Redis 使用的 Reactor 模型包括以下组件:
-
事件处理器(Event Handler):事件处理器是 Redis 中的核心组件,负责处理事件的触发和相应的回调。由一个单独的线程或进程驱动。在 Redis 中,事件处理器负责监听套接字上的事件,可读事件(读取客户端请求)、可写事件(向客户端发送响应)和其他错误事件。
-
事件循环(Event Loop):事件循环是 Reactor 模型的核心。它是一个循环结构,负责不断地监听和分发事件。事件循环会调用事件处理器注册的回调函数来处理各种事件。在 Redis 中,事件循环基于非阻塞 I/O 多路复用技术(如
epoll、select、kqueue等)来等待和监控套接字上的事件,实现高效事件驱动。 -
事件注册和回调:Redis 的事件处理器允许用户注册感兴趣的事件(如套接字的可读和可写事件)。当事件循环监听到相应的事件时,会调用注册的回调函数来处理事件。在 Redis 中,事件的回调函数通常与具体的客户端连接相关,用于处理接收到的命令、发送响应等操作。
Redis通过使用Reactor 模型具有以下优点:
-
高性能:通过非阻塞 I/O 和事件驱动,Redis 可以处理大量并发的请求和连接,实现高吞吐量和低延迟。
-
资源利用率高:Redis 使用单线程(6.x版本是多线程)来处理所有的请求和连接。
-
可扩展性:通过事件驱动模型,Redis 可以轻松地扩展以满足不断增长的负载需求。
在Redis6以下 的 Reactor 模型是单线程的,即使用一个线程来处理所有的事件和请求。这种设计简化了并发控制和数据一致性的问题,也降低了多线程切换带来的额外开销。但是,也意味着 Redis 在处理高并发负载时可能成为性能瓶颈。为了提高吞吐量和并发性能,Redis 6.0 进行了优化,引入了 IO 多线程,把读写请求数据的逻辑,用多线程处理,提升并发性能,但处理请求的逻辑依旧是单线程处理。
使用redis作为缓存需要面临的一些问题:
- 缓存穿透:频繁查询DB与缓存中都不存在的key值,导致每次查询都会进入穿透到DB
- 缓存击穿:大量请求同时请求同一个过期的key,导致所有请求全都打到DB
- 缓存雪崩:高并发下,大量缓存全部失效,所有请求全都打到DB
常用的解决方案:
- 穿透:布隆过滤器、布谷鸟过滤器、缓存空值
- 击穿:分布式锁
- 雪崩:降级限流、缓存永不失效、分散key的过期时间
为什么redis快?
- 单线程,不需要额外的线程上下文切换
- 工作模型采用IO多路复用
- 运行在内存