MySQL基础知识点整理 - 事务

MySQL基础知识点整理 - 事务

一、简介

1. 什么是事务

事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。在整个过程中,无论事务是否成功完成,总能确保数据的完整性。

2. 如何使用事务

银行应用是解释事务必要性的一个经典例子。
假设一个银行的数据库有两张表:支票(checking)表和储蓄(savings)表。现在要从用户的支票账户转移 200 元到他的储蓄账户,那么需要至少三个步骤:

  • 1.检查支票账户的余额高于 200 元。
  • 2.从支票账户余额中减去 200 元。
  • 3.在储蓄账户余额中增加 200 元。

上述三个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。

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

/* 开始事务 */
START TRANSACTION;
/* 检查支票账户的余额高于 200 元 */
SELECT balance FROM checking WHERE customer_id=10233276;
/* 从支票账户余额中减去 200 元 */
UPDATE checking SET balance=balance-200.00 WHERE customer_id=10233276;
/* 在储蓄账户余额中增加 200 元 */
UPDATE savings SET balance=balance+200.00 WHERE customer_id=10233276;
/* 提交事务 */
COMMIT;

二、事务的 ACID 特性

ACID表示原子性 (atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。一个运行良好的事务处理系统,必须具备这些标准特征。

1. 原子性 (atomicity)

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

2. 一致性(consistency)

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

3. 隔离性(isolation)

通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有被减去200元。

4. 持久性(durability)

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。

三、事务的隔离级别

1. READ UNCOMMITTED(未提交读)

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

2. READ COMMITTED(提交读)

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

3. REPEATABLE READ(可重复读)

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

4. SERIALIZABLE(可串行化)

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

四种隔离级别对比

隔离级别 脏读可能性 不可重复读可能性 幻读可能性 加锁读
READ UNCOMMITTED ×
READ COMMITTED × ×
REPEATABLE READ × × ×
SERIALIZABLE × × ×

四、死锁

1. 什么是死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。

2. 死锁实例

设想下面两个事务同时处理 StockPrice 表:

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

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

3. 如何解决死锁问题

为了解决死锁问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,比如 InnoDB 存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。InnoDB 目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。
锁的行为和顺序是和存储引擎相关的。以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。死锁的产生有双重原因:有些是因为真正的数据冲突,这种情况通常很难避免,但有些则完全是由于存储引擎的实现方式导致的。死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型的系统,这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。

五、MySQL 事务的特点

1. 自动提交(AUTOCOMMIT)

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

mysql> SHOW VARIABLES LIKE 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

mysql> SET AUTOCOMMIT=0;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW VARIABLES LIKE 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

mysql]> SET AUTOCOMMIT=1;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW VARIABLES LIKE 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

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;

2. 隐式和显式锁定

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

另外,InnoDB 也支持通过特定的语句进行显式锁定,这些语句不属于SQL规范(这些锁定提示经常被滥用,实际上应当尽量避免使用)

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

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

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

LOCK TABLES 和事务之间相互影响的话,情况会变得非常复杂,在某些 MySQL 版本中甚至会产生无法预料的结果。因此,建议除了事务中禁用了 AUTOCOMMIT,可以使用 LOCK TABLES 之外,其他任何时候都不要显式地执行 LOCK TABLES,不管使用的是什么存储引擎。

参考《高性能MySQL》
阅读 458

推荐阅读
四魂之域
用户专栏

在学习和工作中成长。

986 人关注
87 篇文章
专栏主页