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

[MySQL数据库] 事务与锁

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (93平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(97平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(95平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
🎃Redis(97平均质量分)https://blog.csdn.net/2301_80050796/category_12777129.html?spm=1001.2014.3001.5482
🐰RabbitMQ(97平均质量分) https://blog.csdn.net/2301_80050796/category_12792900.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
在这里插入图片描述

目录

  • 1. 什么是事务
  • 2. 为什么使用事务
  • 3. 怎么使用事务
  • 4. 如何实现原子性
  • 5. 如何实现持久性
  • 6. 隔离性的实现原理
    • 6.1 事务的隔离性
    • 6.2 事务的隔离级别
    • 6.3 锁
      • 6.3.1 锁类型与锁模式
      • 6.3.2 共享锁和独占锁-Shared and Exclusive Locks
      • 6.3.3 意向锁-Intention Locks
      • 6.3.4 索引记录锁-record locks
      • 6.3.5 间隙锁-gap locks
      • 6.3.6 临键锁-next key locks
      • 6.3.7 插入意向锁-Insert Intention Locks
      • 6.3.8 AUTO-INC Locks
      • 6.3.9 死锁
        • 6.3.9.1 什么是死锁
        • 6.3.9.2 死锁产生的条件
        • 6.3.9.3 InnoDB对死锁的检测
        • 6.3.9.4 如何避免死锁
    • 6.4 查看并设置隔离级别
    • 6.5 READ UNCOMMITED 读未提交与脏读
      • 6.5.1 实现方式
      • 6.5.2 存在问题
      • 6.5.3 问题重现
    • 6.6 READ COMMITTED 读已提交与不可重复读
      • 6.6.1 实现方式
      • 6.6.2 存在问题
      • 6.6.3 问题重现
    • 6.7 REPEATABLE READ 可重复读与幻读
      • 6.7.1 实现方式
      • 6.7.2 存在问题
      • 6.7.3 问题重现
    • 6.8 serializable 串行化
      • 6.8.1 实现方式
      • 6.8.2 存在问题
    • 6.9 不提供隔离级别的性能安全
    • 6.10 多版本控制(MVCC)
      • 6.10.1 实现原理
        • 6.10.1.1 版本链
        • 6.10.1.2 ReadView
      • 6.10.2 MVCC如何解决脏读与不可重复读问题

1. 什么是事务

事务指逻辑上的一组操作,就是把多个操作打包为一个操作,组成这组操作的各个单元,要么全部成功,要么全部失败。在不同的环境中,都可以有事务。对应在数据库中,就是数据库事务,数据库事务可以有效避免部分执行,部分未执行的中间状态.如果在执行的过程中,我们不使用事务的话,当服务崩溃的时候,整体的数据就会不正确.
在这里插入图片描述
以下的四点在事务的整个执行过程中必须要得到保证,这也就是事务的ACID特性:

  1. Atomicity(原子性): 一个事务中的所有操作,要么全部成功,要么全部失败,不会出现之执行了一半的情况,如果事务在执行的过程中发生了错误,那么会触发回滚机制(rollback),会回滚到事务开始前的状态,就像这个事务从来没有执行过一样.在回滚的时候,会使用undolog进行回滚,undolog中记录了数据修改之前的记录.
  2. Consistency(一致性): 在事务开始之前和事务结束之后,数据的完整性不会破坏,这表示写入的数据必须完全符合所有的预设规则,包括数据的精度,关联性以及关于事务执行的过程中服务器崩溃后如何恢复.
  3. Isolation(隔离性): 数据库允许多个并发事务同时对数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致,事务可以指定不同的隔离级别,以权衡在不同的应用场景之下数据库的性能和安全,后面的小节会详细介绍.
  4. Durability(持久性): 事务处理之后,对数据的修改将永久的写入存储(磁盘)介质,即便系统故障也不会丢失.

需要重点说明的是,事务最终要保证数据的可靠和⼀致,也就是说 ACID 中的Consistency(⼀致
性)是最终的目的,那么当事务同时满⾜了Atomicity(原⼦性),Isolation(隔离性)和Durability(持久性)时,也就实现了⼀致性.

2. 为什么使用事务

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

3. 怎么使用事务

  • 首先需要存储引擎支持事务,我们默认的存储引擎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启用自动提交,也就是每个语句都是一个事务,不能使用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; # 方式二
  • 手动提交模式之下,提交或回滚事务时直接使用commit或者rollback
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> 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 Transaction或者begin开启事务之后,必须要通过commit提交才会支持持久化,与是否设置set autocommit无关.

4. 如何实现原子性

在一个事务的执行过程中,如果多条DML语句顺利执行,那么结果会最终写入数据库,如果在事务的执行过程中,其中一条DML语句出现异常,导致后面的语句无法继续执行或即使继续执行也会导致数据不完整,不一致,这时前面执行的语句已经对数据做出了修改,如果要保证一致性,就需要对之前的修改做撤销操作,这个撤销操作称为回滚rollback,如下图所示:

在这里插入图片描述

  • 事务的回滚原理是什么呢?回滚过程中依据的是什么呢?依据的是InnoDB中的Undolog,在事务执行每个DML之前会把原始的数据放在一个日志中,作为回滚的依据,这个日志称为undolog,在不考虑缓存和刷盘的条件下,执行过程如下:
    在这里插入图片描述
  • 当需要回滚的时候,MySQL根据操作的类型,Insert undo链或update undo链中读取响应的日志记录,反向执行修改,使数据还原,完成回滚.
  • 通过undo log实现了数据回滚操作,这时就可以保证事务成功的时候全部的sql语句都执行成功,在事务失败的时候全部sql语句都执行失败.

5. 如何实现持久性

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

在这里插入图片描述
在数据真正落盘之前,会把日志写入Redo Log和双写缓冲区中,如果在此期间MySQL发生崩溃,会根据之前生成的RedoLog和双写缓冲区中的数据把数据恢复过来,保证数据正确落盘.

6. 隔离性的实现原理

6.1 事务的隔离性

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

6.2 事务的隔离级别

事务具有隔离性,那么如何实现事务之间的隔离,隔离到什么程度,如何保证数据安全的同时也要兼顾性能,这都是要思考的问题.
在并发执行的过程中,多个线程对同一个共享变量修改的时候,在不加限制的情况下会出现线程安全问题,我们解决线程安全问题时,一般的做法是通过对修改操作进行加锁,同理,多个事务在对同一个表中的同一条数据进行了修改时,如果要实现事务之间的隔离也可以通过锁来完成,在MySQL中常见的锁包括: 读锁,写锁,行锁,间隙锁,next-key锁等,不同的锁策略联合多版本并发控制可以实现事务间不同程度的隔离,称为事务的隔离级别.不同的隔离级别能在性能和安全方面做了取舍,有的隔离级别注重并发性,有的注重安全性,有的则是并发和安全适中,在MySQL的InnoDB引擎中事务的隔离级别是四种,分别是:

  • Read uncommited,读未提交
  • Read committed,读已提交
  • repeatable Read,可重复读(默认)
  • serializable,串行化

6.3 锁

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

6.3.1 锁类型与锁模式

  • 锁类型
    锁类型依赖于存储引擎,在InnoDB存储引擎中按照锁的粒度分为,行级锁recode和表级锁table:
    • 行级锁也叫行锁,是对表中的某些具体的数据进行加锁.
    • 表级锁也叫表锁,是对整个数据表加锁.
    • 在之前的版本中也有页级锁,也叫页锁,锁定的是一个数据页,MySQL8中没有页级锁.
  • 锁模式
    锁模式,用来描述如何请求(申请)锁,分为共享锁(S),独占锁(X),意向共享锁(IS),意向独占锁(IX),记录锁,间隙锁,next-key锁,auto-inc锁,空间索引的谓词锁等.

6.3.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行上的锁.

6.3.3 意向锁-Intention Locks

  • InnoDB支持多粒度锁,允许行锁和表锁共存.
  • InnoDB使用意向锁实现多粒度级别的锁,意向锁是表级别的锁,它并不是真正意义上的加锁,而是在data_locks中记录事务以后要对表中的哪一行加哪种类型的锁(共享锁或者排它锁),意向锁分为两种:
    • 意向共享锁(IS): 表示事务打算对表中的单个行设置共享锁.
    • 意向排它锁(IX): 表示事务打算对表中的单个行设置排它锁.
  • 在获取意向锁时又如下的协议:
    • 在事务获得表中某一行的共享锁(S)之前,它必须首先获得该表上的IS锁或者更强的锁.
    • 在事务获得表中某一行的排它锁(X)之前,它必须首先获得该表上的IX锁.
  • 意向锁可以提高加锁的性能,在真正加锁之前不需要遍历表中的行是否加锁,只需要查看一下表中的意向锁即可.
  • 在请求锁过程中,如果将要请求的锁与现有锁兼容,则将锁授予请求的事务,如果与现有的锁冲突,则不会授予,事务将阻塞等待,直到冲突的锁别释放,意向锁与行级锁的兼容性如下表:

在这里插入图片描述

  • 除了全表锁定请求之外,意向锁不会阻止任何锁请求,意向锁的主要目的是表示事务正在锁定某行或者正在意图锁定某一行.

6.3.4 索引记录锁-record locks

  • 索引记录锁或者称为精准行锁,顾名思义是指索引记录上的锁,如下sql锁住的指定的一行:
# 防⽌任何其他事务插⼊、更新或删除值为1的⾏,id为索引列
SELECT * FROM account WHERE id = 1 For UPDATE;
  • 索引记录锁总是锁定索引行,在没有定义索引的情况之下,InnoDB创建一个隐藏的聚集索引,并使用该索引进行记录锁定,当使用索引进行查找时,锁定的只是满足条件的行,如图所示:

在这里插入图片描述

6.3.5 间隙锁-gap locks

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

6.3.6 临键锁-next key locks

  • next-key锁是索引记录锁和索引记录之前间隙上间隙锁的组合,如图所示:
    在这里插入图片描述
  • InnoDB搜索或扫描一个表的索引时,执行行级锁策略,具体方式是: 在扫描过程中遇到的索引记录上设置共享锁或排它锁,因此,行级锁策略实际上应用的时索引记录锁,索引记录上的next-key锁也会影响该索引记录之间的"间隙",也就是说,next-key锁是索引记录锁加上索引记录前面的间隙锁.
  • 假设索引包含值10,11,13和20,这些索引可能next-key锁覆盖的以下区间,其中圆括号表示不包含区间的端点,方括号表示包含端点(左开右闭):
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
  • 默认情况下,repeatable Read事务隔离级别开启next-key锁并进行搜索和索引扫描,可以防止幻象行,从而解决幻读问题,后面我们再分析.

6.3.7 插入意向锁-Insert Intention Locks

  • 插入意向锁是一个特殊的间隙锁,在向索引记录之前的间隙进行Insert操作插入数据时使用,如果多个事务向相同索引间隙中不同的位置插入记录,则不要彼此等待,假设已经存在值10和20的索引记录,两个事务分别尝试插入索引值为15和16的行,在获得插入行上的排它锁之前,每个事务都用插入意向锁锁住10到20之间的间隙,但不会相互阻塞,因为他们所操作的行并不冲突.

6.3.8 AUTO-INC Locks

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

6.3.9 死锁

6.3.9.1 什么是死锁
  • 由于每个事务都持有另一个事务所需的锁,导致事务无法继续进行的情况称为死锁,以下图为例,两个事务都不会主动释放自己持有的锁,并且都在等待对方持有的资源变得可用.
    在这里插入图片描述
6.3.9.2 死锁产生的条件
  • 互斥访问: 如果线程1获取到锁A,那么线程2就不能同时得到锁A.
  • 不可抢占:获取到锁的线程,只能自己主动释放锁,别的线程不能从他的手中抢占锁
  • 保持与请求:线程1已经获得了锁A,还要在这个基础上再去获了锁B
  • 循环等待:线程1等待线程2释放锁,线程2也等待线程1释放锁,死锁发⽣时系统中⼀定有由两个或两个以上的线程组成的⼀条环路,该环路中的每个线程都在等待着下⼀个进程释放锁

以上四条是造成死锁的必要条件,必须同时满⾜,所以如果想要打破死锁,可以破坏以上四个条件之⼀,最常⻅的⽅式就是打破循环等待

6.3.9.3 InnoDB对死锁的检测
  • InnoDB在运行时会对死锁进行检测,当死锁检测启用时(默认),InnoDB自动检测事务死锁,并回滚一个或者多个事务来打破锁,InnoDB尝试选择小事务来回滚,其中事务的大小由插入,更新或删除的行数决定.
  • 当超过200个事务等待锁资源或等待锁的个数超过1000000个时也会被视为死锁,并尝试将等待列表的事务回滚.
  • 在高并发系统中,多个线程等待相同的锁时,死锁检测的可能会导致性能变慢,此时禁用死锁检测依赖系统变量innodb_lock_wait_timeout设置进行事务回滚可能性能会更高,可以通过设置系统变量innodb_deadlock_detect[={OFF|ON}]禁用死锁检测.
6.3.9.4 如何避免死锁
  • MySQL是一个多线程程序,死锁的情况大概率会发生,但是他并不可怕,除非平凡出现,导致无法运行某些事务.
  • InnoDB使用自动行级锁,即使在值插入或删除单行的事务中,也可能出现死锁,这是因为插入或删除行并不是真正的"原子"操作,同时会对索引记录进行修改并设置锁.
  • 使用以下的技术来处理死锁并降低死锁发生的概率:
    • 使用事务而不是使用LOCK TABLES语句手动加锁,并使用innodb_lock_wait_timeout变量设置锁的超时时间,保证任何情况之下都可以自动释放.
    • 经常使用show engine innodb status命令来确定最近一次死锁的原因,这可以帮助我们修改程序以避免死锁.
    • 如果出现频繁的死锁警告,可以启用innodb_print_all_deadlocks变量来手机调试信息,对于死锁的信息,都记录在MySQL的错误日志中,调试完成之后记得禁用此选项.
    • 如果事务由于死锁而失败,记得重新发起事务,再执行一次
    • 尽量避免大事务,保持事务粒度小而且持续时间短,这样事务之间就不容易发生冲突,从而降低发生死锁的概率.
    • 修改完成之后立即提交事务,特别注意的是,不要在一个交互式会话中长时间打开一个未提交的事务.
    • 当事务中要修改多个表或者是同一个表中的不同行时,每次都要以一致的顺序执行这些操作,使事务中的修改操作形成定义良好的队列,可以避免死锁.而不是在不同的位置编写多个类似的Insert,update和delete语句.我们写的程序其实就是把一系列操作组织成一个方法或者函数.
    • 使用表级锁防止对表进行更新,可以避免死锁,但是代价是系统的并发性降低
    • 如果在查询是时加锁,比如select...for updateselect...for share,尝试使用较低的隔离级别,比如Read commited.

6.4 查看并设置隔离级别

  • 事务的隔离级别分为全局作用域和回话作用域,查看不同的隔离级别,可以使用以下的方式:
# 全局作⽤域
SELECT @@GLOBAL.transaction_isolation;
# 会话作⽤域
SELECT @@SESSION.transaction_isolation;
# 可以看到默认的事务隔离级别是REPEATABLE-READ(可重复读)
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ 				  | # 默认是可重复读
+---------------------------------+
1 row in set (0.00 sec)
  • 设置事务的隔离级别和访问模式,可以使用以下的语法:
# 通过GLOBAL|SESSION分别指定不同作⽤域的事务隔离级别
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level|access_mode;# 隔离级别
level: {REPEATABLE READ # 可重复读| READ COMMITTED # 读已提交| READ UNCOMMITTED # 读未提交| SERIALIZABLE # 串⾏化
}
# 访问模式
access_mode: {READ WRITE # 表⽰事务可以对数据进⾏读写| READ ONLY # 表⽰事务是只读,不能对数据进⾏读写
}
# ⽰例
# 设置全局事务隔离级别为串⾏化
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 设置会话事务隔离级别为串⾏化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 如果不指定任何作⽤域,设置将在下⼀个事务开始⽣效
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • 通过选项文件指定事务的隔离级别,以便MySQL启动的时候读取并设置
[mysqld]
transaction-isolation = REPEATABLE-READ # 隔离级别为可重复读
transaction-read-only = OFF # 关闭只读意味着访问模式为读写
  • 也可以通过以set语法设置系统变量的方式来设置事务的隔离级别
# ⽅式⼀
SET GLOBAL transaction_isolation = 'SERIALIZABLE';
# 注意使⽤SET语法时有空格要⽤"-"代替
SET SESSION transaction_isolation = 'REPEATABLE-READ'; 
# ⽅式⼆
SET @@GLOBAL.transaction_isolation='SERIALIZABLE';
# 注意使⽤SET语法时有空格要⽤"-"代替
SET @@SESSION.transaction_isolation='REPEATABLE-READ';

注意: 设置事务的隔离级别的时候,不能在已经开启的事务中执行,否则会报错.

6.5 READ UNCOMMITED 读未提交与脏读

6.5.1 实现方式

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

6.5.2 存在问题

事务的READ UNCOMMITTED隔离级别的不使用独占锁,所以并发性能很高,但是会出现大量的数据安全问题,比如在事务A中执行了一条Insert语句,在没有执行commit的情况下,会在事务B中被读取到,此时如果事务A执行回滚操作,那么事务B中读取到的事务A写入的数据将没有意义,我们把这个现象叫做"脏读".由于READ UNCOMMITTED读未提交会出现"脏读"现象,在正常的业务出现这种问题会产生非常严重的后果,所以正常情况下应该避免使用READ UNCOMMITTED读未提交这种隔离级别.

6.5.3 问题重现

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

6.6 READ COMMITTED 读已提交与不可重复读

6.6.1 实现方式

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

6.6.2 存在问题

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

6.6.3 问题重现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.7 REPEATABLE READ 可重复读与幻读

6.7.1 实现方式

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

6.7.2 存在问题

在SQL的标准定义中,事务的REPEATABLE READ隔离级别是会出现幻读问题的,但是在InnoDB中使用了Next-key行锁来解决大部分场景下的幻读问题,那么在不加next-key行锁的情况下会出现什么问题吗?
我们知道next-key锁,锁住的时当前索引记录以及索引记录前面的间隙,那么在不加next-key锁的情况下,也就是只对当前修改行加了独占行锁(X),这时记录前面的间隙没有被锁定,其他的事务就可以向这个间隙中插入记录,就会导致一个问题: 事务A查询了一个区间的记录得到结果集A,事务B向这个区间的间隙中写入了一条记录,事务A再查询这个区间的结果集时会查询事务B新吸入的记录得到结果集B,两次查询的结果集不一致,这个现象就是"幻读".

6.7.3 问题重现

由于REPEATABLE READ隔离级别默认使用了next-key锁,为了重现幻读问题,我们把隔离级别退回到更新时只加排它锁的READ COMMITTED
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.8 serializable 串行化

6.8.1 实现方式

  • 读取时: 加共享表锁,读取版本链中最新版本,事务结束时释放.
  • 更新时: 加独占表锁,事务结束时释放,完全串行操作,可以解决所有的事务问题.
  • 与前面不同,这里加的都是表锁,而前面加的都是行锁.

6.8.2 存在问题

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

6.9 不提供隔离级别的性能安全

在这里插入图片描述

6.10 多版本控制(MVCC)

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

6.10.1 实现原理

6.10.1.1 版本链
  • MVCC的实现是基于undo Log版本链和ReadView来完成的,版本链在所有事务中时共享的,所有的事务只要是访问数据的id相同,访问的都是同一条版本链,undo log作为回滚的基础,在执行update或者delete,会将每次操作的上一个版本记录在undo log中,每条undo log中都记录一个叫做roll_pointer的引用信息,通过roll_pointer就可以将某条数据对应的undo Log组织成一个undo链,在数据行的头部通过数据行中的roll_pointer与undo Log中的第一条日志进行关联,这样就构成了一条完整的数据版本链,如下如所示:
    在这里插入图片描述
  • 每一条被修改的记录都会有一条版本链,体现了这条记录的有所变更,当有事务对这条记录进行修改时,将修改后的数据链连接到版本的头部,如下图中的undo3:
    在这里插入图片描述
6.10.1.2 ReadView
  • 每条数据的版本链都构造好之后,在查询时具体选择哪个版本呢?这里就需要ReadView结构来实现了,所谓ReadView是一个内存结构,顾名思义是一个视图,在事务使用select查询数据时就会构造一个ReadView,里面记录了版本链的一些统计值,这样后续查询处理时就不用遍历所有版本链了,这些统计包括:

    • m_ids: 当前所有活跃事务的集合,即没有提交或者没有回滚的事务
    • m_low_limit_id: 活跃事务中最小的事务id,如果undo版本链中的事务id小于此值,说明该事物已经提交
    • m_up_limit_id: 下一个将被分配的事务id,也就是最大的事务id+1,即还没有创建的事务,对于当前事务来说是不可见的.
    • m_creator_trx_id: 创建当前的ReadView的事务id.
  • 构造好的ReadView之后需要根据一定的查询规则找到唯一可用的版本,这个查找规则比较简单,以下图的版本链为例,在m_creator_trx_id=201的事务执行select时,会构造一个ReadView同时对应的变量赋值.

    • m_ids: 活跃事务集合为[90,100,200]
    • m_low_limit_id: 活跃事务最小事务id=90
    • m_up_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时该版本已经提交,可以直接访问,如果不是则遍历并判断下一个版本.
      在这里插入图片描述

6.10.2 MVCC如何解决脏读与不可重复读问题

  • 首先幻读无法通过MVCC单独解决
  • ReadView解决脏读问题: 从版本链头遍历到版本链尾,找到首个符合要求的版本即可,就可以实现查询到的结果都是已经提交的事务数据,解决了脏读问题
    在这里插入图片描述
  • 对于不可重复读问题,在事务中的第一个查询时创建的一个ReadView,后续查询都是用这个ReadView进行判断,所以每次的查询结果都是一样的,从而解决不可重复读问题,在REPEATABLE READ可重复读,隔离级别下就是采用的这种方式.
  • 如果每次查询都创建一个新的ReadView,这样就会出现不可重复读问题,因为每次创建的ReadView的活跃事务id都不一样,所以导致查询版本链的时候每个ReadView查询到的第一个符合条件的数据版本也不一样,从而导致了不可重复读问题,在读已提交中就使用的是这种实现方式.在一个事务中可能会创建多个ReadView.
http://www.xdnf.cn/news/2310.html

相关文章:

  • DIY 3D打印机 原理及步骤概况
  • Java----super 关键字
  • 《ATPL地面培训教材13:飞行原理》——第13章:高速飞行
  • Linux进程解析
  • 信息系统项目管理师备考计划
  • 摸鱼屏保神器工具软件下载及使用教程
  • C#里使用libxl来加载网络传送过来的EXCEL文件
  • 计算机二级MS Office第一套演示文稿
  • 图解 Redis 事务 ACID特性 |源码解析|EXEC、WATCH、QUEUE
  • 【数据湖】Time Travel时间旅行
  • 每日学习Java之一万个为什么?
  • 3.1 掌握RDD的创建
  • 英语学习4.26
  • 进行物联网安全PoC时的注意事项
  • 【Java-Day 1】开启编程之旅:详解Java JDK安装、环境配置与运行HelloWorld
  • 用c语言实现——一个动态顺序存储的串结构
  • 山东大学软件学院项目实训-基于大模型的模拟面试系统-前端美化滚动条问题
  • 2025年4月25日第一轮
  • Vue Composition API 与 Options API:全面对比与使用指南
  • HTML快速入门-4:HTML <meta> 标签属性详解
  • 【漫话机器学习系列】224.双曲正切激活函数(Hyperbolic Tangent Activation Function)
  • 现在流行的linux面板管理工具
  • 三款实用工具推荐:图片无损放大+音乐格式转换+音视频格式转换!
  • TCGA 数据下载与生存分析 //todo
  • FreeRTOS事件标志组详解:高效的任务间通知机制
  • 结合五层网络结构讲一下用户在浏览器输入一个网址并按下回车后到底发生了什么?
  • 机器学习基础理论 - 频率派 vs 贝叶斯派
  • Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?
  • 什么是Lua模块?你会如何使用NGINX的Lua模块来定制请求处理流程?
  • Spring 学习笔记之 @Transactional 异常不回滚汇总