从 select ... for update来分析mysql的锁

时间:2022-01-15 14:26:06

一 mysql的悲观锁 - 以行锁做示例

每次拿数据的时候都认为别的线程会修改数据,所以每次拿数据的时候都会给数据上锁。上锁之后,当别的线程想要拿数据时,就会阻塞。直到给数据上锁的线程将事务提交或者回滚。传统的关系数据库里面很多用了这种锁机制,比如行锁,表锁,共享锁,排他锁等,都是在做操作之前先上锁。

下面的图从网上粘的,用mysql的两个视窗演示一下行锁(左边先执行)

从 select ... for update来分析mysql的锁

 

(1) 左边的线程,在事务中通过select for update语句给sid=1的数据行上了锁,右边的线程此时可以使用select语句读取数据,但是如果也使用select for update语句就会阻塞,使用update, add, delete也会阻塞。 而当左边的线程将事务提交(或者回滚),右边的线程就会获取锁,线程不再阻塞

 

 

从 select ... for update来分析mysql的锁

(2) 此时右边的线程获取锁,左边的线程执行此类操作,也会被阻塞。

 

 

从 select ... for update来分析mysql的锁

(3) 当然,select都不行,update等写操作也要阻塞等待。for update是排他锁

 

 

二 mysql的锁粒度分类

1 行级锁

(1) 描述

行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁

(2)特点

开销大,加锁慢,会出现死锁。发生锁冲突的概率最低,并发度也最高。

 

2 表级锁

(1) 描述

表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)

(2)特点

开销小,加锁快,不会出现死锁。发生锁冲突的概率最高,并发度也最低。

 

3 页级锁

(1) 描述

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。

(2)特点

开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

 

 

 

三 锁级别分类 - 共享锁 & 排他锁 & 意向锁

1 共享锁(Share Lock)

共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

用法

SELECT ... LOCK IN SHARE MODE;

在查询语句后面增加LOCK IN SHARE MODE,MySQL 就会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。

2 排他锁(Exclusive Lock)

排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的*。获准排他锁的事务既能读数据,又能修改数据。

用法

SELECT ... FOR UPDATE;

在查询语句后面增加FOR UPDATE,MySQL 就会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

3 意向锁(Intention Lock)

意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB 中的两个表锁:

  • 意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁;
  • 意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。

意向锁是 InnoDB 自动加的,不需要用户干预。

对于INSERTUPDATEDELETE,InnoDB 会自动给涉及的数据加排他锁;对于一般的SELECT语句,InnoDB 不会加任何锁,事务可以通过以下语句显式加共享锁或排他锁。

共享锁:SELECT ... LOCK IN SHARE MODE;

排他锁:SELECT ... FOR UPDATE;

 

 

四 InnoDB中的行锁,表锁,间隙锁的定性分析

1 客观锁 vs 悲观锁场景分析

假设有一张商品表 goods,它包含 id,商品名称,库存量三个字段,表结构如下:

CREATE TABLE `goods` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_name` (`name`) USING HASH
) ENGINE=InnoDB 

插入如下数据:

INSERT INTO `goods` VALUES ('1', 'prod11', '1000');
INSERT INTO `goods` VALUES ('2', 'prod12', '1000');
INSERT INTO `goods` VALUES ('3', 'prod13', '1000');
INSERT INTO `goods` VALUES ('4', 'prod14', '1000');
...

假设有A、B两个用户同时各购买一件 id=1 的商品,用户A获取到的库存量为 1000,用户B获取到的库存量也为 1000,用户A完成购买后修改该商品的库存量为 999,用户B完成购买后修改该商品的库存量为 999,此时库存量数据产生了不一致。

有两种解决方案:

(1) 悲观锁方案:每次获取商品时,对该商品加排他锁。也就是在用户A获取获取 id=1 的商品信息时对该行记录加锁,期间其他用户阻塞等待访问该记录。悲观锁适合写入频繁的场景。

begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;

(2) 乐观锁方案:每次获取商品时,不对该商品加锁。在更新数据的时候需要比较程序中的库存量与数据库中的库存量是否相等,如果相等则进行更新,反之程序重新获取库存量,再次进行比较,直到两个库存量的数值相等才进行数据更新。乐观锁适合读取频繁的场景。

#不加锁获取 id=1 的商品对象
select * from goods where id = 1

begin;
#更新 stock 值,这里需要注意 where 条件 “stock = cur_stock”,只有程序中获取到的库存量与数据库中的库存量相等才执行更新
update goods set stock = stock - 1 where id = 1 and stock = cur_stock;
commit;

如果我们需要设计一个商城系统,该选择以上的哪种方案呢?

查询商品的频率比下单支付的频次高,基于以上可能会优先考虑第二种方案(这里只考虑以上两种方案的情况下)。

 

2 行锁定性分析

由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,MySQL才会执行Row lock  ,否则MySQL将会执行Table Lock. 

1、只根据主键进行查询,并且查询到数据,主键字段产生行锁。

begin;
select * from goods where id = 1 for update;
commit;

 

2、只根据主键进行查询,没有查询到数据,不产生锁。

begin;
select * from goods where id = 1 for update;
commit;

 

3、根据主键、非主键索引(name)进行查询,并且查询到数据,主键字段产生行锁,name字段产生行锁。

begin;
select * from goods where id = 1 and name='prod11' for update;
commit;

 

4、根据主键、非主键含索引(name)进行查询,没有查询到数据,不产生锁。

begin;
select * from goods where id = 1 and name='prod12' for update;
commit;

 

5、根据非索引(stock)进行查询,并且查询到数据,stock字段产生表锁。

begin;
select * from goods where stock='1000' for update;
commit;

 

6、根据非索引(stock)进行查询,没有查询到数据,stock字段产生表锁。

begin;
select * from goods where stock='9999' for update;
commit;

 

7. 只根据主键进行查询,查询条件为模糊(不等于, like等),无论是否查询到数据,主键字段产生表锁。

begin;
select * from goods where id <> 1 for update;
commit;

 

 
 

3 行锁,表锁小结

InnoDb行锁是通过给索引上的索引项加锁来实现的,这一点mysql与oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现的特点意味着: 只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。

在实际应用中,要特别注意 InnoDB 行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

  • 在不通过索引条件查询的时候,InnoDB 确实使用的是表锁,而不是行锁。
  • 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,因此虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
  • 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引还是普通索引,InnoDB 都会使用行锁来对数据加锁。
  • 即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同的执行计划的代价来决定的。如果 MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查 SQL 的执行计划,以确认是否真正使用了索引。

 

4 间隙锁

mysql innodb的间隙锁定(next-key locking)是为了防止幻读。当mysql的隔离级别为repeatable read时候会触发间隙锁定。

next-key的具体工作方式是:

(1) 选择一个不存在的行,则锁住所有的insert行为

(2) 用范围select,如select * from dual where id > 100, 会锁住所有id > 100的insert行为

 

InnoDB对索引记录的锁定也影响索引记录之前的“间隙gap” 。如果一个用户对索引记录R加了一个共享或排他锁定,那其他用户将不能在R之前立即插入新的记录。这种间隙锁定用于防止所谓的“phantom problem”。假设需读取和锁定表 CHILD 中标识符大于 100 的子行,并更新所搜索到的记录中某些字段。

SELECT * FROM CHILD WHERE ID > 100 FOR UPDATE;

假设表 CHILD 中有一个索引字段 ID。我们的查询将从 ID 大于100的第一条记录开始扫描索引记录。 现在,假设加在索引记录上的锁定不能阻止在间隙处的插入,一个新的子记录将可能在事务处理中被插入到表中。 如果现在在事务中再次执行

SELECT * FROM CHILD WHERE ID > 100 FOR UPDATE;

在查询返回的记录集中将会有一个新的子记录。这与事务的隔离原则是相反的:一个事务应该能够运行,以便它已经读的数据在事务过程中不改变。如果我们把一套行视为数据项,新的“幽灵”子记录可能会违反这一隔离原则。 

 

当InnoDB扫描一个索引之时,它也锁定所以记录中最后一个记录之后的间隙。刚在前一个例子中发生:InnoDB设置的锁定防止任何插入到id可能大过100的表。 

你可以用next-key锁定在你的应用程序上实现一个唯一性检查:如果你以共享模式读数据,并且没有看到你将要插入的行的重复,则你可以安全地插入你的行,并且知道在读过程中对你的行的继承者设置的next-key锁定与此同时阻止任何人对你的行插入一个重复。因此,the next-key锁定允许你锁住在你的表中并不存在的一些东西。

gap间隙锁具体锁定的范围参看一下这篇   http://blog.itpub.net/30221425/viewspace-1787312/  我的理解是索引数据的前后间隙, 假设索引数据是m,范围是 [mPre, mNext)

 

 

五 mysql的死锁分析

1 死锁分析

MyISAM 中是不会产生死锁的,因为 MyISAM 总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而在 InnoDB 中,锁是逐步获得的,就造成了死锁的可能。

在 MySQL 中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条 SQL 语句操作了主键索引,MySQL 就会锁定这条主键索引;如果一条 SQL 语句操作了非主键索引,MySQL 就会先锁定该非主键索引,再锁定相关的主键索引。 在进行UPDATEDELETE操作时,MySQL 不仅锁定WHERE条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key locking.

当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引;另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。

发生死锁后,InnoDB 一般都可以检测到,并使一个事务释放锁回退,另一个获取锁完成事务。 

2 避免死锁

有多种方法可以避免死锁,这里只介绍常见的三种:

  1. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低发生死锁的可能性;
  2. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
  3. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。