乐趣区

关于mysql:S-锁与-X-锁的爱恨情仇死磕MySQL系列-四

系列文章

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

二、毕生挚友 redo log、binlog《死磕 MySQL 系列 二》

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

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

Lock wait timeout exceeded; try restarting transaction

Deadlock found when trying to get lock; try restarting transaction

一、行锁

行锁的锁粒度最小,发送锁抵触的概率最低,并发度也最高。

问题:MySQL 的所有存储引擎都反对行锁吗?

不是的,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 中是有锁的中央就有事务。

所以说加锁就是为了保障当事务完结后,数据库的完整性束缚不被毁坏,从而确保数据一致性。

二、两阶段锁

问题:两阶段锁是什么?

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

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

案例:解释两阶段锁

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

答案必定是不能的。

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

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

当初你了解了两阶段锁,那么试想一下对你在写代码有什么帮忙吗?

三、了解死锁

这幅图是咔咔在 2019 年画的,过后用这种形式来解释死锁对于一部分搭档来说属实有点绕。

谬误的了解:之前在一个博文中看到对死锁是这样解释的

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

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

四、优化你的代码尽量避免死锁

晓得两阶段锁后,在当前的代码实现中要把最可能造成锁抵触也就是死锁的语句放到最初边。

问题:如何了解放到最初边这句话?

这样一个业务场景。

每到中午吃饭时间都是好几个人一起进来,吃饭得付钱吧!复现一下这个流程。

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

2. 给商家的账户增加 10 元。

3. 记录一条交易日志。

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

在这三个操作中最容器造成锁抵触的就是第 2 步给商家的账户增加钱。

所以在编码过程中须要把第 2 步放到最初一步执行,保障在同样后果下锁住的工夫最短。这样能够在编码的水平上尽量保障事务之间锁期待,进步事务并发度。

五、解释死锁的两种计划

第一种形式

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 资源是十分高的。

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

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

为什么要聊这个问题

应用了第二种计划来解决死锁,热点数据死锁检测会十分耗费 CPU(每一个进来被堵的线程都会检测是不是因为本人的退出导致的死锁,有可能是锁期待,但还是须要做判断,所以十分耗费 CPU),所以针对这个问题进行简略讨论一下。

咔咔在其它材料中看到有三种计划。

1. 敞开死锁检测
2. 管制并发度
3. 批改 MySQL 源码对于更新同一行数据,在进入引擎之前排队。这样就不会呈现大量的死锁检测

计划一:敞开死锁检测不思考

这种形式会呈现大量的超时,升高了用户体验,个别状况死锁不会对业务产生严重错误,毕竟呈现死锁,数据大不了回滚即可。

计划二:管制并发度

能够把商家账户扩散多个,所有的账户之和为账户余额。

例如分了 10 个子账户,那么呈现更新同一行数据的概率就升高了 10 倍,这种形式在业务解决时须要简略解决一下。避免账户余额为 0 时用户发动退款的逻辑解决。

这种形式还是很倡议大家应用的,从设计上升高死锁产生。

计划三:批改 MySQL 源码

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

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

这种计划用队列齐全能够解决,所以并不需要从根上解决这个问题。

七、总结

本期从行锁登程引出了两阶段锁,明确了事务提交后才会开释锁。

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

MySQL 也给提供了两种计划来解决死锁问题,对于这两种计划咔咔也给了不同的观点。依据本人的状况来应用。

在这期文章中并没有演示死锁案例,在后边的文章中咔咔会给大家列举几种典型的死锁案例。

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

退出移动版