参考资料
https://www.cnblogs.com/leedaily/p/8378779.html
https://www.jianshu.com/p/163c4df21185
https://www.jianshu.com/p/120fcab69de6
https://blog.csdn.net/qq_37933685/article/details/80717515
讨论MySQL的锁机制肯定要区分不同的存储引擎,常用的有MyISAM和InnoDB。MyISAM支持表级锁,InnoDB支持行级锁和表级锁,此外还有一些引擎支持介于两者之间的页级锁。本文单说一下InnoDB的锁。InnoDB默认行级锁。
一、事务隔离方式
数据库实现事务隔离的方式,基本分为两种:加锁和数据多版本并发控制
1、加锁
在读取数据前,对其加锁,阻止其他事务对数据进行修改。
2、数据多版本并发控制
通过数据多版本并发控制(MVCC)生成数据快照(Snapshot)。
数据读取分为快照读 (snapshot read)与当前读 (current read)。
1)快照读:读取的是记录的可见版本 (有可能是历史版本),不用加锁。
举例如下:
select * from table where ?;
2)当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
举例如下:
select * from table where ? lock in share mode;(共享锁)
select * from table where ? for update;(排它锁)
insert into table values (…);(排它锁)
update table set ? where ?;(排它锁)
delete from table where ?;(排它锁)
备注:如果要读取的行上存在X锁,读操作不会等行上X锁释放,而是去读取行的一个快照版本。由于没有事务会对快照数据进行修改,所以对于快照的读取是不用上锁的。在read committed和repeatable read的事务隔离级别中,都是使用MVCC进行读操作。read committed选择最新的快照版本进行读(事务A)如果另一个事务B修改并提交了该行,事务A会读到事务B修改后的,产生不可重复读问题;在repeatable read的事务隔离级别中,即便B修改提交了该行,只要事务A不提交,就不会读到事务B修改后的行,避免了不可重复读问题。
二、锁的分类
InnoDB共有七中类型的锁。
共享/排它锁(Shared and Exclusive Locks)
意向锁(Intention Locks)
记录锁(Record Locks)
间隙锁(Gap Locks)
临键锁(Next-key Locks)
插入意向锁(Insert Intention Locks)
自增锁(Auto-inc Locks)
以上锁都是在操作之前先上锁,这些锁统称为悲观锁(Pessimistic Lock),属于数据库级别。
乐观锁是先修改,保存时判断是够被更新过,属于应用级别。
1、共享/排它锁
共享锁(Share Locks):也叫S锁、读锁,读取数据时加的锁。事务T对数据A加共享锁,T可以读A,但不能修改A。其它事务只能对A加共享锁。共享锁就是大家可以读,但都别改。
排他锁(EXclusive Locks):也叫X锁、写锁,修改数据时加的锁。事务T对数据A加排它锁,T可以读A、修改A。其它事务不能对A加任何锁。其它事务不能修改,只能快照查找。
说明:
1)读读可并行,读写/写写不可并行。写事务没有提交,加了锁的select也会阻塞(普通的select可以读,快照读)
2)update,delete,insert自动给涉及到的数据加上排他锁(隐式加锁),select语句默认不会加任何锁类型
3)显示加锁,可以使用select …for update语句加排它锁。加锁放在事务中,否则没有意义
4)显示加锁,可以使用select … lock in share mode语句加共享锁。加锁放在事务中,否则没有意义
5)加排他锁的数据行在其他事务中不能修改数据,不能通过for update和lock in share mode锁的方式查询数据
6)加排他锁的数据行在其他事物中可以直接通过select …from…查询数据,因为普通查询没有任何锁机制
7)S锁和X锁可以是行锁、也可以是表锁
2、意向锁
允许行级锁、表级锁共存引入的。是指未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。为了解决行锁与表锁之间的冲突,比如表中的某一行上加了X锁,该表就不能加X锁了。如果不在表上加意向锁,对表加锁的时候,都要去检查表中的某一行上是否加有行锁,很麻烦。
1)意向锁是一个表级别的锁,事务要获得某些行的S/X锁,必须先获得表对应的IS/IX锁
2)意向锁又分为:意向共享锁、意向排它锁
意向共享锁(intention shared lock):也叫IS锁,它预示着事务有意向对表中的某些行加共享S锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排它锁(intention exclusive lock):也叫IX锁,它预示着事务有意向对表中的某些行加排它X锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
InnoDB行锁模式兼容性列表:
冲突的时候会阻塞当前会话,直到拿到锁,或者锁超时。
3)语法
要设置IS锁:select ... lock in share mode;
要设置IX锁:select ... for update;
3、记录锁
单行记录上的锁,行锁是加在索引上的。
4、间隙锁
使用范围条件(非等于)检索数据,并请求共享或排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,键值在条件范围内,但并不存在的记录,叫做间隙,InnoDB也会对这个间隙加锁,就叫间隙锁(锁定记录之间的范围,但不包含记录本身)。间隙锁为了防止幻读。
举例如下:
emp表有101条记录,分别是id为1、2、3...101
执行select * from emp where empid > 100 for update;会对101加锁,大于101但是不存在的间隙也会加锁。
InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,也会使用间隙锁。
举例如下:
session1中执行:select * from emp where empid=102 for update;其中102记录不存在。
session2中执行:insert into emp (id...) values (201...)。session2出现等待。
5、临键锁
记录锁+ 间隙锁,锁定一个范围,包含记录本身。
6、插入意向锁
间隙锁中存在一种插入意向锁,在insert操作时产生。
作用:阻塞临界锁,防止插入数据,这样就不会幻读。插入意向锁互相是兼容的,允许相同间隙、不同数据的并发插入。
三、锁的实现及其它知识
1、行锁实现方式
1)InnoDB行锁是通过给索引加锁实现的,只有通过索引条件检索数据,才使用行级锁,否则,InnoDB将使用表锁!
举例如下:
session1执行select * from a where id=1 for update; session2执行select * from a where id=2 for update;
session1只给一行加了排它锁,session2给其它行加排它锁时会等待,原因是没有索引的情况下,InnoDB使用表锁。给id列加索引才会锁定行。
2)由于行锁是针对索引加的锁,而不是针对记录,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
举例如下:
准备数据,其中id加索引、name不加索引。请求的不是同一行数据,但是索引建是一样的,id=1,所以session2需要等待。
session1执行:select * from a where id=1 and name='1' for update;
session2执行:select * from a where id=1 and name='4' for update;
3)多个索引时,不同事务使用不同索引锁定不同的行。主键索引、唯一索引、普通索引,InnoDB都会使用行锁对数据加锁。
举例如下:
数据如上图,id是主键索引、name是普通索引
session1执行:select * from a where id=1 for update;锁定了两行
session2执行:select * from a where name='2' for update;记录没有锁定,可以获得锁
session2执行:select * from a where name='4' for update;记录被session1锁定,等待获得锁
2、通过状态变量分析行锁争夺情况
show status like 'innodb_row_lock%';
InnoDB_row_lock_waits和InnoDB_row_lock_time_avg值高说明锁争夺严重
3、锁超时
1)产生锁竞争时,持有锁的一方一直不释放锁,请求锁的事务或session一直等待,默认等50s还没获取锁就放弃同时报错。
2)查看锁超时:show variables like '%innodb_lock_wait_timeout%';
4、死锁
1)死锁定义:
死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。
正常情况下,mysql会检查出死锁,并回滚某一个事务,让另一个事务正常运行。
2)预防死锁:
以相同的顺序更新不同的表,会出现锁等待,但不容易出现死锁。
直接申请足够级别的锁,而非先共享锁,再申请排他锁。
正确加索引。没有索引会引起全表扫描。
5、加表级锁
共享锁:LOCK TABLE table_name [ AS alias_name ] READ
排它锁:LOCK TABLE table_name [AS alias_name][ LOW_PRIORITY ] WRITE
6、乐观锁与悲观锁
1)乐观锁与悲观锁是一种思想,不能与共享锁、排它锁等混为一谈。悲观锁正是利用数据库本身提供的锁机制来实现的。
2)使用悲观锁,须关闭mysql数据库的自动提交属性,即set autocommit=0;
3)MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。
4)乐观锁假设数据没有冲突,提交的时候,对数据进行检测,发现冲突则返回错误信息,让用户决定如何去做。
5)乐观锁的实现方式是记录数据版本,一般使用版本号或时间戳。
数据版本是数据增加的一个版本标识。读数据时,一块读出版本标识,数据每更新一次,同时更新版本标识。当提交更新的时候,判断数据库表对应记录的当前版本标识与第一次取出来的版本标识是否相等,如果相等则予以更新,否则认为是过期数据。
举例如下:
select (status,status,version) from t_goods where id=#{id};
update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
其它博文:
https://blog.csdn.net/qq_40378034/article/details/90904573
https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/105742798
https://blog.csdn.net/chongluo0179/article/details/100803453
https://www.cnblogs.com/leedaily/p/8378779.html
https://www.cnblogs.com/tomasman/p/7704941.html