跳转至

对象

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

image-20241227120255641

其中 SETNX key value 表示 set if not exist,等同于 SET key value NX

MGET 表示一次查询多个 MGET name age

底层实现

基本的编码方式是 RAW ,基于简单动态字符串 SDS 实现,储存上限为 512 MB

编码方式

image-20241227120929512

  • INT:当要存可以用 long 表示的整数时使用 INT 存储
  • EMBSTR:当字符串长度小于阈值字节时使用
  • RAW:当字符串长度大于阈值字节时使用

随着操作,编码可能会发生转换:

  • INT -> RAW:当存储范围i超过 long 时
  • RAW -> EMBSTR:

都是一个 redisObjectsdshdr 组成:

EMBSTR

此时 redisObject head 和 SDS 是一段连续的空间,申请内存时只需要调用一次内存分配函数,效率更高

image-20241227121405514

RAW

image-20241227121429752

EMBSTR 向 RAW 编码方式转换的阈值是 44 是为了防止内存碎片(jmalloc 只能申请 2^n 的空间)

INT

直接使用 8 字节的 ptr 指针变为 Long 型的数据

image-20250204214814619

SDS Simple Dynamic String

image-20241227121550853

  • 属性 len 支持快速返回长度,同时不再以 '/0' 作为结尾标准,当存储图像等资源时更加安全,保证了二进制安全
  • 增加了空余空间,并规定预留 min(1M, len) 的长度,为追加数据预留空间

这里的 len 和 alloc 都是用于标记的数值,alloc 表示总大小

key 的层级格式

问题:需要存储用户 / 商品信息到 redis ,但是用户和商品都有 id 为 1 的数据,如何区别?

Redis 的 key 允许有多个单词形成层级结构,多个单词之间用 ':' 隔开,格式为:

项目名:业务名:类型名:id

Redis 的 value 使用 json 字符串:

set proj:user:1 '{"id":1, "name":"Jack", "age":18}'

这里 redis 本身不会去解析 : 这个符号

Hash

Hash 的 value 是一个无序字典

  • value 又分为 field 和 value 两个部分,一个 key 下可以有多个 field - value 对

每个 key 可以存 \(2^{32} - 1\) 个键值对

image-20250115215014914

常用操作

  • 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 是否存在

image-20241227160123919

  • 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 本身就是小集合

底层结构

编码方式:

image-20241227165849550

ZIPLIST

将 field-value 键值对当作 entry

image-20241227165936675

HASHTABLE

与 SET 中 hashtable 的区别在于 SET 中的 value 始终为 null

image-20250204224334405

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 的进出操作

image-20241227135838851

  • 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 查询指定下标处的元素

底层实现

image-20241227141610046

ZIPLIST

一种特殊的双端链表,由一系列特殊编码的连续内存快组成。可以在任意一端进行压入 / 弹出操作,且此操作的时间复杂度为 O(1)

在数据少的时候可以节约内存

LINKEDLIST

在数据多的时候提高更新效率(ZIPLIST 数据量插入导致内存复制)

QUICKLIST

3.2 版本后统一使用 QUICKLIST

quicklist 相当于是使用双向链表把 ziplist 给串起来了

image-20250204220106380

image-20241227142616763

当数据较少的时候,QUICKLIST 的节点只有一个,相当于是一个 ZIPLIST

7.0 版本以后 List 底层结构变为了通用的 QUICKLIST ,而双向链表种的每个节点又从 ZIPLIST 优化为了 LISTPACK

LISTPACK 优化

本质上就是为了解决连锁更新问题。则考虑一种不使用 prevlen 来找到上一个节点的方式

LISTPACK 的 entry 定义:

<encoding-type> <element-data> <element-tot-len>

这里 element-tot-len 中存了 的总长度,且其中的每个 byte 的首个 bit 用于标识是否结束,如果是 0 表示结束,此时可以从后向前地计算出上一个 entry 的首个位置

image-20241227152714112

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

image-20241227153408269

  • 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

编码方式:

image-20241227155138486

INTSET

如果元素都是整数,且元素数量不超过 512 个,使用此编码,内存占用少,但是需要二分查找

image-20241227155805368

HASHTABLE

不满足上述条件,查询复杂度为 O(1)

image-20250204224123072

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

image-20241227211509874

写操作:

ZADD board 100 usr1 90 usr2 80 usr3
ZREM board usr2

扩展参数:

  • 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

底层实现

image-20241227212738226

ZIPLIST

在数据量较小时使用,自己实现排序,将 member 和 score 视为一个整体,并逻辑上实现按照 score 排序

当满足如下条件时使用:

  • 所有字符串对象的长度均小于 64 字节
  • 对象元素的个数小于 128

image-20250204223616574

SKIPLIST + HASHTABLE

相对于 hash 就是多了一个 SkipList

当不满足上述条件中的任意一个时使用

  • 使用 SKIPLIST 优化对于分值的排名操作与范围查询性能
  • 使用 HASHTABLE 可以在 O(1) 时间内查到成员的分数值

image-20241227213812886

底层结构 - 压缩表

  • ziplist 平常说的压缩表
  • listpack 在 redis 7.0 种完全替换了 ziplist

压缩表的优势

由于数据是连续存储的,缓存命中率较高

ZIPLIST 结构

例如有三个 entry 的 ziplist:

image-20241227143510921

  • zlbytes:表示整个 ziplist 占据了多少字节
  • zltail :ziplist 的尾部 entry 相对于开头的偏移量,如果 ziplist 为空则指向 zlend
  • zllen :表示 entry 的数量
  • zlend :标记结束

entry 的结构:

<prevlen> <encoding> <entry-data>
  • prevlen 表示上一个 entry 的数据长度。这是用来定位上一个 entry 的起始位置,从而实现逆向遍历

    image-20241227144655697

  • 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 个字节)

image-20241227150117455

底层数据结构 - hashtable

image-20250204223152272

通过 hashtable 可以只用 O(1) 查找 field 对应的 value

image-20241227180646572

  • size 表示容量
  • used 表示当有了几个 entry
  • sizemask = size - 1 ,注意到 size 是 2 的幂次方,则与 sizemask 和 hash值做与相当于取余

渐进式扩容

不会一口气就扩容完,而是会在扩容完成之前维持两个 dictht, 在每次请求时进行迁移

  • rehashidx 的值从 0 开始,表示当前正在迁移旧哈希表中索引为 rehashidx 的桶(bucket)。

这里的迁移内容和请求的目标无关,而是迁移整个 rehashidx 处的桶及其上面的链表,并自增对应的 rehashidx

image-20241227190359333

  • 查找的时候首先在 h0 中查找,找不到则到 h1 中查找

image-20241227192006390

当 请求到来时,发现对应 rehashidx 位置已经空了,仍然会向后找 10 个再停止

扩容

image-20241227192245732

这里的复制命令是 redis 持久化的相关内容

缩容

image-20241227192306946

底层数据结构 - 跳表

image-20241227195855516

决定层高

每次插入一个节点时,设置的层高在 1 的基础上增加的概率为 25%(75% 的概率失败并停止继续增加)

查找复杂度

查找的最坏复杂度为 O(N) ,平均复杂度为 O(logN)

对象过期时间

过期时间是位一个 key 指定一个时间点,到达这个时间后此数据被认为是过期的,可以被 redis 回收