任何时候,当超过两个查询想要同时修改数据时,并发控制问题就会出现。在MySQL中,这个问题分为两级:服务器级别和存储引擎级别。并发控制是一个很大的话题,在该领域有很多理论和实践,我们在这里只简要介绍MySQL处理读写的方式,帮助你理解本书剩下的知识。
以Unix系统中的邮件为例。传统的mbox文件格式非常简单。mbox邮箱中的所有邮件内容一个接一个地被放在一起。读取和解析这些内容非常简单,邮件的接受也很简单,只需要在文件末尾添加即可。
但是如果两个处理器都想接受邮件,会发生什么呢?很显然,在某些情况下,两个邮件的内容会发生混杂。一个处理良好的邮件系统在这种情况下会使用锁控制写入过程。如果一个邮件在写入过程中,另外一个邮件想要写入,那么必须等待。
这种处理方式看起来很合理,但是他没有并发能力。因为同一时刻只有一封邮件在写入,当因对大容量邮箱时,这将会非常耗时。
读写锁
从邮箱中读取邮件看起来没有什么问题。多个客户端从同一个邮箱同时读取内容,没有任何问题,因为它们没有带来任何变化。但是如果某个人正在删除邮箱,那么会发生什么呢?读取的过程可能会读取得到错误的或者不完整的数据。所以,即使是读取内容,仍然要考虑安全性。
如果你将邮箱当作数据库表格,将每个邮件当作表格中的一行数据,就可以将这两个问题联系起来。在很多情况下,邮箱的作用方式和数据库很相似。修改数据库中某一行的内容,和删除邮箱中的一封邮件,是非常相似的操作。
这类经典的并发问题的解决方案非常简单。处理并发读/写问题的系统通常会实现两种类型的锁,它们通常被称为共享锁和排他锁,也被称为读锁和写锁。
不要太过担心它们的具体行为方式,我们可以将它们的概念描述为:读锁对资源是共享的,或者说非阻塞的,很多客户端可以同时访问资源,而不会打扰其他客户端。另一方面,写锁是排他的,同一时刻只有一个客户端可以写入数据,其他客户端既无法写入数据,也无法读取数据。
在数据库中,锁在各种场景下用到,MySQL要保证,在数据被读取时,没有任何客户端在修改这个数据,通常,这个操作对于用户而言是透明的。
锁粒度
一种提高共享资源并发性能的方式是尽可能缩小锁的范围。在使用锁时,不要锁住整个资源,而要锁住自己需要的资源,也就是自己想要修改的内容。这样,即使对于同一个资源,也可能有多个写锁同时作用,只要它们不相互冲突。
问题在于,锁自身也会消耗很多资源。对锁的每一个操作,包括获得锁,释放锁,检查锁的状态,都会有开销,如果系统耗费太多资源在锁的操作上,那么系统的性能将会降低。
锁策略就是锁开销和数据安全的平衡艺术,这种平衡会影响性能。大多数商业化数据库不会给你太多选择,你会得到行级别的锁,并通过各种方式达到相对较好的性能。
二MySQL提供了一些选择。存储引擎可以根据需求设计不同的锁策略和锁粒度。锁策略在存储引擎的设计中非常重要,修正锁粒度在特定场合下可以极大提高MySQL的性能,但在另外一些场合下性能不好。不管怎样,让我们看看最常用的两种锁策略。
表锁
MySQL中最基本的锁策略是表锁,它的开销是最小的。表锁和之前的邮件系统中的锁类似,它会对整张表加锁,当需要写入数据时(插入、删除、更新等等),会获得写锁。这会导致其他所有写入和读取操作等待。当没有任何写入操作时,读取操作才允许进行,而读取操作不会相互冲突。
有一些表锁的变体可以达到很高的性能。比如,READ LOCAL表锁允许一些并发的写入操作。写锁具有更高的优先级,所以写入操作可以直接插队到操作队列前(但是不能超过上一个写入操作)。
尽管存储引擎有自己的锁,MySQL提供了一些表锁的变体用于一些用途。比如,服务器在进行ALTER TABLE时,不管使用何种存储引擎,都会被加上表锁。
行锁
可以实现更高性能的锁是行锁(虽然也会造成更大的开销)。支持行锁的两个最著名的存储引擎是InnoDB和XtraDB。行锁在存储引擎中实现,而不是服务器中。服务器对与存储引擎中发生的锁操作完全不知情,在后续你将看到,不同的存储引擎使用完全不同的方式实现锁。