在并发情况下,为了保证数据的一致完整性,我们需要对数据库进行锁操作,锁定机制的选择直接影响到数据库的并发能力和性能,所以在选择锁定机制的时候一定要谨慎。
mysql主要使用了三种类型的锁定机制,按照颗粒度从小到大排序为:行级锁定、页级锁定和表级锁定,随着颗粒度的增加,并发能力降低,消耗也降低,发生死锁的概率也降低。
本文主要通过MyISAM和Innodb两种存储引擎来讲解表级锁定和行级锁定。
一. 表级锁定
表级锁定是两种存储引擎都会使用到的锁定机制,MyISAM只会使用表级锁定,Innodb预设使用行级锁定,但在搜索条件中没有指定索引的话,Innodb会使用表级锁定。
表级锁定主要有读锁定和写锁定两种类型,主要通过四个队列来维护这两种锁定,两个队列来维护正在锁定中的读和写锁定的信息,另外两个队列维护等待中的读写锁定的信息,四个队列如下:
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的锁定机制还会有其它一些问题:
死锁
当产生死锁时,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执行时间
合理利用读写锁定优先级,默认情况下写锁定优先级是高于读锁定的,但相互间的优先级是可以设置的,通过参数low_prioroty_updates=1。比如当有大量查询操作时,我们就可以通过设置优先级让查询先进行。
2. Innodb行级锁定
虽然Innodb使用的是行级锁定,并发能力比MyIsam强,但使用不当的操作能导致其性能下降甚至不如MyIsam。
由于Innodb 的行级锁定和事务性,所以肯定会产生死锁,下面是一些比较常用的减少死锁产生概率的的建议:
可以通过Table_locks_immediate 和 Table_locks_waited 两个状态变量来查看表级锁定的情况,前者表示产生表级锁定的次数,后者表示表级锁定争用而发生等待的次数。可以通过这两个值来衡量系统的表级锁定性能。
对于Innodb可以通过命令:show status like 'innodb_row_lock%',来查看相关的状态变量,变量名通俗易懂,就不一一说明了。