MySQL 并发控制
前一节已经说过了,MySQL是多线程应用,并且共享存储数据,很显然,当两个及以上线程对同一块数据进行写将会发生数据不一致等各种问题,比如,同时对一个表增加一条记录,后一个增加的记录可能会覆盖前一条,造成数据丢失。若仅仅是读不会发生错误,但是当读写一同,就有可能发生读错误,所以,对读也是需要必要的控制。
关于数据读写错误的会有哪几种情况,可以参考:事务隔离级别。
以上问题就需要并发控制来解决,所谓的并发,就是每一次只允许一个线程对某一块数据(可以是某个数据库,或某张表,或表里某条记录)写,实现并发控制有多种方式,MySQL采用的是锁以及MVCC(多版本控制)。
(在我看来,同步和并发实质上是一个概念,并发只是更小更细粒度的同步。)
MySQL高性能一书中提到:MySQL提供两个层面的并发控制:服务器级和存储引擎级别。但并没有具体解释这两个是什么意思,查到的资料也很少说明,虽然从字面上能大体懂得是什么。
服务器级:由MySQL(具体应该是SQL处理层)实现,对整个MySQL服务器(全局锁,实际上通过表锁实现)实现并发控制。
存储引擎级:由各个存储引擎实现,可以是表锁,也可以是行级锁。
(用户也可以通过SQL命令进行手动加锁)
锁
读写锁
MySQL提供了两种锁实现并发控制:读锁和写锁。读锁是共享的,也叫共享锁(也叫S锁),相互不会阻塞,多个读锁(多个线程用户)可以同一时刻读取统一资源;写锁则是排他的,也叫排他锁(也叫X锁),同一时间一个资源只能有一个写锁,也就是说,写锁会阻塞其他写锁和读锁。即读锁上面可以加读锁,但不能加写锁,而写锁则不能加任何锁。
上面说的读写锁的“读写”不是针对操作,而是针对数据,锁的是数据,每次操作数据先判断该数据是否加锁,加了什么锁,然后以此判断是否允许本次操作执行,但这样是不是觉得很麻烦?很耗性能?所以才有了数据库事务隔离级别,统一设置一个隔离级别,数据库系统会根据隔离级别隐式的给数据加锁,然后根据这个级别来判断本次操作执行权限,关于事务隔离级别详见:事务隔离级别。
乐观悲观锁
注意,不管是乐观锁,还是悲观锁,都是一种思想,而不是像读锁、写锁一样,是一种具体实现方式。
悲观锁的思想:操作前,悲观地认为所操作数据在操作期间会被其他事务修改,所以,在操作前我要先给我操作的数据加锁才放心。至于加的是读锁还是写锁则看具体应用场景。
乐观锁的思想:操作前,乐观地认为所操作数据在操作期间不会被其他事务修改,我不加锁(别的事务操作期间就可对该数据操作),只在最后更新的时候(如果操作是更新的话)查看原始数据是否被修改,如果没修改,更新数据,否则失败。至于如何知道原始数据被修改,这就是涉及到具体实现方式了,最常用的就是MVCC(乐观锁可以通过MVCC实现,但不是说,MVCC是乐观锁的一种,MVCC也可以采用悲观锁实现,二者是平行关系),还有CAS。
两种思想并没有优劣,而是要看具体应用,乐观锁适用于读多写少的应用,因为减少了锁的开销;悲观锁适用于频繁写操作的应用(写操作多的话,对于乐观锁会带来频繁的操作失败,从而重新进行写操作,耗费性能)。
锁的粒度
理论上,尽量锁定需要修改的部分,而不是所有的数据,锁定的数据单元越小,系统的并发控制度越高,比如行级锁,修改的时候只锁定这一行记录,这个时候其他线程对该表的其他记录修改不影响。但是,加锁也是需要消耗资源的,锁的各种操作:获得锁、检查锁状态、释放锁等都会增加开销,越细粒度的锁开销越大,过多的所操作所带来的是性能急剧下降。
所以我们要采用一种锁策略来平衡并发度和系统性能,MySQL由于存储引擎的插件式,每个存储引擎可以实现自己的锁策略,所以不需要通用的锁策略,只需要在相应应用场景下选择相应的存储引擎即可。
下面介绍两种常用的锁策略:表锁和行级锁。
表锁
MySQL最基本的锁策略,顾名思义,对整张表加读锁和写锁。一般由MySQL服务器层实现,如果这个时候存储引擎层还有锁,优先表锁。
行级锁
更大细粒度的锁,只锁住一行记录,即对不同行记录可并发操作。行级锁只由存储引擎层实现,MySQL服务器层没有实现。
MVCC 多版本控制
事务性存储引擎(支持事务)一般不仅仅使用锁来实现并发控制,因为前面提到过,表锁范围太大,并发度不高,而行级锁范围太小,锁操作开销大,对那些读多写少的应用很不友好,所以才有了MVCC。
所谓的MVCC,即多版本控制,简单地说就是写操作前后是两个版本,从而实现并发控制,至于具体实现如何,详情参考:MVCC。