MySQL中的事务与锁

发布于:2024-12-06 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

事务

InnoDB 和 ACID 模型?

原?性的实现

持久性的实现?

?隔离性的实现

隔离级别?

?多版本控制(MVCC)


事务

1.什么是事务

事务是把组SQL语句打包成为个整体,在这组SQL的执过程中,要么全部成功,要么全部失败,这组SQL语句可以是条也可以是多条

示例:转账的例

在这个例中,涉及了两条更新语句:

# ================账?表====================
CREATE TABLE `account` (
 `id` bigint PRIMARY KEY AUTO_INCREMENT, 
 `name` varchar(255) NOT NULL, # 姓名
 `balance` decimal(10, 2) NOT NULL # 余额
);
INSERT INTO account(`name`, balance) VALUES('张三', 1000);
INSERT INTO account(`name`, balance) VALUES('李四', 1000);
# ================更新操作===================
# 张三余额减少100
UPDATE account set balance = balance - 100 where name = '张三';
# 李四余额增加100
UPDATE account set balance = balance + 100 where name = '李四';

如果不用事务管理的话,当执行第一条sql语句之后,服务器崩溃了,那么整体的数据就不正确了

如果没有发生崩溃转账成功,应该有以下结果:

1. 张三的账余额减少 100 ,变成 900 ,李四的账余额增加了 100 ,变成 1100 ,不能出现张三的余额减少李四的余额没有增加的情况;(原子性)

2. 张三和李四在发转账前后的总额不变,也就是说转账前张三和李四的余额总数为 1000+1000=2000 ,转账后他们的余额总数为 900+1100=2000 ;(一致性)

3. 转账后的余额结果应当保存到存储介质中,以便以后读取;(持久性)

4. 还有点需要要注意,在转账的处理过程中张三和李四的余额不能因其他的转账事件受到扰;(隔离性)

以上这四点在事务的整个执过程中必须要得到保证,这也就是事务的 ACID 特性,即:

1.Atomicity (原性):个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执了半的情况,如果事务在执过程中发错误,会回滚( Rollback )到事务开始前的状态,就像这个事务从来没有执过样;

2.Consistency (致性):在事务开始之前和事务结束以后,数据库的完整性不会被破坏。这表写的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执过程中服务器崩溃后如何恢复;

3.Isolation (隔离性):数据库允许多个并发事务同时对数据进读写和修改,隔离性可以防多个事务并发执时由于交叉执导致数据的不致。事务可以指定不同的隔离级别,以权衡在不同的应场景下数据库性能和安全,后的节会详细介绍;

4.Durability (持久性):事务处理结束后,对数据的修改将永久的写存储介质,即便系统故障也不会丢失

2.为什么要使事务 ACID特性可以保证数据安全

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

3.使事务

1.要使事务那么数据库就要持事务,在MySQL中持事务的存储引擎是InnoDB,可以通过show engines; 语句查看:

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

(1)START TRANSACTION 或 BEGIN 开始个新的事务;

(2)COMMIT 提交当前事务,并对更改持久化保存;

(3)ROLLBACK 回滚当前事务,取消其更改;

(4)SET autocommit 禁或启当前会话的默认动提交模式, autocommit 是个系统变

量可以通过选项指定也可以通过命令设置 --autocommit[={OFF|ON}]

3.演开启个事务,执修改后并回滚

# 开启事务
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)

4.演开启个事务,执修改后并提交

# 开启事务
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)

5.默认情况下MySQL启事务动提交,也就是说每个语句都是个事务,就像被 START TRANSACTION 和 COMMIT 包裹样,不能使 ROLLBACK 来撤销执结果;但是如果在语句 执期间发错误,则动回滚;

6.通过 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)

如果关闭自动提交,完成数据的修改后,必须使用commit提交事务或rollback回滚事务

7.动提交模式下,提交或回滚事务时直接使 commit 或 rollback

# 查看事务提交模式,确定?动提交已关闭
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)

8.通过 SET autocommit 设置动与动提交

9.只要使 START TRANSACTION 或 BEGIN 开启事务,必须要通过 COMMIT 提交或rollback回滚才会持 久化,与是否设置 SET autocommit 关

InnoDB 和 ACID 模型

ACID模型是组数据库设计原则,强调业务数据的可靠性,MySQL的InnoDB存储引擎严格遵循

ACID模型,不会因为软件崩溃和硬件故障等异常导致数据的不完整。在ACID的实现过程中涉及到些系统变量和相关知识点在这先列出来,后我们要逐步介绍:

1.Atomicity(原性):原性主要涉及InnoDB的事务开启与提交:

(1)设置 autocommit[={OFF|ON}] 系统变量,开启和禁事务是否动提交.

(2)使 START TRANSACTION 或 BEGIN TRANSACTION 语句开启事务;

(3)使 COMMIT 语句提交事务;

(4)使 ROLLBACK 语句回滚事务

  1. Consistency(致性):致性主要涉及InnoDB内部对于崩溃时数据保护的相关处理,相关特性包 括:

(1)InnoDB 存储引擎的双写缓冲区 doublewrite buffer ;

(2)InnoDB 存储引擎的崩溃恢复

3. Isolation(隔离性):隔离主要涉及应于每个事务的隔离级别,相关特性包括:

(1)通过 SET TRANSACTION 语句设置事务的隔离级别;

(2)InnoDB 存储引擎的锁,锁可以在 INFORMATION_SCHEMA 系统库和 Performance

Schema 系统库中的 data_locks 和 data_lock_waits 表查看

4.Durability(持久性):持久性涉及MySQL与特定硬件配置的交互,可能性取决于CPU、络和存储设备的性能,由于硬件环境较复杂,所以法提供固定的操作指南,只能根据实际环境进测试得到最佳的性能,相关特性包括:

(1)InnoDB 存储引擎的双写缓冲区 doublewrite buffer ;

(2)innodb_flush_log_at_trx_commit 系统变量的设置;Redo log的刷盘策略

(3)sync_binlog 系统变量的设置;在集群环境中使用

(4)innodb_file_per_table 系统变量的设置;

(5)存储设备(如磁盘驱动器、SSD或RAID磁盘阵列)中的写缓冲区;

(6)存储设备中由电池持的缓存。

(7)运MySQL的操作系统,特别是对 fsync() (内存与磁盘之间同步数据的函数) 系统调的持;

(8)不间断电源UPS (uninterruptible power supply),保护所有运MySQL服务器和数据存储设备的电供应;

(9)备份策略,例如备份的频率和类型,以及备份保留周期;

(10)分布式环境中数据中之间的络连接

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

原性的实现

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

2.回滚操作是如何实现的呢?回滚过程中依据的是什么呢?在InnoDB专题中介绍过UndoLog的

作和原理,我们致回顾下,在事务执每个DML之前,把原始数据记录在个志,做为

回滚的依据,这个志称为 Undo Log (回滚志或撤销志),在不考虑缓存和刷盘的条件下,

执过程如下所:

3.当需要回滚操作时,MySQL根据操作类型,在Insert Undo链或Update Undo链中读取相应的志记录,并反向执修改,使数据还原,完成回滚。

4.通过 Undo Log 实现了数据的回滚操作,这时就可以保证在事务成功的时候全部的SQL语句都执成功,在事务失败的时候全部的SQL语句都执失败,实现在原性。

持久性的实现

1.提交的事务要把数据写(持久化到)存储介质,如磁盘。在正常情况下多没有问题,可是在服务器崩溃或突然断电的情况下,个事务中的多个修改操作,只有部分写了数据件,另部分没有写,如果不做针对处理的话,就会造成数据的丢失,从导致数据不完整,也就不能保证致性。

2.在真正写数据件之前,MySQL会把事务中的所有DML操作以志的形式记录下来,以便在服务器下次启动的时候进恢复操作,恢复操作的过程就是把志中没有写到数据件的记录重新执遍,保证所有的需要保存的数据都持久化到存储介质中,我们把这个志称为 Redo Log (重做志);成重做志是保证数据致性的重要环节。在持久化的处理过程中,还包括缓冲池、 Doublewrite Buffer (双写缓冲区)、 Binary Log (进制志) 等知识点

隔离性的实现

1.事务的隔离性 --隔离是为了把被隔离对象分隔开,防止被隔离对象之间相互影响

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

示例如下:

2.事务的隔离级别

事务具有隔离性,那么如何实现事务之间的隔离?隔离到什么程度?如何保证数据安全的同时也

要兼顾性能?这都是要思考的问题。

如果家学习过多线程技术,都知道在并发执的过程中,多个线程对同个共享变量进修改

时,在不加限制的情况下会出现线程安全问题,我们解决线程安全问题时,般的做法是通过对修改操作进加锁;同理,多个事务在对同个表中的同条数据进修改时,如果要实现事务间的隔离也可以通过锁来完成,在MySQL中常的锁包括:读锁,写锁,锁,间隙锁,Next-Key锁等,不同的锁策略联合多版本并发控制可以实现事务间不同程度的隔离,称为事务的隔离级别;不同的隔离级别在性能和安全做了取舍,有的隔离级别注重并发性,有的注重安全性,有的则是并发和安全适中;在MySQL的InnoDB引擎中事务的隔离级别有四种,分别是:

简单总结就是事务的隔离级别是通过锁策略联合多版本并发控制实现的

实现事务隔离级别的过程中到了锁,所谓锁就是在事务A修改某些数据时,对这些数据加把

锁,防其他事务同时对这些数据执修改操作;当事务A完成修改操作后,释放当前持有的锁,以便其他事务再次上锁执对应的操作。不同存储引擎中的锁功能并不相同,下来介绍InnoDB存储引擎中的锁。

1.锁信息

锁的信息包括锁的请求(申请),锁的持有以及阻塞状态等等,都保存在 performance_schema

库的 data_locks 表中,可以通过以下式查看:

mysql> SELECT * FROM performance_schema.data_locksG
*************************** 1. row ***************************
 ENGINE: INNODB # 持有或请求锁的存储引擎
 ENGINE_LOCK_ID: 139664434886512:1059:139664350547912 # 存储引擎持有或请求
的锁的ID
ENGINE_TRANSACTION_ID: 2569 # 请求锁的事务对应的存储引擎内部ID
 THREAD_ID: 46 # 创建锁的会话的线程ID
 EVENT_ID: 12 # 请求锁的事件ID
 OBJECT_SCHEMA: test_db # 锁定表所在的数据库
 OBJECT_NAME: account # 被锁定表的名称
 PARTITION_NAME: NULL # 锁定分区的名称; 没有使?表分区时为NULL
 SUBPARTITION_NAME: NULL # 锁定?分区的名称; 没有使??分区时为NULL
 INDEX_NAME: NULL # 锁定索引的名称; 没有使?索引时为NULL
OBJECT_INSTANCE_BEGIN: 139664350547912 # 锁在内存中的地址
 LOCK_TYPE: TABLE # 锁的类型
 LOCK_MODE: IX # 锁的模式,表?如何请求锁
 LOCK_STATUS: GRANTED # 锁请求的状态; GRANTED(持有),WAITING(等待)
 LOCK_DATA: NULL # 与锁相关的数据(如果有)
*************************** 2. row ***************************
 ENGINE: INNODB
 ENGINE_LOCK_ID: 139664434886512:2:4:1:139664350544872
ENGINE_TRANSACTION_ID: 2569
 THREAD_ID: 46
 EVENT_ID: 12
 OBJECT_SCHEMA: test_db 
 OBJECT_NAME: account
PARTITION_NAME: NULL
 SUBPARTITION_NAME: NULL
 INDEX_NAME: GEN_CLUST_INDEX
OBJECT_INSTANCE_BEGIN: 139664350544872
 LOCK_TYPE: RECORD # 锁的类型
 LOCK_MODE: X # 锁的模式,表?如何请求锁
 LOCK_STATUS: GRANTED # 锁请求的状态; 
 LOCK_DATA: supremum pseudo-record

锁类型:

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

(1)级锁也叫锁,是对表中的某些具体的数据进加锁;

(2)表级锁也叫表锁,是对整个数据表进加锁

锁模式

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

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

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

共享锁(S锁):允许持有该锁的事务读取表中的记录,同时允许其他事务在锁定上加另个共享锁并读取被锁定的对象,但不能对其进写操作;

独占锁(X锁):允许持有该锁的事务对数据进更新或删除,同时不论其他事务对锁定进读取或修改都不允许对锁定进加锁;

# 对查询结果集中的每?数据都加共享锁
select * from account where id < 2 FOR SHARE; # MYSQL8.0(推荐)
select * from account where id < 2 LOCK IN SHARE MODE; # 8.0以及之前版本

# 对查询结果集中的每?数据都加排他锁
select * from account where id = 1 FOR UPDATE;

# 可以使?以下SQL在监视器中查看锁信息
SHOW ENGINE INNODB STATUSG

使用时的对应操作及出现的情况

1.如果事务T1持有R上的共享锁(S),那么事务T2请求R上的锁时会有如下处理:

(1)T2请求S锁会即被授予,此时T1和T2都对R持有S锁;共享锁可以同时读

(2)T2请求X锁不能即被授予,阻塞到T1释放持有的锁;

2.如果事务T1持有R上的独占锁(X),那么T2请求R上的任意类型锁都不能即被授予,事务T2必须等待事务T1释放R上的锁。

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

3.意向锁 - Intention Locks

1.InnoDB持多粒度锁,允许锁和表锁共存;

2.InnoDB使意向锁实现多粒度级别的锁,意向锁是表级别的锁,它并不是真正意义上的加锁,只是在 data_locks 中记录事务以后要对表中的哪加哪种类型的锁(共享锁或排他锁),意向锁分为两种:(将要发生的加锁操作)

(1)意向共享锁(IS):表事务打算对表中的单个设置共享锁。

(2)意向排他锁(IX):表事务打算对表中的单个设置排他锁。

3.在获取意向锁时有如下协议:

(1)在事务获得表中某的共享锁(S)之前,它必须先获得该表上的IS锁或更强的锁。

(2)在事务获得表中某的排他锁(X)之前,它必须先获得该表上的IX锁。

4.意向锁可以提加锁的性能,在真正加锁之前不需要遍历表中的是否加锁,只需要查看下表中的意向锁即可;

5.在请求锁的过程中,如果将要请求的锁与现有锁兼容,则将锁授予请求的事务,如果与现有锁冲突,则不会授予;事务将阻塞等待,直到冲突的锁被释放;意向锁与级锁的兼容性如下表:

6.除了全表锁定请求之外,意向锁不会阻任何锁请求;意向锁的主要的是表事务正在锁定某或者正在意图锁定某。

4.索引记录锁 - Record Locks

1.索引记录锁或称为精准锁,顾名思意是指索引记录上的锁,如下SQL锁住的是指定的

# 防任何其他事务插、更新或删除值为 1 的, id 为索引列

SELECT * FROM account WHERE id = 1 For UPDATE ;

  1. 索引记录锁总是锁定索引,在表没有定义索引的情况下,InnoDB创建个隐藏的聚集索引,并 使该索引进记录锁定,当使索引进查找时,锁定的只是满条件的,如图所:

5.间隙锁 - Gap Locks

(1)间隙锁锁定的是索引记录之间的间隙,或者第个索引记录之前,再或者最后个索引记录之后的间隙。如图所位置,根据不同的查询条件都可能会加间隙锁:

(2)例如有如下SQL,锁定的是ID (10, 20)之间的间隙,注意不包括10和20的,的是防其他事务将ID值为15的列插到列 account 表中(论是否已经存在要插的数据列),因为指定范围值之间的间隙被锁定了;

SELECT * FROM account WHERE id BETWEEN 10 and 20 For UPDATE ;

(3) 间隙可以跨越单个或多个索引值

(4)对于使唯索引查询到的唯,不使间隙锁,如下语句,id列有唯的索引,只对id值为100的使索引记录锁

#只使 Record Locks

SELECT * FROM account WHERE id = 100 ;

(5) 如果id没有被索引,或者是个唯的索引,以上语句将锁定对应记录前的间隙;

id=xxx没有主键或唯一索引时使用间隙锁

(6)不同事务的间隙锁可以共存,个事务的间隙锁不会阻另个事务在相同的间隙上使间隙锁;共享间隙锁和独占间隙锁之间没有区别。

(7)当事务隔离级别设置为 READ COMMITTED 时间隙锁会被禁,对于搜索和索引扫描不再使间隙锁定。

6.临键锁 - Next-Key Locks

1.Next-key 锁是索引记录锁和索引记录之前间隙上间隙锁的组合,如图所;

2.InnoDB搜索或扫描个表的索引时,执级锁策略,具体式是:在扫描过程中遇到的索引记录上设置共享锁或排他锁,因此,级锁策略实际上应的是索引记录锁。索引记录上的 next-key 锁也会影响该索引记录之前的"间隙",也就是说, next-key 锁是索引记录锁加上索引记录前的间隙锁。如果个会话对索引中的条记录R具有共享锁或排他锁,则另个会话不能在索引记录R之前的空中插新的索引记录。

3.假设索引包含值10、11、13和20,这些索引可能的 next-key 锁覆盖以下区间,其中圆括号表不包含区间端点,括号表包含端点:

(negative infinity, 10]

(10, 11]

(11, 13]

(13, 20]

(20, positive infinity)

  1. 默认情况下, REPEATABLE READ 事务隔离级别开启 next-key 锁并进搜索和索引扫描,可 以防幻象

7.插意向锁 - Insert Intention Locks

记录的是当前操作将来要获取什么样的锁,以锁信息的形式保存在data_locks表中

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

下的例演个事务在获得插记录的排他锁之前,使了插意向锁:

8.AUTO-INC Locks

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

9.死锁 死锁一旦发生,程序就会阻塞,是很严重的问题,必须要解决

9.1.例

由于每个事务都持有另个事务所需的锁,导致事务法继续进的情况称为死锁。以下图为例,

两个事务都不会主动释放持有的锁,并且都在等待对持有的资源变得可

9.2.下通过个例演下死锁的发过程,其中涉及两个客端A和B,并通过启全局变量

innodb_print_all_deadlocks 来查看死锁的信息,同时死锁信息也会保存到错误志中

9.3.先打开个客端A,并执以下操作

9.4.InnoDB的j监视器包含了关于死锁和事务的相关信息,可以通过 SHOW ENGINE INNODB

STATUS; 查看 LATEST DETECTED DEADLOCK 节点的内容

9.5.错误志中也记录了死锁相关的信息

# 查看错误?志路径
mysql> SELECT @@log_error;
+--------------------------+
| @@log_error |
+--------------------------+
| /var/log/mysql/error.log |
+--------------------------+
1 row in set (0.00 sec)

9.6.死锁产的条件

互斥访问:如果线程1获取到了锁A,那么线程2就不能同时得到锁A

不可抢占:获取到锁的线程,只能主动释放锁,别的线程不能从他的中抢占锁

保持与请求:线程1已经获得了锁A,还要在这个基础上再去获了锁B

循环等待:线程1等待线程2释放锁,线程2也等待线程1释放锁,死锁发时系统中定有由两个或两个以上的线程组成的条环路,该环路中的每个线程都在等待着下个进程释放锁

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

9.7.InnoDB对死锁的检测

1.InnoDB在运时会对死锁进检测,当死锁检测启时(默认),InnoDB动检测事务死锁,并回滚个或多个事务来打破死锁。InnoDB尝试选择事务进回滚,其中事务的由插、更新或删除的数决定。

2.如果系统变量 innodb_table_locks = 1 (默认) 和 autocommit = 0 ,InnoDB可以检测到表级锁和级锁级别发的死锁;否则,法检测到由 lock TABLES 语句设置的表锁或由

InnoDB存储引擎设置的锁,对于法检测到的死锁,可以通过设置系统变量

innodb_lock_wait_timeout 的值来指定锁的超时时间来解决死锁问题

3.当超过 200 个事务等待锁资源或等待的锁个数超过 1,000,000 个时也会被视为死锁,并尝试将等待列表的事务回滚。

4.在并发系统中,多个线程等待相同的锁时,死锁检测可能会导致性能降性变慢,此时禁死锁检测并依赖 innodb_lock_wait_timeout 设置进事务回滚可能性能更。可以通过设置系统变量 innodb_deadlock_detect[={OFF|ON}] 禁死锁检测。

9.8如何避免死锁

1.MySQL是个多线程程序,死锁的情况概率会发,但他并不可怕,除频繁出现,导致法运某些事务

2.InnoDB使动级锁,即使在只插或删除单的事务中,也可能出现死锁。这是因为插或删除并不是真正的"原"操作,同时会对索引记录进修改并设置锁

3.使以下技术来处理死锁并降低发死锁的可能性:

(1)使事务不是使 LOCK TABLES 语句动加锁,并使

innodb_lock_wait_timeout 变量设置锁的超时时间,保证任何情况下锁都可以动释放

(2)经常使 SHOW ENGINE INNODB STATUS 命令来确定最近次死锁的原因。这可以帮助我们修改应程序以避免死锁

(3)如果出现频繁的死锁警告,可以通过启 innodb_print_all_deadlocks 变量来收集调

试信息。对于死锁的信息,都记录在MySQL错误志中,调试完成后记得禁此选项

(4)如果事务由于死锁失败,记得重新发起事务,再执次

(5)尽量避免事务,保持事务粒度且持续时间短,这样事务之间就不容易发冲突,从降低发死锁的概率

(6)修改完成后即提交事务也可以降低死锁发的概率。特别注意的是,不要在个交互式会话中时间打开个未提交的事务

(7)当事务中要修改多个表或同表中的不同时,每次都要以致的顺序执这些操作,使事务中的修改操作形成定义良好的队列,可以避免死锁。不是在不同的位置编写多个类似的INSERT、UPDATE和DELETE语句。我们写的程序其实就是把系列操作组织成个法或函数

(8)向表中添加适当的索引,以便查询时扫描更少的索引并设置更少的锁,可以使EXPLAIN

SELECT来确定哪些索引于当前的查询

(9)使表级锁防对表进并发更新,可以避免死锁,但代价是系统的并发性降低

(10)如果在查询时加锁,如 SELECT…FOR UPDATE 或 SELECT…FOR SHARE ,尝试使较低的隔离级别,如 READ COMMITTED

隔离级别

1.查看并设置隔离级别

1.事务的隔离级别分为全局作域和会话作域,查看不同作域事务的隔离级别,可以使以下的式:

# 全局作?域
SELECT @@GLOBAL.transaction_isolation;

# 会话作?域
SELECT @@SESSION.transaction_isolation;

# 可以看到默认的事务隔离级别是REPEATABLE-READ(可重复读)
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ | # 默认是可重复读
+---------------------------------+
1 row in set (0.00 sec)

2.设置事务的隔离级别和访问模式,可以使以下语法:

# 通过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;

3.通过选项件指定事务的隔离级别,以便MySQL启动的时候读取并设置

[mysqld]
transaction-isolation = REPEATABLE-READ # 隔离级别为可重复读
transaction-read-only = OFF # 关闭只读意味着访问模式为读写

4.通过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';

5.设置事务隔离级别的语句不能在已开启的事务中执,否则将会报错:

# 开启事务
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

# 修改事务的隔离级别,将会报错
mysql> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
ERROR 1568 (25001): Transaction characteristics can't be changed while a 
transaction is in progress

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

2.1实现式

(1)读取时:不加任何锁,直接读取版本链中的最新版本,也就是当前读,可能会出现脏读,不可重复读、幻读问题;

(2)更新时:加共享锁(S锁),事务结束时释放,在数据修改完成之前,其他事务不能修改当前数据,但可以被其他事务读取。

2.2.存在问题

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

2.3问题重现

(1)在个客端A中先设置全局事务隔离级别为 READ UNCOMMITTED 读未提交:

# 设置隔离级别为READ UNCOMMITTED读未提交
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

# 查看设置是否?效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED | # 已?效
+--------------------------------+
1 row in set (0.00 sec)

(2)打开另个客端B并确认隔离级别

# 查看设置是否?效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED | # 已?效
+--------------------------------+
1 row in set (0.00 sec)

(3)在不同的客端中执事务

#会话A开启事务A
# 选择数据库
use test_db;
# 开启事务
START TRANSACTION;
# 写??条新数据
insert into account values
(null, '王五', 2000);
# 查询结果,
select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
| 5 | 王五 | 2000.00 |
+----+------+---------+
3 rows in set (0.00 sec)
# 新记录已写?,但是此时事务A并没有提交

#会话B开启事务B
# 选择数据库
use test_db;
# 开启事务
START TRANSACTION;
# 查询结果
select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
| 5 | 王五 | 2000.00 |
+----+------+---------+
3 rows in set (0.00 sec)
# 发现查到了事务A没有提交的数据

#会话A
# 回滚
rollback;
# 查询结果,数据正常回滚
select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)

#会话B
# 再次查询,刚才“王五”这条记录不存在了
select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)

2.4由于 READ UNCOMMITTED 读未提交会出现"脏读"现象,在正常的业务中出现这种问题会产常危重后果,所以正常情况下应该避免使 READ UNCOMMITTED 读未提交这种的隔离级别

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

3.1实现式

读取时:不加锁,但使快照读,即按照 MVCC 机制读取符合 ReadView 要求的版本数据每次查询都会构造个新的 ReadView ,可以解决脏读,但法解决不可重复读和幻读问题;

更新时:加独占锁(X),事务结束时释放,数据在修改完毕之前,其他事务不能修改也不能读取这数据。

3.2存在问题

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

3.3问题重现

(1)在个客端A中先设置全局事务隔离级别为 READ COMMITTED 读未提交:

# 设置隔离级别为READ COMMITTED读未提交
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 查看设置是否?效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-COMMITTED | # 已?效
+--------------------------------+
1 row in set (0.00 sec)

(2)打开另个客端B并确认隔离级别

# 查看设置是否?效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-COMMITTED | # 已?效
+--------------------------------+
1 row in set (0.00 sec)

(3)在不同的客端中执事务

#会话B开始的事务B中
# 选择数据库
use test_db;
# 写??条新测试数据
insert into account values (null, '王五', 2000);
Query OK, 1 row affected (0.00 sec)
# 开启事务
START TRANSACTION; 
# 查询王五的记录,余额是2000
select * from account where name='王五';
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 6 | 王五 | 2000.00 |
+----+------+---------+
1 row in set (0.01 sec)

#会话A开始的事务A中
# 选择数据库
use test_db;
# 开启事务
START TRANSACTION;
# 修改王五的余额为1000
update account set balance=1000  where name = '王五';
Query OK, 1 row affected (0.00  sec)
# 提交事务
commit;

#事务B
# 此时事务并没有提交或回滚
# 再次查询王五的记录发现余额变成了 1000
# 与上?个查询结果不?致
select * from account where name='王五';
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 6 | 王五 | 1000.00 | # 出现
问题
+----+------+---------+
1 row in set (0.00 sec)

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

4.1实现式

读取时:不加锁,也使快照读,按照MVCC机制读取符合ReadView要求的版本数据,但论事务中有次查询,只会在次查询时成个ReadView,可以解决脏读、不可重复读,配合Next-Key锁可以解决部分幻读问题;

更新时:加Next-Key锁,事务结束时释放,在个范围内的数据修改完成之前,其他事务不能对这个范围内的数据进修改、插和删除操作,同时也不能被查询。

4.2存在问题

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

我们知道 Next-Key 锁,锁住的是当前索引记录以及索引记录前的间隙,那么在不加 Next-

Key 锁的情况下,也就是只对当前修改加了独占锁(X),这时记录前的间隙没有被锁定,其他的事务就可以向这个间隙中插记录,就会导致个问题:事务A查询了个区间的记录得到结果集A,事 务B向这个区间的间隙中写了条记录,事务A再查询这个区间的结果集时会查到事务B新写的记录得到结果集B,两次查询的结果集不致,这个现象就是"幻读"。

4.3问题重现

(1)由于 REPEATABLE READ 隔离级别默认使了 Next-Key 锁,为了重现幻读问量,我们把隔离级回退到更新时只加了排他锁的 READ COMMITTED

# 设置隔离级别为READ COMMITTED读未提交
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 查看设置是否?效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-COMMITTED | # 已?效
+--------------------------------+
1 row in set (0.00 sec)

(2)在不同的客端中执事务

#会话B开启事务B
# 选择数据库
use test_db;
# 开启事务
START TRANSACTION;
# 更新五五的余额,使该记录加排他锁
update account set balance=2000 where name='王五';
Query OK, 1 row affected (0.01 sec)
# 查询结果集,更新成功
select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
| 6 | 王五 | 2000.00 | # 更新成功
+----+------+---------+
3 rows in set (0.00 sec)

#会话A开启事务A
# 选择数据库
use test_db;
# 开启事务
START TRANSACTION;
# 在李“四与”和“王五”之间的间隙写??条数据“赵六”
insert into account values (3, '赵六', 5000);
# 查询结果集,写?成功
select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
| 3 | 赵六 | 5000.00 |
| 6 | 王五 | 1000.00 |
+----+------+---------+
4 rows in set (0.01 sec)
# 提交事务
commit;

#会话B开启事务B
# 查询结果集,发现?上?次查询的结果集
# 多出了其他事务写?的数据
select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1100.00 |
| 3 | 赵六 | 5000.00 | # 幻像?
| 6 | 王五 | 2000.00 |
+----+------+---------+
4 rows in set (0.01 sec)
# 提交事务
commit;

5.SERIALIZABLE - 串化

5.1实现式

读取时:加共享表锁,读取版本链中的最新版本,事务结束时释放;

更新时:加独占表锁,事务结束时释放,完全串操作,可以解决所有事务问题

5.2存在问题

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

示例及小总结

多版本控制(MVCC)

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

事务的隔离性是通过锁和MVCC共同实现的

1.实现原理

1.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中的第条志进关联,这样就构成条完整的数据版本链,如下图所

(2)每条被修改的记录都会有条版本链,体现了这条记录的所有变更,当有事务对这条数据进修改时,将修改后的数据链接到版本链接的头部,如下图中 UNDO3

1.2ReadView

1.构造好 ReadView 之后需要根据定的查询规则找到唯的可版本,这个查找规则较简单,以下图的版本链为例,在 m_creator_trx_id =201 的事务执 select 时,会构造个

ReadView 同时对相应的变量赋值 :

(1)m_ids :活跃事务集合为 [90, 100, 200],不包含当前新创建的事务

(2)m_up_limit_id :活跃事务最事务Id = 90

(3)m_low_limit_id :预分配事务ID = 202 ,最事务Id = 预分配事务ID - 1 = 201

(4)m_creator_trx_id :当前创建 ReadView 的事务Id = 201

2.接下来找到版本链头,从链头开始遍历所有版本,根据四步查找规则,判断每个版本:

第步:判断该版本是否为当前事务创建,若 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时该版本已经提交,可以直接访问,如果不是则遍历并判断下个版本

3.这样从版本链头遍历判断到版本链尾,找到个符合要求的版本即可,就可以实现查询到的结果都是已经提交事务的数据,解决了脏读问题

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

先幻读法通过MVCC单独解决


网站公告

今日签到

点亮在社区的每一天
去签到