HCM

Happy Coding Monkey

MySQL和TiDB事务区别

2019-04-21


我们知道, MySQL的默认隔离级别是可重复读(RR, Repeatable Read). TiDB是快照隔离 (SI, Snapshot Isolation), 虽然对外显示为Repeatable Read).

TiDB 使用 Percolator 事务模型, 冲突检测只在事务提交时才触发, 而MySQL则是通过锁等待机制(如SELECT … FOR UPDATE)解决冲突问题.TiDB这样设计有好有坏, 在冲突小的情况下,由于没有锁等待,系统的并发性能更好. 但在冲突严重的情况下, 会造成事务失败增多,影响并发性能.

反映到应用开发中, 我们会发现, 在很多场景, MySQL事务提交成功, 但是TiDB事务提交却可能会失败. 如果应用代码有在事务执行过程中穿插业务判断逻辑, 是不能依赖TiDB自带的事务重试机制的. 这时,要显式的关闭事务重试, 重试逻辑必须放到业务层来做.

例子

下面三个例子, 从TiDB和MySQL执行结果和逻辑的区别, 我们可以看到两个数据库系统在事务处理上的差别.

本文中MySQL的版本是8.0.15, TiDB的版本是v2.1.7, 事务隔离级别都为默认值Repeatable Read.

例1: Update在事务中处理逻辑不同

创建表t1, 语句如下:

CREATE TABLE `t1` (
  `a` int(11) NOT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`a`)
)

在MySQL和TiDB中分别执行下面的两个事务, 执行时序如下:

Time Tx1 Tx2
t0 begin;
t1 select * from t1;
t2 begin;
t3 insert into t1 values(1,10);
t4 commit;
t5 update t1 set b=b+10 where a = 1;
t6 commit;
t7 select * from t1;

MySQL, t7时刻,select语句的执行结果是

select * from t1;
+---+------+
| a | b    |
+---+------+
| 1 |   20 |
+---+------+
1 row in set (0.00 sec)

但在TiDB中执行结果却是:

mysql> select * from t1;
+---+------+
| a | b    |
+---+------+
| 1 |   10 |
+---+------+
1 row in set (0.01 sec)

对于MySQL,在可重复读级别下, 事务中的update操作, 为了不读到脏数据, 是当前读, 而且要加写锁. 虽然事务1的快照读是取的t1时刻的版本(MySQL默认的事务开始时间是事务执行的第一条语句), 但是在t5时刻的update操作, 是要读到当前最新版本的, 这时是能读到Tx2的insert语句的执行结果a=1这行的.

对于TiDB, update一直是快照读, t5时刻是读不到Tx2的已提交的a=1这行数据的, t5时刻的update操作自然没有修改到a=1这行数据.

例2 更新冲突处理逻辑不同

如果a=1这行在Tx1启动时已存在, Tx2在t3时刻更新了a=1这行的数据, 并完成提交. 同样, TiDB中,t5时刻Tx1在update时, 也是读到的过期数据, 但是在事务提交时, 才会报写冲突, 事务提交失败.

事务执行时序如下:

Time Tx1 Tx2
t0 begin;
t1 select * from t1;
t2 begin;
t3 update t1 set b=100 where a = 1;
t4 commit;
t5 update t1 set b=b+10 where a = 1;
t6 commit;
t7 select * from t1;

TiDB Tx1执行结果如下:

ERROR 1105 (HY000): [try again later]: WriteConflict: startTS=407855583704121345, conflictTS=407855586561228801, key={tableID=49, handle=1} primary={tableID=49, handle=1}

在MySQL中, 事务会提交成功, t7时刻select查询返回为:

mysql> select * from t1;
+---+------+
| a | b    |
+---+------+
| 1 |  110 |
+---+------+
1 row in set (0.00 sec)

例3 Select … For Update的执行区别

假设表t1中有(2,1)这行数据,事务执行时序如下:

Time Tx1 Tx2
t0 begin;
t1 select * from t1 where a = 2 for update;
t2
t3 update t1 set b=b+20 where a = 2;
t4
t5 update t1 set b=b*2 where a = 2;
t6 commit; (MySQL: Tx2 BLOCK UNTIL Tx1 commited)
t7 select * from t1 where a = 2;

MySQL,Tx1和Tx2都可以成功提交, t7时刻, select执行结果为:

select * from t1 where a = 2;
+---+------+
| a | b    |
+---+------+
| 2 |   22 |
+---+------+

但Tx2从t3时刻开始,会被阻塞,一直到t6时刻, Tx1完成提交后, Tx2才能提交.

TiDB中, Tx1提交会失败(WriteConflict). t7时刻, select执行结果为:

mysql> select * from t1 where a=2;
+---+------+
| a | b    |
+---+------+
| 2 |   21 |
+---+------+
1 row in set (0.00 sec)

小结

TiDB在事务提交时,检测冲突. 有冲突时,后提交事务的失败.

TiDB使用Percolator事务模型, 并发控制模型感觉像是Timestamp-Base + MutliVersion的综合, 是无锁的并发控制方案.

但是TiDB这样的分布式数据库系统, 如果像MySQL使用的锁机制解决冲突的话, 光分布式锁的开销将会非常大.