数据库事物的四大特性和事务的隔离级别

数据库事物的四大特性(ACID)和事务的隔离级别。

数据库事务的四大特性

一个数据库若声称支持事务的操作,那么该数据库必须要具备ACID四大特性

原子性(Atomicity)

原子性是指事务包含的操作要么全部成功,要么全部失败回滚。因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

一致性(Consistency)

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账举例,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

隔离性(Isolation)

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其它事务的操作所干扰,多个并发事务之间要相互隔离。
即,对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都无法感觉到有其它事务在并发执行。
对于事务的隔离性,数据库提供了多种隔离级别。

持久性(Durability)

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即使在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务已经正确提交,即使此时数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

事务缺乏隔离性的问题

当多个线程都开启事务操作数据库中的数据时,数据库要能进行隔离操作,以保证各个线程获取数据的准确性,如果不考虑事务的隔离性,就会发生以下几种问题。

脏读

脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下

1
2
update account set money=money+100 where name='B'; #(此时A通知B)
update account set money=money-100 where name='A';

当只执行第一条SQL时,A通知B查看账户,B发现钱确实已经到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转成功。

不可重复读

不可重复读是指在数据库中的某个数据,一个事物范围内多次查询却反悔了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发生了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了…

幻读

幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从”1”修改为”2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为”1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点和脏读有区别),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。参考MySQL官方文档对 Phantom Rows 的介绍

幻读和不可重复读区别

  • 不可重复读 主要是说多次读取一条记录, 发现该记录中某些列值被修改过。
  • 幻读 主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。

幻读的演示

  1. 打开客户端1查看隔离级别和初始数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    mysql> SELECT @@SESSION.tx_isolation;
    +------------------------+
    | @@SESSION.tx_isolation |
    +------------------------+
    | REPEATABLE-READ |
    +------------------------+
    1 row in set (0.00 sec)

    mysql> select * from test_transaction;
    +----+-----------+-----+--------+--------------------+
    | id | user_name | age | gender | desctiption |
    +----+-----------+-----+--------+--------------------+
    | 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
    | 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
    | 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
    +----+-----------+-----+--------+--------------------+
    3 rows in set (0.00 sec)

    mysql>
  2. 打开客户端2查看隔离级别及初始数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    mysql> SELECT @@SESSION.tx_isolation;
    +------------------------+
    | @@SESSION.tx_isolation |
    +------------------------+
    | REPEATABLE-READ |
    +------------------------+
    1 row in set (0.00 sec)

    mysql> select * from test_transaction;
    +----+-----------+-----+--------+--------------------+
    | id | user_name | age | gender | desctiption |
    +----+-----------+-----+--------+--------------------+
    | 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
    | 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
    | 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
    +----+-----------+-----+--------+--------------------+
    3 rows in set (0.00 sec)

    mysql>
  3. 在客户端2中开启事务,然后查询数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)

    mysql> select * from test_transaction;
    +----+-----------+-----+--------+--------------------+
    | id | user_name | age | gender | desctiption |
    +----+-----------+-----+--------+--------------------+
    | 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
    | 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
    | 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
    +----+-----------+-----+--------+--------------------+
    3 rows in set (0.00 sec)

    mysql>
  4. 在客户端1中插入一条id为4的新数据(直接自动提交)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
    Query OK, 1 row affected (0.00 sec)
    mysql> select * from test_transaction;
    +----+-----------+-----+--------+--------------------+
    | id | user_name | age | gender | desctiption |
    +----+-----------+-----+--------+--------------------+
    | 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
    | 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
    | 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
    | 4 | 死侍 | 18 | 0 | A bad boy |
    +----+-----------+-----+--------+--------------------+
    4 rows in set (0.00 sec)

    mysql>
  5. 在客户端2事务中再次查询数据,发现数据没有变化(表示可以重复读, 并且克服了幻读)!! 但是在客户端2事务中插入一条id为4的新数据, 发现提示数据已经存在!!!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    +----+-----------+-----+--------+--------------------+
    | id | user_name | age | gender | desctiption |
    +----+-----------+-----+--------+--------------------+
    | 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
    | 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
    | 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
    +----+-----------+-----+--------+--------------------+
    3 rows in set (0.00 sec)

    mysql> select * from test_transaction;
    +----+-----------+-----+--------+--------------------+
    | id | user_name | age | gender | desctiption |
    +----+-----------+-----+--------+--------------------+
    | 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
    | 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
    | 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
    +----+-----------+-----+--------+--------------------+
    3 rows in set (0.00 sec)

    mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
    1062 - Duplicate entry '4' for key 'PRIMARY'
    mysql>

    //并且, 此时`update/delete`也是可以操作这条在事务中看不到的记录的!

原因

参考MySQL官方文档 – 一致性非阻塞读

The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction.
个人认为应该翻译为: 数据库状态的快照适用于事务中的SELECT语句, 而不一定适用于所有DML语句。 如果您插入或修改某些行, 然后提交该事务, 则从另一个并发REPEATABLE READ事务发出的DELETE或UPDATE语句就可能会影响那些刚刚提交的行, 即使该事务无法查询它们。 如果事务更新或删除由不同事务提交的行, 则这些更改对当前事务变得可见。

MVCC并发控制中的读操作可以分成两类: 快照读 (snapshot read)当前读 (current read)

  • 快照读, 读取专门的快照 (对于RC,快照(ReadView)会在每个语句中创建。对于RR,快照是在事务启动时创建的)
    简单的select操作即可(不需要加锁,如: select ... lock in share mode, select ... for update)
    针对的也是select操作

  • 当前读, 读取最新版本的记录, 没有快照。 在InnoDB中,当前读取根本不会创建任何快照。

    1
    2
    select ... lock in share mode
    select ... for update

    针对如下操作, 会让如下操作阻塞:

    1
    2
    3
    insert
    update
    delete
  • 在RR级别下, 快照读是通过MVVC(多版本控制)和undo log来实现的, 当前读是通过手动加record lock(记录锁)和gap lock(间隙锁)来实现的。所以从上面的显示来看,如果需要实时显示数据,还是需要通过加锁来实现。这个时候会使用next-key技术来实现。

当然, 使用隔离性的最高隔离级别SERIALIZABLE也可以解决幻读, 但该隔离级别在实际中很少使用。


数据库隔离级别

READ_UNCOMMITED

  • 事务对当前被读取的数据不加锁;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,能读到事务2对该记录的修改版本,即使该修改记录尚未被提交。
  • 事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。

READ_COMMITED

  • 事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该行记录进行更新时,事务1再次读取该记录,读到的只能是事务2对其更新前的版本,要不就是事务2提交后的版本。
  • 事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。

简而言之,提交读这种隔离级别保证了读到的任何数据都是提交的数据,避免读到中间的未提交的数据,脏读(dirty reads)。但是不保证事务重新读的时候能读到相同的数据,因为在每次数据读完之后其他事务可以修改刚才读到的数据。
提交读保证我们读到的数据都是事务提交后的数据,这样子就保证了程序中数据的正确性,在我们的应用中,基本都是使用的Read Commited隔离级别。

REPEATABLE READ

  • 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。

表现:

  • 事务1读取某行数据时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,读到的仍然是第一次读取的那个版本。
  • 事务1更新某行记录时,事务2不能对这行记录进行更新,直到事务1结束。

SERIALIZABLE

  • 事务在读取数据时,必须先对其加 表级共享锁,直到事务结束才释放;
  • 事务在更新数据时,必须先对其加表级排他锁,直到事务结束才释放。

表现:

  • 事务1正在读取A表中的记录时,则事务2也能读取A表,但不能对A表做更新、新增、删除,直到事务1结束。
  • 事务1正在更新A表中的记录时,则事务2不能读取A表的任意记录,更不可能对A表做更新、新增、删除,直到事务1结束。

转载来源:

  1. 数据库事务的四大特性以及事务的隔离级别
  2. 搞懂 不可重复读和幻读
  3. 数据库隔离级别 及 其实现原理
如果文章对您有帮助,感谢您的赞助支持!