参考文章
第13章_事务基础知识.pdf · 资料文件 · 语雀

1 事务

Transaction Control Language:事务控制语言。MySQL 中,只有InnoDB 是支持事务的。
事务的四个特性:ACID
ACID是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。
数据库事务,其实就是数据库设计者为了方便起见,把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称为一个事务。

  1. 原子性(Atomicity,或称不可分割性)

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

  1. 一致性(Consistency)

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行数据都是合法的状态(即数据要继续满足我们定义的约束,如主键、外键、唯一性、转账后数据要一致等。

  1. 这个合法的定义是语义上的而不是语法上的,跟具体的业务有关。满足预定的约束的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的。如果事务中的某个操作会破坏这个合法性,那么系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
  2. 数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
    1. 隔离性(Isolation)

事务的隔离性是指一个事务的执行不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。Java并发编程。

  1. 持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故暲不应该对其有任何影响。
持久性是通过事务日志来保证的:日志包括了重做日志和回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进
行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

事务的状态:五大状态
MySQL根据这些操作所执行的不同阶段把事务大致划分成几个状态

  1. 活动的(active)

事务对应的数据库操作正在执行过程中时,我们就说该事务处在活动的状态。

  1. 部分提交的(partially committed)

当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在部分提交的状态。

  1. 失败的(failed)

当事务处在 活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务
处在 失败的状态。

  1. 中止的(aborted)

如果事务执行了一部分而变为失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的
过程称之为回滚 。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的状态。

  1. 提交的(committed)

当一个处在部分提交的状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了提交的状态。
image.png

2 事务的使用

隐式事务

事务的自动提交
隐式事务的前提就是事务的自动提交功能:MySQL中有一个系统变量autocommit,默认情况下,如果我们不显式的使用START TRANSACTION或者BEGIN语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交
也就是说,不以START TRANSACTION或者BEGIN语句显式的开启一个事务,那么下边这两条语句就相当于放到两个独立的事务中去执行:

UPDATE account SET balance = balance - 10 WHERE id = 1; //单独的事务 UPDATE account SET balance = balance + 10 WHERE id = 2; //单独的事务

隐式提交数据的情况

  1. 数据定义语言(Data definition language,缩写为:DDL)
    • 即使关闭了自动提交也没用的。
    • 数据库对象,指的就是数据库、表、视图、存储过程等结构。当我们使用CREATE、ALTER、DROP等语句去修改数据库对象时,就会隐式的提交前边语句所属于的事务。

image.png

  1. 隐式使用或修改mysql数据库中的表
    • 即使关闭了自动提交属性也是没用的。
    • 当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SETPASSWORD等语句时也会隐式的提交前边语句所属于的事务。
  2. 事务控制或关于锁定的语句
    • 当我们在一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务。即:

image.png

  • 当前的autocommit系统变量的值为OFF,我们手动把它调为ON时,也会隐式的提交前边语句所属的事务。
  • 使用LOCK TABLESUNLOCK TABLES等关于锁定的语句也会隐式的提交前边语句所属的事务。
    1. 加载数据的语句

使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。

  1. 关于MySQL复制的一些语句

使用START SLAVESTOP SLAVERESET SLAVECHANGE MASTER TO等语句时会隐式的提交前边语句所属的事务。

  1. 其它的一些语句

使用ANALYZE TABLECACHE INDEXCHECK TABLEFLUSHLOAD INDEX INTO CACHEOPTIMIZE TABLEREPAIR TABLERESET等语句也会隐式的提交前边语句所属的事务。

关闭事务的自动提交
当然,如果我们想关闭这种自动提交的功能,可以使用下边两种方法之一:

  • 显式的的使用START TRANSACTION或者BEGIN语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能(即下文的显示事务)。
  • 把系统变量autocommit的值设置为OFF:SET autocommit = OFF;SET autocommit = 0;

这样的话,我们写入的多条语句就算是属于同一个事务了,直到我们显式的写出COMMIT语句来把这个事务提交掉,或者显式的写出ROLLBACK语句来把这个事务回滚掉(默认为ROLLBACK)。

  1. #1.0 设为不自动提交
  2. set autocommit=0;
  3. #2.0 执行sql语句时将自动开启事务#
  4. update dream_datas set title='使用set autocommit=0来开启' where id=1;
  5. update dream_datas set title='使用set autocommit=0来开启' where id=2;
  6. #3.0 自动提交事务,如果不执行commit,系统将不提交
  7. commit/rollback;

显示事务

显示事务即事务是显示开始的。

  • 可使用start transactionbegin;命令手动开启一个事务。start transaction 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符
    1. - READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
    2. - READ WRITE :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
    3. - WITH CONSISTENT SNAPSHOT :启动一致性读。
  • 从用户执行start transaction命令到用户执行commit命令之间的一系列操作为一个完整的事务周期。
    1. - 中间操作主要是DML,不含DDL
  • 当用户执行commit命令时,当前事务提交,若不执行commit命令,系统则默认事务回滚ROLLBACK。
    1. #1.0 手动开启一个事务
    2. start transaction;//or begin;
    3. update dream_datas set title='使用start transaction1' where id=1;
    4. update dream_datas set title='使用start transaction2' where id=2;
    5. #2.0 手动开启手动提交
    6. commit; //rollback

    补充

  1. autocommit参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。
  2. 在mysql中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务。
    • 如DDL语句,如create table/drop table/alter/tablelock tables语句等等。事务里面create了表,即使末尾使用了rollback,表也会创建。
    • 不过,常用的select、insert、update和delete命令,不会强制提交事务。

1 事务的基础使用 - 图4 1 事务的基础使用 - 图5

  1. 事务的保存点:savepoint到rollback中间的可回滚,其余的不可回滚。

    代码案例 START TRANSACTION; DELETE FROM user WHERE id=1; SAVEPOINT a;#设置保存点 DELETE FROM user WHERE id=2; ROLLBACK TO a;#回滚到保存点 1 事务的基础使用 - 图6

  2. 一个标准的事务创建/提交案例

    1. #使用显示事务
    2. ##1.0 手动开启方式
    3. START TRANSACTION;
    4. ## 2.0 编写一组事务的语句
    5. UPDATE account SET balance = 1000 WHERE username='张无忌';
    6. UPDATE account SET balance = 1000 WHERE username='赵敏';
    7. ##3.0 手动提交/手动回滚结束事务
    8. commit;//rollback;

    3 数据的并发:4

    脏写

    对于两个事务Session A、Session B,如果可以在事务Session A里面直接修改另一个已经提交了的事务Session B修改过的数据,或者在A里面直接修改事务B尚未提交的数据那就意味着发生了脏写。需要注意的是:脏写这个问题非常严重,不论事务设置成哪种隔离级别,都是不允许脏写的情况发生(其已经违背了事务的持久性、隔离性),所以这只是我们做理论研究上的说明,实际情况不会发生。

    脏读

    当前事务A中可以读到其他事务B未提交的数据(脏数据),这种现象是脏读。
    对于两个事务 Session A、Session B,如果事务Session A 修改了另一个未提交事务Session B 修改过 的数
    据,那就意味着发生了 脏写
    1 事务的基础使用 - 图7
    极简演示
    1 事务的基础使用 - 图8

    不可重复读

    在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。
    对于两个事务Session A、Session B,Session A读取了一个字段,然后 Session B 更新了该字段(B事务已经提交了的)。 之后Session A再次读取同一个字段, 值就不同 了。那就意味着发生了不可重复读。
    1 事务的基础使用 - 图9
    极简演示
    1 事务的基础使用 - 图10
    注:如果在同一事务的两次select中update了那条数据,则两次select会不同,此范畴不属于不可重复读。同理幻读也是如此。
    1 事务的基础使用 - 图11

    幻读

    对于两个事务Session A、Session B。Session A从一个表中读取了一个字段,然后Session B在该表中插入了一些新的行。 之后,如果 Session A 再次读取同一个表, 就会多出几行。那就意味着发生了幻读。

    • Q1:如果Session B中删除了一些符合studentno > 0的记录而不是插入新记录,那Session A之后再根据studentno >0的条件读取的记录变少了,这种现象算不算幻读呢?

A:这种现象不属于幻读,幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。

  • Q2:那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?

A:这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。
不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。
1 事务的基础使用 - 图12

4 并发问题的解决(事务的隔离级别):4

引出事务的隔离级别

  1. 背景:MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接每个客户端。客户端与服务器连接上之后,就可以称为一个会话(Session ),每个客户端都可以在自己的会话中向服务器发出请求语句。如果没有事务的隔离级别来保证数据的并发性。那么就会出现上面的数据并发问。
  2. 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。正因为事务有隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对性能影响太大,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,那就看二者如何权衡取舍了。
  3. 隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

    • (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
    • (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

      通用的事务隔离级别

      上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:脏写 > 脏读 > 不可重复读 > 幻读
      我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。

      SQL标准中设立了4个隔离级别

    • read uncommitted:读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免数据并发的脏读、不可重复读、幻读。

    • read committed:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。
      • 可以避免脏读,但不可重复读、幻读问题仍然存在。
      • 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。
    • repeatable read:可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。
      • 可以避免脏读、不可重复读,但幻读问题仍然存在。
      • mysql默认的隔离级别。
    • serializable:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
      | 隔离级别 | 解决脏写 | 解决脏读 | 解决不可重复读 | 解决幻读 | | 加锁读 | | —- | —- | —- | —- | —- | —- | —- | | read uncommitted | YES | NO | NO | NO | | no | | read committed | YES | YES | NO | NO | | no | | repeatable read | YES | YES | YES | NO | | no | | serializable | YES | YES | YES | YES | | yes |
  • 脏写问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。
  • 在实际应用中
    • 读未提交在并发时会导致很多问题,而性能相对于其他隔离级别提高却很有限,因此使用较少。
    • 可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。
    • 在大多数数据库系统中,默认的隔离级别是读已提交(如Oracle)或可重复读(后文简称RR)。

      MySQL的事务隔离级别

      查看/修改务的隔离级别
      1. select @@tx_isolation;
      2. set session|global transaction isolation level 隔离级别;
      3. //eg: set session transaction isolation level read uncommitted;#将数据库的隔离级别更改为允许重复读
      read uncommitted读未提交
      set session|global transaction isolation level read uncommitted;
      此时代表事务可以读取到其他事务未提交的delete/update/insert数据,此时存在脏读/不可重复读/幻读的情况。

      案例演示 脏读:事务B读取到了事务A还未commit的值。 1 事务的基础使用 - 图13 不可重复读:事务B首先读取了user表的值为fly,事务A修改并commit了这条数据,事务B再读取user表的这条数据,发现已经被更改为了fly_update,此为不可重复读。 1 事务的基础使用 - 图14 幻读:事务B前后select count() from user;表的值,发现数据不一致,此为幻读。note:并不是count()就代表着幻读,而是因为插入了新值导致了count(*)值不一样。 1 事务的基础使用 - 图15

read committed读已提交
set session|global transaction isolation level read committed;
此时代表事务只能读取到其他事务已提交的delete/update/insert数据,解决了脏读的问题,但存在不可重复读和幻读的情况。

案例演示 脏读:已解决 1 事务的基础使用 - 图16 不可重复读:未解决 1 事务的基础使用 - 图17 幻读:未解决 1 事务的基础使用 - 图18

repeatable read可重复读(mysql默认)
set session|global transaction isolation level repeatable read;
当前隔离级别解决了脏读和不可重复读,但仍然存在幻读的情况

案例 不可重复读问题:已解决 1 事务的基础使用 - 图19 幻读:仍存在 1 事务的基础使用 - 图20

扩展:innodb的mvcc

mysql的InnoDB默认的隔离级别是repeatable read,需要注意的是,在SQL标准中,repeatable read是无法避免幻读问题的,但是InnoDB实现的repeatable read避免了幻读问题。repeatable read解决脏读、不可重复读、幻读等问题,使用的是MVCC:MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。概括来说,通过锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)等,实现了一定程度的隔离性,可以满足大多数场景的需要。
不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离。具体信息详见锁机制一节。
serializable(序列化)
set session|global transaction isolation level serializable;

案例演示:可以有效避免脏读、不可重复读、幻读的情况 脏读:已解决 1 事务的基础使用 - 图21