← 行间 技术 / TiDB
技术

TiDB系列(I)-从一段SQL来看TiDB和MySQL的差异

从一段SQL开始

让我们以 TiDB v8.5.0和 MySQL 8.0 作为测试环境,先初始化测试数据

# 首先初始化库和表以及测试需要的数据
CREATE DATABASE test;
CREATE TABLE test.student (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);
INSERT INTO test.student (name) VALUES ('John');
INSERT INTO test.student (name) VALUES ('Alice');
INSERT INTO test.student (name) VALUES ('Bob');
INSERT INTO test.student (name) VALUES ('Emma');

然后我们分别在两个环境按照顺序执行下列SQL

Process1 Process2
use test;
start transaction;
use test;
start transaction;
select name from student s where id=2 for update;
select name from student s where id=2 for update;
update student set name = ‘Eric’ where id = 1;
commit;
select * from student where id = 1;
commit;

我们会发现对于Process2select name from student s where id=2 for update;这一句SQL,TiDB和MySQL的输出是不同的

# TiDB中没有读取到Process1的修改
select * from student where id = 1;
+----+------+
| id | name |
+----+------+
|  1 | John |
+----+------+

# MySQL中读取到了Process1的修改
select * from student where id = 1;
+----+------+
| id | name |
+----+------+
|  1 | Eric |
+----+------+

# TiDB和MySQL的隔离级别一致
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

这是一个非常奇怪的结果,而且如果仔细想一想这实际上会造成很大的问题,举个例子,有些服务会把数据库当作分布式锁,,使用方式大致如下

  • 在某些时刻会有多个节点被触发
  • 所有触发节点尝试获取同一个行锁(类似于select name from student s where id=2 for update;
  • 先拿到行锁的服务完成操作,并更新一个记录(类似于update student set name = 'Eric' where id = 1;),包含时间以及其他信息,然后退出锁
  • 后拿到锁的服务发现记录被更新过了(类似于select * from student where id = 1;),即这个操作已经被完成了,因此不再进行操作,直接退出

这样在MySQL上执行起来确实没什么问题,因为回顾上述的流程会发现,在MySQL上Process2是可以读取到Process1更新的内容的,后续的节点会读取到第一个节点已经操作完成的记录,从而直接退出

然而某一天程序员看到TiDB的简介

TiDB 高度兼容 MySQL 5.7 协议、MySQL 5.7 常用的功能及语法。MySQL 5.7 生态中的系统工具(PHPMyAdmin、Navicat、MySQL Workbench、mysqldump、Mydumper/Myloader)、客户端等均适用于 TiDB。

觉得这完全没问题啊,随后把服务迁移到了TiDB上,那么从此之后倒霉的程序员就会发现一个奇怪的问题 - 分布式锁好像失效了,但又没完全失效。服务确实一次只能执行一个,然而不止第一个,而是所有服务都会执行操作

这个其实也很好理解,看上述流程就可以发现,Process2在TiDB上读取不到Process1的改动了,所以每个节点都认为自己是第一个,都会把操作执行一遍,如果操作是幂等的,那程序员顶多看着日志困惑不已,如果不是幂等的,那…

原因解析

首先我们会发现虽然TiDB和MySQL在这个SQL上的表现不一致,然而却都是符合Repeated-Read这个隔离等级的要求的,因此这并不代表TiDB出现了bug,而这个问题实际的根源是TiDB和MySQL对于一致性读的实现不同。

大致来讲TiDB在start transaction;时就生成了一致性读的快照,而MySQL中select name from student s where id=2 for update;是一个加锁读,此时并不生成一致性读快照

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句(第一个快照读语句),事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot 这个命令。

因此,对MySQL而言其实直到select * from student where id = 1;才生成一致性读快照,这就导致MySQL读到了Process1的改动内容而TiDB无法读取到

问题解决

如果只针对这个问题,其实解决也很简单,直接把TiDB的隔离级别从RR降低到RC即可,这样Process2就能读取到已经提交的内容了,但也有一些风险,是否有一些功能依赖RR来实现呢?这个我询问了一些社区的成员,感觉也不是很清楚,不过目前来看应该没什么问题就是了。