MySQL 锁机制存在的价值是什么?

我们都知道 MySQL 中有各种各样的锁,例如:表锁、间隙锁、意向锁、行锁等等。但你是否想过:为啥 MySQL 要有锁机制的存在,它的存在是为了解决什么问题?今天我们就来聊聊这个问题。

没有锁的串行世界

我们先假设这样一个场景:王五现在账户里没有钱,于是向张三、李四各借 100 元,张三、李四很爽快地答应了。如果数据库这时候是串行的,没有并发执行的线程,那么其转账示意图如下所示。

王五借款串行执行 - 示意图

从上图可以看到:

  • 时间点 1 - 2 的时候,数据库处理了张三的转账请求,读取到王五的账户余额为 0,并将其余额加 100,此时王五账户余额为 100。
  • 时间点 3 - 4 的时候,数据库处理了李四的转账请求,读取到王五的账户余额为 100,并将其余额加 100,此时王五账户余额为 200。
  • 最后,在时间点 5 的时候,王五账户余额为 200 元。

可以看到最终王五的账户余额是 200 元,转账是没问题的。这种数据库访问方式虽然能保证数据一致性,但是每次只能执行一个请求,并发访问性能太差。

没有锁的并行世界

为了提高数据库的并发访问性能,MySQL 其实是支持多线程并发执行的。我们上面的例子,如果使用多线程并发处理,其可能存在的一种情况如下图所示。

这种情况的处理流程可能是这样的:

  • 在时间点 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 中的处理流程如下图所示。

  • 在时间点 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 隔离级别时存在问题。

看到这里,相信大家已经明白:锁的存在就是为了解决并发访问下数据的不一致问题。而数据库之所以要提供并发访问,是为了提高数据库的运行效率。

 
友情链接
鄂ICP备19019357号-22