性能调优 6. MySQL锁机制与优化实践

1. 锁机制详解


‌‌‌  锁是计算机协调多个进程或线程并发访问某一资源的机制。

‌‌‌  在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供需要用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。

2. 锁分类

2.1. 性能上分类

乐观锁


‌‌‌  乐观锁(用版本对比或CAS机制),乐观锁适合读操作较多的场景,写太多版本号变更太快,就会导致循环太多,cpu空转。

悲观锁

‌‌‌  悲观锁适合写操作较多的场景,会对操作的数据进行加锁,其它事务操作需要堵塞等待。


注意

‌‌‌  1. 乐观锁和悲观锁区别一个是不等待锁(更新不成功不等待),一个是等待锁。

2.2. 数据操作的粒度分


‌‌‌  分为表锁、页锁、行锁。

表锁

‌‌‌  每次操作锁住整张表。开销小,加锁快(对这张表,记录锁字段属性进行标识);不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低一般用在整表数据迁移的场景

‌‌‌  注意

‌‌‌  InnoDB支持表锁,MyISAM是只支持表锁

例子

‌‌‌  1. 创建示例数据。

#--建表SQL
‌‌‌  CREATE TABLE`mylock`(
‌‌‌  `id`INT(11)NOT NULL AUTO_INCREMENT,
‌‌‌  `name`VARCHAR(20) DEFAULT NULL,
 PRIMARY KEY(`id`)
‌‌‌  ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3;

#--插入数据
‌‌‌  INSERT INTO`mytest`.`mylock`(`id`,`name`)VALUES('1','a');
‌‌‌  INSERT INTO`mytest`.`mylock`(`id`,`name`)VALUES('2','b');
‌‌‌  INSERT INTO`mytest`.`mylock`(`id`,`name`)VALUES('3','c');
‌‌‌  INSERT INTO`mytest`.`mylock`(`id`,`name`)VALUES('4','d')

‌‌‌  2. 加锁,查看加锁的表,解锁所有表锁

‌‌‌  加锁语法:
    #读锁---对读操作共享,写互斥 写锁---对任何操作都互斥
‌‌‌  lock table 表名称 read(write),表名称2 read(write);

‌‌‌  例子:
‌‌‌  lock table mylock(加锁的表名) write 或者 read (写或者读锁);

    #查看所有表加锁的状态
‌‌‌  show open tables; 

	#删除所有表锁
‌‌‌  unlock tables


页锁

‌‌‌  知道概念就行,只有BDB存储引擎支持页锁(InnoDB,MyISAM不支持),页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

‌‌‌  页相当于B+tree树叶子节点一个磁盘页数据,也就是这个磁盘页的所有数据去加锁。

行锁

‌‌‌  每次操作锁住一行数据。开销大,加锁慢(需要遍历找到这行数据再标记);会出现死锁;锁定粒度最小,发生锁冲突的概率最低,可并发度最高

‌‌‌  InnoDB支持行级锁,MYSAM不支持

‌‌‌  注意

‌‌‌  1. InnoDB的行锁实际上是针对索引加的锁(在索引对应的索引项上,底层C语言实现。有个索引项做标记),不是针对整个行记录加的锁该索引不能失效,否则会从行锁升级为表锁。(RR级别会升级为表锁,RC级别不会升级为表锁,我猜RC最终会对查找的索引数据的索引项加锁,可能是聚集索引或者辅助索引的数据)。

‌‌‌  例子

‌‌‌  在RR级别事务下执行如下SQL语句,name不是索引字段,事务不提交。这时候则其它会话对该表任意一行记录做修改操作都会被阻塞住。这时候是表锁,通过命令查看所有表锁能看到该表加了锁(这边需要别的会话进行操作数据堵塞,才会触发看到表锁)。


	#where条件里的name字段无索引
	#对这个操作加写锁,查询也能加写锁
	
	START TRANSACTION
	
	
	set transaction_isolation='REPEATABLE-READ';
	
	
	SELECT * FROM account WHERE `name`='lilei' for update;
	
	# COMMIT



‌‌‌  START TRANSACTION


‌‌‌  set transaction_isolation='READ-COMMITTED';

‌‌‌  UPDATE account SET balance=300 where  `name`='lilei'


#COMMIT


‌‌‌  在RR级别事务下。执行如下SQL,balance是索引字段,事务不提交。其它会话对该条数据更新是堵塞的,其它数据更新是不堵塞,这时候是行锁


	START TRANSACTION
	
	
	set transaction_isolation='REPEATABLE-READ';
	
	
	SELECT * FROM account WHERE balance = 400 for update;
	
	#COMMIT



‌‌‌  START TRANSACTION


‌‌‌  set transaction_isolation='READ-COMMITTED';

‌‌‌  UPDATE account SET balance=300 where id=1

‌‌‌  UPDATE account SET balance=700 where id=2


#COMMIT


‌‌‌  2. 在RR或者RC隔离级别下,需要通过给查询数据加写锁,解决脏写和幻读问题。在遍历扫索引记录时候(看情况可能最终扫描查询是主键索引的记录,还是辅助索引的记录),为了防止扫描过的索引数据,被其它事务修改(查询数据被修改,拿错误的数据做操作)或间隙被其它事务插入记录(幻读问题),从而导致数据不一致。所以MySQL的解决方案就是把所有扫描过的索引数据和间隙都锁上

‌‌‌  但是RR级别比较特别直接加表锁,可能并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其它事务锁住了表里的其它行记录。正常应该都是等待别事务行锁释放直接加表锁

2.3. 数据库操作的类型分


‌‌‌  分为读锁和写锁(都属于悲观锁),还有意向锁。

‌‌‌  读锁和写锁在不同数据操作粒度下,可以分为表锁,页锁和行锁。

‌‌‌读锁(共享锁,S锁(Shared))


‌‌‌  针对同一份数据,多个读操作可以同时进行而不会互相影响。

‌‌‌  SELECT * FROM account lock in share mode

写锁(排它锁,X锁(eXclusive))


‌‌‌  当前写操作没有完成前(在事务下,事务没提交前),它会堵塞其它事务加了写锁和读锁的操作。MySQL数据修改操作都会自动加写锁,查询也可以通过for update加写锁。


‌‌‌  SELECT * FROM account for update

意向锁(IntentionLock)


‌‌‌  又称I锁,针对表锁,主要是为了提高加表锁的效率,是MySQL数据库自己加的。当有事务给表的数据行加了共享锁或排他锁同时会给表设置一个标识,代表已经有行锁了,其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁。特别是表中的记录很多时,逐行判断加表锁的方式效率很低,而这个标识就是意向锁

‌‌‌  意向锁主要分为:

‌‌‌  意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁(说明可以对表加共享锁)。

‌‌‌  意向排他锁,IX锁,对整个表加排他锁之前,需要先获取到意向排他锁(说明可以对表加排他锁)。

间隙锁(GapLock)


‌‌‌  间隙锁,锁的就是两个值之间的空隙,间隙锁是在可重复读(Repeatable Read)和串行化(Serializable)隔离级别下生效。用在查询数据范围内禁止其它事务增删改

‌‌‌  MySQL默认事务级别是repeatable-read,有幻读问题,间隙锁是可以解决幻读问题的。

‌‌‌  假设account表里数据如下

在这里插入图片描述

‌‌‌  那么间隙,就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,不包括边界,间隙随着插入成功数据不断变更。

‌‌‌  在一个会话中执行如下sql

‌‌‌  select * from account where id = 18 for update;

‌‌‌  则其它会话没法在这个(10,20)这个间隙范围里插入任何数据。

‌‌‌  如果执行下面这条sql

‌‌‌  select * from account where id = 25 for update;

‌‌‌  则其它会话没法在这个(20,正无穷)这个间隙范围里插入任何数据。

‌‌‌  可以理解为在叶节点的两个索引数据的间隙加了标志。

‌‌‌  注意

‌‌‌  1. 间隙锁情况下,在其它事务操作加锁间隙时候会堵塞,上升为表锁

‌‌‌  2. 如果间隙锁是字符串,那么就是按字符串逐位比对得到的间隙。

‌‌‌  3. 间隙锁可以加读锁和写锁。

‌‌‌解决幻读问题

‌‌‌  只要在间隙范围内锁一条不存在的记录,会锁住整个间隙范围,不锁边界记录,这样就能防止其它会话在这个间隙范围内插入数据,就解决了可重复读隔离级别的幻读问题

临键锁(Next-keyLocks)


‌‌‌  就是行锁与间隙锁的组合。

‌‌‌  如下面的SQL语句,意思是锁住 id 3到10数据的间隙,同时锁住id=10的数据。


‌‌‌  select * from account where id > 3 and id<=10 for update;

‌‌‌  注意

‌‌‌  1. 临键锁情况下,在其它事务操作加锁间隙和数据时候会堵塞,上升为表锁

总结

‌‌‌  1. MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。没有事务相当于一条SQL语句就是一个事务。

‌‌‌  2. InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁,加的是写锁。读锁会阻塞写,但是不会阻塞读。写锁则会把读和写都阻塞。

3. 锁等待分析


‌‌‌  通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况。


‌‌‌  show status like 'innodb_row_lock%';

	#8.0开始时间单位是ms

	# 对各个状态量的说明如下:
	
	# 当前正在等待锁定的数量,事务要操作数据锁等待的总数量
‌‌‌  Innodb_row_lock_current_waits

	# 从系统启动到现在锁定总时间长度,所有锁的总时长
‌‌‌  Innodb_row_lock_time

	# 每次等待所花平均时间,每次堵塞获取锁等待时间
‌‌‌  Innodb_row_lock_time_avg

	# 从系统启动到现在等待最长的一次所花时间,堵塞获取锁最长的一次时间
‌‌‌  Innodb_row_lock_time_max

	# 系统启动后到现在总共等待的次数,堵塞获取锁的总次数
‌‌‌  Innodb_row_lock_waits

	#对于这5个状态变量,比较重要的主要是:
‌‌‌  Innodb_row_lock_time_avg(等待平均时长)
‌‌‌  Innodb_row_lock_waits(等待总次数)
‌‌‌  Innodb_row_lock_time(等待总时长)

‌‌‌  
  尤其是当等待次数很高而且每次等待时长也不小的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。

查看INFORMATION_SCHEMA系统库锁相关数据表



	#查看正在执行事务,停止的不算
	
‌‌‌  select * from INFORMATION_SCHEMA.INNODB_TRX;

	#查看加的锁包括还没加上的锁,8.0之后需要换成performance_schema.data_locks这张表


‌‌‌  select * from INFORMATION_SCHEMA.INNODB_LOCKS;

‌‌‌  select * from performance_schema.data_locks;

	#查看正在的锁等待(请求锁的事务id,当前加锁的事务id等),8.0之后需要换成	performance_schema.data_lock_waits这张表
	


‌‌‌  select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

‌‌‌  select * from performance_schema.data_lock_waits;


	#释放锁,根据正在运行事务查出其关联的线程id,借助kill指令释放
	select concat('KILL ',id,';') from information_schema.processlist p inner
 join information_schema.INNODB_TRX x on p.id=x.trx_mysql_thread_id
‌‌‌  where db='指定数据库或者不加where查所有库的事务';

‌‌‌  ![[Pasted image 20230817142804.png]]

	trx_mysql_thread_id可以从INNODB_TRX表里查看到,是事务的id

‌‌‌  kill 56 就是杀死线程

	#查看锁等待详细信息,可以看status的信息数据

‌‌‌  show engine innodb status

‌‌‌ 
注意

‌‌‌  1. 分析锁等待可以用下面SQL语句分析


	#查看正在执行事务,停止的不算
‌‌‌  select * from INFORMATION_SCHEMA.INNODB_TRX;

	#查看加的锁包括还没加上的锁,8.0之后需要换成这张表
	performance_schema.data_locks

‌‌‌  select * from INFORMATION_SCHEMA.INNODB_LOCKS;

‌‌‌  select * from performance_schema.data_locks;

	#查看正在锁等待(请求锁的事务id,当前加锁的事务id等),8.0之后需要换成这张表
	performance_schema.data_lock_waits

‌‌‌  select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

‌‌‌  select * from performance_schema.data_lock_waits;

‌‌‌  如果锁等待很长,可以释放锁,并分析为啥等待这么久。

‌‌‌  2. 分析死锁可以用


	#查看锁等待详细信息,可以看status的信息数据
	
‌‌‌  show engine innodb status;

4. 死锁问题分析



# 设置事务为可重复读
‌‌‌  set tx_isolation='repeatable-read';

#Mysql 8.0设置事务为可重复读

‌‌‌  set transaction_isolation='repeatable-read';

#会话1执行
‌‌‌  select * from account where id=1 for update;

#会话2执行
‌‌‌  select * from account where id=2 for update;

#会话1执行
‌‌‌  select * from account where id=2 for update;

#会话2执行
‌‌‌  select * from account where id=1 for update;

#查看近期死锁日志信息

‌‌‌  show engine innodb status;

‌‌‌  在死锁日志信息搜Deadlock。

在这里插入图片描述

‌‌‌  大多数情况MySQL可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况MySQL没法自动检测死锁,这种情况可以通过日志分析找到对应事务线程id,可以通过kill杀掉

5. 锁优化实践


‌‌‌  1. 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁,因为MySQL默认事务是RR级别

‌‌‌  2. 合理设计索引,尽量缩小锁的范围。比如id 20-100不能插入数据,可以先插入id=100数据,然后查询id=30数据,加写锁进行间隙锁,锁id 20-100的数据。

‌‌‌  3. 尽可能减少检索条件范围,避免间隙锁

‌‌‌  4. 尽量控制事务大小减少锁定资源量和时间长度,涉及事务加锁的SQL尽量放在事务最后执行尽可能用低的事务隔离级别。

  • 34
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值