行锁表锁间隙锁临键锁共享锁排他锁死锁
- 手动提交
- 行锁(记录锁Record Locks)
- 表锁
- 间隙锁(Gap Locks)
- 临键锁(Next-Key Locks)
- 共享锁,排他锁
- 死锁
- 查看事物,锁的命令
- 死锁的4个必要条件
- 模拟死锁
- 如何防止死锁
手动提交
//查看自动提交
show VARIABLES like 'autocommit';
//关闭自动提交,改为手动提交
set autocommit = 0;
CREATE table test_lock(
id int(11) DEFAULT NULL,
username varchar(32) DEFAULT NULL,
age tinyint(4) DEFAULT NULL,
KEY idx_id (id),
KEY idx_name (username),
KEY idx_age (age)
) ENGINE=INNODB DEFAULT CHARSET = utf8;
INSERT INTO `test_lock` (`id`, `username`, `age`) VALUES ('1', 'zhangsan', '11');
INSERT INTO `test_lock` (`id`, `username`, `age`) VALUES ('3', 'lisi', '22');
INSERT INTO `test_lock` (`id`, `username`, `age`) VALUES ('5', 'wangwu', '33');
INSERT INTO `test_lock` (`id`, `username`, `age`) VALUES ('7', 'chenqi', '44');
INSERT INTO `test_lock` (`id`, `username`, `age`) VALUES ('9', 'liuba', '55');
行锁(记录锁Record Locks)
首先新建一个查询窗口A,来修改一条数据并查询(注意此时并没有提交事物)
UPDATE test_lock SET username = 'huangshang' WHERE id = 1;
SELECT * from test_lock;
id = 1 的 username 已经修改为 huangshang
然后再新建一个查询窗口B,查询,此时查询到的id=1 的username 还是zhangsan
这是因为窗口A里面的事物并没有提交,当窗口执行commit后,窗口B的查询结果就是最新的数据
此时窗口A 和 窗口B同时执行修改操作(窗口A没有commit)
// 窗口A 把id=1的username 改为111
UPDATE test_lock SET username = '111' WHERE id = 1;
// 窗口B 把id=1的username 改为222
UPDATE test_lock SET username = '111' WHERE id = 1;
可以看到下图中,窗口A的修改执行完成,窗口B执行失败因为窗口A把id=1这行上锁了
如果窗口A commit之后会怎么样?
可以看到窗口Acommit之后,窗口B立马就执行成功了,因为commit之后窗口A的行锁就释放了,B就可以进行修改操作了
表锁
当行锁的索引失效就会升级为表锁,例如窗口A修改id=1 or id=2的数据,然后窗口B修改id=3的数据,如果按照行锁来处理的话,窗口B应该会执行成功的,因为窗口A会锁id = 1 和 2 的这2行数据,id=3的记录并没有上锁
// 窗口A
UPDATE test_lock SET username = '111' WHERE id = 1 or id = 2;
// 窗口B
UPDATE test_lock SET username = '222' WHERE id = 3;
如图窗口B是执行失败的,因为窗口A的sql索引失效,造成了表锁
只有当窗口Acommit之后 ,表锁释放,此时窗口B就可以执行成功了
间隙锁(Gap Locks)
先把表里面id改为 13579,中间的2468是没有的
此时窗口A执行修改操作,修改 1<id<9 的数据,那么此时1-9的行就会上锁
此时窗口B执行插入操作,因为id=4的记录并没有
// 窗口A
UPDATE test_lock SET username = '111' WHERE id > 1 or id < 9;
//窗口B
INSERT INTO test_lock (id, username) VALUES ('4', 'gaogao');
可以看到,窗口A的事物把1-9的记录 都上锁了,包过新增的id=4的记录,所以窗口B执行失败,这就是间隙锁
临键锁(Next-Key Locks)
如表所示,age列潜在的临键值有
(-∞, 11]
(11, 22]
(22, 33]
(33, 44]
(44, +∞]
现在窗口A执行修改,并排他锁读
-- 根据非唯一索引列age 修改姓名
UPDATE test_lock SET username = 'kkk' WHERE age = 22;
-- 根据非唯一索引列age 锁住22这行记录
SELECT * FROM test_lock WHERE age = 22 FOR UPDATE;
然后窗口B执行插入操作
INSERT INTO test_lock (id, username, age) VALUES ('11', 'lisi', '25');
可以看到窗口B的插入操作被阻塞了,因为窗口A没有释放锁,锁住了age=22-33这个区间范围,导致窗口B在插入age=25的时候被阻塞了,这就是临键锁
关于记录锁,间隙锁,临键锁的区别
- InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
- 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
- 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
- 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。
共享锁,排他锁
// 共享锁(S)
select * from test_lock where id = 1 lock in share mode
// 排他锁(X)
select * from test_lock where id = 1 for update
- 共享锁:不会阻塞其他事务对同一行的读请求,但会阻塞对同一行的写请求。只有当读锁释放后,才会执行其它事物的写操作。
- 排他锁:会阻塞其他事务对同一行的读和写操作,只有当写锁释放后,才会执行其它事务的读写操作。
如图窗口A使用的共享锁,窗口B可以正常读操作
如图窗口A使用的共享锁,窗口B写操作会阻塞,需要等A执行完成之后才能进行写操作
而对应排他锁来说,窗口A的sql如果加了排他锁,那么窗口B的读和写操作都会阻塞,需要等A释放锁之后才能操作
死锁
查看事物,锁的命令
//查看正在进行中的事务
SELECT * FROM information_schema.INNODB_TRX
//查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
//查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
//查询是否锁表
SHOW OPEN TABLES where In_use > 0;
//查看最近死锁的日志
show engine innodb status
死锁的4个必要条件
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 环路等待条件
模拟死锁
-- 窗口A修改age=22的姓名
begin;
UPDATE test_lock SET username = 'www' WHERE age = 22;
-- 窗口B修改age=33的姓名
begin;
UPDATE test_lock SET username = 'qqq' WHERE age = 33;
这2个操作都是行锁,互不干扰,都可以执行成功,注意这2个操作都没有提交
现在窗口A来修改age=33的记录,窗口B来修改age=22的记录
此时窗口A执行会阻塞等待窗口B age=33释放锁,如果此时窗口B也执行的话,就会造成死锁,因为它需要等窗口A的age=22释放锁,这样就形成了闭环,相互持有锁,都在等对方释放锁,就造成死锁了,此时MySQL会让窗口B的事物报错,同时释放窗口B的锁,这样窗口A就能正常执行了
如何防止死锁
- 使用共享锁,可以防止在读取数据的过程中,其它事务对数据进行更新;其它事务可以并发读取数据
- 使用排他锁,可以阻塞其他事物的读写操作
- 避免并发修改同一条数据
- 一个事物中的所有操作都必须加锁
- 通过加锁顺序来控制事物的执行顺序
- 一个事物的执行时间不能过长,如果有很多操作可以考虑进行拆分成多个事物
- 使用尽可能低的隔离级别
- 保证事物简短并在一个批处理当中执行