跳转至

事务

ACID

  • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;

  • 一致性: 执行事务前后,数据保持一致;

  • 隔离性: 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;

  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库 发生故障也不应该对其有任何影响。

隔离级别

事务隔离级别 (transaction isolation levels),隔离级别就是对对事务并发控制的等级。

很多 DBMS 定义了不同的 “事务隔离等级” 来控制锁的程度,多数的数据库事务都避免高等级的隔离等级 (如可序列化) 从而减少对系统的锁的开销,高的隔离级别往往会增加死锁发生的几率。

隔离级别配置

InnoDB 默认是可重复读的 (REPEATABLE READ),提供 SQL-92 标准所描述的所有四个事务隔离级别,可以在启动时用 --transaction-isolation 选项设置,也可以配置文件中设置。

$ cat /etc/my.cnf
[mysqld]
transaction-isolation = {READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE}

用户可以用 SET TRANSACTION 语句改变单个会话或者所有新进连接的隔离级别,语法如下:

mysql> SET autocommit=0;
mysql> SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL
       {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

如果使用 GLOBAL 关键字,当然需要 SUPER 权限,则从设置时间点开始创建的所有新连接均采用该默认事务级别,不过原有链接事务隔离级别不变。

可以用下列语句查询全局和会话事务隔离级别。

mysql> SHOW VARIABLES LIKE 'tx_isolation';
mysql> SELECT @@global.tx_isolation;
mysql> SELECT @@session.tx_isolation;
mysql> SELECT @@tx_isolation;

读取异常

在 SQL 92 规范的定义中,规定了四种隔离级别,同时对可能出现的三种现象进行了说明(不包含如下的丢失更新)。

Lost Update

丢失更新,当两个事务读取相同数据,然后都尝试更新原来的数据成新的值,此时,第二个事务可能完全覆盖掉第一个所完成的更新。

丢失更新是唯一一个用户可能在所有情况下都想避免的行为,在 SQL 92 中甚至没有提及。

Dirty Read - 脏读

一个事务中读取到另一个事务未提交的数据。例如,事务 T1 读取到另一个事务 T2 未提交的数据,如果 T2 回滚,则 T1 相当于读取到了一个被认为不可能出现的值。

Non-Repeatable Read - 不可重复读

在一个事务中,当重复读取同一条记录时,发现该记录的结果不同或者已经被删除了;如在事务 T1 中读取了一行,接着 T2 修改或者删除了该行并提交,那么当 T1 尝试读取新的值时,就会发现改行的值已经修改或者被删除。

Phantom Read - 幻读

通常是指在一个事务中,当重复查询一个结果集时,返回的两个不同的结果集,可能是由于另一个事务插入或者删除了一些记录。

例如,事务 T1 读取一个结果集,T2 修改了该结果集中的部分记录 (例如插入一条记录),T1 再次读取时发现与之前的结果不同 (多出来一条记录),就像产生幻觉一样。

不可重复读与幻读的区别:

不可重复读的重点是修改:同样的条件, 你读取过的数据, 再次读取出来发现值不一样了,其只需要锁住满足条件的记录

幻读的重点在于新增或者删除:同样的条件, 第1次和第2次读出来的记录数不一样,要锁住满足条件及其相近的记录

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力,因为解决幻读需要锁表了。

mysql是以乐观锁为理论基础的MVCC(多版本并发控制)来避免不可重复度和幻读的。

隔离级别 脏读 不可重复读取 幻影数据行
READ UNCOMMITTED(RU) - 读未提交 YES YES YES
READ COMMITTED(RC) - 读已提交 NO YES YES
REPEATABLE READ(RR) - 可重复读 NO NO YES
SERIALIZABLE(SZ) - 串行化 NO NO NO

事务超时

与事务超时相关的变量可以参考。

----- 设置锁超时时间,单位为秒,默认50s
mysql> SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set (0.00 sec)

----- 超时后的行为,默认OFF,详见如下介绍
mysql> SHOW VARIABLES LIKE 'innodb_rollback_on_timeout';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| innodb_rollback_on_timeout | OFF   |
+----------------------------+-------+
1 row in set (0.02 sec)

innodb_rollback_on_timeout 变量默认值为 OFF,如果事务因为加锁超时,会回滚上一条语句执行的操作;如果设置 ON,则整个事务都会回滚。

MVCC

MVCC(Multi-Version Concurrency Control)即多版本并发控制,MVCC 是一种并发控制的方法,以乐观锁的方式解决事务中不可重复读和幻读的问题。

MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。MVCC实现了非阻塞的读操作,写操作也只是锁定必要的行。MVCC会保存某个时间点上的数据快照(Snapshot)。这意味着事务可以看到一个一致的数据视图,不管他们需要跑多久。这同时也意味着不同的事务在同一个时间点看到的同一个表的数据可能是不同的。

MySQL的InnoDB存储引擎实现MVCC的策略

InnoDB默认事务隔离级别是可重复读。InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现。这两个列,一个保存了行的创建时间,一个保存了行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来查询到的每行记录的版本号进行比较。

Select操作

InnoDB会根据以下两个条件检查每行记录。

  1. InnoDB只查找版本号早于当前事务版本的数据行,即行的版本号小于或等于事务的系统的版本号,这可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过过的。

  2. 行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除。

INSERT操作

InnoDB为新插入的每一行保存当前系统版本号作为行版本号

DELETE操作

InnoDB为删除的每一行保存当前系统版本号作为行删除标识

UPDATE操作

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识(这只是理论,innoDB实际是通过undo log来备份旧记录的)。

MVCC 只在 REPEATABLE READ 和 READ COMMITTED 两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为 READ UNCOMMITTED 总是读取最新的数据行,而不是符合当前事务版本的数据行。SERIALIZABLE则会对所有读取的行都加锁。

快照读和当前读

快照读

快照读 即:snapshot read,官方叫法是:Consistent Nonlocking Reads,即:一致性非锁定读。只能看到 别的事务生成快照前提交的数据,而不能看到 别的事务生成快照后提交的数据或者未提交的数据。

快照读 是 repeatable-read 和 read-committed 级别下,默认的查询模式,好处是:读不加锁,读写不冲突,这个对于 MySQL 并发访问提升很大。

快照读:读取的是快照版本,也就是历史版本。普通的SELECT就是快照读。就是读取数据的时候会根据一定规则读取事务可见版本的数据(可能是过期的数据),不用加锁

快照读的实现方式:undolog和多版本并发控制MVCC

使用快照读的场景:

  • 单纯的select操作,不包括上述 select … lock in share mode、select … for update

Read Committed隔离级别下快照读:每次select都生成一个快照读

Read Repeatable隔离级别下快照读:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读

在 read-committed 隔离级别下,事务中的快照读,总是以最新的快照为基准进行查询的。

在 repeatable-read 隔离级别下,快照读是以事务开始时的快照为基准进行查询的,如果想以最新的快照为基准进行查询,可以先把事务提交完再进行查询。

在 repeatable-read 隔离级别下,别的事务在你生成快照后进行的删除、更新、新增,快照读是看不到的。

当前读

当前读:读取的是最新版本, 并且对读取的记录加锁,保证其他事务不会再并发的修改这条记录,避免出现安全问题。

使用当前读的场景:

  • select…lock in share mode (共享读锁)
  • select…for update
  • update
  • delete
  • insert

当前读的实现方式:next-key锁(行记录锁+Gap间隙锁):

间隙锁:只有在Read Repeatable、Serializable隔离级别才有,就是锁定范围空间的数据,假设id有3,4,5,锁定id>3的数据,是指的4,5及后面的数字都会被锁定,因为此时如果不锁定没有的数据,例如当加入了新的数据id=6,就会出现幻读,间隙锁避免了幻读。

1.对主键或唯一索引,如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。

2.没有索引的列,当前读操作时,会加全表gap锁,生产环境要注意。

3.非唯一索引列,如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。例如,某表数据如下,非唯一索引2,6,9,9,11,15。如下语句要操作非唯一索引列9的数据,gap锁将会锁定的列是(6,11],该区间内无法插入数据。

存储引擎层面的锁是为了最大程度的支持并发处理,在InnoDB,锁分行锁、Metadata Lock(事务级表锁),行锁的算法共有三种:Record Lock,Gap Lock,Next-Key Lock

Record Lock:单个行记录的上锁 Gap Lock:间歇锁,不包含记录本身的区间锁 Next-Key Lock:包含记录本身的区间锁 只有在RR隔离级别下才会有gap lock,next-key lock,其中 当where条件为 普通索引时为gap lock或者Next-key Lock 当where条件为 主键索引的时候,Next-key Lock 和Gap Lock的锁策略降级为行锁 当where条件 不是索引的时候,innodb会给所有数据上锁,然后返回Mysql server层,然后在Server层过滤掉不符合条件的数据,通过调用 unlock_row方法解锁

总结

  1. 不可重复读分为2部分:1.快照读 2.当前读
  2. 行锁+间隙锁解决了当前读可能会导致的不可重复读的问题
  3. mvcc+undo log解决了快照读可能会导致的不可重复读的问题.
  4. mysql的锁和mvcc的设计不单单解决了不可重复读的问题,也解决了幻读的问题

事务日志

参考资料