本期来介绍一下Mysql中的锁机制。
首先,分享一个今天抖音刷到的一个视频,很有意思且对上一期和这期的知识理解很有帮助,非常形象的体现了事务隔离级别在工作中出现的问题案例:6.41 复制打开抖音,看看【孟小哥捉虫vlog的作品】想不通为啥有bug,骨干:你投下代码! # 程序员... https://v.douyin.com/i5dpJhs6/ Z@m.Qk 07/25 aNW:/
锁机制详解
锁是计算机协调多个进程或线程
并发访问
某一资源的机制。
在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供需要用户共享的 资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响
数据库并发访问
性能的一个重要因素。(
要先清晰的认识到锁存在的目的是什么
)
锁分类
1.从性能上分为
乐观锁
(用版本对比或CAS机制)和
悲观锁
,乐观锁适合读操作较多的场景,悲观
锁适合写操作较多的场景,如果在写操作较多的场景使用乐观锁会导致比对次数过多,影响性能。
(简单介绍乐观锁:即在存放的数据最后加一个字段,存放version版本号或者时间戳,每次修改这个表的记录是给版本号+1,那么当我们查询数据时同时查询当前数据的版本号,那么当你在一个事务中查询完数据想要更新数据时,需要将查询到的版本号给更新数据逻辑,即只有当数据库当前数据的版本号是相等的时候,才能成功进行数据更新操作。总结即乐观锁是先比对再交换数据然后更新值。
注
:乐观锁不要到RR可重复读里面去用,因为RR的查询是快照查,你的逻辑中的查询出的版本号并不保证是最新的)
(悲观锁就比较简单,就是在一个事务对某行数据进行修改时,对这行数据加了行锁,在这个事务没有完成commit之前,其他事务都处于阻塞等待状态。)
乐观和悲观的区别可以理解为:乐观锁信任所有事务,都放你进来再验证身份,如果版本号比对成功就执行;悲观锁则是在事务执行时,不信任其他一切的事务,都得等着。
2.
从对数据操作的粒度分,分为
表锁、页锁、行锁(顾名思义嘛,就是针对表、页、行上锁,再上锁状态时,其他对其操作的事务阻塞等待)
解析一下三者的差异:
表锁
:
每次操作锁住整张表。
开销小,加锁快
;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,
并发度最低
;一般用在整表数据迁移的场景。(开销小、加锁快是从他的加锁角度看的,加表锁可以看做对一张表加一个标志位,就单纯的表示这个表有没有被锁,实现起来很简单快捷)
页锁 :
只有
BDB存储引擎支持页锁
,页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中 可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的 数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
行锁:
每次操作锁住一行数据。
开销大,加锁慢
;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,
并发度最高
。(同样的哈,从加锁的角度看,要给一行数据加行锁,首先我们得去定位这行数据的位置,然后再对其加锁,肯定是比表锁复杂的。相反从并发度的角度看:表锁等于整个表锁住禁止其他事务操作,并发性低,而行锁只是禁止对上锁行的数据禁止其他事务修改)
3.从对数据库操作的类型分,分为
读锁和写锁
(都属于悲观锁),还有
意向锁
读锁
(共享锁,S锁(
S
hared)):针对同一份数据,多个读操作可以同时进行而不会互相影响,比如:
select * from T where id=1 lock in share mode
写锁
(排它锁,X锁(e
X
clusive)):当前写操作没有完成前,它会阻断其他写锁和读锁,数据修改操作都 会加写锁,查询也可以通过for update加写锁,比如:
select * from T where id=1 for update
意向锁
(Intention Lock):又称
I锁
,针对
表锁
,主要是为了提高加表锁的效率,是mysql数据库自己 加的。当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有行锁了,其他事 务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不 该加表锁。特别是表中的记录很多时,逐行判断加表锁的方式效率很低。而这个标识就是意向锁。
意向锁主要分为:
意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁。
意向排他锁,IX锁,对整个表加排他锁之前,需要先获取到意向排他锁。
所以这就得提到为什么现在主流引擎是InnoDB:
InnoDB相对于MYISAM的最大不同有两点:
InnoDB支持事务(TRANSACTION)
InnoDB支持行级锁
注意:
InnoDB的行锁实际上是
针对索引加的锁
(
在索引对应的索引项上做标记
),不是针对整个行记录加的
锁。并且
该索引不能失效,否则会从行锁升级为表锁
。(
RR级别会升级为表锁,RC级别不会升级为表锁
)
比如我们在RR级别执行如下sql(当前表的索引只有id)
select * from account where name = 'lilei' for update; ‐‐where条件里的name字段无索引
在这个事务没有commit之前,其它Session对该表
任意一行记录做修改操作
都会被阻塞住。(其实就是相当于表锁了)
PS:关于RR级别行锁升级为表锁的原因分析:
因为在RR隔离级别下,需要解决不可重复读和幻读问题,所以在遍历扫描聚集索引记录时,为了防止扫描过的索引被其它事务修改(不可重复读问题) 或 间隙被其它事务插入记录(幻读问题),从而导致数据不一致,所以MySQL的解决方案就是把所有
扫描过的索引记录和间隙都锁上
,这里要注意,并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其它事务锁住了表里的其它行记录。
间隙锁(Gap Lock)
间隙锁,锁的就是两个值之间的
空隙
,
间隙锁是在可重复读隔离级别下才会生效
。
上一期提过
,
Mysql默认级别是repeatable-read,有幻读问题,间隙锁是可以解决幻读问题的。
举例:
假设account表里数据如下:

那么当前数据存在的间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,在Session_1下面执行如下sql:
select * from account where id = 18 for update;
这一句语句是以查询(10,20)区间内一个不存在的数据并加锁,效果相当于给(10,20)这个间隙加了间隙锁,则其他Session没法在这个(10,20)这个间隙范围里插入任何数据。
如果执行下面这条sql:
select * from account where id = 25 for update;
同理,效果为其他Session没法在这个(20,正无穷)这个间隙范围里插入任何数据。
也就是说,只要在间隙范围内锁了一条不存在的记录会锁住整个间隙范围,
不锁边界记录
,这样就能防止其 它Session在这个间隙范围内插入数据,就
解决了可重复读隔离级别的幻读问题
。
总结:
MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会 自动给涉及的表加写锁。
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
另外,
读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更 高一下,但是在整体并发处理能力方面要远远优于MYISAM的表级锁定的。当系统并发量高的时候,Innodb 的整体性能和MYISAM相比就会有比较明显的优势了。
但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MYISAM高,甚至可能会更差。
最后说明一下
锁等待分析
:
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况。
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等 待,然后根据分析结果着手制定优化计划。
show status like 'innodb_row_lock%';
2
3 对各个状态量的说明如下:
4 Innodb_row_lock_current_waits: 当前正在等待锁定的数量
5 Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
6 Innodb_row_lock_time_avg: 每次等待所花平均时间
7 Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
8 Innodb_row_lock_waits: 系统启动后到现在总共等待的次数
9
10 对于这5个状态变量,比较重要的主要是:
11 Innodb_row_lock_time_avg (等待平均时长)
12 Innodb_row_lock_waits (等待总次数)
13 Innodb_row_lock_time(等待总时长)
查看INFORMATION_SCHEMA系统库锁相关数据表:
select * from INFORMATION_SCHEMA.INNODB_TRX;
3 ‐‐ 查看锁,8.0之后需要换成这张表performance_schema.data_locks
4 select * from INFORMATION_SCHEMA.INNODB_LOCKS;
5 ‐‐ 查看锁等待,8.0之后需要换成这张表performance_schema.data_lock_waits
6 select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
7
8 ‐‐ 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
9 kill trx_mysql_thread_id
10
11 ‐‐ 查看锁等待详细信息
12 show engine innodb status;
死锁问题分析
大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁, 这种情况我们可以通过日志分析找到对应事务线程id,可以通过kill杀掉。
set tx_isolation='repeatable‐read';
2 Session_1执行:select * from account where id=1 for update;
3 Session_2执行:select * from account where id=2 for update;
4 Session_1执行:select * from account where id=2 for update;
5 Session_2执行:select * from account where id=1 for update;
6 查看近期死锁日志信息:show engine innodb status;
锁优化实践(结合事务和锁的原理理解):
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
合理设计索引,尽量缩小锁的范围
尽可能减少检索条件范围,避免间隙锁
尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
尽可能用低的事务隔离级别