当前位置: 首页 > news >正文

事务和锁(进阶)

事务和锁(进阶)

  • 一.回顾事务
    • 1.什么是事务
    • 2 为什么要使用事务
    • 3 怎么使用事务
  • 二.InnoDB和ACID模型
  • 三. 如何实现原子性
  • 四.如何实现持久性
  • 五.隔离性实现原理
    • 1.事务的隔离性
    • 2.事务的隔离级别
    • 3.锁
      • 1)锁信息
      • 2) 共享锁和独占锁-Shared and Exclusive Locks
      • 3) 意向锁-Intention Locks
      • 4) 索引记录锁 - Record Locks
      • 5) 间隙锁 - Gap Locks
      • 6) 临键锁 - Next-Key Locks
      • 7) 插⼊意向锁 - Insert Intention Locks
      • 8)AUTO-INC Locks
      • 9)死锁
        • (1)示例
        • (2)死锁产生的条件
        • (3)InnoDB对死锁的检测
        • (4)如何避免死锁
    • 4.查看并设置隔离级别
    • 5. READ UNCOMMITTED - 读未提交与脏读
      • 1) 实现方式
      • 2) 存在问题
      • 3) 问题重现
    • 6. READ COMMITTED - 读已提交与不可重复读
      • 1) 实现方式
      • 2) 存在问题
      • 3) 问题重现
    • 7. REPEATABLE READ - 可重复读与幻读
      • 1)实现方式
      • 2 )存在问题
      • 3) 问题重现
    • 8 SERIALIZABLE - 串行化
      • 1) 实现方式
      • 2) 存在问题
    • 9 不同隔离级别的性能与安全
    • 10.多版本控制(MVCC)
      • 1)实现原理
        • (1)版本链
        • (2) ReadView
      • 2)MVCC是否可以解决不可重复读与幻读

一.回顾事务

1.什么是事务

事务是把⼀组SQL语句打包成为一个整体,在这组SQL的执行过程中,要么全部成功,要么全部失
败,这组SQL语句可以是⼀条也可以是多条。再来看⼀下转账的例子,如图:在这里插入图片描述
在这个例⼦中,涉及了两条更新语句:在这里插入图片描述

如果转账成功,应该有以下结果:

  1. 张三的账户余额减少 100 ,变成 900 ,李四的账⼾余额增加了 100 ,变成 1100 ,不能出现张三的余额减少⽽李四的余额没有增加的情况;
  2. 张三和李四在发⽣转账前后的总额不变,也就是说转账前张三和李四的余额总数为1000+1000=2000 ,转账后他们的余额总数为 900+1100=2000 ;
  3. 转账后的余额结果应当保存到存储介质中,以便以后读取;
  4. 还有⼀点需要要注意,在转账的处理过程中张三和李四的余额不能因其他的转账事件而受到干扰;
    以上这四点在事务的整个执行过程中必须要得到保证,这也就是事务的 ACID 特性,即:
  • Atomicity (原子性):一个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执行了一半的情况,如果事务在执行过程中发生错误,会回滚( Rollback )到事务开始前的状态,就像这个事务从来没有执行过一样;
  • Consistency (一致性):在事务开始之前和事务结束以后,数据库的完整性不会被破坏。这表示写⼊的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执行过程中服务器崩溃后如何恢复;
  • Isolation (隔离性):数据库允许多个并发事务同时对数据进⾏读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不⼀致。事务可以指定不同的隔离级别,以权衡在不同的应用场景下数据库性能和安全。
  • Durability (持久性):事务处理结束后,对数据的修改将永久的写⼊存储介质,即便系统故障也不会丢失。

2 为什么要使用事务

事务具备的ACID特性,也是我们使⽤事务的原因,在我们日常的业务场景中有⼤量的需求要用事务来保证。⽀持事务的数据库能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题,在使⽤事务过程中,要么提交,要么回滚,不用去考虑网络异常,服务器宕机等其他因素,因此我们经常接触的事务本质上是数据库对 ACID 模型的⼀个实现,是为应⽤层服务的。

3 怎么使用事务

  • 要使用事务那么数据库就要⽀持事务,在MySQL中支持事务的存储引擎是InnoDB,可以通过
    show engines; 语句查看:在这里插入图片描述

  • 通过以下语句可以完成对事务的控制:

    • START TRANSACTION 或 BEGIN 开始⼀个新的事务;
    • COMMIT 提交当前事务,并对更改持久化保存;
      -ROLLBACK 回滚当前事务,取消其更改;
      -SET autocommit 禁⽤或启⽤当前会话的默认⾃动提交模式, autocommit 是⼀个系统变量可以通过选项指定也可以通过命令行设置 --autocommit[={OFF|ON}]
  • 演示开启一个事务,执行修改后并回滚

# 开启事务
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
# 在修改之前查看表中的数据
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 张三余额减少100
mysql> UPDATE account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 李四余额增加100
mysql> UPDATE account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 在修改之后,提交之前查看表中的数据,余额已经被修改
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 回滚事务
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
# 再查询发现修改没有⽣效
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
  • 演示开启一个事务,执行修改后并回提交:
# 开启事务
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
# 在修改之前查看表中的数据
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 张三余额减少100
mysql> UPDATE account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 李四余额增加100
mysql> UPDATE account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 在修改之后,提交之前查看表中的数据,余额已经被修改
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 提交事务
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
# 再查询发现数据已被修改,说明数据已经持久化到磁盘
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
  • 默认情况下MySQL启⽤事务自动提交,也就是说每个语句都是一个事务,就像被 START TRANSACTIONCOMMIT 包裹一样,不能使用 ROLLBACK 来撤销执行结果;但是如果在语句执行期间发生错误,则自动回滚;
  • 通过 SET autocommit 设置自动与手动提交
# 查看当前的事务提交模式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | # ON表⽰⾃动提交模式
+---------------+-------+
1 row in set, 1 warning (0.02 sec)
# 设置为⼿动提交(禁⽤⾃动提交)
mysql> SET AUTOCOMMIT=0; # ⽅式⼀
mysql> SET AUTOCOMMIT=OFF; # ⽅式⼆
Query OK, 0 rows affected (0.00 sec)
# 再次查看事务提交模式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF | # OFF表⽰关闭⾃动提交,此时转为⼿动提交
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
  • 手动提交模式下,提交或回滚事务时直接使用 commitrollback
# 查看事务提交模式,确定⾃动提交已关闭
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF | # OFF表⽰关闭⾃动提交,此时转为⼿动提交
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
# 查询表中现在的数据
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 张三余额减少100
mysql> UPDATE account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 在修改之后查看表中的数据,余额已经被修改
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 | # ⽐原来的减少了100
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 回滚事务
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
# 再查询是被修改之后的值,发现修改没有⽣效
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 上⼀个事务已回滚,接下来重新执⾏更新操作,让张三余额减少100
mysql> UPDATE account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 在修改之后查看表中的数据,余额已经被修改
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 | # ⽐原来的减少了100
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
# 再查询是被修改之后的值,说明数据已经持久化到磁盘
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
  • 通过 SET autocommit 设置自动与自动提交
# 查看当前的事务提交模式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF | # ⼿动提交模式
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
mysql> SET AUTOCOMMIT=1; # ⽅式⼀
mysql> SET AUTOCOMMIT=ON; # ⽅式⼆
Query OK, 0 rows affected (0.00 sec)
# 再次查看事务提交模式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | # ON表⽰⾃动提交模式
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
  • 注意:只要使用 START TRANSACTIONBEGIN 开启事务,必须要通过 COMMIT 提交才会持久化,与是否设置 SET autocommit 无关。

二.InnoDB和ACID模型

  • ACID模型是⼀组数据库设计原则,强调业务数据的可靠性,MySQL的InnoDB存储引擎严格遵循ACID模型,不会因为软件崩溃和硬件故障等异常导致数据的不完整。

  • Atomicity(原子性):原子性方面主要涉及InnoDB的事务开启与提交,我们之前做过详细讲解与回顾

    • 设置 autocommit[={OFF|ON}] 系统变量,开启和禁⽤事务是否自动提交.
    • 使用 START TRANSACTIONBEGIN TRANSACTION 语句开启事务;
    • 使用 COMMIT 语句提交事务;
    • 使用 ROLLBACK 语句回滚事务。
  • Consistency(⼀致性):⼀致性主要涉及InnoDB内部对于崩溃时数据保护的相关处理,相关特性包括:

    • InnoDB 存储引擎的双写缓冲区 doublewrite buffer ;
    • InnoDB 存储引擎的崩溃恢复
  • Isolation(隔离性):隔离方面主要涉及应用于每个事务的隔离级别,相关特性包括:

    • 通过 SET TRANSACTION 语句设置事务的隔离级别;
    • InnoDB 存储引擎的锁,锁可以在 INFORMATION_SCHEMA 系统库和 Performance Schema 系统库中的 data_locksdata_lock_waits 表查看
  • Durability(持久性):持久性涉及MySQL与特定硬件配置的交互,可能性取决于CPU、⽹络和存储设备的性能,由于硬件环境比较复杂,所以无法提供固定的操作指南,只能根据实际环境进行测试得到最佳的性能,相关特性包括:

    • InnoDB 存储引擎的双写缓冲区 doublewrite buffer
    • innodb_flush_log_at_trx_commit 系统变量的设置;
    • sync_binlog 系统变量的设置;
    • innodb_file_per_table 系统变量的设置;
    • 存储设备(如磁盘驱动器、SSD或RAID磁盘阵列)中的写缓冲区;
    • 存储设备中由电池⽀持的缓存。
    • 运行MySQL的操作系统,特别是对 fsync() 系统调⽤的⽀持;
    • 不间断电源UPS (uninterruptible power supply),保护所有运行MySQL服务器和数据存储设备的电力供应;
    • 备份策略,例如备份的频率和类型,以及备份保留周期;
    • 分布式环境中数据中心之间的网络连接。
  • 需要重点说明的是,事务最终要保证数据的可靠和⼀致,也就是说 ACID 中的Consistency(⼀致性)是最终的⽬的,那么当事务同时满⾜了Atomicity(原⼦性),Isolation(隔离性)和Durability(持久性)时,也就实现了⼀致性。

  • 接下来我们分别讨论MySQL是如何实现原子性,持久性和隔离性

三. 如何实现原子性

  • 在⼀个事务的执行过程中,如果多条DML语句顺利执行,那么结果最终会写入数据库;如果在事务
    的执行过程中,其中⼀条DML语句出现异常,导致后⾯的语句无法继续执行或即使继续执行也会导致数据不完整、不⼀致,这时前⾯执行的语句已经对数据做了修改,如果要保证⼀致性,就需要对之前的修改做撤销操作,这个撤销操作称为回滚 rollback ,如下图所示:在这里插入图片描述
  • 那么回滚操作是如何实现的呢?回滚过程中依据的是什么呢?在InnoDB专题中介绍过UndoLog的作用和原理,我们大致回顾⼀下,在事务执行每个DML之前,把原始数据记录在⼀个日志⾥,做为回滚的依据,这个日志称为 Undo Log (回滚日志或撤销日志),在不考虑缓存和刷盘的条件下,执行过程如下所示:在这里插入图片描述
  • 当需要回滚操作时,MySQL根据操作类型,在Insert Undo链或Update Undo链中读取相应的日志记录,并反向执行修改,使数据还原,完成回滚。
  • 通过 Undo Log 实现了数据的回滚操作,这时就可以保证在事务成功的时候全部的SQL语句都执
    行成功,在事务失败的时候全部的SQL语句都执行失败,实现在原子性。

四.如何实现持久性

  • 提交的事务要把数据写入(持久化到)存储介质,比如磁盘。在正常情况下大多没有问题,可是在服务器崩溃或突然断电的情况下,⼀个事务中的多个修改操作,只有⼀部分写⼊了数据文件,而另⼀部分没有写入,如果不做针对处理的话,就会造成数据的丢失,从而导致数据不完整,也就不能保证⼀致性。
  • 在真正写入数据文件之前,MySQL会把事务中的所有DML操作以日志的形式记录下来,以便在服务器下次启动的时候进行恢复操作,恢复操作的过程就是把日志中没有写到数据文件的记录重新执行⼀遍,保证所有的需要保存的数据都持久化到存储介质中,我们把这个日志称为 Redo Log (重做日志);生成重做日志是保证数据⼀致性的重要环节。在持久化的处理过程中,还包括缓冲池、
    Doublewrite Buffer (双写缓冲区)、 Binary Log (二进制日志) 等知识点,关于InnoDB的日志生成机制以及崩溃恢复机制我们在InnoDB 存储引擎专题进行了详细讲解。

五.隔离性实现原理

1.事务的隔离性

MySQL服务可以同时被多个客⼾端访问,每个客户端执行的DML语句以事务为基本单位,那么不同的客户端在对同一张表中的同⼀条数据进行修改的时候就可能出现相互影响的情况,为了保证不同的事务之间在执行的过程中不受影响,那么事务之间就需要要相互隔离,这种特性就是隔离性。

2.事务的隔离级别

学习过多线程技术,都知道在并发执行的过程中,多个线程对同一个共享变量进行修改时,在不加限制的情况下会出现线程安全问题,我们解决线程安全问题时,⼀般的做法是通过对修改操作进行加锁;同理,多个事务在对同⼀个表中的同⼀条数据进行修改时,如果要实现事务间的隔离也可以通过锁来完成,在MySQL中常见的锁包括:读锁,写锁,行锁,间隙锁,Next-Key锁等,不同的锁策略联合多版本并发控制可以实现事务间不同程度的隔离,称为事务的隔离级别;

不同的隔离级别在性能和安全方面做了取舍,有的隔离级别注重并发性,有的注重安全性,有的则是并发和安全适中;在MySQL的InnoDB引擎中事务的隔离级别有四种,分别是:

  • READ UNCOMMITTED ,读未提交
  • READ COMMITTED ,读已提交
  • REPEATABLE READ ,可重复读(默认)
  • SERIALIZABLE ,串行化

3.锁

实现事务隔离级别的过程中用到了锁,所谓锁就是在事务A修改某些数据时,对这些数据加⼀把锁,防⽌其他事务同时对这些数据执行修改操作;当事务A完成修改操作后,释放当前持有的锁,以便其他事务再次上锁执行对应的操作。不同存储引擎中的锁功能并不相同,这⾥我们重点介绍InnoDB存储引擎中的锁。

1)锁信息

  • 锁的信息包括锁的请求(申请),锁的持有以及阻塞状态等等,都保存在 performance_schema 库的 data_locks 表中,可以通过以下方式查看:在这里插入图片描述在这里插入图片描述

  • 锁类型
    锁类型依赖于存储引擎,在InnoDB存储引擎中按照锁的粒度分为,行级锁 RECORD 和表级锁
    TABLE :

    • 行级锁也叫行锁,是对表中的某些具体的数据行进行加锁;
    • 表级锁也叫表锁,是对整个数据表进行加锁。

在之前版本的BDB存储引擎中还支持页级锁,锁定的是⼀个数据页,MySQL8中没有页级锁

  • 锁模式
    锁模式,⽤来描述如何请求(申请)锁,分为共享锁(S)、独占锁(X)、意向共享锁(IS)、意向独占锁
    (IX)、记录锁、间隙锁、Next-Key锁、AUTO-INC 锁、空间索引的谓词锁等

这⾥介绍的锁类型和锁模式,也就是⼤家经常听过的锁分类

2) 共享锁和独占锁-Shared and Exclusive Locks

InnoDB实现了标准的行级锁,分为两种分别是共享锁(S锁)和独占锁(X锁),独占锁也称为排他锁。

  • 共享锁(S锁):允许持有该锁的事务读取表中的一行记录,同时允许其他事务在锁定行上加另⼀个
    共享锁并读取被锁定的对象,但不能对其进行写操作;
  • 独占锁(X锁):允许持有该锁的事务对数据行进行更新或删除,同时不论其他事务对锁定行进行读
    取或修改都不允许对锁定行进行加锁;在这里插入图片描述
  • 如果事务T1持有R行上的共享锁(S),那么事务T2请求R行上的锁时会有如下处理:
    • T2请求S锁会立即被授予,此时T1和T2都对R行持有S锁;
    • T2请求X锁不能立即被授予,阻塞到T1释放持有的锁
  • 如果事务T1持有R行上的独占锁(X),那么T2请求R⾏上的任意类型锁都不能⽴即被授予,事务T2必须等待事务T1释放R行上的锁。

TIPS:
读锁是共享锁的⼀种实现,写锁是排他锁的⼀种实现。

3) 意向锁-Intention Locks

  • InnoDB⽀持多粒度锁,允许行锁和表锁共存;
  • InnoDB使⽤意向锁实现多粒度级别的锁,意向锁是表级别的锁,它并不是真正意义上的加锁,而只是在 data_locks 中记录事务以后要对表中的哪⼀行加哪种类型的锁(共享锁或排他锁),意向锁分为两种:
    • 意向共享锁(IS):表示事务打算对表中的单个行设置共享锁。
    • 意向排他锁(IX):表示事务打算对表中的单个行设置排他锁。
  • 在获取意向锁时有如下协议:
    • 在事务获得表中某一行的共享锁(S)之前,它必须⾸先获得该表上的IS锁或更强的锁。
    • 在事务获得表中某一行的排他锁(X)之前,它必须⾸先获得该表上的IX锁。
  • 意向锁可以提⾼加锁的性能,在真正加锁之前不需要遍历表中的⾏是否加锁,只需要查看⼀下表中
    的意向锁即可;
  • 在请求锁的过程中,如果将要请求的锁与现有锁兼容,则将锁授予请求的事务,如果与现有锁冲
    突,则不会授予;事务将阻塞等待,直到冲突的锁被释放;意向锁与行级锁的兼容性如下表:在这里插入图片描述
  • 除了全表锁定请求之外,意向锁不会阻止任何锁请求;意向锁的主要目的是表示事务正在锁定某行或者正在意图锁定某行。

4) 索引记录锁 - Record Locks

  • 索引记录锁或称为精准行锁,顾名思意是指索引记录上的锁,如下SQL锁住的是指定的一行:在这里插入图片描述
  • 索引记录锁总是锁定索引行,在表没有定义索引的情况下,InnoDB创建⼀个隐藏的聚集索引,并
    使用该索引进行记录锁定,当使用索引进行查找时,锁定的只是满足条件的行,如图所示:在这里插入图片描述

5) 间隙锁 - Gap Locks

  • 间隙锁锁定的是索引记录之间的间隙,或者第⼀个索引记录之前,再或者最后⼀个索引记录之后的
    间隙。如图所示位置,根据不同的查询条件都可能会加间隙锁:在这里插入图片描述
  • 例如有如下SQL,锁定的是ID (10,20)之间的间隙,注意不包括10和20的行,目的是防止其他事务将ID值为15的列插⼊到列 account 表中(无论是否已经存在要插入的数据列),因为指定范围值之间的间隙被锁定了;在这里插入图片描述
  • 间隙可以跨越单个或多个索引值;在这里插入图片描述
  • 对于使⽤唯⼀索引查询到的唯一行,不使⽤间隙锁,如下语句,id列有唯一的索引,只对id值为100的行使用索引记录锁:在这里插入图片描述
  • 如果id没有被索引,或者是一个非唯一的索引,以上语句将锁定对应记录前⾯的间隙;
  • 不同事务的间隙锁可以共存, 一个事务的间隙锁不会阻止另一个事务在相同的间隙上使⽤间隙锁;共享间隙锁和独占间隙锁之间没有区别。
  • 当事务隔离级别设置为 READ COMMITTED 时间隙锁会被禁⽤,对于搜索和索引扫描不再使⽤间
    隙锁定。

6) 临键锁 - Next-Key Locks

  • Next-key 锁是索引记录锁和索引记录之前间隙上间隙锁的组合,如图所示;在这里插入图片描述
  • InnoDB搜索或扫描⼀个表的索引时,执行行级锁策略,具体方式是:在扫描过程中遇到的索引记录上设置共享锁或排他锁,因此,行级锁策略实际上应用的是索引记录锁。索引记录上的 nextkey 锁也会影响该索引记录之前的"间隙",也就是说, next-key 锁是索引记录锁加上索引记录前⾯的间隙锁。如果⼀个会话对索引中的⼀条记录R具有共享锁或排他锁,则另⼀个会话不能在索引记录R之前的空⽩中插⼊新的索引记录行。
  • 假设索引包含值10、11、13和20,这些索引可能的 next-key 锁覆盖以下区间,其中圆括号表示不包含区间端点,方括号表示包含端点:在这里插入图片描述
  • 默认情况下, REPEATABLE READ 事务隔离级别开启 next-key 锁并进行搜索和索引扫描,可
    以防止幻象行,从而解决幻读问题。

7) 插⼊意向锁 - Insert Intention Locks

  • 插⼊意向锁是一个特殊的间隙锁,在向索引记录之前的间隙进行insert操作插⼊数据时使⽤,如果多个事务向相同索引间隙中不同位置插入记录,则不需要彼此等待。假设已经存在值为10和20的索引记录,两个事务分别尝试插入索引值为15和16的行,在获得插入行上的排他锁之前,每个事务都用插⼊意向锁锁住10到20之间的间隙,但不会相互阻塞,因为他们所操作的行并不冲突;
  • 下面的示例演示一个事务在获得插⼊记录的排他锁之前,使用了插入意向锁:在这里插入图片描述在这里插入图片描述在这里插入图片描述

8)AUTO-INC Locks

AUTO-INC锁也叫自增锁是⼀个表级锁,服务于配置了 AUTO_INCREMENT ⾃增列的表。在插入数据时会在表上加自增锁,并生成自增值,同时阻塞其他的事务操作,以保证值的唯一性。需要注意的
是,当⼀个事务执行新增操作已生成自增值,但是事务回滚了,申请到的主键值不会回退,这意味着
在表中会出现自增值不连续的情况。

9)死锁

(1)示例
  • 由于每个事务都持有另一个事务所需的锁,导致事务无法继续进行的情况称为死锁。以下图为例,
    两个事务都不会主动释放自己持有的锁,并且都在等待对方持有的资源变得可用。在这里插入图片描述

  • 下⾯通过⼀个示例演示⼀下死锁的发生过程,其中涉及两个客户端A和B,并通过启用全局变量
    innodb_print_all_deadlocks 来查看死锁的信息,同时死锁信息也会保存到错误日志中

  • ⾸先打开⼀个客户端A,并执行以下操作 在这里插入图片描述在这里插入图片描述

  • 接下来,打开客户端B并执行以下操作在这里插入图片描述

  • 在另⼀个客户端中查看两个select操作持有的锁信息在这里插入图片描述

  • 在客户端B中更新animals表中的行在这里插入图片描述

  • 客户B开始等待,可以查看锁的等待信息:在这里插入图片描述在这里插入图片描述

  • InnoDB只有在事务试图修改数据时才使⽤顺序事务id,之前的只读事务id由411549995855872变为52005

  • 如果客户端A试图同时更新birds中的一行,将导致死锁在这里插入图片描述

  • 死锁发⽣时,InnoDB主动回滚导致死锁的事务,此时可以看到客户端B的更新执行成功。在这里插入图片描述

  • 可以通过以下方式查看当前服务器发生死锁的次数在这里插入图片描述

  • InnoDB的j监视器包含了关于死锁和事务的相关信息,可以通过 SHOW ENGINE INNODB
    STATUS;
    查看 LATEST DETECTED DEADLOCK 节点的内容在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 错误⽇志中也记录了死锁相关的信息在这里插入图片描述

(2)死锁产生的条件
  • 互斥访问:如果线程1获取到了锁A,那么线程2就不能同时得到锁A
  • 不可抢占:获取到锁的线程,只能⾃⼰主动释放锁,别的线程不能从他的⼿中抢占锁
  • 保持与请求:线程1已经获得了锁A,还要在这个基础上再去获了锁B
  • 循环等待:线程1等待线程2释放锁,线程2也等待线程1释放锁,死锁发⽣时系统中⼀定有由两个或两个以上的线程组成的⼀条环路,该环路中的每个线程都在等待着下⼀个进程释放锁以上四条是造成死锁的必要条件,必须同时满⾜,所以如果想要打破死锁,可以破坏以上四个条件之⼀,最常见的方式就是打破循环等待
(3)InnoDB对死锁的检测
  • InnoDB在运行时会对死锁进行检测,当死锁检测启⽤时(默认),InnoDB⾃动检测事务死锁,并回
    滚⼀个或多个事务来打破死锁。InnoDB尝试选择⼩事务进⾏回滚,其中事务的大小由插入、更新
    或删除的行数决定。
  • 如果系统变量 innodb_table_locks = 1 (默认) 和 autocommit = 0 ,InnoDB可以检测到表级锁和⾏级锁级别发⽣的死锁;否则,⽆法检测到由 lock TABLES 语句设置的表锁或由⾮InnoDB存储引擎设置的锁,对于⽆法检测到的死锁,可以通过设置系统变量innodb_lock_wait_timeout 的值来指定锁的超时时间来解决死锁问题- 当超过 200 个事务等待锁资源或等待的锁个数超过 1,000,000 个时也会被视为死锁,并尝试将等待列表的事务回滚。
  • 在⾼并发系统中,多个线程等待相同的锁时,死锁检测可能会导致性能降性变慢,此时禁⽤死锁检
    测并依赖 innodb_lock_wait_timeout 设置进行事务回滚可能性能更⾼。可以通过设置系统变量 innodb_deadlock_detect[={OFF|ON}] 禁⽤死锁检测。
(4)如何避免死锁
  • MySQL是⼀个多线程程序,死锁的情况⼤概率会发⽣,但他并不可怕,除⾮频繁出现,导致无法运行某些事务
  • InnoDB使用自动行级锁,即使在只插入或删除单行的事务中,也可能出现死锁。这是因为插⼊或
    删除行并不是真正的"原⼦"操作,同时会对索引记录进行修改并设置锁
  • 使⽤以下技术来处理死锁并降低发生死锁的可能性:
    • 使⽤事务⽽不是使⽤ LOCK TABLES 语句⼿动加锁,并使用innodb_lock_wait_timeout 变量设置锁的超时时间,保证任何情况下锁都可以⾃动释放
    • 经常使⽤ SHOW ENGINE INNODB STATUS 命令来确定最近⼀次死锁的原因。这可以帮助我
      们修改应⽤程序以避免死锁
    • 如果出现频繁的死锁警告,可以通过启⽤ innodb_print_all_deadlocks 变量来收集调试信息。对于死锁的信息,都记录在MySQL错误⽇志中,调试完成后记得禁⽤此选项
    • 如果事务由于死锁而失败,记得重新发起事务,再执行⼀次
    • 尽量避免大事务,保持事务粒度小且持续时间短,这样事务之间就不容易发⽣冲突,从⽽降低
      发⽣死锁的概率
    • 修改完成后立即提交事务也可以降低死锁发⽣的概率。特别注意的是,不要在⼀个交互式会话
      中⻓时间打开⼀个未提交的事务
    • 当事务中要修改多个表或同⼀表中的不同⾏时,每次都要以⼀致的顺序执行这些操作,使事务中的修改操作形成定义良好的队列,可以避免死锁。⽽不是在不同的位置编写多个类似的INSERT、UPDATE和DELETE语句。我们写的程序其实就是把⼀系列操作组织成⼀个方法或函数
    • 向表中添加适当的索引,以便查询时扫描更少的索引并设置更少的锁,可以使用EXPLAIN SELECT来确定哪些索引用于当前的查询
    • 使用表级锁防止对表进行并发更新,可以避免死锁,但代价是系统的并发性降低
    • 如果在查询时加锁,比如 SELECT…FOR UPDATE 或 SELECT…FOR SHARE ,尝试使⽤较低的隔离级别,比如 READ COMMITTED

4.查看并设置隔离级别

  • 事务的隔离级别分为全局作⽤域和会话作⽤域,查看不同作⽤域事务的隔离级别,可以使用以下的
    方式:在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 设置事务的隔离级别和访问模式,可以使⽤以下语法:在这里插入图片描述

  • 通过选项⽂件指定事务的隔离级别,以便MySQL启动的时候读取并设置在这里插入图片描述

TIPS:
官⽹MySQL8.0更新描述:The tx_isolation and tx_read_only system variables have been removed. Use transaction_isolation and transaction_read_only instead.所以在MySQL5.7及以前的版本中使用 tx_isolation 和 tx_read_only 来设置事务的隔离级别和访问模式

  • 通过SET语法设置系统变量的⽅式设置事务的隔离级别在这里插入图片描述
  • 设置事务隔离级别的语句不能在已开启的事务中执⾏,否则将会报错:在这里插入图片描述

5. READ UNCOMMITTED - 读未提交与脏读

1) 实现方式

  • 读取时:不加任何锁,直接读取版本链中的最新版本,也就是当前读,可能会出现脏读,不可重复读、幻读问题;
  • 更新时:加共享行锁(S锁),事务结束时释放,在数据修改完成之前,其他事务不能修改当前数据,但可以被其他事务读取。

2) 存在问题

事务的 READ UNCOMMITTED 隔离级别不使⽤独占锁,所以并发性能很高,但是会出现大量的数
据安全问题,比如在事务A中执行了⼀条 INSERT 语句,在没有执行 COMMIT 的情况下,会在事务B
中被读取到,此时如果事务A执行回滚操作,那么事务B中读取到事务A写⼊的数据将没有意义,我们
把这个理象叫做 “脏读” 。

3) 问题重现

  • 在一个客户端A中先设置全局事务隔离级别为 READ UNCOMMITTED 读未提交:在这里插入图片描述
  • 打开另⼀个客⼾端B并确认隔离级别在这里插入图片描述
  • 在不同的客户端中执行事务在这里插入图片描述 在这里插入图片描述在这里插入图片描述在这里插入图片描述
  • 由于 READ UNCOMMITTED 读未提交会出现"脏读"现象,在正常的业务中出现这种问题会产⽣非
    常危重后果,所以正常情况下应该避免使⽤ READ UNCOMMITTED 读未提交这种的隔离级别。

6. READ COMMITTED - 读已提交与不可重复读

1) 实现方式

  • 读取时:不加锁,但使⽤快照读,即按照 MVCC 机制读取符合 ReadView 要求的版本数据,每次
    查询都会构造⼀个新的 ReadView ,可以解决脏读,但⽆法解决不可重复读和幻读问题;
  • 更新时:加独占⾏锁(X),事务结束时释放,数据在修改完毕之前,其他事务不能修改也不能读取
    这⾏数据。

2) 存在问题

为了解决脏读问题,可以把事务的隔离级别设置为 READ COMMITTED ,这时事务只能读到了其
他事务提交之后的数据,但会出现不可重复读的问题,比如事务A先对某条数据进行了查询,之后事务
B对这条数据进行了修改,并且提交( COMMIT )事务,事务A再对这条数据进行查询时,得到了事务B
修改之后的结果,这导致了事务A在同⼀个事务中以相同的条件查询得到了不同的值,这个现象要"不
可重复读"。

3) 问题重现

  • 在⼀个客户端A中先设置全局事务隔离级别为 READ COMMITTED 读未提交:在这里插入图片描述

  • 打开另⼀个客⼾端B并确认隔离级别在这里插入图片描述

  • 在不同的客户端中执行事务在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

7. REPEATABLE READ - 可重复读与幻读

1)实现方式

  • 读取时:不加锁,也使⽤快照读,按照MVCC机制读取符合ReadView要求的版本数据,但无论事务中有几次查询,只会在⾸次查询时⽣成⼀个ReadView,可以解决脏读、不可重复读,配合Next-Key行锁可以解决⼀部分幻读问题;
  • 更新时:加Next-Key行锁,事务结束时释放,在⼀个范围内的数据修改完成之前,其他事务不能对这个范围内的数据进行修改、插入和删除操作,同时也不能被查询。

2 )存在问题

事务的 REPEATABLE READ 隔离级别是会出现幻读问题的,在 InnoDB 中使⽤了Next-Key行锁来解决⼤部分场景下的幻读问题,那么在不加 Next-Key ⾏锁的情况下会出现什么问题吗?

我们知道 Next-Key 锁,锁住的是当前索引记录以及索引记录前⾯的间隙,那么在不加 NextKey 锁的情况下,也就是只对当前修改⾏加了独占⾏锁(X),这时记录前的间隙没有被锁定,其他的事务就可以向这个间隙中插⼊记录。

就会导致⼀个问题:事务A查询了⼀个区间的记录得到结果集A,事务B向这个区间的间隙中写⼊了⼀条记录,事务A再查询这个区间的结果集时会查到事务B新写⼊的记录得到结果集B,两次查询的结果集不⼀致,这个现象就是"幻读"。

3) 问题重现

  • 由于 REPEATABLE READ 隔离级别默认使⽤了 Next-Key 锁,为了重现幻读问量,我们把隔离
    级回退到更新时只加了排他锁的 READ COMMITTED .在这里插入图片描述在这里插入图片描述

  • 在不同的客户端中执行事务在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

8 SERIALIZABLE - 串行化

1) 实现方式

  • 读取时:加共享表锁,读取版本链中的最新版本,事务结束时释放;
  • 更新时:加独占表锁,事务结束时释放,完全串行操作,可以解决所有事务问题。

2) 存在问题

所有的更新都是串行操作,效率极低。

9 不同隔离级别的性能与安全

在这里插入图片描述

10.多版本控制(MVCC)

上⼀个小节介绍了实现事务隔离性的锁机制,但是频繁加锁与释放锁会对性能产生比较大的影响,为了提高性能,InnoDB与锁配合,同时采用另⼀种事务隔离性的实现机制 MVCC ,即 MultiVersioned Concurrency Control 多版本并发控制,⽤来解决脏读、不可重复读等事务之间读写问题,MVCC 在某些场景中替代了低效的锁,在保证了隔离性的基础上,提升了读取效率和并发性。

1)实现原理

(1)版本链
  • MVCC的实现是基于 Undo Log 版本链和 ReadView 来完成的,Undo Log做为回滚的基础,在执行Update或Delete操作时,会将每次操作的上⼀个版本记录在Undo Log中,每条Undo Log中都记录⼀个叫做 roll_pointer 的引用信息,通过 roll_pointer 就可以将某条数据对应的Undo Log组织成⼀个Undo链,在数据行的头部通过数据行中的 roll_pointer 与Undo Log中的第一条⽇志进⾏关联,这样就构成⼀条完整的数据版本链,如下图所示:在这里插入图片描述
  • 每⼀条被修改的记录都会有⼀条版本链,体现了这条记录的所有变更,当有事务对这条数据进行修
    改时,将修改后的数据链接到版本链接的头部,如下图中 UNDO3在这里插入图片描述
(2) ReadView
  • 每条数据的版本链都构造好之后,在查询时具体选择哪个版本呢?这⾥就需要使⽤ ReadView 结
    构来实现了,所谓 ReadView 是⼀个内存结构,顾名思义是⼀个视图,在事务使⽤ select 查询
    数据时就会构造⼀个ReadView,⾥⾯记录了该版本链的⼀些统计值,这样在后续查询处理时就不
    ⽤遍历所有版本链了,这些统计值具体包括:
    • m_ids :当前所有活跃事务的集合
    • m_low_limit_id :活跃事务集合中最⼩事务Id
    • m_up_limit_id :下⼀个将被分配的事务Id,也就是 最大的事务Id + 1
    • m_creator_trx_id :创建当前 ReadView 的事务Id
  • 对应的源码如下
/*****************************************************************************
/** @file include/read0types.h
Cursor read
Created 2/16/1997 Heikki Tuuri
*******************************************************/
// ... 省略
// Friend declaration
class MVCC;
/** Read view lists the trx ids of those transactions for which a consistent
read should not see the modifications to the database. */
class ReadView {
// ... 省略
private:
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
trx_id_t m_low_limit_id; // ⼤于等于此值的是未开启的事务,不可⻅
/** The read should see all trx ids which are strictly
smaller (<) than this value. In other words, this is the
low water mark". */
trx_id_t m_up_limit_id; // ⼩于此值的是已提交事务,可⻅
/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_t m_creator_trx_id; // 创建当前ReadView的事务Id
/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids; // 当前所有活跃事务的集合
/** The view does not need to see the undo logs for transactions
whose transaction number is strictly smaller (<) than this value:
they can be removed in purge if not needed by other views */
// 如果当前ReadView和其他ReadView不需要事务Id⼩于此值的Undo⽇志,可以在purge阶段删
除
trx_id_t m_low_limit_no;
/** AC-NL-RO transaction view that has been "closed". */
bool m_closed; // 是否关闭标识
// ... 省略
};
  • 构造好 ReadView 之后需要根据⼀定的查询规则找到唯⼀的可⽤版本,这个查找规则比较简单,以下图的版本链为例,在 m_creator_trx_id=201 的事务执行select 时,会构造⼀个ReadView 同时对相应的变量赋值
    • m_ids :活跃事务集合为 [90, 100, 200]
    • m_up_limit_id :活跃事务最小事务Id= 90
    • m_low_limit_id :预分配事务ID= 202 ,最⼤事务Id=预分配事务ID-1= 201
    • m_creator_trx_id :当前创建 ReadView 的事务Id= 201 在这里插入图片描述
  • 接下来找到版本链头,从链头开始遍历所有版本,根据四步查找规则,判断每个版本:
    • 第⼀步:判断该版本是否为当前事务创建,若 m_creator_trx_id 等于该版本事务id,意味
      着读取自己修改的数据,可以直接访问,如果不等则到第⼆步
    • 第⼆步:若该版本事务Id< m_up_limit_id (最⼩事务Id),意味着该版本在ReadView⽣成之前已经提交,可以直接访问,如果不是则到第三步
    • 第三步:若该版本事务Id>= m_low_limit_id (最⼤事务Id),意味着该版本在ReadView⽣成之后才创建,所以肯定不能被当前事务访问,所以⽆需第四步判断,直接遍历下⼀个版本,如果不是则到第四步
    • 第四步:若该版本事务Id在 m_up_limit_id (最⼩事务Id)和 m_low_limit_id (最⼤事务Id)之间,同时该版本不在活跃事务列表中,意味着创建ReadView时该版本已经提交,可以直接访问,如果不是则遍历并判断下⼀个版本在这里插入图片描述
  • 这样从版本链头遍历判断到版本链尾,找到⾸个符合要求的版本即可,就可以实现查询到的结果都
    是已经提交事务的数据,解决了脏读问题。在这里插入图片描述

2)MVCC是否可以解决不可重复读与幻读

  • ⾸先幻读⽆法通过MVCC单独解决
  • 对于不可重复读问题,在事务中的第⼀个查询时创建⼀个ReadView,后续查询都是⽤这个ReadView进⾏判断,所以每次的查询结果都是⼀样的,从而解决不可重复读问题,在REPEATABLE READ 可重复读,隔离级别下就采⽤的这种方式
  • 如果事务每次查询都创建⼀个新的ReadView,这样就会出现不可重复读问题,在 READ
    COMMITTED 读已提交的隔离级别下就是这种实现方式
http://www.dtcms.com/a/356316.html

相关文章:

  • 使用 Docker 部署 Squid 为 Kubernetes 中的 Nexus3 提供公网代理访问
  • Windows12概念曝光,巧用远程控制工具抢先体验
  • 人脸识别“不备案“有哪些后果?
  • 公司内网部署离线deepseek+docker+ragflow本地模型实战
  • Day15 Logurs框架学习
  • Elasticsearch核心配置与性能优化
  • Linux 线程调度核心要点
  • 期权合约作废了怎么处理?
  • AI共链·智存未来 | 绿算技术受邀出席华为AI SSD发布会
  • 若依微服务一键部署(RuoYi-Cloud):Nacos/Redis/MySQL + Gateway + Robot 接入(踩坑与修复全记录)
  • 吱吱企业通讯软件可私有化部署,构建安全可控的通讯办公平台
  • C++异常处理指南:构建健壮程序的错误处理机制
  • 2025年渗透测试面试题总结-39(题目+回答)
  • FDTD_mie散射_仿真学习(2)
  • AWS集成开发最佳实践:构建高效可靠的云管理平台
  • 海运业务怎么管?解析海运货代系统的核心功能模块
  • Blender建模软件基本操作--学习笔记1
  • CSS text-decoration-thickness:精细控制文本装饰线粗细的新属性
  • Git 9 ,.git/index.lock 文件冲突问题( .git/index.lock‘: File exists. )
  • 亚马逊巴西战略升级:物流网络重构背后的生态革新与技术赋能之路
  • 基于SpringBoot的足球青训俱乐部管理系统
  • 【数组特殊排序最小最大次小次大依次类推规律放置】2022-10-27
  • 香港电讯为知名投资公司搭建高效、安全IT管理服务体系
  • Java学习day_13之API(常用API对象克隆)
  • 高效接入:Suno API 与主流编程语言的结合
  • 从“安全诉讼”说起:奖励模型(Reward Model)是LLM对齐的总阀门(全视角分析)
  • 龙迅#LT7641GX适用于四路HDMI2.1/DP/TPYE-C转HDMI2.1混切应用,分辨率高达8K60HZ!
  • 【谷歌浏览器】浏览器实用自用版——谷歌浏览器(Google Chrome)离线纯净版安装 官方版无任何捆绑及广告 【离线安装谷歌浏览器】
  • 智能体开发:学习与实验 ReAct
  • AI Agent实战:提升大模型应用能力——提示链、响应净化与结构化输出技术详解