MySQL事务和锁

目录

一、引言:为什么需要事务

二、事务的基本概念

2.1 事务是什么

2.2 事务的ACID特性

三、事务的隔离级别

3.1  读未提交(Read Uncommitted)

3.2  读已提交(Read Committed)

3.3 可重复读(Repeatable Read)

3.4 串行化(Serializable)

3.5 不同隔离级别总结

四、MVCC

4.1 什么是MVCC

4.2 MVCC的核心结构

4.3 MVCC如何实现读写互不阻塞

4.4 不同隔离级别下的MVCC

4.5 MVCC与幻读

4.6 版本链清理

五、常见锁的分类

5.1 共享锁

5.2 排他锁(独占锁)

5.3 意向锁

5.4 间隙锁

六、 死锁

6.1 什么是死锁

6.2 死锁产生的条件

6.3 死锁检测

6.4 死锁避免

七、结语


一、引言:为什么需要事务

事务是InnoDB存储引擎才支持的,本文的默认存储引擎就是InnoDB。

假设,有两个银行账户,账户A和账户B。账户A向账户B转100块钱。

第一步:从账户A的余额中减去100。

第二步:给账户B的余额加上100。

如果没有事务,可能会发生以下问题:

  1. 原子性问题:如果第一步执行成功,在第二步执行前,系统突发故障(比如数据库崩溃),那么账户A的钱已经扣了,但是账户B的钱没有增加,相当于这100块钱就消失了。事务可以保证,这两个步骤要么全部执行成功,要么全部执行失败。也就是说,如果第二步执行失败,那么将会进行回滚操作,账户A的钱不会减少。
  2. 一致性问题:假设账户A和账户B的余额总和在转账前是2000元,转账后应该还是2000元。如果没有事务,在第一步和第二步之间,其他操作可能看到不一致的状态(比如账户A已经扣款,账户B还未增加,总和是1900元)。事务通过隔离性来保证一致性,确保中间状态不会被其他操作看到。
  3. 隔离性问题:如果同时有另一个转账操作也在进行,比如从账户B转账50元到账户A。如果没有事务的隔离,两个转账操作可能会相互干扰,导致最终余额不正确。事务的隔离性可以防止这种并发问题。
  4. 持久性问题:一旦转账成功,账户A和账户B的余额变化应该是永久性的,即使系统故障也不会丢失。事务的持久性保证一旦提交,修改就会永久保存到数据库中。

所以,事务就是为了保证这一系列操作能够安全、可靠、一致地执行。

二、事务的基本概念

2.1 事务是什么

事务本质上就是包装了一组SQL语句,这组操作要么全部执行成功,要么不做。

只要这组SQL语句中有一句执行失败,那么将会进行回滚,前面已经成功执行的操作也会被撤销。

2.2 事务的ACID特性

原子性(Atomicity):事务中的操作要么全部执行成功,要么全部回滚,通过 undo log(回滚日志) 实现。

执行一个UPDATE操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

在执行修改操作之前,会先将旧数据写入Undolog日志(回滚日志),其次才是对数据进行修改。如果事务执行成功,那么Undolog将被标记为可清理;如果执行失败,那么事务将会进行回滚,将数据恢复成旧值,就像update未曾发生过。

上图是Undolog日志,它是二进制文件,所以看不懂很正常。

顺便提个问题:为什么Undolog文件是二进制文件,而不是文本文件?

因为存储同一份数据,采用二进制存储往往要比直接进行文本储存所需要的空间更少,即文件大小更小。对于Undolog这样的文件,如果业务比较大,那么Undolog日志是要被频繁写入的,为了避免文件膨胀太快,节省一些磁盘空间,所以采用了二进制存储而不是直接进行文本存储。这么做的代价就是牺牲了文件的可读性。

一致性(Consistency):事务执行前后,数据库数据始终保持合法状态(如字段 “年龄” 不会因事务执行出现负数)。

一致性是结果,不是单一的机制。它是靠其他三个特性(原子性、隔离性、持久性)来共同保障的。

隔离性(Isolation):多个并发事务之间相互隔离,避免数据干扰。

隔离性是通过锁和MVCC来实现的,后面我们细说。

持久性(Durability):事务提交后,数据修改会永久保存,即使数据库崩溃也不会丢失,通过 redo log(重做日志) 实现(事务提交时先写 redo log,再刷盘,避免 “掉电丢失”)。

执行一个UPDATE操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

还是以这个update操作为例。

开启事务---->对内存中的数据进行修改--->将所有修改写入redo log buffer--->将redo log buffer进行刷盘--->提交事务--->稍后,后台线程慢慢将被修改的数据页(脏页)刷到磁盘。

假设在对内存中的数据进行修改后,因为外星人袭击地球,导致服务器断电。下面我们来分析看会不会导致数据丢失。

因为内存是带电存储的,所以毫无疑问,内存中的所有数据将会丢失,包括我们刚进行修改的数据页。在服务器恢复供电以后,重启服务器。服务器就会根据redo log来恢复数据,但是由于redo log中也不存在这条修改记录,所以修改并为成功,还是之前的数据。而且,事务也为提交成功,所以也会进行回滚,总之,这条数据就不会被修改。

三、事务的隔离级别

以下将基于这张学生表进行演示。MySQL中,默认的事务隔离级别是可重复读。

3.1  读未提交(Read Uncommitted)

可以读到别人未提交的改动,而这个数据可能是无效的,它只是中间数据。

左边的是事务还未提交修改,但是右边的事务已经看到了。这就是读未提交隔离级别带来的问题——脏读。因为读到的这个数据不一定是有效的,所以这种情况就叫做脏读。

3.2  读已提交(Read Committed)

不能读到别人未提交的改动,只能读到别人已经提交的改动。

在读已提交隔离级别下,解决了读未提交隔离级别下的脏读问题。但是,仍然存在不可重复读问题。所谓的不可重复读,就是同一事务中,多次读取同一数据,结果不一样。

可以看到,右边的事务,前后两次查询张三的数学成绩,查到的结果是不一样的。

3.3 可重复读(Repeatable Read)

在同一事务中,多次读取同一数据,结果是一致的。

在可重复读隔离级别下,解决了不可重复读问题,但是,依然存在幻读——在同一事务中,同样的查询条件,第一次和第二次查询时,记录数不一样(因为其他事务插入或删除了满足条件的记录)。

3.4 串行化(Serializable)

所有事务,一个一个来,完全隔离,安全性最高,但是效率最低。

3.5 不同隔离级别总结

隔离级别简称说明问题
READ UNCOMMITTED读未提交能看到其他事务未提交的修改脏读、不可重复读、幻读
READ COMMITTED读已提交只能看到其他事务已提交的修改不可重复读、幻读
REPEATABLE READ可重复读事务内多次读取同一数据,结果一致(MySQL 默认级别)幻读(InnoDB 已通过 MVCC 解决)
SERIALIZABLE串行化最高隔离级别,事务串行执行,完全避免并发问题性能极低(锁表)

四、MVCC

4.1 什么是MVCC

MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB实现非锁定读的核心技术。它让读操作不阻塞写操作,写操作不阻塞读操作,从而大幅提升并发性能。

光看这句话,比较抽象,下面展开来说说。

要理解MVCC可以“让读操作不阻塞写操作,写操作不阻塞读操作”这句话,我们就要先从没有MVCC时,读写操作为什么会相互阻塞说起。

如果没有MVCC,那么读写操作就只能通过锁来实现并发控制,自然就会导致读写互斥的问题。关于锁的概念,这里可以先做个简单了解,后面再进一步理解。

读阻塞写:当事务A读取数据时,会加共享锁(S锁),此时事务B要修改该数据,需要加排他锁(X锁),但S锁和X锁互斥,事务B必须等待事务A释放S锁才能执行,即读阻塞了写。

写阻塞读:当事务B修改数据时,会加排他锁(X锁),此时事务A要读取该数据,需要加共享锁(S锁),但S锁和X锁互斥,事务A必须等待事务B释放X锁才能执行,即写阻塞了读。

在这种情况下,大多的线程都是在阻塞等待,所以导致并发效率低。至于MVCC如何实现“让读操作不阻塞写操作,写操作不阻塞读操作”,下面会详细分析。

4.2 MVCC的核心结构

1)隐藏字段

// 实际的存储结构(简化)
struct InnoDB_ROW {
    DB_ROW_ID row_id;           // 行ID
    DB_TRX_ID db_trx_id;        // 最近修改它的事务ID
    DB_ROLL_PTR db_roll_ptr;    // 回滚指针(指向UNDO LOG)
    // ... 用户实际数据
    // ... 所有索引列
};

2)Read View(读视图)

一个决定事务能看到什么的数据结构,当一个事务执行select时,会生成一个Read View,主要包含以下字段:

m_ids: 当前系统中活跃的事务ID集合(未提交的事务ID集合)

min_trx_id: 活跃的最小事务ID

max_trx_id: 下一个将要分配的事务ID(当前最大的事务ID + 1)

creator_trx_id: 创建这个Read View的事务ID

3)版本链

当一个事务要更新一行数据时,首先要要写Undo Log 日志,然后再修改数据,并且更新当前行的DB_ROLL_PTR(回滚指针),使其指向刚刚存入Undo Log的旧版本数据。这个旧版记录本身也包含指向上一版数据的回滚指针。因此,所有的历史版本数据被串联成一个链表,称为版本链。

4.3 MVCC如何实现读写互不阻塞

MVCC的可见性判断:

事务A发起一个select查询,数据库为事务A创建一个Read View,然后事务A开始遍历数据行。事务A检查这个数据行的DB_TRX_ID(最近修改它的事务ID)。

如果DB_TRX_ID  < min_trx_id(当前活跃的最小事务ID),那么则说明在创建这个Read View之前,修改这行数据的事务已经提交了,所以当前的Read View可以看到这行数据。

如果DB_TRX_ID >= max_trx_id(下一个要分配的事务ID),那么则说明这个数据行的当前版本,是在当前Read View创建之后才开启的事务创建的,对当前的Read View 不可见。

如果 min_trx_id <= DB_TRX_ID < max_trx_id:

        如果DB_TRX_ID在m_ids(活跃事务集合)中,说明该事务还未提交,该数据对当前的Read View不可见。

        如果DB_TRX_ID不在m_ids集合中,说明该事务已经提交,该数据行对当前的Read View可见。

通过以上规则,就限制了事务只能看到它该看的数据。

MVCC实现读写互不阻塞:

MVCC的本质是:InnoDB为每个数据行维护多个版本的快照,读操作读取历史版本,写操作创建新版本,读写操作的目标版本不同,所以不存在锁的竞争,因而读写互不阻塞。下面举个例子:

事务A执行读操作(不加锁)时,不直接读取最新版的行数据,而是通过Read View从Undo Log中找到自己能看到的历史版本,读取这个历史快照。

此时,如果事务B要修改这行数据,那么它不会覆盖旧版本数据,而是创建一个新版本数据,并对新版本数据加一个排他锁(X锁)。

读取数据时,不需要加锁,而且读取的是历史数据,就算该数据被其他事务修改,也不影响读取。

4.4 不同隔离级别下的MVCC

1)读已提交

每次select查询时,都会创建新的Read View,所以在该隔离级别下总是能看到最新提交的数据。因为它只能读取到提交的数据,所以就解决了脏读的问题。

2)可重复读

在可重复读隔离级别下,只在第一次执行 SELECT 时生成一个 Read View,并在整个事务期间都使用这个视图。这就保证了在事务内看到的数据是一致的。所以MVCC可以解决不可重复读问题。

4.5 MVCC与幻读

-- 可重复读下,MVCC能避免"读幻读",但不能避免"写幻读"

-- 事务A
BEGIN;
SELECT * FROM users WHERE age > 20;  -- 返回2条记录

-- 事务B插入并提交
INSERT INTO users(name, age) VALUES('Bob', 25);
COMMIT;

-- 事务A再次SELECT(快照读)
SELECT * FROM users WHERE age > 20;  -- 还是2条(MVCC避免读幻读)

-- 但事务A尝试INSERT相同记录
INSERT INTO users(name, age) VALUES('Bob', 25);
-- 报错:Duplicate entry!这就是"写幻读"
-- MVCC的SELECT看不到,但唯一约束能检测到

在可重复读隔离级别下,MVCC+间隙锁可以解决幻读问题。间隙锁锁定查询范围的 “行 + 间隙”,阻止其他事务插入 / 删除符合条件的行,这样就确保了当前读到的结果集不变。

4.6 版本链清理

由于MVCC会创建很多旧版本数据,如果这些数据一直保留,磁盘和内存会无限增长。因此,数据库需要一个后台的垃圾回收机制。当系统中没有任何活跃的事务还需要某个旧版本数据时(即没有Read View依赖于它),这个旧版本数据就可以被安全地清除掉。Purge线程会定期的清理这些过期的Undo Log。

五、常见锁的分类

5.1 共享锁

也叫读锁(S锁),允许持有锁的事务读取数据,但禁止修改。

5.2 排他锁(独占锁)

也叫写锁(X锁),允许持有锁的事务读取和修改数据, 并禁止其他事务对该数据进行任何操作。

5.3 意向锁

提前声明事务对表中数据的锁定意图,即要加的是S锁还是X锁。

5.4 间隙锁

锁定索引记录间的数据,不包含索引记录本身,防止其他事务在间隙中插入新记录。解决幻读问题(避免事务再次查询时出现新插入的行)。

六、 死锁

6.1 什么是死锁

在MySQL中,死锁是指两个或多个事务相互持有对方所需要的锁,进而导致相互等待,如果没有外力介入,那么这个相互等待的状态将一直持续下去。这就是死锁。

6.2 死锁产生的条件

1)互斥条件:一把锁同一时间只能被一个事务持有。

2)持有并等待:一个事务在等待其他事务释放锁时,不会主动释放自己已经持有的锁。

3)不可剥夺:一个事务持有的锁,不能被其他事务剥夺,只能由自己主动释放。

4)循环等待:多个事务形成闭环等待链。

以上条件同时满足,则死锁发生。

6.3 死锁检测

InnoDB内置了死锁检测机制,当检测到死锁发生时,InnoDB会选择回滚代价最小的事务(修改行数最少),打破形成死锁的条件。本质上打破的是循环等待这个条件,当将其中一个事务回滚时,就切开了循环等待链。

6.4 死锁避免

1)通过设置系统变量innodb_lock_wait_timeout来控制锁的超时时间。

2)减少事务持有锁的时间,因为事务持有锁的时间越长,越容易和其他事务发生冲突。

3)统一加锁顺序,避免循环等待。

4)避免大事务,降低事务的粒度,这样可以避免一个事务持有很多个锁,容易与其他事务的锁产生冲突。

七、结语

如有错误,还请指出!


完~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值