我们都知道 MySQL 中有各种各样的锁,例如:表锁、间隙锁、意向锁、行锁等等。但你是否想过:为啥 MySQL 要有锁机制的存在,它的存在是为了解决什么问题?今天我们就来聊聊这个问题。
没有锁的串行世界
我们先假设这样一个场景:王五现在账户里没有钱,于是向张三、李四各借 100 元,张三、李四很爽快地答应了。如果数据库这时候是串行的,没有并发执行的线程,那么其转账示意图如下所示。
![36eda6c27f5b50cbd18e9ced2d1d3db7.png](https://i-blog.csdnimg.cn/blog_migrate/9ead68ef1de940fe769ada8f6b84ba98.png)
从上图可以看到:
时间点 1 - 2 的时候,数据库处理了张三的转账请求,读取到王五的账户余额为 0,并将其余额加 100,此时王五账户余额为 100。
时间点 3 - 4 的时候,数据库处理了李四的转账请求,读取到王五的账户余额为 100,并将其余额加 100,此时王五账户余额为 200。
最后,在时间点 5 的时候,王五账户余额为 200 元。
可以看到最终王五的账户余额是 200 元,转账是没问题的。这种数据库访问方式虽然能保证数据一致性,但是每次只能执行一个请求,并发访问性能太差。
没有锁的并行世界
为了提高数据库的并发访问性能,MySQL 其实是支持多线程并发执行的。我们上面的例子,如果使用多线程并发处理,其可能存在的一种情况如下图所示。
![8ed27a67523a15198ce260fa8fc6af46.png](https://i-blog.csdnimg.cn/blog_migrate/97c0f8c4610b1229dc5bbb627fcfd74a.png)
这种情况的处理流程可能是这样的:
在时间点 1 的时候,线程 A 读取到王五的账户余额为 0。
在时间点 2 的时候,线程 B 读取到王五的账户余额为 0。
在时间点 3 的时候,线程 A 将王五账户余额加 100,此时王五账户余额为 100。
在时间点 4 的时候,线程 B 将王五账户余额加 100,此时王五账户余额为 100。
在时间点 5/6 的时候,线程 A、B 都将王五的余额回写回去,王五账户余额为 100。
正常来说,王五最终的账户余额应该是 200 元,但实际上王五账户余额却只有 100 元。通过分析上面的转账示意图,我们会发现问题的关键点在于时间 4。
在这个时间点时,王五的账户余额应该是 100 元了,但是数据库线程 B 还是以为王五的账户余额是 0 元,所以导致了最后的数据不一致。那么如何解决数据不一致的问题呢?答案就是:锁机制。
有锁的并行世界
实际上,对于上述的转账例子,在 InnoDB 中的处理流程如下图所示。
![a8845b3e9a0eb618e542a0dfbc18ddeb.png](https://i-blog.csdnimg.cn/blog_migrate/9f5bf6feb14b4da474f81694f28e0b7e.png)
在时间点 2 的时候,线程 A 读取到王五的账户余额为 0。
在时间点 3 的时候,线程 B 读取到王五的账户余额为 0。
在时间点 4 的时候,线程 A 将王五账户余额加 100,并获取到锁,此时王五账户余额为 100。
在时间点 5 的时候,线程 B 准备将王五账户余额加 100,但此时发现王五账户被锁了,于是阻塞等待。
在时间点 6 的时候,线程 A 提交事务。
在时间点 7 的时候,线程 B 重新读取王五最新的余额为 100 元,并加 100 元,最终在时间点 8 提交事务。
在时间点 4 的时候,数据库线程 A 对王五账号余额加锁,告诉其他线程:我正在更新这条数据,你们其他人不要动。 在时间点 5 的时候,当数据库线程 B 准备将对王五账号余额做加 100 操作时,其发现已经有数据库线程 A 在操作了,所以其将线程阻塞了。
等到数据库线程 A 提交事务,释放锁之后,数据库线程 B 获取到对应的锁。这时候数据库线程 B 发现王五账号的余额是 100 了,所以就在 100 余额的基础上做更新,之后提交事务,最终王五账号的余额就是 200 元。
提示:该例子只是为了粗略说明 InnoDB 是如何通过锁解决数据一致性问题的,在一些细节上大家不必对于纠结。例如这个例子在事务隔离级别为 READ COMMIT 的时候适用,但是在 REPEATABLE READ 隔离级别时存在问题。
看到这里,相信大家已经明白:锁的存在就是为了解决并发访问下数据的不一致问题。而数据库之所以要提供并发访问,是为了提高数据库的运行效率。
上面所说的例子只是众多数据不一致情况的一种,下回我们来聊聊并发访问导致的 5 个问题。
加星标关注,可以及时收到推送哦。ღ(´・ᴗ・`) 比心
推荐阅读