对象
redisObject¶
Redis 是 key-value 存储,key 和 value 都被抽象为对象,key 只能是 String 对象,value 支持丰富的对象种类:String, List, Set, Hash, Sorted Set, Stream
redisObject 定义:
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void* ptr;
} robj;
- type 表示是哪种 Redis 对象
- encoding 表示使用哪种底层编码
- refcount 有多少指针指向该对象
- ptr 指向 Object 的实际内容
Redis 中每个键只能是一种数据类型,对同一个键执行不同的更新操作时会将 key 的类型给覆盖掉
底层三剑客:
- SDS
- Ziplist
- Dict
通用命令¶
- KEYS:比如 keys * ,查看所有 key;或者 keys a* 查看以 a 开头的 key
- DEL:删除一个或多个 key
- EXIST:是否存在某个 key
- EXPIRE:给一个 key 设置有效期(以秒为单位),节省内存,比如 expire age 20
- TTL:查看一个 key 的剩余有效期
- -1 表示永久有效
- -2 表示已失效
String¶
value 是字符串,可以表示 string, int, float,一个 value 最大不超过 512m
使用
object encoding key
可以查看 key 使用的编码类型
常用操作¶
- SET:set key value 添加或修改已存在的字符串
- MSET:批量增加
- GET:根据给定的 key 查 value
- setnx key value 等价于 set key value nx,返回 nil 表示失败
- setex key 10 value 等价于 set key value ex 10
其中 SETNX key value 表示 set if not exist,等同于 SET key value NX
MGET 表示一次查询多个 MGET name age
底层实现¶
基本的编码方式是 RAW ,基于简单动态字符串 SDS 实现,储存上限为 512 MB
编码方式:
INT
:当要存可以用 long 表示的整数时使用 INT 存储EMBSTR
:当字符串长度小于阈值字节时使用RAW
:当字符串长度大于阈值字节时使用
随着操作,编码可能会发生转换:
- INT -> RAW:当存储范围i超过 long 时
- RAW -> EMBSTR:
都是一个 redisObject
和 sdshdr
组成:
EMBSTR¶
此时 redisObject head 和 SDS 是一段连续的空间,申请内存时只需要调用一次内存分配函数,效率更高
RAW¶
EMBSTR 向 RAW 编码方式转换的阈值是 44 是为了防止内存碎片(jmalloc 只能申请 2^n 的空间)
INT¶
直接使用 8 字节的 ptr 指针变为 Long 型的数据
SDS Simple Dynamic String¶
- 属性 len 支持快速返回长度,同时不再以 '/0' 作为结尾标准,当存储图像等资源时更加安全,保证了二进制安全
- 增加了空余空间,并规定预留
min(1M, len)
的长度,为追加数据预留空间
这里的 len 和 alloc 都是用于标记的数值,alloc 表示总大小
key 的层级格式¶
问题:需要存储用户 / 商品信息到 redis ,但是用户和商品都有 id 为 1 的数据,如何区别?
Redis 的 key 允许有多个单词形成层级结构,多个单词之间用 ':' 隔开,格式为:
Redis 的 value 使用 json 字符串:
这里 redis 本身不会去解析 : 这个符号
Hash¶
Hash 的 value 是一个无序字典
- value 又分为 field 和 value 两个部分,一个 key 下可以有多个 field - value 对
每个 key 可以存 \(2^{32} - 1\) 个键值对
常用操作¶
- HSET key field value(可以针对单个 value 进行修改)
- HGET key field 找到目标 key 下对应的 value
- HMSET key field value field value ... 例如:hmset proj:user:4 name Jack age 18 gender male
- HMGET key field field ... 例如:hmget proj:user:4 name age gender
- HMGETALL key 拿到 key 下的所有 field 和 value
- HKEYS key 拿到 key 下的所有 field
- HSETNS key field value 判断的是key下对应的 field 是否存在才插入,而不是判断 key 是否存在
- HSET 手动构造一系列的键值对
- HSETNX key field value 如果 field 不存在,则设置成指定的 value,返回成功更新的数据数量
- HDEL key field 删除指定的 field,可一次删多个对
查询操作:
- HGETALL 找到 key 中的所有数据
- HGET key field 查询 field 对应的 value
- HLEN 查询 Hash 中总共的元素数量
-
HSCAN key cursor [MATCH pattern] [COUNT count]
处于 ziplist 下时 count 不管填多少都是返回全部,因为 ziplist 本身就是小集合
底层结构¶
编码方式:
ZIPLIST¶
将 field-value 键值对当作 entry
HASHTABLE¶
与 SET 中 hashtable 的区别在于 SET 中的 value 始终为 null
List¶
可以看作每个 key 下有一个双向链表,可以用于表示有(先后)序的一个集合
目前 List 的最大元数个数为 \(2^{64} - 1\)
常用操作¶
- LPUSH:lpush users 1 2 3 这里结果从左到右为 3, 2, 1
- LRANGE:lrange users 1 2 小标为 1 到下标为 2 的元素
阻塞式获取:
- BLPOP / BRPOP key time:如果无法获取,则会放弃 CPU 进行阻塞等待指定时间,直到另一个客户端插入符合条件的值,才会被立即唤醒
如何使用 List 模拟一个阻塞队列
- 使用 BLPOP 或 BRPOP ,这里阻塞是有就取,没有就等待不取
支持类似 deque 的进出操作
-
LREM key count value 从左向右移除 count 个等于 value 的元素,当 count = 0 时表示移除所有等于 value 的元素
返回值为被移除的元素的数量
- DEL 是对整个 List 对象的删除,返回值为成功删除的
键
的个数
UNLINK 是异步删除命令,只是取消键值关联,删除时不会阻塞客户端线程;但是 DEL 是同步删除命令,会阻塞客户端
- LLEN key 查看 List 的长度
- LRANGE key start stop 查看 List 种从 start 到 stop 的元素个数(stop 可以超过容量)
- 还有一个 LINDEX key index 查询指定下标处的元素
底层实现¶
ZIPLIST¶
一种特殊的双端链表,由一系列特殊编码的连续内存快组成。可以在任意一端进行压入 / 弹出操作,且此操作的时间复杂度为 O(1)
在数据少的时候可以节约内存
LINKEDLIST¶
在数据多的时候提高更新效率(ZIPLIST 数据量插入导致内存复制)
QUICKLIST¶
3.2 版本后统一使用 QUICKLIST
quicklist 相当于是使用双向链表把 ziplist 给串起来了
当数据较少的时候,QUICKLIST 的节点只有一个,相当于是一个 ZIPLIST
7.0 版本以后 List 底层结构变为了通用的 QUICKLIST ,而双向链表种的每个节点又从 ZIPLIST 优化为了 LISTPACK
LISTPACK 优化¶
本质上就是为了解决连锁更新问题。则考虑一种不使用 prevlen 来找到上一个节点的方式
LISTPACK 的 entry 定义:
这里 element-tot-len 中存了
Set¶
与 HashSet 类似,可以看作一个 value 为 null 的 HashMap(保证了 key 的唯一性)
- 元素无序,不可重复
- 查找快
- 支持交集,并集,差集运算(可用于共同关注等功能)
使用场景¶
某个用户关注了哪些公众号,且 set 提供了查交集,并集等功能,可以方便地找出共同关注
常用操作¶
增 / 减;查询总数量;判断存在性;拿出所有
- SADD key member :向 set 中添加一个或多个元素
- SREM key member :移除 set 中的指定(多个)元素
- SCARD key :返回 set 中元素的个数
- SISMEMBER key member :相当于 contains() 方法
- SMEMBERS :获取 set 中的所有元素
交集,差集,并集
- SINTER key1 key2
- SDIFF key1 key2 :存在于 key1 的集合但是不存在于 key2 的集合
- SUNION key1 key2
- SISMEMBER 查询元素是否存在
- SCARD 查询集合元素的个数
- SMEMBERS 查看集合的所有元素
-
SSCAN key cursor MATCH
当集合中元素过多时,无法一次性返回所有元素,可以更根据 MATCH 筛选或者用 COUNT 指定返回的数量
- 返回的内容为
- 下一个 cursor 的位置
- 返回元素的列表
127.0.0.1:6379> SADD myset "apple" "banana" "cherry" "date" "elderberry" (integer) 5 127.0.0.1:6379> SSCAN myset 0 1) "0" # 游标值为 0,表示迭代结束 2) 1) "apple" # 返回的元素列表 2) "banana" 3) "cherry" 4) "date" 5) "elderberry" 127.0.0.1:6379> SSCAN myset 0 MATCH b* 1) "0" # 游标值为 0,表示迭代结束 2) 1) "banana" # 返回匹配模式 "b*" 的元素 127.0.0.1:6379> SSCAN myset 0 COUNT 2 1) "3" # 下一个游标值 2) 1) "apple" # 本次返回的元素 2) "banana" 127.0.0.1:6379> SSCAN myset 3 COUNT 2 1) "0" # 游标值为 0,表示迭代结束 2) 1) "cherry" # 本次返回的元素 2) "date" 3) "elderberry"
- 返回的内容为
集合操作:
- SINTER key [key...] 返回第一个集合和后面所有集合的交集
- SUNION key [key...] 返回所有集合的并集
- SDIFF key [key...] 返回第一个集合相比后面的集合多了哪些元素
底层实现¶
-
使用 HashTable,也就是 Redis 中的 Dict 来保证元素唯一性和查询的效率
使用 DIct 中的 key 来存储元素,value 统一为 null
- 当存储的所有数据都是整数,且数量不超过阈值时,使用 IntSet
编码方式:
INTSET¶
如果元素都是整数,且元素数量不超过 512 个,使用此编码,内存占用少,但是需要二分查找
HASHTABLE¶
不满足上述条件,查询复杂度为 O(1)
ZSet¶
即 Sorted Set ,同一 ZSET 下的每个元素为:
- 成员:元素的唯一标识,通常为字符串
- 分数:一个浮点数,用于对成员排序
常用操作¶
排名从 0 开始
下面的所有排名默认为升序排名,如果需要降序,则在命令的 Z 后面添加 REV 即可
增改:
- ZADD key score member :添加一个或多个元素到 sorted set,已存在则更新 score 值
- ZINCRBY key diff member :修改指定 member 的 score
- ZREM key member :删除 sorted set 中的一个指定元素
获取信息:
- ZSCORE key member :获取指定 member 的 score 值
- ZCARD key :获取 zset 中元素的个数
排序相关:
- ZRANK key member :获取指定元素的排名
- ZCOUNT key min max :获取 score 在范围内的所有元素的个数
- ZRANGEBYSCORE key min max :获取指定 score 范围内的所有元素
- ZRANGE key min max :获取指定排名值内的元素
交集,并集,差集
ZDIFF / ZINTER / ZUNION
写操作:
扩展参数:
- XX:只更新存在的成员,不添加新成员
- NX:不更新存在的成员,只添加新成员
- LT:更新新分值比原分值小的成员,不存在则新增
- GT:更新新分值比原分值大的成员,不存在则新增
读操作:
# 查看成员总数
ZCARD board
# 查找 start 到 stop 范围的数据(并显示 score)
ZRANGE board 0 -1 [WITHSCORE]
# 从大到小遍历
ZREVERANGE board 0 -1 [WITHSCORE]
# 计算 min-max 积分范围的个数
ZCOUNT board 100 200
# 查看 Member 的排名索引,第一索引为 0
ZRANK board usr1
# 产看 Member 成员的分数
ZSCORE board usr3
底层实现¶
ZIPLIST¶
在数据量较小时使用,自己实现排序,将 member 和 score 视为一个整体,并逻辑上实现按照 score 排序
当满足如下条件时使用:
- 所有字符串对象的长度均小于 64 字节
- 对象元素的个数小于 128
SKIPLIST + HASHTABLE¶
相对于 hash 就是多了一个 SkipList
当不满足上述条件中的任意一个时使用
- 使用 SKIPLIST 优化对于分值的排名操作与范围查询性能
- 使用 HASHTABLE 可以在 O(1) 时间内查到成员的分数值
底层结构 - 压缩表¶
- ziplist 平常说的压缩表
- listpack 在 redis 7.0 种完全替换了 ziplist
压缩表的优势¶
由于数据是连续存储的,缓存命中率较高
ZIPLIST 结构¶
例如有三个 entry 的 ziplist:
- zlbytes:表示整个 ziplist 占据了多少字节
- zltail :ziplist 的尾部 entry 相对于开头的偏移量,如果 ziplist 为空则指向 zlend
- zllen :表示 entry 的数量
- zlend :标记结束
entry 的结构:
-
prevlen
表示上一个 entry 的数据长度。这是用来定位上一个 entry 的起始位置,从而实现逆向遍历
encoding
表示编码类型。encoding 中还有一个 entry 的长度信息,用于正向遍历
- entry-data 即为实际的数据
ZIPLIST 查询数据¶
查询数据总量(llen)¶
注意到 zllen 中存储了 entry 的数量,一般情况下可以在 O(1) 时间内得到 entry 数量
但是 zllen 只有两个字节,当 zllen 大于 65534 时, zllen 字段无效,此时需要遍历得到真实的节点数量
这样设计是因为 redis 中 ZIPLIST 一般用于节点个数少的场景
查询指定的数据节点(lindex)¶
需要遍历整个 List ,复杂度为 O(N)
更新数据¶
连锁更新问题:
连锁更新指的是节点后移发生了多次,本质上就是 entry 中 prevlen 字段的长度从 1 涨到了 5 然后下一个节点的 prevlen 也需要从 1 涨到 5(超过了 256 - 2 个字节)
底层数据结构 - hashtable¶
通过 hashtable 可以只用 O(1) 查找 field 对应的 value
- size 表示容量
- used 表示当有了几个 entry
- sizemask = size - 1 ,注意到 size 是 2 的幂次方,则与 sizemask 和 hash值做与相当于取余
渐进式扩容¶
不会一口气就扩容完,而是会在扩容完成之前维持两个 dictht, 在每次请求时进行迁移
rehashidx
的值从0
开始,表示当前正在迁移旧哈希表中索引为rehashidx
的桶(bucket)。这里的迁移内容和请求的目标无关,而是迁移整个 rehashidx 处的桶及其上面的链表,并自增对应的 rehashidx
- 查找的时候首先在 h0 中查找,找不到则到 h1 中查找
当 请求到来时,发现对应 rehashidx 位置已经空了,仍然会向后找 10 个再停止
扩容¶
这里的复制命令是 redis 持久化的相关内容
缩容¶
底层数据结构 - 跳表¶
决定层高¶
每次插入一个节点时,设置的层高在 1 的基础上增加的概率为 25%(75% 的概率失败并停止继续增加)
查找复杂度¶
查找的最坏复杂度为 O(N) ,平均复杂度为 O(logN)
对象过期时间¶
过期时间是位一个 key 指定一个时间点,到达这个时间后此数据被认为是过期的,可以被 redis 回收