锁是计算机协调多个进程或线程并发访问某一资源的机制。
Mysql用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 这些锁统称为悲观锁(Pessimistic Lock)。
MySQL锁概述
比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);
BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;
InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
表级锁: 开销小,加锁快; 不会出现死锁; 锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁: 开销大,加锁慢; 会出现死锁; 锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁: 开销和加锁时间界于表锁和行锁之间; 会出现死锁; 锁定粒度界于表锁和行锁之间,并发度一般 。
从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!
仅从锁的角度 来说: 表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用; 而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有 并发查询的应用,如一些在线事务处理(OLTP)系统。
MyISAM表锁
对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求; 对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作; MyISAM表的读操作与写操作之间,以及写操作之间是串行的! 根据如表20-2所示的 例子可以知道,当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。 其他线程的读、写操作都会等待,直到锁被释放为止。
当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。 其他线程的读、写操作都会等待,直到锁被释放为止。

一个session使用LOCK TABLE命令给表film_text加了读锁,这个session可以查询锁定表中的记录,但更新或访问其他表都会提示错误; 同时,另外一个session可以查询表中的记录,但更新就会出现锁等待。

如何加表锁
给MyISAM表显示加锁,一般是为了在一定程度模拟事务操作,实现对某一时间点多个表的一致性读取。 例如, 有一个订单表orders,其中记录有各订单的总金额total,同时还有一个订单明细表order_detail,其中记录有各订单每一产品的金额小计 subtotal,假设我们需要检查这两个表的金额合计是否相符,可能就需要执行如下两条SQL:
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
Lock tables orders read local, order_detail read local;
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
Unlock tables;
1、上面的例子在LOCK TABLES时加了“local”选项,其作用就是在满足MyISAM表并发插入条件的情况下,允许其他用户在表尾并发插入记录,有关MyISAM表的并发插入问题,在后面还会进一步介绍。
(1)对actor表获得读锁:
mysql> lock table actor read;
Query OK, 0 rows affected (0.00 sec)
mysql> select a.first_name,a.last_name,b.first_name,b.last_name
from actor a,actor b
where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom'
and a.last_name <> b.last_name;
mysql> lock table actor as a read,actor as b read;
Query OK, 0 rows affected (0.00 sec)
mysql> select a.first_name,a.last_name,b.first_name,b.last_name
from actor a,actor b where a.first_name = b.first_name
and a.first_name = 'Lisa' and a.last_name = 'Tom'
and a.last_name <> b.last_name;
+————+———–+————+———–+
| first_name | last_name | first_name | last_name |
+————+———–+————+———–+
| Lisa | Tom | LISA | MONROE |
+————+———–+————+———–+
1 row in set (0.00 sec)
查询表级锁争用情况
mysql> show status like 'table%';
Variable_name | Value
Table_locks_immediate | 2979
Table_locks_waited | 0
2 rows in set (0.00 sec))
并发插入(Concurrent Inserts)

可以利用MyISAM存储引擎的并发插入特性,来解决应 用中对同一表查询和插入的锁争用。 例如,将concurrent_insert系统变量设为2,总是允许并发插入; 同时,通过定期在系统空闲时段执行 OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。
MyISAM的锁调度
InnoDB锁
事务是由一组SQL语句组成的逻辑处理单元,事务具有4属性,通常称为事务的ACID属性。
相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。 但并发事务处理也会带来一些问题,主要包括以下几种情况。
一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了! 这种现象叫做“不可重复读”。
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
在并发事务处理带来的问题中,“更新丢失”通常应该是完全避免的。 但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。 从用户的角度,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读? 哪些操作又是当前读呢? 以MySQL InnoDB为例:
select * from table where ?;
下面语句都属于当前读,读取记录的最新版本。 并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。 其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。
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 ?;

获取InonoD行锁争用情况
mysql> show status like 'innodb_row_lock%';

如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
InnoDB的行锁模式及加锁方法

如果一个事务请求的锁模式与当前的锁兼容,InnoDB就请求的锁授予该事务; 反之,如果两者两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X); 对于普通SELECT语句,InnoDB不会加任何锁。
事务可以通过以下语句显式给记录集加共享锁或排他锁:
共享锁(S):
mysql SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
。
排他锁(X):
mysql SELECT * FROM table_name WHERE ... FOR UPDATE
。
用
mysql SELECT ... IN SHARE MODE
获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。
但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT… FOR UPDATE方式获得排他锁。
InnoDB行锁实现方式
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。 下面通过一些实际例子来加以说明。
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb;
Query OK, 0 rows affected (0.15 sec)
mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

在上面的例子中,看起来session_1只给一行加了排他锁,但session_2在请求其他行的排他锁时,却出现了锁等待! 原因就是在没有索引的情况下,InnoDB只能使用表锁。 当我们给其增加一个索引后,InnoDB就只锁定了符合条件的行,如下例所示:
创建tab_with_index表,id字段有普通索引:
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb;
mysql> alter table tab_with_index add index id(id);

(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。 应用设计的时候要注意这一点。
在下面的例子中,表tab_with_index的id字段有索引,name字段没有索引:
mysql> alter table tab_with_index drop index name;
Query OK, 4 rows affected (0.22 sec) Records: 4 Duplicates: 0
Warnings: 0
mysql> insert into tab_with_index values(1,'4');
Query OK, 1 row affected (0.00 sec)
mysql> select * from tab_with_index where id = 1;

InnoDB存储引擎使用相同索引键的阻塞例子.

(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
在下面的例子中,表tab_with_index的id字段有主键索引,name字段有普通索引:
mysql> alter table tab_with_index add index name(name);
Query OK, 5 rows affected (0.23 sec) Records: 5 Duplicates: 0
Warnings: 0

(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决 定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。 因此,在分析锁冲突 时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
比如,在tab_with_index表里的name字段有索引,但是name字段是varchar类型的,检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。 通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点。
mysql> explain select * from tab_with_index where name = 1 \G
mysql> explain select * from tab_with_index where name = '1' \G
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁; 对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。
举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
InnoDB存储引擎的间隙锁阻塞例子

小结
本文重点介绍了MySQL中MyISAM表级锁和InnoDB行级锁的实现特点,并讨论了两种存储引擎经常遇到的锁问题和解决办法。
(1)共享读锁(S)之间是兼容的,但共享读锁(S)与排他写锁(X)之间,以及排他写锁(X)之间是互斥的,也就是说读和写是串行的。
(1)InnoDB的行锁是基于索引实现的,如果不通过索引访问数据,InnoDB会使用表锁。
(2)介绍了InnoDB间隙锁(Next-key)机制,以及InnoDB使用间隙锁的原因。
本文分享自微信公众号 - 程序猿周先森(zhanyue_org)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/4216645/blog/4429443