mysql锁定机制研究

在并发情况下,为了保证数据的一致完整性,我们需要对数据库进行锁操作,锁定机制的选择直接影响到数据库的并发能力和性能,所以在选择锁定机制的时候一定要谨慎。

mysql主要使用了三种类型的锁定机制,按照颗粒度从小到大排序为:行级锁定、页级锁定和表级锁定,随着颗粒度的增加,并发能力降低,消耗也降低,发生死锁的概率也降低。

本文主要通过MyISAM和Innodb两种存储引擎来讲解表级锁定和行级锁定。

一. 表级锁定

表级锁定是两种存储引擎都会使用到的锁定机制,MyISAM只会使用表级锁定,Innodb预设使用行级锁定,但在搜索条件中没有指定索引的话,Innodb会使用表级锁定。

表级锁定主要有读锁定和写锁定两种类型,主要通过四个队列来维护这两种锁定,两个队列来维护正在锁定中的读和写锁定的信息,另外两个队列维护等待中的读写锁定的信息,四个队列如下:

  • Current read-lock queue,即当前持有读锁的所有线程的队列,队列中的顺序按照获取到锁的时间顺序存放
  • Pending read-lock queue,即当前正在等待读锁定资源的所有线程的队列,队列中的顺序也是按照时间顺序存放
  • Current write-lock queue,同Current read-lock queue类似
  • Pending write-lock queue,同Pending read-lock queue类似

mysql除了读写锁定之外还有其它9种锁定,本文只要讲解读写锁定。

读锁定

一个线程在请求获取读锁定资源的时候,需要满足两个条件:

1. 请求锁定的资源当前没有被写锁定;

2. 写锁定等待队列(Pending write-lock queue)中没有更高优先级的写锁定等待;

如果上述两个条件都得到满足,则该请求的相关信息会存入Current read-lock queue中,否则,会被存入Pending read-lock queue中等待。

写锁定

一个线程在请求获取写锁定资源的时候,需要满足三个条件:

1. Current write-lock queue中没有对相同资源的锁定信息存在;

2. Pending write-lock queue中也没有对相同资源的等待锁定信息存在;

3. Current read-lock queue中对相同资源的锁定信息存在。

只有满足上述三个条件时,才能获取写锁定资源,否则,进入Pending write-lock queue队列。

上述情况通俗一点来说就是:在没有特别设置优先级的情况下,如果一个资源被读锁定,则不能阻塞接下来的读锁定,但能阻塞写锁定;如果一个资源被写锁定,则读写锁定都被阻塞;除了写锁定之外,Pending write-lock queue 中的其它任何写锁定都比读锁定的优先级低。

二. 行级锁

行级锁定不是mysql自己实现的锁定方式,是由其它存储引擎实现的,比如经常使用的Innodb。

主要分析Innodb的锁定特性。

Innodb主要有共享锁(读锁)、排他锁(写锁)和意向共享锁、意向排他锁,前两个用于行级锁定,后两个用于表级锁定。意向锁不是一种真正意味上的锁,它只是表示要对某行记录进行操作,所以意向锁之间不会产生冲突,只有对行加锁的时候才会有冲突。

它们之间的关系为:

共享锁 排他锁 意向共享锁 意向排他锁
共享锁 兼容 冲突 兼容 冲突
排他锁 冲突 冲突 冲突 冲突
意向共享锁 兼容 冲突 兼容 兼容
意向排他锁 冲突 冲突 兼容 兼容

Innodb是通过在指向数据记录的第一个索引键之前和最后一个索引键之后的空域空间上标记锁定信息而实现锁定的,这种锁定实现方式被称为间隙锁(next-key locking)。如果query执行过程是通过范围查找的话,便会锁定整个范围内所有的索引键值,即使这个键值并不存在,所以这会造成在锁定的时候无法对锁定键值范围的数据进行插入。

Innodb的锁定机制还会有其它一些问题:

  • 当query无法利用索引的时候,Innodb会放弃使用行级锁定而改用表级锁定,造成并发降低;
  • 当query使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所关联数据可能有部分并不属于该query的结果集的行列,但是也会被锁定,因为间隙锁锁定的死一个范围,而不是具体的索引键;
  • 当Query 在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会被锁定

死锁

当产生死锁时,Innodb会选择两个事务中较小的事务回滚。事务的大小是根据事务中插入、更新或者删除的数据量来判断的。也就是说当死锁产生时,影响记录数更多的事务将会完成。

Innodb锁定机制实例

create table test(a int(11),b varchar(16))engine=innodb;
create index idx_test_a on test(a);

开两个session都设置set autocommit=off;

1. 基本行锁

时刻 session A session B
1 update test set b = 'b1' where a=1; (更新但不提交)
2 update test set b ='b2' where a=1;(被阻塞,等待)
3 commit;
4 阻塞解除,更新被提交

2. 无索引造成行级锁变成表级锁

1 update test set b='b3' where b='b2';
2 update test set b='b4' where b='b3';(被阻塞,等待)
3 commit;
4 阻塞解除,更新完成

3. 间隙锁

1 update test set b='b4 where a<4 and a>1;'
2 insert into test values(2,'b2');(被阻塞)
3 commit;
4 阻塞解除,插入成功

4.使用共同索引不同数据阻塞

1 update test set b='b4 where a=1 and b='b2';
2 update test set b='b4 where a=1 and b='b3';(被阻塞)
3 commit;
4 阻塞解除,更新成功

5. 死锁

1 update test set b='b4 where a=1;
2 update test set b='b5' where a=2;
3 update test set b='b6' where a=2;(被阻塞)
4 update test set b='b7 where a=1;(被阻塞)

 

for update锁定

项目里经常使用for update来进行锁定,它是一种排他锁,在事务中使用,下面用一个简单的商品购买例子来演示怎样使用for update:

$conn = mysql_connect('localhost','root','root') or die ("数据连接错误!!!");
mysql_select_db('test',$conn);
mysql_query("BEGIN");	\\事务开始
$res = mysql_query("select cnt from product where id = $pid for update");    \\取出商品数量
...  \\一些逻辑代码,主要检查商品数量是否足够
mysql_query("update product set cnt=cnt+$quantity where id = $pid");	\\更新数量   
mysql_query('commit');	\\结束事务


三. 优化锁定机制

1.MyIsam表级锁定

因为MyIsam的锁定机制只有表级锁定,不能更改它的级别,所以优化的手段就是锁时间变短,能并发的操作尽可能并发执行。

缩短锁定时间,即缩短query执行时间

  • 将大而复杂的query分成几个小query去执行;
  • 让query执行更快;
  • 被锁定的表尽可能小;
  • 优化MyIsam表数据文件

合理利用读写锁定优先级,默认情况下写锁定优先级是高于读锁定的,但相互间的优先级是可以设置的,通过参数low_prioroty_updates=1。比如当有大量查询操作时,我们就可以通过设置优先级让查询先进行。

2. Innodb行级锁定

虽然Innodb使用的是行级锁定,并发能力比MyIsam强,但使用不当的操作能导致其性能下降甚至不如MyIsam。

  • 尽可能让所有的数据查询都通过索引来完成,这样可以避免行级锁定升级为表级锁定;
  • 尽可能减少基于范围的数据检索过滤条件,避免间隙锁造成范围锁定;
  • 尽量控制事务的大小,减少锁定的资源量和时间;
  • 在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少MySQL 因为实现事务隔离级别所带来的附加成本;

由于Innodb 的行级锁定和事务性,所以肯定会产生死锁,下面是一些比较常用的减少死锁产生概率的的建议:

  • 类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁;
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
  • 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;

可以通过Table_locks_immediate 和 Table_locks_waited 两个状态变量来查看表级锁定的情况,前者表示产生表级锁定的次数,后者表示表级锁定争用而发生等待的次数。可以通过这两个值来衡量系统的表级锁定性能。

对于Innodb可以通过命令:show status like 'innodb_row_lock%',来查看相关的状态变量,变量名通俗易懂,就不一一说明了。

你可能感兴趣的:(mysql锁定机制研究)