Redis 深度历险: 核心原理和应用实践3

目录

Sentinel 基本使用 集群 1:李代桃僵 —— Sentinel 

集群 2:分而治之 —— Codis 

集群 3:众志成城 —— Cluster 

拓展 1:耳听八方 —— Stream 

拓展 2:无所不知 —— Info 指令

拓展 3:拾遗漏补 —— 再谈分布式锁

拓展 4:朝生暮死 —— 过期策略

拓展 5:优胜劣汰 —— LRU 

拓展 6:平波缓进 —— 懒惰删除 

拓展 7:妙手仁心 —— 优雅地使用 Jedis

拓展 8:居安思危 —— 保护 Redis 

极度深寒 几节放弃!!!


Sentinel 基本使用 集群 1:李代桃僵 —— Sentinel 

目前我们讲的 Redis 还只是主从方案,最终一致性。读者们可思考过,如果主节点凌晨 
3 点突发宕机怎么办?就坐等运维从床上爬起来,然后手工进行从主切换,再通知所有的程
序把地址统统改一遍重新上线么?毫无疑问,这样的人工运维效率太低,事故发生时估计得
至少 1 个小时才能缓过来。如果是一个大型公司,这样的事故足以上新闻了Redis 深度历险: 核心原理和应用实践3_第1张图片

所以我们必须有一个高可用方案来抵抗节点故障,当故障发生时可以自动进行从主切
换,程序可以不用重启,运维可以继续睡大觉,仿佛什么事也没发生一样。Redis 官方提供
了这样一种方案 —— Redis Sentinel(哨兵)

Redis 深度历险: 核心原理和应用实践3_第2张图片


 我们可以将 Redis Sentinel 集群看成是一个 ZooKeeper 集群,它是集群高可用的心脏,
它一般是由 3~5 个节点组成,这样挂了个别节点集群还可以正常运转

它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为
主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,
然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地
址,sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节
点切换。
比如上图的主节点挂掉后,集群将可能自动调整为下图所示结构。 

Redis 深度历险: 核心原理和应用实践3_第3张图片

从这张图中我们能看到主节点挂掉了,原先的主从复制也断开了,客户端和损坏的主节
点也断开了。从节点被提升为新的主节点,其它从节点开始和新的主节点建立复制关系。客
户端通过新的主节点继续进行交互。Sentinel 会持续监控已经挂掉了主节点,待它恢复后,
集群会调整为下面这张图。 

Redis 深度历险: 核心原理和应用实践3_第4张图片

此时原先挂掉的主节点现在变成了从节点,从新的主节点那里建立复制关系

消息丢失

Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消
息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别
多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以
限制主从延迟过大。

min-slaves-to-write 1

min-slaves-max-lag 10 

第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服
务,丧失可用性。 
何为正常复制,何为异常复制?这个就是由第二个参数控制的,它的单位是秒,表示如
果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没
有给反馈。

Sentinel 基本使用 

有个问题是,但 sentinel 进行主从切换时,客户端如何知道地址变更了 ? 通过分析源
码,我发现 redis-py 在建立连接的时候进行了主库地址变更判断。 
连接池建立新连接时,会去查询主库地址,然后跟内存中的主库地址进行比对,如果变
更了,就断开所有连接,重新使用新地址建立新连接。如果是旧的主库挂掉了,那么所有正
在使用的连接都会被关闭,然后在重连时就会用上新地址

集群 2:分而治之 —— Codis 

在大数据高并发场景下,单个 Redis 实例往往会显得捉襟见肘。首先体现在内存上,单
个 Redis 的内存不宜过大,内存太大会导致 rdb 文件过大,进一步导致主从同步时全量同
步时间过长,在实例重启恢复时也会消耗很长的数据加载时间,特别是在云环境下,单个实
例内存往往都是受限的。其次体现在 CPU 的利用率上,单个 Redis 实例只能利用单个核
心,这单个核心要完成海量数据的存取和管理工作压力会非常大

Codis 是 Redis 集群方案之一,令我们感到骄傲的是,它是中国人开发并开源的,来自
前豌豆荚中间件团队。有了 
Codis 技术积累之后,项目「突头人」刘奇又开发出来中国人自己的开源分布式数据库 —— 
TiDB,可以说 6 到飞起。 

Redis 深度历险: 核心原理和应用实践3_第5张图片

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

Codis 上挂接的所有 Redis 实例构成一个 Redis 集群,当集群空间不足时,可以通过动
态增加 Redis 实例来实现扩容需求

因为 Codis 是无状态的,它只是一个转发代理中间件,这意味着我们可以启动多个 
Codis 实例,供客户端使用,每个 Codis 节点都是对等的。因为单个 Codis 代理能支撑的 
QPS 比较有限,通过启动多个 Codis 代理可以显著增加整体的 QPS 需求,还能起到容灾功
能,挂掉一个 Codis 代理没关系,还有很多 Codis 代理可以继续服务。 Redis 深度历险: 核心原理和应用实践3_第6张图片

Codis 分片原理 

Codis 要负责将特定的 key 转发到特定的 Redis 实例,那么这种对应关系 Codis 是如
何管理的呢? 
Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进
行 crc32 运算计算哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余
数,这个余数就是对应 key 的槽位。 

扩容 
刚开始 Codis 后端只有一个 Redis 实例,1024 个槽位全部指向同一个 Redis。然后一
个 Redis 实例内存不够了,所以又加了一个 Redis 实例。这时候需要对槽位关系进行调整,
将一半的槽位划分到新的节点。这意味着需要对这一半的槽位对应的所有 key 进行迁移,迁
移到新的 Redis 实例

那 Codis 如果找到槽位对应的所有 key 呢? 
Codis 对 Redis 进行了改造,增加了 SLOTSSCAN 指令,可以遍历指定 slot 下所有的 
key。Codis 通过 SLOTSSCAN 扫描出待迁移槽位的所有的 key,然后挨个迁移每个 key 到
新的 Redis 节点。 
在迁移过程中,Codis 还是会接收到新的请求打在当前正在迁移的槽位上,因为当前槽
位的数据同时存在于新旧两个槽位中,Codis 如何判断该将请求转发到后面的哪个具体实例
呢? 
Codis 无法判定迁移过程中的 key 究竟在哪个实例中,所以它采用了另一种完全不同的
思路。当 Codis 接收到位于正在迁移槽位中的 key 后,会立即强制对当前的单个 key 进行
迁移,迁移完成后,再将请求转发到新的 Redis 实例

自动均衡 

Redis 新增实例,手工均衡 slots 太繁琐,所以 Codis 提供了自动均衡功能。自动均衡会
在系统比较空闲的时候观察每个 Redis 实例对应的 Slots 数量,如果不平衡,就会自动进行
迁移

Codis 的代价

Codis 给 Redis 带来了扩容的同时,也损失了其它一些特性。因为 Codis 中所有的 key 
分散在不同的 Redis 实例中,所以事务就不能再支持了,事务只能在单个 Redis 实例中完
成。同样 rename 操作也很危险,它的参数是两个 key,如果这两个 key 在不同的 Redis 实
例中,rename 操作是无法正确完成的。Codis 的官方文档中给出了一系列不支持的命令列

同样为了支持扩容,单个 key 对应的 value 不宜过大,因为集群的迁移的最小单位是 
key,对于一个 hash 结构,它会一次性使用 hgetall 拉取所有的内容,然后使用 hmset 放置
到另一个节点。如果 hash 内部的 kv 太多,可能会带来迁移卡顿。官方建议单个集合结构
的总字节容量不要超过 1M。如果我们要放置社交关系数据,例如粉丝列表这种,就需要注
意了,可以考虑分桶存储,在业务上作折中。 
Codis 因为增加了 Proxy 作为中转层,所有在网络开销上要比单个 Redis 大,毕竟数据
包多走了一个网络节点,整体在性能上要比单个 Redis 的性能有所下降。但是这部分性能损
耗不是太明显,可以通过增加 Proxy 的数量来弥补性能上的不足。 
Codis 的集群配置中心使用 zk 来实现,意味着在部署上增加了 zk 运维的代价,不过
大部分互联网企业内部都有 zk 集群,可以使用现有的 zk 集群使用即可。 

MGET 指令的操作过程

Redis 深度历险: 核心原理和应用实践3_第7张图片

架构变迁 

Codis 作为非官方 Redis 集群方案,近几年来它的结构一直在不断变化,一方面当官方
的 Redis 有变化的时候它要实时去跟进,另一方面它作为 Redis Cluster 的竞争方案之一,
它还得持续提高自己的竞争力,给自己增加更多的官方集群所没有的便捷功能。 
比如 Codis 有个特色的地方在于强大的 Dashboard 功能,能够便捷地对 Redis 集群进
行管理。这是 Redis 官方所欠缺的。另外 Codis 还开发了一个 Codis-fe(federation 联邦) 工
具,可以同时对多个 Codis 集群进行管理。在大型企业,Codis 集群往往会有几十个,有这
样一个便捷的联邦工具可以降低不少运维成本。

Codis 的尴尬

Codis 不是 Redis 官方项目,这意味着它的命运会无比曲折,它总是要被官方 Redis 牵
着牛鼻子走。当 Redis 官方提供了什么功能它欠缺时,Codis 就会感到恐惧,害怕自己被市
场甩掉,所以必须实时保持跟进。官方对重视内核,对工具无暇顾及,只提供基本的工具,其它完全交给第三方去开
发。 

集群 3:众志成城 —— Cluster 

RedisCluster 是 Redis 的亲儿子,它是 Redis 作者自己提供的 Redis 集群化方案。 相对于 Codis 的不同,它是去中心化的,如图所示,该集群有三个 Redis 节点组成,
每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相
互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。 

客户端为了可以直接定位某个具体的 key 所在的节点,它就需要缓存槽位相关信息,这样才可以准确快速地定位到相应的节点。同时因为槽位的信息可能会存在客户端与服务器不
一致的情况,还需要纠正机制来实现槽位信息的校验调整。

跳转

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自
己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端
去连这个节点去获取数据

客户端收到 MOVED 指令后,要立即纠正本地的槽位映射表。后续所有 key 将使用新
的槽位映射表

迁移 

Redis Cluster 提供了工具 redis-trib 可以让运维人员手动调整槽位的分配情况,它使用 
Ruby 语言进行开发,通过组合各种原生的 Redis Cluster 指令来实现。这点 Codis 做的更加
人性化,它不但提供了 UI 界面可以让我们方便的迁移,还提供了自动化平衡槽位工具,无
需人工干预就可以均衡集群负载。不过 Redis 官方向来的策略就是提供最小可用的工具,其
它都交由社区完成
容错 
Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其
中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完
全处于不可用状态。不过 Redis 也提供了一个参数 cluster-require-full-coverage 可以允许部分
节点故障,其它节点还可以继续提供对外访问

网络抖动

真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如
网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正
常。 
为解决这种问题,Redis Cluster 提供了一种选项 cluster-node-timeout,表示当某个节点持
续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个
选项,网络抖动会导致主从频繁切换 (数据的重新复制)。 
还有另外一个选项 cluster-slave-validity-factor 作为倍乘系数来放大这个超时时间来宽松容
错的紧急程度。如果这个系数为零,那么主从切换是不会抗拒网络抖动的。如果这个系数大
于 1,它就成了主从切换的松弛系数。 

 可能下线 (PFAIL-Possibly Fail) 与确定下线 (Fail) 
因为 Redis Cluster 是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都
认为它失联了。所以集群还得经过一次协商的过程,只有当大多数节点都认定了某个节点失
联了,集群才认为该节点需要进行主从切换来容错。 
Redis 集群节点采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变。比
如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可
以收到这点失联信息。如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集
群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节
点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换

拓展 1:耳听八方 —— Stream 

Redis5.0 被作者 Antirez 突然放了出来,增加了很多新的特色功能。而 Redis5.0 最大的
新特性就是多出了一个数据结构 Stream,它是一个新的强大的支持多播的可持久化的消息队
列,作者坦言 Redis Stream 狠狠地借鉴了 Kafka 的设计Redis 深度历险: 核心原理和应用实践3_第8张图片

Redis Stream 的结构如上图所示,它有一个消息链表,将所有加入的消息都串起来,每
个消息都有一个唯一的 ID 和对应的内容。消息是持久化的,Redis 重启后,内容还在。 
每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消
息时自动创建。 
每个 Stream 都可以挂多个消费组,每个消费组会有个游标 last_delivered_id 在 Stream 
数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个 Stream 
内唯一的名称,消费组不会自动创建,它需要单独的指令 xgroup create 进行创建,需要指定
从 Stream 的某个消息 ID 开始消费,这个 ID 用来初始化 last_delivered_id 变量。 
每个消费组 (Consumer Group) 的状态都是独立的,相互不受影响。也就是说同一份 
Stream 内部的消息会被每个消费组都消费到。 

同一个消费组 (Consumer Group) 可以挂接多个消费者 (Consumer),这些消费者之间是
竞争关系,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。每个消费者有
一个组内唯一名称

消费者 (Consumer) 内部会有个状态变量 pending_ids,它记录了当前已经被客户端读取
的消息,但是还没有 ack。如果客户端没有 ack,这个变量里面的消息 ID 会越来越多,一
旦某个消息被 ack,它就开始减少。这个 pending_ids 变量在 Redis 官方被称之为 PEL,也
就是 Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一
次,而不会在网络传输的中途丢失了没处理。 

消息 ID 

消息 ID 的形式是 timestampInMillis-sequence,例如 1527846880572-5,它表示当前的消
息在毫米时间戳 1527846880572 时产生,并且是该毫秒内产生的第 5 条消息。消息 ID 可以
由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面
加入的消息的 ID 要大于前面的消息 ID

消息内容 
消息内容就是键值对,形如 hash 结构的键值对,这没什么特别之处

增删改查
    1、xadd 追加消息 
    2、xdel 删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度 
    3、xrange 获取消息列表,会自动过滤已经删除的消息 
    4、xlen 消息长度 
    5、del 删除 Stream 

Redis 深度历险: 核心原理和应用实践3_第9张图片

独立消费 

我们可以在不定义消费组的情况下进行 Stream 消息的独立消费,当 Stream 没有新消
息时,甚至可以阻塞等待。Redis 设计了一个单独的消费指令 xread,可以将 Stream 当成普
通的消息队列 (list) 来使用。使用 xread 时,我们可以完全忽略消费组 (Consumer Group) 
的存在,就好比 Stream 就是一个普通的列表 (list)。

Redis 深度历险: 核心原理和应用实践3_第10张图片

客户端如果想要使用 xread 进行顺序消费,一定要记住当前消费到哪里了,也就是返回
的消息 ID。下次继续调用 xread 时,将上次返回的最后一个消息 ID 作为参数传递进去,
就可以继续消费后续的消息。 
block 0 表示永远阻塞,直到消息到来,block 1000 表示阻塞 1s,如果 1s 内没有任何
消息到来,就返回 nil。 
 
127.0.0.1:6379> xread block 1000 count 1 streams codehole $ (nil) (1.07s) 
创建消费组 

Redis 深度历险: 核心原理和应用实践3_第11张图片

Redis 深度历险: 核心原理和应用实践3_第12张图片

Redis 深度历险: 核心原理和应用实践3_第13张图片

消费 

Stream 提供了 xreadgroup 指令可以进行消费组的组内消费,需要提供消费组名称、消
费者名称和起始消息 ID。它同 xread 一样,也可以阻塞等待新消息。读到新消息后,对应
的消息 ID 就会进入消费者的 PEL(正在处理的消息) 结构里,客户端处理完毕后使用 xack 
指令通知服务器,本条消息已经处理完毕,该消息 ID 就会从 PEL 中移除。Redis 深度历险: 核心原理和应用实践3_第14张图片

Redis 深度历险: 核心原理和应用实践3_第15张图片

Stream 消息太多怎么办?

Redis 自然考虑到了这一点,所以它提供了一个定长 Stream 功能。在 xadd 的指令提供
一个定长长度 maxlen,就可以将老的消息干掉,确保最多不超过指定长度

Redis 深度历险: 核心原理和应用实践3_第16张图片

消息如果忘记 ACK 会怎样?

Stream 在每个消费者结构中保存了正在处理中的消息 ID 列表 PEL,如果消费者收到
了消息处理完了但是没有回复 ack,就会导致 PEL 列表不断增长,如果有很多消费组的
话,那么这个 PEL 占用的内存就会放大。

拓展 2:无所不知 —— Info 指令

在使用 Redis 时,时常会遇到很多问题需要诊断,在诊断之前需要了解 Redis 的运行状
态,通过强大的 Info 指令,你可以清晰地知道 Redis 内部一系列运行参数。 
Info 指令显示的信息非常繁多,分为 9 大块,每个块都有非常多的参数,这 9 个块分
别是: 

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

Redis 每秒执行多少次指令

这个信息在 Stats 块里,可以通过 info stats 看到\

Redis 深度历险: 核心原理和应用实践3_第17张图片

Redis 连接了多少客户端

Redis 深度历险: 核心原理和应用实践3_第18张图片

这个信息也是比较有用的,通过观察这个数量可以确定是否存在意料之外的连接。如果
发现这个数量不对劲,接着就可以使用 client list 指令列出所有的客户端链接地址来确定源
头。 
关于客户端的数量还有个重要的参数需要观察,那就是 rejected_connections,它表示因
为超出最大连接数限制而被拒绝的客户端连接次数,如果这个数字很大,意味着服务器的最
大连接数设置的过低需要调整 maxclients 参数。 > redis-cli info stats |grep reject rejected_connections:0 

Redis 内存占用多大 ? 

这个信息在 Memory 块里,可以通过 info memory 看到。 > redis-cli info memory | grep used | grep human 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 脚本引擎占用的内存大小 
 
如果单个 Redis 内存占用过大,并且在业务上没有太多压缩的空间的话,可以考虑集群
化了

复制积压缓冲区多大? 

这个信息在 Replication 块里,可以通过 info replication 看到。 > redis-cli info replication |grep backlog repl_backlog_active:0 repl_backlog_size:1048576  # 这个就是积压缓冲区大小 repl_backlog_first_byte_offset:0 
repl_backlog_histlen:0 

复制积压缓冲区大小非常重要,它严重影响到主从复制的效率。当从库因为网络原因临
时断开了主库的复制,然后网络恢复了,又重新连上的时候,这段断开的时间内发生在 
master 上的修改操作指令都会放在积压缓冲区中,这样从库可以通过积压缓冲区恢复中断的
主从同步过程

积压缓冲区是环形的,后来的指令会覆盖掉前面的内容。如果从库断开的时间过长,或
者缓冲区的大小设置的太小,都会导致从库无法快速恢复中断的主从同步过程,因为中间的
修改指令被覆盖掉了。这时候从库就会进行全量同步模式,非常耗费 CPU 和网络资源

如果有多个从库复制,积压缓冲区是共享的,它不会因为从库过多而线性增长。如果实
例的修改指令请求很频繁,那就把积压缓冲区调大一些,几十个 M 大小差不多了,如果很
闲,那就设置为几个 M。 > redis-cli info stats | grep sync sync_full:0 sync_partial_ok:0 sync_partial_err:0  # 半同步失败次数 
 
通过查看 sync_partial_err 变量的次数来决定是否需要扩大积压缓冲区,它表示主从半同
步复制失败的次数。

拓展 3:拾遗漏补 —— 再谈分布式锁

在第三节,我们细致讲解了分布式锁的原理,它的使用非常简单,一条指令就可以完成
加锁操作。不过在集群环境下,这种方式是有缺陷的,它不是绝对安全的。 
比如在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感
知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节
点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当
另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户
端同时持有,不安全性由此产生
Redis 深度历险: 核心原理和应用实践3_第19张图片

 
 不过这种不安全也仅仅是在主从发生 failover 的情况下才会产生,而且持续时间极短,
业务系统多数情况下可以容忍

Redlock 算法

为了使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。
同很多分布式算法一样,redlock 也使用「大多数机制」。 
加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点 set 
成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还
需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读
写,意味着相比单实例 Redis 性能会下降一些

Redlock 使用场景 

如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,那就应该考虑 redlock。不
过代价也是有的,需要更多的 redis 实例,性能也下降了,代码上还需要引入额外的 
library,运维上也需要特殊对待,这些都是需要考虑的成本,使用前请再三斟酌

拓展 4:朝生暮死 —— 过期策略


Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。你可以想象 
Redis 内部有一个死神,时刻盯着所有设置了过期时间的 key,寿命一到就会立即收割。 
 
你还可以进一步站在死神的角度思考,会不会因为同一时间太多的 key 过期,以至于忙
不过来。同时因为 Redis 是单线程的,收割的时间也会占用线程的处理时间,如果收割的太
过于繁忙,会不会导致线上读写指令出现卡顿

过期的 key 集合 
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个
字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓
惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期
了就立即删除。定时删除是集中处理,惰性删除是零散处理

定时扫描策略 
Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是
采用了一种简单的贪心策略

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

同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时
间的上限,默认不会超过 25ms

业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置
一个随机范围,而不能全部在同一时间过期。

从库的过期策略 

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 
文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 
key 

拓展 5:优胜劣汰 —— LRU 

当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。
交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率
基本上等于不可用。 
在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提
供了配置参数 maxmemory 来限制内存超出期望大小。 
当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让
用户自己决定该如何腾出新的空间以继续提供读写服务。

 noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样
可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。 
 volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过
期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。 
 volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 
越小越优先被淘汰。 
 volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。 
 allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不
只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。 
 allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。 
 volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 
key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时
不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 
策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘

LRU 算法

实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照
一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问
时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时
间顺序。 
位于链表尾部的元素就是不被重用的元素,所以会被踢掉。位于表头的元素就是最近刚
刚被人用过的元素,所以暂时不会被踢。

拓展 6:平波缓进 —— 懒惰删除 

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

Redis 为什么要懒惰删除(lazy free)

删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延
迟。不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,那么删
除操作就会导致单线程卡顿。 
Redis 为了解决这个卡顿问题,在 4.0 版本引入了 unlink 指令,它能对删除操作进行懒
处理,丢给后台线程来异步回收内存。

> unlink key

OK

flush

Redis 提供了 flushdb 和 flushall 指令,用来清空数据库,这也是极其缓慢的操作。
Redis 4.0 同样给这两个指令也带来了异步化,在指令后面增加 async 参数就可以将整棵大树
连根拔起,扔给后台线程慢慢焚烧。

> flushall async

OK  

不是所有的 unlink 操作都会延后处理,如果对应 key 所占用的内存很小,延后处理就
没有必要了,这时候 Redis 会将对应的 key 内存立即回收,跟 del 指令一样

AOF Sync 也很慢 
Redis 需要每秒一次(可配置)同步 AOF 日志到磁盘,确保消息尽量不丢失,需要调用
sync 函数,这个操作会比较耗时,会导致主线程的效率下降,所以 Redis 也将这个操作移到
异步线程来完成。执行 AOF Sync 操作的线程是一个独立的异步线程,和前面的懒惰删除线
程不是一个线程,同样它也有一个属于自己的任务队列,队列里只用来存放 AOF Sync 任

拓展 7:妙手仁心 —— 优雅地使用 Jedis

Java 程序一般都是多线程的应用程序,意味着我们很少直接使用 Jedis,而是要用到 
Jedis 的连接池 —— JedisPool。同时因为 Jedis 对象不是线程安全的,当我们要使用 Jedis 
对象时,需要从连接池中拿出一个 Jedis 对象独占,使用完毕后再将这个对象还给连接池

Redis 深度历险: 核心原理和应用实践3_第20张图片

上面的代码有个问题,如果 doSomething 方法抛出了异常的话,从连接池中拿出来的 
Jedis 对象将无法归还给连接池。如果这样的异常发生了好几次,连接池中的所有链接都被持
久占用了,新的请求过来时就会阻塞等待空闲的链接,这样的阻塞一般会直接导致应用程序
卡死

为了避免这种情况的发生,程序员需要在使用 JedisPool 里面的 Jedis 链接时,应该使
try-with-resource 语句来保护 Jedis 对象

Redis 深度历险: 核心原理和应用实践3_第21张图片

但是当一个团队够大的时候,并不是所有的程序员都会非常有经验,他们可能因为各种
原因忘记了使用 try-with-resource 语句,惨剧就会突然冒出来让运维人员措手不及。我们需
要在代码上加上一层硬约束,通过这层约束,当程序员想要访问 Jedis 对象时,不会再出现
使用了 Jedis 对象而不归还Redis 深度历险: 核心原理和应用实践3_第22张图片

我们通过一个特殊的自定义的 RedisPool 对象将 JedisPool 对象隐藏起来,避免程序员
直接使用它的 getResource 方法而忘记了归还。程序员使用 RedisPool 对象时需要提供一个
回调类来才能使用 Jedis 对象。 

但是每次访问 Redis 都需要写一个回调类,真是特别繁琐,代码也显得非常臃肿。幸好 
Java8 带来了 Lambda 表达式,我们可以使用 Lambda 表达式简化上面的代码。 

Redis 深度历险: 核心原理和应用实践3_第23张图片

这样看起来就简洁优雅多了。但是还有个问题,Java 不允许在闭包里修改闭包外面的变
量。比如下面的代码,我们想从 Redis 里面拿到某个 zset 对象的长度,编译器会直接报
错。

Redis 深度历险: 核心原理和应用实践3_第24张图片

Redis 深度历险: 核心原理和应用实践3_第25张图片

有了上面定义的 Holder 包装类,就可以绕过闭包对变量修改的限制。只不过代码上要
多一层略显繁琐的变量包装过程。这些都是对程序员的硬约束,他们必须这么做才可以得到
自己想要的数据

重试 

我们知道 Jedis 默认没有提供重试机制,意味着如果网络出现了抖动,就会大范围报
错,或者一个后台应用因为链接过于空闲被服务端强制关闭了链接,当重新发起新请求时就
第一个指令会出错。而 Redis 的 Python 客户端 redis-py 提供了这种重试机制,redis-py 在
遇到链接错误时会尝试进行重连,然后再重发指令。 
那如果我们希望在 Jedis 上面增加重试机制,该如何做呢?有了上面的 RedisPool 对
象,重试就非常容易进行了。 

Redis 深度历险: 核心原理和应用实践3_第26张图片

拓展 8:居安思危 —— 保护 Redis 

指令安全 

Redis 有一些非常危险的指令,这些指令会对 Redis 的稳定以及数据安全造成非常严重
的影响。比如 keys 指令会导致 Redis 卡顿,flushdb 和 flushall 会让 Redis 的所有数据全
部清空。如何避免人为操作失误导致这些灾难性的后果也是运维人员特别需要注意的风险点
之一。 
Redis 在配置文件中提供了 rename-command 指令用于将某些危险的指令修改成特别的
名称,用来避免人为误操作。比如在配置文件的 security 块增加下面的内容:

rename-command keys abckeysabc 
 
如果还想执行 keys 方法,那就不能直接敲 keys 命令了,而需要键入 abckeysabc。 如
果想完全封杀某条指令,可以将指令 rename 成空串,就无法通过任何字符串指令来执行这
条指令了。 rename-command flushall "

端口安全 

Redis 默认会监听 *:6379,如果当前的服务器主机有外网地址,Redis 的服务将会直接
暴露在公网上,任何一个初级黑客使用适当的工具对 IP 地址进行端口扫描就可以探测出
来。 
Redis 的服务地址一旦可以被外网直接访问,内部的数据就彻底丧失了安全性。高级一
点的黑客们可以通过 Redis 执行 Lua 脚本拿到服务器权限,恶意的竞争对手们甚至会直接
清空你的 Redis 数据库。

bind 10.100.20.13 

所以,运维人员务必在 Redis 的配置文件中指定监听的 IP 地址,避免这样的惨剧发
生。更进一步,还可以增加 Redis 的密码访问限制,客户端必须使用 auth 指令传入正确的
密码才可以访问 Redis,这样即使地址暴露出去了,普通黑客也无法对 Redis 进行任何指令
操作。 requirepass yoursecurepasswordhereplease 
 
密码控制也会影响到从库复制,从库必须在配置文件里使用 masterauth 指令配置相应的
密码才可以进行复制操作。 masterauth yoursecurepasswordhereplease 
 

极度深寒 几节放弃!!!

 

你可能感兴趣的:(redis,redis)