《Redis深度历险》读书笔记

文章目录

  • 1. Redis 的用途
  • 2. 字符串的实现
  • 3. 列表list
    • 3.1. quicklist
  • 4. 字典hash
    • 4.1. rehash
  • 5. 对象的过期时间
  • 6. 应用:分布式锁
  • 7. 应用:限流(zset,漏斗限流)
  • 8. 使用标准结构存储的阈值
  • 9. Redis分布式与CAP定理
  • 10. Sentinel
  • 11. 集群
    • 11.1. Codis
    • 11.2. Redis-Cluster的实现
  • 12. Info详解
    • 12.1. 常见info
  • 13. 过期策略
    • 13.1. 定时扫描策略
    • 13.2. 从库的过期策略
  • 14. 超出内存的策略-LRU
  • 15. 懒惰删除-Redis的多线程
  • 16. listpack的结构

1. Redis 的用途

  1. 缓存
  2. 分布式锁
  3. 排行榜

2. 字符串的实现

当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间,字符串最大长度为 512M。

如果是个整数,那么可以对其增减,范围是signed long的最大最小值,超过会报错。

如果字符串长度小于44时且不是整数类型或整数大于signed long类型时,会使用embstr编码,因为只需要一次malloc,减少内存碎片。

3. 列表list

3.1. quicklist

在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,它将所有的元素紧挨着一起存储,分配的是一块连续的内存。。

列表元素较多时采用quicklist,因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

quicklist就相当于块状链表一样,将多个ziplist串起来。

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 算法压缩深度
    ...
}

quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个ziplist。ziplist 的长度由配置参数 list-max-ziplist-size 决定

4. 字典hash

Redis 的字典的值只能是字符串。

struct dict {
 ...
 dictht ht[2];
}

一个字典结构包含2个hashtable,是为了扩容缩容时的渐进式rehash。

4.1. rehash

Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当 hash 移除了最后一个元素之后,该数据结构自动被删除

5. 对象的过期时间

Redis 所有的数据结构都可以设置过期时间,且是以对象为单位。

如果一个字符串已经设置了过期时间,然后调用set 方法修改了它,它的过期时间会消失。

6. 应用:分布式锁

占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑

要想完美解决,要使用Redlock 算法。不使用可能因为集群情况下主从复制时主节点宕机,从节点没有及时收到数据而出现同一把锁被多次获取。

7. 应用:限流(zset,漏斗限流)

  1. zset限流,score和value为时间,将时间窗口之外的score和value删除,统计剩下的数量,超过就要限流

  2. 漏斗限流

8. 使用标准结构存储的阈值

Redis 规定在小对象存储结构的限制条件如下:

  1. hash-max-zipmap-entries 512 # hash 的元素个数超过 512 就必须用标准结构存储
  2. hash-max-zipmap-value 64 # hash 的任意元素的 key/value 的长度超过 64 就必须用标准结构存储
  3. list-max-ziplist-entries 512 # list 的元素个数超过 512 就必须用标准结构存储
  4. list-max-ziplist-value 64 # list 的任意元素的长度超过 64 就必须用标准结构存储
  5. zset-max-ziplist-entries 128 # zset 的元素个数超过 128 就必须用标准结构存储
  6. zset-max-ziplist-value 64 # zset 的任意元素的长度超过 64 就必须用标准结构存储
  7. set-max-intset-entries 512 # set 的整数元素个数超过 512 就必须用标准结构存储

9. Redis分布式与CAP定理

一句话概括 CAP 原理就是——网络分区发生时,一致性可用性两难全。

分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇叫着「网络分区」。

在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的「一致性」将无法满足,因为两个分布式节点的数据不再保持一致。除非我们牺牲「可用性」,也就是暂停分布式节点服务,在网络分区发生时,不再提供修改数据的功能,直到网络状况完全恢复正常再继续对外提供服务

Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求

当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。

Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。

Redis 的复制是异步进行的,wait 指令可以让异步复制变身同步复制,确保系统的强一
致性 (不严格)。wait 指令是 Redis3.0 版本以后才出现的

10. Sentinel

Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以
限制主从延迟过大。

min-slaves-to-write 1 # 表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性
min-slaves-max-lag 10  # 如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常

11. 集群

11.1. Codis

Codis 是 Redis 集群方案之一。Codis 使用 Go 语言开发,它是一个代理中间件,它和 Redis 一样也使用 Redis 协议对外提供服务,当客户端向 Codis 发送指令时,Codis 负责将指令转发到后面的 Redis 实例来执行,并将返回结果再转回给客户端。

Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进行 crc32 运算计算哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。每个槽位都会唯一映射到后面的多个 Redis 实例之一。

11.2. Redis-Cluster的实现

将所有数据划分为 16384 的 slots

Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态

12. Info详解

Info 指令显示的信息分为 9 大块:

  1. Server 服务器运行的环境参数
  2. Clients 客户端相关信息
  3. Memory 服务器运行内存统计数据
  4. Persistence 持久化信息
  5. Stats 通用统计数据
  6. Replication 主从复制相关信息
  7. CPU CPU 使用情况
  8. Cluster 集群信息
  9. KeySpace 键值对统计数量信息

12.1. 常见info

  • instantaneous_ops_per_sec:每秒执行多少次指令info stat

  • 占用内存info memory

    used_memory_human:827.46K # 内存分配器 (jemalloc) 从操作系统分配的内存总量
    used_memory_rss_human:3.61M # 操作系统看到的内存占用 ,top 命令看到的内存
    used_memory_peak_human:829.41K # Redis 内存消耗的峰值
    used_memory_lua_human:37.00K # lua 脚本引擎占用的内存大小
    
  • repl_backlog_size:1048576 :积压缓冲区大小info replication

13. 过期策略

13.1. 定时扫描策略

Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是
采用了一种简单的贪心策略如下,同时扫描时间的上限,默认不会超过 25ms。

  1. 从过期字典中随机 20 个 key;
  2. 删除这 20 个 key 中已经过期的 key;
  3. 如果过期的 key 比率超过 1/4,那就重复步骤 1;

但是如果所有的key同一时间过期,就会导致Redis持续扫描过期字典,这会导致卡顿。

也许你会争辩说“扫描不是有 25ms 的时间上限了么,怎么会导致卡顿呢”?这里打个比方,假如有 101 个客户端同时将请求发过来了,然后前 100 个请求的执行时间都是25ms,那么第 101 个指令需要等待多久才能执行?2500ms,这个就是客户端的卡顿时间,是由服务器不间断的小卡顿积少成多导致的。

如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。

# 在目标过期时间上增加一天的随机时间
redis.expire_at(key, random.randint(86400) + expire_ts)

13.2. 从库的过期策略

从库不会进行过期扫描,主库在key到期时,会在AOF文件里增加一条del指令,同步到所有从库。由于指令是异步进行的,所以会出现主从不一致。

14. 超出内存的策略-LRU

当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。

在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。

当实际内存超出 maxmemory 时,有如下策略:

  1. noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
  2. volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
  3. volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
  4. volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
  5. allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
  6. allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。

volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。

15. 懒惰删除-Redis的多线程

一直以来我们认为 Redis 是单线程的,单线程为 Redis 带来了代码的简洁性和丰富多样的数据结构。不过 Redis 内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作。

del大对象时会出现卡顿,为了解决这个问题,4.0版引入了unlink指令,对删除操作懒处理,丢给后台线程异步回收内存。不是所有的 unlink 操作都会延后处理,如果对应 key 所占用的内存很小,则跟del指令一样。

同样的还有flushdb、flushall指令,在后面加async参数即可丢给后台线程处理。

16. listpack的结构

listpack没有级联更新的行为,设计是用来取代ziplist的,目前只用在了Stream的数据结构中。

元素结构根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;
}

你可能感兴趣的:(中间件及编程工具)