文章目录

  • Mysql 锁分类
  • 按照粒度分类
  • 1. 全局锁
  • 2. 表级锁
  • 3. 页级锁
  • 4. 行级锁
  • 按属性分类
  • 1. 共享锁
  • 2. 排他锁
  • 按加锁方式分类
  • 按照算法分类
  • 1. 间隙锁
  • 2. 临键锁
  • 3. 记录锁
  • 按照模式分类
  • 1. 悲观锁
  • 2. 乐观锁
  • 按照状态分类
  • 1. 意向共享锁
  • 2. 意向排它锁


Mysql 锁分类

在之前,我们了解了数据库事务和各种事务隔离级别,在并发的情况下,数据库是通过锁的机制实现隔离级别。

数据库中存在各种各样的锁,我们在说某种锁的时候,一定要先说明,按照什么分类,分为哪些锁。

锁的目的和并发编程中锁的一样,是为了解决并发情况下,对同一个资源访问限制,旨在强制实施互斥排他、并发控制策略。

MySQL有几种锁 mysql的锁有哪些_数据

按照粒度分类

按锁的粒度分类,全局锁、表级锁、页级锁、行级锁。

1. 全局锁

全局锁就对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的MDL、DDL语句、更新操作的事务提交语句都将被阻塞。其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

通过以下SQL 可以添加全局锁:

FLUSH TABLES WITH READ LOCK;

然后其他事务进行操作时,可以看到一直处于阻塞状态,只能读取数据:

MySQL有几种锁 mysql的锁有哪些_database_02


可以通过以下SQL 释放全局锁:

UNLOCK TABLES;

2. 表级锁

表级锁会对当前操作的整张表加锁,最常使用的 MyISAM 与 InnoDB 都支持表级锁定。

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。

添加表锁,格式如下

lock tables xxx read/write;

比如以下方式表示,对表添加只读锁,表加上了表读锁之后,本线程和其他线程都可以读数据,本事务写数据会报错,其他事务写数据会阻塞。

LOCK TABLES account_tbl READ;

MySQL有几种锁 mysql的锁有哪些_database_03

比如以下方式表示,对表添加只写锁,表加上了表写锁之后,当前线程的读操作会被阻塞,写可以正常执行;其他线程的读写操作都会被阻塞。

LOCK TABLES account_tbl WRITE;

MySQL有几种锁 mysql的锁有哪些_数据_04

元数据锁(Metadata Locking,简称:MDL锁),MySQL5.5版本引入了MDL锁,用于解决或者保证DDL操作与DML操作之间的一致性。

当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL写锁。

可以看到开启事务,进行查询操作时,对字段进行更新的操作会一直处理阻塞状态:

MySQL有几种锁 mysql的锁有哪些_数据_05


MySQL有几种锁 mysql的锁有哪些_数据_06

3. 页级锁

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

4. 行级锁

行级锁是粒度最低的锁,发生锁冲突的概率也最低、并发度最高。但是加锁慢、开销大,容易发生死锁现象。MySQL中只有InnoDB支持行级锁,行级锁可分为共享锁和排他锁。

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

注意事项

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

按属性分类

按属性分类,可分为共享锁和排他锁。

1. 共享锁

共享锁,又称之为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。只有当数据上的读锁被释放后,其他事务才能对其添加写锁。共享锁主要是为了支持并发的读取数据而出现的,读取数据时,不允许其他事务对当前数据进行修改操作,从而避免”不可重读”的问题的出现。

可以通过以下SQL 添加共享锁:

-- 开启事务
START TRANSACTION;
SELECT
	* 
FROM
	account_tbl 
WHERE
	id = "11111111" LOCK IN SHARE MODE;

其他事务修改时,则会进入阻塞,但是是可以正常读取数据的,而且其他事务可以继续添加共享锁:

MySQL有几种锁 mysql的锁有哪些_mysql_07


但是在共享锁上再添加排它锁时,会进入阻塞,直到共享锁释放,或等待锁超时:

MySQL有几种锁 mysql的锁有哪些_数据库_08

2. 排他锁

排他锁锁住一行数据后,其他事务不能再在其上加其他的锁。

排它锁,又称之为写锁、独占锁,简称X锁,当事务对数据加上写锁后,其他事务既不能对该数据添加读锁,也不能对该数据添加写锁,写锁与其他锁都是互斥的。只有当前数据写锁被释放后,其他事务才能对其添加写锁或者是读锁。写锁主要是为了解决在修改数据时,不允许其他事务对当前数据进行修改和读取操作,从而可以有效避免”脏读”问题的产生。

mysql InnoDB引擎默认的修改数据语句(update,delete,insert),都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句。

加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for updatelock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。

案例:打开一个窗口,开启事务并添加一个排它锁

MySQL有几种锁 mysql的锁有哪些_database_09


在其他事务中,继续添加其他锁,或者修改数据时,会进入阻塞,直到排他锁释放,或等待锁超时:

MySQL有几种锁 mysql的锁有哪些_数据_10

按加锁方式分类

按加锁方式划分,可分为自动锁、显示锁。

隐式加锁

  • InnoDB自动加意向锁。
  • 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
  • 对于普通SELECT语句,InnoDB不会加任何锁;

显示加锁

  • 共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
  • 排他锁(X) :SELECT * FROM table_name WHERE … FOR UPDATE

按照算法分类

按照算法分类,可分为间隙锁、临键锁、记录锁。

1. 间隙锁

间隙锁基于非唯一索引,它锁定一段范围内的索引记录。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。

select * from goods where id between 1 and 10 for update;

即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。

2. 临键锁

临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间,是一个左开右闭区间。临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。

每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。

3. 记录锁

记录锁是封锁记录,记录锁也叫行锁,例如:

select *from goods where id=1 for update;

它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。

按照模式分类

按照模式分类,可分为悲观锁和乐观锁。

1. 悲观锁

悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的。

特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高,还有增加产生死锁的风险。

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  • 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  • 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

拿比较常用的 MySql Innodb 引擎举例,来说明一下在 SQL 中如何使用悲观锁。

要使用悲观锁,必须关闭 MySQL 数据库的自动提交属性。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。(sql语句:set autocommit=0)

以电商下单扣减库存的过程说明一下悲观锁的使用:

MySQL有几种锁 mysql的锁有哪些_database_11


以上,在对id = 1的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。

如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。

上面提到,使用 select…for update 会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

2. 乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

一般通过版本号机制实现乐观锁,在数据表中加上版本号字段 version,表示数据被修改的次数。当数据被修改时,这个字段值会加1。

举个简单的例子:假设帐户信息表中有一个 version 字段,当前值为 1 ,而当前帐户的余额( balance )为 100 。

  • 操作员 A 此时准备将其读出( version=1 ),并从其帐户余额中扣除 50( 100-50 );
  • 操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 20 ( 100-20 );
  • 操作员 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50 ),提交到数据库完成更新;
  • 操作员 B 完成了操作,也将版本号加1( version=2 )试图向数据库提交数据( balance=80 ),但此时比对数据库记录版本发现,操作员 B 提交的数据版本号为 2 ,数据库记录的当前版本也为 2 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。

因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改,最终造成覆盖操作员 A 操作结果的可能。

按照状态分类

按状态分为:意向共享锁、意向排它锁。

意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。由于意向锁仅仅表明意向,它其实是比较弱的锁,意向锁之间并不相互互斥,而是可以并行。

意向锁的存在价值在于在定位到特定的行所持有的锁之前,提供一种更粗粒度的锁,可以大大节约引擎对于锁的定位和处理的性能,因为在存储引擎内部,锁是由一块独立的数据结构维护的,锁的数量直接决定了内存的消耗和并发性能。例如,事务A对表t的某些行修改(DML通常会产生X锁),需要对t加上意向排它锁,在A事务完成之前,B事务来一个全表操作(alter table等),此时直接在表级别的意向排它锁就能告诉B需要等待(因为t上有意向锁),而不需要再去行级别判断。

1. 意向共享锁

意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁。

select ... lock in share mode,要设置IS锁,事务要获得某些行的S锁,必须先获得表的IS锁。

2. 意向排它锁

意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁。

select ... for update,要设置IX锁,事务要获得某些行的X锁,必须先获得表的IX锁。

比如我们开启一个事务,对某条记录添加一个排他锁,此时表存在两把锁:表上的意向排它锁与 id 为 "11111111"的数据行上的排它锁。

-- 开启事务
START TRANSACTION;
SELECT
	* 
FROM
	account_tbl 
WHERE
	id = "11111111" FOR UPDATE;

如果另外一个书屋T2想要添加共享锁时,此时检测到其他事务持有表的意向排他锁,就可以得知该表必然持有该表中某些数据行的排他锁,那么T2对表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排它锁。

MySQL有几种锁 mysql的锁有哪些_mysql_12