Redis 学习笔记

在这里插入图片描述

一、基础命令

1.1 通用命令

  • KEYS pattern 查找所有符合给定模式 pattern 的 key,其中 * 匹配零个或多个字符,? 匹配一个字符。
  • DEL key [key ...] 删除给定的一个或多个 key。
  • EXISTS key 检查给定 key 是否存在。
  • EXPIRE key seconds 为给定 key 设置生存时间,生存时间为 0 时该 key 过期,它会被自动删除。
  • TTL key 以秒为单位,返回给定 key 的剩余生存时间。

1.2 String

  • SET key value [EX seconds] [PX milliseconds] [NX|XX] 将字符串值 value 关联到 key。如果 key 已经持有其他值,SET 就覆写旧值,无视类型。对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
  • GET key 返回 key 所关联的字符串值。如果 key 不存在那么返回特殊值 nil。如果 key 储存的值不是字符串类型,返回一个错误,因为 GET 只能用于处理字符串值。
  • MSET key value [key value ...] 同时设置一个或多个 key-value 对。MSET 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置。
  • MGET key [key ...] 返回一个或多个给定 key 的值,如果给定的 key 里面有某个 key 不存在,那么这个 key 返回 nil
  • INCR key 将 key 中储存的字符串解释为十进制 64 位有符号整数并加一。如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行 INCR 操作。如果值包含错误的类型,或字符串类型的值不能解释为数字,那么返回一个错误。
  • INCRBY key increment 将 key 所储存的值加上增量 increment,具体实现与 INCR 类似。
  • HINCRBYFLOAT key field increment 将 key 所储存的值加上浮点数增量 increment,INCRBYFLOAT 的计算结果最多只能表示小数点的后十七位。
  • SETNX key value 即 set if not exists,当且仅当 key 不存在时将 key 的值设为 value,若给定的 key 已经存在,则 SETNX 不做任何动作。
  • SETEX key seconds value 即 set with expire,将值 value 关联到 key,并将 key 的生存时间设为 seconds。如果 key 已经存在, SETEX 命令将覆写旧值。

1.3 Hash

  • HSET key field value 将哈希表 key 中的域 field 的值设为 value。如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。如果域 field 已经存在于哈希表中,旧值将被覆盖。
  • HGET key field 返回哈希表 key 中给定域 field 的值。
  • HMSET key field value [field value ...] 同时将多个 field-value 对设置到哈希表 key 中。
  • HMGET key field [field ...] 返回哈希表 key 中,一个或多个给定域的值。如果给定的域不存在于哈希表,那么返回一个 nil 值。
  • HGETALL key 返回哈希表 key 中所有的域和值。
  • HKEYS key 返回哈希表 key 中的所有域。
  • HVALS key 返回哈希表 key 中所有域的值。
  • HINCRBY key field increment 为哈希表 key 中的域 field 的值加上增量 increment。增量也可以为负数,相当于对给定域进行减法操作。
  • HSETNX key field value 将哈希表 key 中的域 field 的值设置为 value,当且仅当域 field 不存在。

1.4 List

  • LPUSH key value [value ...] 将一个或多个值 value 依次插入到列表 key 的表头(左侧)。
  • LPOP key 移除并返回列表 key 的头元素。
  • RPUSH key value [value ...] 将一个或多个值 value 依次插入到列表 key 的表尾(右侧)。
  • RPOP key 移除并返回列表 key 的尾元素。
  • LRANGE key start stop 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。
  • BLPOP key [key ...] timeoutLPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。
  • BRPOP key [key ...] timeoutRPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。

1.5 Set

  • SADD key member [member ...] 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
  • SREM key member [member ...] 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。
  • SCARD key 返回集合 key 中元素的数量。
  • SISMEMBER key member 判断 member 元素是否集合 key 的成员。
  • SMEMBERS key 返回集合 key 中的所有成员,不存在的 key 被视为空集合。
  • SINTER key [key ...] 返回一个集合的全部成员,该集合是所有给定集合的交集。
  • SDIFF key [key ...] 返回一个集合的全部成员,该集合是所有给定集合之间的差集。
  • SUNION key [key ...] 返回一个集合的全部成员,该集合是所有给定集合的并集。

1.6 SortedSet

  • ZADD key score member [[score member] [score member] ...] 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。score 值可以是整数值或双精度浮点数。
  • ZREM key member [member ...] 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。
  • ZSCORE key member 返回有序集 key 中,成员 member 的 score 值。如果 member 元素不是有序集 key 的成员或 key 不存在,返回 nil
  • ZRANK key member 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。
  • ZCARD key 返回有序集 key 的基数。
  • ZCOUNT key min max 返回有序集 key 中,score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max)的成员的数量。
  • ZINCRBY key increment member 为有序集 key 的成员 member 的 score 值加上增量 increment,increment 可以为负。
  • ZRANGE key start stop [WITHSCORES] 返回有序集 key 中,指定区间内的成员。其中成员的位置按 score 值递增来排序。
  • ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间的成员。

二、Redis 和数据库的数据一致性

在并发场景下,如果只有读操作并不会发生数据不一致,但如果存在写操作,不管是先更新数据库还是先更新缓存,只要两次更新操作中夹杂了其他线程的完整更新操作都会导致最终的数据不一致。举两个例子来说就是 线程 A 更新缓存 -> 线程 B 更新缓存 -> 线程 B 更新数据库 -> 线程 A 更新数据库线程 A 更新数据库 -> 线程 B 更新数据库 -> 线程 B 更新缓存 -> 线程 A 更新缓存,以上两种情况下,线程 B 的完整更新均使得线程 A 的更新成为了部分更新,因此发生了最终的数据不一致。

旁路缓存策略(Cache Aside)能在一定程度上解决数据不一致的问题,该策略主要分为读策略和写策略两个部分:

  • 写策略:先更新数据库中的数据,再删除缓存中的数据。
  • 读策略:如果读取的数据命中了缓存,则直接返回数据。如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

对于写策略,主要有两个注意事项:

  • 首先是删除缓存还是更新缓存。如果采用更新缓存,那么每次更新数据库都要对应更新缓存,但这些数据并不一定会被查询,此时会导致大量的无效写操作。而删除缓存只需要让缓存失效,开销更小,同时还能在一定程度上避免 线程 A 更新数据库 -> 线程 B 更新数据库 -> 线程 B 更新缓存(删除) -> 线程 A 更新缓存(删除) 的数据不一致问题。
  • 其次是先操作数据库还是先操作缓存。其实,不管是先操作谁都有可能发生数据不一致(见下图),不过由于缓存的写入通常要远远快于数据库的写入,因此先操作数据库发生数据不一致的可能性非常小。

在这里插入图片描述

基于旁路缓存策略,可以通过加锁将更新数据库和删除缓存整合为一个原子操作从而保证强一致性,但很明显这会影响性能。同时,也可以在写入缓存时指定 TTL,这样哪怕发生数据不一致也能在缓存失效后得到解决。

此外,如果采用先删除缓存再更新数据库,通过延迟双删(在更新数据库后延迟一定时间再删除一次缓存)也可以在一定程度上解决数据不一致的问题。因此,任何一种缓存更新方案都不能说是绝对的最优解,具体用哪种方案就要看对性能和一致性的取舍了。


三、缓存穿透

缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,所有的请求都会访问数据库。

常见的解决方案有三种:

  • 限制非法请求:可以在 API 入口处对请求进行判断,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
  • 缓存空对象:缓存空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。优点是实现简单、维护方便,缺点是会产生额外的内存消耗已以及短期的数据不一致。
  • 布隆过滤器:布隆过滤器通过对数据取模在位图数组中相应位置进行标记,访问数据前只需要判断对应位置是否被标记即可。因此布隆过滤器能够快速判断数据是否存在,从而过滤请求。优点是内存占用少,不过实现复杂而且存在误判的可能。

四、缓存雪崩

缓存雪崩是指在同一时段大量的缓存数据同时失效或者 Redis 故障宕机,导致大量请求到达数据库,为数据库带来巨大压力。

解决大量的缓存数据同时失效:

  • 均匀设置过期时间:在对缓存数据设置过期时间时,给过期时间加上一个随机数,尽量避免数据在同一时间过期。
  • 对缓存构建加锁:业务线程如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据并更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 后台缓存更新:缓存设置为永久有效,业务线程也不再负责更新缓存,将更新缓存的工作交由后台线程定时执行。在业务刚上线的时候,我们可以提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制很适合完成这个工作。

解决 Redis 故障宕机:

  • 服务熔断、降级或请求限流机制:因为 Redis 故障宕机而导致缓存雪崩问题时,我们可以启用服务熔断,暂停业务应用对缓存服务的访问,直接返回错误,或者启用服务降级,只提供默认值或简化的服务而不是完全失败,从而降低对数据库的访问压力,等到 Redis 恢复正常后,再允许业务应用访问缓存服务。服务熔断机制保护了数据库,但是暂停了业务访问,为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,其余请求就在入口直接被拒绝,等到 Redis 恢复并把缓存预热完后,再解除请求限流。
  • 构建 Redis 缓存高可靠集群:可以搭建 Redis 集群,如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

五、缓存击穿

缓存击穿也叫热点 key 问题,是指一个被高并发访问并且缓存重建业务较为复杂的 key 突然失效了,无数的请求同时访问数据库并重建缓存。

常见的解决方案有两种:

  • 互斥锁:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。这种方法可以保证数据一致性但是互斥锁会影响性能。
  • 逻辑过期:不显式地为热点 key 指定 TTL,而是由专门地异步线程在热点 key 逻辑过期前进行缓存更新。这种方法性能较好但是不能保证数据的强一致性。

六、数据持久化

6.1 RDB

RDB(Redis Database Backup)全称 Redis 数据库备份,也被称为 Redis 数据快照。它的主要作用是将内存中的所有数据保存到磁盘上的 RDB 文件中。当 Redis 实例发生故障并需要重启时,可以从磁盘上的快照文件中读取数据并进行恢复。这种机制保证了数据的持久性和可靠性。

SAVEBGSAVE 命令用于执行 RDB 操作。在 Redis 服务停止时,会自动执行一次 SAVE 操作。另一方面,BGSAVE 会根据配置文件中的 save <seconds> <changes> 定时在后台执行。具体来说,BGSAVE 命令会创建一个子进程,从而实现后台的异步执行持久化操作。同时通过 fork 的写时复制策略,它可以避免父进程读操作与子进程写操作之间的冲突,确保数据的一致性。最终,子进程会用生成的新的 RDB 文件替换旧的 RDB 文件,实现持久化。

通过 RDB 实现持久化主要有两个缺点:

  • RDB 执行间隔长,两次 RDB 之间写入的数据有丢失的风险
  • fork 子进程、压缩以及写入 RDB 文件的操作都比较耗时

可以在 redis.conf 文件中进行 RDB 的相关配置:

# save <seconds> <changes> 表示如果在 seconds 秒内发生了 changes 次修改就进行一次 RDB
save 900 1
save 300 10
save 60 10000

# 是否开启压缩,压缩能减少磁盘占用,但是会消耗 CPU
rdbcompression yes

# RDB 文件名称
dbfilename dump.rdb

# RDB 文件保存路径
dir /var/lib/redis

6.2 AOF

AOF(Append-Only File)全称为追加文件,是 Redis 的另一种持久化机制。与 RDB 不同,AOF 并不是将整个数据集保存到磁盘,而是记录所有写操作命令,以日志形式追加到文件中。AOF 文件包含了一系列 Redis 命令,这些命令按顺序记录了数据集的变化过程。当 Redis 重启时,它会重新执行 AOF 文件中的命令,以恢复数据到之前的状态。这种方式保证了数据的持久性,并且可以提供更精确的数据恢复,因为它记录了每个写操作。

因为是记录命令,AOF 文件会比 RDB 文件大的多。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才有意义。因此 Redis 提供了 BGREWRITEAOF 命令,可以让 AOF 文件执行重写功能,用最少的命令达到相同效果。

AOF 默认是关闭的,可以在 redis.conf 文件中进行相关设置:

# 是否开启 AOF 功能,默认是 no
appendonly yes

# AOF 文件的名称
appendfilename "appendonly.aof"

# AOF 的记录频率
# 每执行一次写命令立即记录到 AOF 文件
appendfsync always
# 写命令执行完先放入 AOF 缓冲区,然后表示每隔 1 秒将缓冲区数据写到 AOF 文件,是默认方案
appendfsync everysec
# 写命令执行完先放入 AOF 缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

# AOF 文件比上次文件增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100

# AOF 文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

七、主从同步

Redis 主从同步主要分为全量同步增量同步

全量同步一般发生在从节点第一次连接主节点时,它用于建立主节点与从节点之间的初始数据关系。全量同步期间,主节点会通过 RDB 文件发送完整的数据集给从节点,后续命令则记录在 repl_baklog,依次发送给从节点。

增量同步一般发生在从节点断线重连后,从节点会提交自己的 offset 到主节点,主节点会获取 repl_baklog 中 offset 之后的命令给从节点进行同步。

同步过程中,主节点主要通过比较各自的 replidoffset 以确定是进行全量同步还是增量同步,以及从哪个数据状态开始同步:

  • replid(replication id)replid 是主节点的一个标识符,id 一致则说明是同一数据集。每一个主都有唯一的 replid,从节点会继承主节点的 replid

  • offsetoffset 用于表示数据偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。从完成同步时也会记录当前同步的 offset。如果从节点的 offset 小于主的 offset,说明从节点数据落后于主节点,需要更新。

八、哨兵

在这里插入图片描述
哨兵(Sentinel)的作用如下:

  • 监控:哨兵会通过心跳机制监测主节点和从节点的服务状态。
  • 故障恢复:如果主节点故障,哨兵会将一个从节点提升为主节点,并且当故障实例恢复后该节点也会一直作为主节点。
  • 通知:充当 Redis 客户端的服务发现来源,当集群发生故障时,会将最新信息推送给 Redis 客户端。

哨兵基于心跳机制监测服务状态,会每隔 1 秒向集群的每个实例发送 PING 命令:

  • 主观下线:如果某哨兵实例发现某实例未在规定时间响应,则认为该实例主观下线
  • 客观下线:若超过指定数量 quorum 的哨兵实例都认为该实例主观下线,则该实例客观下线。其中 quorum 的值最好超过哨兵实例数量的一半。

一旦发现主节点故障,哨兵需要选定一个从节点作为新的主节点,选择依据如下:

  • 首先会判断从节点与主节点断开时间长短,如果超过指定值(一般为下线时间阈值 down-after-milliseconds * 10)则会排除该从节点。
  • 然后判断从节点的 slave-priority 值,越小优先级越高,如果是 0 则永不参与选举。
  • 如果 slave-prority 一样,则判断从节点的 offset 值,越大说明数据越新,优先级越高。
  • 最后是判断从节点的运行 id 大小(运行 id 由从节点启动时 Redis 自动分配),越小优先级越高。

当选出一个新的主节点后,切换流程如下:

  • 哨兵给备选的从节点发送 SLAVEOF NO ONE命令,让该节点成为主节点。
  • 哨兵给所有其它从节点发送 SLAVEOF 主节点地址 主节点端口 命令,让这些从节点成为新的主节点的从节点,并开始从新的主节点上同步数据。
  • 最后,哨兵将故障节点标记为从节点,当故障节点恢复后会自动成为新的主节点的从节点。

九、Redis 底层数据结构

9.1 SDS

SDS(Simple Dynamic Strings)是 Redis 中的一种字符串表示方式,它是 Redis 自定义的字符串类型,用于代替 C 语言中的原生字符串。

以下为 8 位版本的 SDS 结构体定义,开头 8 位 len 保存实际存储的字节数,接下来 8 位 alloc 保存实际申请的总的字节数,这二者都不包括头部及结束标识。接下来是 flags 用于标识 SDS 类型,因为 SDS 除了 8 位版本还有 5、16、32、64 几种类型,之所以要进行分类也是为了更合理地使用内存空间,同时由于总共只有 5 类,因此只有低 3 位是有效位。

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

__attribute__ ((__packed__)) 是 GCC 编译器的扩展属性,它告诉编译器不要进行字节对齐。之所以不进行内存对齐是因为 SDS 结构体的指针不指向结构体开头的 len,而是位于 flagsbuf[] 之间(会在 SDS 初始化时由 SDS 内部完成偏移),这样一方面指针减 1 即能取到 flags 的值,另一方面也使得 SDS 能够兼容 C 语言中的部分字符串函数。如果进行内存对齐,flags 后面会被进行内存填充,不仅浪费了内存,还导致无法直接访问 flags

SDS 主要具有以下特点:

  • 动态大小:SDS 可以根据字符串的长度动态分配内存,而不像 C 原生字符串需要预分配固定大小的内存空间。这使得 SDS 更加灵活,能够节省内存
  • 缓冲区预分配:SDS 为字符串分配的内存总是多于实际字符串长度的,这样在字符串增长时,不需要频繁地重新分配内存,提高了性能
  • 二进制安全:SDS 存储了字符串的长度,因此不会对数据进行任何预处理或解释,而是将数据存储为原始字节序列。从而可以存储包括 \0 在内任意二进制数据,这意味着它不仅可以保存字符串,还可以存储图片、序列化对象等各种数据。
  • 获取字符串长度的复杂度为 O ( 1 ) O(1) O(1):SDS 维护了字符串的长度信息,因此可以在常数时间内获取字符串的长度,而不需要遍历整个字符串。
  • 修改字符串的复杂度为 O ( N ) O(N) O(N):在 SDS 中进行字符串的修改操作,如追加、删除、插入字符,其时间复杂度是线性的,与字符串的长度成正比。

9.2 IntSet

IntSet 是 Redis 中的一种专门用于存储整数值的数据结构,它是 Redis 的一个内部数据类型,能够保证元素唯一、有序。

以下为 IntSet 的结构体定义,其中 encoding 指定了编码方式,IntSet 支持存放 16 位、32 位、64 位三种整数编码方式,且支持升级操作,即当新的整数值无法用当前编码方式表示时,它会将整个数据集转换成更高位数的编码方式,这保证了 IntSet 的内部数据始终以最紧凑的方式存储。而为了方便查找,Redis 会将 IntSet 中所有的整数按照升序依次保存在 contents[] 中,并通过二分查找提高查询效率。

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

9.3 Dict

Dict 是 Redis 中用于存储键值对的数据结构,也被称为字典或哈希表,实现了键值对的快速存储和检索。

struct dictEntry {
    void *key; // 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; // 值
    struct dictEntry *next; /* Next entry in the same hash bucket. */
};

struct dict {
    dictType *type; // 内置的不同哈希函数

    dictEntry **ht_table[2]; // 两个二维 dictEntry 数组(指针指针数组),一个用于存储当前数据,另一个一般为空,只在 rehash 时用作临时存储
    unsigned long ht_used[2]; // 标识每个二维 dictEntry 数组中的 dictEntry 个数

    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /* Keep small vars at end for optimal (minimal) struct padding */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

Dict 还会根据负载因子动态扩容和收缩,并通过渐进式 rehash 来保证性能。渐进式 rehash 的流程如下:

  • 计算新 hash 表的 size,值取决于当前要做的是扩容还是收缩:
    • 如果是扩容,则新 size 为第一个大于等于 ht_used[0] + 1 2 n 2^n 2n
    • 如果是收缩,则新 size 为第一个大于等于 ht_used[0] 2 n 2^n 2n,但不能小于 4。
  • 按照新的 size 给 dict.ht_table[1] 申请内存空间。
  • 设置 dict.rehashidx = 0,标识开始 rehash。
  • 每次执行新增、查询、修改和删除操作时,都检查一下 dict.rehashidx 是否大于 -1,如果是则将 dict.ht_table[0][rehashidx] 的 entry 链表 rehash 到 dict.ht_table[1],并且将 rehashidx++。直至 dict.ht_table[0] 的所有数据都 rehash 到 dict.ht_table[1]
  • dict.ht_table[1] 赋值给 dict.ht_table[0]dict.ht_table[1] 置为空哈希表,释放原来的 dict.ht_table[0] 的内存。
  • rehashidx 赋值为 -1,代表 rehash 结束。
  • 在 rehash 过程中,新增操作直接写入 dict.ht_table[1],查询、修改和删除则会在 dict.ht_table[0]dict.ht_table[1] 中依次查找并执行。这样可以确保 dict.ht_table[0] 的数据只减不增,随着 rehash 最终为空。

9.4 ZipList 和 QuickList

ZipList 是一种特殊的双端链表,由一系列特殊编码的连续内存块组成。不支持随机存取,但是可以在任意一端进行压入与弹出操作。不过与普通的链表不同,ZipList 节点之间不是通过指针连接,而是通过在每个 entry 中记录上一节点和本节点的长度来寻址,因此内存占用较低,但如果列表数据过多,链表过长,可能会影响查询性能。此外,增或删较大数据时有可能发生连续更新问题。

ZipList 虽然节省内存,但申请内存必须是连续空间,如果内存占用较多,申请内存效率很低。因此,Redis 在 3.2 版本引入了新的数据结构 QuickList,它是一个双端链表,但链表中的每个节点都是一个 ZipList。

ZipList 结构如下:

在这里插入图片描述

属性类型长度用途
zlbytesuint32_t4字节记录整个压缩列表占用的内存字节数。
zltailuint32_t4字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址。
zllenuint16_t2字节记录了压缩列表包含的节点数量。最大值为 UINT16_MAX(65534),如果超过这个值,此处会记录为 65535,但节点的真实数量需要遍历整个压缩列表才能计算得出。
entry列表节点不固定压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlenduint8_t1字节特殊值 0xFF(十进制 255),用于标记压缩列表的末端。

9.5 SkipList

Redis 中的 SkipList(跳表)首先是双向链表,但所有元素按照 score 值升序存储,同时每个节点可能包含多个指针,每个指针跨度不同。

typedef struct zskiplistNode {
    sds ele; // 节点存储的值
    double score; // 节点的分数,主要用来排序和查找
    struct zskiplistNode *backward; // 前一个节点的指针
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 下一个节点的指针
        unsigned long span; // 索引跨度
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头尾节点指针
    unsigned long length; // 节点数量
    int level; // 最大的索引层级
} zskiplist;

SkipList 结构如下:

在这里插入图片描述

9.6 RedisObject

Redis 中所有的键和值等数据对象都会被封装为一个 RedisObject,它里面包含了数据类型、编码方式、值以及一些其他信息,以便 Redis 能够正确地管理和操作这些数据对象。

struct redisObject {
    unsigned type:4; // 对象类型,分别是 string、hash、list、set 和 zset,占 4 位
    unsigned encoding:4; // 底层编码方式,共有 11 种,占 4 位
    unsigned lru:LRU_BITS; // 该对象最后一次被访问的时间,便于淘汰 key,低 8 位表示频率,高 16 位表示访问时间
    int refcount; // 对象引用计数器,计数器为 0 表明无人引用,可以被回收
    void *ptr; // 指向实际数据的指针
};

Redis 会根据存储的数据类型不同,选择不同的编码方式,累计 11 种:

编号编码方式说明
0OBJ_ENCODING_RAWraw 编码动态字符串
1OBJ_ENCODING_INTlong 类型的整数的字符串
2OBJ_ENCODING_HThash 表(字典 dict)
3OBJ_ENCODING_ZIPMAP已废弃
4OBJ_ENCODING_LINKEDLIST双端链表
5OBJ_ENCODING_ZIPLIST压缩列表
6OBJ_ENCODING_INTSET整数集合
7OBJ_ENCODING_SKIPLIST跳表
8OBJ_ENCODING_EMBSTRembstr 的动态字符串
9OBJ_ENCODING_QUICKLIST快速列表
10OBJ_ENCODING_STREAMstream 流

五大数据类型与编码方式的对应关系如下:

数据类型编码方式
OBJ_STRINGINT、EMBSTR 和 RAW
OBJ_LISTLinkedList 和 QuickList(3.2以后)
OBJ_SETIntSet 和 HT
OBJ_ZSETZipList、HT 和 SkipList
OBJ_HASHZipList 和 HT

十、Redis 基础数据结构

10.1 String

String 的底层实现主要分以下三种情况:

  • 默认编码方式是 RAW,基于简单动态字符串 SDS 实现,存储上限为 512 MB。
  • 如果存储的 SDS 长度小于等于 44 字节(见下图),则会采用 EMBSTR 编码,此时 RedisObject 与 SDS 是一段连续空间,申请内存时只需要调用一次内存分配函数(jemalloc 以 2 n 2^n 2n 进行内存分配),效率更高且不容易产生内存分配。
  • 如果存储的字符串是整数值,并且大小在 LONG_MAX 范围内,则会采用 INT 编码,直接将数据保存在 RedisObject 的 ptr 指针位置(刚好 8 字节),不再需要 SDS 了。

在这里插入图片描述

10.2 List

在 3.2 版本之前,Redis 采用 ZipList 和 LinkedList 来实现 List,当元素数量小于 512 并且元素大小小于 64 字节时采用 ZipList 编码,超过则采用 LinkedList 编码。在3.2版本之后,Redis 统一采用 QuickList 来实现 List。

在这里插入图片描述

10.3 Set

Set 不保证元素有序,但保证元素唯一,查询效率要求极高。为此,Set 默认采用 HT 编码,Dict 中的 key 用来存储元素,value 统一为 null。不过 Dict 内存并不连续,因此当存储的所有数据都是整数,并且元素数量不超过 set-max-intset-entries 时,Set 会采用 IntSet 编码以节省内存,并通过二分查找保证元素唯一。

10.4 ZSet

ZSet 底层数据结构必须满足键值存储、键必须唯一以及可排序,因此 Redis 默认通过 SkipList + HT 的方式实现 ZSet(见下图)。

在这里插入图片描述

不过 SkipList + HT 的编码方式相当于存储了两份数据,比较耗费内存,因此在满足以下两个条件时 ZSet 会采用 ZipList 实现:

  • 元素数量小于 zset_max_ziplist_entries,默认值 128。
  • 每个元素都小于 zset_max_ziplist_value 字节,默认值 64。

不过 ZipList 本身没有排序功能,而且没有键值对的概念,因此需要额外进行以下处理:

  • ZipList 是连续内存,因此 scoreelement 是一前一后紧挨在一起的两个 entry
  • score 越小越接近队首,score 越大越接近队尾,按照 score 值升序排列。

10.5 Hash

Hash 的实现方式与有序集合 ZSet 基本相同。对于较小的数据集,为了节省内存,可以采用 ZipList 的方式存储。而对于较大的数据集或元素较大的情况,会直接采用哈希表 Dict 的方式存储。不过,由于哈希表不需要对元素进行排序,因此无需使用 SkipList。


十一、分布式锁

11.1 简单分布式锁

简单的分布式锁的加锁操作通过 SET <key> <value> NX PX <有效期> 实现,解锁操作通过以下 Lua 脚本实现:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

需要注意的是,这里的 value 必需保证唯一,并在删除前进行判断,从而保证加锁和解锁的同源性,一般情况下可以采用 UUID + 线程标识。

11.2 Redisson 的 RedissonLock

上述简单分布式锁存在不可重入、不可重试与锁超时自动释放的问题,Redisson 的 RedissonLock 能够在单 Redis 实例的情况下有效解决这些问题。

RedissonLock 的主要特点及实现原理如下:

  • 可重入:分布式锁采用 Hash 结构实现,记录了线程标识和重入次数,并在加锁和解锁时通过 Lua 脚本对可重入次数进行判断和处理。
  • 可重试:利用 Future 和发布订阅(pub/sub)实现获取锁失败时的重试机制,并通过等待与唤醒避免了 CPU 的浪费。
  • 超时续约:当 tryLock() 函数 leaseTime 参数使用默认值时,利用 watch dog 机制,通过定时任务每隔 releaseTime / 3 时间重置超时时间,并在锁释放时取消定时任务。

11.3 Redisson 的 RedissonMultiLock 和 RedissonRedLock

普通 RedissonLock 的局限性在于仅适用于单实例 Redis。对于 Redis 集群,在主节点上加锁后,由于 Redis 主从同步是异步的,主节点如果在将加锁信息转移到从节点之前宕机,就可能会导致并发问题。

RedissonMultiLock 联锁可以同时操作多个锁,以达到对多个锁进行统一管理的目的。联锁的操作是原子性的,即要么全部锁住,要么全部解锁。这样可以保证多个锁的一致性。

RedissonRedLock 继承自联锁,主要区别在于它认为获取到 n / 2 + 1 个锁即为获取成功,从而提高了 Redis 集群的可用性。不过解锁时仍然会在每一个节点上执行解锁操作,以保证所有的锁都能够被释放。


参考:

http://doc.redisfans.com/index.html
https://www.xiaolincoding.com/redis

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值