笔记来源:MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!
事务的隔离性由这数据库锁来实现
。在开发过程中加锁是为了保证数据的一致性,这个思想在数据库领域中同样很重要
。对并发操作进行控制
,因此产生了锁。同时锁机制也为实现MySQL的各个隔离级别提供了保证。锁冲突也是影响数据库并发访问性能的一个重要因素
。所以锁对数据库而言显得尤其重要,也更加复杂。在这种情况下会发生脏写
的问题,任何一种隔离级别都不允许这种问题的发生
。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行
,这个排队的过程其实是通过锁
来实现的。这个所谓的锁其实是一个内存中的结构
。
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。比如,事务T1要对这条记录做改动,就需要生成一个锁结构与之关联:
在锁结构里有很多信息,为了简化理解,只把两个比较重要的属性拿了出来:
trx信息
:代表这个锁结构是哪个事务生成的。is_waiting
:代表当前事务是否在等待。当事务T1改动了这条记录后,就生成了一个锁结构与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false
,我们把这个场景就称之为获取锁成功
,或者加锁成功
,然后就可以继续执行操作了。
在事务T1提交之前,另一个事务T2也想对该记录做改动,那么先看看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构的is_waiting属性值为true
,表示当前事务需要等待,我们把这个场景就称之为获取锁失败
,或者加锁失败
,图示:
在事务T1提交之后,就会把该事务生成的锁结构释放
掉,然后看看还有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒
,让它继续执行,此时事务T2就算获取到锁了。效果图就是这样:
比如MySQL在 REPEATABLE READ隔离级别上就已经解决了幻读问题
。ReadView
,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建
)。查询语句只能读到在生成ReadView之前已提交事务所做的更改
,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突
。ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改
,也就是避免了脏读现象这样也就意味着读操作和写操作也像写-写操作那样排队执行
。脏读
的产生是因为当前事务读取了另一个未提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。不可重复读
的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,如果在当前事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录,自然也不会发生不可重复读了。幻读
问题的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录。采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁)对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写
和读-写
的情况可能会引起一些问题,需要使用MVCC
或者加锁
的方式来解决它们。
在使用加锁的方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写和读-写情况中的操作相互阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁
(Shared Lock,S锁)和排他锁
(Exclusive Lock,X锁),也叫读锁
和写锁
。
对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。
X锁 | S锁 | |
---|---|---|
X锁 | 不兼容 | 不兼容 |
S锁 | 不兼容 | 兼容 |
在采用加锁方式解决脏读、不可重复读、幻读这些问题时,读取一条记录时需要获取该记录的S锁,其实是不严谨的,有时候需要在读取记录时就获取记录的X锁,来禁止别的事务读写该记录,为此MysQL提出了两种比较特殊的SELECT语句格式:
# 对读取加S锁
SELECT ... LOCK IN SHARE MODE;
#或
SELECT ... FOR SHARE;#(8.日新增语法)
#对读取加X锁
SELECT ... FOR UPDATE;
MySQL8.0新特性:在5.7及之前的版本,SELECT … FOR UPDATE,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。在8.0版本中,SELECT …FOR UPDATE,SELECT … FOR SHARE添加NOWAIT、SKIP LOCKED语法,跳过锁等待,或者跳过锁定,立即返回。如果查询的行已经加锁:
为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好
,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度
”的概念。锁的粒度主要分为表级锁、页级锁和行锁。
该锁会锁定整张表,它是MySQL中最基本的锁策略,并且表锁是开销最小的策略
(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。当然,锁的粒度大的致命缺点
就是出现锁资源争用的概率是最高的
,导致并发率大大降低
。
一般情况下,不会使用InnoDB存储引擎提供的表级别的S锁和X锁
。只会在一些特殊情况下,比方说崩溃恢复过程中用到。
手动获取InnoDB存储引擎提供的表t的S锁或者X锁可以这么写:
# InnoDB存储引擎会对表t加表级别的S锁。
LOCK TABLES t READ;
# InnoDB存储引擎会对表t加表级别的X锁。
LOCK TABLES t WRITE;
尽量避免
在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁,关于InnoDB表级别的S锁和X锁了解一下就可以了
。
MyISAM在执行查询语句(SELECT)前,会给涉及的所有表加读锁,在执行增删改操作前,会给涉及的表加写锁。
锁类型 | 自己可读 | 自己可写 | 自己可操作其他表 | 他人可读 | 他人可写 |
---|---|---|---|---|---|
读锁 | 是 | 否 | 否 | 是 | 否 |
写锁 | 是 | 是 | 否 | 否 | 否 |
InnoDB支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁。
意向锁分为两种:
事务要获取某些行的S锁,必须先获得表的IS锁
。事务要获取某些行的X锁,必须先获得表的IX锁
。意向锁是由存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB会先获取该数据行所在数据表的对应意向锁。
意向锁要解决的问题
页
或行
是否存在锁;如果存在意向锁,那么此时就会受到t1控制的表级意向锁的阻塞
。t2在锁定表前不必检查页锁或行锁,只需检查表上的意问锁即可。意向锁的兼容互斥性
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
意向共享锁(IS) | 兼容 | 兼容 |
意向排他锁(IX) | 兼容 | 兼容 |
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
共享锁(S) | 兼容 | 互斥 |
排他锁(X) | 互斥 | 互斥 |
意向锁不会与行级的共享锁/排他锁互斥!
意向锁的并发性
意向锁之间互不排斥
,但除了IS与S兼容外,意向锁会与共享锁/排他锁互斥。只会和表级的X,S发生冲突
。意向锁实例
行锁(Row Lock))也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要的注意的是,MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。
InnoDB与MyISAM的最大不同有两点:一是支持事务
((TRANSACTION);二是采用了行级锁
。
记录锁也就是仅仅把一条记录锁上,官方的类型名称为:LOCK_REC_NOT_GAP。比如我们把id值为s的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为s的记录,对周围的数据没有影响。
记录锁是有S锁和X锁之分的,称之为S型记录锁
和X型记录锁
。
为了解决幻读的幻影记录问题
,InnoDB提出了一种称之为Gap Locks的锁,官方的类型名称为:LOCK_GAP,我们可以简称为gap锁。比如,把id值为8的那条记录加一个gap锁的示意图如下。
图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录
,其实就是id列的值(3,8)这个区间的新记录是不允许立即插入的。直到拥有这个gap锁的事务提交了之后,id列的值在区间(3,8)中的新记录才可以被插入。
如果想再最后一条记录之后加间隙锁,或者仔第一条记录前加间隙锁,则要使用数据页的两条伪记录Infimum记录,supremum记录。例如在本例中,如果想再id为20的记录后面加间隙锁,可以使用语句:
select * from student where id >20 lock in share mode;
间隙锁死锁实例
:
Session1 | Session2 |
---|---|
begin; select * from student where id = 5 for update; |
begin; select * from student where id = 5 for update; |
INSERT INTO student VALUES(5,'宋红康,‘二班’);#阻塞 | |
INSERT INTO student VALUES(5,'宋红康;‘二班’); (ERROR1213(40001): Deadlock found when trying to get lock; tryrestarting transaction) |
有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以InnoDB就提出了Next-Key Locks,官方的类型名称为:LOCK_ORDINARY。Next-Key Locks是在存储引擎innodb、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks
。比如,我们把id值为8的那条记录加一个next-key锁的示意图如下:
临建锁的本质就是一个记录锁和一个间隙锁的合体
,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙。
我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了gap锁(next-key锁也包含gap锁),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。InnoDB就把这种类型的锁命名为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION,我们称为插入意向锁。插入意向锁是一种gap锁,不是意向锁
,在insert操作时产生。
比如,现在T1为id值为s的记录加了一个gap锁,然后T2和T3分别想向student表中插入id值分别为4、5的两条记录,所以现在为id值为8的记录加的锁的示意图就如下所示:
从图中可以看到,由于T1持有gap锁,所以T2和T3需要生成一个插入意向锁的锁结构并且处于等待状态。当T1提交后会把它获取到的锁都释放掉,这样T2和T3就能获取到对应的插入意向锁了,插入意向锁之间不会相互阻塞
,故T2和T3之间也并不会相互阻塞,它们可以同时获取到id值为8的插入意向锁然后执行插入操作。插入意向锁也不会阻止别的事务继续获取该记录上任何类型的锁
。
每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的
。当某个层级的锁数量超过了这个层级的阈值
时,就会进行锁升级
。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。思维方式
。某种程度上来说,乐观锁和悲观锁并不是锁,而是锁的设计思想
。悲观锁,顾名思义,就是很悲观,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁
,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据日都需要阻塞挂起。Java中 synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
悲观锁引用举例:秒杀超卖现象
悲观锁不适用的场景较多,它存在一些不足,因为悲观锁大多数情况下依靠数据库的锁机制来实现,以保证程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是长事务而言,这样的开销往往无法承受,这时就需要乐观锁。
很乐观,认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁
,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制
,而是通过程序来实现
。在程序上,我们可以采用版本号机制
或者CAS机制
实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下的原子变量类就是使用了乐观锁的一种实现方式:CAS实现的。乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读–写和写–写的冲突。
不通过特性语句,而是搜索引擎自动加锁的方式称为隐式加锁
一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id的存在,相当于加了个隐式锁。别的事务在对这条记录加S锁或者X锁时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构然后自己再生成一个锁结构后进入等待状态。隐式锁是一种延迟加锁的机制,从而来减少加锁的数量。
隐式锁在实际内存对象中并不含有这个锁信息。只有当产生锁等待时,隐式锁转化为显式锁。
隐式锁的逻辑过程如下:
两个
或者两个以上事务持有锁
并且申请新的锁
不兼容
循环等待
对一条记录加锁的本质就是在内存中创建一个锁结构与之关联,那么是不是一个事务对多条记录加锁,就要创建多个锁结构呢?理论上创建多个锁结构没问题,但是如果一个事务要获取10000条记录的锁,生成10000个锁结构也太崩溃了!
在对不同记录加锁时,如果符合下边这些条件的记录会放到一个锁结构中:
InnoDB存储引擎中的锁结构如下:
结构解析
关于MySQL锁的监控,我们一般可以通过检查InnoDB_row_lock等状态变量来分析系统上的行锁的争夺情况
SHOW STATUS LIKE 'innodb_row_lock%'
对各个状态量的说明如下:
Innodb_row_lock_time
:从系统启动到现在锁定总时间长度;(等待总时长)Innodb_row_lock_time_avg
:每次等待所花平均时间;(等待平均时长)。Innodb_row_lock_waits
:系统启动后到现在总共等待的次数;(等待总次数)尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
其他监控方法: