数据库——一文解析MySQL事务机制

一、前言

本文介绍MySQL中事务机制相关知识,全文分为五个部分:事务的ACID特性、事务的隔离机制、死锁、事务日志、MySQL中的事务。一起来看看吧!

二、事务的ACID特性

2.1 初识事务

含义:
事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引华能够成功地对数据库应 用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为渣或其 他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执 行成功,要么全部执行失败。

事务是数据库区别于文件系统的重要特征之一,事务会把数据库库从一种状态转换为另一种状态。在数据库提交工作时,可以确保要么所有修改都保存了,要么所有修改都不保存。

2.2 事务的特性

事务的特性:原子性atomicity、一致性consistency、隔离性isolation、持久性durability,事务的四种特性取英文首字母为ACID,这就是事务的ACID四种特性。

原子性:指处于同一个事务中的多条语句是不可分割的。
一致性:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
隔离性:指多线程环境下,一个线程中的事务不能被其他线程中的事务打扰
持久性:事务一旦提交,就应该被永久保存起来。

2.3 事务ACID实例:银行

银行应用是解释事务必要性的一个经典例子。假设一个银行的数据库有两张表:支票(checking)表和储蓄( savings)表,现在要从用户Jane的支票账户转移200美元到她的储蓄账户,那么需要至少三个多骤:
1.检查支票账户的余额高于200美元
2.从支票账户余额中减去200美元。
3.在储账户余额中增加200美元。
上述三个参骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。

可以用 START TRANSACTION语句开始一个事务,然后要么使用COMMIT提交事务将修改的数据持久保留,要么使用ROLLBACK撤销所有的修改,事务SQL的样本如下:

START TARNSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
COMMIT;

单纯的事务概念并不是故事的全部,试想一下,如果执行到第四条话句时服务器崩溃了。会发生什么?用户可能会损失200美元,再假如,在执行到第三条语句和第四条语句之间时,另外一个进程要删除支票账户的所有余额,那么结果可能就是银行在不知道这个逻辑的情况下白白给了Jane200美元。

除非系统通过严格的ACID测试,否则空谈事务的概念是不够的。ACID表示原子性 (atomicity).一致性( consistency)、隔离性( isolation)和持久性( durability),一个运行良好的事务处理系统,必须具备这些标准特征。

原子性( atomicity)
一个事务必须被视为一个不可分断的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性( consistency)
数据库总是从一个一致性的状态转换到另外一个一致性的状态。在前面的例子中,一致性确保了,即使在执行第三、四条语句之间时系统崩溃,支票账户中也不会损失200美元,因为事务最终没有提交,所以事务中所做的修改也不会保存到数据库中。

隔离性( isolation)
通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有被减去200美元,后面我们讨论隔离级别( Isolation level)的时候,会发现为什么我们要说“通常来说”是不可见的。

持久性( durability)
一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩渍,修改的数据也不会丢失。持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证的策略(如果数据库本身就能做到真正的持久性,那么备份又怎么能增加持久性呢?)。

事务的ACID特性可以确保银行不会弄丢你的钱,而在应用逻辑中,要实现这一点非常难, 甚至可以说是不可能完成的任务,一个兼容ACID的数据库系统,需要做很多复杂但可能用户并没有觉察到的工作,才能确保ACID的实现。

事务处理过程中额外的安全性需要数据库系统做更多的额外工作。一个实现了ACID的数据库,相比没有实现ACID的数据库,通常会需要更强的CPU处理能力、更大的内存和更多的磁盘空间。

用户可以根据业务是否需要事务处理,来选择合适的存储引擎。对于一些不需要事务的查询类应用,选择一个非事务型的存储引擎,可以获得更高的性能,即使存储引擎不支持事务,也可以 通过LOCK TABLE语句为应用提供一定程度的保护,这些选择用户都可以自主决定。

2.4 小结

第二部分分为三小节,2.1 通过事务的含义引入事务,2.2 介绍事务的ACID特性,2.3 通过银行实例复习事务的含义和ACID特性。ACID特性是MySQL事务的最基本特性,为下面事务的隔离机制准备。

三、事务的隔离机制

在SQL标准中定义了四种隔离极别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低,反之,较高级别的隔离保证了较高的安全性,但是可以执行的并发变小,系统开销更高。

先引入几个数据库事务隔离相关问题的含义:
脏读:指一个线程中的事务读取到了另外一个线程中未提交的数据。
不可重复读(虚读):指一个线程中的事务读取到了另外一个线程中提交的update的数据。
幻读:指一个线程中的事务读取到了另外一个线程中提交的insert的数据。

下面简单地介绍一下四种隔离级别。

READ UNCOMMITTED(未提交读)
在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读( Dirty Read),这个级别会导致很多问题,从性能上来说, READ UNCOMMITTED不会比其他的极别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。

READ CONIIED(提交读)
大多数数据库系统的默认都是 READ COMITTED(但 MySQL不是),READ COMMITTED满足前面提到的隔离性的简单定义:一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的,这个级别有时候也叫做不可重复读( nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。

REPEATABLE READ(可重复读)
REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的,但是理论上,可重复读隔离级别还是无法解决另外一个幻读 ( Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行( Phantom Row). InnoDB和 XtraDB存储引擎通过多版本并发控制(MvC, Multiversion Concurrency Control)解决了幻读的问题,。可重复读是MySQL的默认事务隔离级别

SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了幻读问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别, 只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。

一表小结:

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能

小结:级别越高,数据越安全,但性能越低。

附:不可重复读与幻读辨析
不可重复读与幻读比较相似,都是在一个事务中多次读取到不同的数据。

不可重复读
又称虚读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。
一种更易理解的说法是:在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。

幻读
是指事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。
一种更易理解的说法是:当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检锁范围为只读,这样就避免了幻读。简单来说,幻读是由插入或者删除引起的。

不可重复读(虚读)和幻读的差别:
大致的区别在于不可重复读是由于另一个事务对数据的更改所造成的,而幻读是由于另一个事务插入或删除引起的。
从总的结果来看, 似乎两者都表现为两次读取的结果不一致.
从控制的角度来看,两者的区别就比较大:对于前者, 只需要锁住满足条件的记录;对于后者, 要锁住满足条件及其相近的记录

四、死锁

死锁是指两个或者多个事务在同一资源上相互占用,井请求锁定对方占用的资源,从而导致恶性循坏的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁,多个事务同时锁定同一个资源时,也会产生死锁。例如,设想下面两个事务同时处理 StockPrice表:

事务1
    START TRANSACTION;
    UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2019-05-01';
    UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2019-05-02';
    COMMIT;
事务2
    START TRANSACTION;
    UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 and date = '2019-05-02';
    UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = '2019-05-01';
    COMMIT;

如果凑巧,两个事务都执行了第一条UPDATE语句,更新了一行数据,同时也锁定了该行数据,接着每个事务都尝试去执行第二条UPDATE语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷入死循环。除非有外部因素介入才可能解除死锁。

为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,比如 InnoDB存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。

锁的行为和顺序是和存储引擎相关的。以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。死锁的产生有双重原因:有些是因为真正的数据冲突,这种情况通常很难避免,但有些则完全是由于存储引擎的实现方式导致的。
死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型的系
统,这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。

五、事务日志

事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。事务日志持久以后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志( Write-Ahead logging),修改数据需要写两次磁盘。

如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。具体的恢复方式则视存储引擎而定。

六、MySQL中的事务

MySQL提供了两种事务型的存储引擎:InnoDB和NDB Cluster。

自动提交( AUTOCOMMIT)

MySQL默认采用自动提交( AUTOCOMMIT)模式,也就是说,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作,在当前连接中,可以通过设置 AUTOCOMMIT变量来启用成者禁用自动提交模式。

mysql> SET AUTOCOMMIT = 1;
mysql> SET AUTOCOMMIT = 0;

1或者ON表示启用,0或者OFF表示禁用。当AUTOCOMMIT=0时,所有的查询都是在一个 事务中,直到显式地执行COMMIT提交或者ROLLBACK回滚,该事务结束,同时又开始了另一个新的事务。 修改AUTOCOMMIT对非事务型的表,比如 MyISAM或者内存表,不会有任何影响。对这类表来说,没有COMMIT或者ROLLBACK的概念,也可以说是相当于一直处于AUTOCOMMIT启用的模式。

另外还有一些命令,在执行之前会强制执行 COMMIT提交当前的活动事务。典型的例子, 在数据定义语言(DDL)中,如果是会导致大量数据改变的操作,比如 ALTER TABLE, 就是如此。另外还有LOCK TABLES等其他语句也会导致同样的结果。如果有需要,请检查对应版本的官方文档来确认所有可能导致自动提交的语句列表。

MySQL可以通过执行 SET TRANSACTION ISOLATION LEVEL命令来设置隔离级别。新的隔离级别会在下一个事务开始的时候生效,可以在配置文件中设置整个数据库的隔离级别,也可以只改变当前会话的隔离级别:

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

MySQL能够识别所有的4个ANSI隔离级别, InnoDB引擎也支持所有的隔离级别。

在事务中混合使用存储引擎

MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。

如果在事务中混合使用了事务型和非事务型的表(例如 InnoDB和 MyISAM表),在正常提交的情况下不会有什么问题。

但如果该事务需要回滚,非事务型的表上的变更就无法撒销,这会导致数据库处于不一 致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。

在非事务型的表上执行事务相关操作的时候, MySQL通常不会发出提醒,也不会报错。有时候只有回滚的时候才会发出一个警告:“某些非事务型的表上的变更不能被回滚”。但大多数情况下,对非事务型表的操作都不会有提示。

隐式和显式锁定

InnoDB采用的是两阶段锁定协议( two-phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有在执行COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定, InnoDB会根据隔离级别在需要的时候自动加锁。

另外, InnoDB也支持通过特定的语句进行显式锁定,这些语句不属于SQL规范:

SELECT ... LOCK IN SHARE MODE
SELECT ... FOR UPDATE

MySQL也支持LOCK TABLES和UNLOCK TABLES语句,这是在服务器层实现的,和存储引擎无关。它们有自己的用途,但并不能替代事务处理。如果应用需要用到事务,还是应该选择事务型存储引擎。

经常可以发现,应用已经将表从 MyISAM转换到InnoDB,但还是显式地使用LocK TABLES语句。这不但没有必要,还会严重影响性能,实际上 InnoDB的行级锁工作得更好。

七、小结

本文介绍MySQL中事务机制相关知识,全文分为五个部分:事务的ACID特性、事务的隔离机制、死锁、事务日志、MySQL中的事务,帮助初学者学好MySQL事务相关知识。

天天打码,天天进步!

发布了190 篇原创文章 · 获赞 71 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_36963950/article/details/104547852