MySQL事务与锁机制
什么是数据库的事务
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务性质ACID
-
原子性(Atomicity)
事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。
-
一致性(Consistency)
几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。
事务前后数据的完整性必须保持一致。事务操作成功后,数据库所处的状态和他的业务规则是一致的,即数据不会被破坏。如A账户转账100元到B账户,不管操作成功与否,A和B账户的存款总额是不变的。
-
隔离性(Isolation)
事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
-
持久性(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)
如何解决读一致性问题?
- 在读取数据前,对其加锁,阻止其他事务对数据进行修改——Lock Based Concurrency Control (LBCC)
- 生成一个数据请求时间点的一致性数据快照(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锁。
为什么意向锁是表级别的?
当我们需要加一个排他锁时,需要根据意向锁去判断表中有没有数据行被锁定(行锁);
- 如果意向锁是行锁,则需要遍历每一行数据去确认
- 如果意向锁是表锁,则只需要判断一次即可知道有没数据行被锁定,提升性能。
共享锁/排他锁与意向共享锁/意向排他锁的兼容性关系
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已经检查到了死锁,于是事务二被回滚,事务一提交成功。