TiDB作为新一代分布式SQL数据库,它支持强一致性事务。ANSI SQL-92 对于事务的隔离级别有明确的定义,这也是大部分传统数据库(MySQL、PostgreSQL、Oracle、DB2、SqlServer等)都遵循的标准。TiDB的事务隔离级别并没有完全支持ANSI标准中的所有隔离级别,它缺省的隔离级别是"Snapshot Isolation"(简称SI),这种隔离级别类似于 ANSI 标准中的 "可重复读"(简称RR),但是与它又不完全相同:RR会发生“幻像读”,SI不会发生;RR不会发生“写偏斜(write skew)”,而SI会发生。准确理解TiDB的事务隔离级别是非常重要的,特别对于应用开发人员来说,如果不了解TiDB事务隔离级别的特点以及"乐观锁"的特点,那么就无法开发出正确的应用。下面将结合一些例子对TiDB的SI隔离级别和乐观锁机制进行详细的说明,希望对大家能有所帮助。
1. 创建示例数据库和示例表
这里使用的TiDB环境,假设用户已经阅读过本人写的文章“使用Docker Compose快速搭建一个单机TiDB集群”。为了很好的理解这些例子,希望大家能动手试一试。
(1) 使用mysql client连接到TiDB
mysql -h 127.0.0.1 -P 4000 -u root
(2) 创建数据库
create database bankdb;
(3) 创建用户表
create table account ( id int, name varchar(8), balance decimal, primary key (id) );
(4) 插入示例数据
insert into account values(1,'user1',100) , (2,'user2',100);
2. SI隔离级别不会发生"幻像读"
事务1 事务2
begin;
select count(*)
from account
where balance =100; begin;
insert into account values(3,'user3',100);
commit;
select count(*)
from account
where balance = 100;
commit;
说明:事务1执行第一次查询之后(查询结果是2,即余额为100元的记录有2条),事务2开始执行insert语句插入一条余额为100元的记录并提交事务。事务1继续执行第2个查询(在事务2已经提交之后执行),对于TiDB来讲执行结果仍然为2,即看不到事务2已提交的新增满足查询条件的记录,也就是说TiDB的SI隔离级别不会发生"幻像读。
3. SI隔离级别会发生" 写偏斜(write skew)"
什么是写偏离?我们结合一个例子来说明:
事务1 事务2
begin; begin;
set @bal1 = (select balance set @bal1 = (select balance
from account where id = 1); from account where id = 1);
set @bal2 = (select balance set @bal2 = (select balance
from account where id = 2); from account where id = 2);
select @bal1 + @bal2 select @bal1 + @bal2
update account set bal = bal -200
where id =2 and
(@bal1+@bal2 -200)>=0;
commit;
update account set bal = bal -200
where id = 1 and
(@bal1+@bal2 -200) >=0;
commit;
说明:
(1) 上面的事务1和事务2都要保证id为1和id为2的两个账户余额之和始终要大于等于0这一规则,包括在扣减账户余额之后也要遵循这一规则。TiDB中运行这两个事务,在缺省的隔离级别SI下是不能保证这一规则的。 事务1和事务2在执行完select @bal1 + @bal2时,返回的都是200 。事务2先执行update语句,扣减id为2的账户余额200元,事务提交、执行成功(这时id=2的账户余额为-100元,满足上面的规则);事务1在执行update语句扣减id 为1的账户余额200元也能执行成功,这是因为SI隔离级别使得事务1看不到事务2对于id=2记录的更新,它看到的仍然是事务1开始时的值(即id=2账户的余额 100 元),所以事务1会更新id=1的记录成功。两个事务执行完后,id为1的账户和id为2的账户余额都是 -100 元,两者之和已经不满足大于等于0这个规则,即发生了 "写偏斜" 。传统数据库在RR隔离级别下,一般是采用"悲观锁"机制对事务命中的记录行加锁(例如,事务1执行的查询语句会对id=1和id=2两条记录加 "共享行锁",这个锁一直到保持到事务结束才释放),所以能保证不会发生写偏斜。
(2) TiDB采用的是 "乐观锁" 机制,不会通过 "加锁"来阻塞其它并发运行的事务,TiDB是在事务提交的时候才检查事务之间是否发生冲突。如果有冲突(即不同事务并发修改了相同的记录行),TiDB会自动进行重试;如果自动重试失败,TiDB会返回错误消息给客户端。对于上面的例子,事务1和事务2不会发生写写冲突(因为,事务1修改的是id=1的记录,而事务2修改的是id=2的记录)。
为了解决TiDB SI隔离级别的写偏斜问题,PingCap官方给出的方法是使用select * from for update语句,上面示例中两个事务中的
set @bal1 = (select bal from account where id = 1 );
set @bal2 = (select bal from account where id = 2 );
都要要改为:
set @bal1 = (select bal from account where id = 1 for update);
set @bal2 = (select bal from account where id = 2 for update);
这样做就可以告知TiDB,在事务提交时会检查这两条记录上是否会有 “冲突” 。如果监测到冲突,失败的事务会报如下信息:
ERROR 8002 (HY000): [1] can not retry select for update statement
应用捕捉到这个错误信息,需要自己进行后续的处理。
通过前面的例子,大家应该对TiDB的事务隔离级别的特点以及它所采用的"乐观锁"并发控制机制有一定了解了。这里再做一下小结:
(1) TiDB支持ANSI SQL-92标准中的“可重复读”事务隔离级别,这也是它的缺省隔离级别。对于“可重复读”隔离级别,在TiDB中叫做“Snapshot Isolation”(快照隔离级别,简称SI),这种隔离级别不会产生“幻像读”,但是会产生写偏斜(write skew)。
(2) TiDB 使用乐观锁模型,在事务中执行Update、Insert、Delete、Select等语句时不像某些传统RDBMS那样使用行级锁锁定相关记录行,只有在事务真正提交时才会检查写写冲突。如果有冲突(即不同事务并发修改了相同的记录行),TiDB会自动进行重试;如果自动重试失败,TiDB会返回错误消息给客户端。应用端需要注意检查 commit 的返回值,即使事务执行时没有出错,commit的时候也可能会出错。
(3) 为了解决SI隔离级别的写偏斜问题,需要应用开发人员使用select * from for update。