事务是什么

事务就是指逻辑上的一组SQL语句操作,组成这组操作的各个SQL语句,执行时要么全成功要么全失败。

在 MySQL 中,事务支持是在引擎层实现的。MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。

比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一

事务的四大特性

  1. 原子性(Atomicity)
    • 事务是一个不可分割的单位,事务中的所有SQL等操作要么都发生,要么都不发生。
  2. 一致性(Consistency)
    • 事务发生前和发生后,数据的完整性必须保持一致。
  3. 隔离性(Isolation)
    • 当并发访问数据库时,一个正在执行的事务在执行完毕前,对于其他的会话是不可见的,多个并发事务之间的数据是相互隔离的。也就是其他人的操作在这个事务的执行过程中是看不到这个事务的执行结果的,也就是他们拿到的是这个事务执行之前的内容,等这个事务执行完才能拿到新的数据。
  4. 持久性(Durability)
    • 一个事务一旦被提交,它对数据库中的数据改变就是永久性的。如果出了错误,事务也不允撤销,只能通过’补偿性事务’。

事务的开启

  • 开启:

    begin/start transaction 执行第一个语句是开启事务

    start transaction with consistent snapshot 直接开启事务

    隐性事务:set autocommit=0,这个命令会将这个线程的自动提交关掉。

    意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

    有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。

  • 提交:commit

  • 回滚:rollback

在事务中混合使用存储引擎

MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。

如果在事务中混合使用了事务型和非事务型的表(例如innodb和myisam表),在正常提交的情况下不会有什么问题。

但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。

所以,为每张表选择合适的存储引擎非常重要。

在非事务型的表上执行事务相关操作的时候,MySQL通常不会发出提醒,也不会报错。有时候只有回滚的时候才会发出一个警告:“某些非事务型的表上的变更不能被回滚”。

但大多数情况下,对非事务型表的操作都不会有提示。

脏读 幻读 不可重复读

脏读

所谓脏读是指一个事务中访问到了另外一个事务未提交的数据,如下图:

s1s2
beginbegin
update test set number = 100 where id = 1;
select number from test where id = 1;
commitcommit
  • 如果会话 2 更新 number 为 100,但是在 number 之前,会话 1 希望得到 number,那么会获得的值就是更新前的值。或者如果会话 2 更新了值但是执行了 rollback,而会话 1 拿到的仍是 100。这就是脏读。

不可重复读

一个事务查询同一条记录2次,得到的结果不一致:

S1S2
beginbegin;
select number from test where id = 1;
update test set number = 200 where id = 1;
commit
select number from test where id = 1;
commit;
  • 由于在读取中间变更了数据,所以会话 1 事务查询期间的得到的结果就不一样了。

幻读

一个事务查询2次,得到的记录条数不一致:

S1S2
beginbegin
select number from test where id < 5;
insert into test value(2,3);
commit
select number from test where id < 5;
commit
  • 幻读是不可重复读的一种特殊场景。

事务的隔离级别

MySQL 里有四个隔离级别:

READ UNCOMMITTED(读未提交)

在read uncommitted级别,事务中的修改 ,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读。

这个级别会导致很多问题,从性能上来说,read uncommitted不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真个有非常必要的理由,在实际应用中一般很少使用。

READ COMMITTED(提已提交)

大多数数据库系统的默认隔离级别都是READ COMMITTED(但MySQL不是)。

READ COMMITTED满足前面提到的隔离性的简单定义:一个事务开始时,只能看见已经提交的事务所做的修改。

换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的,这个级别有时候也叫做不可重复读。

不可重复读现象:当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象成为不可重复读。

REPEATABLE READ(读可重复读)

REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读的问题。

所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。

可重复读是MySQL的默认事务隔离级别。

SERIZLIZABLE(可串行化)

SERIZLIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。

简单来说,SERIZLIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。

实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。

不同事务隔离级别的效果:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED(未提交读)
READ COMMITTED(提已提交)
REPEATABLE READ(可重复读)
SERIZLIZABLE(可串行化)

在 InnoDB 中,默认为 Repeatable 级别,InnoDB 中使用一种被称为 next-key locking 的策略来避免幻读(phantom)现象的产生。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

事务隔离的实现

隔离级别为默认可重复读

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。 记录上的最新值,通过回滚操作,都可以得到前一个状态的值。 假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。 如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4, 同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。 对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。 同时你会发现, 即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的

参考文章