禧云Redis跨机房双向同步实践( 二 )

  • 写另一机房失败本机房数据是否需要回滚?
  • 现在java业务系统使用jedis和redisson都有,适配难度大,且不利于客户端版本升级
  • 所以,客户端侧解决方案被第一个否决了 。
    再来看代理层解决方案会面临哪些问题:
    1. 代理的性能怎么样,代理本身的延时高不高?
    2. 我司现有Redis架构基本都是Sentinel模式的,代理层解决方案每接入一个同步任务就需要重新调整Redis架构和网络,对运维同学不太友好
    3. 代理层解决方案同样面临同步写其他机房还是异步写其他机房,写其他机房失败怎么处理的问题 。
    4. 开源界的Redis代理项目基本都不是基于java语言的,对于我们团队来说二次开发的难度较大 。
    代理层解决方案应该能解决我们的问题,但也不是特别理想 。
    接下来看看如果我们从服务端侧解决,会面临哪些问题:
    1. 我们需要解析到客户端的指令
    2. 需要解决同步回环问题:A -> B -> A
    解决掉这两个问题,一个Redis指令写入两个机房的基本上就解决了 。所以我们选择了从server端同步来实现 。Redis主从协议的实现采用的国人开源的redis-replicator;回环同步问题下文中会详细讲到 。
    四、实现细节
    本节会详细介绍一些具体问题的解决方案 。
    4.1 网络架构
    我司在网络层用双机房双向VPN隧道打通局域网,北京到上海的网络延时稳定在30ms左右 。这样我们的Redis节点就不需要暴露在公网,数据安全这块就不需要考虑了 。
    4.2 同步回环
    同步回环是指同一条指令在两个Redis节点重复执行,看系统架构图的下面一截:图4-2
    禧云Redis跨机房双向同步实践

    文章插图
    图4-2
    如果不对同步指令加任何干预,业务写的一条指令会在A、B两个机房的Redis节点上无限循环执行:
    1. 业务系统在A机房redis-A写入指令 set a 1
    2. A机房replicator-A作为redis-A的从节点接收到指令set a 1
    3. rotter-A将指令set a 1写入B机房redis-B
    4. B机房replicator-B作为redis-B的从节点接收到指令set a 1
    5. rotter-B将指令set a 1写入A机房redis-A …
    为了打破这个循环,我们采用了增加一个辅助key的办法:
    1. 业务系统在A机房redis-A写入指令 set a 1
    2. A机房replicator-A作为redis-A的从节点接收到指令set a 1
    3. rotter-A MD5(set a 1) 得到circle-key-md5,拼装成指令setex circle-key-md5 120 1
    4. rotter-A将指令setex circle-key-md5 120 1和指令set a 1一起写入B机房redis-B
    5. B机房replicator-B作为redis-B的从节点接收到指令setex circle-key-md5 120 1 和set a 1
    6. rotter-B直接忽略circle-key指令
    7. rotter-B在本机房执行del circle-key-md5,如果成功说明是回环KEY,不需要同步至A机房
    4.3 高吞吐量
    要提高吞吐量我们首先需要知道性能瓶颈在哪 。从上面处理回环同步的问题我们可以看到,同步流程中存在两处需要业务Redis交互:
    1. Rotter收到同步指令之后需要在本机房执行del circle-key-md5来判断当前指令是否为循环指令
    2. Rotter将同步指令和circle-key-md5写入另一机房Redis
    这里我们有两个武器:多线程和Pipeline 。
    多线程:
    引入多线程的同时会引入另外一个问题:如何保证指令的顺序性?
    顺序性的保证在同步回环校验阶段和跨机房写入阶段略有不同 。
    同步回环阶段我们只要得到指令是否为回环指令就行,和各指令之间校验的顺序没关系,但需要保证在往下一环节发送的时候是有序的 。所以我们是采用线程队列实现的 。关键代码如下 :
    ……
    写目标机房阶段我们需要严格保证执行顺序,假如业务系统执行两条指令set a 1和del a,如果同步时执行顺序反了,会对业务系统产生不可预估的后果 。但如果是两个不同的key,大部分场景下是可以交换执行顺序的 。
    所以我们自己实现了一个简单的有序线程池,对同步的key取hash后再取mod,mod值相同的指令放在同一线程执行,这样就保证了同一个key的执行顺序一定是有序的 。同时我们还支持将指定的key分配到同一个线程执行,满足业务系统存在key之间相互依赖的场景 。
    Pipeline:
    通过redis pipeline批量执行指令能够大量的减少Rotter和Redis的交互次数,但也会带来一个问题:不能将指令阻塞在pipeline中太久而增加同步延时,所以我们需要另外一个线程来触发提交pipeline数据,目前Rotter采取的策略是每100条或者10ms发送一次 。


    推荐阅读