Writeset
优质博文:IT-BLOG-CN
MySQL
的WriteSet
功能主要用于增强复制的并发性和一致性,特别是在主从复制环境中。WriteSet
是MySQL 5.7
引入的一个特性,主要用于解决复制过程中可能出现的写冲突问题。
MySQL
并行复制目前经历过三个比较关键的时间结点“库间并发”,“组提交”,“写集合”;
一、库间并发
库间并发:一个实例内可能会有多个库schema
,不同的库之间没有什么依赖关系,所以在slave
那边为每一个库schema
单独起一个SQL
线程,这样就能通过多线程并行复制的方式来提高主从复制的效率。
这个理论听起来没问题,但是事实上一个实例也就一个业务库,所以这种库间并发就没什么作用了;也就是说这个方式的适用场景比较少,针对这个不足直到“组提交”才解决!
二、组提交
组提交:如果多个事务他们能在同一时间内提交,这个就间接说明了这个几个事务锁上是没有冲突的,也是就说他们各自持有不同的锁,互不影响;逻辑上我们几个事务看一个组,在slave
以“组”为单位分配给SQL
线程执行,这样多个SQL
线程就可以并行跑了;而且不在以库为并行的粒度,效果上要比“库间并发”要好一些。
这个事实上也有一些问题,因为它要求库上要有一定的并发度,不然就有可能变成每个组里面只有一个事务,这样就有串行没什么区别了,为了解决这个问题MySQL
提供了两个参数就是希望在提交时先等一等,尽可能的让组内多一些事务,以提高并行复制的效率。
binlog_group_commit_sync_no_delay_count
设置一个下水位,也就是说一个组要凑足多少个事务再提交;为子防止永远也凑不足。那么多个事务MySQL
还以时间为维度给出了另一个参数binlog_group_commit_sync_delay
这个参数就是最多等多久,超过这个时间长度后就算没有凑足也提交。
这两个参数特别难找到合的值,就算今天合适,过几天业务上有点变化后,又可能变的不合适了;如果MySQL
能自己达到一个自适应的效果就好了;这个自适用要到WriteSet
才完成(WriteSet
并不是通过自动调整这两个参数来完成,它采用了完全不同的解决思路)。
三、WriteSet
WriteSet
解决了“组提交”的问题
案例:假如第一天更新id=1
的数据,第二天更新了id=2
的数据,第三天有个slave
同步数据了,以“组提交”的特点,这两个更新会被打包到不同的“组”,也就是说会有两个组;由于每个组内只有一个事务,所以逻辑上就串行起来了。
身为DBA
可以看出来这两个事实上是可以打包到同一个组里来的,因为他们互不冲突,就算打包到同一个组也不引起数据的不一致。 于是你有两个办法:
【1】将binlog_group_commit_sync_no_delay_count
设置成2
,也就是说一个组至少要包含两个事务,并且把binlog_group_commit_sync_delay
设置成24
小时以上!如果你真的做了,你就可以回家了,你的数据库太慢了(第一条update
等了一天),才完成!
【2】MySQL
用一本小本子记下它最近改了什么,如果现在要改的数据和之前的数据不冲突,那么他们就可以把包到同一个组;刚才的例子,由于第二天改的值的id=2
所以它和第一天的不冲突,那么它完全可以把第二天的更新和第一天的更新打包到同一个组。这样组里面就有两个事务了,在slave
第三天回放时就会有一种并行的效果。
binlog_transaction_dependency_history_size
这个参数可以设置小本子的容量了;mysql-5.7.22
之后的版本小本子就是它生来就有的。
只要在master
上新增两个参数开启writeSet
binlog_transaction_dependency_tracking = WRITESET # COMMIT_ORDER
transaction_write_set_extraction = XXHASH64
binlog_transaction_dependency_history_size = 500000
binlog_transaction_dependency_tracking
这个参数用于配置二进制日志中事务依赖关系的跟踪方式。它的主要目的是提高从库的并行复制能力。可选值:
【1】COMMIT_ORDER
:基于提交顺序的事务依赖跟踪。这是默认值,表示从库按照主库上事务的提交顺序来执行事务。
【2】WRITESET
:基于写集的事务依赖跟踪。启用这个选项后,MySQL
会生成每个事务的写集,并在从库上进行冲突检测,以允许并行执行不冲突的事务。
【3】WRITESET_SESSION
:基于写集的事务依赖跟踪,但在会话级别进行。与WRITESET
类似,但冲突检测仅在同一会话内进行。
transaction_write_set_extraction
这个参数用于配置写集提取的算法。写集提取是生成事务写集的过程,主要用于冲突检测。可选值:
【1】OFF
:禁用写集提取。这是默认值。
【2】XXHASH64
:使用XXHASH64
算法进行写集提取。XXHASH64
是一种快速的非加密哈希算法,适用于高性能场景。
binlog_transaction_dependency_history_size
是内存中保存的做冲突检测的数据行数量的上限值,该值越大,保存的数据行越多,事务间冲突检测后并发度越高。
实际上Writeset
是一个集合,使用的是C++ STL
中的set
容器,在类Rpl_transaction_write_set_ctx
中包含了如下定义:
std::set<uint64> write_set_unique;
集合中的每一个元素都是hash
值,这个hash
值和我们的transaction_write_set_extraction
参数指定的算法有关,其来源就是行数据的主键和唯一键。每行数据包含了两种格式:
【1】字段值为二进制格式
【2】字段值为字符串格式
四、实现细节
【1】生成WriteSet
: 当一个事务在主库上提交时,MySQL
会扫描事务中的所有写操作(如INSERT
、UPDATE
、DELETE
),并将这些操作涉及的键生成一个WriteSet
。
假设我们有一个简单的事务,它在一个表users
上执行了一些写操作:
START TRANSACTION;
INSERT INTO users (id, name) VALUES (1, 'Alice');
UPDATE users SET name = 'Bob' WHERE id = 2;
DELETE FROM users WHERE id = 3;
COMMIT;
在这个事务中,涉及的写操作包括一个插入操作、一个更新操作和一个删除操作。MySQL
会生成一个WriteSet
来表示这些操作涉及的键。
WriteSet
示例:假设users
表的主键是id
,那么生成的WriteSet
可能会包含以下信息:
INSERT 操作:插入了 id = 1 的记录。
UPDATE 操作:更新了 id = 2 的记录。
DELETE 操作:删除了 id = 3 的记录。
简化后的WriteSet
可以表示为:
WriteSet = { (users, id, 1), (users, id, 2), (users, id, 3) }
WriteSet = { (users, id, 1), (users, id, 2), (users, id, 3) }
这里的(users, id, 1)
表示在users
表中主键为1
的记录。
在实际实现中,WriteSet
可能会使用更复杂的数据结构和算法来存储和处理这些信息。例如,MySQL
可能会使用哈希表或其他高效的数据结构来存储这些键,并使用哈希算法(如XXHASH64
)来生成键的哈希值。
【2】传播WriteSet
: 生成的WriteSet
会随着二进制日志binlog
一起传播到从库。在从库上,MySQL
会解析这些日志并提取出WriteSet
。
【3】冲突检测和处理: 从库在执行事务前,会检查当前事务的WriteSet
是否与正在执行的其他事务的WriteSet
有冲突。如果有冲突,当前事务会被推迟执行,直到冲突消除。
冲突检测:当从库接收到这个事务时,它会使用WriteSet
来进行冲突检测。假设从库上有另一个事务正在执行,它的WriteSet
是:
WriteSet = { (users, id, 2), (users, id, 4) }
从库会检测两个WriteSet
是否有交集。在这个例子中,两个WriteSet
都有 (users, id, 2)
,说明存在写冲突。因此,从库会推迟执行这个事务,直到冲突消除。
五、WriteSet实践
我们要执行的目标SQL
如下
create database tempdb;
use tempdb;
create table person(id int not null auto_increment primary key,name int);insert into person(name) values(1);
insert into person(name) values(2);
insert into person(name) values(3);
insert into person(name) values(5);
看一下组提交对上面SQL
的分组情况
last_committed(组号) | sequence_num(组内id) | SQL语句 |
---|---|---|
1 | 1 | create database tempdb |
2 | 2 | create table person(id int not null auto_increment primary key,name int) |
3 | 3 | insert into person(name) values(1) |
4 | 4 | insert into person(name) values(2) |
5 | 5 | insert into person(name) values(3) |
6 | 6 | insert into person(name) values(5) |
看write_set
的对“组提交”优化后的情况
last_committed(组号) | sequence_num(组内id) | SQL语句 |
---|---|---|
1 | 1 | create database tempdb |
2 | 2 | create table person(id int not null auto_increment primary key,name int) |
3 | 3 | insert into person(name) values(1) |
3 | 4 | insert into person(name) values(2) |
3 | 5 | insert into person(name) values(3) |
3 | 6 | insert into person(name) values(5) |
可以看到各个insert
是可以并行执行的,所以它们被分到了同个组last_committed
相同。
last_committed
,sequence_number
这两个值在binlog
里面记着就有,我在解析binlog
的时候习惯使用如下选项
mysqlbinlog -vvv --base64-output='decode-rows' mysql-bin.000002