Redis深度历险笔记
基础与应用
Redis基础数据结构
- 5种基础数据结构:
string
、list
、hash
(字典)、set
(集合)、zset
(有序集合) - 所有数据以唯一的key字符串作为名称,通过这个唯一的key获得value
- 键值对操作
-
set key value
:添加key:value
键值对 -
get key
:得到key
对应的value
-
exists key
:检查key
是否存在 -
del key
:删除key
以及对应的value
-
mset key1 value1 key2 value2 ...
:添加多个键值对 -
mget key1 key2 ...
:获取多个键值对 -
expire key time
:设置key
有效时间,过期自动删除 -
setex key time value
:set+expire
,添加具有有效时间限制的键值对 -
setnx key value
:如果key
不存在,则创建键值对 -
incr value
:如果value
是整数,自增操作,增加1,最大不超过signed long
-
incrby value step
:以step
大小增减value
,在signed long
数值范围,否则报错
-
-
list
(列表)- 相当于
Java
语言的LinkedList
,双向链表,实际上其是一个“快速链表”(quicklist
)的一个结构- 快速链表内部由一个个压缩列表(
ziplist
)串起来,压缩列表使用连续内存存储列表元素,这样就相当于结合了链表和数组 - 优点:
- 元素查找相比传统列表更快
- 插入删除性能也没有收到太大的影响
- 减轻内存碎片化
- 快速链表内部由一个个压缩列表(
- 通常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串,塞进
Redis
的列表,另一个线程从这个列表中轮询数据进行处理。 -
rpush key element1 element2 ...
:在名为key
的列表右端加入元素 lpush key element1 element2 ...
-
lpop key
:从链表左端弹出元素,相当于先入先出FIFO
-
rpop key
:从链表右端弹出元素,相当于先入后出,栈 -
lindex key index
:从名为key
的链表遍历到下标为index
的元素 -
lrange key start end
:提取key
链表中[start,end]
范围内的元素 -
ltrim key start end
:去除key
链表中[start,end]
范围之外的两端元素,可以实现一个定长链表
- 相当于
-
hash
(字典)- 相当于
Java
中的HashMap
,无序字典,内部存储很多键值对 - 结构上与
Java
一致,数组+链表,当产生Hash
碰撞时,即会将碰撞的元素使用链表串接起来 - 不同的是,
Redis
的字典值只能存取字符串,Java
的rehash
是个耗时操作,需要一次性全部rehash
(发生在扩容的时候),Redis
为了高性能,不能阻塞服务,采用渐进式rehash
策略,在rehash
时保留新旧两个hash
结构,查询时,同时查询两个结构,当rehash
结束,采用用新的hash
结构替代老的结构 -
hash
结构可以用来存储用户信息,可以对用户结构中的每个字段单独存取 - 缺点:存储消耗高于存储单个字符串
-
hset hash_name key "value"
:不加引号的话,如果alue
中含有空格,会自动分隔成多个value
-
hgetall hash_name
:获得hash_name
字典名中的所有内容 -
hlen hash_name
:获得hash_name
字典中key
的个数 -
hget hash_name key
:获得字典中指定key
下的元素 -
hmset hash_name key1 "value1" key2 "value2" ...
:增加多个key
及对应元素 -
hincrby hash_name key step
:如果hash_name
中key
对应的value
为数字,可以执行增减step
操作
- 相当于
-
Set
(集合)-
Redis
中的Set
相当于Java
中的HashSet
,内部的键值对是无序的、唯一的,内部的实现相当于一个字典,字典中的所有value
都是NULL
-
sadd key value1 value2 ...
:为指定key
添加1个或多个值 -
smembers key
:列出key
中的值 -
sismember key value
:检测value
是否在key
对应的集合中 -
scard key
:统计key
集合中的value
个数 -
spop key
:随机弹出一个集合中的value
-
-
zset
(有序列表)- 类似于
Java
中的SortedSet
和HashMap
的结合体,内部实现用的是一种“跳跃列表”的数据结构 - 为集合中的每个
value
赋予一个score
,代表这个value
的排序权重 -
zadd key score value
:在key
集合中添加权重为score
的value
-
zrange key start end
:根据score
排序的结果(从小到大) -
zrevrange key start end
:根据score
排序的结果(逆序,从大到小) -
zcard key
:统计key
集合中的value
个数 -
zscore key value
:得到key
集合中value
的score
-
zrank key value
:得到key
集合中value
的rank
-
zrangebyscore key low_score high_score
:key
集合中权重在[low_score,high_score]
中的value
,可以使用inf
-
zrem key value
:删除key
集合中的value
- 类似于
- 通用规则
- 容器型数据结构,1、不存在则创建;2、没有元素就自动删除
- 过期时间
- 过期时间以对象为单位,一个
hash
结构的过期是整个hash
对象的过期 - 设置完过期时间的对象,再次设置时可能改变过期设置
- 过期时间以对象为单位,一个
分布式锁
- 多用户同时操作时可能出现数据冲突问题,因为读取和保存状态不是原子操作,不加控制的执行可能导致数据出错
-
setnx(set if not exists)
:设置锁,del
删除锁 -
死锁:如果设置完锁之后,在
del
之前出现异常,则会陷入死锁,锁永远得不到释放 - 死锁处理办法:在拿到锁之后,为锁设定一个过期时间,保证出现异常之后,经过指定时间,锁也能得到释放
- 如果设置过期时间时,服务器宕机,导致
expire
得不到执行,也会造成死锁 -
setnx
和expire
一起执行:set lockname value ex time nx
- 如果设置过期时间时,服务器宕机,导致
-
超时问题:如果加锁和释放锁之间逻辑执行耗时超过为锁设定的过期时间,则为执行完的逻辑不能得到严格串行执行
- 稍微安全的做法:
set
指定的value
设定为一个随机数,释放锁时先匹配随机数是否一致,然后再删除key
,可以确保当前线程占有的锁不会被其他线程释放,除非这个锁是因为过期而被服务器释放的 - 但是匹配
value
和删除key
不是一个原子操作,Lua脚本
可以实现多个指令的原子性执行 - 没有解决问题,如果真的超时了,当前逻辑没有执行完,其他线程也会乘虚而入
- 稍微安全的做法:
- 可重入性:线程在持有锁的情况下,可以再次加锁
延时队列
- 异步消息队列
- 通过
list
实现,队列消息空了,需要让线程sleep
,不然客户端会不断尝试pop
数据,造成空轮询,消耗资源
- 通过
- 睡眠会导致消息延迟增大,缩短睡眠时间会降低延迟,但这样也就越来越接近空轮询
-
阻塞读:
blpop
和brpop
,b
代表blocking
,阻塞读在队列没有数据的时候会立即进入睡眠状态,一旦数据到来,则立刻醒过来。 - 空闲连接自动断开:客户端闲置过久,服务器一般会主动断开连接,此时
blpop/brpop
抛出异常,需要对此进行处理 - 锁冲突处理:加锁未成功怎么处理?
- 直接抛出异常,通知用户稍后重试
-
sleep
一会儿,然后再重试 - 将强求转移至延时队列,过一会再试
- 延时队列实现
- 通过
zset
实现,将消息作为zset
的value
,到期处理时间未score
,多线程轮询zset
获取到期的任务进行处理(多线程保障可用性,某线程挂了,还有其他线程可以处理,多线程需要考虑并发争抢任务,使用zrem
,根据删除的返回值确定是否拿到消息) - 使用
lua
进行原子化操作,避免争抢任务的浪费
- 通过
位图
- 不是特殊的数据结构,其内容是普通的字符串,
byte数组
,超过范围自动扩展 -
setbit key index 1/0
:设置key
对应value
在index
位上的值 -
getbit key index
:获得某个具体位置的值0/1
-
bitcount key
:统计key
的value
的二进制中1的个数 -
bitcount key start end
:[start,end]
范围的字符的二进制中1的个数 -
bitpos key 0
:第一个0位 -
bitpos key 1
:第一个1位 -
bitpos key 0/1 start end
:从[start,end]
范围的字符start
开始,第一个0/1
位 -
bitfield key get u4 index
:从key
的value
的第index+1
位开始,取4个位,结果为无符号数 -
bitfield key get i4 index
:从key
的value
的第index+1
位开始,取4个位,结果为有符号数 -
bitfield key set u8 8 97
:将key
的value
的第9位开始,将接下来的8位用无符号数97代替 -
bitfield key incrby u4 index 1
:从第index+1位开始,对接下来的4位无符号数加1-
incrby
可能溢出,三种处理方式(wrap
:折返,最大变最小;fail
:失败;sat
:饱和截断)
-
HyperLogLog
- 提供不精确的去重计数方案,标准误差是0.81%
-
pfadd key value
:与sadd
用法一致,添加value
-
pfcount key
:与scard
用法一致,获取计数值 -
pfmerge
:合并多个pf
计数值 - 缺点:需要额外占用
12kb
空间(与内部数学原理分桶有关)
布隆过滤器
- 解决去重问题,节约空间,有一定的误判概率
- 可以准确的确定元素是否不在集合中,但是部分不在集合中的元素会被认为已经加入集合(误判)
bf.add
bf.exists
-
bf.reserve
显示创建布隆过滤器,可以设置key
、error_rate
、initial_size
- 原理:大型的位数组+几个不一样的无偏的
hash函数
简单限流
- 限制一段时间用户的访问量,用时间窗口,通过
zset
实现,以时间为score
,每次更新删除有效score
的范围之外的访问,并计算当前的访问量,如果当前的访问量已经达到最大允许,则不允许新的访问
漏斗限流
- 根据漏斗模型,如果漏斗嘴流水速率小于灌水的速率,就要等待漏斗具有足够的空间再进行灌水
- 将漏斗模型中需要的字段(比如流水速率,剩余空间,上一次放水时间等)放入
Hash结构
中,单数hash结构
取值和回填无法保证原子性,需要适当的加锁控制,但是加锁也有可能失败,失败的话又要选择重试或者放弃。 -
Redis-Cell
:限流Redis模块
,使用漏斗算法,提供原子的限流指令 cl.throttle key capacity rate [need-elem]
- 例如
cl.throttle laoqian:reply 15 30 60 1
:用户老钱回复行为的频率为每60s最多30词,速率分成了2个参数,漏斗初始的容量为15,至少有1条回复
GeoHash
- 地理位置
Geo
模块 - 应对问题:高并发场景下,在数据库中查询矩形区域算法(根据坐标查找附近矩形区域的目标)性能有限
- 地理位置距离排序算法
GeoHash
,其将二维数据映射到一维,一维上相近的点,空间距离也近- 将二维平面切分(可以分成四块,确定目标数据所在的方块,进行编码,例如
00(左上)
、01(右上)
、10(左下)
、11(右下)
),不断往下切分,切分的块将越来越接近目标,拼接得到的编码可以用来计算距离
- 将二维平面切分(可以分成四块,确定目标数据所在的方块,进行编码,例如
geohash内部采用普通的zset结构实现
geoadd set-name 经度 纬度 地址名
geodist set-name 地址1 地址2 距离单位
-
geopos set-name 地址
:返回的结果与输入有些许误差,因为输入时二维转一维损失了部分精度 -
geohash setname 地址
:获取地址对应的经纬度转换后,在集合中保存的编码字符串 -
georadiusbymember set-name 地址 距离 距离单位 返回结果的数量限制 结果排序方式
:查询指定元素附近的其他元素withcoord
-
withdist
:显示距离 withhash
-
georadius
:根据坐标查询附近的元素
scan
- 如何从海量的
key
中找出满足特定前缀的key
列表 -
keys 正则表达式字符串
:根据正则表达式的要求,过滤出满足条件的key
- 没有
offset
、limit
参数,满足条件的key
过多,将会全部输出 - 是一种遍历算法,复杂度
O(n)
,因为Redis
是单线程程序,顺序执行所有指令,如果key
非常多,则非常耗时,导致服务卡顿
- 没有
-
scan
相比keys
优点- 复杂度依然
O(n)
,但是其通过游标分步进行,不会阻塞线程 - 提供
limit
参数,可以控制每次返回结果的最大条数,limit
只是一个hint
,返回的结果可多可少 - 和
keys
一样,提供模式匹配功能 - 服务器不需要为游标保存状态,游标的唯一状态就是
scan
返回给客户端的游标整数 - 遍历的过程中,如果有数据修改,改动后的数据能不能遍历到是不确定的
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零
- 复杂度依然
scan 游标 match 正则字符串 count 数量
-
scan
的遍历顺序不是从第一维的散列数组的第0位开始,而是采用高位进位加法的方式,根据位掩膜遍历所有的槽 - 高位进位加法的遍历顺序保证扩容缩容的重复遍历(缩容时可能在某个槽位上的元素重复遍历)
- 由于
渐进式rehash
,scan
需要同时扫描新旧槽位,将结果融合后返回 - 针对指定容器的遍历:
zscan
、hscan
、sscan
-
大key
扫描- 如果
Redis
实例中形成了很大的对象,这个大key
将会导致数据迁移卡顿,扩容、删除等操作也会造成卡顿 - 尽量避免大key的产生
-
redis-cli
中提供--bigkeys
扫描
- 如果
原理
线程I/O模型
- Redis是单线程程序
- 所有的数据都在内存中,所有的运算都是内存级别的运算
- 高并发实现:多路复用(
IO多路转接
)- 使用
非阻塞IO
- 事件轮询(
select
、poll
、epoll
、kqueue
,Java
中的NIO
) - 指令队列:客户端的指令先到先服务,顺序处理
- 响应队列:将指令的返回结果回复给客户端
- 定时任务:定时任务记录在一个“最小堆”结构中,将最快要执行的任务排在堆的最上方,将最快要执行的任务还需要的时间记录下来,这个时间就是
select
系统调用中的timeout
参数(也就是执行系统调用时,将这部分耗时与定时任务结合起来)
- 使用
通信协议
-
Redis
作者认为数据库系统的瓶颈一般不在于网络流量,而在于数据库自身内部的逻辑处理上 -
RESP
(Redis Serialization Protocol
):Redis
序列化协议- 实现简单,解析性好
- 单元结束统一加上
\r\n
- 单行
“+”
开头、多行“$”
开头,后跟字符串长度 - 整数值以
":"
开头,后跟整数的字符串形式 - 错误消息以
“-”
开头 - 数组以
“*”
开头,后跟数组长度:\*3\r\n:1\r\n:2\r\n:3\r\n
-
NULL
:$-1\r\n
- 空行:
$0\r\n\r\n
(两个\r\n
之间隔的是空行)
- 客户端->服务器:发送多行字符串数组
- 例如:
*3\r\n$3\r\nset\r\n$6\r\nauthor\r\n$8\r\ncodehole\r\n
set author codehole
- 例如:
- 服务器->客户端:多种响应的组合
持久化
- 防止突然宕机,内存中的数据丢失
- 两种持久化方式:快照、
AOF日志
- 快照
- 一次数据全量备份,二进制序列化形式,存储上非常紧凑
- 使用多进程的写时拷贝
COW
(copy on write
)机制实现持久化,所以父进程的数据改变,持久化的内容也会相应改变 - 子进程做数据持久化,不会修改现有内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中
- 当父进程对数据段的一个页面进行修改时,会将被共享的页面复制一份出来
-
AOF日志
- 连续的增量备份,记录的是内存数据修改的指令记录文本,长期运行
AOF
会变得无比巨大,需要定期重写,瘦身 - 通过对
AOF
指令重放,恢复内存数据结构的状态
- 连续的增量备份,记录的是内存数据修改的指令记录文本,长期运行
-
AOF
重写-
Redis
提供了bgrewriteaof
指令用于对AOF日志文件
进行瘦身 - 原理就是开辟一个子进程对内存进行遍历,转换成一系列
Redis
的操作指令,序列化到一个新的AOF日志文件
中,序列化完毕之后,再将新的增量AOF日志
追加到新的AOF文件
中
-
-
fsync
- 写操作的内容是写到内存缓冲中的,将数据刷回磁盘的过程中,服务器宕机会造成日志丢失
-
fsync
指定文件fd
的内容强制刷到磁盘,也就是冲洗,IO
操作是一个耗时操作,所以Redis
通常默认是每个1s执行一次fsync
- 运维
- 通常有
Redis
的从节点执行持久化操作,这样不增加主节点的负担 - 如果从节点长期连不上主节点,就会出现数据不一致的情况
- 需要实时监控
- 多增加从节点,降低网络分区(造成无法访问主节点)的概率
- 通常有
-
Redis
混合持久化- 很少用
rdb
(快照)来恢复内存状态,因为会丢失大量数据 - 通常使用
AOF
日志重放 - 混合:
rdb
文件的内容和增量的AOF
日志文件组合,AOF
不再是全量的日志,而是持久化开始到持久化结束这段时间发生的增量AOF日志
- 很少用
管道
- 技术本质上由
Redis
客户端提供,跟服务器没有什么直接的关系 - 管道将多个操作合并,消耗一次网络,节省
IO
时间
事务
- 确保连续多个操作的原子性
-
begin
、commit
、rollback
分别对应Redis
的multi
、exec
、discard
- 所有的指令在
exec
之前不执行,而是缓存在服务器的一个事务队列中 -
Redis
的事务不具备原子性,仅仅满足事务的“隔离性”中的串行化 -
watch
:乐观锁- 分布式锁:悲观锁
-
Redis
会检查关键变量自watch
之后是否被修改了,如果被修改,则返回NULL
,告知事务执行失败 -
watch
不能在multi
和exec
之间
PubSub
-
Redis
消息队列的不足:不支持消息的多播机制 -
PubSub
(publisher subscriber
发布者/订阅者模式) - 消费者使用
listen
阻塞监听消息 - 模式订阅
Pattern Subscribe
:订阅多个主题 - 缺点:
- 生产者传递来一个消息,
Redis
会立刻将消息向消费者发送,如果没有消费者,那么这个消息直接被丢弃 -
Redis
停机重启,PubSub
消息无法恢复
- 生产者传递来一个消息,
小对象压缩
- 如果使用
32bit
编译,内部所有的数据结构所使用的指针空间占用会少一半 - 小对象压缩存储(
ziplist
)-
ziplist
是一个紧凑的字节数组结构
-
- 内存回收机制
- 操作系统以页作为单位回收数据,但是
Redis
的key
可能是保存在多个页上的,而由于页上还有其他key
,所以系统不会立刻回收资源 -
flushdb
删除所有数据,系统基本会立刻回收(emmm,这操作。。。) - 虽然系统没有回收数据,但是
Redis
可以使用它删除了的内容占有的位置
- 操作系统以页作为单位回收数据,但是
- 内存分配算法
- 使用第三方库
jemalloc
(facebook
)来管理内存,也可以切换到tcmalloc
(google
)
- 使用第三方库
集群
主从同步
- 从节点作为主节点宕机的备用
-
CAP
原理- C:
Consistent
一致性 - A:
Avaliabilty
可用性 - P:
Partition tolerance
分区容忍性 - 当网络分区发生时(也就是不同节点的网络通信被阻断),一致性和可用性两难全
- C:
- 最终一致
-
Redis
主从数据是异步同步的,所以分布式Redis不满足一致性要求 - 即使主从节点断开,
Redis
依然对外提供修改服务,所以Redis
满足可用性 -
Redis
保证最终一致性,从节点努力追赶主节点
-
- 主从同步和从从同步
- 增量同步
- 将指令流发送给从节点,从节点一边接收指令一边执行,将同步的进度(偏移量)进行反馈
- 为了节约指令流缓存占用的内存,内部使用环形数组保存待发送的指令流,如果数组满了,从数组的头部开始覆盖,如果主从网络出现问题或者从节点执行太慢,从节点未执行的指令被覆盖,则无法再通过指令流同步
- 快照同步
- 主节点使用快照备份数据,将数据发送给从节点,从节点接收并加载数据,之后进行增量同步
- 由于快照同步过程中,主节点的复制
buffer
还在不断向前移动,如果快照同步时间过长或者复制buffer
太小,则增量指令可能会被覆盖,导致从节点开启增量同步失败,又重新快照同步,造成快照同步死循环
- 增加从节点
- 无盘复制
- 直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点一边遍历内存,一边将序列化的内容发送到从节点
-
wait
指令- 让异步复制变身同步复制,确保系统的强一致性(不严格)
-
wait
节点数 等待时间:如果等待时间为0,则一直等待
Sentinel(哨兵)
-
Redis Sentinel
集群可以看作一个Zookeeper
集群,是一个集群高可用心脏 - 负责监控从节点的健康,当主节点宕机,自动选择一个最优的从节点切换成主节点
- 客户端连接集群时,首先连接
Sentinel
,通过Sentinel
查询主节点的地址 - 消息丢失
- 主节点宕机时,从节点可能没有收到全部同步消息,未同步的消息就丢失了
-
Sentinel
用法-
discover_xxx
发现主从地址(discover_master
) -
xxx_for
从连接池拿出一个连接使用
-
Codis
- 单个
Redis
的内存不宜过大,内存太大导致rdb
文件过大 -
Redis
集群方案将众多小内存的Redis
实例整合起来,完成海量数据的存储和高并发读写操作 -
Codis
负责将客户发来的指令转发到后面的Redis
实例来运行 -
Codis
是无状态的,是一个转发代理中间件,可以启动多个Codis
供客户端使用,每个Codis
节点是对等的 -
Codis
分片原理- 将所有的
key
划分为1024个槽位(可以设置) - 多客户端传来的
key
进行crc32
运算计算hash值
- 将
hash值
对1024进行取模运算,得到的余数就是对应的槽位 - 每个槽位都会唯一的映射到后面的
Redis实例
,Codis
在内存中维护槽位和Redis实例
的映射关系
- 将所有的
- 不同的
Codis实例
之间的槽位关系如何同步- 如果槽位映射关系只存储在内存中,则不同的
Codis实例
之间的槽位关系就无法得到同步,需要持久化槽位关系 -
Codis
将槽位关系存储在Zookeeper
中,提供一个Dashboard
观察和修改槽位关系,Codis Proxy
监听变化,并重新同步槽位关系
- 如果槽位映射关系只存储在内存中,则不同的
- 扩容
- 一开始1024个槽全部指向同一个
Redis
,如果Redis
内存不足,则增加Redis
实例,这时候需要将一半槽位划分到新节点,需要对这一半的槽位对应的所有key
进行迁移 -
Codis
对Redis
进行了改造,增加了SLOTSSCAN
指令,可以遍历指定slot
下的所有key
- 迁移过程中,如果接收到新的请求(正在迁移的服务器),会立即强制对当前请求的
key
进行迁移,迁移完成后,将请求转发到新的Redis实例
- 一开始1024个槽全部指向同一个
- 自我均衡
- 自动均衡会在系统比较空闲的时候观察每个
Redis
实例对应的slot
数量,如果不平衡,自动进行迁移
- 自动均衡会在系统比较空闲的时候观察每个
-
Codis
代价- 因为所有
key
分散在不同的Redis
实例中,所以不再支持事务,事务只能在单个实例中完成 - 单个
key
的value
不宜过大 - 增加
Proxy
作为中转层,网络开销要比单个Redis
大 - 集群配置中心使用
zookeeper
实现,增加了zookeeper
运维代价
- 因为所有
- 优点
- 比官方集群简单
-
mget
:批量获取多个key
Cluster
- 将所有数据划分为16384个槽位,每个节点负责其中一部分槽位
- 槽位的信息存储与每个节点中,不像
Codis
,不需要另外的分布式存储空间来存储节点槽位信息 - 当
Redis Cluster
的客户端来连接集群时,也会得到一份集群的槽位配置信息。当客户要查找某个key
时,可以直接定位到目标节点 - 槽位定位算法
- 默认对
key
使用crc16
算法进行hash
,对16384取模得到具体槽位 - 允许强制将某个
key
放到特定槽位上
- 默认对
- 跳转
- 当客户端像一个错误的节点发出了指令后,该节点向客户端发送一个特殊的跳转指令,告诉客户端去指定节点获取数据
MOVED 槽位编号 目标节点地址
- 迁移
-
redis-trib
可以让运维人员手动调整槽位的分配情况 - 提供自动化平衡槽位工具
-
Redis
迁移的单位是槽,当一个槽正在迁移时,这个槽处于中间过渡状态- 源节点状态为
migrating
- 目标节点状态为
importing
-
redis-trib
首先设置两节点的状态,然后一次性获取源节点槽位的所有key列表
,在挨个key
进行迁移,迁移结束,源节点删除对应key内容
- 迁移过程是同步的,源节点处于阻塞状态,知道
key
删除
- 源节点状态为
-
- 容错
- 为主节点设置若干从节点
- 网络抖动
- 可能下线与确定下线
- 去中心化,一个节点认为某个节点失联了并不代表所有节点都认为它失联了,集群需要一次协商的过程
- 槽位迁移感知
MOVED
ASKING
- 集群变更感知
拓展篇
Stream
- 支持多播的可持久化消息队列
- 简述
-
Stream消息队列
实现了可持久化,消息内容比较安全。其上的消息通过链表的结构串接起来,使用消息消费组去消费消息,消费组内部的消费组之间存在竞争关系,消费组只会消费消息一次,具体内部由哪个消费者消费,只会让最快响应的消费者处理消息。另外,消费组从哪一个消息位置消费,需要利用last_delivered_id
进行标记。还有,抢到消息的消费者有可能突然宕机,没能处理消息或者没能返回ack
(确认信息),那么我们不能确保这个消息已经被正常消费,需要将这个未能确认的状态记录下来,这里使用PEL,确保客户端至少消费消息一次
-
- 每个
Stream
具有唯一的名称,就是Redis
中的key
,首次使用xadd
指令时自动创建 - 每个
Stream
都可以挂多个消费组,每个消费组会有个游标last_delivered_id
,表示当前消费组已经消费到哪条消息了 - 每个消费组都有一个
Stream
内的唯一的名称,消费组不会自动创建,需要单独的指令xgroup create
创建,指定从Stream
中的某个消息ID
开始消费 - 消费组之间的状态独立,同一份
Stream
内部的消息会被每个消费组都消费到 - 同一个消费组可以挂接多个消费者,这些消费者之间是竞争关系,任何一个消费者读取了消息都会使游标往前移动
- 消费者内部有一个状态变量
pending_ids
,记录当前已经被客户端读取,但是还没有ack
的消息,如果没有ack
,这些状态加入PEL
(pending entries list
),确保客户端至少消费了消息一次 -
消息ID
:可以服务器自动生成,也可以由客户端指定,必须是“整数-整数”,后面加入的消息比前面的大 - 消息内容:键值对
- 增删改查
xadd
xdel
xrange
xlen
del
- 独立消费
xread
- 创建消费组
xgroup create
- 消费
-
Stream
消息过多:可以设置定长Stream
功能 - 消息如果忘记
ack
:PEL列表
将会不断增长 -
PEL
如何避免消息丢失:PEL
记录了已经发送的ID
,根据ID
重发消息 -
Stream
高可用:在Sentinel
和Cluster
集群下,Stream
支持高可用。但由于Redis
的指令复制是异步的,在failover
发生时,可能丢失极小部分数据 - 分区
partion
- 不支持原生分区能力,通过分配多个
Stream
模拟
- 不支持原生分区能力,通过分配多个
info指令
- 显示
Redis
状态 Server
-
Clients
:客户端相关信息 -
Memory
:服务器运行内存统计数据 -
Persistence
:持久化信息 -
Stats
:通用统计数据 -
Replication
:主从复制相关信息 CPU
-
Cluster
:集群信息 -
KeySpace
:键值对统计数量信息
再谈分布式锁
- 集群中的分布式锁,如果主机点申请成功了一把锁,但这把锁还没有同步到从节点,此时主节点宕机,从节点变成主节点,丢失了锁
- 解决:使用
Redlock
算法,加锁时,将超过一半的节点加锁成功,此时才算加锁成功,释放锁时,向所有节点发送del
指令(大多数机制,投票机制)
过期策略
- 过期的
key
集合- 将过期的
key
放入一个独立的字典中,定时遍历这个字典来删除到期的key
,还利用惰性删除(只要下一次访问已经删除的元素,就立即删除)
- 将过期的
- 定时扫描
- 从节点的过期策略
- 主节点在
key
到期时,会在AOF
文件里增加一条del
指令,同步到所有从节点,从节点通过执行del
指令来删除过期的key
- 主节点在
LRU
- 当
Redis
内存超过物理内存限制,内存数据会开始与磁盘产生频繁的交换,降低Redis
性能 -
maxmemory
限制最大内存 - 当内存超过
maxmemory
-
noeviction
:不允许继续服务写请求,读请求继续进行 -
volatile-lru
:尝试淘汰设置了过期时间的key
,最少使用的key
优先被淘汰 -
volatile-ttl
:与上面一样,但是淘汰策略不是lru
,而是比较key
的剩余寿命ttl
的值,越小越先淘汰(优先淘汰最快要过期的key
) -
volatile-random
:随机淘汰设置了过期时间的key
-
allkeys-lru
:最少使用的key优先被淘汰(针对全体key
,没有设置过期时间的key
也会被淘汰) -
allkeys-random
:随机淘汰key
-
-
Redis
使用近似LRU算法
- 采用随机采样法来淘汰元素,在现有字段上增加了最后一次被访问的时间戳字段
- 使用懒惰处理,当内存超过
maxmemory
,执行LRU
,直到内存满足要求 - 例如随机采样5个
key
(可以设置),淘汰掉最旧的key
(不同设置,采样集合不同)
懒惰删除
-
Redis
为什么采用懒惰删除?- 如果单个
key
是个非常大的对象,删除操作会导致单线程卡顿 - 使用
unlink key
,对删除操作懒处理,丢给后台线程来异步回收内存
- 如果单个
-
flush
-
Redis
提供的flushdb
和flushall
指令,用来清空数据库 -
flushall async
:异步处理清除指令,由后台线程慢慢处理
-
- 异步队列
- 主线程将对象的引用从“大树“中摘除后,将这个
key
的内存回收操作包装成一个任务,塞进异步任务队列,后台从这个队列中取任务 - 必须是一个线程安全的队列
- 主线程将对象的引用从“大树“中摘除后,将这个
-
AOF Sync
也很慢-
AOF Sync
操作的线程是一个独立的异步线程,和懒惰删除线程不是一个线程
-
- 更多异步删除点
- 很多删除点也可以使用异步删除机制
Jedis
-
Jedis
连接池-JedisPool
-
Jedis
不是线程安全的
保护Redis
- 指令安全
-
Redis
在配置文件中提供了rename-command
指令将某些危险的指令修改成特别的名称,用来避免人为误操作。 - 将指令
rename
成”“
,将导致这条指令无法在Redis
中执行了
-
- 端口安全
-
Redis
默认监听6379
,如果当前服务器有外网ip
,Redis
服务将会直接暴露在公网上 - 可以在配置文件中指定监听的
IP地址
- 可以增加
Redis
的密码访问机制
-
-
Lua
脚本安全 -
SSL
代理spiped
Redis安全通信
-
Redis
本身并不支持SSL安全连接,需要借助SSL
代理软件,让通信数据得到加密 spiped