MySQL 事务机制

事务处理是保证数据安全的重要机制,事务有四个重要属性 ,根据它们的英文名称能够记为ACID:html

  • 原子性(Atomic): 事务操做是不可分割的; 事务只存在已执行和未执行两种状态,不存在只执行了部分指令的状况
  • 一致性(Consistency): 数据库老是从一个一致的状态转换到另外一个一致状态
  • 隔离性(Isolation): 同时执行的事务之间相互隔离,不会互相影响。
  • 持久性(Durability): 事务成功提交后, 其写入的数据直到被覆盖永久有效

咱们以银行转帐操做为例理解事务:mysql

START TRANSACTION;
UPDATE account_balance SET balance = balance - 200.00 WHERE customer_id = 1;
UPDATE account_balance SET balance = balance + 200.00 WHERE customer_id = 2;
COMMIT;

上述事务执行先后数据库只可能有两种状态: 帐户一、2的余额未变化, 帐户1余额减小200元帐户2余额增长200元。不可能存在帐户1余额减小而帐户2余额不变的状态。sql

减小帐户1余额和增长帐户2余额是一个连续的过程, 不容许在事务执行过程当中对帐户一、2余额进行其它操做。数据库

事务的原子性体如今两方面:安全

  • 事务执行过程当中不容许插入其它操做。 减小帐户1余额和增长帐户2余额是一个连续的过程, 不容许在事务执行过程当中对帐户一、2余额进行其它操做。
  • 事务中的全部更改要么都发生要么都不发生, 不存在部分完成的状况。 减小帐户1余额和增长帐户2余额要么都发生,要么都不发生

事务一致性体如今: 事务执行先后数据库老是维持在一致状态, 转帐开始前到转帐结束(不管转帐成功或失败)的整个过程当中, 帐户一、2的总余额始终不变。并发

事务隔离性体如今: 在转帐事务减小帐户1余额后提交以前,另外一个事务查询到的帐户1余额还是减小以前的。mvc

并发事务的潜在问题

  • 脏读: 事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果(即脏数据)。 如事务A在执行转帐操做,从转出帐户扣除了余额但未修改转入帐户余额,此时事务B读取了转入帐户的余额, 即发生了脏读。性能

  • 不可重复读: 在同一个事务中,对于同一条数据两次查询读到的结果不一致。好比,在事务A两次查询中间事务B修改了某条记录,那么事务A两次查询会读到不一样的结果。rest

  • 幻读: 在同一个事务中,对于同一个查询返回的记录数不一致。形成这种现象的缘由是在事务A的两次查询中间事务B添加或删除了记录,致使事务A两次查询读到不一样的结果。code

幻读和不可重复读的区别在于,不可重复读是对已存在记录的修改致使的只须要对某一条记录加锁便可,幻读增删记录致使的必须对全表加锁。

事务隔离级别

MySQL提供四级事务隔离级别:

  • Read Uncommitted: 禁止多个事务同时修改同一条记录,其它事务能够读取未提交的修改。 隔离级别最低,并发性能最高,会出现脏读,不可重复读和幻读。

  • Read Committed: 禁止多个事务同时修改同一条记录, 修改在提交前其它事务只能读取修改前的版本。不会出现脏读,但会出现不可重复读和幻读。

  • Repeated Read: 禁止多个事务同时修改同一条记录, 事务提交前会锁定全部读取到的行,禁止其它事务修改它正在读取的行。默认隔离级别,不会出现脏读和不可重复读,但会出现幻读。

  • Serializable: 串行化执行,会锁定全部涉及的数据表。能够解决脏读、不可重复读和幻读, 隔离级别最高,并发性能最低。

在实际应用中咱们须要根据须要选择合适的事务隔离级别。

SET TRANSACTION语句能够设置事务隔离级别:

-- SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 设置全部新链接的事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 设置当前链接的事务隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 设置下一个事务的隔离级别

事务并发控制原理

咱们一般有3种思路进行并发控制:

  • 悲观锁: 在事务进行过程当中数据老是处于被锁定状态。
    悲观锁对数据被其它事务修改的可能性持悲观态度(倾向于可能发生), 经常使用于数据争用激烈的情景。咱们一般使用的锁便是悲观锁。

  • 乐观锁: 在事务执行过程当中数据不被锁定, 在事务提交时会对是否发生数据争用进行判断,若未发生冲突则完成提交, 不然回滚事务。
    乐观锁认为数据争用发生的可能性较小, 经常使用于数据争用比较少的情景。CAS原语是乐观锁的一个典型示例。

  • 快照: 全部对数据的修改都是在原有数据上产生了一个新的版本, 对数据的读取是在快照(历史版本)上进行的。写操做产生新的版本不会影响在旧版本执行的读操做。

MySQL默认使用的InnoDB存储引擎使用悲观锁和快照(多版本并发控制, Multi Version Concurrent Control, MVCC)来实现事务的并发控制。

InnoDB采用两阶段锁协议, 即事务分为扩张阶段和收缩阶段, 扩张阶段只容许加锁不能释放锁, 收缩阶段只能释放锁不能加锁。

MVCC

InnoDB提供了全局惟一且有序的事务序列号, 以修改数据的事务序列号做为数据版本号。并为每条记录维护两个版本号: 最近修改版本号, 删除事务号。

以 REPEATABLE READ 隔离级别下 MVCC 机制为例:

  • SELECT: 被检索的行必须同时知足两个条件:

    • 行的修改版本号必须小于或等于当前事务序列号

    • 行的删除版本号为空或者大于当前事务序列号

    当前事务读取到的数据老是事务开始前的版本或事务进行中修改的版本, 更晚开始的事务的修改不会被读取。这种读取方式称为快照读。

  • INSERT: 将当前事务序列号做为修改版本号
  • UPDATE: 插入一行新的记录并使用当前事务序列号做为修改版本号, 并将当前事务序列号做为旧记录的删除事务号(标记为已删除)。
  • DELETE: 将当前事务序列号做为记录的删除事务号(标记为已删除)。

由于快照读不会读取到更晚开始事务的修改, 所以不会产生不可重复读和幻读的问题。

在不一样事务隔离级别下,快照读的一致性是不一样的:

  • READ COMMITTED: 每次SELECT时生成快照。SELECT 能够看到其它已提交事务的修改
  • REPEATABLE READ: 事务开始时生成快照,事务内的更改会修改快照,SELECT 语句看不到其它事务的修改。

GAP 锁

快照读的缺陷在于只能读取事务开始前的版本, 而对于修改操做而言必须读取最新版本。

读取最新版本的需求被InnoDB称为当前读(Locking Read), 使用当前读的语句包括 UPDATE, DELETE 和 SELECT ... IN SHARE MODE, SELECT ... FOR UPDATE。

InnoDB使用锁来解决当前读的问题, InnoDB 中存在三种行级锁:

  • Record Lock: 单条行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包括记录自己
  • Next-Key Lock: Record Lock + Next-Key Lock

用一个示例来讲明GAP锁:

1> START TRANSACTION;
1> DELETE FROM user WHERE age < 18;

在执行 DELETE 语句时 GAP LOCK 锁定了全部 age < 18 的行。咱们在另外一个会话中开始另外一个事务, 此时事务1还没有提交:

2> START TRANSACTION;
2> INSERT INTO user (age) VALUES (17);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

能够看到事务2等待锁超时, 在事务1释放 GAP LOCK 以前不能插入 age < 18 的行, 原有的 age < 18 的行也没法修改。

InnoDB 锁定索引而非锁定数据行, BTREE索引是有序的。GAP LOCK 锁定了索引树中 age < 18 的空间(即索引间的空隙), 被锁定的区间不能插入记录也不能修改已有记录。

在 REPEATABLE READ 隔离级别下不出现幻读是 InnoDB 存储引擎的特性不是 MySQL 的要求, 在使用其它存储引擎时仍可能出现幻读问题。

更多关于读一致性的内容能够参考 InnoDB 官方文档: innodb-consistent-read

相关文章
相关标签/搜索