数据最终一致性(binlog)

在数据库操作成功后,需要进行一些其他操作,如:发送一条消息到MQ中、更新缓存或者更新搜索引擎中的索引等。

最好的办法是换一种思路去解决

  • 不要同时去更新数据库和其他组件,只是简单的更新数据库即可。
  • 如果数据库操作成功,必然会产生binlog。之后,我们通过一个组件,来模拟的mysql的slave,拉取并解析binlog中的信息。通过解析binlog的信息,去异步的更新缓存、索引或者发送MQ消息,保证数据库与其他组件中数据的最终一致。
  • binlog同步组件:已经有很多开源的实现,例如linkedin的databus,阿里巴巴的canal,美团点评的puma等。

常见场景

增量索引

  • 通常索引分为全量索引增量索引。对于增量索引的部分,可以通过监听binlog变化,根据binlog中包含的信息,转换成es语法,进行实时索引更新。当然,你可能并没有使用es,而是solr,这里只是以es举例。

可靠消息

  • 可靠消息是指的是:保证本地事务与发送消息到MQ行为的一致性。一些业务使用本地事务表或者独立消息服务,来保证二者的最终一致。Apache RocketMQ在4.3版本开源了事务消息,也是用于完成此功能。事实上,这两种方案,都有一定侵入性,对业务不透明。通过订阅binlog来发送可靠消息,则是一种解耦、无侵入的方案。

缓存一致性

  • 换一种思路,只更新数据库,数据库更新成功后,通过拉取binlog来异步的更新缓存(通常是删除,让业务回源到数据库)。如果数据库更新失败,没有对应binlog,那么也不会去更新缓存,从而实现最终一致性。

如何解决多个slave会给master带来一些额外管理上的开销

本质上要满足的是:一份binlog数据,同时提供给多个不同业务场景使用,彼此之间互不影响。

  • 消息中间件是一个很好的解决方案。现在很多主流的消息中间件,都支持consumer group的概念,如kafka、rocketmq等。同一个topic中的数据,可以由多个不同consumer group来消费,且不同的consumer group之间是相互隔离的

保证消息的顺序性

异地多活

常见的两地三中心,三地五中心 实现异地多活
如何解决异地问题的数据同步,以及下面的两个问题。

  • 数据冲突:双方同时插入了一个相同主键的值,那么往对方同步时,就会出现主键冲突的错误。
  • 数据回环:一个库A中插入的数据,通过binlog同步到另外一个库B中,依然会产生binlog。此时库B的数据再次同步回库A,如此反复,就形成了一个死循环。

基本原理

原理基本就是利用mysql 类似主从同步的原理(用户的数据都是写入主库Master,Master将数据写入到本地二进制日志binary log中。从库Slave启动一个IO线程(I/O Thread)从主从同步binlog,写入到本地的relay log中,同时slave还会启动一个SQL Thread,读取本地的relay log,写入到本地,从而实现数据同步)

开源的实现类似mysql slave的组件

  • 阿里巴巴开源的canal
  • 美团开源的puma
  • linkedin开源的databus

核心就是完成两件事情

  1. 从源库拉取binlog并进行解析:binlog syncer
  2. 将获取到的binlog转换成SQL插入目标库,这个功能称之为sql write

1. 解决重复插入

  • 解决方案就是在数据同步时,限制记录必须有要有主键或者唯一索引

2. 解决唯一键冲突

  • 对于这种情况,通常建议是使用一个全局唯一的分布式ID生成器来生成唯一索引,保证不会产生冲突。
  • 另外,如果真的产生冲突了,同步组件应该将冲突的记录保存下来,以便之后的问题排查。

3. 对于DDL语句如何处理

缘由
如果数据库表中已经有大量数据,例如千万级别、或者上亿,这个时候对于这个表的DDL变更,将会变得非常慢,可能会需要几分钟甚至更长时间,而DDL操作是会锁表的,这必然会对业务造成极大的影响。

方案
同步组件通常会对DDL语句进行过滤,不进行同步。DBA在不同的数据库集群上,通过一些在线DDL工具(如gh-ost),进行表结构变更。

4. 如何解决数据回环问题

数据回环问题,是数据同步过程中,最重要的问题。我们针对INSERT、UPDATE、DELETE三个操作来分别进行说明:
问题描述

  • INSERT操作:假设在A库插入数据,A库产生binlog,之后同步到B库,B库同样也会产生binlog。由于是双向同步,这条记录,又会被重新同步回A库。由于A库应存在这条记录了,产生冲突。
  • UPDATE操作:先考虑针对A库某条记录R只有一次更新的情况,将R更新成R1,之后R1这个binlog会被同步到B库,B库又将R1同步会A库。对于这种情况下,A库将不会产生binlog。因为A库记录当前是R1,B库同步回来的还是R1,意味着值没有变。 然而,这并不意味UPDATE 操作没有问题,事实上,其比INSERT更加危险。考虑A库的记录R被连续更新了2次,第一次更新成R1,第二次被更新成R2;这两条记录变更信息都被同步到B库,B也产生了R1和R2。由于B的数据也在往A同步,B的R1会被先同步到A,而A现在的值是R2,由于值不一样,将会被更新成R1,并产生新的binlog;此时B的R2再同步会A,发现A的值是R1,又更新成R2,也产生binlog。由于B同步回A的操作,让A又产生了新的binlog,A又要同步到B,如此反复,陷入无限循环中。
  • DELETE操作:同样存在先后顺序问题。例如先插入一条记录,再删除。B在A删除后,又将插入的数据同步回A,接着再将A的删除操作也同步回A,每次都会产生binlog,陷入无限回环。

解决方案1 控制binlog同步方向

  1. 当把一个binlog转换成sql时,插入某个库之前,我们先判断这条记录是不是原本就是这个库产生的,如果是,那么就抛弃,也可以避免回环问题。现在问题就变为,如何给binlog加个标记,表示其实那个mysql集群产生的。
ROW模式下的SQL
mysql主从同步,binlog复制一般有3种模式。STATEMENT,ROW,MIXED。默认情况下,STATEMENT模式只记录SQL语句,ROW模式只记录字段变更前后的值,MIXED模式是二者混合。 binlog同步一般使用的都是ROW模式,高版本Mysql主从同步默认也是ROW模式。
采取的方案是,在执行的SQL之前加上一段特殊标记,表示这个SQL的来源。例如
 /*IDC1:DB1*/insert  into  users(name) values("tianbowen")
 其中/*IDC1:DB1*/是一个注释,表示这个SQL原始在是IDC1的DB1中产生的。之后,在同步的时候,解析出SQL中的IDC信息,就能判断出是不是自己产生的数据。
 然而,ROW模式下,默认只记录变更前后的值,不记录SQL。所以,我们要通过一个开关,让Mysql在ROW模式下也记录INSERT、UPDATE、DELETE的SQL语句。具体做法是,在mysql的配置文件中,添加以下配置:
 binlog_rows_query_log_events =1
 这个配置可以让mysql在binlog中产生ROWS_QUERY_LOG_EVENT类型的binlog事件,其记录的就是执行的SQL。

通过这种方式,我们就记录下的一个binlog最初是由哪一个集群产生的,之后在同步的时候,sql writer判断目标机房和当前binlog中包含的机房相同,则抛弃这条数据,从而避免回环。
优点:功能上可以解决问题;
缺点:实践起来非常麻烦,需要业务对每个非select 的sql 都需要加标识,一旦忘记添加标识那就麻烦了,无限循环即将开始。
如果采用这种方案可以在中间件层加入sql标识,统一屏蔽业务层。即使这样也不能保证所有的sql都是通过中间件来写入。
如果某个员工手动操作数据库时悲剧即将发生-TMD
所以总体来说方案不是很美好!

解决方案2 通过附加表 或者增加冗余标识
大致思路是,在db中都加一张额外的表,例如叫direction,记录一个binlog产生的源集群的信息。例如

CREATE TABLE `direction` (
  `idc` varchar(255) not null,
  `db_cluster` varchar(255) not null
) ENGINE=InnoDB  DEFAULT  CHARSET=utf8mb4

idc字段用于记录某条记录原始产生的IDC,
**db_cluster **用于记录原始产生的数据库集群(注意这里要使用集群的名称,不能是server_id,因为可能会发生主从切换)。

解决方案3 通过GTID (全局事务id)的概念

GTID 由2个部分组成:server_uuid : transaction_id 其中server_uuid是mysql随机生成的,全局唯一。transaction_id事务id,默认情况下每次插入一个事务,transaction_id自增1。
GTID提供了一个会话级变量gtid_next,指示如何产生下一个GTID。可能的取值如下:

  • AUTOMATIC: 自动生成下一个GTID,实现上是分配一个当前实例上尚未执行过的序号最小的GTID。
  • ANONYMOUS: 设置后执行事务不会产生GTID,显式指定的GTID。

默认情况下,是AUTOMATIC,也就是自动生成的,GTID会在每个事务(Query->...->Xid)之前,设置这个事务下一次要使用到的GTID。

方案
从源库订阅binlog的时候,由于这个GTID也可以被解析到,之后在往目标库同步数据的时候,我们可以显示的的指定这个GTID,不让目标自动生成。也就是说,往目标库,同步数据时,变成了2条SQL:

SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1’
insert  into users(name) values("tianbowen")

由于我们显示指定了GTID,目标库就会使用这个GTID当做当前事务ID,不会自动生成。同样,这个操作也会在目标库产生binlog信息,需要同步回源库。再往源库同步时,我们按照相同的方式,先设置GTID,在执行解析binlog后得到的SQL,还是上面的内容 由于这个GTID在源库中已经存在了,插入记录将会被忽略

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容