MySQL事务和锁

记录一下 MySQL事务的实现机制和锁,以及不同隔离级别下的测试笔记。

事务隔离级别

数据准备

-- 创建表
CREATE TABLE t(
  a INT PRIMARY KEY,
  b INT,
  c INT
);
-- 插入数据
INSERT INTO t VALUES (1,1,1);
INSERT INTO t VALUES (2,2,2);
INSERT INTO t VALUES (3,3,3);
INSERT INTO t VALUES (4,4,4);
INSERT INTO t VALUES (5,5,5);
INSERT INTO t VALUES (6,6,6);

READ UNCOMMITTED(读未提交)

-- 查询当前事务隔离级别 默认:REPEATABLE-READ
SELECT @@tx_isolation;

-- 设置 读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 事务A
begin;
update t set c = 99 where a = 6;

-- 事务B
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
select * from t where a = 6;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 6 |    6 |   99 |
+---+------+------+

-- 事务B可以查询事务A未提交的数据
-- 一旦事务A回滚,事务B读到的就是 脏数据

隔离级别最弱的,会出现“脏读”,一般不会使用。违反了 ACID 原则。

READ COMMITTED(读已提交)

-- 事务A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
begin;
update t set c = 88 where a = 6;

-- 事务B
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
begin;
select * from t where a = 6;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 6 |    6 |   99 |
+---+------+------+

-- 事务A
commit;

-- 事务B
select * from t where a = 6;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 6 |    6 |   88 |
+---+------+------+

不同的事务之间,可以读取其他事务已经提交的数据。

但是会有一个问题:事务 B 多次读取,可能读取的数据不一致。称作:不可重复读

REPEATABLE READ(可重复读)

-- 事务A
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
begin;

-- 事务B
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
begin;

-- 事务A
update t set c = 77 where a = 6;
commit;

-- 事务B
select * from t where a = 6;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 6 |    6 |   88 |
+---+------+------+
-- 即使事务A已提交,事务B查询的还是旧数据,解决了“**不可重复读**”的问题
-- 事务B结束后,才能查看事务A提交的数据。

REPEATABLE READ 解决了 READ COMMITTED 中“不可重复读”的问题。

在对事务隔离级别的定义中,REPEATABLE READ 会有“幻行”的问题。InnoDB 实现了 MVCC(多半版本并发控制),解决了“幻行”的问题。

注意

事实上,事务 A 和事务 B 并非完全隔离,InnoDB 通过 ReadView 和事务 ID 来判断哪些数据对于当前事务是可见的,哪些是不可见的。

例如:

  1. 事务 A 开启,但是没有查询(没创建 ReadView)。
  2. 事务 B 开启,修改数据,提交事务。
  3. 事务 A 可以查询到事务 B 修改的数据。

事务 A 查询、创建 ReadView 时,事务 B 已经提交(不活跃),事务 A 会将事务 B 修改的数据也读出来。具体的判断依据会在后面记录。

SERIALIZABLE(可串行化)

-- 事务A
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
begin;

-- 事务B
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
begin;

-- 事务A
update t set c = 66 where a = 6;

-- 事务B
select * from t where a = 6;
-- SELECT操作会被阻塞,事务A会给查询出来的数据行加 排它锁。
-- 事务B不可读不可写,直到事务A结束,释放行锁。

在 SERIALIZABLE 隔离级别下,事务会给查询到的数据行加共享锁,其他事务可读不可写。给写入的数据加排它锁,其他事务不可读,不可写。

SERIALIZABLE 是最强的隔离级别,也是并发性能最差的隔离级别,一般也很少使用。InnoDB 默认的隔离级别(REPEATABLE READ)通过实现 MVCC 来最大程度的给减少给数据行加锁,在遵循 ACID 的同时最大程度的来支持高并发

保存点

对于一个事务,要么全部执行,要么全部回滚。

如果事务涉及到很多操作,但是在最后一步操作失误,如果全部回滚,意味着所有的操作都要重来一遍。

保存点(SAVEPOINT) 提供了一个“存档”的功能,可以让事务回滚到指定的位置。通过“版本链”的方式实现,后面会讲到。

-- 事务A
begin;
UPDATE t SET c = 999 WHERE a = 6;

-- 设置保存点
SAVEPOINT s;

UPDATE t SET c = 999 WHERE a = 5;

-- 回滚到保存点
ROLLBACK TO s;

SELECT * FROM t;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 1 |    1 |    1 |
| 2 |    2 |    2 |
| 3 |    3 |    3 |
| 4 |    4 |    4 |
| 5 |    5 |    5 |
| 6 |    6 |  999 |
+---+------+------+

-- 删除保存点
RELEASE SAVEPOINT s1;

类似于“游戏存档”的功能。

MVCC 实现机制

在 InnoDB 中,数据以“行”的格式保存在磁盘中,MySQL 支持多种行格式。具体的“行格式”可以查看笔者以前的博客。

InnoDB 会在数据行中保存两个隐藏列:

  • TRX_ID:事务 ID,每次数据被修改都会保存相应事务的 ID。
  • ROLL_PTR:回滚指针,数据行被修改时,会生成一个版本链,通过回滚指针连接,用于事务回滚时恢复数据。

版本链

数据行被修改时,会生成一条“版本链”。

不同的数据行版本之间,通过“回滚指针”连接,事务回滚时,通过 回滚指针 来恢复数据。

MySQL事务和锁
在这里插入图片描述

ReadView

InnoDB 中,通过 ReadView 来判断:不同的数据行版本中,哪些版本对于当前事务是可见的,哪些是不可见的。

在 READ UNCOMMITTED 隔离级别下,事务读取的总是 最新的版本,所以不需要 ReadView。在 SERIALIZABLE 隔离级别下,通过对数据行加锁来限制读写,也不需要 ReadView。

只有 READ COMMITTED 和 REPEATABLE READ 需要 ReadView,二者创建的时机不同。

  • READ COMMITTED 可以访问其他事务已提交的数据,所以每次查询都会创建新的 ReadView。

  • REPEATABLE READ 第一次查询就创建 ReadView,之后的查询都会直接使用已有的 ReadView,可以保证即使其他事务已提交,当前事务仍然认为它是活跃的。

核心内容

ReadView 中主要包含的核心内容:

  • m_ids:创建 ReadView 时活跃的事务 ID 集合。
  • min_trx_id:创建 ReadView 时,活跃的最小事务 ID。
  • max_trx_id:创建 ReadView 时,下一个应该分配的事务 ID(递增)。
  • creator_trx_id:创建 ReadView 的当前事务 ID。

max_trx_id 不代表 m_ids 中的最大值,因为有的事务可能在创建 ReadView 之前就已经提交,也就不会出现在 m_ids 中。

判断依据

  • 访问的数据行版本 TRX_ID 等于 creator_trx_id,代表当前事务在访问自己修改的数据,可以访问。
  • 访问的数据行版本 TRX_ID 小于 min_trx_id,说明当前事务开启前,该版本的事务已经提交,可以访问。
  • 访问的数据行版本 TRX_ID 大于 max_trx_id,说明该版本的事务在 ReadView 创建之后开启,不可访问。
  • 访问的数据行版本 TRX_ID 在 min_trx_id 和 max_trx_id 之间,则要判断是否在 m_ids 中,如果在:说明事务活跃,不可访问。如果不在:说明事务已经提交,可以访问。

1、看不到 ReadView 创建以后启动的事务 2、看不到 ReadView 创建时活跃的事务

INNODB_TRX

活跃的事务信息保存在:INFORMATION_SCHEMA.INNODB_TRX 表中,采用 MEMORY 存储引擎,速度非常快。

事务 ID 是递增的,且 读和写的事务 ID 是分开记录的。

查看活跃的事务 ID

BEGIN;
SELECT * FROM t;
SELECT trx_id FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();
COMMIT;
-- 读 281479607707384

BEGIN;
UPDATE t SET c = 99 WHERE a = 6;
SELECT trx_id FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();
COMMIT;
-- 写 8206301

锁的类型

  • 读锁:共享锁、S 锁
  • 写锁:排他锁,X 锁

只有“读读”锁不会冲突,对于一个资源,可以加多个读锁,其他锁加多个均会冲突、阻塞。

SELECT 操作不会加任何锁,InnoDB 利用 MVCC 来支持高并发,读都可以读,但不能同时写。

手动加锁

读锁
-- 事务A 加读锁
begin;
select * from t where a = 1 lock in share mode;

-- 事务B
-- 可以查询
select * from t where a = 1;
+---+------+------+
| a | b    | c    |
+---+------+------+
| 1 |    1 |    1 |
+---+------+------+
-- 可以再加读锁
select * from t where a = 1 lock in share mode;
-- 再加写锁 会阻塞
select * from t where a = 1 for update;
写锁
-- 事务A 加写锁
begin;
select * from t where a = 1 lock in share mode;

-- 事务B
-- 再加读锁/写锁 均会阻塞
-- SELECT查询是可以,查询不会加任何锁

锁的范围

表锁

-- 读锁
lock tables t read;
-- 写锁
lock tables t write;

行锁

  • LOCK_REC_NOT_GAP 行记录锁,只锁住 数据行。

  • LOCK_GAP 间隙锁,锁定一个范围,但不包括记录本身。

  • LOCK_ORDINARY 间隙锁,锁定一个范围,且包括记录本身。

对于不同的事务隔离级别,InnoDB 锁住的数据范围不同。

READ COMMITTED 锁范围

  • 使用主键 只对主键对应的数据行加锁。

  • 使用唯一索引 只对唯一索引和对应数据行加锁。

  • 使用普通索引 对满足条件的索引和对应数据行加锁。

  • 不使用索引 对满足条件的索引和对应数据行加锁。

REPEATABLE READ 锁范围

  • 使用主键 只对主键对应的数据行加锁。

  • 使用唯一索引 只对唯一索引和对应数据行加锁。

  • 使用普通索引 对符合条件的索引、数据行以及间隙加锁。

  • 没有使用索引 对表中所有的记录和间隙加锁。

意向锁

  • IS 锁:意向共享锁。
  • IX 锁,意向排他锁。

当事务在数据行中加锁时,会先在表中加对应的意向锁。

目的:为了快速判断表中的数据行是否有加锁,没有意向锁标记,需要遍历每一行去判断。

死锁

多个事务在互相占用对方锁住的资源,都在等待对方释放锁,造成死循环。

-- 事务A
begin;
select * from t where a = 1 for update;

-- 事务B
begin;
select * from t where a = 2 for update;

-- 事务A
select * from t where a = 2 for update;-- 阻塞

-- 事务B
select * from t where a = 1 for update;-- 死锁
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

MySQL 有死锁检测机制,一旦发现死锁会自动回滚占用资源相对较少的事务来释放锁。

使用合理的索引,可以减少死锁的概率。


原文始发于微信公众号(程序员小潘):MySQL事务和锁

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/28638.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!