mysql中存在很多锁,而网上很多分类介绍都是语焉不详,且不清晰。下面这篇文章将详细介绍mysql的锁机制。
从性能来分
乐观锁
何为乐观锁,从语义上就能看出,就是很乐观,认为每次取数据时别人不会修改。所以并不会实际上锁,只是每次更新数据的时候,会去检查在自己在读取和更新数据的这段时间别人有没有修改过这个数据,如果没有则直接更新。如果修改过,要么放弃修改要么重新读取更新数据。
乐观锁相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
乐观锁适合读操作较多的场景。如果在写操作较多的场景使用乐观锁会导致比对次数过多,影响性能。
那么我们如何实现乐观锁呢,一般来说有以下2种方式:
1.使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
2.乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
悲观锁
就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程A,其他线程就必须等待该线程A处理完才可以处理
数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及syncronized实现的锁均为悲观锁
悲观锁适合写操作较多的场景
无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。
从对数据库操作的类型分:
mysql的所有的隔离级别数据修改操作都会加写锁,而查询不会加任何锁,不过查询也可以通过for update加写锁,通过lock in share mode加读锁。
读锁(共享锁,S锁(Shared))
针对同一份数据,多个读操作可以同时进行而不会互相影响。换言之,读锁会堵塞写但不会堵塞读。
写锁(排它锁,X锁(eXclusive))
当前写操作没有完成前,它会阻断其他写锁和读锁
意向锁(Intention Lock)
又称I锁,针对表锁,主要是为了提高加表锁的效率,是mysql数据库自己加的。当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有行锁了, 其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁。特别是表中的记录很多时,逐行判断加表锁的方式效率很低。而这个标识就是意向锁。
意向锁主要分为:
意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁。
意向排他锁,IX锁,对整个表加排他锁之前,需要先获取到意向排他锁
从对数据操作的粒度分:
表锁
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。
innodb的 表锁,分成共享锁和排他锁,表锁是innodb引擎⾃动加的,不⽤我们⾃⼰去加。但是也可以通过命令给表加锁解锁。
页锁
只有BDB存储引擎支持页锁,页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
行锁
每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
InnoDB的行锁实际上是针对索引加的锁(在索引对应的索引项上做标记),不是针对整个行记录加的锁。并且该索引不能失效,否则会从行锁升级为表锁。(RR级别会升级为表锁,RC级别不会升级为表锁) 比如我们在RR级别执行如下sql
select * from account where money = 103 for update; ‐‐where条件里的money字段无索引,数据库中money在100到200之间只有money为100、200两条数据
则其它Session对该表任意一行记录做修改操作都会被阻塞住。因为money没有索引,所以会将扫描到的主键索引和间隙加锁,若有索引则会锁二级索引,以及主键索引符合条件的记录
比如我们在RR级别下给money添加索引,再去执行上面的sql,只会锁住二级索引,不会去锁主键索引, 即无法在money的(100,200)的范围内插入数据。
关于RR级别行锁升级为表锁的原因分析:
因为在RR隔离级别下,需要解决不可重复读和幻读问题,所以在遍历扫描聚集索引记录时,为了防止扫描过的索引被其它事务修改(不可重复读问题) 或 间隙被其它事务插入记录(幻读问题),从而导致数据不一致,所以MySQL的解决方案就是把所有扫描过的索引记录和间隙都锁上,这里要注意,并不是直接将整张表加表锁,只是把扫描过的索引和间隙加锁。 因为不一定能加上表锁,可能会有其它事务锁住了表里的其它行记录。
其他锁
间隙锁(Gap Lock)
间隙锁,锁的就是两个值之间的空隙,间隙锁是在可重复读隔离级别下才会生效。Mysql默认级别是repeatable-read,有幻读问题,间隙锁是可以解决幻读问题的。
例:假设account表里数据如下:
那么间隙就有 id 为 (3,8),(8,15),(15,正无穷) 这三个区间,在Session_1下面执行如下sql:
select * from account where id = 13 for update;
则其他Session没法在这个(8,15)这个间隙范围里插入任何数据。 如果执行下面这条sql:
select * from account where id = 25 for update;
则其他Session没法在这个(15,正无穷)这个间隙范围里插入任何数据。也就是说,只要在间隙范围内锁了一条不存在的记录会锁住整个间隙范围,不锁边界记录,这样就能防止其它Session在这个间隙范围内插入数据,就解决了可重复读隔离级别的幻读问题。
innodb自动使用间隙锁的条件:
- 必须在RR级别下
- 检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)
临键锁(next-key锁)
next-key锁其实包含了记录锁(行锁)和间隙锁,即锁定一个范围,并且锁定记录本身,InnoDB默认加锁方式是next-key 锁。 上面的案例一session 1中的sql是:
select * from account where id = 13 for update;
next-key锁锁定的范围为间隙锁+记录锁,即区间(8,13)(13,15)加间隙锁,同时id = 13的记录加记录锁(行锁)。