CockroachDB SQL开发基础 ——事务隔离级别和并发控制介绍 (1)

     CockroachLabs公司的CockroachDB(简称CRDB)是一款适合于海量数据环境下高并发OLTP和OLAP场景的开源分布式NewSQL/HTAP数据库。它采用了新的架构和设计、新的分布式理论和算法,吸收了关系型数据库和NoSQL数据库各自的优点,无需应用级的分库分表,具有SQL模型的简单易用、水平弹性扩展、支持强一致性分布事务、异地多活的高可用、云原生等一系列特点。

     CRDB作为新一代分布式New SQL数据库,它支持ACID事务。ANSI SQL-92 对于事务的隔离级别有明确的定义,这也是大部分传统数据库(MySQL、PostgreSQL、Oracle、DB2、SqlServer等)都遵循的标准。CRDB(PingCap的TiDB也类似)并没有完全支持ANSI标准中的所有隔离级别,它支持如下两种事务隔离级别:

  • 快照隔离级别(Snapshot Isolation,简称SI)
  • 串行化快照隔离级别(Serializable Snapshot Isolation,简称SSI)

     SI隔离级别实现相对简单,性能较好。这种隔离级别类似于 ANSI 标准中的 "可重复读"(简称RR),但是与它又不完全相同:RR会发生“幻像读”,SI不会发生;RR不会发生“写偏斜(write skew)”,而SI会发生。在对性能要求较高,并且“write skew”对于应用不是问题的情况下可使用SI隔离级别。

     相比而言,SSI实现上稍微复杂一些,但仍然能保证较高性能(读写冲突严重的情况下性能会有下降),但是不存在“写偏斜(write skew)”。在CRDB中,SSI是默认的事务隔离级别,用户须根据业务的要求以及实际性能情况,选择合适的隔离级别。

     准确理解CRDB的事务隔离级别和并发控制行为是非常重要的,特别对于应用开发人员来说,如果不了解这些,那么就无法开发出正确的应用。CRDB的官方文档有对其事务的详细介绍,感觉有些复杂,而且有些地方描述含混,甚至是有笔误的地方。下面将结合一些例子对CRDB的两种隔离级别进行说明,力图简洁、清晰地让大家了解其事务隔离级别和并发控制的行为特点,希望对大家能有所帮助。本文介绍SI隔离级别,在下一篇文章中介绍SSI隔离级别。

  1. SI隔离级别

     在CRDB中,运行在SI隔离级别的事务具有如下行为特点:

      (1) 不会发生“脏读”、“不可重复读”和“幻象读”,但是会发生写偏斜。

      (2) CRDB还是会在事务的运行期间,在某些情况下使用"write lock"的,所以叫做Lockless,而不是Lock free。"write lock"的生命周期是事务级的(事务提交或回滚后才释放)。在SI隔离级别下两个运行中的事务,若对相同表的相同记录进行更新,先操作的会获得"write lock",阻塞后操作的事务,直到先操作的事务提交或回滚之后锁才释放,被阻塞的事务才能继续运行;对同一行记录的读和写之间不会发生阻塞。

      下面我们结合示例来进行体会(下面的示例使用的是CockroachDB 2.0.6版本):

      (1) 创建用户表account并插入数据

create table account ( id int, name varchar(8), balance decimal(15,2), primary key (id) );

insert into account values(1,'user1',100) , (2,'user2',100);

      (2) SI隔离级是“可重复读”的

      在两个终端中,分别执行CRDB的命令行工具cockroach sql --insecure,然后在一个终端中交互式运行事务1,在另外一个终端中交互是运行事务2(在后面的示例中都是如此)。运行的时序和事务如操作如下:

 事务1                                                     事务2

 begin;                       

   set transaction isolation

         level snapshot;                           

   select balance                                                            

   from account 

   where id =1;                                        begin;

                                                                  set transaction isolation

                                                                        level snapshot;  

                                                                  update account

                                                                  set balance = balance + 5;

                                                                  where id = 1

      select  balance

      from  account 

      where id = 1;                                     

                                                               commit;

      select balance

      from account

      where id = 1; 

      update account

       set balance = balance - 10;

       where id = 1;

       select balance

       from account

       where id = 1;

       commit;

       说明: 事务1在第一次读取账户id为1的记录的余额值为100,事务2更新相同的记录,把余额加5;事务1再次读取,读到值仍然为100,这说明SI隔离级别不会发生“脏读”。 在事务1第二次读取之后,事务2提交更新,这时id=1的账户的余额为105。事务1进行第三次读取,读取到的余额仍然是100;随后它也更新id=1的账户余额,把余额减少10元,然后第四次读取,得到的值是90。随后,事务1提交,提交时CRDB检测发现id=1的记录在事务1开始后,已经被另一个已提交的事务(事务2)更新过,这就发生了冲突,CRDB会abort事务并回滚它。如下图所示:

        一定要注意:在SI隔离级别下只能读到事务启动时已经提交的其他事务修改的数据,其它事务未提交的数据或在事务启动后其他事务提交的数据是不可见的。此外,SI隔离级别下,不能并发的更新同一行。

      (3) SI隔离级别不会发生"幻像读"

      在两个终端中,分别执行CRDB的命令行工具cockroach sql --insecure,然后在一个终端中交互式运行事务1,在另外一个终端中交互是运行事务2。运行的时序和事务如操作如下:

 事务1                                                     事务2

 begin;                       

   set transaction isolation

         level snapshot;                           

   select count(*)                                                             

   from account 

   where balance =100;                          begin;

                                                                  set transaction isolation

                                                                        level snapshot;  

                                                                  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已经提交之后执行),对于CRDB来讲执行结果仍然为2,即看不到事务2已提交的新增满足查询条件的记录,也就是说CRDB的SI隔离级别不会发生"幻像读。

 

     (4) SI隔离级会发生"写偏斜(write skew)"

 事务1                                                  事务2

  begin;                                                 begin;

    set transaction isolation

          level sanpshot;                             set transaction isolation

                                                                     level sanpshot;     

                                                               update account 

                                                               set balance = balance - 200

                                                              where id =2 and (

                                                                   (select balance

                                                                       from account where id =1) +

                                                                   (select balance

                                                                       from account where id = 2)

                                                                   -200

                                                              ) >=0 ;

                                                              commit;

                                        

   update account 

   set balance = balance - 200

   where id =1 and (

        (select balance

             from account

          where id =1)  +

        (select balance

           from account

        where id = 2)

         -200

      )  >=0 ;

commit;

说明:

    A. 上面的事务1和事务2都要保证id为1和id为2的两个账户余额之和始终要大于等于0这一规则,包括在扣减账户余额之后也要遵循这一规则。

    B. 在CRDB中运行这两个事务,在隔离级别SI下是不能保证这一规则的。 事务1和事务2在执行update语句时由于WHERE条件中嵌套执行的SELECT语句得到的两个账户之和都符合条件。事务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这个规则,即发生了 "写偏斜" 。

    C. 对于上面的例子,事务1和事务2也不会发生写写冲突。因为,事务1修改的是id=1的记录,而事务2修改的是id=2的记录,所以能够正常执行成功。

    D. 传统数据库在RR隔离级别下,一般是采用"悲观锁"机制对事务命中的记录行加锁(例如,事务1执行的查询语句会对id=1和id=2两条记录加 "共享行锁",这个锁一直到保持到事务结束才释放),所以能保证不会发生写偏斜。

 

   (5)在SI下的两个事务运行期间,对相同记录进行更新,先操作的会阻塞后操作的事务

  事务1                                             事务2

  begin;                                            begin;

     set transaction isolation

        level sanpshot;                           set transaction isolation

                                                              level sanpshot;     

    update account

    set balance = 120

    where id = 1;

                                                           select *

                                                           from account

                                                           where id = 1;     --不会阻塞

                                                           update account

                                                           set balance = 150

                                                           where id = 1;     --被阻塞,直到事务1结束

    commit;

                                                           --上面阻塞的update执行完成

                                                           select * 

                                                            from account

                                                            where id = 1;

                                                            commit;            -- 读取到余额为150

 

猜你喜欢

转载自blog.csdn.net/u011782423/article/details/83213137