Redis
默认是单线程执行命令(Redis v6.0
之前), 在支持命令多线程的情况下,如何保证数据一致性,对于Flushall
, FlushDB
等命令又该如何处理呢? 通过在Redis-Sync中支持串并转换, 可以较好的解决并发执行命令时的数据一致性问题.
一, 背景简介
Redis-Sync
隶属于TendisX冷热混合存储项目
, 是项目中的同步层组件, 负责对Tendis缓存层
和Tedis存储版
进行数据同步.由于Tedis存储版
支持多线程并发执行命令,我们需要在Redis-Sync
中支持并同步命令至存储层, 以此来充分发挥Tendis存储版
的性能.在Redis-sync
中采用多线程并发模型进行数据同步, 架构如下:

如上图所示, Redis-Sync
通过多线程并发的模式来发挥Tendis存储版
的性能.此处有以下需要注意:
1. 通过特定的hash算法
, 我们将统一Slots的命令投递至相同的Queue中, 可以满足数据一致性, 避免数据的破坏与覆盖
2. 借助多队列的N-Tier架构
,理论上可以支持充分的水平扩展, N:M架构下进行Tendis缓存层
和Tendis存储版
的混部
Redis-Sync
作为TendisX冷热混合存储项目
的同步层, 整体架构如下:

如上如所示, 不过在目前的架构下,存在命令投递上的局限性:
1. 对于FlushAll
, FlushDB
之类的命令, 会造成数据破坏
2. 目前Redis-Sync
依赖Tendis存储层
的集群模式保证无跨Slots的命令
基于上述存在问题的考量, 我们通过支持Redis-Sync
的串并转换来解决数据一致性的问题.
二, 串并转换设计
串并转换指的是进行串行,并行执行两种模式之间进行切换.那么, 串并转换为什么能解决前述问题呢?
前述问题的本质在于: FlushaAll
/FlushDB
命令是针对所有db
/slots
的. Redis-Sync
的TaskQueue
是进行Slots维度的命令拆分. 所以,当同一命令涉及多个甚至所有Slots时, TaskQueue
不能将命令进行很好的拆分, 所以会造成数据破坏/覆盖等问题.通过将串并转换, 在命令跨Slots时, 牺牲部分性能将执行串行化, 确保数据一致性,在特殊的跨Slots命令执行完毕后, 恢复并行执行, 以保证充分发挥Tendis存储版
的性能.

如上图所示,通过串并转换来解决涉及全部Slots的FlushDB/FlushAll
问题
籍由Redis-Sync
的N-Tier
架构中使用的TaskQueue
,提供特殊的SignalTask
及WaitTask
来实现串并转换, 如下图所示:

(上述case, 假定由Queue2进行FlushDB的分发)
在上述实现中, 通过在FlushDB
前的所有任务中,Push WaitTask
来保证其他所有并行任务转化为串行, FlushDb
前的所有命令必须已经被执行, FlushDB
后的所有命令必须被延后执行(在FlushDB
完成后). 通过多值信号量进行Signal
, Wait
操作设计实现.
三, 串并转换实现
在进行方案设计后,需要考虑串并转换的对应实现. 具体的实现需要考虑下述问题:
如何抽象 Signal
,Wait
的概念?如何合理的进行挂起/唤醒 ?
在此展示其中一种实现方案:
Signal
与Wait
的语义为 等待, 唤醒, 在已有的标准库设施下, 优先考虑倒计时(CountDown)
进行实现, 个中理由在于: 此处可能会有N组并发, 仅使用二值信号量(cond_variable
)不太合适, 使用语义更丰富的多值信号量更为适宜, 因为后续对应要并转串
->串转并
, 所以使用倒计时(CountDown)
更为适宜.挂起/唤醒是必须要处理的case, 因为信号量可能会存在**"假唤醒"**, 即:无任何Signal情况下, 会自动唤醒. 所以需要使用 while
检测, 而非if
进行判断. (此特性被POSIX认为是feature, 所以不予修复)
综合上述考量, 下面为核心代码相关实现:
void CountDown::Wait(int waitInterval) {
using namespace std::chrono_literals;
std::unique_lock<std::mutex> lk(mtx_);
while (var_.load() != 0) {
cv_.wait_for(lk, waitInterval * 1ms,
[&]() -> bool { return var_.load() == 0; });
}
}
void CountDown::Signal() {
int val = var_.fetch_sub(1, std::memory_order_relaxed);
assert(val > 0);
if (val == 1) {
cv_.notify_all();
}
}
结合上述的CountDown, 在Redis-sync
中再抽象相对应的WaitTask
及SignalTask
. 即可在Redis-Sync
的命令传递中实现串并转换了
四, 更多思考
上述的讨论目前仅落在FlushAll
/FlushDB
上, 此二命令涉及all-slots
, 进行全局的串并转换是合理的. 那对于涉及部分slots的case该如何处理呢?
仅仅涉及部分slots的case是很常见的, 目前是由于Redis-Cluster
模式下不支持跨Slots命令, 因此无需处理部分跨Slots命令, 例:MSET
等.在脱离Redis-Cluster
后, 如何在兼顾数据一致性和性能的条件下支持跨slots命令呢?
略微思考下, 其实上述问题已经解决了. FlushAll/FlushDB
是跨Slots的特殊case而已. 那么, 只要进行少量的调整, 便可以支持部分串并转换(PartialSerialToParallel)
.例如 1 : Queue.size() - 1
的SignalTask与WaitTask比例, 调整为1 : across-slots.size()
即可. 同时支持部分串并转换并不会牺牲太多的性能. 在优先保证数据一致性的前提下, 还能获得较为可观的性能.
五, 总结与反思
本篇主要分析了Redis-Sync
的并发任务模型在跨Slots命令下的局限与破局. 通过串并转换来支持FlushAll/FlushDB
等会破坏数据一致性的命令. 之后进行相关的延伸, 通过支持部分半同步从而解决跨slots命令的问题. 并能在确保数据一致性的前提下, 兼有不错的性能.文中所提出的方案设计与实现仅是一家之见,也欢迎提出不同的想法与意见.




