如今的系统大都是7 * 24不间断运行的,对高可用的要求很高。除此以外,系统须要保证不一样城市,甚至于不一样国家的可用性。html
MySQL数据库通常来讲是主从结构,经过跨地域部署主从复制查询能够保证跨地域的可用性。可是写操做一旦跨机房,网络的延迟和不可靠可能会形成执行时间超长或者执行失败。而这对于那些对高可用系统有跨地域的要求的系统来讲,就是不能接受的了。mysql
mysql自己具有多主复制的能力,可是咱们不能在多个mysql上进行毫无顾忌的写,一旦两个mysql实例在同一个表的同一行记录都进行了写操做就会形成复制问题,或者是实例之间数据的不一致。sql
从MySQL 5.7.17开始,发布了MySQL Group Replication,Group Replication具有了多主的能力,它是一种强一致的实现,多个MySQL实例可以实时保证数据的一致性,可是它对网络延时要求高,并且比价适合于金融业务场景。一旦跨地域性能很是低,网络的延迟和抖动甚至于会形成整个系统的不可用。数据库
大多数配置了多主复制的mysql集群,在业务上咱们也是写一个点,或者严格按照主键把请求哈希路由到不一样的实例上来写。服务器
固然大多数业务都是选择单点写,这样业务上好控制。当业务跨IDC部署时,跨IDC的写操做延时就会很高,影响业务性能。目前多IDC的业务部署有如下几种:网络
这种模式能写性能很是高,可是对业务程序的要求更高,不只增长了开发难度,并且部署成本会偏高,须要上层有对应的路由功能。并且一旦写请求对应的数据不属于本IDC,同样须要跨机房路由到其余机房去写。
并且这种模式一旦扩展到3个IDC的时候,每每须要在MySQL上部署环形复制,这种模式极易形成数据的不一致。架构
这种模式下,只有一个真正的主库,全部的写操做必须落到这个主库上,是最多见的模式。它不须要有路由来根据不一样的key路由到不一样的服务程序上,可是对于不在主库所在的IDC的服务程序须要实现读写分离。
这种模式顾名思义非对称,就是说不一样IDC的服务程序部署有必定的差别性,一旦咱们须要切换主库时不少状况下就须要手工操做,而且容易形成切换时的数据不一致。
同时,因为IDC2中的写操做是跨机房的,因为跨机房的网络延时和丢包影响,会对应用程序的服务性能产生巨大的影响。写操做过多,就不可取。运维
从图中能够看出,虽然采用代理层可以解决对称部署,可是没法解决跨机房写延时的问题。代理层是一个支持多套MySQL集群的中间件平台,详细了解请参考:(http://www.jianshu.com/p/bc50221972ca)。函数
目前MySQL的主从复制是一种强一致的数据库部署方案,咱们公司内部有不少业务都是没有强一致性需求,而更亲赖于高可用性,须要容忍机房故障,在任何一个机房出现故障时,也不能影响服务的正常进行。因此这种场景,咱们通常采用最终一致性数据库模型。性能
MyShard是一种最终一致性数据库,它一个支持多主多写模式的存储,是一个跨机房的对等部署的典范,可是MyShard的问题在于系统太重,部署和运维都是一件很是头痛的事情,目前咱们数据库团队已经再也不对外推广MyShard了,只是对线上已有的MyShard仍是提供技术支持和维护。
可是目前咱们公司仍是有不少业务是对于这种多机房同时写有强需求,如何解决这个问题呢?我在去年就提出要开发一套基于MySQL的多写方案,最近花时间突击研究了一下终于有了突破,基本上实现了想要的轻量级的多主MySQL集群。
从MySQL5.7开始,有了通讯渠道的概念,每个通讯渠道都是一个从服务器从主服务器得到二进制日志的连接。这意味着每一个通讯渠道都得有一个IO_THREAD .咱们须要运行不一样的 “CHANGE MASTER” 命令, 对于每个主服务器。咱们须要用到 “FOR CHANNEL”这个参数来提供通讯连接的名字。
举个例子:
MySQL > CHANGE MASTER to MASTER_HOST='127.0.0.1',MASTER_PORT=6301 , MASTER_USER='repl', MASTER_PASSWORD='repl',MASTER_AUTO_POSITION = 1 FOR CHANNEL="idc1"; MySQL > start slave for channel='idc1' ;
多主MySQL是基于MySQL 5.7.19改造,充分利用了多源复制功能,每一个MySQL实例都会成为集群中的其余的实例的从库。
改造后的MySQL是通用和多写的混合版本,对于不一样的表采用不一样的策略,支持多写的表有必定的限制:
一、存储在__mm 数据库下 二、必须添加一个 __version字段,而且是在表的第一个字段__version字段用来处理版本冲突,其中最后一个bit用来描述是否该记录以删除。 最后一位为0:有效数据 最后一位为1:删除数据
在该模式下,各个IDC的应用程序,均可以往机房内的MySQL实例进行读写操做。
写:HASET table set col1 = ? AND col2=? WHERE K1=? AND k2=? 若是对应主键数据不存在,就插入数据,存在就更新数据 删:HADELETE FROM table where k1=? AND k2=? 若是对应主键数据不存在,插入一条删除记录,存在,就改成带删除标记的数据
mmversion(seed,delete_flag):函数代两个参数,第一个参数是版本生成的种子,若是为0,采用系统时间自动生成种子;第二个参数是删除标记。
mysql> select mmversion(0,0) ; +--------------------------------+ | mmversion(0,0) | +--------------------------------+ | 3232157937589817394 | +--------------------------------+ 1 row in set (0.00 sec)
mmversion_is_local(version):该函数用来返回是不是在本实例产生
mysql> select mmversion_is_local(3232157937589817394) ; +-----------------------------------------+ | mmversion_is_local(3232157937589817394) | +-----------------------------------------+ | 1 | +-----------------------------------------+
注意:该版本没有对MySQL基本的语法作任何逻辑上的变动,因此基本的查询语法、DML语法都是可使用的,而且也能够用在多写的表上面。
CREATE TABLE `b` ( `__version` bigint(20) unsigned NOT NULL, `id` bigint(20) auto_increment NOT NULL, `name` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表的第一个字段必须是__version字段,bigint类型。
下面咱们同过DML语法在这种多主表中的写法:
mysql> insert into b ( __version, id, name) values ( mmversion(0, 0 ), mmuuid(), '123') ; Query OK, 1 row affected (0.04 sec) mysql> select last_insert_id() ; +---------------------+ | last_insert_id() | +---------------------+ | 1617051614604954626 | +---------------------+ 1 row in set (0.00 sec) mysql> select * from b where id= 1617051614604954626 ; +---------------------+---------------------+------+ | __version | id | name | +---------------------+---------------------+------+ | 3234103229209909252 | 1617051614604954626 | 123 | +---------------------+---------------------+------+ 1 row in set (0.00 sec)
按照最终一致性的原理,版本号大的会胜出,更新语法中必定要判断版本号,并更新版本号:
mysql> update b set name='234',__version=mmversion(0,0) where id=1 and __version < mmversion(0,0) ; Query OK, 1 row affected (0.03 sec) Rows matched: 1 Changed: 1 Warnings: 0
也能够支持复杂的update
mysql> update b set name='4444', __version=mmversion(0,0) where name like '1%' and __version < mmversion(0,0) ; Query OK, 1 row affected (0.03 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> update b set __version=mmversion(0,1) where name like '44%' and __version < mmversion(0,1) ; Query OK, 1 row affected (0.07 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> haset b set name='123' where id=1 ; Query OK, 1 row affected (0.03 sec) mysql> haset b set name='234' where id=2 ; Query OK, 1 row affected (0.03 sec) mysql> select * from b ; +--------------------------------+----+--------+ | __version | id | name | +--------------------------------+----+--------+ | 3232159101525960754 | 1 | 123 | | 3232159110115897394 | 2 | 234 | +--------------------------------+----+--------+ 2 rows in set (0.00 sec)
mysql> hadelete from b where id=1 ; Query OK, 2 rows affected (0.23 sec) mysql> select * from b ; +--------------------------------+----+--------+ | __version | id | name | +--------------------------------+----+--------+ | 3232159183130343475 | 1 | 123 | | 3232159110115897394 | 2 | 234 | +--------------------------------+----+--------+ 2 rows in set (0.01 sec)
hadelete不会实际的删除一条记录,而是把__version字段的删除标记为标位1。
mysql> select * from b where id=1 and __version % 2 = 0 ; Empty set (0.00 sec) mysql> select * from b where __version %2 = 0 ; +--------------------------+----+--------+ | __version | id | name | +--------------------------+----+--------+ | 3232159110115897394 | 2 | 234 | +--------------------------+----+--------+ 1 row in set (0.00 sec)
这种语法不建议开发人员使用,可能形成数据不一致,通常由dba清除数据用
delete from b where id=1 ;
咱们仍然可使用自增id,这要求在集群中的不一样MySQL实例要设置不一样的偏移,例如:
实例1:
auto-increment-increment = 2 auto-increment-offset = 1
实例2:
auto-increment-increment = 2 auto-increment-offset = 2
插入数据:
mysql> insert into c ( __version, name ) values ( mmversion(-1,0), '444') ; Query OK, 1 row affected (0.03 sec) mysql> select last_insert_id() ; +------------------+ | last_insert_id() | +------------------+ | 1 | +------------------+ 1 row in set (0.00 sec) mysql> select * from c where id=1 ; +---------------------+----+------+ | __version | id | name | +---------------------+----+------+ | 3234105709553524740 | 1 | 444 | +---------------------+----+------+ 1 row in set (0.00 sec)
MySQL原生的语法和功能都没有改变,因此咱们能够利用事物来处理一些复琐事情。
start transaction; select * from a where id=1 and __version < new_version for update ; if ( has key ) update a set xxxx = ffff, __version = new_version where id=1; else insert into a ( id, xxxx , __version) values ( 1, 'ffff' ,new_version); end commit transaction ;
小礼物走一走,来简书关注我