关于锁,不管是出于面试来学习,还是工作所需要来学习,都是非常有必要的。
接下来,我们就来分析分析MySQL中的相关锁。
锁的种类
数据库里有的锁有很多种,为了方面理解,所以我根据其相关性"人为"的对锁进行了一个分类,分别如下
基于锁的属性分类:共享锁、排他锁。
基于锁的粒度分类:表锁、行锁、记录锁、间隙锁、临键锁。
基于锁的状态分类:意向共享锁、意向排它锁。
还有死锁.....
乐观锁与悲观锁
虽然说,乐观锁和悲观锁不在MySQL锁分类中,但是不代表我们就不去管它们,毕竟这两个锁是我们大多数人最新听说大锁。
乐观锁
乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
比如:我们要对商品表t_goods中的某个商品已经下架,此时我们不是去删除这条记录,而是去改变该商品的状态即可。
另外,我们在商品表中加了一个字段version,我们先是通过比较当前的version和商品id是否一致,一致就修改状态为2,否则不不修改。
update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
看这个是不是想起了并发编程中的CAS,比较然后替换。
乐观锁相对简单些,下面,我们来聊聊悲观锁。
悲观锁
与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。
接着,我们来聊聊共享锁与排它锁。
共享锁与排它锁
共享锁
共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。共享单词是share,因此,也称之为S锁。
相当于对于同一把门,它拥有多个钥匙一样。就像这样,你家有一个大门,大门的钥匙有好几把,你有一把,你女朋友有一把,然后你们打开用钥匙打开门,进去可以papapa啦,一下理解了哈,没错,这个就是所谓的共享锁。
刚刚说了,对于悲观锁,一般数据库已经实现了,共享锁也属于悲观锁的一种,那么共享锁在MySQL中是通过什么命令来调用呢。通过查询资料,了解到通过在执行语句后面加上 lock in share mode就代表对某些资源加上共享锁了。
比如:
SELECT id,t_name,t_status from t_goods where id = "1" lock in share mode;
排它锁
排他锁又称写锁(eXclusive Lock),简称X锁,取名为X锁,我猜也是为了大家更好的辨别,那为什么不起E锁呢?
哈哈哈,题外话,没什么卵用的,就一个名词而已,爱叫什么叫什么。
当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。
排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。
比如:
SELECT id,t_name,t_status from t_goods where id = "1" for update
排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁。
表锁与行锁
我们首先来了解一下表锁和行锁:表锁是指对一整张表加锁,一般是 DDL 处理时使用;而行锁则是锁定某一行或者某几行,或者行与行之间的间隙。
推荐:
对表锁和行锁有点认识了后,我们先来说说行锁:
行锁
表锁由 MySQL Server 实现,行锁则是存储引擎实现,不同的引擎实现的不同。在 MySQL的常用引擎中InnoDB 支持行锁,而 MyISAM则只能使用 MySQL Server提供的表锁。
简言之,表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问。特点就是粒度大、加锁简单、容易冲突。
表锁由 MySQL Server 实现,一般在执行 DDL 语句时会对整个表进行加锁,比如说 ALTER TABLE 等操作。在执行 SQL 语句时,也可以明确指定对某个表进行加锁。
比如:
mysql> lock table user read(write); # 分为读锁和写锁 Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 100; # 成功
mysql> select * from role where id = 100; # 失败,未提前获取该 role的读表锁
mysql> update user set name = 'Tom' where id = 100; # 失败,未提前获得user的写表锁
mysql> unlock tables; # 显示释放表锁Query OK, 0 rows affected (0.00 sec)
表锁使用的是一次性锁技术,也就是说,在会话开始的地方使用 lock 命令将后续需要用到的表都加上锁,在表释放前,只能访问这些加锁的表,不能访问其他表,直到最后通过 unlock tables 释放所有表锁。
除了使用 unlock tables 显示释放锁之外,会话持有其他表锁时执行lock table 语句会释放会话之前持有的锁;会话持有其他表锁时执行 start transaction 或者 begin 开启事务时,也会释放之前持有的锁。
表锁
表锁由 MySQL Server 实现,一般在执行 DDL 语句时会对整个表进行加锁,比如说 ALTER TABLE 等操作。在执行 SQL 语句时,也可以明确指定对某个表进行加锁。
mysql> lock table user read(write); # 分为读锁和写锁Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 100; # 成功m
ysql> select * from role where id = 100; # 失败,未提前获取该 role的读表锁
mysql> update user set name = 'Tom' where id = 100; # 失败,未提前获得user的写表锁
mysql> unlock tables; # 显示释放表锁Query OK, 0 rows affected (0.00 sec)
表锁使用的是一次性锁技术,也就是说,在会话开始的地方使用 lock 命令将后续需要用到的表都加上锁,在表释放前,只能访问这些加锁的表,不能访问其他表,直到最后通过unlock tables 释放所有表锁。
除了使用unlock tables显示释放锁之外,会话持有其他表锁时执行lock table 语句会释放会话之前持有的锁;会话持有其他表锁时执行start transaction 或者 begin 开启事务时,也会释放之前持有的锁。
行锁的模式
锁的模式有:
读意向锁
写意向锁
读锁
写锁
自增锁(auto_inc)。
NB,居然有这么多,很多人是不是都只是听说名词,甚至部分朋友连很多名词都是第一次听说。
很多人喜欢说InnoDb是行锁,这里纠正一下:
行锁在 InnoDB 中是基于索引实现的,所以一旦某个加锁操作没有使用索引,那么该锁就会退化为表锁。
别急,下面我们一个一个来搞清楚。
读写锁
读锁,又称共享锁(Share locks,简称 S 锁),加了读锁的记录,所有的事务都可以读取,但是不能修改,并且可同时有多个事务对记录加读锁。
写锁,又称排他锁(Exclusive locks,简称 X 锁),或独占锁,对记录加了排他锁之后,只有拥有该锁的事务可以读取和修改,其他事务都不可以读取和修改,并且同一时间只能有一个事务加写锁。
意向锁
锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式。
释义:
意向共享锁(IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
意向互斥锁(IX):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
意向共享锁和意向排它锁总称为意向锁。意向锁的出现是为了支持Innodb支持多粒度锁。
首先,意向锁是表级别锁。
理由:当我们需要给一个加表锁的时候,我们需要根据意向锁去判断表中有没有数据行被锁定,以确定是否能加成功。如果意向锁是行锁,那么我们就得遍历表中所有数据行来判断。如果意向锁是表锁,则我们直接判断一次就知道表中是否有数据行被锁定了。所以说将意向锁设置成表级别的锁的性能比行锁高的多。
所以,意向锁的作用就是:
当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。
自增锁
AUTOINC锁又叫自增锁(一般简写成 AI 锁),是一种表锁,当表中有自增列(AUTOINCREMENT)时出现。当插入表中有自增列时,数据库需要自动生成自增值,它会先为该表加AUTOINC 表锁,阻塞其他事务的插入操作,这样保证生成的自增值肯定是唯一的。AUTOINC锁具有如下特点:
AUTO_INC 锁互不兼容,也就是说同一张表同时只允许有一个自增锁;
自增值一旦分配了就会 +1,如果事务回滚,自增值也不会减回去,所以自增值可能会出现中断的情况。
显然,AUTOINC 表锁会导致并发插入的效率降低,为了提高插入的并发性,MySQL从 5.1.22 版本开始,引入了一种可选的轻量级锁(mutex)机制来代替AUTOINC 锁,可以通过参数 innodbautoinclockmode 来灵活控制分配自增值时的并发策略。
比如:
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) ,
`password` varchar(255) ,
`email` varchar(255),
`age` int,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
从上面的建表语句中可以看出,t_user表中的id就用到了自增。
行锁类型
根据锁的粒度可以把锁细分为表锁和行锁,行锁根据场景的不同又可以进一步细分。下面要说的三种锁,也是我们面试中的加分项,所以很有必要来聊聊。
记录锁(Record Locks)
间隙锁(Gap Locks)
临键锁(Next-Key Locks)
记录锁(Record Locks)
记录锁就是为某行记录加锁,它封锁该行的索引记录:
-- id 列为主键列或唯一索引列
SELECT * FROM t_user WHERE id = 1 FOR UPDATE;
id 为 1 的记录行会被锁住。
需要注意的是:id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁。
同时查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。
我们也可以在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:
-- id 列为主键列或唯一索引列
UPDATE t_user SET age = 50 WHERE id = 1;
其实。还是蛮简单的哈,但是很多人估计也还都不知道。
间隙锁(Gap Locks)
间隙锁基于非唯一索引,它锁定一段范围内的索引记录。间隙锁基于下面将会提到的Next-Key Locking 算法,请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
SELECT * FROM t_user WHERE id BETWEN 1 AND 10 FOR UPDATE;
即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。
除了手动加锁外,在执行完某些 SQL后,InnoDB也会自动加间隙锁。
临键锁(Next-Key Locks)
临键锁是一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
比如:表信息 t_user(id PK, age KEY, name)
该表中 age 列潜在的临键锁有:
在事务 A 中执行如下命令:
-- 根据非唯一索引列 UPDATE 某条记录
UPDATE t_user SET name = Vladimir WHERE age = 24;
-- 或根据非唯一索引列 锁住某条记录
SELECT * FROM t_user WHERE age = 24 FOR UPDATE;
不管执行了上述 SQL 中的哪一句,之后如果在事务 B 中执行以下命令,则该命令会被阻塞:
INSERT INTO t_user VALUES(100, 26, 'tian');
很明显,事务 A 在对 age 为 24 的列进行 UPDATE 操作的同时,也获取了 (24, 32] 这个区间内的临键锁。
不仅如此,在执行以下 SQL 时,也会陷入阻塞等待:
INSERT INTO table VALUES(100, 30, 'zhang');
那最终我们就可以得知,在根据非唯一索引 对记录行进行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作时,InnoDB 会获取该记录行的 临键锁 ,并同时获取该记录行下一个区间的间隙锁。
即事务 A在执行了上述的 SQL 后,最终被锁住的记录区间为 (10, 32)。
死锁
死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并发读写请求场景中。当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” Deadlock found when trying to get lock...”。
从死锁的定义来看,MySQL 出现死锁的几个要素为:
两个或者两个以上事务
每个事务都已经持有锁并且申请新的锁
锁资源同时只能被同一个事务持有或者不兼容
事务之间因为持有锁和申请锁导致彼此循环等待
死锁分析思路
大致分为两个步骤:
查看死锁日志时,首先看一下发生死锁的事务等待获取锁的语句都是啥。
找到发生死锁的事务中所有的语句之后,对照着事务获取到的锁和正在等待的锁的信息来分析死锁发生过程。
如何预防死锁?
innodb_lock_wait_timeout 等待锁超时回滚事务
直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另一个事务就能继续执行。
wait-for graph算法来主动进行死锁检测
每当加锁请求无法立即满足需要并进入等待时,wait-for graph算法都会被触发。
wait-for graph要求数据库保存以下两种信息:
锁的信息链表
事务等待链表
那么如何解决死锁?
1.等待事务超时,主动回滚。
2.进行死锁检查,主动回滚某条事务,让别的事务能继续走下去。
下面提供一种方法,解决死锁的状态:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;--查看正在被锁的事务
kill trx_mysql_thread_id;--(上图trx_mysql_thread_id列的值)
总结
文章比较长,能认真看到这里的你已经非常棒了。文中一共讲了13中锁:
乐观锁
悲观锁
行锁
表锁
共享锁
排它锁
间隙锁
记录锁
临键锁
读写锁
意向排它锁
意向共享锁
死锁
此时,如果面试官再来问我,不能说全部能说出来,但是最少能回答的七八种是没问题了,也不至于面试冷场,尬聊。