MySQL 事务与锁机制深入

MySQL事务与锁机制


什么是数据库的事务

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

事务性质ACID

  1. 原子性(Atomicity)

    事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。

  2. 一致性(Consistency)

    几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。

    事务前后数据的完整性必须保持一致。事务操作成功后,数据库所处的状态和他的业务规则是一致的,即数据不会被破坏。如A账户转账100元到B账户,不管操作成功与否,A和B账户的存款总额是不变的。

  3. 隔离性(Isolation)

    事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。

  4. 持久性(Durability)

    对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。

开启和关闭事务

show GLOBAL VARIABLES LIKE ‘autocommit’

可以查询当前数据库是否开启自动提交功能

-- BEGIN 用于开启事务,COMMIT,ROLLBACK用来结束事务,客户端中断也会结束事务
BEGIN; -- 开启事务
   UPDATE USER t set t.age=12 WHERE t.id=1;
COMMIT; -- 提交事务
ROLLBACK; -- 回滚事务

并发下引起有事务问题

select @@tx_isolation; – 查看当前数据的隔离级别

保证数据库隔离级别为 READ-UNCOMMITTED否则事务将无法测试。

  • 脏读:一个事务读取到另一个事务未提交的数据
    在这里插入图片描述

    从图中可以看到事务A在事务B提交修改数据之前读到修改的数据。

  • 幻读:在同一事务中多次进行,由于其他提交事务所做的新增,每次返回不同的结果集。

    在这里插入图片描述

    从图中可以看到事务A读取到事务B新增的数据。

  • 不可重复读:在同一事务中多次进行,由于其他提交事务所做的修改和删除,每次返回不同的结果集。

在这里插入图片描述

从图中可以看到事务A读取到事务B修改的数据。

事务隔离级别(SQL92)

在这里插入图片描述

如何解决读一致性问题?

  1. 在读取数据前,对其加锁,阻止其他事务对数据进行修改——Lock Based Concurrency Control (LBCC)
  2. 生成一个数据请求时间点的一致性数据快照(Snapshout),并用这个快照来提供一定级别(语句级或事务级)的一致性读取——Multi-Version Concurrency Control (MVCC)

锁的粒度及区别

表锁与行锁的区别

​ 锁定粒度:表锁 > 行锁

​ 加锁效率:表锁 > 行锁

​ 冲突概率:表锁 > 行锁

​ 并发性能:表锁 > 行锁

MyISAM 锁的粒度为表锁,InnoDB 锁的数度为行锁

锁的模式

-- 查询锁的使用情况
SELECT * FROM information_schema.INNODB_LOCKS t;
SELECT * from information_schema.INNODB_LOCK_WAITS t;
  • 共享锁(行锁):Shared Locks
  • 排它锁(行锁):Exclusive Locks
  • 意向共享锁(表锁):Intention Shared Locks
  • 意向排它锁(表锁):Intention Exclusive Locks
  • 插入意向锁(Insert Intention Locks)
  • 自增锁(AUTO-INC Locks)

锁的算法

  • 记录锁:(Record Locks)
  • 间隙锁:(Gap Locks)
  • 临键锁:(Next-key Locks)

共享锁(行锁)

共享锁(S锁)又称为读锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改

加锁方式:select … Lock in share mode

释放锁:commit/rollback

-- TRANSACTION A
BEGIN;
SELECT * from `user`  LOCK in SHARE MODE;
COMMIT;
-- TRANSACTION B
BEGIN;
SELECT * from `user`  LOCK in SHARE MODE;
COMMIT;

事务A和事务B都可以查询到数据因此共享锁可以读。

-- TRANSACTION A
BEGIN;
SELECT * from `user`  LOCK in SHARE MODE;
-- COMMIT;
-- TRANSACTION B
BEGIN;
  DELETE FROM `user`;
ROLLBACK;

可以看到事务B再执行的时候处于阻塞状态只有事务A提交后,事务B才会继续执行。因此共享锁只能读不能修改

排他锁(行锁)

排它锁(X锁)又称为写锁,排他锁不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的锁(共享锁,排他锁),只有获取了排他锁的事务可以对数据行进行读取和修改。

加锁方式:

​ 自动:delete/update/insert 默认加上X锁

​ 手动:select … FOR UPDATE

-- TRANSACTION A
BEGIN;
-- 自动加写锁
UPDATE `user` t set t.age=45 WHERE t.id=1
-- COMMIT;
-- TRANSACTION B
BEGIN;
  -- 共享锁
  SELECT * from `user` LOCK IN SHARE MODE;
  -- 手动加排他锁
  SELECT * FROM `user` FOR UPDATE;
  -- 自动加排他锁
  DELETE FROM `user`  WHERE id=1; 
ROLLBACK;

可以看到事务B再执行的时候无论是共享锁还是排他锁都是阻塞状态,因此排他锁不能与其他锁并存。

意向共享锁/意向排它锁(表锁)

意向锁是由数据引擎自己维护的,用户无法手动操作意思锁。

  • 意向共离锁(Intention Shared Locks,简称IS锁)表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该有的IS锁。
  • 意向排他锁(Intention Exclusive Locks,简称IX锁)表示事务准备给数据加入排他锁,说明事务在一个数据行加排他锁前必须先取处该表的IX锁。

为什么意向锁是表级别的?

当我们需要加一个排他锁时,需要根据意向锁去判断表中有没有数据行被锁定(行锁);

  1. 如果意向锁是行锁,则需要遍历每一行数据去确认
  2. 如果意向锁是表锁,则只需要判断一次即可知道有没数据行被锁定,提升性能。

共享锁/排他锁与意向共享锁/意向排他锁的兼容性关系

X IX S IS
X 互斥 互斥 互斥 互斥
IX 互斥 兼容 互斥 兼容
s 互斥 互斥 兼容 兼容
IS 互斥 兼容 兼容 兼容

这里需要重点关注的是IX锁和IX锁是相互兼容的,这可能会造成死锁。

插入意向锁(行锁)

插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。如果说间隙锁锁住的是一个区间,那么插入意向锁锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。与间隙锁的另一个非常重要的差别是:尽管插入意向锁也属于间隙锁,但两个事务却不能在同一时间内一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。这里我们再回顾一下共享锁和排他锁:共享锁用于读取操作,而排他锁是用于更新或删除操作。也就是说插入意向锁、共享锁和排他锁涵盖了常用的增删改查四个动作。

自增锁(表锁)

AUTO-INC锁是一种特殊的表级锁,发生涉及AUTO_INCREMENT列的事务性插入操作时产生。

锁的算法详解

表如图:

在这里插入图片描述

现有user表ID为主键,其中四条记录ID分别为1,4,7,10

记录锁

唯一性索引(唯一/主键)等值查询,精确匹配。数据存在

在这里插入图片描述

-- 锁住:ID=4
BEGIN;
  SELECT * from `user` t WHERE t.id=4  for update;
-- COMMIT;
-- 查看锁有类型为排他锁(X),
SELECT * FROM information_schema.INNODB_LOCKS t;

在这里插入图片描述

间隙锁

记录不存在的情况,间隙锁解决幻读。
在这里插入图片描述

-- TRANSACTION A 锁定(4,7)
BEGIN;
  SELECT * from `user` t WHERE t.id>4 AND t.id<7  for update;
-- COMMIT;
-- TRANSACTION B 锁定(4,7)
BEGIN;
  SELECT * from `user` t WHERE t.id=6  for update;
-- COMMIT;

两个事务锁定的空间都 是(4,7)所以GAP锁之间不冲突。

-- TRANSACTION A 锁定(4,7)
BEGIN;
  SELECT * from `user` t WHERE t.id>4 AND t.id<7  for update;
-- COMMIT
-- TRANSACTION B 插入id为6的记录
BEGIN;
  INSERT INTO `user` VALUES(6,'six',22);
COMMIT ;

因为事务A锁定了(4,7)所以事务B处理等待状态,只有事务A提交后事务B才可以后续操作。

-- 查看锁有类型为排他锁(X),
SELECT * FROM information_schema.INNODB_LOCKS t;

在这里插入图片描述

临键锁

范围查询,包含记录和区间。

在这里插入图片描述

-- TRANSACTION A 锁定(4,7] (7,10] 左开右闭
BEGIN;
  SELECT * from `user` t WHERE t.id>5 AND t.id<9  for update;
-- COMMIT;
-- TRANSACTION B
BEGIN;
  INSERT INTO `user` VALUES(6,'six',22);
COMMIT ;

因为事务A锁定了(4,7] (7,10]所以事务B处理等待状态,只有事务A提交后事务B才可以后续操作。

-- 查看锁有类型为排他锁(X),
SELECT * FROM information_schema.INNODB_LOCKS t;

在这里插入图片描述

死锁示例

CREATE TABLE `test` (
  `id` int(20) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
session1 session2
begin;
begin;
select * from test where id = 12 for update;
先请求IX锁并成功获取
再请求X锁,但因行记录不存在,故得到的是间隙锁(10,15)
select * from test where id = 13 for update;
先请求IX锁并成功获取
再请求X锁,但因行记录不存在,故得到的是间隙锁(10,15)
insert into test(id, name) values(12, “test1”);

请求插入意向锁(12),因事务二已有间隙锁,请求只能等待
锁等待中 insert into test(id, name) values(13, “test2”);

请求插入意向锁(13),因事务一已有间隙锁,请求只能等待
锁等待解除 死锁,session 2的事务被回滚

在场景中,因为IX锁是表锁且IX锁之间是兼容的,因而事务一和事务二都能同时获取到IX锁和间隙锁。另外,需要说明的是,因为我们的隔离级别是RR,且在请求X锁的时候,查询的对应记录都不存在,因而返回的都是间隙锁。接着事务一请求插入意向锁,这时发现事务二已经获取了一个区间间隙锁,而且事务一请求的插入点在事务二的间隙锁区间内,因而只能等待事务二释放间隙锁。这个时候事务二也请求插入意向锁,该插入点同样位于事务一已经获取的间隙锁的区间内,因而也不能获取成功,不过这个时候,MySQL已经检查到了死锁,于是事务二被回滚,事务一提交成功。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章