MySQL数据库锁详解

目录

一、锁的分类

一、按照粒度分类

1、分类

2、代码操作

二、按照属性分类

1、共享锁

2、排他锁

3、代码操作

三、按照加锁方式分类

1、隐式的自动加锁

2、显示的加锁

四、按照算法分类

1、间隙锁

2、临键锁

3、记录锁

五、按照模式分类

1、悲观锁        

2、乐观锁

六、按照状态分类

1、意向锁的解释

1、意向共享锁

2、意向排它锁

二、锁的监控

一、相关变量

二、系统的表信息

三、死锁

一、如何处理死锁

二、如何避免死锁


一、锁的分类

在这里插入图片描述

一、按照粒度分类

1、分类

  1. 全局锁:锁定数据库中的所有表。
  2. 表级锁:每次操作锁住整张表。
  3. 行锁:每次操作锁住对应的行数据。

2、代码操作

-- 全局锁的加锁和释放

FLUSH TABLES WITH READ LOCK;

unlock tables;


-- 表级锁的加锁和释放

lock tables 表名 read/write;

unlock tables;

-- 行锁

二、按照属性分类

1、共享锁

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

注意:

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

2、排他锁

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

注意:

  1. mysql InnoDB引擎默认的修改数据语句(update,delete,insert),都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句。
  2. 加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。

3、代码操作

-- 在事务中使用共享锁
STAET TRANSACTION;
SELECT * FROM tb1 WHERE id = 123 LOCK IN SHARE MODE;

-- 在事务中使用排他锁
STAET TRANSACTION;
SELECT * FROM tb1 WHERE id = 123 FOR UPDATE;

三、按照加锁方式分类

1、隐式的自动加锁

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

2、显示的加锁

  1. 共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
  2. 排他锁(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)

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

-- 0 开始事务
BEGIN;
-- 1 查询出商品库存信息
SELECT quantity FROM items WHERE id = 1 FOR UPDATE;
-- 2 修改商品库存为2
UPDATE items SET quantity = 2 WHERE id = 1;
-- 3 提交事务
COMMIT;

        以上,在对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 操作结果的可能。


六、按照状态分类

1、意向锁的解释

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

意向锁的存在价值在于在定位到特定的行所持有的锁之前,提供一种更粗粒度的锁,可以大大节约引擎对于锁的定位和处理的性能,因为在存储引擎内部,锁是由一块独立的数据结构维护的,锁的数量直接决定了内存的消耗和并发性能。例如,事务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对表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排它锁。


二、锁的监控

一、相关变量

  1. innodb_row_lock开头的变量

二、系统的表信息

        MySQL把事务和锁的信息都记录在information_schema库中,其中一下三个表涉及到锁。

  1. INNODB_TRX
  2. INNODB_LOCKS
  3. INNODB_LOCK_WAITS

三、死锁

一、如何处理死锁

  1. 方式一:等待,直至超时,一个事务阻塞的时间超过innodb_lock_wait_timeout 时,就会回滚。
  2. 方式二:使用死锁检测进行死锁处理。

二、如何避免死锁

  1. 合理设计索引,使业务SQL 尽可能通过索引定位更少的行,减少锁竞争。
  2. 调整业务逻辑SQL执行顺序,避免 update/delete 长时间持有锁的 SQL在事务前面
  3. 避免大事务,尽量将大事务拆成多个小事务来处理,小事务缩短锁定资源的时间,发生锁冲突的几率也更小。
  4. 在并发比较高的系统中,不要显式加锁,特别是是在事务里显式加锁。如 select ...for update 语句,如果是在事务里运行了start transaction 或设置了autocommit 等于0,那么就会锁定所查找到的记录
  5. 降低隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免降低隔离级别。如果业务允许,可以避免掉很多因为gap锁造成的死锁。

猜你喜欢

转载自blog.csdn.net/iuu77/article/details/129126204
今日推荐