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:回滚指针,数据行被修改时,会生成一个版本链,通过回滚指针连接,用于事务回滚时恢复数据。

版本链

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

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

在这里插入图片描述

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有死锁检测机制,一旦发现死锁会自动回滚占用资源相对较少的事务来释放锁。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值