事务基础

53 阅读13分钟

事务介绍

事务的定义:

  • 事务(Transaction)是指一组数据库操作,这组操作要么全部执行成功,要么全部执行失败,不会出现部分执行成功部分执行失败的情况。事务是保证数据一致性和完整性的重要机制之一。

为什么需要事务?

  • 举一个银行转账的例子,张三给李四转账1000元,对应以下操作:
    • 张三账户扣减 1000 元
    • 李四账户增加 1000 元
  • 要依次执行两条 SQL 语句

image.png

  • 假如执行完第一条 SQL 之后,系统出现异常,第二条没有被执行。那么就会造成张三账户的钱减少但李四账户的钱没有相应增加,钱凭空消失了,这种情况是不能接受了。因此需要将这两条SQL放到同一事务中,用事务来保证其要么全部执行成功,要么全部执行失败(如果第一条SQL执行完后系统异常,数据库会回滚事务,撤销第一条SQL的修改)。

事务操作演示

事务的结束操作分为两类:提交和回滚

  • 提交 Commit:提交事务的所有操作。具体地说就是将事务中所有对数据库的更新写到磁盘上的物理数据库中去,事务正常结束。
  • 回滚 Rollback:即在事务的运行过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的操作全部撤销,回滚到事务开始时的状态。

使用 MySQL Workbench 进行演示。

  • 为了更好的演示效果,我们用一个事务更新数据,用另一个事务观察更新的结果。
  • 在 MySQL Workbench 中,同一个连接共用事务,要演示多个事务需要再创建一个连接。

image.png

(1)演示事务默认自动提交

  • 在连接 local2 的 Query 窗口中,查询 student 表 id = 1 的数据,返回的数据 age = 16。
  • 在连接 local 的 Query 窗口中,更新 id = 1 的数据的 age 字段为 18。
  • 在连接 local2 的 Query 窗口中,查询 student 表 id = 1 的数据,返回的数据 age = 18。

image.png

image.png

image.png

  • 事务默认是自动提交的,当执行一条SQL语句就会自动成为一个事务并提交。因此这处的update直接生效了。

(2)演示使用事务控制语句显示开启事务并提交

  • 在连接 local2 的 Query 窗口中,查询 student 表 id = 1 的数据,返回的数据 age = 18。
  • 在连接 local 的 Query 窗口中,使用 begin 开始事务,更新 id = 1 的数据的 age 字段为 20。
  • 在连接 local 的 Query 窗口中,查询 student 表 id = 1 的数据,返回的数据 age = 20,同一个事务能查到更新的数据。

image.png

image.png

image.png

  • 在连接 local2 的 Query 窗口中,查询 student 表 id = 1 的数据,返回的数据 age = 18,更新未生效。
  • 在连接 local 的 Query 窗口中,使用 commit 提交事务,使数据更新生效。
  • 在连接 local2 的 Query 窗口查询,此时 age = 20,更新已生效。

image.png

image.png

image.png

(3)演示使用事务控制语句显示开启事务并回滚

  • 在连接 local2 的 Query 窗口中,查询 student 表 id = 1 的数据,返回的数据 age = 20。
  • 在连接 local 的 Query 窗口中,使用 begin 开始事务,更新 id = 1 的数据的 age 字段为 22,在同一个事务中查询能查到更新的数据
  • 使用 rollback 回滚事务。

image.png

image.png

image.png

  • 在连接 local2 的 Query 窗口中,查询 student 表 id = 1 的数据,返回的数据 age = 20,更新被回滚。

image.png

事务的四大特性 - ACID

在关系型数据库中,事务具有以下四个特性,通常被称为 ACID 特性:

  • 原子性(Atomicity):事务是一个原子操作,要么全部执行成功,要么全部失败回滚。如果在事务执行过程中发生了错误,所有的修改都会被撤销,回滚到事务开始前的状态,保证数据的一致性。
  • 一致性(Consistency):事务执行前后,数据库的状态应该保持一致。这意味着,在事务执行过程中,数据库的约束条件、触发器、外键等约束关系都应该得到满足,保证数据的正确性和完整性。
  • 隔离性(Isolation):多个事务并发执行时,每个事务都应该感觉不到其他事务的存在,每个事务都应该像是在独立的环境中执行。这意味着,在事务执行过程中,其他事务对该事务的修改应该被隔离起来,保证数据的正确性和一致性。
  • 持久性(Durability):事务执行成功后,对数据库的修改应该永久保存,即使系统崩溃或断电也不会丢失。这意味着,在事务执行成功后,对数据库的修改应该被持久化到磁盘中,保证数据的可靠性和持久性。

这四个特性是事务的基本特性,也是保证数据库的数据一致性和可靠性的关键。

事务的并发问题

什么是事务并发?

  • 事务并发指的是多条事务同时执行。

事务并发会造成什么问题?

  • 事务并发可能会造成以下三类问题:
    • 脏读
    • 不可重复读
    • 幻读

事务的并发问题 – 脏读

脏读:一个事务会读取到另一个事务未提交的数据。

  • 事务A修改了数据但还未提交,事务B读取到了事务A修改的数据。然后事务A因为某些错误回滚了,这个时候事务B读取到的数据就是脏的,这就是脏读。

image.png

事务的并发问题 – 不可重复读

不可重复读:在同一事务内,事务两次读取同一条数据的值不一样(原数据中同一条数据被修改)。

  • 事务A读取了一条数据之后,事务B修改了这条数据并提交了事务,然后事务A再次读取这条数据,就会发现两次结果不一致。这就是不可重复读。

image.png

事务的并发问题 – 幻读

幻读:事务中的同一个查询在不同的时间产生不同的行集(数据总条数新增)。

  • 事务A使用一定的条件查询,然后事务B增加了符合条件的记录,当事务A再次查询的时候,发现两次查询的结果集不一样,好像产生了幻觉。这就是幻读。

image.png

事务隔离级别

事务具有隔离性,隔离性分为不同级别,隔离级别从低到高依次为:

  • 读未提交(Read Uncommitted):最低的隔离级别,允许一个事务读取另一个事务未提交的数据。这种隔离级别可能导致脏读、不可重复读、幻读。
  • 读已提交(Read Committed):允许一个事务读取另一个事务已提交的数据。这种隔离级别可以避免脏读问题,但可能会出现不可重复读、幻读问题。
  • 可重复读(Repeatable Read):保证在一个事务中多次读取同一数据时,结果是一致的。这种隔离级别可以避免脏读和不可重复读问题,但可能会出现幻读问题。可重复读是 MySQL 默认的事务隔离级别。
  • 串行化(Serializable):最高的隔离级别,强制事务串行执行,避免了所有并发问题。这种隔离级别可以避免脏读、不可重复读和幻读等问题,但会影响并发性能。

事务隔离级别越高,数据一致性越高,但并发性能越低。在实际应用中,需要根据具体的业务需求选择合适的事务隔离级别。

查看事务隔离级别 select @@transaction_isolation;

image.png

设置事务隔离级别 set [ session | global ] transaction isolation level <isolation level>

image.png

设置完事务隔离级别后,要关闭连接重新打开,让设置变更生效。

事务隔离级别 – 读未提交

  • 演示《读未提交》隔离级别下的脏读现象

image.png

《读未提交》隔离级别下可能出现脏读、不可重复读、幻读。

事务隔离级别 – 读已提交

  • 演示《读已提交》隔离级别下的不可重复读现象

image.png

《读已提交》隔离级别下可能出现不可重复读、幻读。

事务隔离级别 – 可重复读

  • 演示《可重复读》隔离级别是否解决不可重复读问题

image.png

*《可重复读》解决不可重复读问题的原理是 MVCC,事务在第一次执行 SQL 时生成快照。

  • 注意:是第一次执行SQL时,不是执行 begin 开启事务时。

image.png

  • 演示《可重复读》隔离级别是否解决幻读问题,情况一:不会出现幻读

image.png

  • 演示《可重复读》隔离级别是否解决幻读问题,情况二:出现幻读。

image.png

《可重复读》隔离级别在某些情况下能解决幻读问题,但仍可能出现幻读问题。

《可重复读》在有些场景下能解决幻读问题,有些场景下不能,为什么?

  • MySQL 里面实际上有两种读,“快照读”和“当前读”:
    • 快照读(snapshot read):是指在读取数据时,会创建一个快照来保证在事务执行期间多次读取同一数据时,结果是一致的。
    • 当前读(current read):是指在读取数据时,读取的是所有已提交的最新数据,而不是之前的快照数据。在读已提交隔离级别下,MySQL使用当前读来读取数据。
    • 在《可重复读》隔离级别下,普通 select 使用的是快照读,update 使用的是当前读。
  • 情况二与情况一的区别是:情况二在事务A第二次读取前执行了 update ,由于 update 使用的是当前读,会把事务B插入的数据(王五)也一并更新。我们知道,事务内能读取到当前事务更新的数据,所以先执行过 update 再执行 select 就出现了幻读。

事务隔离级别 – 串行化

  • 《串行化》隔离级别下,所有的事务都会被串行执行,即每个事务必须等待前一个事务执行完成后才能开始执行。MySQL 使用锁实现串行化,会对读取的数据进行加锁,直到事务结束才会释放锁。

image.png

《串行化》隔离级别不会出现脏读、不可重复读、幻读这些事务并发问题,因为这个隔离级别下事务是串行的,不会并发。由于使用了锁机制,串行化容易出现死锁,一般无特殊要求不会使用串行化。

事务隔离级别 – 总结

image.png

事务隔离级别实现的关键 - MVCC

MVCC的概念:Multi-Version Concurrency Control ,多版本并发控制。数据库隔离级别读已提交、可重复读 都是基于MVCC实现的。

思考:如何实现可重复读?

  • 这里提供一个思路:
    • 保存多个版本的数据
    • 第一次读取时记录数据的版本 v1
    • 第二次读取时取 v2 的数值

这个方案有什么问题?

  • 需要记录所有数据的版本,实现难度大。

image.png

MVCC实现的关键知识点

  • (1)事务版本号:事务每次开启时,都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务开始执行的先后顺序,不能从事务ID判断事务提交的顺序,因为事务ID小的事务可能后提交。
  • (2)隐式字段:对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id、roll_pointer,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id。

image.png

  • (3)Undo Log:回滚日志,用于记录数据被修改前的信息。Undo Log 的用途:事务回滚;MVCC快照读。
  • (4)版本链:某一行数据被多次修改,对该行数据的修改会产生多个版本,然后通过回滚指针 roll_pointer连成一个链表,这个链表就称为版本链。

image.png

  • (5)Read View:事务执行SQL语句时,产生的读视图。
  • 每个SQL语句执行前都会得到一个Read View。
  • Read View的作用是可见性判断的,即判断当前读操作可见哪个版本的数据。
  • Read View 的几个重要属性
    • creator_trx_id: 创建当前read view的事务ID
    • m_ids: 当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List
    • min_limit_id: 表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值
    • max_limit_id: 表示生成ReadView时,系统中应该分配给下一个事务的id值

image.png

Read View 匹配:判断当前读操作可见哪个版本的数据

  • Read View 匹配的基本原理:
    • 事务创建 Read View 后,希望除当前事务本身外其他事务对数据的更新对该 Read View 都不可见,否则无法实现可重复读。
    • 读取版本链时,如果读取到创建 ReadView 时还在活跃或还未创建的事务id,那么这个版本不可见,因为这个版本的提交一定在 Read View 创建之后。
    • 如果读取到的版本的事务id等于创建 Read View 的事务id,那么该版本可见。

image.png

image.png

Read View 匹配规则:

  • 如果数据事务ID trx_id < min_limit_id,表明生成该版本的事务在生成Read View前就已经提交了,所以该版本对当前读操作可见。
  • 如果 trx_id>= max_limit_id ,表明生成该版本的事务在生成ReadView后才生成,所以该版本对当前读操作不可见。
  • 如果 min_limit_id =< trx_id < max_limit_id,需要分3种情况讨论
    • 如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了修改的结果,该版本对当前读操作可见。
    • 如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此该版本对当前读操作可见。
    • 如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,事务未提交,并且不是自己生产的,所以该版本对当前读操作不可见。

image.png

Read View 的匹配规则 – 举例

image.png

image.png

MVCC实现的原理

  • InnoDB 实现MVCC,是通过 Read View + Undo Log 实现的
  • Undo Log 保存了历史快照
  • Read View 可见性规则帮助判断当前版本的数据是否可见

基于MVCC查询一条数据的过程

  • 获取事务自己的版本号,即事务ID
  • 获取Read View
  • 查询得到的数据,然后Read View中的事务版本号进行比较
  • 如果不符合Read View的可见性规则, 即就需要Undo log中历史快照
  • 最后返回符合规则的数据

读已提交和可重复读两个隔离级别的区别在于获取Read View。

MVCC实现读已提交:每次读重新生成 Read View

假设 trx_id = 100 的事务完成了数据插入

image.png

image.png

image.png

  • 第一次读时的 Read View 和版本链,最新数据符合可见规则,money = 500

image.png

image.png

  • 第二次读时的 Read View 和版本链,最新数据符合可见规则,money = 1500

MVCC实现可重复读:仅在第一个读生成 Read View,之后读使用之前生成的 Read View 快照

image.png

image.png

image.png

  • 第一次读时的 Read View 和版本链,最新数据符合可见规则,money = 500

image.png

image.png

  • 第二次读时的 Read View(使用第一次的快照) 和版本链,最新数据不符合可见规则,undo log 中最近一条符合可见规则, money = 500

基于MVCC的原理分析可重复读下发生幻读的情况

image.png

image.png

  • 第一次读时的 Read View,“王五”数据未插入,还没有版本链,本次读取只有{张三, 李四}满足要求。

image.png

  • 第二次读时的版本链,Read View 使用之前的快照,最新数据不符合可见规则,本次读取只有{张三, 李四}满足要求。

image.png

  • 第三次读时的版本链,Read View 使用之前的快照,最新数据符合可见规则,本次读取{张三, 李四, 王五}满足要求,发生幻读。