1. 概述
事务就是用户定义的一系列执行SQL语句的操作, 这些操作要么完全地执行,要么完全地都不执行, 它是一个不可分割的工作执行单元。
开启事务:start transaction/begin;
提交事务:commit;
回滚事务:rollback;
事务没提交前,实际界面不显示删除,在搜索中显示
2. 特性
2.1. 原子性(Atomicity)
原子性 指一个数据库事务中的所有操作是不可分割的单元,只有事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态
2.2. 一致性(Consistency)
一致性 指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
2.3. 隔离性(Isolation)
隔离性还有其他的称呼,如并发控制、可串行化、锁等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。
当数据库上有多个事务同时执行的时候,就可能出现脏读、不可重复读、幻读的问题,这块我们后面会再细说。
2.4. 持久性(Durability)
持久性要求事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。
需要注意的是,持久性是保证事务系统的高可靠性,而不是高可用性。事务本身能保证结果的永久性,在事务提交后,所有的变化都是永久的。但对于一些外部因素,如磁盘损坏、自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。对于高可用性的实现,事务本身并不能保证,需要一些系统共同配合来完成。
3. 分类
- 扁平事务(Flat Transactions)
- 带有保存点的扁平事务(Flat Transactions with Savepoints)
- 链事务(Chained Transactions)
- 嵌套事务(Nested Transactions)
- 分布式事务(Distributed Transactions)
对于InnoDB存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。对于嵌套事务,其并不原生支持。
3.1. 扁平事务
扁平事务是事务类型中最简单的一种,也是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,由 BEGIN/START TRANSACTION 开始,由 COMMIT 或 ROLLBACK 结束,其间的操作是原子的。
3.2. 带有保存点的扁平事务
带有保存点的扁平事务允许在事务执行过程中回滚到同一事务中较早的一个状态。我们可以在事务过程中设置一些保存点(Savepoint),保存点用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。
对于扁平事务来说,其在事务开始的时候隐式地设置了一个保存点,扁平事务就只有这一个保存点,因此,回滚只能回滚到事务开始时的状态。
可以通过 SAVEPOINT 创建一个保存点,ROLLBACK TO SAVEPOINT 回滚到某个保存点。
3.3. 链事务
链事务就是一个事务在提交的时候自动将上下文传给下一个事务,也就是说一个事务的提交和下一个事务的开始是原子性的,下一个事务可以看到上一个事务的结果,就好像在一个事务中进行的一样。
链事务可视为保存点模式的一种变种,不同的是,带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限于当前事务。
MySQL 的链式事务可以通过 SET completion_type = 1 来打开,后面会举例说明。
3.4. 嵌套事务
嵌套事务是一个层次结构框架,由一个顶层事务控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务,其控制每一个局部的变换。子事务提交后不会真的提交,而是等到父事务提交才真正的提交,父事务回滚了,会回滚所有子事务。
MySQL 不支持嵌套事务,不过我们可以通过带有保存点的事务来模拟串行的嵌套事务。
3.5. 分布式事务
分布式事务通常是一个在分布式环境下运行的扁平事务,需要根据数据所在位置访问网络中的不同节点。后面会有一个专门的系列来学习分布式事务。
4. 并发事务带来的问题
我们的业务系统往往都是多线程并发执行多个事务,数据库层面也会多个事务并发执行,那么就可能会对同一条数据查询和修改。既然是并发,跟Java中的多线程一样,就会有线程安全问题。
并发事务涉及到四个问题:脏写、脏读、不可重复读、幻读。
按问题的严重程度排个序就是:脏写 > 脏读 > 不可重复读 > 幻读。
4.1. 脏读
如果一个事务A读到了另一个事务B修改过的未提交的数据,那事务A的读取就是脏读
4.2. 不可重复读
在没有脏读的情况下,如果一个事务多次读取同一个数据不一致,那说明发生了不可重复读的问题,也就是同一个数据无法重复读取,违反了数据库事务一致性的要求。
4.3. 幻读
幻读就是一个事务用同样的条件查询,由于另一个事务新增了数据,导致看到了之前没有的数据。
4.4. 脏写
脏写也称为数据丢失、更新丢失,简单来说就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。
有两类情况会导致脏写:
1、事务A回滚把事务B已提交的修改给覆盖了,就会造成事务B的修改丢失。
不过InnoDB存储引擎不会发生这个问题,因为InnoDB在更新数据时加了排他锁, 这样在事务A在未完成的时候, 其他事务是无法对事务A涉及到的数据做修改并提交的。例如上面的示意图中,事务B在执行余额增加200的时候,由于事务A修改了同一条数据且未提交,这时这条数据已经加了排它锁了,因此事务B修改时会阻塞住,等待加锁后才能修改。
2、事务A覆盖了事务B已提交的修改,造成事务B的修改丢失。
这种情况有两种方式可以避免脏写发生:
- 基于数据库悲观锁
在查询时使用 for update 实现一个排它锁,保证在该事务结束时其他事务无法更新该数据。不过这样就会导致并发更新的性能降低。
SELECT * FROM account WHERE id = 1 FOR UPDATE;
- 基于乐观锁
可以在表中增加一个版本号字段,查询时将版本号查出来,更新时带上版本号作为条件,更新成功则是同一条记录,否则就时更新失败。更新失败就可以返回“记录不存在或版本不一致”这样的错误,让用户可以重新查询再更新一次。
UPDATE account SET balance=balance+100, version=version+1 where id = 1 and version = 1
5. 事务隔离级别
前面说到并发事务有四个问题:脏写、脏读、不可重复度、幻读。其中,脏写可以通过乐观锁或悲观锁的方式来解决,剩下的3个问题,其实是数据库读一致性造成的,需要数据库提供一定的事务隔离机制来解决,也就是事务的隔离性。
SQL标准定义了四个隔离级别:
- READ UNCOMMITTED:读未提交,简称 RU。
- READ COMMITTED:读已提交,简称 RC。
- REPEATABLE READ:可重复读,简称 RR。
- SERIALIZABLE:可串行化。
不同的隔离级别,分别能解决一部分事务问题,具体情况可查看下面的表格。
- READ UNCOMMITTED:会发生脏读、不可重复读、幻读的问题。
- READ COMMITTED:会发生不可重复读、幻读的问题,不会发生脏读的问题。
- REPEATABLE READ:会发生幻读的问题,不会发生脏读、不可重复读的问题。
- SERIALIZABLE:脏读、不可重复读、幻读的问题都不会发生。
6. MYSQL的事务隔离级别
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
7. 事务特性实现方式
7.1. 持久性(D)
事务的持久性通过数据库的redo log来实现,redo log称为重做日志。在更新Buffer Pool中的数据页时,会同时记录对应的 redo log,这样就算脏页没有刷盘,在MySQL宕机重启时,也可以通过 redo log 来恢复数据。
7.2. 原子性(A)
事务的原子性通过数据库的undo log来实现,undo log称为撤销日志或回滚日志。在一个事务中进行增删改操作时,都会记录对应的 undo log。
- 当 delete 一条数据的时候,就记录这条数据的信息,回滚的时候,insert 这条旧数据
- 当 update 一条数据的时候,就记录之前的旧值,回滚的时候,根据旧值执行 update 操作
- 当 insert 一条数据的时候,就这条记录的主键,回滚的时候,根据主键执行 delete 操作
而且 undo log 形成的版本链还用于实现多版本并发控制(MVCC),InnoDB的RC和RR隔离级别就是是基于MVCC来实现高性能事务,而且通过MVCC来避免幻读的发生。
7.3. 隔离性(I)
事务的隔离性由锁来实现,不同的加锁方式,可以实现不同的事务隔离机制。
7.4. 一致性(C)
事务的一致性需要两个层面来保证:
- 数据库层面:数据库必须要实现AID三大特性,才有可能实现一致性。
- 应用层面:如果在事务里故意写出违反约束的代码,一致性还是无法保证的。应用层面应该通过代码判断数据库数据是否有效,然后决定回滚还是提交数据。