Mysql锁机制与优化实践以及MVCC底层原理剖析
学习来源-图灵课堂
https://vip.tulingxueyuan.cn
锁学习参考:
https://juejin.cn/post/7307889500545253395
锁机制
为了保证数据的一致性,当访问共享变量的时候我们可以针对共享数据加锁,但是加锁要时要注意加锁的成本,还有加锁的粒度,还有就是是否会发生死锁,还有就是发生了死锁要如何排查,如何解决这个死锁。
数据的修改,最好是在原来字段的基础上进行修改,而不是直接改成某个值,直接修改成一个值可能会导致数据的覆盖丢失;因为修改是使用当前最新的值进行修改。修改方式
例如要对a的值进行加十操作,最好是使用如下的SQL update table set a = a + 10 where id = 1; 这种操作是在代码中对a加过了十,但是可能此时数据库的值已经被修改了 update table set a = x where id = 1;
MySQL的锁
- 从性能上分为乐观锁(用版本对比或CAS机制)和悲观锁,乐观锁适合读操作较多的场景,悲观锁适合写操作较多的场景,如果在写操作较多的场景使用乐观锁会导致比对次数过多,影响性能
- 从对数据操作的粒度分,分为表锁、页锁、行锁
- 从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁),还有意向锁
读锁(共享锁,S锁(Shared)):
针对同一份数据,多个读操作可以同时进行而不会互相影响,比如:
select * from T where id=1 lock in share mode; -- lock in share mode是手动加上读锁。
写锁(排它锁,X锁(eXclusive)):
select * from T where id=1 for update; -- for update手动在查询的时候加上写锁。
针对同一条数据来说其读写锁:读读共享;读写互斥,写读互斥,写写互斥。共享时是可以并发操作的;互斥时就是一个执行,另外一个或者多个在阻塞中,可能超时死锁等。
MySQL InnoDB引擎默认update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型
意向锁(Intention Lock):
表锁
手动增加表锁
lock table 表名称 read(write),表名称2 read(write);
例如lock tables t1 read, t2 write; 命令,对t1加表级读锁;对t2加表级写锁。则其他线程写 t1、读写 t2 的语句都会被阻塞。读t1不阻塞。
同时,在执行 unlock tables 释放表锁之前,也只能执行读 t1、读写 t2 的操作。不能在unlock tables之前访问其他表。表级写锁中还是可以进行读的。
表级别的读写锁是严格互斥的。
查看表上加过的锁
show open tables;
删除表锁
unlock tables;
页锁
行锁
- InnoDB支持事务(TRANSACTION)
- InnoDB支持行级锁
注意,加了写锁并不是说就不能读了,只是不能加读锁了,还是可以select的,因为默认的select查询不会加锁。
查询锁(行锁)
类似于行读锁写锁。直接锁住了这条数据。
间隙锁(Gap Lock)
在RR隔离级别下才生效的。
是针对范围的加锁,并不锁边界,对这个范围加锁之后就可以阻止在这个访问中插入数据,可以有效的防止幻读的发生。
SELECT * from account WHERE id = 5 for UPDATE;
这条SQL语句查询的id是5,因为不存在,就会锁住5,6,7这三个主键值,无法在这三个值中选取主键插入表中。
临键锁(Next-key Locks)
这个是连边界也锁住了,类似于大于等于,加上了等于。类似行锁+间隙锁。
总结
锁等待分析
对各个状态量的说明如下:
- 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(等待总时长)
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
-- 查看事务 select * from INFORMATION_SCHEMA.INNODB_TRX; -- 查看锁 select * from performance_schema.data_locks; -- 查看锁等待 select * from performance_schema.data_lock_waits; -- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到 kill trx_mysql_thread_id -- 查看锁等待详细信息 show engine innodb status\G;
注意:版本是MySQL8.0
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 尽可能减少检索条件范围,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
- 尽可能用低的事务隔离级别
使用版本号对比的时候,除了要加上where后面的查询条件,还要在后面加上一个版本号,如果版本号不一致,就不能进行修改;
CAS机制,就是比较并交换,在while true循环中,先对比本线程缓存的值和要修改的目标值是否相等,如果相等就修改;如果不等,就终止本次修改,去循环判断,直到能成功修改。
MVCC机制
- readview和可见性算法其实就是记录了sql查询那个时刻数据库里提交和未提交所有事务的状态。
- 要实现RR隔离级别,事务里每次执行查询操作readview都是使用第一次查询时生成的readview,也就是都是以第一次查询时当时数据库里所有事务提交状态来比对数据是否可见,当然可以实现每次查询的可重复读的效果了。
- 要实现RC隔离级别,事务里每次执行查询操作readview都会按照数据库当前状态重新生成readview,也就是每次查询都是跟数据库里当前所有事务提交状态来比对数据是否可见,当然实现的就是每次都能查到已提交的最新数据效果了。
RR级别下,读是快照读,一直都是历史版本的数据,别的事务的任何修改都不能获取到;但是如果是本事务进行编辑,那么就是拿最新的数据去编辑,如果没有别的事务进行编辑或者别的事务编辑已经提交或回滚,此时该事务就进行编辑,此时锁就变成了行写锁,排他锁,如果一直不提交,就会阻塞别的事务的写操作。这个要注意。