MySQL(四):锁的原理

本文详细介绍了数据库锁的各种类型,包括全局锁、表锁、行锁、元数据锁和间隙锁,重点讨论了它们的作用、应用场景及优缺点。通过实例分析了如何避免死锁,以及在实际操作中如何优化事务处理,以提高并发性能。同时,提到了元数据锁在DDL操作中的角色,以及如何处理长事务和DDL冲突。
摘要由CSDN通过智能技术生成

大纲内容

  • 全局锁
  • 表锁
  • 行锁
  • 元数据锁
  • 间隙锁
  • 临界锁

读书才能够系统的学习,听别人偶尔的两句断章取义的结论,远远不能够支撑设计一套系统。: 博主建立了一个群, 有兴趣的可以一起一起分享问题, 解决问题, 分享面试题, 内推。
在这里插入图片描述


全局锁

概念:针对于整个DB加的锁,目的是为了全库备份,整个DB只有读操作。MySQL提供了一种全局读锁的方法,保证线程只会读,不会做其他的DML,DDL操作。

操作命令:DML:增删改查(查除外)。DDL:增加字段,修改索引,改表结构等。

flush table with read lock;  
特殊情况:当全局锁开启之前,还存在其他线程的读写操作,
会等读写完结束后,再执行flush table with read lock; 

MVCC机制明显优于全局锁技术,但MVCC需要引擎能提供事务支持,所以只有InnoDB具备MVCC机制,非事务引擎可以尝试使用全局锁来保证隔离性。

那为什么不使用set global readonly=true保证可读?
  • 在有的系统中,set global readonly可用于判断是否是主库还是从库。
  • 从异常处理机制来看,全局锁开启,当MySQL跟客户端断开链接时,会主动释放全局锁,set global readonly开启了,会一直保持可读状态。
  • set global readonly对超级管理员无效。

表锁

表锁细分:读锁和写锁。

语法:lock tables … read/write,可以使用unlock tables断开客户端释放锁,lock tables …read和select * from table … in share mode,都是加读锁。

lock tables …write和select. * from table…for update,都是加写锁。

读锁:读锁之间相互隔离,读读不互斥,大家都能获取读锁,大家都不准写。

写锁:写锁会阻塞其他线程读和写,除了获取写锁的线程,不准其他读和写。

获取读锁的线程进行写操作,会报错,其他线程写,会阻塞。

InnoDB在执行查询时(非串行化级别),不会加读锁,执行增删改时,会加行锁。
MyIsam在执行查询时,会加读锁,在增删改时,会加写锁。

优势:每次操作会锁住整张表,不会出现死锁,开销小,一般用于整张表的数据迁移,不支持行锁的执行引擎只能用表锁来控制并发。


行锁

行锁是InnoDB引擎独有的锁,是针对于索引加的锁,锁的粒度更细,针对于表中的行数据进行加锁,比如事务A更新了一行id=1,未提交,事务B也更新id=1这行数据,会阻塞,必须要等待事务A释放完行锁之后,才能更新。
在这里插入图片描述
1:事务A开启时候,更新id=1这条数据,此时事务A会具备id=1这条记录的行锁。

行锁是执行更新语句时,才尝试获取,等到事务结束之后,行锁就会释放

这就是两阶段锁协议。

2:事务B更新id=1时,必须等事务A提交完事务之后,才能更新。


优化场景:假设我们现在有一个买票系统,1:用户A去买票,会扣余额表中的钱,2:电影院B会加上对应的钱,3:操作记录会记录一条当前用户操作。
当在一个事务内,会执行三个DML操作,更新用户表和电影院表的余额,新增操作记录,我们如何保证最大粒度优化事务A中的执行顺序呢?
如果按照1,2,3顺序执行,假设事务总共花费5s,假设现在又有一个事务S,需要给电影院B再加钱,由于两阶段锁协议,当前事务S会阻塞,必须等事务提交后,锁释放,事务S才能尝试获取行锁,执行加钱。这样事务S需要5s的等待时间,如果将执行顺序换成了3,1,2,可以最大程度减少事务之间的锁等待时间,提高了并发度。

行锁是在执行语句时,才尝试获取行锁,在事务结束之后,释放锁

如果事务中存在更新多个行,我们要尝试减少锁冲突,尽可能的把存在锁冲突的操作放在事务最后。

缺点:会带来死锁,死锁会导致CPU占用率100%,我们可以尝试避免死锁。

在这里插入图片描述
两个事务之间存在相互依赖,互相等待对方释放锁,导致死锁,死锁会一直消耗CPU,相互依赖就会出现问题(spring的循环依赖也是相互依赖)

解决死锁的办法
  • 开启死锁等待时间,innodb_lock_wait_timeout来设置,默认是50s

  • 开启死锁检测,主动回滚死锁链条中的任意一个事务,让其他事务执行。inndodb_deadlock_detect设置为on,即开启,开启后,会判断每个事务之间是否存在死锁,假设有1000个线程来更新同一条数据,死锁检测次数为1000*1000=100万,假设最终结果没有死锁,还是会占用很高的CPU。

  • 可以考虑修改一行数据改成新增多条流水记录。比如更新电影院B的余额,可以多加一张表来作为账单流水表,用最终的SQL查询sum(amount)来统计电影院的账户余额的。(目前我们系统就是新增流水来记录某个商家的总额)

  • 可以考虑使用中间件,统一对更新统一行的请求进行排队处理,控制并发度。

在1中,开启死锁等待时间太久,业务系统不允许这么久的等待时间,但设置值太小,也会导致频繁修改失败。在2中,可能会做很多无用功,还是会占用CPU资源,我们可以尝试在业务角度去解决死锁问题。

注意:要避免行锁升级为表锁。

假设更新语句:update table set k=3 where name=“peter”,当前name没有索引,MySQL会进行全表扫描,扫描过的索引对象都会加锁,导致行锁升级成了表锁。并不是加了索引条件,一定就能避免行锁升级为表锁,最终要看优化器判断是全表扫描效率高,还是索引扫描效率高。

解决办法:1:在配置文件中开启安全更新模式,2:强制走索引。

在我们更新时,要确保where条件能走索引,还要在测试环境判断是否能走索引,防止全表扫描。


元数据锁

元数据锁细分:元数据读锁和元数据写锁。

在访问一个表时,自动会加上元数据锁,对一张表的增删改查都必须先获取MDL读锁,目的是为了保证读写的正确性,比如一个线程在遍历所有的数据,而另外一个线程删除了某一列字段,那么查询结果跟表结构的结果对不上,元数据锁是server层的锁,主要用于隔离DML和DDL之间的干扰,每执行一条DML,DDL语句时,都会申请DML元数据锁,DML操作是获取DML读锁,DDL操作是获取DML写锁。

MDL元数据锁只有在提交事务后,才释放锁。

MDL读锁:对DB做DML操作,要获取MD读锁。

MDL写锁:对DB做DDL操作,要获取MDL写锁。

MDL读锁之间是不互斥的,可以多个线程之间对一张表中的数据进行增删改查操作。

MDL写读锁之间是互斥的,MDL写锁之间也是互斥的,用来保证变更表结构操作的安全性,如果两个线程同时要给一个表加字段,后一个线程会阻塞。

事务1:开启事务,执行查询,获取MDL读锁。

在这里插入图片描述

事务2:修改表结构,获取MDL写锁,被阻塞了,截图阻塞了42s

在这里插入图片描述

事务3:由于事务2获取MDL写锁,导致事务3获取MDL读锁也阻塞了。

截图阻塞了46s

在这里插入图片描述

事务3获取MDL读锁时,为什么也阻塞了呢?

当事务阻塞时,无论后续是DDL还是DML操作都会被阻塞,有点队列的意思,个人理解:申请MDL锁的操作会形成一个队列,队列中写锁优先级是高于读锁优先级,一旦出现写锁等待,不仅当前操作会被阻塞,同时还会阻塞后续所有的操作,事务一旦申请MDL锁后,只有事务提交,锁才能释放。

获取MDL写锁是遵循online ddl规则

1: 获取MDL写锁

2: 降级锁为MDL读锁

3: 执行DDL操作

4: 升级为MDL写锁

5: 释放MDL写锁

当事务1执行commit执行后,事务2还在阻塞,事务3阻塞结束了,按照上述问题,既然存在队列维护了优先级,为什么还阻塞了事务2?

这里存在online ddl规则,事务1(获取读锁)–>事务2(获取写锁,由于读写互斥,固阻塞)–>事务3(获取读锁,固阻塞)–>事务1(提交commit)–>事务2(尝试获取MDL写锁,MDL锁遵循online ddl原则, 降级为MDL读锁)–>事务3(读读不互斥,固获取了MDL读锁)–>事务2(DML读锁升级为MDL写锁时,发现事务3获取了读锁,固又阻塞了)–>事务3(提交事务,锁释放)–>事务2(完成online ddl,可释放锁)

优化:为了避免获取MDL写锁时,把后续所有的请求都给阻塞,我们应该设置超时等待时间,如果执行DDL操作导致写锁超时,因为添加字段影响到业务阻塞是不能接受的,这就是为什么晚上上线的原因之一了。

元数据写锁要注意长事务,因为长事务未提交,锁一直不能被释放,会一直阻塞后续请求,要么就暂停DDL,等待长事务执行完,要么就kill掉长事务,要么就在执行DDL时,设置等待时间,超过时间未获取DDL锁,则放弃执行。

表锁和元数据锁的区别

表锁在乎的是数据,元数据锁在乎的是表结构。表锁是自己手动加上的,而元数据锁是执行语句时,自动加上的。


间隙锁

在可重复读的隔离级别下生效,锁的是两个值之间的间隙,假如表中有id=1,id=5,id=10,id=20,固(-无穷,1), (1,5),(5,10),(10,20),(20,+无穷)都是间隙锁。其实行锁还可以细分为记录锁和间隙锁,一条记录就是记录锁,多条记录锁组成的叫间隙锁,两个存在记录的数据之间即是间隙。

间隙锁之间是不存在任何冲突的,当事务A先获取间隙锁,事务B进行新增,会阻塞。

间隙锁:是锁不存在记录的锁,个人理解:在RR级别下,只要查找未存在的记录,就会加上间隙锁,主键索引和普通索引都会存在间隙锁,间隙锁是左开右开。


临界锁

跨越两个间隙锁的区间,即是临界锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值