MySQL锁的概述

锁是计算机协调多个进程或线程并发访问某一资源的机制。
MySQL中不同的存储引擎支持不同的锁机制。InnoDB存储引擎支持行级锁和表级锁,默认情况下使用的是行级锁。MyISAM支持表级锁。
表级锁:开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概览极高,并发度最低。
行级锁: 开销大,加锁满,会出现死锁,锁定粒度最小,发生锁冲突的概览最低,并发度也最高。
页面锁: 开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。

MyISAM表锁

MySQL的表级锁有两种模式:表共享读锁,表独占写锁。对MyISAM的读不会阻塞其它用户的读请求,但是会阻塞对同一表的写请求。当一个线程获得了一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其它读写操作都会被阻塞。

如何加表锁

MyISAM在执行select操作之前会自动给涉及到的表加读锁。在执行更新操作(update,insert,delete)前,会自动给涉及到的表添加写锁。因此一般是不需要显式的去加锁的。
一般手动加锁是模拟事务:
比如:
检查两个表的合计金额是否相同,

1
2
3
//这种写法显然可能会出现错误(第一条sql执行之后,可能会有金额变动)
select sum(total) from orders;
select sum(subtotal) from order_detail;

采用加锁来实现模拟事务

1
2
3
4
Loco tables orders read local,order_detail read local;
select sum(total) from orders;
select sum(subtotal) from order_detail;
Unlock tables;

值得注意的是在加锁时有一个Local关键字,其作用是在并发情况下,允许其它用户在表尾并发插入记录。

并发插入

总体而言MyISAN表的读写是串行的,但在一定的条件下,MyISAM表也支持查询和插入操作的并发进行。
MyISAM存储引擎中有一个系统变量concurrent_insert,专门用来控制并发插入的行为,其值分别为:0,1,2

0: 不允许并发插入
1: 如果MyISAM表中没有空洞(即表中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录,这也是默认值。
2: 无论MyISAM表中有无空洞,,都允许在表尾并发插入记录。

MyISAM的锁调度

如果一个进程请求某个MyISAM的读锁同时里一个进程也请求同一个表的写锁,*那么写进程先获得锁。不仅如此,如果读请求先到达等待队列,写请求后到,写锁也会被插入到读锁之前。因为MySQL认为写请求比读请求更加重要。正是如此,当有大量的更新操作时,查询操作很难获得读锁。当然可以通过一些设置来条件MyISAN的调度行为。

InnoDB锁

InnoDB与MyISAN的最大不同在于:一是支持事务,二是采用行级锁。

事务及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元。它具有以下性质:

  1. 原子性:事务是一个原子操作,其对数据的修改,要么全部执行,要么全部都不执行。
  2. 一致性: 在事务的开始和完成时,数据都必须保持一致状态,这意味着所有相关的数据规则都必须应用于事务的修改,以保持其完整性;事务结束后,所有的内部数据结构也都必须都是正确的。
  3. 隔离性: 数据库系统提供一种一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行,这意味着事务处理过程中的中间状态对外部都是不可见的。
  4. 持久性: 事务完成之后,他对数据的修改是永久性的,即使系统出现故障也能保持。

并发事务带来的问题

  1. 更新丢失:当两个或多个事务选择同一行,然后基于最初选定的值进行更新该行时,由于每个事务都不知道其它事务的存在,就会发生丢失更新问题,最后的更新覆盖了其它事务所做的更新。
  2. 脏读:一个事务正在对一条记录做修改,在这个事务提交前,这条记录的数据处于不一致状态,这是,如果另一个事务也来读取同一个记录,如果不加以控制,第二个事务读取了这些“脏”的数据。
  3. 不可重复读:一个事务在读取某些数据时,某些数据已经发生了改变,或某些数据已经被删除。
  4. 幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其它事务插入了满足其条件的新数据。

事务的隔离级别

“更新丢失”是一种应该完全避免的,但不能仅仅依靠数据库,需要引用程序对更新的数据加必要的锁来解决。因此,防止更新丢失应该是应用的责任。

脏读,不可重复读,幻读都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
一般可以采取以下几种方式:

  1. 在读取数据前,对其加锁,阻止其它事务对数据进行修改。
  2. 不加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照。并用这个快照来提供一定级别的一致性读。从用户角度来看,好像是提供了数据的多个版本,因此该技术也较数据多版本并发控制(MVCC,MCC)

在MVCC中,读操作可以分成两类:快照读和当前读。快照读读取的是记录的可见版本,不用加锁。当前读,读取的是记录的最新版本,并且当前读返回的记录都会加锁

哪些属于快照读,哪些属于当前读?

  1. 简单的select操作,属于快照读
1
select * from table where ?
  1. 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁
1
2
3
4
5
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;

事务的隔离级别越严格,并发副作用越小。但付出的代价也越大。因为事务隔离的实质就是在一定程度上让事务“串行化”

四种隔离级别:
nz5pjS.png

InnoDB的行锁模式即加锁方法

共享锁(S)(读锁):允许一个事务去读一行,阻止其它事务获得相同数据集的排他锁(其它事务只能加共享锁,不能加排他锁)。
排他锁(X)(写锁):允许获取排他锁的事务其更新数据,阻止其它事务取得相同的数据集共享读锁和排他写锁。

当一个事务对数据加上排他锁后,其它事务不能获取相同数据集的共享锁和排他锁,但是可以使用select * from .. 进行查询的,因为普通的查询没有任何锁机制,它根本就不需要获得锁,因此排他锁也无法阻止它

为了允许行锁和表锁的共存,InnoDB还引入了意向锁(表锁)。
意向共享锁(IS):事务打算给数据行加共享锁,必须在这之前获得该表的意向共享锁。
意向排他锁(IX):事务打算给数据行加排他锁,必须在这之前获得该表的意向排他锁。

InnoDB行锁模式兼容性列表
ups0q1.png
如果事务请求的锁模式于当前的锁兼容,InnoDB就将请求的锁授予该事务,反之,如果两者不兼容,该事务就要等待锁释放。

意向锁是InnoDB自动加的,不需要人工干预对应更新操作,InnoDB会自动加上排他锁,但对于普通的select语句,InnoDB不会加任何锁

InnoDB行锁的实现方式

  1. 在不通过索引进行查询的时候,InnoDB使用的是表锁,而不是行锁。
  2. MySQL的行锁是针对索引加的锁,不是针对记录加的锁。如果使用相同的索引键仍然会出现锁冲突。
  3. 当表种有多个索引时,不同的事务可以使用不同的索引锁定不同的行。
  4. 即便在sql中使用了索引,但是MySQL的执行计划如果采用的是全表扫描的话,那么仍然将使用表锁

间隙锁(Next-key锁)

当使用范围条件进行检索数据并请求共享和排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值条件在范围内但不存在的记录(间隙)也会加锁。

例如:
//假如emp表中有101条数据,且empid的值分别是1,2,3…100,101

1
select * from emp where empid >100 for update

那么InnoDB不仅会对empid为101的进行加锁,empid 在100之后哪怕不存在的也会加锁。
因此,在实际应用开发中,尤其是并发插入比较多的应用,我们需要尽量优化业务逻辑,进行使用相同条件来更新数据,避免使用范围条件
并且在使用等值条件时,如果记录不存在,也会使用间隙锁