S 锁与 X 锁的爱恨情仇《死磕MySQL系列 四》

2021年11月26日 阅读数:4
这篇文章主要向大家介绍S 锁与 X 锁的爱恨情仇《死磕MySQL系列 四》,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

系列文章

1、原来一条select语句在MySQL是这样执行的《死磕MySQL系列 一》数据库

2、一辈子挚友redo log、binlog《死磕MySQL系列 二》并发

3、MySQL强人“锁”难《死磕MySQL系列 三》学习

获取MySQL各类学习资料

前言

下边两幅图还熟悉吧!就是第三期文章中的前言,但上一期文章并未说起死锁,只是引出了全局锁、表锁的概念。本期文章将继续聊聊锁的内容。优化

Lock wait timeout exceeded; try restarting transaction编码

Deadlock found when trying to get lock; try restarting transactionurl

1、行锁

行锁的锁粒度最小,发送锁冲突的几率最低,并发度也最高。spa

问题:MySQL的全部存储引擎都支持行锁吗?.net

不是的,MySQL中只有Innodb存储引擎才支持行锁,其它的并不支持,MyIsam存储引擎也只支持表锁。线程

因此Myisam存储引擎只能使用表锁来解决并发,表锁开销小,加锁快,锁定粒度大,发生锁冲突的几率最高,并发度最低。设计

问题:锁粒度指的是什么?

这种名词不能只记名字,须要知道其表明的含义。锁粒度指的是加锁的范围。

上期文章讲的全局锁锁的是整库、表锁锁定的全表、行锁指的是锁定某一行或某个范围的数据。

问题:如何加行锁?

Innodb存储引擎在执行update、delete、insert语句时会隐式加排它锁,而对于select不会加任何锁。

一样也能够手动加锁。

共享锁:select * from tableName where id = 100 lock in share more

排它锁:select * from tableName where id = 100 for update

共享锁、排它锁也被称之为读锁、写锁。读锁与读锁之间不互斥,读锁与写锁、写锁与写锁之间是互斥的。

问题:为何要加锁?

MySQL事务的四大特性分别是原子性、隔离性、一致性、持久性,当你了解完事务的四大特性以后就发现都是为了保证数据一致性为最终目的的。

常说一句话有人地方就有江湖,放在MySQL中是有锁的地方就有事务。

因此说加锁就是为了保证当事务结束后,数据库的完整性约束不被破坏,从而确保数据一致性。

2、两阶段锁

问题:两阶段锁是什么?

说实话,这个名字属实很唬人,猛然间你有没有想到另外一个名词两阶段提交。这里回忆一下,两阶段提交是确保redo log跟binlog同时提交成功,如有一方提交失败则回滚。

在Innodb存储引擎中,行锁是在须要的时候加上的,但并非不须要了就直接释放的,而是要等到事务结束才释放。

案例:解释两阶段锁

上图中MySQL1客户端开启事务并执行了两条update语句,紧接着MySQL2开启另外一个事务执行update语句,那么此时MySQL的更新语句会执行成功吗?

答案确定是不能的。

这个结论取决于MySQL1事务在执行完两条 update 语句后,持有哪些锁,以及在何时释放。你能够验证一下:MySQL2事务 update 语句会被阻塞,直到MySQL1事务 执行 commit 以后,才能继续执行。

万事有因必有果,有头必有尾,锁是开启事务后添加的也需提交事务后解除。

如今你理解了两阶段锁,那么试想一下对你在写代码有什么帮助吗?

3、理解死锁

这幅图是咔咔在2019年画的,当时用这种方式来解释死锁对于一部分伙伴来讲属实有点绕。

错误的理解:以前在一个博文中看到对死锁是这样解释的

现实中这样的案例比比皆是,家里有两个小孩,给老大冲了一杯奶,这时老二过来也想喝。但奶嘴只有一个,此时老二只能处于等待状态,让老大先喝完。这个就是死锁。

不要把锁等待跟死锁一同对待,锁等待是,一个事务中的语句添加了共享锁,另外一个事务开启了排它锁。此时就须要等待共享锁的释放,这个过程是锁等待。而死锁是两个事务互相等待对方。

4、优化你的代码尽可能防止死锁

知道两阶段锁后,在之后的代码实现中要把最可能形成锁冲突也就是死锁的语句放到最后边。

问题:如何理解放到最后边这句话?

这样一个业务场景。

每到中午吃饭时间都是好几我的一块儿出去,吃饭得付钱吧!复现一下这个流程。

1.你给商家付了10块钱,这笔钱从你的余额中扣。

2.给商家的帐户添加10元。

3.记录一条交易日志。

在这个过程当中可得知进行了两次update操做,一次insert操做。使用为了保证交易的原子性数据的一致性此时必须得把三个操做放到一个事务。

在这三个操做中最容器形成锁冲突的就是第2步给商家的帐户添加钱。

因此在编码过程当中须要把第2步放到最后一步执行,保证在一样结果下锁住的时间最短。这样能够在编码的程度上尽可能保证事务之间锁等待,提升事务并发度。

5、解释死锁的两种方案

第一种方式

MySQL已经给我们提供好了,使用参数innodb_lock_wait_timeout来设置超时时间。若等待时间超过设置的值则返回超时错误。

在MySQL8.0版本中此值默认为50s,意味着当出现死锁之后,被锁住的线程须要50s才会自动退出,而后其它线程才会继续执行。这个等待时间通常是没法接受的。

但设置时间过短会形成不少锁等待的语句直接返回超时,形成严重误伤。

重要的话再说一遍:“不要把锁等待跟死锁一同对待,锁等待是,一个事务中的语句添加了共享锁,另外一个事务开启了排它锁。此时就须要等待共享锁的释放,这个过程是锁等待。而死锁是两个事务互相等待对方。

第二种方式

另外一个种方式,一样MySQL也给提供了一个参数innodb_deadlock_detect,默认值为on,意思是当发现死锁后,MySQL主动回滚死锁链条中的某一个事务,让其余事务得以继续执行。

检测死锁的流程是当一个事务被堵住时,就要看它所在的线程是否被别的线程锁住,如若没有则继续找下一个线程进行检测,最后判断是否出现了循环等待,也就是死锁。

过程示例:新来的线程F,被锁了后就要检查锁住F的线程(假设为D)是否被锁,若是没有被锁,则没有死锁,若是被锁了,还要查看锁住线程D的是谁,若是是F,那么确定死锁了,若是不是F(假设为B),那么就要继续判断锁住线程B的是谁,一直走知道发现线程没有被锁(无死锁)或者被F锁住(死锁)才会终止

问题:平时在开发中使用那种方案呢?

存在必合理,通常状况仍是采用第二种方式,这种方式在有死锁时是可以快速进行处理的。

做为开发者确定听过一句这样的话要么用空间换时间,要么用时间换空间。二者只可兼一种。

这种方式虽能够很是迅速的处理死锁问题,一样也会带来额外的负担。

思考:带来了那些额外的负担?

假设你负责的业务都须要更新同一行数据。

此时按照第二种方式,当发现死锁后,主动回滚死锁链条的某一个事务,那么,每个进来被堵住的线程,都要判断是否是因为本身的加入致使死锁,这个时间复杂度是O(n)的操做。

假设有1000个线程都在更新同一行,操做的数据量是100W,检测出来死锁消耗资源还不怕,若最终检测结果没有死锁,这个期间消耗的CPU资源是很是高的。

就如何解决这种问题再进行谈论一下。

6、如何解决热点数据的更新

为何要聊这个问题

使用了第二种方案来解决死锁,热点数据死锁检测会很是消耗CPU(每个进来被堵的线程都会检测是否是因为本身的加入致使的死锁,有多是锁等待,但仍是须要作判断,因此很是消耗CPU),因此针对这个问题进行简单讨论一下。

咔咔在其它资料中看到有三种方案。

1.关闭死锁检测 2.控制并发度 3.修改MySQL源码对于更新同一行数据,在进入引擎以前排队。这样就不会出现大量的死锁检测

方案一:关闭死锁检测不考虑

这种方式会出现大量的超时,下降了用户体验,通常状况死锁不会对业务产生严重错误,毕竟出现死锁,数据大不了回滚便可。

方案二:控制并发度

能够把商家帐户分散多个,全部的帐户之和为帐户余额。

例如分了10个子帐户,那么出现更新同一行数据的几率就下降了10倍,这种方式在业务处理时须要简单处理一下。防止帐户余额为0时用户发起退款的逻辑处理。

这种方式仍是很建议你们使用的,从设计上下降死锁发生。

方案三:修改MySQL源码

大多数公司连DBA都没有,何谈存在能够修改MySQL源码的人,这种对于企业的成本是很是大的,并且也没那个必要。

修改MySQL源码想要实现的功能是当更新同一行数据时,在进入存储引擎以前排队。

这种方案用队列彻底能够解决,因此并不须要从根上解决这个问题。

7、总结

本期从行锁出发引出了两阶段锁,明白了事务提交后才会释放锁。

死锁的产生,如何从代码的角度来减小死锁的产生。

MySQL也给提供了两种方案来解决死锁问题,对于这两种方案咔咔也给了不一样的观点。根据本身的状况来使用。

在这期文章中并无演示死锁案例,在后边的文章中咔咔会给你们列举几种典型的死锁案例。

坚持学习、坚持写做、坚持分享是咔咔从业以来所秉持的信念。愿文章在偌大的互联网上能给你带来一点帮助,我是咔咔,下期见。