三、MySQL并发控制、事务与隔离级别

本文详细介绍了MySQL的并发控制机制,包括读写锁、锁粒度,以及多版本并发控制(MVCC)。讨论了事务的四大特性:原子性、一致性、隔离性和持久性,并详细阐述了事务的四种隔离级别及其可能导致的并发问题。此外,还讲解了MySQL验证事务隔离级别的方法、死锁现象以及事务日志的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当有多个连接对MySQL表中数据进行并发读写时,就会产生并发问题。为了避免并发问题,MySQL中引入了相关的锁。

1、MySQL的锁机制

1.1、读写锁

当多个客户端同时读取表中的数据时,不会产生并发问题。但是当有客户端在写入数据时,其他客户端来读取数据就会产生并发问题。为了提高并发程度,MySQL中使用了读写锁的机制。

读锁(共享锁):读锁是共享的,读锁之间是相互不阻塞的。多个客户端在同一时刻可以共同读取同一资源。

写锁(排它锁):写锁是互斥的,一个写锁会阻塞其他的写锁和读锁。

一个用户加了写锁之后,其他用户不能写入,也不能读取;一个用户加了读锁后,其他用户可以读取,但是不能写入。

1.2、锁粒度

通常来说,锁的粒度越大,锁住的资源越多,并发程度越低,但是越安全。MySQL中每个存储引擎都各自实现了自己的锁策略,其中InnoDB的锁机制包括表锁和行锁。

表锁:锁定整张表,是最基本的锁策略。MySQL的存储引擎层和服务器层都实现了表锁。

行锁:锁定某一行,可以最大程度的支持并发。行锁只在存储引擎中实现,而服务器层没有实现。

2、MySQL多版本并发控制(MVCC)

InnoDB中,为了提高并发程度,除了实现表锁之外,还提供了多版本并发控制(Multi Version Concurrency Control, MVCC)。MVCC可以理解为是一种特殊的行锁,且类似于乐观锁。在很多情况下能够避免加锁操作,实现非阻塞的读,而写入时也只需要对指定的某一行进行加锁。

MVCC是通过保存数据在某个时间点的快照来实现的。因此,在同一个事务中,能够保证多次读取的数据是一致的,不会出现不可重复读的问题,实现了可重复读的隔离级别,这也是MySQL(InnoDB)中事务默认的隔离级别。

对于MVCC,不同存储引擎可以有不同的实现。InnoDB实现MVCC的基本原理如下:在每行数据的后面,保存两个隐藏列,这两列中分别存储了这一行的创建时间、过期时间(删除时间)。这两列存储的并非是实际的时间的值,而是存储的一个系统版本号。也就是说,InnoDB为每行数据保存了两个版本号:创建版本号、删除版本号

每开始一个事务,系统版本号都会自动递增。同时,事务开始时刻的系统版本号会作为这个事务的版本号,事务版本号用于查询时和系统版本号进行比较。

InnoDB事务默认的隔离级别是可重复读,在这个隔离级别下,InnoDB在增删改查时基本原理如下:

查询(select)

创建版本号:只会读取创建版本号小于或等于当前事务版本号的数据行。这样能够保证事务中读取到的数据要么是在事务开始前就已经修改过的,要么就是在本事务中被修改的。而事务开始后,被其他事务修改后的数据是不会被读取到的,因为它们的创建版本号一定是大于当前事务版本号的。删除版本号:只能读取到删除版本号是无效值,或者删除版本号大于当前事务版本号的记录行。否则,说明该行在本事务之前已经被删除。

插入(insert):将当前的系统版本号保存到行的创建版本号中。

删除(delete):将当前的系统版本号保存到行的删除版本号中,作为删除标识。

更新(update):新插入一条记录,并将当前的系统版本号保存到新行的创建版本号中;同时,将当前的系统版本号保存到原来的行的删除版本号中,作为旧行的删除标识。

得益于额外保存的这两个版本号,InnoDB中对于大多数操作都可以在不加锁的情况下进行。并且能够保证读取的数据都是正确的。缺点就是InnoDB需要为这两列做一些额外的维护操作。

InnoDB中,MVCC只能在读已提交和可重复读这两个隔离级别中起作用,而与其他两个隔离级别不兼容。因为读未提交永远都是读取最新版本号的行,不会用到这两个版本号;而串行化则会为所有读取的行都进行加锁,也没有必要使用这两个版本号。

3、事务的特性

数据库的事务可以简单理解为是一组SQL语句。对于事务内的SQL语句,要么全部执行成功,要么全部执行失败。事务具有四大特性(ACID):

3.1、原子性(Atomicity)

一个事务是一个不可分割的最小工作单元,事务包含的所有操作要么全部提交成功,要么全部失败回滚。

3.2、一致性(Consistency)

数据库总是从一个一致性状态转换到另一个一致性状态。

拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,也就是说,数据总是从一个一致性状态转换到另一个一致性状态,这就是事务的一致性。

3.3、隔离性(Isolation)

事务的隔离性指的是,当并发产生多个事务时,各个事务相互之间相互隔离、互不影响。对于隔离性来说,数据库可以设置不同的隔离级别,隔离级别越高,事务之间的相互影响程度就越低,但是并发程度也会随之下降。

3.4、持久性(Durability)

一旦事务提交,所做的修改就会永久保存在数据库中。

4、事务的隔离级别

4.1、并发问题

在了解事务的隔离级别之前,首先了解一下在并发事务中存在的问题:

4.1.1、脏读

事务A读取了事务B已修改但未提交的数据。

4.1.2、不可重复读

事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

4.1.3、幻读

例如,事务A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是事务B就在这个时候插入了一条具体分数的记录,当事务A修改结束值后,再次查询发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

4.1.4、不可重复读与幻读的区别

不可重复读是由对某一行数据的修改引起的,侧重于对表中已有数据的修改(update);幻读是增加或者删除了某行引起的,重于对往表中新插入或者删除一条记录(insert/delete)。因此,解决不可重复读的问题通常只需对某一行加锁,解决幻读需要对整张表加锁。

4.2、隔离级别

了解了事务中存在的并发问题之后,再来看下事务的隔离级别。在SQL标准定义了4种隔离级别,分别如下:

4.2.1、读未提交(Read Uncommitted)

事务中的修改,即使没有提交,对其他事务也是可见的。换句话说,在一个事务里面,能够读取到其他事务尚未提交的数据,未提交的数据称为脏数据,这种情况就叫做脏读。

读未提交的隔离级别最低,存在脏读问题,相当于啥也没干,事务之间没有隔离,不能解决什么问题。而且效率并不比其他级别好太多,因此实际应用非常少。

4.2.2、读已提交(Read Committed)

一个事务开始时,只能看到其他已经提交的事务所做的修改,对其他事务未提交的修改是看不见的。换言之,一个事务所做的修改在最终提交之前,对其他事务是不可见的。这就解决了前面的脏读问题,它满足隔离性简单的定义,是多数数据库默认的隔离级别(但不是MySQL的默认隔离级别)。

但是,读已提交不能解决不可重复读的问题。也就是说,在一个事务A中,首先读取了一条数据,然后事务B对这条数据进行了修改,并且进行了提交,然后在事务A中再次读取此数据时,就会发现与之前读取的值不同,这就是不可重复读。

4.2.3、可重复读(Repeatable Read)

解决了脏读和不可重复读问题,是MySQL的默认隔离级别。在可重复读隔离级别中,能够保证在同一个事务中,多次读取同一行记录时,读取到的结果是相同的。但是,如果在一个事务中,多次读取的不是某一行记录的值,而是总的行数之类的数据,就有可能出现幻读问题。比如事务A首先读取了表中总的行数,然后事务B往表中新插入或者删除了一条数据并进行了事务提交,然后在事务A中再次读取表中总的行数,就会发现与之前读取的结果不同,这种情况就叫做幻读。

4.2.4、串行化(Serializable)

强制事务的串行执行,避免了包括幻读在内的一切并发问题,但是效率太低。

事务的隔离级别与对应存在的并发问题总结如下(MySQL):

事务隔离级别

存在脏读问题

存在不可重复读问题

存在幻读问题

未提交读

(read-uncommitted)

不可重复读

(read-committed)

可重复读

(repeatable-read)

串行化(serializable)

补充:

1、事务隔离级别为读提交时,写数据只会锁住相应的行

2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。

3、事务隔离级别为串行化时,读写数据都会锁住整张表

4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

5、MySQL验证事务的隔离级别

MySQL中默认隔离级别为:可重复读。

MySQL中支持隐式和显式的加锁。在事务的执行过程中,随时可以加锁,但是只有在commit和rollback的时候才会释放锁,并且所有的锁是同时释放。 根据设置的隔离级别,MySQL会自动在需要的时候进行加锁,这是隐式锁定。

此外,MySQL也支持显式锁定。如以下两条语句,显式的对该行资源进行了加锁

5.1、验证读未提交级别

 首先打开一个MySQL窗口,设置隔离级别为读未提交,开启一个事务,查看表中数据,此时count=50

 再打开一个客户端,设置隔离级别为读未提交,开启一个事务,将count修改为45,并且不提交事务

 此时再到客户端A中查看,可以看到读取到了事务B尚未提交修改的值:count=45

5.2、验证读已提交级别

 首先打开一个MySQL窗口A,设置隔离级别为读已提交,开启一个事务,查看表中数据,此时count=50

 再打开一个客户端,设置隔离级别为读已提交,开启一个事务,将count修改为45,并且不提交事务

再次在窗口A中的事务中 查看数据,读取到的count仍然为50,不会读取到事务B未提交的脏数据。

 在窗口B中提交事务

再次在窗口A中的事务查看,读取到了事务B已经提交的数据count=45,在同一个事务中多次读取同一个数据,得到了不同的结果,出现了不可重复读的问题。

5.3、验证可重复读级别

5.4、验证串行化级别

6、死锁

MySQL死锁是指两个或多个事务各自占用了一个资源,并请求对方占用的资源,从而导致恶性死循环的现象。当多个事务以不同的顺序请求不同的资源时,就有可能导致死锁。 如下:

假设两个事务此时都执行了各自第一条语句,并对该行记录进行了加锁,同时都去请求对第二行数据加锁,却发现第二行数据都已经被对方加锁,这就会导致死锁。

为了解决死锁问题,MySQL实现了各种死锁检测和死锁超时机制。InnoDB实现了非常复杂的检测机制,能够在发生死锁时,立即返回一个错误,从而避免长时间死锁。InnoDB检测到死锁时,会将当前持有行级排他锁数量最少的事务回滚。

7、事务日志

MySQL使用了事务日志,以提高事务的效率。有了事务日志,存储引擎在修改表中数据时,可以只在内存中修改,再将该修改日志记录到事务日志中。事务日志是追加写的,是顺序IO,比修改表中数据的随机IO性能更快。事务日志持久化之后,MySQL后台再将内存中的表数据慢慢进行持久化。

假设对表的修改日志已经持久化到事务日志中,但表的数据还没有刷到磁盘中,若此时MySQL发生了故障,依赖于事务日志,MySQL也能将数据正确恢复回来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值