Redis概述
Redis
是一个开源的使用ANSI C
语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value
(键值型)数据库(非关系型数据库),并提供多种语言的API
。Redis
是一个高性能的Key-Value
数据库。它的出现很大程度补偿来MemCached
这类Key-Value
型存储的不足,在部分场合下可以对关系型数据库起到很好的补充作用。它提供来Java
、C/C++
、PHP
、JavaScript
、Perl
、Object-C
、Python
、Ruby
、Erlang
等客户端,使用方便。Redis
支持主从同步,Redis
能够借助于Sentinel
(哨兵,Redis
自带的)工具来监控主从节点,当主节点发生故障时,会自己提升另外一个从节点成为新的主节点。
数据类型
Redis常见的数据结构有5种: String,List, Set, ZSet, Hash
String(字符串)
字符串类型由int和动态字符串(Simple Dynamic String,SDS)
int
数字结构,编码方式是int
SDS
数据结构,编码方式是embstr
len
:表示已使用的字符长度。alloc
:表示分配的内存大小。buf
:实际存储字符的数组。
3.2版本之后,SDS
结构会根据字符串的长度来选择对应的数据结构
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5) // 32
return SDS_TYPE_5;
if (string_size < 1<<8) // 256
return SDS_TYPE_8;
if (string_size < 1<<16) // 65536 64k
return SDS_TYPE_16;
if (string_size < 1<<32) // 4294967296 4G
return SDS_TYPE_32;
return SDS_TYPE_64;
}
优点
- 获取字符串长度的时间复杂度为
O(1)
:因为长度是保存在结构中的,不需要遍历整个字符串。 - 惰性空间释放:在缩短字符串时,并不会立即缩小内存,而是保留以备后用,减少了频繁的内存分配和释放。
- 预分配:在扩展字符串时,按一定策略多分配一些内存,减少了内存分配的次数。
操作命令
SET key value
:设置key
的值。如果key
存在,覆盖。GET key
:获取key
的值。INCR key
:将key
的值加1
。如果key
不存在,初始化为0
后再加1
。DECR key
:将key
的值减1
。如果key
不存在,初始化为0
后再减1
。APPEND key value
:将value
追加到指定key
的值之后。如果key
不存在,则创建一个新的key
。STRLEN key
:获取key
的值的长度。MSET key value [key value ...]
:同时设置多个key-value
对。MGET key [key ...]
:同时获取多个key
的值。GETSET key value
:设置新的值并返回旧的值。
使用场景
- 缓存数据:常用于缓存数据,如:缓存数据,减少数据库交互,提高性能。
- 分布式锁:通过
SETNX
命令可以实现分布式锁。 - 计数器:例如记录访问量,通过
INCR
和DECR
命令实现。
List(列表)
列表是一种有序的数据结构,允许在头部和尾部进行插入和删除操作。 列表中的每个元素都是一个字符串,并且可以通过索引下标进行访问。 列表底层是双向链表,当元素较少时,会用压缩列表来实现。
数据结构
- 压缩列表(
ziplist
):当列表中的元素较少且每个元素长度较短时,Redis
使用压缩列表实现。这是一种连续内存块,内存占用较少,但在执行插入和删除操作时需要移动大量数据(连锁更新问题)。 - 双向链表(
linkedlist
):当列表中的元素较多或元素较大时,Redis
使用双向链表实现。双向链表的优点是插入和删除操作的时间复杂度为O(1)
,但每个节点都需要额外的内存来存储前驱和后继指针。 - 快列表(
quicklist
)在Redis 3.0
版本中,小的列表使用ziplist
实现以节省内存。从Redis 3.2
版本开始,列表使用quicklist
(快列表)实现,它是由多个ziplist
组成的链表,每个ziplist
都有一个固定的最大长度。 - 列表包(
listpack
)Redis
在5.0
新设计一个数据结构叫listpack
,目的是替代ziplist
解决连锁更新问题,在Redis 7.0
版本中已经替换为listpack
,它最大特点是listpack
中每个节点不再包含前一个节点的长度,而是记录当前节点的长度。
操作命令
LPUSH key value
:将value
插入到列表的左端。RPUSH key value
:将value
插入到列表的右端。LPOP key
:移除并返回列表的左端元素。RPOP key
:移除并返回列表的右端元素。LRANGE key start stop
:获取列表中指定范围内的元素。范围从start
到stop
,包括start
和stop
。LINDEX key index
:通过索引获取列表中的元素,索引从0
开始。LSET key index value
:通过索引设置列表中元素的值。LLEN key
:获取列表的长度。LINSERT key BEFORE|AFTER pivot value
:在列表中指定的值前或后插入新值。
使用场景
- 消息队列:通过
LPUSH
和RPOP
命令实现,生产者将消息放入队列左端,消费者从右端取出消息。 - 任务队列:存储待处理的任务,通过
BRPOP
实现阻塞队列,等待任务的到来。 - 最近访问记录:例如浏览历史,最新访问的内容总是插入到列表头部。
ziplist
使用条件
当列表中的元素数量较少且每个元素的大小也较小时,Redis
会选择使用ziplist
。
具体来说,如果列表的元素数量不超过一定阈值(例如512
个元素),并且每个元素的大小不超过一定的限制(例如64
字节),Redis
就会使用ziplist
。
优点
ziplist
可以显著节省内存,因为它将多个值紧密地存储在一起,减少了内存碎片和提高了内存利用率。
对于小列表,ziplist
提供了更好的内存效率。
缺点
当列表增长时,可能需要转换为其他数据结构。
quicklist
在5.0之前版本是基于ziplist
+linkedlist
实现的。之后是listpack
。
使用条件
quicklist
是Redis 3.2
版本后默认使用的list
类型的底层实现。
当列表的元素数量较多或者元素的大小超过ziplist
的限制时,Redis
会自动将列表转换为quicklist
。
quicklist
是一个由多个ziplist
组成的双向链表,每个ziplist
都有一个固定的最大长度(例如512
字节)。
优点
quicklist
提供了更好的性能和内存管理,特别是在列表较大时。
由于quicklist
中的每个节点都是一个独立的ziplist
,因此可以更好地利用内存并且减少单个ziplist
的大小。
quicklist
支持在链表的两端进行高效的插入和删除操作。
缺点
相对于 ziplist,在小列表的情况下可能不是最节省内存的选择。
linkedlist
使用条件
在Redis 3.2
之前的版本中,当列表太大以至于不能使用ziplist
时,Redis
会使用linkedlist
。
但在Redis 3.2
及以后的版本中,quicklist
成为了默认的list
类型实现,因此linkedlist
不再作为首选实现。
优点:
支持在链表的两端进行高效的插入和删除操作。 对于非常大的列表,可以提供较好的性能。
缺点:
内存使用效率低于ziplist
和quicklist
。
现在版本的Redis
中不再作为首选实现。
listpack
ziplist
会出现连锁更新
的现象,为了解决这个问题,最开始Redis
引入了quicklist
,通过控制QuicklistNode
结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响。但是quicklist
并没有完全解决连锁更新的问题。
为了彻底避免连锁更新的出现,在Redis 7
使用listpack
代替ZipList
彻底解决连锁更新的问题。
要想彻底解决ziplist
连锁更新问题,本质上要修改ziplist
的存储结构,也就是不要让每个
元素保存上一个
元素的长度,因此迭代出了listpack
listpack
每个元素项不再保存上一个
元素的长度,而是通过记录entry
长度以及element-tot-len
中特殊的结束符,来保证既可以从前也可以向后遍历
转换机制
后续版本中ziplist
替换成了ListPack
,转换机制还是一样的
ziplist
->quicklist
- 当列表的元素数量增加到某个阈值(例如
512
个元素)时,Redis
会检查列表是否仍然适合使用ziplist
。 - 如果列表的总大小(包括所有元素的大小加上
ziplist
的开销)超过了某个阈值(例如1 MB
),Redis
会将列表从ziplist
转换为quicklist
。 - 如果列表中的任何一个元素的大小超过了
ziplist
的限制(例如64
字节),Redis
也会触发转换。
- 当列表的元素数量增加到某个阈值(例如
quicklist
->ziplist
- 如果列表减小到了足够小的程度,
Redis
会检查列表是否可以再次使用ziplist
。 - 如果列表的元素数量减少到某个阈值以下,并且每个元素的大小都在
ziplist
的限制之内,Redis
会将quicklist
转换回ziplist
。 - 一般来说,这种转换不会频繁发生,因为
Redis
试图避免不必要的转换带来的性能开销。
- 如果列表减小到了足够小的程度,
转换策略
- 自动检测
Redis
会在执行列表相关的命令时自动检测列表的状态,比如LPUSH
、RPUSH
、LPOP
、RPOP
等命令。- 检测机制会检查列表的当前状态,包括元素的数量、每个元素的大小以及列表的总大小。
- 根据检测的结果,
Redis
会决定是否进行数据结构的转换。
Sets(集合)
集合是一种无序且不重复的字符串集合。集合底层基于哈希表,当元素较少时会使用整数数组。
数据结构
- 整数集合(
intset
):当集合中的元素都是整数且数量较少时,使用整数集合实现。整数集合是一种紧凑的数据结构,内存占用少,但只支持整数类型。 - 哈希表(
hashtable
):当集合中的元素较多或包含非整数类型时,使用哈希表实现。哈希表的查找、插入和删除操作时间复杂度为O(1)
,但每个元素需要额外的内存来存储哈希值和指针。
操作命令
SADD key member
:向集合添加一个元素。如果元素已存在,则忽略该操作。SREM key member
:移除集合中的一个元素。如果元素不存在,则忽略该操作。SMEMBERS key
:返回集合中的所有元素。SISMEMBER key member
:判断member
是否是集合中的元素。SUNION key [key ...]
:返回给定所有集合的并集。SINTER key [key ...]
:返回给定所有集合的交集。SDIFF key [key ...]
:返回第一个集合与其他集合的差集。SCARD key
:获取集合的元素数量。
使用场景
- 标签管理:例如给文章添加标签,一个标签集合对应一个文章。
- 好友关系:存储用户的好友列表,通过集合的交集操作可以找到共同好友。
- 去重操作:如:存访问
IP
,通过集合的无重复特性实现去重。
Sorted Sets(有序集合,也叫ZSet)
有序集合类似于集合,但每个元素都会关联一个分数(score
),Redis
会按分数值进行排序。
分数可以是任意双精度浮点数。与集合不同,有序集合中的元素是有序的。
有序集合的底层实现是跳跃表(skiplist
)和哈希表(hashtable
)的结合。
数据结构
- 跳跃表(
skiplist
):跳跃表是一种以层级结构实现的有序数据结构,支持高效的范围查询和按分数排序。跳跃表由多个层级构成,每一层是一个有序链表,底层链表包含所有元素,每高一层的链表是低层链表的一个子集。跳跃表的查找、插入和删除操作的平均时间复杂度为O(log N)
。 - 哈希表(
hashtable
):哈希表用于快速查找元素和分数,支持O(1)
时间复杂度的插入、删除和查找操作。
这种结构让有序集合具备高效的范围查询和排序能力,能快速进行元素查找和更新操作。
操作命令
ZADD key score member
:向有序集合添加元素,并设置其分数。如果元素已存在,则更新其分数。ZREM key member
:移除有序集合中的一个元素。ZRANGE key start stop [WITHSCORES]
:返回指定范围内的元素(按分数从低到高排序)。ZREVRANGE key start stop [WITHSCORES]
:返回指定范围内的元素(按分数从高到低排序)。ZRANK key member
:返回元素的排名(按分数从低到高)。ZREVRANK key member
:返回元素的排名(按分数从高到低)。ZSCORE key member
:返回元素的分数。ZINTERSTORE destination numkeys key [key ...]
:计算给定有序集合的交集,并存储在新的有序集合中。ZUNIONSTORE destination numkeys key [key ...]
:计算给定有序集合的并集,并存储在新的有序集合中。
使用场景
- 排行榜:例如积分排行榜,通过分数进行排序,实时更新排名。
- 优先级队列:通过分数表示优先级,分数越低优先级越高。
- 延迟队列:通过分数表示延迟时间,分数越低延迟越短。
Hash(哈希类型、关联数组)
哈希是一种键值对集合,每个键对应一个哈希表,哈希表内部包含多个字段和对应的值,适用于存储对象数据。 哈希类型的数据结构类似于传统的字典或映射表,特别适合表示对象(例如用户信息、商品信息等)。
数据结构
- 压缩列表(
ziplist
):当哈希表中的字段较少且字段和值长度较短时,使用压缩列表实现。压缩列表是一种连续内存块,内存占用较少,但在执行插入和删除操作时需要移动大量数据。 - 哈希表(
hashtable
):当哈希表中的字段较多或字段和值较长时,使用哈希表实现。哈希表的查找、插入和删除操作时间复杂度为O(1)
,但每个字段和值需要额外的内存来存储哈希值和指针。 - 列表包(
listpack
)Redis
在5.0
新设计一个数据结构叫listpack
,目的是替代ziplist
解决连锁更新问题,在Redis 7.0
版本中已经替换为listpack
,它最大特点是listpack
中每个节点不再包含前一个节点的长度,而是记录当前节点的长度。
操作命令
HSET key field value
:设置哈希表中指定字段的值。如果字段不存在,则创建。HGET key field
:获取哈希表中指定字段的值。HDEL key field [field ...]
:删除哈希表中指定字段。HGETALL key
:获取哈希表中所有字段和值。HKEYS key
:获取哈希表中的所有字段。HVALS key
:获取哈希表中的所有值。HLEN key
:获取哈希表中的字段数量。HEXISTS key field
:判断哈希表中是否存在指定字段。HMSET key field value [field value ...]
:同时设置哈希表中多个字段的值。HMGET key field [field ...]
:同时获取哈希表中多个字段的值。
使用场景
- 存储信息:例如用户信息、商品信息等,通过哈希表存储。
- 会话信息:存储会话状态和数据。
Bitmaps(位图)
位图是一种紧凑的方式来存储二进制数据,可以将其视为一个位数组。
每个位可以存储0
或1
,用于表示布尔值。
位图通常用于记录状态信息,如用户签到、活动参与情况等。
数据结构
- 位图是基于字符串实现的,字符串的每个字节由
8
个比特位构成,可以表示8
个布尔值。位图操作实际上是对字符串进行位操作。
操作命令
SETBIT key offset value
:将位图中指定偏移量的位设置为0
或1
。GETBIT key offset
:获取位图中指定偏移量的位的值。BITCOUNT key [start end]
:统计位图中值为1
的位的数量。BITOP operation destkey key [key ...]
:对一个或多个位图进行按位操作,并将结果存储在新的位图中。操作包括AND
、OR
、NOT
、XOR
。
使用场景
- 用户签到:记录用户每天的签到情况,一个位代表一天。
- 活动参与:记录用户是否参与活动。
- 权限管理:记录权限位,一个位代表一种权限。
HyperLoglog
HyperLogLog
是一种用于基数统计的概率算法,适用于需要统计大量数据的场景,如独立IP
访问量、用户数等。
它的优势在于占用内存非常小,但能够在一定误差范围内提供准确的基数估计。
数据结构
HyperLogLog
的数据结构基于概率算法,通过哈希函数将数据映射到不同的桶,并记录桶中的最大值。它使用少量内存(通常12KB
)来存储基数估计信息。
操作命令
PFADD key element [element ...]
:将元素添加到HyperLogLog
中。PFCOUNT key [key ...]
:返回HyperLogLog
中独立元素的估计数量。PFMERGE destkey sourcekey [sourcekey ...]
:合并多个HyperLogLog
并将结果存储在新的HyperLogLog
中。
使用场景
- 独立访客统计:统计网站独立访客数量。
- 用户行为分析:统计不同用户的行为次数,如点击、点赞等。
Geo(地理空间)
Geo
可以存储地理位置数据,并提供基于位置的操作命令,如附近位置查询、距离计算等。
数据结构
- 地理空间数据类型基于有序集合(
Sorted Set
)实现。每个成员的分数是通过Geohash
算法计算得到的,使得地理位置可以通过有序集合进行存储和排序。
操作命令
GEOADD key longitude latitude member
:将地理位置添加到地理空间集合中。GEOPOS key member [member ...]
:获取地理空间集合中成员的位置(经度和纬度)。GEODIST key member1 member2 [unit]
:计算两个成员之间的距离,单位可以是m(米)
、km(千米)
、mi(英里
)、ft(英尺)
。GEORADIUS key longitude latitude radius m|km|mi|ft
:以给定的经纬度为中心,查询指定半径范围内的所有成员。GEORADIUSBYMEMBER key member radius m|km|mi|ft
:以给定的成员为中心,查询指定半径范围内的所有其他成员。
使用场景
- 附近地点查询:例如餐厅、商店、加油站等。
- 用户位置服务:提供基于位置的服务,如打车、外卖等。
性能
100万
较小的键存储字符串,大概消耗100M
内存;Redis
是单线程,如果服务器主机上有多个CPU
,只有一个能够使用,但并不意味着CPU
会成为瓶颈,因为Redis
是一个比较简单的K-V
数据存储,CPU
不会成为瓶颈的在常见的
linux
服务器上,500K
(50万
)的并发,只需要一秒
处理,如果主机硬件较好的情况下,每秒钟可以达到上百万的并发
Redis与MemCache
MemCache
只能使用内存来缓存对象。而Redis
除了可以使用内存来缓存对像,还可以周期性的将数据保存到磁盘上,对数据进行永久存储。当服务器突然断电或死机后,Redis
基于磁盘中的数据进行恢复Redis
是单线程服务器,只有一个线程来响应所有的请求。MemCache
是多线程的Redis
支持更多的数据类型
持久化
Redis
提供了多种级别的持久化方式:
RDB
持久化可以在指定时间间隔生成数据的时间快照(point-in-time snapshot
)。AOF
持久化记录服务器所有的写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据,AOF
文件中的命令全部以Redis
协议的格式保存,新命令追加到文件的末尾,Redis
还可以在后台对AOF
文件重写,保证AOF
文件大小不超过保存数据状态的实际大小
RDB
RDB
触发可以分两种手动触发和自动触发,手动触发对应save
和bgsave
命令
save
阻塞当前redis
服务器,直到RDB
完成,对内存大的实例会造成长时间的阻塞,线上不建议使用
bgsave
:Redis
进程执行fork
操作创建子线程,由子线程负责,完成后自动结束,阻塞只会在fork
时,一般很短
自动触发通过配置save
,如save m n
,表示m秒
内数据集存在n次
修改时,
自动触发bgsave
, 节点执行全量复制操作,主节点自动执行bgsave
生成RDB
文件并发送给从节点,
执行debug reload
命令重新加载Redis
时,也会自动触发save
操作,
默认情况下执行shutdown
命令时,如果没有开启AOF
持久化则自动执行bgsave
,bgsave
是RDB
主流的持久化方式,
执行bgsave
命令时,Redis
父进程判断是否存在正在执行的RDB
、AOF
子进程,有就直接返回,没有就fork
操作创建一个子线程,fork
过程中父进程会阻塞,
通过info stats
可以查看latest_fork_usec
选项,可以返回一个最近fork
操作的耗时,单位微秒,fork
完后,
bgsave
返回background saving started
不在阻塞父进程,可以继续其他命令,子线程创建RDB
文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换,
通过lastsave
命令可以查看最后一次生成RDB
时间,子线程完成后告诉父线程,父线程更新信息,RDB
文件保存在dir
配置指定的目录下
优点
它是一个二进制的文件,代表Redis
在某个时间节点上的数据快照,非常适合备份,全量复制,比如每6小时
执行bgsave
备份,
并把RDB
文件拷贝到远程机器或文件系统,用于灾难恢复,Redis
加载RDB
恢复数据远远快于AOF
方式
缺点
没办法做到实时/秒级持久化,因为每次bgsave
都要执行fork
创建子线程,属于重量级操作,频繁操作成本太高,
RDB
文件使用特定的二进制保存,Redis
老版本无法兼容新版本
AOF
开启AOF
需要设置配置:appendonly yes
,默认不开启,AOF
文件保存路径也是通过dir
配置
AOF
流程是:命令写入(append
)、文件同步(sync
)、文件重写(rewrite
)、重启加载(load
)
AOF
所有的写入命令会追加到aof_buf
(缓冲区),Redis
使用单线程响应命令,如果每次写AOF
文件命令都追加到硬盘,那性能全部取决于硬盘的负债
Redis
可以提供多种缓冲区同步硬盘的策略,在性能和安全做出平衡,根据AOF
的文件越来越大,需要定期重写AOF
文件,达到压缩,
重写就是把已经超时的数据不在写入,还有各种数据定义和改变过程,只保留最终的值,多个写入的命令也可以合并一个,
重写不止减少空间,也是为了Redis
更快的重载,
重写过程可以手动触发和自动触发
- 手动触发直接调用
bgrewriteaof
命令,自动触发根据设置的参数(auto-aof-rewrite-min-size
最小大小 默认64M
,auto-aof-rewrite-percentage
当前文件空间(aof_current_size
)和上次重写AOF文件空间(aof_base_size
)的比值) - 自动触发机制:当前文件大小大于最小体积 && (当前大小-上次重写大小) / 上次重写大小 >= 规定的比值
Redis
重启时,若开启AOF
持久化并存在AOF
文件,优先加载AOF
文件,AOF
关闭或文件不存在,加载RDB
,加载文件成功后,
Redis
启动成功,文件存在错误,启动失败并打印错误信息
高可用
Redis
的几种常见使用方式包括
- 单副本
- 多副本(主从)
Sentinel
(哨兵)Cluster
- 自研
主从复制
Redis
主从复制主要分两个角色,主机(master
)主要负责读写操作,从机(slave
)主要负责读操作,主机定期同步数据到从机上,保证数据一致性。
Redis
同步数据主要分两种,全量同步和增量同步。
主从复制不会阻塞master
,在同步数据时,master
还可以继续处理请求,Redis
会生成新的进程来解决同步问题。
主从里面的从也可以是主(树形结构),提高效率,减少主机压力。 主机可以有多个从机,从机只能有一个主机。
主从配置一般是修改redis.conf
文件内的slaveof
格式:slaveof ip port
,
redis-cli -p 6379 info Replication
:可以查看主机有几个从机。
同步数据
主要分全量同步和增量同步
从机第一次链接一定是全量同步,短线重连根据runid
判断是否一致来执行全量同步或增量同步,
每个redis服务器都有自己的runid
,主机根据runid
查询有没有保存,
没有就全量同步,有就增量同步,
主从服务器会分别维护一个offset
(复制偏移量)主机每次向服务器传播N
个字节的数据时,
就会把自己的offset
的值加N
,从机每次接受到N
个字节数据时,就将自己的offset
加N
。
复制积压缓冲区是主机维护的一个固定长度的先进先出的队列,默认大小1M
,
主要是当主机传播命令时,把命令放入,当断开时,
主机会将缓冲区的所有数据发给从机(断开之后的数据)。
同步执行过程,从机链接时判断自己是否保存了主机的runid
(判断是否第一次),
没有保存就向主机发出全量同步,有保存就把runid
发送给主机,主机判断是否和自己的一致,
不一致就把当前的runid
在发给从机并执行全量同步,一致就会判断offset
相差有没有超过缓冲区的大小,
没有就等待主机同步数据给从机,超过主机就生成快照文件,给从机在同步缓冲区的数据。
全量同步分三个流程:
- 同步快照(主机创建并发送快照给从机,从机进入快照并解析,主机同时将此阶段生成的新命令写入到缓冲区),
- 同步缓冲区(主机向从机同步缓冲区的写的操作命令),
- 同步增量(主机同步写操作到从机)
增量同步主要在从机完成初始化正常工作时,主机发生写操作就同步到从机, 正常主机每执行一个写命令就向从机发请求,从机接受并处理。
哨兵(sentinel
)机制
sentinel
主要监控Redis
集群中master
的状态,当master
发生故障时,可以实现master
和slave
的切换,保证系统的高可用。
主从的缺点,没法对master
进行动态选举,这需要sentinel
机制完成。
sentinel
会不断检查master
和slave
状态是否正常,当发现某个节点出问题时,
sentinel
可以通过API
向管理员或其他应用程序发送通知。
当master
不能正常操作时,sentinel
会开始一次故障转移,会将失效的master
下的一个slave
升级为新的master
,
并让其他slave
改为新的master
,当客户端试图链接失效的master
,集群会向客户端展示新的master
地址,切换后对应的配置文件都会有所变化,
master
会对一个slaveof
的配置,slave
对应的master
也改成新的,sentinel.conf
的监控对象也会改变。
sentinel 故障判断原理
每个sentinel
进程每秒钟一次的频率向整个集群中的master
、slave
以及其他的sentinel
进程发送一个ping
的请求
如果一个实例距离最后一次有效ping
请求超过down-after-milliseconds
规定的值,这个实例就会被sentinel
标记为主观下线(SDOWN
)
如果一个master
被标记为主观下线,则正在监视这个master
的sentinel
进程要以每秒一次
的频率确定master
的确进入主观下线状态
当超过配置文件中给定的sentinel
的数量,在指点的时间范围内确定master
进入了主观下线状态
,则master
会被标记为客观下线(ODOWN
)
一般情况每个sentinel
会以每10s一次
的频率向集群中所有的master
、slave
发送info
命令,
当master
被标记为客观下线
,sentinel
会向下面所有的slave
发送info
的频率改为1s一次
若没有一定数量的sentinel
同意master
下线,那master
的客观下线状态会被移除
若master
对ping
的命令有回复,master
的主观下线状态也会被移除
Redis雪崩、穿透、并发等问题
在高并发中,Redis
会出现雪崩、穿透、并发等问题,其实大体就是:数据一致性和缓存访问不到的问题
雪崩
问题描述:
大量缓存数据在同一时间失效或Redis
出现问题,导致缓存不能命中,直接访问数据库,承受巨大压力
解决方案:
- 分散缓存过期时间:避免所有缓存同时失效,可以通过为不同的缓存项设置随机的过期时间来实现。
- 缓存预热:在系统启动或预计有高峰流量前,预先加载热点数据到缓存中。
- 降级策略:当缓存失效且数据库负载过高时,可以暂时返回缓存中的旧数据或者默认值,直到缓存更新完成。
- 使用后备存储:如断路器,以保护后端服务免受突发流量的影响。
穿透
问题描述:
如果查询的数据在缓存和数据库中都不存在,每次请求都会直接打到数据库上
解决方案:
- 空值缓存:将查询结果为空的情况也进行缓存,通常设置较短的过期时间。
- 布隆过滤器:用于判断一个元素是否在一个集合中,可以快速过滤掉大部分不存在的查询,减少数据库的无效访问。
击穿
问题描述:
Redis击穿(也称为热点key击穿)指的是某个非常热门的key在其缓存失效的瞬间,大量的并发请求直接打到后端数据库上,造成数据库压力骤增,甚至可能导致数据库宕机。 这种情况通常发生在热点数据的缓存失效时,因为这些数据被频繁访问,一旦缓存失效,所有请求会立即转向数据库。
解决方案:
- 互斥锁(Mutex):在缓存失效时,使用分布式锁(如Redis的SETNX或SET命令的NX选项)来控制只允许一个请求去加载数据并更新缓存,其他请求则等待锁释放后再尝试获取数据。这样可以避免所有请求同时访问数据库。
if (redis.get(key) == null) { // 尝试获取锁 if (redis.setnx(lockKey, lockValue)) { try { // 锁获取成功,执行数据库操作并更新缓存 Object value = dbOperation(); redis.set(key, value); } finally { // 释放锁 redis.del(lockKey); } } else { // 锁获取失败,等待并重试 Thread.sleep(someTime); retry(); } }
- 缓存预热:在系统启动或预测到高峰流量到来之前,提前加载热点数据到缓存中,避免在高峰期因缓存失效而引发击穿。
- 二级缓存:在主缓存失效后,可以先从二级缓存(如内存中的Map或其他缓存系统)中获取数据,同时异步更新主缓存,这样可以减轻数据库的压力。
- 限流:对数据库的访问进行限流,可以使用漏桶算法或令牌桶算法来控制单位时间内到达数据库的请求量,避免瞬时大量请求冲击数据库。
- 超时时间随机化:对于热点数据,可以设置一个较长的缓存超时时间,并在此基础上增加一定的随机延时,避免所有请求在同一时间点失效。
并发
问题描述:
多个客户端同时对同一个键进行读写操作,可能导致数据不一致或丢失。
解决方案:
- 乐观锁/悲观锁:在更新缓存时使用锁机制,如
WATCH
命令或外部锁服务,确保数据的一致性。 - 队列机制:将并发的写操作放入队列,按顺序执行,避免同时写入冲突。
- 原子操作:利用
Redis
的原子命令如INCR
,DECR
,GETSET
等,这些命令可以在不使用锁的情况下保证操作的原子性。
高并发下Redis保持数据一致性
在高并发环境下,为了保持数据的一致性,可以使用以下几种策略:
- 使用
Redis
事务(Transaction
)来确保命令的执行的顺序性和原子性。 - 使用乐观锁或悲观锁来避免并发写入导致的数据不一致。
- 使用
Lua
脚本来封装复杂的数据操作,保证其原子性。 - 使用
Redis
的发布/订阅机制来同步数据状态。
以下是使用Lua脚本来保证数据一致性的例子:
-- Lua脚本保证数值增加的原子性
local key = KEYS[1]
local increment = tonumber(ARGV[1])
if not increment then
return redis.error_reply('ERR invalid increment value')
end
local current_value = redis.call('GET', key)
if not current_value then
current_value = 0
end
current_value = current_value + increment
redis.call('SET', key, current_value)
return current_value
在执行这个Lua
脚本之前,可以通过Redis
客户端提供的EVAL或EVALSHA命令来执行它。
这个脚本会原子性地增加指定键的值,如果键不存在,则初始化为0后再增加。
在高并发环境中,使用这种方式可以保证数据的一致性,避免出现竞争条件或数据丢失。
Redis和Mysql数据一致性
上面的是Redis
操作数据一致性问题,MySQL
和Redis
之间也存在数据一致性问题
在使用Redis
作为MySQL
的缓存层时,保持两者之间的数据一致性是系统设计中的一项重要考虑。
数据一致性确保了缓存中的数据与主数据库中的数据在任何时刻都是匹配的,这对于保证业务逻辑的正确性和用户体验至关重要。
以下是几种常用的方法来维持Redis
和MySQL
之间的数据一致性:
- 双删策略(
Cache Aside Pattern
)- 读取流程:首先尝试从
Redis
中读取数据,如果存在则直接返回;如果不存在,则从MySQL
中读取数据,更新Redis
缓存。 - 写入流程:先更新
MySQL
中的数据,然后删除Redis
中的对应缓存。这样后续的读取请求会触发缓存的重新加载,从而保持数据的一致性。
- 读取流程:首先尝试从
- 读写分离与延时双删
- 读取流程:同上。
- 写入流程:先更新
MySQL
中的数据,然后异步地删除Redis
中的缓存。为了避免在删除缓存过程中出现的数据不一致,可以设置一个短暂的延时,使得数据库的更新先于缓存的删除完成。
- 订阅发布机制(
Pub
/Sub
)- 利用
MySQL
的Binlog
(二进制日志)功能,监听数据变化事件,当MySQL
数据发生变化时,通过发布订阅机制通知Redis
更新缓存。
- 利用
- 异步更新缓存
- 当
MySQL
数据发生变化时,不是立即更新Redis
,而是将更新操作放入队列中,由专门的后台进程异步处理,这样可以减少对Redis
的即时写入压力。
- 当
- 使用中间件或框架
- 中间件或框架(如
Spring Cloud Cache
)提供了缓存一致性解决方案,可以自动处理缓存更新和数据同步。
- 中间件或框架(如
- 乐观锁或版本号
- 在
MySQL
表中增加一个版本字段,每次更新时检查版本号是否匹配,如果Redis
中的版本号与MySQL
中的版本号不一致,则强制更新Redis
缓存。
- 在
- 全量更新与增量更新
- 定期进行全量数据的更新,同时针对实时变化的数据采用增量更新策略,确保数据的最新状态。
- 分布式锁
- 在更新数据时,使用分布式锁来确保同一时间只有一个进程可以更新数据,避免并发更新造成的不一致。
- 数据校验与回滚
- 实现数据校验机制,定期检查
Redis
和MySQL
数据的一致性,一旦发现不一致,触发数据回滚或修复流程。
- 实现数据校验机制,定期检查
- 监控与报警
- 设置监控系统,监控缓存命中率、数据延迟等指标,一旦发现问题立即报警并采取措施。
通过上述策略的合理组合与应用,可以有效地解决Redis
和MySQL
之间数据一致性的问题,确保系统的稳定运行和数据的准确无误。
Redis的key的过期时间,删除策略
Redis
提供了对键(key
)设置过期时间的功能,以及相应的删除策略来管理过期的键。
这有助于释放内存空间,保持数据的有效性,并减少不必要的数据存储。
可以使用以下命令为键设置过期时间:
EXPIRE key seconds
:设置键在给定的秒数后过期。PEXPIRE key milliseconds
:设置键在给定的毫秒数后过期。EXPIREAT key timestamp
:设置键在Unix
时间戳指定的时间过期。PEXPIREAT key milliseconds-timestamp
:设置键在Unix
时间戳(以毫秒为单位)指定的时间过期。
删除策略,主要有两种:
- 惰性删除(
Lazy Eviction
)- 当客户端尝试访问一个已经过期的键时,
Redis
会在这个操作过程中检查键是否已过期,如果过期则删除键并返回nil
给客户端。这是一种被动删除策略,减少了对CPU
资源的消耗,但不能保证及时删除所有过期键。
- 当客户端尝试访问一个已经过期的键时,
- 定期删除(
Periodic Eviction
)Redis
会在后台周期性地执行一个任务(默认每秒
运行10次
),检查并删除一部分已过期的键。这个过程并不是检查所有键,而是采样一小部分进行检查,因此不会对性能造成太大影响。定期删除可以看作是对惰性删除的补充,帮助及时清理冷数据。
此外,当Redis
的内存使用达到配置的最大值(maxmemory
)时,会触发额外的删除策略,使用LFU
(Least Frequently Used
)或LRU
(Least Recently Used`)等算法挑选并删除部分键
volatile-lru
:从设置了过期时间的键中,选择最近最少使用的键进行删除。volatile-random
:随机从设置了过期时间的键中选择键进行删除。volatile-ttl
:从设置了过期时间的键中,选择剩余生存时间最短的键进行删除。allkeys-lru
:从所有键中,选择最近最少使用的键进行删除。allkeys-random
:随机从所有键中选择键进行删除。
Redis
实际采用的是惰性删除与定期删除的组合策略,结合内存压力下的主动清理机制,既保持了高性能,又有效管理了内存。
这种策略在大部分应用场景下能较好地平衡了系统性能与内存利用率
Redis
的并发竞争问题和CAS
Redis
是一个单线程模型的内存数据库,这意味着在任何给定时间,只有一个客户端的命令会被处理。
尽管如此,在高并发环境中,Redis
仍可能遇到并发竞争问题,尤其是在涉及多个客户端对同一键进行读写操作时。
这种竞争可能导致数据不一致,具体表现如下:
- 并发写竞争:多个客户端尝试同时更新同一个键的值,可能导致数据版本混乱或丢失部分更新。
- 并发读竞争:多个客户端同时读取一个键,然后各自更新,最后写回,这可能导致
脏读
或丢失更新
的问题。
解决方案:CAS
(Compare and Swap)
CAS
是一种常用的解决并发竞争问题的技术,它允许在不锁定数据的情况下进行原子更新。
在Redis
中,CAS
的概念可以通过几种方式实现:
- 使用
WATCH
和MULTI
/EXEC
命令:WATCH
命令监视一个或多个键,如果在EXEC
命令执行前这些键被其他客户端改变,那么整个事务将被取消。- 这种机制可以确保在读取数据和实际更新数据之间的数据一致性。
- 使用原子操作:
Redis
提供了一系列原子操作命令,如INCR
,DECR
,GETSET
等,这些命令可以在不使用锁的情况下保证操作的原子性,从而避免并发问题。
- 使用
Lua
脚本:Lua
脚本可以在Redis
服务器端执行,允许你编写一系列操作,这些操作将作为一个整体执行,从而避免了并发问题。
- 使用版本号或时间戳:
- 为每个键添加一个版本号或时间戳,每次更新键时检查版本号是否匹配,如果不匹配,则拒绝更新,这类似于
CAS
的思想。
- 为每个键添加一个版本号或时间戳,每次更新键时检查版本号是否匹配,如果不匹配,则拒绝更新,这类似于
示例:使用WATCH
/MULTI
/EXEC
实现CAS
假设你有一个计数器,需要在高并发环境下安全地对其进行递增:
WATCH counter_key
current_value = GET counter_key
MULTI
INCRBY counter_key 1
EXEC
在这个例子中,WATCH
命令监视counter_key
,如果在执行MULTI
和EXEC
之间的任何时间,counter_key
被另一个客户端修改,那么EXEC
将会返回nil
,表示事务失败,此时客户端需要重新开始整个过程。
通过使用CAS
或类似机制,Redis
能够在高并发环境中保持数据的一致性和完整性,避免并发竞争带来的问题。
Redis
集群的最大槽数是16384
个
Redis Cluster 采用数据分片机制,定义了 16384个 Slot槽位,集群中的每个Redis 实例负责维护一部分 槽以及槽所映射的键值数据。 Redis每个节点之间会定期发送ping/pong消息(心跳包包含了其他节点的数据),用于交换数据信息。
Redis集群的节点会按照以下规则发ping消息:
- 每秒会随机选取5个节点,找出最久没有通信的节点发送ping消息
- 每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于clusternode-timeout/2 则立刻发送ping消息
心跳包的消息头里面有个myslots的char数组,是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的。
- 如果采用 16384 个插槽,那么心跳包的消息头占用空间 2KB (16384/8);如果采用 65536 个插槽,那么心跳包的消息头占用空间 8KB (65536/8)。 可见采用 65536 个插槽,发送心跳信息的消息头达8k,比较浪费带宽。
- 一般情况下一个Redis集群不会有超过1000个master节点,太多可能导致网络拥堵。
- 哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩。bitmap的填充率越低,压缩率越高。 其中bitmap 填充率 = slots / N (N表示节点数)。所以,插槽数越低, 填充率会降低,压缩率会提高。
Redis
实现消息队列
使用list
类型保存数据信息,rpush
生产消息,lpop
消费消息,当lpop
没有消息时,可以sleep
一段时间,然后再检查有没有信息,如果不想sleep
的话,可以使用blpop
,在没有信息的时候,会一直阻塞,直到信息的到来。
BLPOP queue 0 //0表示不限制等待时间
BLPOP
和LPOP
命令相似,唯一的区别就是当列表没有元素时BLPOP
命令会一直阻塞连接,直到有新元素加入。
redis
可以通过pub
/sub
主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
PUBLISH channel1 hi
SUBSCRIBE channel1
UNSUBSCRIBE channel1 //退订通过SUBSCRIBE命令订阅的频道。
PSUBSCRIBE channel?*
,按照规则订阅。PUNSUBSCRIBE channel?*
,退订通过PSUBSCRIBE
命令按照某种规则订阅的频道。
其中订阅规则要进行严格的字符串匹配,PUNSUBSCRIBE *
无法退订channel?*
规则。
pipeline
pipeline
是Redis
的一个高级特性,允许客户端将多个命令打包发送到服务器,然后服务器一次处理多个命令,从而减少网络传输和CPU
消耗。
redis
客户端执行一条命令分4个过程:发送命令
、命令排队
、命令执行
、返回结果
。
使用pipeline
可以批量请求,批量返回结果,执行速度比逐条执行要快。
BLPOP queue 0 //0表示不限制等待时间
PUBLISH channel1 hi
SUBSCRIBE channel1
UNSUBSCRIBE channel1 //退订通过SUBSCRIBE命令订阅的频道。
使用pipeline
组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline
命令完成。
原生批命令(mset和mget)与 pipeline 对比:
- 原生批命令是原子性, pipeline 是非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚。
- 原生批命令只有一个命令,但 pipeline 支持多命令。