redis单元化
概述
什么是单元化
单元化是指异地多活架构,例如北京与深圳两个数据中心各部署一套Redis实例,两者数据保持同步。业务层保证只对本地单元的数据进行写操作,但可以读取其他单元的数据。
单元化主要有两个优势:
- 异地多活:两个数据中心同时提供服务,一个故障时另一个可以继续提供服务
- 低时延:读写操作都在本地数据中心完成,避免跨地域网络延迟
本文档不详细讨论单元化的优势与理念,主要讲解Redis单元化的具体实现方案。
方案总体介绍
单元化方案的核心设计:
- 通过一个同步工具将两个Redis主节点构成一条单元化链路,该同步工具可以单向或双向地同步两者的写操作
- 将对端单元传过来的数据加上单元归属信息(如unit_id),本单元不允许对其他单元的key做任何写操作(包括淘汰、过期等)
整体部署架构
下面展示了单元化系统的整体部署架构。同步工具是整个系统的核心组件,负责连接两个单元的Redis主节点,并协调数据同步。
架构说明:
- 同步工具:单点部署,同时连接两个Redis master,负责在两个单元之间同步写操作
- Redis Master:每个单元的主节点,负责处理本地写请求和同步过来的命令
- 从节点:每个单元的从节点,通过常规主从复制与本地主节点同步
- 数据流向:同步工具从源端获取binlog,经过处理后转发到目标端
** failover 机制**:同步工具同时监听两个单元的Sentinel,当检测到主从切换时自动重连到新的主节点。
同步工具详解
设计理念:无状态
同步工具采用无状态设计,这是整个系统可靠性的基础:
- 所有binlog持久化到本地临时文件
- 可随时重启,从上次位点继续同步
- 不依赖外部状态,只需配置文件
这种设计使得同步工具可以任意切换,failover时只需重新连接即可。
内部架构
同步工具内部包含两组协程:一组负责与源端连接,另一组负责与目标端连接。
源端协程职责:
- 连接源端Redis
- 解析binlog
- 写入内存缓冲区
- 刷盘到临时文件
目标端协程职责:
- 从临时文件读取binlog
- 发送到目标端
- 处理ACK确认
- 更新位点索引
数据流转(内存+临时文件交互)
同步工具需要处理binlog的持久化与传输,确保数据不丢失。
- 内存缓冲区:暂存待发送的binlog,达到阈值(约256KB-1MB)后刷盘
- 临时文件:顺序写入,按binlog位点建立索引
- 清理机制:目标端ACK确认后,删除已确认的binlog文件
- 重启恢复:通过索引文件快速定位到上次同步位点
回环检测机制
同步工具通过以下方法防止命令回环——即一条命令不会经过同步工具两次:
- 每个真实命令前追加
UNIT_SYNC_BINLOG命令(作为标记) - 回环检测逻辑:如果先收到
UNIT_SYNC_BINLOG,则后续命令为回环命令,直接丢弃
同步流程详解
连接建立
同步工具启动时,首先需要与两个Redis节点建立连接。
- 同步工具向目标端(r2)发起连接,通过自定义命令
UNIT_SYNC_NEW_SESSION,参数包括:源单元名、链路名称、session ID - 从r2的响应中获取其单元化的repl_id和offset(这是该同步链路的当前同步位点)
- 建立与源端(r1)的连接,发起
KUNIT_PSYNC命令,附带刚获取的repl_id和offset
全量同步(RDB解析)
当源端判断需要全量同步时,会返回RDB数据。同步工具需要解析RDB并转发到目标端。
每个RDB的key-value对,会被包装成一个MULTI EXEC事务,包含三条命令:
UNIT_SYNC_BINLOG:附带session_id、binlog_seq(递增)、repl_id、offset、源单元名和链路名UNIT_SYNC_PRE_RESTORE+key:告知目标端即将执行RESTORERESTORE+ 命令参数(REPLACE和ABSTTL)
在所有RDB命令解析完成后,会增加一条 UNIT_SYNC_BINLOG,附带真正的 repl_id 和 offset,用于告知目标端同步位点。
增量同步(稳态)
稳态阶段,同步工具持续在源端和目标端之间同步增量命令。
与源端(r1)的稳态交互
- 持续解析从r1收到的命令
- 如果是
unit命令,只更新offset,不持久化到文件 - 否则包装成
MULTI命令,持久化到本地文件中
- 如果是
- 每秒向r1回复一次
REPLCONF ACK offset
与目标端(r2)的稳态交互
- 持续从本地文件读取之前持久化的命令,发送给r2
- 将已发送但未收到回复的请求附带seq记录到队列
binlog_info_q中 - 持续从r2收回复包,每收到一个命令的回复,删除队列中对应记录
- 定期根据队列记录的进度,删除目标端已收到的命令对应的文件
位点管理与持久化
Redis引入单元化同步的进度结构,相当于在常规的repl_offset基础上增加了unit_repl_offset:
- 每个节点记录自身的unit_repl_id和unit_repl_offset,主要用于重连时让同步工具获取该节点单元化的同步位点
- 对于单一节点,单元化写操作的binlog和普通命令产生的binlog共用缓冲区和repl_id/offset
Redis端实现
关键数据结构
KUnitSession
单元同步会话结构,维护跨单元同步状态:
1 | struct KUnitSession { |
每个Redis最多支持16个并发会话(KUNIT_SESSION_COUNT_MAX)。
robj.kunit
Redis对象中的单元归属信息字段:
1 | struct { |
unit_id = 0:表示该key属于本单元unit_id > 0:表示该key属于其他单元
UnitIdPair
记录单元名称与ID的映射关系:
1 | struct UnitIdPair { |
自定义命令
KUNIT_SYNC_NEW_SESSION
同步工具初始化时获取目标端的同步进度。
1 | 请求: KUNIT_SYNC_NEW_SESSION <UNIT_NAME> <RL_NAME> <SID> |
KUNIT_SYNC_BINLOG
更新同步位点并设置后续命令的key的单元归属信息。
1 | 请求: MULTI |
KUNIT_SYNC_PRE_RESTORE
在RESTORE命令执行前做校验,检查key的单元归属信息是否匹配。
1 | 请求: MULTI |
写命令处理逻辑
对于本地写命令,将单元归属信息置为 unit_id=0,表明该key属于本单元。
对于单元化同步过来的写命令(在MULTI中),处理流程如下:
- 在EXEC执行前,清空所有预置的单元归属信息
- 依次执行命令,执行到
UNIT_SYNC_BINLOG时,设置后续key的单元归属信息 - 在
dbAdd时,将该单元归属信息写入value对象
关键函数说明
keyBelongToThisKUnit()
判断key是否属于本单元:
1 | int keyBelongToThisKUnit(redisDb *db, robj *key) { |
- 返回
1:key属于本单元,可以被淘汰或过期 - 返回
0:key不属于本单元,不进行淘汰或过期操作
lookupKeyWriteWithFlags()
写操作时更新unit_id:
1 | robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) { |
- 本地写命令:
unit_id设置为0(属于本单元) - 同步过来的命令:
unit_id设置为对端单元ID
propagateDeletionForKunit()
单元内删除不传播到从节点:
1 | void propagateDeletionForKunit(redisDb *db, robj *key, int lazy) { |
只有当key属于本单元时,才向从节点传播删除操作。
关键配置项
allow-evict-non-local
是否允许淘汰非本单元key:
| 值 | 行为 |
|---|---|
no (默认) |
只淘汰属于本单元的key (unit_id=0) |
yes |
允许淘汰所有key,不区分单元 |
1 | # 默认配置 - 只淘汰本单元的key |
intra-unit-expiration-enable
是否允许单元内key过期:
| 值 | 行为 |
|---|---|
no (默认) |
本单元不处理其他单元key的过期 |
yes |
允许处理所有key的过期 |
1 | # 默认配置 - 不处理非本单元key的过期 |
其他相关配置
unit-name:本单元名称,用于标识unit-id:本单元ID(自动生成,也可手动指定)
Failover处理
同步工具本身是无状态的,可以任意更换。因此failover处理主要关注源端和目标端的主从切换场景。
同步工具同时监听源端和目标端Sentinel的+switch-master频道,检测主从切换事件。
源端master发生主从切换
当同步工具检测到源端failover事件时:
- 断开与源端的连接
- 建立与源端新主节点(原slave)的连接
- 使用原先保存的repl_id和offset尝试构建psync
增量复制判断:在实践经验中,由于同步延迟一般很低,基本都是增量复制。当主从切换时,Redis内核会将原先的repl_id变为repl_id2,并生成新的repl_id。当psync连进来时,若判断可以增量复制,Redis新主会回复”+continue repl_id”。
- 同步工具更新repl_id参数,并通过
KUNIT_SYNC_BINLOG通知目标Redis也更新该repl_id
目标端master发生主从切换
目标端的所有主从节点都保存了本次单元同步链路的信息,包括unit_repl_id和unit_repl_offset等。
当检测到目标端failover时,同步工具只需:
- 使用
KUNIT_SYNC_NEW_SESSION重新获取同步位点 - 从头开始同步(通常也是增量复制)