锁是计算机协调多个进程或线程访问某一资源的机制。
锁的粒度就是锁的作用范围。数据库中锁的粒度从高到低依次划分为:数据库、表、页、行。
mysql的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。
myisam和memory采用的是表级锁,innodb默认采用的是行级锁,但是也支持表级锁。
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发性最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
如果对死锁不够了解,参考:死锁
表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,比如web应用。而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的用户,比如一些事务处理系统。
先来说说myisam的表级锁。
对myisam表的读操作,不会阻塞其他用户对同一表的读请求,但是会阻塞对同一表的写请求。
对myisam表的写操作,会阻塞其他用户对同一表的读和写操作。
下面来看myisam存储引擎的写阻塞例子:
Session1 | Session2 |
---|---|
获得表film_text的WRITE锁定 mysql> lock table film_text write; Query OK, 0 rows affected (0.00 sec) | |
当前session对锁定表的查询、更新、插入操作都可以执行 :mysql> insert into film_text (film_id,title) values(1003,’Test’); | 其他session对锁定表的查询被阻塞,需要等待锁被释放: |
释放锁:mysql> unlock tables; | 等待 |
Session2获得锁,查询返回 |
这里需要注意一点,用户一般不需要用lock table命令给myisam表显示加锁,因为在执行查询语句前,myisam表会自动给涉及到的所有表加读锁,在更新操作前,会自动给表加写锁。
myisam表在添加表锁的时候,只能访问加锁的这些表,不能访问未加锁的表,它总是一次获得SQL语句所需要的全部锁,这也是myisam表不会出现死锁的原因。
下面看看myisam存储引擎的读阻塞例子:
Session1 | Session2 |
---|---|
获得表film_text的READ锁定 mysql> lock table film_text read; Query OK, 0 rows affected (0.00 sec) | |
当前session可以查询该表记录 | 其他session也可以查询该表的记录 |
当前session不能查询没有锁定的表 | 其他session可以查询或者更新未锁定的表 |
当前session中插入或者更新锁定的表都会提示错误 | 其他session更新锁定表会获得等待锁 |
释放锁 | 等待 |
Session获得锁,更新操作完成 |
上面的读写都是串行的,接下来我们看看并发操作。
这里首先需要了解的一点就是myisam存储引擎有一个系统变量concurrent_insert,用来控制其并发插入的行为,取值为0、1、2。
0代表不允许并发插入。
1代表如果myisam表中没有空洞(即表的中间没有被删除的行),myisam允许在一个进程读表的同时,另一个进程从表尾插入记录。这是mysql的默认设置。
2代表无论myisam表中有无空洞,都运行在表尾并发插入记录。
下面看看myisam存储引擎的并发读写例子:
Session1 | Session2 |
---|---|
获得表film_text的READ LOCAL锁定 | |
当前session不能对锁定表进行更新或者插入操作 | 其他session可以进行插入操作,但是更新会等待 |
当前session不能访问其他session插入的记录 | |
释放锁 | 等待 |
当前session解锁后可以获得其他session插入的记录 | Session2获得锁,更新操作完成 |
我们可以利用myisam存储引擎的并发插入特性,来解决应用中对同一表查询和插入操作。
myisan的读锁和写锁是互斥的,那么如果两个进程分别请求同一个表的读锁和写锁,怎么办呢?答案是写操作优先,因为写操作一般比读操作重要。这就是myisam表不太适合有大量更新操作和查询操作应用的原因,因为大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。
幸运的是,我们可以通过修改默认配置参数来提高读操作的优先级。
innodb和myisam引擎的区别在于其支持事务和行级锁。事务我们在前面已经讲过。
下面我们看看innodb的行锁。innodb实现了两种类型的行锁:共享锁和排它锁。
共享锁:允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
排它锁:允许获得排它锁的事务更新程序,阻止其他事务取得相同数据集的共享读锁和排它写锁。
为了允许行锁和表锁共存,实现多粒度锁机制,innodb还有两种内部使用的意向锁,都属于表锁。
意向共享锁:事务在给一个数据行加共享锁前必须先取得该表的意向共享锁。
意向排它锁:事务在给一个数据行加排它锁前必须先取得该表的意向排它锁。
意向锁是innodb自动加入的,不需要用户干预。对于更新删除插入语句,innodb会自动给涉及的数据集加排它锁。对于查询语句,innodb不会加任何锁。可以通过以下语句显示加锁:
共享锁:
select * from table_name where …… lock in share mode
排它锁:
select * from table_name where …… for update
我们来看看innodb存储引擎的共享锁例子:
Session1 | Session2 |
---|---|
mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; | mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; |
当前session对actor_id=178的记录加share mode 的共享锁 | |
其他session仍然可以查询记录,并也可以对该记录加share mode的共享锁 | |
当前session对锁定的记录进行更新操作,等待锁 | 其他session也对该记录进行更新操作,则会导致死锁退出 |
获得锁后,可以成功更新 |
再看看innodb存储引擎的排它锁例子:
Session1 | Session2 |
---|---|
mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; | mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; |
当前session对actor_id=178的记录加for update的排它锁 | |
其他session可以查询该记录,但是不能对该记录加共享锁,会等待获得锁 | |
当前session可以对锁定的记录进行更新操作,更新后释放锁 | 其他session获得锁,得到其他session提交的记录 |
可以看出,innodb行锁是通过给索引项加锁来实现的,只有通过索引条件检索数据,innodb才使用行级锁,否则innodb将使用表锁。
在实际应用中,这一点必须要注意,否则可能导致大量的锁冲突,从而影响并发性能。
第一个,在不通过索引条件查询的时候,innodb使用的是表锁而不是行锁。
下面看看innodb存储引擎在不使用索引时使用表锁的例子:
Session1 | Session2 |
---|---|
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_no_index where id = 1 ; | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_no_index where id = 2 ; |
mysql> select * from tab_no_index where id = 1 for update; | |
mysql> select * from tab_no_index where id = 2 for update;等待 |
上面的例子中,session1只给一行加了排它锁,而session在请求其他行时却出现了等待情况,原因就是在没有索引的情况下,innodb只能使用表锁。
当我们给id添加索引后,就可以正常了。
下面看看innodb存储引擎在使用索引时使用行锁的例子:
Session1 | Session2 |
---|---|
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_with_index where id = 1 ; | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_with_index where id = 2 ; |
mysql> select * from tab_with_index where id = 1 for update; | |
mysql> select * from tab_with_index where id = 2 for update;OK |
这次就没有出现锁等待的情况了。
第二个,mysql的行锁是针对索引加的锁,不是针对记录加的锁,如果使用相同的索引键访问不同行的记录,也会出现锁冲突。
假设id字段有索引,而name字段没有索引。
我们看看innodb存储引擎使用相同索引键的阻塞例子:
Session1 | Session2 |
---|---|
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> select * from tab_with_index where id = 1 and name = ‘1’ for update; | |
虽然session2访问的是和session1不同的记录,但是因为使用了相同的索引,所以需要等待锁: mysql> select * from tab_with_index where id = 1 and name = ‘4’ for update;等待 |
上面例子中两个session都查询id=1,索引相同,所以出现了锁等待。
第三个,当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外不论是主键索引、唯一索引还是普通索引,innodb都会使用行锁。
我们假设id是主键索引,name是普通索引。
我们来看看innodb存储引擎使用不同索引的阻塞例子:
Session1 | Session2 |
---|---|
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> select * from tab_with_index where id = 1 for update;结果为id=4,name=4 | |
Session2使用name的索引访问记录,因为记录没有被索引,所以可以获得锁:mysql> select * from tab_with_index where name = ‘2’ for update; | |
由于访问的记录已经被session1锁定,所以等待获得锁:mysql> select * from tab_with_index where name = ‘4’ for update;等待 |
innodb是有可能发生死锁的,因为除了单个SQL组成的事务外,锁是逐步获得的。这一点它不像myisam一次性获得所需的全部锁。
我们可以通过设计和SQL调整来减少innodb的锁冲突。
1. 使用较低的隔离级别
隔离级别我在事务的文章中说的。innodb在不同隔离级别下,对sql语句采取的锁的方法不同。比如只有在最高级别SERIALIZABLE中才会将select隐式转换为select …… lock in share mode,也就是添加共享锁。
至于如何调整事务隔离级别可以参考这篇文章
2. 多使用索引访问数据,减少锁冲突的机会
3. 合理控制事务的大小,事务越小发生冲突几率也越小