MySQL 锁机制
1.MySQL锁机制概述
1.1什么是锁
锁是计算机协调多个进程或纯线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所在有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决。
1.2锁的运作
事务T在度某个数据对象(如表、记录等)操作之前,先向系统发出请求,对其加锁,加锁后事务T就对数据库对象有一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象。
1.3锁定机制分类
锁定机制就是数据库为了保证数据的一致性而使各种共享资源在被并发访问访问变得有序所设计的一种规则。MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。
按照锁类型分类:
-
排它锁:会阻塞其他事务读和写
又称写锁,X锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对加任何类型的锁,知道T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
-
共享锁:会阻塞其他事务修改表数据
又称读锁,S锁,若事务T对数据对象A加上S锁,则其他事务只能再对A加S锁,而不能X锁,直到T释放A上的锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
排它锁:
Session1 | Session2 |
---|---|
获得表mylock的write锁定 image-20200514041706638 | 待Session1开启写锁后,Session2再开启终端 |
当前Session1对锁定表的查询+更新+插入均可执行 image-20200514042202682 | 其他Session队锁定表的查询被阻塞,需要等待锁被释放 image-20200514042129157 |
释放锁 image-20200514042245435 | Session2获得锁, image-20200514042309422 |
共享锁:
Session1 | Session2 |
---|---|
获得mylock的read锁定 image-20200514163038565 | 连接终端 |
当前终端可以查看表记录 image-20200514163225001 | 其他session也可以查询表记录 image-20200514163306373 |
当前session不能查询其他没有锁定的表 image-20200514163400464 | 其他session可以更新或查询其他未锁定的表 image-20200514163559463 |
当前session插入或更新锁定的表会提示错误 image-20200514163902187 | 其他session插入会更新锁定的表会一直等待 image-20200514163944917 |
当前session释放锁 image-20200514164102266 | session2获得锁,更新操作完成 image-20200514164048105 |
按照锁的数据粒度分类:
-
行级锁定:行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。
缺陷:由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁
-
表级锁定:表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
和行级锁定相反,表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。
缺陷:锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并发度大打折扣
-
页级锁定:页级锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。
缺陷:页级锁定和行级锁定一样,会发生死锁
从这里我们应该引申去思考行锁更多的缺点:(因为我们执行sql主要依赖行锁来提高并发度)
1- 比表级锁、页级锁消耗更多内存
2- 如果你在大部分数据上经常进行GROUP BY操作或者必须经常扫描整个表,比其它锁定明显慢很多。
3- 更容易发生死锁。
1.4数据库事务机制
1.4.1事务ACID
事务简称ACID,是恢复和并发控制的基本单位
-
A 原子性(Atomicity):
指一个事务要么全部执行,要么不执行.也就是说一个事务不可能只执行了一半就停止了.比如你从取款机取钱,这个事务可以分成两个步骤:1划卡,2出钱.不可能划了卡,而钱却没出来.这两步必须同时完成.要么就不完成. -
C 一致性(Consistency):
指事务的运行并不改变数据库中数据的一致性.例如,完整性约束了a+b=10,一个事务改变了a,那么b也应该随之改变. -
I 独立性(Isolation):
事务的独立性也有称作隔离性,是指两个以上的事务不会出现交错执行的状态.因为这样可能会导致数据不一致. -
D 持久性(Durability):
事务的持久性是指事务执行成功以后,该事务所对数据库所作的更改便是持久的保存在数据库之中,不会无缘无故的回滚
1.4.2 并发调度问题
- 脏读:A事务读取B事务尚未提交的更改数据,并在这个数据基础上操作。如果B事务回滚,那么A事务读到的数据根本不是合法的,称为脏读。
- 不可重复读:A事务读取了B事务已经提交的更改数据。比如A事务第一次读取数据,然后B事务更改该数据并提交,A事务再次读取数据,两次读取的数据不一样。
- 幻读:A事务读取了B事务已经提交的新增数据。注意和不可重复读的区别,这里是新增与删除,不可重复读是更改。这两种情况对策是不一样的,对于不可重复读,只需要采取行级锁防止该记录数据被更改或删除,然而对于幻读必须加表级锁,防止在这个表中新增一条数据。
- 丢失更新:A事务撤销时,把已提交的B事务的数据覆盖掉。
- 覆盖更新:A事务提交时,把已提交的B事务的数据覆盖掉。
不可重复读与幻读的区别:
不可重复读重在修改,就是你刚读过的一个数据,再读一次又一不一样了。
而幻读重在记录的增删,就是你第一次读的数据量,第二次读又不一样了。
所以要解决:
解决不可重复读:这个数据只有在修改事务完全提交后才可读取数据。
解决幻读:符合这个条件下的数据,在操作事务完成处理之前,其他事务不可添加新数据、或者删除数据。
1.5事务隔离级别
这个是mysql用意向锁来解决事务并发问题,为了区别封锁协议,弄了一个新概念隔离性级别:包括Read Uncommitted、Read Committed、Repeatable Read、Serializable。mysql 一般默认Repeatable Read。
-
读未提交(Read Uncommited,RU)
一句总结:读取数据一致性在最低级别,只能保证不读物理上损坏的数据,会脏读,会不可重复读,会幻读。
这种隔离级别下,事务间完全不隔离,会产生脏读,可以读取未提交的记录,实际情况下不会使用。
-
读提交(Read commited,RC)
一句总结:读取数据一致性在语句级别,不会脏读,会不可重复读,会幻读。
仅能读取到已提交的记录,这种隔离级别下,会存在幻读现象,所谓幻读是指在同一个事务中,多次执行同一个查询,返回的记录不完全相同的现象。幻读产生的根本原因是,在RC隔离级别下,每条语句都会读取已提交事务的更新,若两次查询之间有其他事务提交,则会导致两次查询结果不一致。虽然如此,读提交隔离级别在生产环境中使用很广泛。
-
可重复读(Repeatable Read, RR)
一句总结:读取数据一致性在事务级别,不会脏读,不会不可重复读,会幻读。
可重复读隔离级别解决了不可重复读的问题,但依然没有解决幻读的问题。不可重复读重点在修改,即读取过的数据,两次读的值不一样;而幻读则侧重于记录数目变化【插入和删除】。
-
串行化(Serializable)
一句总结:读取数据一致性在最高级别,事务级别,不会脏读,不会不可重复读,不会幻读。
在串行化隔离模式下,消除了脏读,幻象,但事务并发度急剧下降,事务的隔离级别与事务的并发度成反比,隔离级别越高,事务的并发度越低。实际生产环境下,dba会在并发和满足业务需求之间作权衡,选择合适的隔离级别。
隔离级别 脏读 不可重复读 幻读 读未提交 会 会 会 读已提交 不会 会 会 可重复读 不会 不会 会 串行化 不会 不会 不会
1.6活锁与死锁
采用封锁的方法可以有效防止数据的不一致性,单封锁本身会引起麻烦,就是死锁和活锁。
1.6.1活锁
如果事务T1封锁了数据对象R后,事务T2也请求封锁R,于是T2等待,接着T3也请求封锁R。当T1释放了加载R上的锁后,系统首先批准T3的请求,T2只能继续等待。接着T4也请求封锁R,T3释放R上的锁后,系统又批转了T4的请求。这样的一直循环下去,事务T2就只能永远等待了,这样情况叫活锁。
解决办法:采用先来先服务的队列策略。队列式申请。
1.6.2死锁
当两个事务分别锁定了两个单独的对象,这时每一个事务都要求在另一个事务锁定的对象上获得一个锁,因此每一个事务都必须等待另一个事务释放占有的锁。这就发生了死锁了