文章目录
- 简介
- 数据结构
- Redis对象通用对象
- 字符串string
- 常用操作
- 数据结构
- 存储方式
- 为什么是44字节
- 扩容
- 列表list
- 常用操作
- 快速列表quicklist
- 数据结构
- 存储方式
- 特点
- 整数集合intset
- 字典
- 常用操作
- 使用场景
- 数据结构
- 扩容
- 关于扩容的问题
- 高位进位法扩容
- 字典遍历
- hash攻击
- 集合set
- 常用操作
- 跳表skiplist
- 常用操作
- 使用场景
- 数据结构
- 跳表的构建
- 特点
- 压缩列表ziplist
- 数据结构
- 级联更新问题
- 紧凑列表listpack
- 数据结构
- 解决级联更新问题
- 为什么listpack比ziplist更好
- 取代ziplist
- 基数树
- 压缩存储
- 应用
- 位图
- 布隆过滤器
- 常用操作
- 使用场景
- 原理
- 空间占用预估
- 模糊统计HyperLogLog
- 实现
- PubSub发布订阅
- 缺点
- Stream
- 常用操作
- 结构图
- 几个问题分析
- 限流模块Cell
- 使用方式
- 集群与高可用
- 数据持久化
- 快照
- AOF
- 混合持久化
- 主从同步
- 哨兵机制
- 流程图
- 在这里插入图片描述
- Cluster集群
- 结构图
- 迁移
- 容错
- 网络抖动
- 关于Slave节点是否分担读压力
- Redis多线程
- Redis4.0多线程
- Redis6.0多线程
- 数据过期与删除
- key过期
- 分布式锁
- setnx
- Redlock算法
- 分布式锁
- 近似LRU
- 实现
- 改进
- LFU
- 数据结构
- 惰性删除
- 具体步骤
- 其他
- Gen地理位置模块
- 常用操作
- 实现原理
- 使用技巧
- Scan扫描数据
- 常用操作
- 定位大key
- 定时任务
- 管道
- 压测试验证
- 事务
- 事务的问题
- 内存管理
- 内存管理
- 参考
简介
总结redis常用的知识点,及其应用。
数据结构
Redis对象通用对象
// 所有redis结构都有这个头
struct RedisObject {
int4 type; // 4bits
int4 encoding; // 4bits
int24 lru; // 24bits
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
} robj;
字符串string
动态字符串,可修改,使用预分配空间方式减少内存频繁分配。
常用操作
set、mset、get、mget、setex(设置过期)、expire(指定过期)
incr(针对数字递增1)、incrby(递增N)
数据结构
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}
存储方式
长度小于44字节使用embstr方式存储,否则使用raw方式存储,查看命令如下:
127.0.0.1:6379> debug object g1
Value at:0x7f4cd72a43c8 refcount:1 encoding:embstr serializedlength:4 lru:15045668 lru_seconds_idle:2
为什么是44字节
-
jemalloc/tcmalloc
可以分配32、64字节大小的内存,超过64字节则被认为是大内存。
RedisObject + SDS = 19字节
64字节 - 19字节 = 45字节(其中\0为redis字符串的结尾)
- CPU缓存行为64字节,embstr长度和
cpu cache line
保持一致可以更快加载、重用缓存行
扩容
小于1M加倍扩容,超过1M按照1M的扩容至512M。
列表list
非数组所以插入、删除很快(O1),但是定位(On)常用在异步队列操作。
常用操作
rpush、lpop、rpop、llen、lrange、ltrim(修剪列表)等等
快速列表quicklist
在元素较少时使用压缩链表ziplist
,元素较多使用quicklist
因为指针占用内存较多所以不用双向链表实现
数据结构
struct ziplist {
...
}
struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
...
}
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
...
}
支持LZF
算法压缩,以及设定压缩深度。
存储方式
查看数据结构信息
127.0.0.1:6379> debug object g2
Value at:0x7f4cd7228710 refcount:1 encoding:quicklist serializedlength:18 lru:15046649 lru_seconds_idle:270 ql_nodes:1 ql_avg_node:2.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:16
特点
- 顺序存储
- 不需要指针(针对ziplist)
- 高速缓存(cpu cache line)
- 直接用偏移访问很快
- O1的头尾插入
但是缺点是:
- 删除ziplist中节点需要数据搬移
整数集合intset
如果存储整数集合,为了减少存储空间redis使用inset
存储以便节省空间
例如使用uint16
可以存储全部整数类型,如果超过了uint16
范围则使用uint32
存储
字典
常用操作
hset key field value
hget key field
hgetall key
hmset key field value …
hincrby key field 1 (某个字段单独自增) 等等…
使用场景
字典结构、redis的全部key存储、zset集合存储value和score的映射关系。
数据结构
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx;
unsigned long iterators;
} dict;
包含了2个字典结构其中一个在扩容时会用到
struct dictEntry {
void* key;
void* val;
dictEntry* next; // 链接下一个 entry
}
struct dictht {
dictEntry** table; // 二维
long size; // 第一维数组的长度
long used; // hash 表中的元素个数
...
}
默认的hash函数是:siphash,性能好、生成随机数效果好。
用分桶的方式解决hash冲突。
扩容
字典如果元素很多扩容时需要重新申请内存,然后迁移数据这是一个十分耗时的动作,所以redis使用渐进式
方式rehash。
如果使用了hset、hdel等指令在处于迁移过程中,将新元素挂接到新数组(ht[1])上,旧数组(ht[0])不会写入数据。另外有定时任务定期异步线程搬移数据。
关于扩容的问题
扩容条件:当元素和容量比例1:1时进行扩容,如果处于bgsave、bgaof过程则不会只想扩容,但是当容量快速增加到1:5时强制开始扩容操作
触发扩容的指令:新增、更新、删除操作。会查询ht[0],若存在key则迁移这个key所在的桶并返回元素,如果不存在则ht[1]查找
扩容针对某个db还是全部:针对单个db
高位进位法扩容
字典使用分桶的方式解决hash冲突,查找hashkey时先找到位置,然后往下找item。
看一下高位进位法扩容。例如槽号为3,此时发送了扩容:
可以看下高位进位结果:例如槽号3扩容后的槽3和11在遍历上是相邻的,当扩容时当前槽位从011扩容为0011和1011,则我们直接可以从0011槽位开始遍历避免之前的数据重新遍历。缩容同理,但是由于数据合并可能会有对当前操作数据有些重复遍历。
字典遍历
redis提供2种迭代器,分别是安全、非安全迭代器。
typedef struct dictIterator {
dict *d; // 目标字典对象
long index; // 当前遍历的槽位置,初始化为-1
int table; // ht[0] or ht[1]
int safe; // 这个属性非常关键,它表示迭代器是否安全
dictEntry *entry; // 迭代器当前指向的对象
dictEntry *nextEntry; // 迭代器下一个指向的对象
long long fingerprint; // 迭代器指纹,放置迭代过程中字典被修改
} dictIterator;
非安全迭代器是只读操作,不影响rehash过程,但是可能遍历一些重复元素。另外迭代前和迭代后指纹fingerprint
(MD5码)不可以被修改,否则服务会异常。这里元素修改不会有影响,但是结构变化例如size变化则指纹会修改。
安全迭代器(dict
结构中的iterators表示安全迭代器数量)
迭代器的使用场景:keys使用安全迭代器保证结果不重复、bgaof和bgsave需要遍历对象持久化也是使用安全迭代器、如果需要处理过期操作同理。
其它情况下,允许遍历过程出现元素重复,不修改字典的场景使用非安全迭代器。
hash攻击
利用key生成比较集中,导致字典单桶数据太多,访问时间复杂度为On降低系统性能。
集合set
底层结构使用字段,只不过value为null
常用操作
sadd key value
smember key(可能无序)
sismember key value(是否存在)
scard key(获取长度)
spop key(弹出一个)
跳表skiplist
常用操作
zadd key score value
zrange key 0 1(也可以0到-1)
zrevrange key 0 -1(逆序输出)
zrem key value (删除)
zcard key(相当于count)
zrank key field(获取排名)
zrangebyscore students 0 80.5 (根据分值区间遍历 zset)
zrangebyscore students -inf 80.5 withscores (根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。)
使用场景
数据具有唯一性。使用场景很多例如:value为粉丝ID,score为关注时间,则可以根据关注时间排序粉丝。又或者value为学生ID,score为考试成绩,则可以快速排行。
数据结构
struct zslforward {
zslnode* item;
long span; // 跨度为了快速计算排名
}
struct zslnode {
String value;
double score;
zslforward*[] forwards; // 多层连接指针
zslnode* backward; // 回溯指针
}
struct zsl {
zslnode* header; // 跳跃列表头指针
int maxLevel; // 跳跃列表当前的最高层
map<string, zslnode*> ht; // hash 结构的所有键值对
}
元素少于128+member长度小于64字节用ziplist否则skiplist。
使用ziplist存储时:
ziplist第一个节点保存member、第二个节点保存score,score从小到大排序。
使用skiplist储存时:
score从小到大保存,字典保存从value到score的映射,这样可以快速从value查询到score。
跳表的构建
跳跃列表采取一个随机策略来决定新元素可以跨越到第几层。这里随机概率为25%,且redis最大层高为64。
插入过程
搜索路径、创建节点、分配层数(随机)、修改向前向后指针、修改最大高度
删除过程
同插入类型
更新
这里redis比较粗暴,如果是score修改则先删除后插入的策略更新节点
快速获取排名
forwards
中的span字段表示从当前节点沿着zslforward
指针跳到下一个节点中间跳过了多少节点,在节点插入、删除时会同时更新span的值,这样在计算rank值时,只需要将所有节点跨度span值进行叠加即可。
特点
- 较耗内存,因为需要重复分层存节点,但是也可以通过调参降低内存消耗(节点升级的概率)
- 双向链表连接节点,方便范围操作,另外操作更区域化更好的利用缓存
- zrank操作O(log(N))的时间复杂度
对比平衡树:
1. 插入时修改前后指针,平衡树可能需要做一些`rebalance`操作涉及到树的其他部分
2. 平衡树的顺序输出需要中序遍历,而skiplist使用双向链表快速输出
3. 内存上平衡树需要2个指针节点,跳表(redis)只需要平均1.33个指针即可
压缩列表ziplist
数据结构
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
struct entry {
int<var> prevlen; // 前一个 entry 的字节长度(倒叙遍历)
int<var> encoding; // 元素类型编码
optional byte[] content; // 元素内容
}
压缩列表的每个元素紧凑一起,随着容纳元素不同结构也不一样。
当元素小于254时,prevlen为1字节,否则为5字节(其中开头为0xFE)。
另外对于很小的整数,数值内联到了encoding中以便节省空间(设计比较复杂)。
级联更新问题
由于ziplist的后一个entry都会记录前一个entry的字节长度,所以如果entry的长度从253变成254则下一个entry的pervlen
字段就需要从1字节更新到5字节,如果再后面的entry也是253字节那么也会更新
紧凑列表listpack
Redis5.0中引入了紧凑列表listpack,对ziplist进行了改进,另外在存储空间也会更加节省。
数据结构
struct listpack<T> {
int32 total_bytes; // 占用的总字节数
int16 size; // 元素个数
T[] entries; // 紧凑排列的元素列表
int8 end; // 同 zlend 一样,恒为 0xFF
}
struct lpentry {
int<var> encoding;
optional byte[] content;
int<var> length;
}
这里没有使用zltail_offset
字段来定位最后一个元素,因为length
在entry的最后面,通过total_bytes
和zlend
则可以得到最后一个元素位置,通过尾部length
可以计算出元素大小。
另外长度字段使用varint
进行编码,不同于 skiplist 元素长度的编码为 1 个字节或者 5 个字节,listpack 元素长度的编码可以是1、2、3、4、5个字节。同 UTF8 编码一样,它通过字节的最高为是否为 1 来决定编码的长度(同样设计复杂,不做简介)。
解决级联更新问题
listpack的设计彻底消灭了ziplist 存在的级联更新行为,元素与元素之间完全独立,不会因为一个元素的长度变长就导致后续的元素内容会受到影响。
为什么listpack比ziplist更好
- 解决了级联更新问题,元素间相互独立
- 反向遍历无须维护tail偏移
- 占用空间少,listpack基础结构少了4byte
取代ziplist
listpack 的设计的目的是用来取代 ziplist,不过当下还没有做好替换 ziplist 的准备,因为有很多兼容性的问题需要考虑,ziplist 在 Redis 数据结构中使用太广泛了,替换起来复杂度会非常之高。它目前只使用在了新增加的 Stream 数据结构中。
基数树
Rax(基数树Radix Tree)属于字典树,按照key的字典序排序,支持快速定位、插入、删除。
struct raxNode {
int<1> isKey; // 是否没有 key,没有 key 的是根节点
int<1> isNull; // 是否没有对应的 value,无意义的中间节点
int<1> isCompressed; // 是否压缩存储,这个压缩的概念比较特别
int<29> size; // 子节点的数量或者是压缩字符串的长度 (isCompressed)
byte[] data; // 路由键、子节点指针、value 都在这里
}
压缩存储
如果只有一个子节点,那么路由键就是一个字符串,例如上图的“许多”路由键就是any
。这就是压缩形式的存储。
应用
Rax被用在Redis Stream(见下文)结构中用于存储消息队列,在Stream中消息ID是:时间戳+序号,Rax可以快速根据消息ID定位到具体的消息,然后继续遍历后续的消息。
位图
和普通的位图结构、使用上都一样,这里列出使用指令
setbit x 1 1 (第一位为1)
getbit x 2 (获取第二位)
bitcount x 0 10(位图统计)
bitpos b 1 0 10 (从0-10查找第一个1的位置)
bitfield 一次操作多个指令
布隆过滤器
例如在推荐场景,看新闻不停推荐新内容,这些内容必须是不同的,所以我们需要记录全部用户的记录,所以这里要考虑如何缓存、如何快速查询。
布隆过滤器 (Bloom Filter) 就是专门解决这种去重问题,在去重同时还能节省90%以上的空间,只是稍微没有那么精确,存在误判率。
如果一个元素被判断存在时,这个值可能不存在,但是如果判断不存在,则该元素肯定不存在。所以我们在使用的时候如果判断存在,则需要继续查询缓存(或者其他存储)来判断元素是否真的存在。
常用操作
bf.add (添加元素)
bf.exists(查询元素是否存在)
bf.madd (批量添加元素)
bf.mexists(批量查询元素是否存在)
使用场景
邮件过滤、推荐内容过滤、爬虫URL、DB查询磁盘。
原理
布隆过滤器对应在redis中就是一个大的位数组,以及多个hash函数。
添加
向布隆过滤器中添加key时,会使用多个hash函数对key进行hash计算得到一个整数数值,然后对数组长度取模得到多个index,这样将多个index值都置位1即可。
查询
同理会使用多个hash函数对key进行hash计算得到一个整数数值,然后对数组长度取模得到多个index,然后查询数组这些位置是否都为1即可。
如果这个位数组比较稀疏,判断正确的概率就会很大,如果这个位数组比较拥挤,判断正确的概率就会降低。
空间占用预估
这里是布隆过滤器空间占用计算器
这是公式推导过程
k=0.7*(l/n) # 约等于
f=0.6185^(l/n) # ^ 表示次方计算,也就是 math.pow
- 位数组相对越长 (l/n),错误率 f 越低,这个和直观上理解是一致的
- 位数组相对越长 (l/n),hash 函数需要的最佳数量也越多,影响计算效率
- 当一个元素平均需要 1 个字节 (8bit) 的指纹空间时 (l/n=8),错误率大约为 2%
- 错误率为 10%,一个元素需要的平均指纹空间为 4.792 个 bit,大约为 5bit
- 错误率为 1%,一个元素需要的平均指纹空间为 9.585 个 bit,大约为 10bit
- 错误率为 0.1%,一个元素需要的平均指纹空间为 14.377 个 bit,大约为 15bit
模糊统计HyperLogLog
HyperLogLog算法是一种非常巧妙的近似统计海量去重元素数量的算法。例如我们想统计统计网站主页uv(一个用户1天统计1次,但是不要求完全准确)这时候我们使用其他组件实现起来会比较麻烦,在访问量很大的时候也可能比较占内存。
此时使用HyperLogLog十分合适,误差0只有.81%可能更低,但是存储只占用12K,计数小时存储使用稀疏矩阵存储空间占用小。
实现
一系列整数0位的最大连续长度(代表数量级),这一系列值均分到16384(2^14)个桶中,取调和平均数作为整体的预估值。
占用空间:每个桶的maxbits需要6bits存储*(记录对数),最大maxbits=63,2^14 * 6 / 8 = 12k字节。
PubSub发布订阅
支持消息多播,即消息生产一次中间件将消息复制到多个消息队列。
缺点
消息丢失问题
Redis 会直接找到相应的消费者传递过去。如果一个消费者都没有,那么消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。
高可用问题
如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于一个消费者都没有,所有的消息直接被丢弃。
Stream
Redis5.0新增,支持多播的可持久化消息队列,多个消费组独立游标,同组竞争消费。
解决了Redis Pub/Sub不能持久化消息的问题,但是不同于kafka这些消息队列,Stream不支持分区。
常用操作
xadd(增加消息)
xdel(删除消息)
xrange(获取消息列表,过滤已删除的)
xlen(消息长度)
del(删除stream)
xread(独立消费,阻塞消费)
xread block 0 count 1 streams key(阻塞从尾部读取1个数据)
详细全部指令还是看下官方文档,或者找一些示例再看下。
结构图
几个问题分析
消息太多怎么处理
因为xdel其实不会删除指令只是做了一个标记位,只有总数据长度超过maxlen时才会触发删除操作。
消息没有ACK怎么处理
每个消费者中都保存了正在处理中的消息ID列表PEL,如果消费者受到消息处理完成但是没有ACK会导致PEL列表不断增长,如果消费者很多这个列表就会很长。
PEL如何避免消息丢失
服务端保存了ID列表,如果客户端突然断开,等待下一次连接后,可以再次拿到PEL的全部消息ID。
高可用
在主从赋值的基础上,和其他数据结构复制机制一样,因为Redis指令是异步复制的所以在极端情况下还是可能会丢失数据。
限流模块Cell
Redis4.0中新增的漏桶限流组件
使用方式
cl.throttle allen:reply 15 30 60 1
# key allen
# 15 capacity 这是漏斗容量
# 30 operations / 60 seconds 这是漏水速率
# need 1 quota (可选参数,默认值也是1)
上面这个指令的意思是允许「用户Allen助力行为」的频率为每 60s 最多 30 次(漏水速率),漏斗的初始容量为 15,也就是说一开始可以连续助力15个人,然后才开始受漏水速率的影响。
cl.throttle laoqian:reply 15 30 60
1) (integer) 0 # 0 表示允许,1表示拒绝
2) (integer) 15 # 漏斗容量capacity
3) (integer) 14 # 漏斗剩余空间left_quota
4) (integer) -1 # 如果拒绝了,需要多长时间后再试(漏斗有空间了,单位秒)
5) (integer) 2 # 多长时间后,漏斗完全空出来(left_quota==capacity,单位秒)
集群与高可用
数据持久化
快照
数据二进制序列化存储,结构紧凑。基于COW(copy on write)实现。
COW:父进程创建子进程时,两个进程共享代码、数据段以便节约内存,只有数据发送变更才会触发内存拷贝。所以子进程等同于快照,直接写入数据到磁盘即可。
AOF
- AOF日志,连续增量备份,记录执行指令,但是需要定期合并重写。对一个空Redis执行AOF则包含全部数据。
- 先执行指令才日志存盘。
- 重写:开启一个新AOF文件,然后瘦身,随后将增量记录追加进去
- fsync:文件写操作都是到内存缓冲,然后内核异步刷盘,fsync同步一次
- 持久化:一般是从节点进行,因为没有请求不会有数据不一致问题
- 如果出现了网络分区,从节点了长期连不上主,如果主挂了就丢失数据,此时只能靠监控来防止
混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
主从同步
增量同步
主写入记录到buffer(环形数组),然后从机去同步,并反馈同步到了哪里。
快照同步
主bgsave到磁盘(遍历数据),然后发送给从机,从机执行全量加载(加载前清空数据)。
存在的问题
如果环形buffer太小会触发全量同步。
新节点加入
从节点第一次加入,会触发一次快照同步,随后增量同步。
无盘复制
因为每次落盘太消耗资源,所以主节点可以直接发送数据到从节点,从节点落盘数据后加载数据。
主机不会发生写IO操作。
wait指令
wait指令:wait 1 0 同步N(1)个从库等待(0为无限等待)数据同步完成
哨兵机制
Redis Sentinel保证高可用,实现自动切换主,无须防止运维接入。类似zookeeper,使用3-5个节点组成一个集群。
负责监控主从节点健康,主挂了选优从变成主。客户端连接时也是先连接Sentinel询问主的地址,主故障,需要重新找Sentinel要地址,程序无需重启。 即使原来的主恢复也是从新主重新复制数据。
存在的问题
但是主从异步复制还是会丢数据,Sentinel尽量保证少丢数据。
涉及的配置文件
min-slaves-to-write 1 至少一个从节点正在复制,否则停止写
min-slaves-max-lag 10 10秒都没有收到从节点反馈表示从节点同步异常
流程图
Cluster集群
结构图
它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相互连接组成一个对等的集群,它们之间通过 Gossip协议相互交互集群信息。
将所有数据划分为16384个槽,key对crc16然后hash得到一个整数对16384取模。由于节点心跳要带上槽信息,这里槽信息使用bitmap存储16384个bit只占用2K空间,理论上集群不会超过1000,所以不需要更大的槽。
如果某个节点错误的槽号对应key,将会回复客户端跳转指令让其链接指定正确的槽。
迁移
Redis迁移的单位是槽,一个槽一个槽的迁移。迁移过程:从源节点获取内容=>存到目标节点=>从源节点删除内容,这里是同步迁移,主线程会处理阻塞状态,直到key被成功删除。
网络故障
如果迁移过程中突然出现网络故障,整个slot的迁移只进行了一半。这时两个节点依旧处于中间过渡状态。待下次迁移工具重新连上时,会提示用户继续进行迁移。
大key问题
如果key都比较小,迁移可以很快执行,不会有卡顿现象,但是如果key很大,因为迁移是阻塞指令会同时导致原节点和目标节点卡顿,影响集群的稳定型。所以在集群环境下业务逻辑要尽可能避免大key的产生。
容错
Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。不过 Redis 也提供了一个参数cluster-require-full-coverage
可以允许部分节点故障,其它节点还可以继续提供对外访问。
网络抖动
为了防止网络抖动造成的机器下线(可能波动后网络很快恢复),这里支持某个节点持续timeout时间才会认为故障,如果是主节点则会执行主从切换。
关于Slave节点是否分担读压力
Redis中Slave节点主要是做高可用,不分担查询压力。redis 定位就是支持高性能的查询的,所有主节点就够了。
Redis多线程
Redis4.0多线程
数据支持后台删除,避免主线程卡顿。
Redis6.0多线程
因为Redis使用IO多路复用非阻塞IO,但是IO读写本身是阻塞的,数据从内核态空间拷贝到用户态空间给Redis调用,拷贝过程是阻塞的。
这里监控文件读写事件,通知线程执行去执行IO操作,保证主线程非阻塞。
即使socket读写并行化,但是Redis命令依旧主线程串行执行,这里Redis默认没有开启这个选项,有大佬实际测试性能提升了1倍!
数据过期与删除
key过期
Redis将全量设置过期的key存储到字典。
过期策略
定期过期、惰性过期(客户端访问时)
定期过期
每秒10次扫描,使用贪心策略过期key
- 从过期字段随机选取20key、删除20key中过期的,如果过期比例超过1/4则重复执行。
- 扫描时间不会大于25ms,并且此时是阻塞的,所以可能客户端访问超时但是slowlog查不到慢查询日志,因为这里是等待而不是逻辑处理慢导致的耗时。
大key问题
这里如果定期过去遇到大key,此时删除key将会导致卡顿,并且redis内存回收时也会造成cpu飙高。
分布式锁
setnx
setnx(set if not exists)允许一个客户端占用,用完了在调用del指令释放。
这里的问题就是如果del没有被调用,这样就会进入死锁。
解决办法
我们在加到锁后增加过期时间,例如:
setnx lock:allen true
expire lock:allen 5
但是这里的问题是如果setnx和expire之间服务器出现问题,导致expire不能正确执行导致死锁。
set扩展指令
Redis2.8增加了扩展指令将setnx和expire一起执行。
set lock:allen true ex 5 nx
使用lua脚本
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
超时问题
Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。
Redlock算法
获取锁执行步骤如下:
- 获取当前时间(毫秒数)。
- 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串
my_random_value
,也包含过期时间(比如PX 30000
,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。 - 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
- 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
- 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。
释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。
分布式锁
近似LRU
Redis如果改造成完全LRU,对数据结构设计影响较大,且消耗额外内存,所以Redis使用了近似LRU算法。
实现
每个key增加时间戳记录最后一次访问时间,Redis写操作时,如果内存超了maxmemory,随机取5个淘汰最旧的元素,直到内存正常。
改进
Redis3.0增加淘汰池,进一步优化数据过期的策略。随机待过期的key和淘汰池的多个key对比时间戳,淘汰最旧的key,剩余的保留的等待下次触发。
下面是随机 LRU 算法和严格 LRU 算法的效果对比图:
LFU
Redis中增加LFU算法按照访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。
数据结构
每个对象都会保存热度信息。因为存储的是对数值,所以更新字段时使用概率方法递增。
// redis 的对象头
typedef struct redisObject {
unsigned type:4; // 对象类型如 zset/set/hash 等等
unsigned encoding:4; // 对象编码如 ziplist/intset/skiplist 等等
unsigned lru:24; // 对象的「热度」
int refcount; // 引用计数
void *ptr; // 对象的 body
} robj;
惰性删除
Redis维护了一个队列专门交给异步线程消费任务执行。
- Redis4.0 unlink key交给后台线程异步删除(key过期、LRU淘汰等)
- flushdb flushall缓慢清空数据库,增加async指令,扔到异步任务队列后台线程执行
- AOF也是异步队列执行
具体步骤
执行懒惰删除时,Redis 将删除操作的相关参数封装成一个bio_job
结构,然后追加到链表尾部。异步线程通过遍历链表摘取 job 元素来挨个执行异步任务。
struct bio_job {
time_t time; // 时间字段暂时没有使用,应该是预留的
void *arg1, *arg2, *arg3; // 分别是普通对象、字典、基数树对象
};
其他
Gen地理位置模块
将二维坐标映射到一维平面中,二维越接近,一维越接近。
具体做法:二维地图二分切割,一个元素在一个小正方体,例如分4块,则元素可以用00-01-10-11的4bit表示,正方形越小,二进制越大,精度越高。
常用操作
新增:geoadd company 116.48105 39.996794 juejin
删除:直接用zset的zrem指令
距离:geodist company juejin ireader km
位置:geopos company juejin
Hash值(base32):geohash company ireader
附近其他元素: georadiusbymember company ireader 20 km count 3 asc
实现原理
Redis将经纬度用52位整数编码放入zset,value为元素的key,查询时因为跳表连续,很方便查询附近的元素。
使用技巧
- 建议单Redis实例部署,否则集群迁移数据卡顿
- 数据量过大应该根据国家、地区、市单独划分出来key
Scan扫描数据
获取全部key:keys c
(c开头的key)但是复杂度O(n)主线程扫描较慢影响线上业务。
Redis2.8增加Scan,通过游标渐进式进行,提供limit获取数量,需但是要客户端去重(扩容时)
常用操作
scan 0 match key* count 1000 (开始扫匹配1000个,返回匹配的数据)
定位大key
定位大key:scan命令逐步拿到全部key使用对应接口的size、len获取大小
redis的方法:redis-cli -h 127.0.0.1 -p 7001 –-bigkeys
具体如下:
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1
扫描100次休眠0.1秒
定时任务
Redis维护了一个任务小顶堆,记录下次执行的timeout时间,每个循环周期查询堆顶即可(Nginx同理)。
管道
Redis管道(流水线),正常的 req => rsp,req => rsp,我们可以合并先req => req,然后收到2个rsp,这样称为管道,合并请求,减少了网络调用极大的提高吞吐能力。
正常write很快,但是read要等待对端返回。但是并行之后,收到第一个read说明其他的响应也在内核缓冲区了,可以快速读取。
压测试验证
redis-benchmark -t set -q # 测试10w/s
redis-benchmark -t set -P 2 -q # 测试20w/s,增加-P参数最多到100w/s。
事务
multi事务开始、exec事务执行、discard事务丢弃。redis服务器缓存执行,收到exec执行。
事务的问题
非原子性,失败了不回滚,仅仅是满足隔离性。
内存管理
内存管理
使用facebook的jemalloc做内存管理(info命令可以查看)
参考