MySQL的事务和锁机制

首先,我们从事务的四大特性(ACID)出发:

  • 原子性:事务是最小的执行单位,不允许分割
  • 一致性:执行事务前后,数据库状态一致
  • 隔离性:事务之间是相互独立的,一个事务不能被其它事务干扰
  • 持久性:事务提交之后对数据库的修改是永久的,即使发生崩溃也能恢复

事务具有这样的特性,但是当多个事务并发执行时,结果可能与我们预期的不同,可能会出现以下几类问题:

  • 丢失修改:事务A的操作被事务B覆盖
  • 脏读:事务A读取了事务B还未提交的数据,可能根据这个数据做出错误的操作
  • 不可重复读:一个事务内先后两次查询到的数据不一致
  • 幻读:一个事务内先后同样的查询得到的数据多了或者少了

我们都知道MySQL提供了四种隔离级别,不同的隔离级别可以解决相应的问题。

  • 读未提交:允许读取还没有提交的数据,可能导致脏读、幻读和不可重复读
  • 读已提交:允许读取已经提交的数据,解决脏读,可能导致幻读和不可重复读
  • 可重复读:安全,解决脏读、不可重复读、幻读
  • 串行化:事务串行执行,安全

那么,MySQL究竟是如何实现这些隔离级别效果(即解决并发事务可能出现的各种效果)的呢?我们从以下几方面来分析,这些机制并不是相互独立的。

1、锁

MySQL提供了两种锁:

  • 共享锁(读锁):允许事务读取一行数据,兼容读锁但不兼容写锁
  • 排他锁(写锁):允许事务删除或更新一行数据,不兼容读锁和写锁,其它锁均需要等待释放后再获取

通过加锁可以解决一些并发问题,MySQL中根据操作是否加锁有快照读和当前读两个概念:

  • 快照读:读取记录数据的可见版本,不加锁,普通的SELECT语句都是快照读
  • 当前读:读取记录数据的最新版本,显式加锁(FOR UPDATE、LOCK IN SHARE MODE)的是当前读,此外,更新操作(INSERT、UPDATE、DELETE)也是当前读,会加锁

所以,当一个事务在更新一行数据时,其它事务就不能对这行数据进行操作,只有等当前事务提交并释放锁之后其它事务才能读取或修改,也就避免了数据库层面的丢失修改问题。基于此机制,即使是读未提交级别也能避免丢失修改问题。通过显式加锁(FOR UPDATE、LOCK IN SHARE MODE)也可以避免脏读问题。

2、MVCC

MVCC即多版本并发控制,每个数据都有多个版本,MySQL通过MVCC来实现读已提交和可重复读隔离级别,可以避免脏读和不可重复读问题。

首先说说MVCC的流程:当一个事务读一个数据时,会比较数据的版本,判断该数据是否对自己可见,如果不可见则通过read view和undo log回滚版本,直到找到对自己可见的数据版本。

要理解MVCC的具体实现,需要理解涉及的一些底层原理:

  • 事务版本号:每个事务都有自己的ID,用于判断先后顺序

  • undo log:回滚日志,一种逻辑日志

  • 隐式字段:每个数据记录都有隐式字段

    • trx_id:记录操作这个数据的事务ID
    • roll_pointer:指向回滚段的undo日志
    • row_id(没有主键和非NULL唯一键时才有,是单调递增的行ID)
  • read_view:一致性视图,基于整库的快照。

    • m_ids:系统中活跃的事务(即未提交的事务)ID,是一个list
    • min_limit_id: m_ids中最小的ID
    • max_limit_id: m_ids中最大ID+1
    • create_trx_id:创建当前read view的事务ID,即当前事务的ID

如何判断数据是否可见?

通过比较trx_id与min_limit_id、max_limit_id来判断,具体:

①trx_id < min_limit_id:说明操作这个版本的事务已经提交,可见

②trx_id >= max_limit_id:说明操作这个版本的事务在生成read view之后才开启,不可见

③min_limit_id < trx_id <max_limit_id :

​ 若trx_id不在m_ids中,说明操作这个版本的事务已经提交,可见;

​ 若trx_id在m_ids且不等于create_trx_id:说明操作这个版本的事务还未提交,且不是当前事务,不可见

你可能要问了,为什么MVCC不仅能实现可重复读,还能实现已提交读呢?两种隔离级别下MVCC的实现有什么区别?

这是因为MVCC在RR(可重复读)和RC(读已提交)下生成read view的时机不同:

  • RR:事务开始时创建read view,之后事务里的查询都用这个一致性视图
  • RC:每一个语句执行前都创建一个read view

注意一点,可重复读隔离级别下,普通的SELECT操作是快照读,但如果要更新数据,那就不能根据数据的历史版本来了,会造成丢失修改,所以不可重复读中两次读到的数据不一样指的是在自己没有操作的情况下,因为别的事务修改导致的不一样,如果自己在事务中进行了UPDATE,是当前读,根据最新的版本进行,之后再读到的就是自己修改后的新数据了。

3、Next-Key Lock

InnoDB中提供了三种锁算法:

  • Record Lock :锁单个记录
  • Gap Lock(间隙锁):锁一个范围,但不包括记录本身
  • Next-Key Lock: Record Lock+Gap Lock

Next-Key Lock可以解决幻读问题。

通过一个例子来说明“什么是幻读”这个问题:

首先创建一个表并插入数据,结果如下:

CREATE TABLE `test`(
		id INT,
		a INT,
		PRIMARY KEY(id),
		KEY a(a)
);

INSERT INTO `test` VALUES(1,3),(2,5),(3,7);

SELECT * FROM test;

+----+---+
| id | a |
+----+---+
|  1 | 3 |
|  2 | 5 |
|  3 | 7 |
+----+---+

开启两个会话,并将隔离级别设置为读已提交,该隔离级别下没有间隙锁

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

首先从会话1中开启事务,查询a=3的记录

BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;
+----+---+
| id | a |
+----+---+
|  1 | 3 |
+----+---+
1 row in set (0.02 sec)

这时从会话B中插入一条数据:

mysql> INSERT INTO test VALUES(4,3);
Query OK, 1 row affected (0.01 sec)

这时会话1再执行相同的查询语句:

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;
+----+---+
| id | a |
+----+---+
|  1 | 3 |
|  4 | 3 |
+----+---+
2 rows in set (0.02 sec)

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)

从会话1的角度来看整个过程就是这样的:一个事务内没有进行插入,先后两次执行同样的查询,但却多出来一条数据,就像发生了幻觉!这便是幻读。

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;
+----+---+
| id | a |
+----+---+
|  1 | 3 |
+----+---+
1 row in set (0.02 sec)

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;
+----+---+
| id | a |
+----+---+
|  1 | 3 |
|  4 | 3 |
+----+---+
2 rows in set (0.02 sec)

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)

行锁锁住的只是a=3记录行,而新插入记录是在记录之间的“间隙

接下来将隔离级别设置为可重复读,并将表复原,再执行上述过程:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

这时你会发现会话B的插入语句执行后就没有反应了(被阻塞了),会话A中两次查询结果也是一致的

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;
+----+---+
| id | a |
+----+---+
|  1 | 3 |
+----+---+
1 row in set (0.02 sec)

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;
+----+---+
| id | a |
+----+---+
|  1 | 3 |
+----+---+
1 row in set (0.03 sec)

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)

直到会话A中的事务提交之后,会话B的语句才能执行,耗时38.05 sec就是由于阻塞。

mysql> INSERT INTO test VALUES(4,3);
Query OK, 1 row affected (38.05 sec)

这时会话A再查询,才能看到会话B插入的数据

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;
+----+---+
| id | a |
+----+---+
|  1 | 3 |
|  4 | 3 |
+----+---+
2 rows in set (0.03 sec)

下面我们讨论具体实现。

以上表的数据为例:

+----+---+
| id | a |
+----+---+
|  1 | 3 |
|  2 | 5 |
|  3 | 7 |
+----+---+

当执行这条更新语句时,Next-Key Lock会给记录a=3这一行加锁,还会给间隙(-∞,3)U(3,5)加锁

mysql> SELECT * FROM test WHERE a = 3 FOR UPDATE;

进行测试:

INSERT INTO test VALUES(4,2);		//阻塞
INSERT INTO test VALUES(5,3);		//阻塞
INSERT INTO test VALUES(6,4);		//阻塞
INSERT INTO test VALUES(7,5);		//正常执行
INSERT INTO test VALUES(8,-1);		//阻塞

所以,Next-Key Lock就是幻读的解决办法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MySQL事务锁机制数据库管理系统中重要的概念。事务是一组数据库操作(例如插入、更新、删除等)的执行单元,要么全部成功执行,要么全部回滚。锁机制用于管理并发访问数据库时的数据一致性和并发控制。 在MySQL中,事务由以下四个特性组成,通常简称为ACID: 1. 原子性(Atomicity):事务中的操作要么全部完成,要么全部回滚,不存在部分完成的情况。 2. 一致性(Consistency):事务开始和结束时,数据库的状态必须是一致的。即事务执行前后,数据库中的数据必须满足预定义的完整性约束。 3. 隔离性(Isolation):并发执行的事务之间相互隔离,一个事务的执行不应该受其他事务的影响。 4. 持久性(Durability):一旦事务提交,其结果应该永久保存在数据库中,即使发生系统故障也不会丢失。 MySQL中的锁机制用于控制对数据的并发访问。主要有两种类型的:共享(Shared Lock)和排他(Exclusive Lock)。共享允许多个事务同时读取同一数据,但不允许并发写操作。排他则只允许一个事务独占地进行读写操作。 MySQL提供了多种级别的,包括表级、行级和页面。表级是最粗粒度的,对整个表进行加;行级是最细粒度的,只对操作的行进行加;页面介于表级和行级之间,对一定范围的行进行加。 通过合理使用事务锁机制,可以确保数据库的数据一致性和并发控制,避免脏读、不可重复读和幻读等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值