推荐阅读:
恕我直言!收藏这个MySQL文档,你其余的MySQL学习资料都可以扔了
如果数据库中的事务都是串行执行的,这种方式可以保障事务的执行不会出现异常和错误,但带来的问题是串行执行会带来性能瓶颈;而事务并发执行,如果不加以控制则会引发诸多问题,包括死锁、更新丢失等等。这就需要我们在性能和安全之间做出合理的权衡,使用适当的并发控制机制保障并发事务的执行。
首先我们先来了解一下并发事务会带来哪些问题。并发事务访问相同记录大致可归纳为以下3种情况:
因为读取记录并不会对记录造成任何影响,所以同个事务并发读取同一记录也就不存在任何安全问题,所以允许这种操作。
如果允许并发事务都读取同一记录,并相继基于旧值对这一记录做出修改,那么就会出现前一个事务所做的修改被后面事务的修改覆盖,即出现提交覆盖的问题。
另外一种情况,并发事务相继对同一记录做出修改,其中一个事务提交之后之后另一个事务发生回滚,这样就会出现已提交的修改因为回滚而丢失的问题,即回滚覆盖问题。
这两种问题都造成丢失更新,其中回滚覆盖称为第一类丢失更新问题,提交覆盖称为第二类丢失更新问题。
这种情况较为复杂,也最容易出现问题。
如果一个事务读取了另一个事务尚未提交的修改记录,那么就出现了脏读的问题;
如果我们加以控制使得一个事务只能读取其他已提交事务的修改的数据,那么这个事务在另一事物提交修改前后读取到的数据是不一样的,这就意味着发生了不可重复读;
如果一个事务根据一些条件查询到一些记录,之后另一事物向表中插入了一些记录,原先的事务以相同条件再次查询时发现得到的结果跟第一次查询得到的结果不一致,这就意味着发生了幻读。
对于以上提到的并发事务执行过程中可能出现的问题,其严重性也是不一样的,我们可以按照问题的严重程度排个序:
丢失更新 > 脏读 > 不可重复读 > 幻读
因此如果我们可以容忍一些严重程度较轻的问题,我们就能获取一些性能上的提升。于是便有了事务的四种隔离级别:
值得注意的是以上四种隔离级别都不会出现回滚覆盖的问题,但是提交覆盖的问题对于MySQL来说,在Read Uncommitted、Read Committed以及Repeatable Read这三种隔离级别下都会发生(标准的Repeatable Read隔离级别不允许出现提交覆盖的问题),需要额外加锁来避免此问题。
SQL规范定义了以上四种隔离级别,但是并没有给出如何实现四种隔离级别,因此不同数据库的实现方式和使用方式也并不相同。而SQL隔离级别的标准是依据基于锁的实现方式来制定的,因为有必要先了解一下传统的基于锁的隔离级别是如何实现的。
既然说到传统的隔离级别是基于锁实现的,我们先来了解一下锁。
传统的锁有两种:
需要注意的是,加了共享锁的记录,其他事务也可以获得该记录的共享锁,但是无法获取该记录的排他锁,即S锁和S锁是兼容的,S锁和X锁是不兼容的;而加了排他锁的记录,其他事务既无法获取该记录的共享锁也无法获取排他锁,即X锁和X锁也是不兼容的。
另外,刚刚说到事务对一条记录进行读操作时,需要先获取该记录的S锁,但有时事务在读取记录时需要阻止其他事务访问该记录,这时就需要获取该记录的X锁。以MySQL为例,有以下两种锁定读的方式:
SELECT ... LOCK IN SHARE MODE;
如果事务执行了该语句,则会在读取的记录上加S锁,这样就允许其他事务也能获取到该记录的S锁;而如果其他事务需要获取该记录的X锁,那么就需要等待当前事务提交后释放掉S锁。
SELECT ... FOR UPDATE;
如果事务执行了该语句,则会在读取的记录上加X锁,这样其他事务想要说去该记录的S锁或X锁,那么需要等待当前事务提交后释放掉X锁。
对于锁的粒度而言,锁又可以分为两种:
在基于锁的实现方式下,四种隔离级别的区别就在于加锁方式的区别:
这里面有一些细节值得注意:
不同数据库对于SQL标准中规定的隔离级别支持是不一样的,数据库引擎实现隔离级别的方式虽然都在尽可能地贴近标准的隔离级别规范,但和标准的预期还是有些不一样的地方。
MySQL(InnoDB)支持的4种隔离级别,与标准的各级隔离级别允许出现的问题有些出入,比如MySQL在可重复读隔离级别下可以防止幻读的问题出现,但也会出现提交覆盖的问题。
相对于传统隔离级别基于锁的实现方式,MySQL 是通过MVCC(多版本并发控制)来实现读-写并发控制,又是通过两阶段锁来实现写-写并发控制的。MVCC是一种无锁方案,用以解决事务读-写并发的问题,能够极大提升读-写并发操作的性能。
为了方便描述,首先我们创建一个表book,就三个字段,分别是主键book_id, 名称book_name, 库存stock。然后向表中插入一些数据:
INSERT INTO book VALUES(1, '数据结构', 100);INSERT INTO book VALUES(2, 'C++指南', 100);INSERT INTO book VALUES(3, '精通Java', 100);
对于使用InnoDB存储引擎的表,其聚簇索引记录中包含了两个重要的隐藏列:
如果在一个事务中多次对记录进行修改,则每次修改都会生成undo日志,并且这些undo日志通过roll_pointer指针串联成一个版本链,版本链的头结点是该记录最新的值,尾结点是事务开始时的初始值。
例如,我们在表book中做以下修改:
BEGIN;UPDATE book SET stock = 200 WHERE id = 1;UPDATE book SET stock = 300 WHERE id = 1;
那么id=1的记录此时的版本链就如下图所示:
对于使用Read Uncommitted隔离级别的事务来说,只需要读取版本链上最新版本的记录即可;对于使用Serializable隔离级别的事务来说,InnoDB使用加锁的方式来访问记录。而Read Committed和Repeatable Read隔离级别来说,都需要读取已经提交的事务所修改的记录,也就是说如果版本链中某个版本的修改没有提交,那么该版本的记录时不能被读取的。所以需要确定在Read Committed和Repeatable Read隔离级别下,版本链中哪个版本是能被当前事务读取的。于是ReadView的概念被提出以解决这个问题。
首先我们需要知道的一个事实是:事务id是递增分配的。ReadView的机制就是在生成ReadView时确定了以下几种信息:
这样事务id就可以分成3个区间:
下面我们根据ReadView提供的条件信息,顺着版本链从头结点开始查找最新的可被读取的版本记录:
1、首先判断版本记录的trx_id与ReadView中的creator_trx_id是否相等。如果相等,那就说明该版本的记录是在当前事务中生成的,自然也就能够被当前事务读取;否则进行第2步。
2、根据版本记录的trx_id以及上述3个区间信息,判断生成该版本记录的事务是否是已提交事务,进而确定该版本记录是否可被当前事务读取。
如果某个版本记录经过以上步骤判断确定其可被当前事务读取,则查询结果返回此版本记录;否则读取下一个版本记录继续按照上述步骤进行判断,直到版本链的尾结点。如果遍历完版本链没有找到可读取的版本,则说明该记录对当前事务不可见,查询结果为空。
在MySQL中,Read Committed和Repeatable Read隔离级别下的区别就是它们生成ReadView的时机不同。
之前说到ReadView的机制只在Read Committed和Repeatable Read隔离级别下生效,所以只有这两种隔离级别才有MVCC。在Read Committed隔离级别下,每次读取数据时都会生成ReadView;而在Repeatable Read隔离级别下只会在事务首次读取数据时生成ReadView,之后的读操作都会沿用此ReadView。
下面我们通过例子来看看Read Committed和Repeatable Read隔离级别下MVCC的不同表现。我们继续以表book为例进行演示。
假设在Read Committed隔离级别下,有如下事务在执行,事务id为10:
BEGIN; // 开启Transaction 10UPDATE book SET stock = 200 WHERE id = 2;UPDATE book SET stock = 300 WHERE id = 2;
此时该事务尚未提交,id为2的记录版本链如下图所示:
然后我们开启一个事务对id为2的记录进行查询:
BEGIN;
当执行SELECT语句时会生成一个ReadView,该ReadView中的m_ids为[10],min_trx_id为10,max_trx_id为11,creator_trx_id为0(因为事务中当执行写操作时才会分配一个单独的事务id,否则事务id为0)。按照我们之前所述ReadView的工作原理,我们查询到的版本记录为
+----------+-----------+-------+| book_id | book_name | stock |+----------+-----------+-------+| 2 | C++指南 | 100 |+----------+-----------+-------+
然后我们将事务id为10的事务提交:
BEGIN; // 开启Transaction 10UPDATE book SET stock = 200 WHERE id = 2;UPDATE book SET stock = 300 WHERE id = 2;COMMIT;
同时开启执行另一事务id为11的事务,但不提交:
BEGIN; // 开启Transaction 11UPDATE book SET stock = 400 WHERE id = 2;
此时id为2的记录版本链如下图所示:
然后我们回到刚才的查询事务中再次查询id为2的记录:
BEGIN;SELECT * FROM book WHERE id = 2; // 此时Transaction 10 未提交SELECT * FROM book WHERE id = 2; // 此时Transaction 10 已提交
当第二次执行SELECT语句时会再次生成一个ReadView,该ReadView中的m_ids为[11],min_trx_id为11,max_trx_id为12,creator_trx_id为0。按照ReadView的工作原理进行分析,我们查询到的版本记录为
+----------+-----------+-------+| book_id | book_name | stock |+----------+-----------+-------+| 2 | C++指南 | 300 |+----------+-----------+-------+
从上述分析可以发现,因为每次执行查询语句都会生成新的ReadView,所以在Read Committed隔离级别下的事务读取到的是查询时刻表中已提交事务修改之后的数据。
我们在Repeatable Read隔离级别下重复上面的事务操作:
BEGIN; // 开启Transaction 20UPDATE book SET stock = 200 WHERE id = 2;UPDATE book SET stock = 300 WHERE id = 2;
此时该事务尚未提交,然后我们开启一个事务对id为2的记录进行查询:
BEGIN;SELECT * FROM book WHERE id = 2;
当事务第一次执行SELECT语句时会生成一个ReadView,该ReadView中的m_ids为[20],min_trx_id为20,max_trx_id为21,creator_trx_id为0。根据ReadView的工作原理,我们查询到的版本记录为
+----------+-----------+-------+| book_id | book_name | stock |+----------+-----------+-------+| 2 | C++指南 | 100 |+----------+-----------+-------+
然后我们将事务id为20的事务提交:
BEGIN; // 开启Transaction 20UPDATE book SET stock = 200 WHERE id = 2;UPDATE book SET stock = 300 WHERE id = 2;COMMIT;
同时开启执行另一事务id为21的事务,但不提交:
BEGIN; // 开启Transaction 21UPDATE book SET stock = 400 WHERE id = 2;
然后我们回到刚才的查询事务中再次查询id为2的记录:
BEGIN;SELECT * FROM book WHERE id = 2; // 此时Transaction 10 未提交SELECT * FROM book WHERE id = 2; // 此时Transaction 10 已提交
当第二次执行SELECT语句时不会生成新的ReadView,依然会使用第一次查询时生成ReadView。因此我们查询到的版本记录跟第一次查询到的结果是一样的:
+----------+-----------+-------+| book_id | book_name | stock |+----------+-----------+-------+| 2 | C++指南 | 100 |+----------+-----------+-------+
从上述分析可以发现,因为在Repeatable Read隔离级别下的事务只会在第一次执行查询时生成ReadView,该事务中后续的查询操作都会沿用这个ReadView,因此此隔离级别下一个事务中多次执行同样的查询,其结果都是一样的,这样就实现了可重复读。
在Read Committed和Repeatable Read隔离级别下,普通的SELECT查询都是读取MVCC版本链中的一个版本,相当于读取一个快照,因此称为快照读。这种读取方式不会加锁,因此读操作时非阻塞的,因此也叫非阻塞读。
在标准的Repeatable Read隔离级别下读操作会加S锁,直到事务结束,因此可以阻止其他事务的写操作;但在MySQL的Repeatable Read隔离级别下读操作没有加锁,不会阻止其他事务对相同记录的写操作,因此在后续进行写操作时就有可能写入基于版本链中的旧数据计算得到的结果,这就导致了提交覆盖的问题。想要避免此问题,就需要另外加锁来实现。
之前提到MySQL有两种锁定读的方式:
SELECT ... LOCK IN SHARE MODE; // 读取时对记录加S锁,直到事务结束SELECT ... FOR UPDATE; // 读取时对记录加X锁,直到事务结束
这种读取方式读取的是记录的当前最新版本,称为当前读。另外对于DELETE、UPDATE操作,也是需要先读取记录,获取记录的X锁,这个过程也是一个当前读。由于需要对记录进行加锁,会阻塞其他事务的写操作,因此也叫加锁读或阻塞读。
当前读不仅会对当前记录加行记录锁,还会对查询范围空间的数据加间隙锁(GAP LOCK),因此可以阻止幻读问题的出现。
本文介绍了事务的多种并发问题,以及用以避免不同程度问题的隔离级别,并较为详细描述了传统隔离级别的实现方式以及MySQL隔离级别的实现方式。但数据库的并发机制较为复杂,本文也只是做了大致的描述和介绍,很多细节还需要读者自己查询相关资料进行更细致的了解。
作者:Turling_hu
链接:https://juejin.im/post/5e72246ae51d4527143e6a0d
推荐阅读:
恕我直言!收藏这个MySQL文档,你其余的MySQL学习资料都可以扔了
ylbtech-DB-MySQL:MySQL 事务 1.返回顶部 1、 MySQL 事务 MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务! 在 MySQL 中只有使用了...
五、锁与事务隔离级别 事务隔离级别简单的说,就是当激活事务时,控制事务内因SQL语句产生的锁定需要保留多入,影响范围多大,以防止多人访问时,在事务内发生数据查询的错误。设置事务隔离级别将影响整条连接。 SQL Server 数据库引擎支持所有这些隔离级别: · 未提交读(隔离事务的最低级别,只能保证不读取物理上损坏的数据) · 已提...
MySQL的事务支持不是绑定在MySQL服务器本身,而是与存储引擎相关1.MyISAM:不支持事务,用于只读程序提高性能 2.InnoDB:支持ACID事务、行级锁、并发 3.Berkeley DB:支持事务 一个事务是一个连续的一组数据库操作,就好像它是一个单一的工作单元进行。换言之,永远不会是完整的事务,除非该组内的每个...
1. 基于注解的事务配置 1. 在需要添加事务的方法上加上@Transactional注解 2. Spring的配置文件中配置事务管理器 1
2 《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---34
以下为阅读《Linux命令行与shell脚本编程大全 第3版》的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下: 转载于:https://www.cnblogs.com/guochaoxxl/p/7894620.html...
先吐为敬! 最近心血来潮研究nodejs如何完成微信支付功能,结果网上一搜索,一大堆“代码拷贝党”、“留一手”、“缺斤少两”、“不说人话”、“自己都没跑通还出来发blog”、“各种缺少依赖包”、“各种注释都没有”、“自己都不知道在写什么”的程序大神纷纷为了增加自己博客一个帖子的名额而发布了各种千奇百�...
阅读ceph源码过程中需要明确当前操作是由哪个线程发出,此时需要根据线程id来确认线程名称 C++获取线程id是通过系统调用来直接获取
函数描述
头文件:
面试题 分库分表之后,id 主键如何处理? 面试官心理分析 其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。 面试题剖析 基于数据库的实现方案 数据库自增 id 这个就是说你的...
ORM操作 单表、一对多表操作 1 from django.db import models 2 3 4 class UserGroup(models.Model): 5 title = models.CharField(max_length=32) 6 7 8 class UserInfo(m...
建立如下表: 建表语句: class表创建语句 create table class(cid int not null auto_increment primary key, caption varchar(32) not null)engine=innodb default charset=utf8;student表创建语句 c...