1. 概述
工作中使用 mysql 比较多,mysql 之所以在业内具有如此崇高的地位,与他严密的加解锁逻辑也是分不开的。 本文进行了一番总结。
2. 两阶段锁协议
Innodb 使用的是两阶段锁协议,指的是将整个事务分成两个阶段,前一个阶段为加锁阶段,后一个阶段为解锁阶段。 在加锁阶段,事务只能加锁和操作数据,不能解锁。 一旦事务释放了一个锁,那么事务就进入解锁阶段,在解锁阶段,除了操作数据外,只能解锁,不能加锁。 两阶段锁协议使得事务有较高的并发度,但是并没有解决死锁问题,如果两个事务分别申请了 A、B 两把锁,接着有申请对方的锁,就会进入死锁状态。 Innodb 只有在 commit 或 rollback 时才会同时释放所有的锁。
3. 显式锁 — select … lock in share mode & for update
Innodb 除了上文所说的隐式锁,还支持在 select 语句中显式锁定某行:
- select … lock in share mode;
- select … for update;
LOCK IN SHARE MODE 锁定当前查询的行,不允许其他事务对行进行写操作,但其他事务可以进行读操作。 FOR UPDATE 锁定行,阻止其他事物对该行的任何读写操作。
4. MVCC
mysql 的事务性存储引擎大多使用一种用来增加并发性的加锁机制 — 多版本并发控制(MVCC),在 Oracle、PostgreSQL 及其他一些数据库系统中同样使用该机制实现锁机制,所以也称为乐观锁。 MVCC 在许多情况下避免了使用锁,同时可以提供更小的开销,根据实现的不同,他可以允许非阻塞式读。 MVCC 会保存某个时间点上的数据快照,以保证无论事务需要跑多久,他都将看到一个一致的数据视图,而这也意味着,不同的事务在相同的时间可能看到同一个表的数据是不同的。 并发控制分为乐观的并发控制和悲观的并发控制,每个存储引擎的实现不同。
InnoDB 通过为每一行记录添加两个额外的隐藏值来实现 MVCC,这两个值一个记录这行数据何时被创建时的系统版本号,一个记录这行数据何时被删除时的系统版本号,每个事务在开始的时候都会记录他自己的系统版本号,每个查询必须去检查每行数据的版本号与事务的版本号是否相同。 只有事务版本号小于等于记录的删除版本号并且大于等于记录的创建版本号的记录才会被事务查询到。
这样,对数据库行的增加、删除和更新根本不需要加锁。
- 插入数据 — 为这个新行记录当前的系统版本号
- 删除数据 — 将当前系统版本号写入这一行的删除版本号
- 更新数据 — 创建一个数据的新拷贝,并将新行的创建系统版本号和旧行的删除版本号都设置为当前版本号
只有在 commit 的时候才会发生真正意义的删除。
5. 快照读和当前读
1. 快照读 — 简单的 select 操作,不加锁 2. 当前读 — 特殊的读操作,如插入、更新、删除等操作,属于当前读,需要加锁
6. MySQL 的隔离级别
InnoDB 定义了以下四种隔离级别: 1. Read Uncommitted(读取未提交内容) — 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read) 2. Read Committed(读取提交内容) — 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果,同时,也存在幻读的问题 3. Repeatable Read(可重读) — 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题 4. Serializable(可串行化) — 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争,所以不建议使用
7. 事务隔离可能引起的问题
上述的四种隔离级别可能引起下面的问题: 1. 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的 2. 不可重复读(Non-repeatable read) — 在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。 3. 幻读(Phantom Read) — 在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的
四种隔离级别可能引起的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(Read Uncommitted) | Y | Y | Y |
读已提交(Read Committed) | X | Y | Y |
可重复读(Repeatable Read) | X | X | X |
可串行化(Serializable) | X | X | X |
8. mysql 修改事务隔离级别
用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。
代码语言:javascript复制SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL
{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
默认的行为(不带session和global)是为下一个(未开始)事务设置隔离级别。 如果你使用GLOBAL关键字,语句在全局对从那点开始创建的所有新连接(除了不存在的连接)设置默认事务级别,需要SUPER权限。 使用SESSION 关键字为将来在当前连接上执行的事务设置默认事务级别。
任何客户端都能自由改变会话隔离级别(甚至在事务的中间),或者为下一个事务设置隔离级别。
9. 查询全局和会话事务隔离级别
可以用下列语句查询全局和会话事务隔离级别:
代码语言:javascript复制SELECT @@global.tx_isolation;
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;
10. 实例
10.1. mysql 默认事务隔离等级
由图可见,mysql 默认是使用 RR 的隔离方式执行的。
我们创建了一个表用于测试,并插入了5条测试数据:
10.2. Read Uncommitted
首先我们将当前 session 的隔离级别设置为 READ UNCOMMITTED,并开启一个新的事务,然后执行一次查询,可以看到查询到了原始的数据。
这时,我们打开一个新的终端,同样开启一个新的事务,并执行一条 update 语句更新数据。
接下来,我们切换到原来的终端,重新执行查询:
我们发现数据发生了变化,然而执行 update 语句的终端并没有提交事务,我们看到出现了脏堵现象。
10.3. Read Committed
与上面一样,我们首先将当前 session 的隔离级别设置为 READ COMMITTED,然后开启一个新事务,并执行一次查询,可以看到查询到了原始数据。
然后,在另一个终端中,我们同样开启一个新事务并执行一条 update 语句更新数据。
在更新事务尚未提交时,我们回到开始的终端,重新执行查询。
可以看到,脏读问题已经不存在了。
那么,接下来,我们在提交另一个终端中的更新事务,并回到开始的终端中重新执行查询:
我们看到,开始的终端里查询到的数据发生了变化,出现了不可重复读的问题。
同时我们在另一终端中开启新的事务,插入一条数据并提交事务,然后我们再在原来的终端中查询数据。
我们看到,原来的终端的查询结果看到了新增的数据,出现了幻读问题。
10.4. Repeatable Read
同样的,我们切换到 REPEATABLE READ 隔离方式,然后查询原始数据:
此时,我们在另一个终端中执行插入并提交:
然后回到开始的终端查询:
可以看到,幻读的问题已经被解决了。
10.5. Serializable
串行化的隔离方式会明显降低事务的处理效率,因此不建议使用,由于他保证事务的严格传行执行,所以可以保证上述问题的避免,这里我们不做实践了。