TiDB事务隔离级别和并发控制的特点详述

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u011782423/article/details/81064051

       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。

         

               

 

展开阅读全文

没有更多推荐了,返回首页