MySQL学习总结系列
锁机制是为了解决数据库的并发控制问题而产生的。如在同一时刻,客户端对同一个表做更新或查询操作,为了保证数据的一致性,必须对并发操作进行控制。同时,在事务特性中,锁机制也为实现 MySQL 的各个隔离级别提供了保证。
锁的分类
广义上的锁分为乐观锁和悲观锁,它是一种概念不同的软件有各自的实现方式。在MySQL中乐观锁:被访问的数据不会被别人修改,因此先不加锁。如果在操作过程中发现数据被修改,则采用cas重新获取数据进行更新。悲观锁:被访问的数据随时都有可能被修改,因此访问数据之前,先对数据加锁。
MySQL中根据数据的占用粒度又分为共享锁、排它锁和意向锁。其中共享锁:允许事务读取一行数据,阻止其他事务对数据进行修改。排它锁:允许事务去读取或者修改一行记录,阻止其他事务对数据进行查询或者修改。意向锁:先进行表锁,成功后再进行行锁。意向锁之间相互兼容不会产生阻塞。为了实现表锁和行锁的共存,实现多粒度锁机制。
不同锁之间的兼容/互斥关系
MySQL中根据应用粒度不同还分为行锁、页锁和表锁。其中行锁基于索引实现,其锁定的粒度最小,因此对应的并发也更高,但上锁过程的判断较为复杂,上锁和释放过程都占用较多系统资源,容易产生死锁,代表引擎innodb。表锁与行锁的优缺点基本相反,表锁是针对整个表进行锁定的,其实现简单执行速度快,但由于锁的粒度较大导致并行度较低,代表引擎mylsam。页锁比较少见,顾名思义它是基于数据页进行上锁的,其特性基于上述两者之间,代表引擎BDB。
从MySQL的实现进行分类锁包括以下几种:
自增长锁:对于自增长id进行插入时如采用表级锁则影响性能;mysql提供了auto_lock_model来处理自增长问题。如果设置为0(插入完成后释放,即便事务未提交也释放锁);1(判断自增长需要使用的数字后立即释放锁,事务回滚会导致不连续),2 直接分配,不使用锁,性能较好,但主键可能不连续。它是一种特殊的表级锁,特定应用在事务插入aotu_increatment类型的列。它能够保障同一个事务插入数据的顺序性;如果是非自增列,则使用插入意向锁(gaplock的一种),在插入位置不冲突的情况下它并不会阻塞彼此。
外键锁:当插入和更新子表时,首先检查父表记录,这可能对数据的检索造成阻塞,不建议增加外键。
Record lock:单条记录上的锁。
Gap lock:间隙锁,锁定一个范围但不包括记录本身。
Nextkey lock:临键锁,record lock+gap lock 锁定一个范围并锁定数据本身。
间隙锁和临键锁都只在RR事务隔离级别下有效,临键锁能够在可重复读的事务隔离级别下协助解决幻读问题,避免串行化重量级锁对性能的影响。
死锁
数据库是一个多用户使用的共享资源,当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。当两个事务需要一组有冲突的锁,而不能将事务继续下去的话,就会出现死锁。产生死锁需要满足以下四个条件同时成立:
互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
MySQL中导致死锁的主要有以下情况
1.事务之间对资源访问顺序的交替
A线程锁定a资源且访问b资源,B线程锁定b资源且访问a资源。两个线程在争用锁的过程中彼此等待导致死锁。
2.并发修改同一个记录
两个线程并发的修改同一个记录,起先两个线程都获取的是共享锁(读数据),在执行修改时由共享锁升级为排它锁(这个过程需要一定时间),线程A先尝试获取排它锁,发现线程B的共享锁没有释放则进行等待。对于线程B而言也是同样的逻辑,两个线程互相等待导致死锁。(网上的有些说法有问题,其实B线程并没有拿到排它锁,我个人理解这个比较正确)
3.索引使用不当导致的全表扫描
索引使用不当导致执行了全表扫描,一方面降低了事务的执行速度,使得数据库越来越慢,从时间维度提高了死锁发生的概率;另一个方面将行锁升级为表锁,从锁粒度方面增加了死锁概率。
死锁的避免
从死锁的必要条件考虑避免死锁的发生
1.尽量避免并发的执行涉及到修改数据的语句。
2.预先规定一个加锁顺序,所有的事务都必须按照这个顺序对数据执行封锁。如不同的过程在事务内部对对象的更新执行顺序应尽量保证一致。
3.尽可能缩短事务的隔离级别、锁粒度、锁持有时间。