日常工作中,经常和MySQL打交道,但是关于锁的知识没有深入研究过,最近花了些时间做个总结。平常对MySQL锁相关知识模棱两可的伙伴们可以看看本文,希望对你有收获。如有描述不清的地方,欢迎批评指正,谢谢!
平常我们会听到这些名词:行锁、表锁、死锁、排它锁、间隙锁、悲观锁、乐观锁…这么多锁到底是啥意思呢,很多文章都是对这些锁做了一些概念性陈述,而这些锁是什么时候加的,他和事务又有什么关系,诸如此类问题好像解释得不太多,我想在本文阐述清楚。
测试表:
CREATE TABLE `account` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (1, 'lilei', 12143);
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (2, 'hanmei', 998);
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (3, 'hanmei3', 998);
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (10, 'lucy', 998);
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (18, '55', 12143);
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (20, 'kk', 12143);
因为存在并发操作,所以需要加锁,只有获取到锁的线程才能执行相应的代码。但是,加锁就代表程序(SQL)都串行执行么?这是不一定的,得看是什么锁,如果是排他锁(互斥锁),也就是任意时刻只有一条线程能够获取到他,那就是串行执行,完全不需要担心线程安全问题。若是共享锁,比方说读锁,对于任意线程读请求,其实是可以并发执行的,若碰到写请求,那就不能都并发执行了。所以,在并发环境下,锁是一把利刃,怎么把他用好,还得区分下锁的类型。
锁的区分可以从多个维度来看。
1、从性能上分:
2、从数据操作上分:
3、从数据操作粒度上分
除此之外,还有些其他锁,比如间隙锁,后边再对其做介绍。
这里的悲观和乐观其实就是指开发人员对并发产生的概率估计,如果觉得某些数据极有可能会频繁发生数据争用的情况,那就要加悲观锁。因为会频繁的发生并发操作,所以要将其转成串行执行,这样才能保证数据的一致性,那要将并发操作转成串行执行,就有很多解决方案,比如在Java代码层加ReentrantLock
、Synchronized
锁,也可以在MySQL层加行锁、表锁,这些都属于悲观锁。
lock.lock(); // 获取锁,获取不到就阻塞(也可以不阻塞)
// 执行程序
lock.unlock();
对于乐观锁,就是某些数据大部分情况下是不会发生数据争用,那我去给他加锁的话,性能开销是比较大的,为了保证少数情况并发操作的数据一致性,而牺牲大部分无并发操作的性能质量,这是不可取的。那有什么办法呢?比如在这些数据上加一个版本号的字段,每次成功修改的话都往版本号上+1
,同时修改的时候也会去比对版本号是否和刚开始读取到的版本号一致,如果不一致就重试,这其实就是自旋+CAS的方式。
User user = mysqlDao.getUser(); // user里有version字段
int update =
“update user set age = #{user.getAge},version = version + 1 where userid = #{user.getUserId} and version = #{user.getVersion}”
while (update != 1) {
// 重试
}
读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响。
写锁(排它锁,X锁(eXclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁。
以上几种锁,都是从概念上进行解释,至于表锁和行锁,就是比较具体的锁了。首先要明白一个概念,这个锁还和执行引擎有关系。对于MyISAM
执行引擎,是没有行锁的,也不支持事务。而行锁的概念也只有在InnoDB
执行引擎下讨论才有意义。
表锁,顾名思义,就是把整张表锁住,当然这个锁也是分为读锁和写锁
# 表读锁
lock table xxx read;
# 表写锁
lock table xxx write;
# 解锁
unlock tables;
# 查询正被加锁的表
show OPEN TABLES where in_use;
那这两把锁啥区别,看了上边概念介绍,相信你一定能想明白的哈。
行锁,顾名思义,就是把数据库里的一行锁住,同样有两种方式,一般称为排它锁和共享锁,都是悲观锁的实现。切记:行锁只有在事务中才生效!
# 排它锁
begin;
select * from account where id = 1 for update;
commit; # 释放锁
# 共享锁
begin;
select * from account where id = 1 lock in share mode;
commit; # 释放锁
表锁和事务没有关系,无论在不在事务里,只要加了表锁,就会生效
# session一
lock table account READ;
# session二
update account set balance = 998 where id = 1 # 锁等待
########################################################
# session一
lock table account WRITE;
# session二
select * from account where id = 1 # 锁等待
行锁需要在事务里才生效,事务提交才能释放锁
# 事务一
begin;
select * from account where id = 1 for update; # 获取行互斥锁
# 事务二
begin;
select * from account where id = 1 for update; # 锁等待(如果不是事务这里也会等待)
########################################################
# 事务一
begin;
select * from account where id = 1 lock in share mode; # 获取行共享锁
# 事务二
begin;
select * from account where id = 1 lock in share mode; # 获取行共享锁
update account set balance = 99 where id = 1; # 更新失败 必须等事务一提交了才行
行锁为什么会升级成表锁?简而言之就是MySQL能够马上定位到你要锁得行,那就给你锁,要是不能马上定位到,也就是没有索引,那我就把表锁住。
# 事务一
begin;
select * from account where balance = 998 lock in share mode; #获取共享锁
# 事务二
update account set balance = 998 where id = 1 # balance没有索引,事务一的行锁升级成表锁,当前操作会等待
间隙,顾名思义,就是把记录间的缝隙给锁住,比如这样的一条事务,你觉得我在另一个事务中进行插入,能插入成功么?
# 事务一
begin;
select * from account where id <18 and id > 2 lock in share mode;
# 事务二
begin;
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (4, '55', 12143); # 插入等待
你会发现,根本就插入不成功,为什么?这就是间隙锁,他把[2,18]之间的间隙也锁住了,即使这些位置上没有值,他也不允许插入。可以说在一定程度上解决了幻读的问题。当然这个间隙区间怎么算出来的,好像也有一套算法,可能锁的区间比这还大一些,有兴趣可以深入研究看看,大概知道这么回事就行。
死锁其实就是某个线程(或事务、session)一直持有着锁而没有释放,导致其他请求一直获取不到锁,这样就很影响系统的使用。比如说如下场景:
# 事务一
begin;
select * from account where id = 1 for update; # 先获取id=1记录的行锁
select * from account where id = 2 for update; # 等事务二获取了id=2的锁后再去获取锁
# 事务二
begin;
select * from account where id = 2 for update; # 先获取id=2记录的行锁
select * from account where id = 1 for update; # 事务一已先于当前事务获取到id=1的锁
这样一来MySQL会提示:
> 1213 - Deadlock found when trying to get lock; try restarting transaction
其实我们在实际开发中并不会傻到写这种代码吧?但是有些方法嵌套调用,调来调去可能就会产生这样的死锁。
还有一些死锁现象就是某个资源获取到锁,因为一些异常状况,而忘了解表锁或者提交事务,那也会让其他资源不停等待下去,这也算是一种死锁。
如果出现了死锁,可定得想办法定位到是什么地方被锁了,短时间内还得先把他释放掉,让线上系统再撑一会。
表锁死锁解决方案
# 查看当前被锁住得表
show open tables where in_use > 0;
# 查看死锁的进程id
SHOW PROCESSLIST;
# 杀死
kill pid;
行锁死锁解决方案
# 查看事务信息
select * from INFORMATION_SCHEMA.INNODB_TRX;
# kill掉锁等待的事务、死锁的事务
kill ${trx_mysql_thread_id}
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits
: 当前正在等待锁定的数量Innodb_row_lock_time
: 从系统启动到现在锁定总时间长度Innodb_row_lock_time_avg
: 每次等待所花平均时间Innodb_row_lock_time_max
:从系统启动到现在等待最长的一次所花时间