MySQL性能系列
本篇文章采用的MySQL版本是8代,同时自己使用的是Linux mysql8,本篇内容主要介绍MySQL的12种锁及死锁机制。
MySQL锁概述
计算机在执行多线程或线程时,锁是一种用于控制并发访问多条线程同一共享资源的同步机制,而MySQL中的锁,是在存储引擎层或者服务器层实现的机制,它的作用是保证数据访问的一致性与有效性。
在了解MySQL的锁之前,先了解MySQL 的死锁,因为在锁使用不当的情况下,很容易有死锁的情况发生,先了解死锁,能更好的避免死锁的发生。
死锁(deadlock) :在数据库事务执行过程中,两个或多个事务由于相互持有对方所需的资源而造成相互等待的现象,最终导致这些事务都无法继续执行。
造成死锁的原因
避免死锁
思维导图
MySQL锁的划分
粒度划分
模式划分
共享锁(Shared Lock)
排他锁(Exclusive Lock)
意向共享锁
意向排他锁
用法划分
区间划分
按锁粒度划分
主要划分
表级锁: 对当前整张表进行锁定,粒度最粗的锁,资源消耗少。
行级锁: 对当前表中的单独行进行锁定,粒度最细的锁,资源消耗非常多,InnoDB引擎默认锁。
额外
全局锁(Global Lock): 在整个数据库上的锁,使数据库无法进行任何修改操作,变为只读状态。常用来备份或者复制数据库时使用(确保数据唯一性)。
页级锁(Page Lock): 锁定表中的某一页数据,BDB引擎默认锁,一种介于表级锁和页级锁之间的锁,避免了表级锁冲突多,行级锁速度慢的缺陷,相对折中的一种锁。
按锁级别划分
全局锁:
- 对整个数据库实例添加的锁,范围最大的锁。
适用场景:全库逻辑备份、阻止数据变动
注意:
全局锁会让数据库处于只读状态,这会阻塞所有写操作,对数据库可能会有很大影响。
页级锁:
页级锁是介于表级锁与行级锁之间的一种锁的粒度,这里的"页"指的是(Page)数据库存储数据的基本单位,是一种比较特殊的锁,其并发与性能开销也是介于两者之间。
支持引擎:Berkeley DB(MySQL5.1弃用)
性能开销:因为减少了锁总数,所以降低需要的内存和CPU资源,但保留了部分并发性能(页级锁比表级锁灵活,稍逊行级锁)。
死锁:虽然相比表级锁,页级锁的数量降低,但依旧会增加锁定争用的可能,可能会出现死锁的风险。
表级锁:
- InnoDB与MyISAM都支持的锁级别,用于控制整个表的访问和操作,在MySQL中,当用户对某个表执行写操作(如插入、更新、删除)时,MySQL系统会自动获取锁,通常不需要自己再加锁。
种类:共享锁,排他锁
支持引擎:InnoDB、MyISAM。
特点:对当前的整张表加锁,操作简单,资源消耗较少,并发度较低,操作冲突较多。
锁定范围:以整个表为单位进行锁定,即一次性锁定整个表,无论表中的数据有多行。
互斥:表级锁对读操作共享,对写操作互斥。
低并发:其他事务无法同时对该表进行写操作,导致并发度较低。
注意:
- 在InnoDB引擎下,只会在执行全表扫描且没有使用任何索引的情况下进行表锁,这种情况非常罕见(如果事务隔离级别为Read Committed 或更高的级别,InnoDB会使用更精细的行级锁)。
- 在MyISAM引擎下,通常不需要用户明确使用Lock Table命令锁定,MySQL会根据需要自动管理读写锁,但如果自己想在某些操作以原子方式锁定表,避免其他操作干扰自己的从操作,可以使用Lock Tables进行手动锁定。
共享锁(Shared Lock):
- 加共享锁后允许多个事务对同一张表同时进行读取,但不能写入。简单来说,就是上锁后,只允许读取,不能写入,因此也被称为读锁。
共享锁(读锁)
手动添加读锁
LOCK TABLES table_name READ;
这条语句的作用是:对此表(table_name)添加共享锁,使得其他事务可以并发读取这些行,但不能进行修改,直至当前事务完成。
排他锁(Exclusive Lock):
- 当一个事务对表上排他锁后,其他事务无法同时对此表进行读写操作,直到A事务完成。简单理解为:A事务加排他锁后,除A自己以外的其他事务都无法操作此表。
排他锁(写锁)
手动添加写锁
LOCK TABLES table_name WRITE;
这条语句的作用为:对此表(table_name)请求一个排他锁,阻止其他事务对这些行进行读取或修改,直至当前事务完成。
行级锁:
- 对满足条件的某行数据进行加锁。
- 在MySQL中,行级锁是一种粒度最细的锁,发生冲突的概率最低,并发性更好,相应的开销会更大。
支持引擎:InnoDB(在MysQL中有且仅有InnoDB支持)
特点:在MySQL中,行级锁是一种粒度最细的锁,发生冲突的概率最低,并发性更好,相应的开销会更大。
高并发:仅作用于某行的锁,多个查询和更新操作可以在同一表的不同行上并行执行。
开销大:高并发会导致高负载,可能会频繁出现多个事务试图锁定同一行数据的情况,这会使性能相应降低。
死锁: 由于行锁的粒度更细的缘故,行级锁比表级锁更容易遇到死锁的情况。(两个或多个事务在等待对方释放锁,从而导致它们互相阻塞)
注意:
在MySQL的InnoDB引擎中,行级锁是通过给索引加锁来实现的,如果当条sql操作的是主键索引,MySQL锁定的就是主键索引;如果当条sql操作的是非主键索引,那么MySQL会先锁定该非主键索引,再锁定相关的主键索引。
共享锁(读锁):
- 与表级锁大致相同,上锁后,该行数据只允许读取,不能写入。
手动添加行级读锁的方法:
SELECT * FROM table_name WHERE criteria LOCK IN SHARE MODE;
该条sql的作用为:对满足criteria(标准)的行施加一个共享锁,这表示在释放该锁之前,此行为仅读状态,不能修改或施加排他锁。
排他锁(写锁):
- 与表级锁大致相同,上锁后,此表除本身上锁事务本身外,其他事务无法进行操作。
手动添加行级写锁的方法:
SELECT * FROM table_name WHERE criteria FOR UPDATE;
此sql的作用为:对满足criteria(标准)得行施加排他锁,这表示在当前事务完成之前,其他事务无法进行读取或修改这些行。
表级读写锁与行级读写锁区别:
共享锁(S):
- 共享锁,又称为读锁,意为共享读,可以让其他事务共同读取,但不能进行修改,也不能加写锁。
- 简单来说,共享锁的作用是:当一个事务A对一行数据或一张表进行加读锁后,B事务与C事务可以读取A锁定的行数据或表,且可以对A再添加读锁(S锁),但不能添加写锁(X锁),同样也不能对A锁定的数据进行除读以外的任何操作,直至A将读锁释放。
适用引擎:InnoDB。
共享(仅读):其他事务对上锁的数据只能读取,不能进行修改,同一时间可以存在多个读锁。
并发性:支持多个事务同时持有同一数据的共享锁,可同时读取数据。
互斥:共享锁(S锁)与排他锁(X锁)互斥。
在InnoDB显示添加共享锁
SELECT * FROM table_name WHERE conditions LOCK IN SHARE MODE;
此sql的作用:MySQL执行这条SQL时,会对满足条件的行加上共享锁
排他锁(写锁):
- 与表级锁大致相同,上锁后,此表除本身上锁事务本身外,其他事务无法进行操作。
手动添加写锁的方法:
1.SELECT * FROM table_name WHERE criteria FOR UPDATE;
2.SELECT * FROM table_name WHERE conditions FOR SHARE;
此sql的作用为:对满足criteria(标准)/ conditions 的行施加排他锁,这表示在当前事务完成之前,其他事务无法进行读取或修改这些行。
排他锁(S):
- 排他锁,又称为写锁,意为独占写,只允许添加锁的事务进行读写操作,其他事务不能进行读写操作。
- 简单来说,独占锁的作用为:当一个事务A对一行或一张表进行加锁后,B事务与C事务不能读取或修改A锁定的行数据或表,且不能再次添加独占锁,有且仅有A能对锁定的数据进行操作,直至A将独占锁释放。
- 注意:在某些隔离级别下(可重复读),B与C事务能对A锁定数据进行"快照读",意为:读取A锁定数据前保存的数据版本。
适用引擎:InnoDB。
独占:其他事务对上锁的数据不能进行读写,有且仅有自己能操作,同一时间只能存在一个写锁。
并发性:支持多个事务同时持有同一数据的共享锁,可同时读取数据。
互斥:排他锁(X锁)与共享锁(S锁)互斥。
在InnoDB中显示添加独占锁
START TRANSACTION; --开启事务
SELECT * FROM table_name WHERE condition FOR UPDATE; --添加锁
-- 此处添加更新数据的SQL语句
COMMIT; --结束事务 / ROLLBACK --回滚事务
语法解释:
意向锁:
- 意向锁,特殊的表级锁,用于协调行级锁和表级锁关系的锁。
- 简单理解,当A事务在一张表有行锁时,MySQL会自动申请一个意向锁,如果B事务想对此表添加一个写锁,此时MySQL不需要遍历每一行去判断行锁是否存在,而是直接判断此表是否存在意向锁,如果存在,则使B事务的写锁请求等待,直至行锁的释放。
注意!!! 意向锁的作用是为了优化锁冲突的一种机制,而不是直接阻止锁的添加!
意向共享锁(IS锁)
- 意向共享锁,IS锁,表示事务有意向对表中的某些行加上共享锁。
- 注意:IS锁表示一个事物打算在更细粒度(行级)的层次上读取数据,是为了在更高层次(如表级)上判断是否可能存在锁冲突,是一种优化机制,而不是直接用于数据的锁定。
适用引擎:InnoDB。
共享:IS锁之间互相兼容,意为多个事务可以同时对同一张表加IS锁。
意向:获取IS锁,表示事务意图为读操作,而不是写操作。
优化:减少需要检查锁数量,避免逐行遍历,提高数据库并发性能。
S锁前置条件:事务想要获取某些行的S锁,必须得先获包含该行的表的IS锁。
意向排他 锁(IX锁)
- 意向排他锁,IX锁,表示事务有意向对表中的某些行加上排他锁。
- 注意:IX锁表示一个事物打算在更细粒度(行级)的层次上读取数据,是为了在更高层次(如表级)上判断是否可能存在锁冲突,是一种优化机制,而不是直接用于数据的锁定。
适用引擎:InnoDB。
意向:获取IX锁,表示事务意图为想在表的某行添加排他锁。
优化:减少需要检查锁数量,避免逐行遍历,提高数据库并发性能。
兼容:IX锁与其他意向锁兼容,意为:一个A事物在一张表上持有IX锁,另一个事务B也可以在同一张表上获取IS锁或IX锁。
X锁前置条件:事务想要获取某些行的X锁,必须得先获包含该行的表的IX锁。
乐观锁
- 乐观锁是一种常用的锁,它的核心思想是"持有乐观态度,假设最好的情况"。
- 顾名思义,乐观锁在操作数据时持乐观态度,认为别的线程不会同时修改数据,在提交更新数据时才会检查是否存在冲突。
适用引擎:InnoDB
适用场景:写少读多情况(少操作冲突,提高系统效率)。
优
高并发:由于大部分时间不需要加锁,乐观锁允许更高级别的并发。
低死锁风险:不涉及传统的锁机制,不存在死锁的问题。
低开销:不像悲观锁持续持有,减少锁资源开销。
缺
冲突开销大:版本发生冲突,需要重新更新,增加额外逻辑处理。
设计复杂:乐观锁需额外字段(版本号或时间戳)和冲突检测逻辑。
更新风险:高冲突环境下,频繁更新失败,会影响用户使用体验。
乐观锁实现方式
乐观锁通常有两种实现方式:
版本号
- 每条记录在MySQL中都会有一个版本号字段,每次更新,版本号都会自动增加。当事务试图更新一条记录前,会检查当前记录的版本号是否与事务开始时读取到的版本号一致,如相同,则说明在这期间没有其他事务修改过此记录,可以进行更新,并将此版本号加一;如不同,则说明有事务已修改过此记录,此次修改就会被拒绝。
使用版本号实现乐观锁的场景
MySQL中版本号实现乐观锁
步骤1:在名为table_name的表中添加一个版本号字段,字段为1(这条语句代表我对表中添加一个名为version的列,并为它设置默认值1)
ALTER TABLE table_name ADD COLUMN version INT DEFAULT 1;
步骤2:读取数据时,获取这个版本号(执行这条语句后,会从MySQL获取到对应的数据,包括version列,获得一个当前读取到的版本号:current_version)
SELECT column1, column2, ..., version FROM table_name WHERE condition;
步骤3:当满足condition条件且满足当前版本号等于步骤2获取的版本号时,将提及的字段修改为对应值,且将版本号更新:version+1。
UPDATE your_table_name SET column1 = value1, column2 = value2, ..., version = version + 1 WHERE condition AND version = current_version;
注意!!
version = current_version必须满足,此条件代表当前版本并没有被其他事务更新过,才可以进行更新操作!
时间戳
- 每条记录都会保留一个时间戳来表示最后一次的更新时间,更新逻辑基本类似于版本号,只是比较的是时间戳。如果事务在提交时发现记录的时间戳比它开始读取记录时的时间戳更晚,表示此条记录已经被修改,因此此次更新将被拒绝。
使用时间戳实现乐观锁的场景
MySQL中时间戳实现乐观锁
步骤1:在名为table_name的表中添加一个时间戳字段,字段会自动填充为当前的日期和时间(current_timestamp),记录被修改的最后时间(last_update_timestamp)
ALTER TABLE table_name ADD COLUMN last_update_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
步骤2:读取数据时,获取这个时间戳:timestamp(类似版本号步骤2)
SELECT column1, column2, ..., last_update_timestamp FROM table_name WHERE condition;`
步骤3:当满足condition,且last_update_timestamp = timestamp时,将提及的字段修改为对应的值,且将时间戳更新为当前最新的时间戳。
UPDATE table_name SET column1 = value1, column2 = value2, ..., last_update_timestamp = CURRENT_TIMESTAMP WHERE condition AND last_update_timestamp = timestamp;
注意!!
last_update_timestamp = timestamp必须满足,此条件代表当前时间戳并未被更新,才可以进行更新操作!
悲观锁
- 悲观锁也是一种常用的锁,它的核心思想是"持有悲观态度,假设最坏的情况"。
- 顾名思义,乐观锁在操作数据时持悲观态度,默认别的线程会同时修改数据,所以每次拿数据的时候都会上锁,别的线程想拿到这个数据会阻塞它,直至前一个悲观锁释放。
适用引擎:InnoDB
适用场景:写多读少的场景(有效防止数据冲突和不一致性)。
优
少冲突:锁机制能有效防止多线程同时修改数据造成数据的冲突,能很好保证数据一致。
缺
高开销:悲观锁的机制需要数据库额外维护锁的信息,会影响一定性能。
低并发:防止多条线程同时访问同一个锁,悲观锁过多会导致系统并发能力被限制。
死锁:两个或多个事务互相等待对方释放锁时,可能会导致系统挂起。
悲观锁实现方式
悲观锁的类型
数据库级锁(较为少见)
- 数据库级别锁通常用的最多的有:行级锁,表级锁,页级锁。它们通过锁定特定的行或页来锁定大范围的数据。
几种实现悲观锁的常见数据库级锁
行级锁实现悲观锁
①.悲观排他锁
START TRANSACTION;
SELECT * FROM table_name WHERE condition FOR UPDATE;
-- 执行更新操作
UPDATE table_name SET column_name = value WHERE condition;
COMMIT;
开启事务后通过SELECT ... FOR UPDATE
开启悲观锁(排他),执行修改或删除操作后,结束事务,释放锁,此方法添加的是排他锁,其他事务仅能等待该事务结束后方可进行操作。
②.悲观共享锁
START TRANSACTION;
SELECT * FROM table_name WHERE condition LOCK IN SHARE MODE;
-- 只读操作
COMMIT;
开启事务后通过SELECT ... LOCK IN SHARE MODE
开启悲观锁(共享),执行读取操作后,结束事务,并释放锁,此方法添加的是共享锁,其他事务仅可同时读,不可修改,直至锁释放。
应用级锁
- 应用级锁,一种不依赖与数据库特定功能的锁机制,这种锁灵活性更好,不会局限于数据库的控制,完全由应用控制,比如使用关键字、接口、逻辑等。
几种实现悲观锁的应用级锁
在Java中,synchronized是一个关键字,它的作用是用来控制共享资源的并发访问,在方法或代码块中,可以确保同一时间只能有一个线程执行该段代码,达到悲观锁的效果。
实例方法使用关键字
实例方法中锁定当前对象实例,保证同一时刻只有一个线程执行这个方法。
public class TestLock{
public synchronized void synchronizedInstanceMethod(){
//获取当前实例的锁,从此刻开始只有一个线程能执行这个方法
//执行业务操作
}
}
静态方法使用关键字
静态方法中使用关键字,锁定这个类的Class对象,同一时刻只有一个线程可以执行类的任何一个静态方法。
public class TestLock {
public static synchronized void synchronizedStaticMethod() {
// 获取当前类的锁,从此刻开始,同一时间只有一个线程可以执行类的静态方法
// 执行业务操作
}
}
代码块使用关键字
public class TestLock {
private final Object lock = new Object();
public void method() {
synchronized(lock) {
// 只有获得lock对象的锁时,才能执行此代码块
// 进行业务操作
}
}
}
注意
在Java中,ReentrantLock是一个包(java.until,comcurrent.locks)中的一个类,它比synchronized更灵活,与之相比,ReentrantLock提供了一些更为好用的功能:定时请求、可中断、公平锁及非公平锁。
ReentrantLock特性
ReentrantLock语法
import java.util.concurrent.locks.ReentrantLock;
private ReentrantLock lock = new ReentrantLock();
public void performTask() {
lock.lock(); // 获取锁
try {
// 执行需要加锁的操作
} finally {
lock.unlock(); // 释放锁
}
}
ReentrantLockd的使用
import java.util.concurrent.locks.ReentrantLock; //导包
public class Counter {
private final ReentrantLock lock = new ReentrantLock(); //实例化ReentrantLock,final后不能指向另一个ReentrantLock实例
private int count = 0; //计数追踪
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁(避免死锁)
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
间隙锁
- 间隙锁:一种开区间的排他锁,它锁定一段范围的索引记录,主要用于事务性数据库中,维护数据的一致性和隔离性。常用于可重复度或串行化隔离级别的数据库。
- 开区间的锁:如果我想要在锁住1-10这个范围的数据,意为着我需要指定的区间为(0,11),这个时候1-10的数据行的记录就会被锁住,而0和11两行不会有任何影响。
- 注意:间隙锁锁定的是一个区间,而不仅仅是这个区间中的每一条数据。
适用引擎:InnoDB
优
防止幻读:锁定一个范围,防止其他事务在这个范围内插入数据。(幻读:事务A在读取某个范围的记录时,B事务在这个范围插入新的数据,这时候A事务再次读取这个范围时会看到不同的数据。)
缺
锁竞争:高并发环境会影响数据库性能。
死锁风险增加:涉及多张表,多个事务,死锁风险更高。
使用
在MySQL中,间隙锁不需要手动开启,在可重复度与串行化的隔离级别情况下,给定查询的范围即可自定加上间隙锁。
设置隔离级别
-- 设置为可重复读(默认级别,自动使用间隙锁)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 设置为串行化(会使用更严格的锁定)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
范围查询
START TRANSACTION; //开启事务
-- 选择范围查询,InnoDB将自动在需要的地方使用间隙锁
SELECT * FROM table_name WHERE column_name BETWEEN value1 AND value2;
-- 进行其他业务操作...
COMMIT;
假设现在想要在表t1查询id为2-9的数据行,因为间隙锁是开区间锁,所以应该查询的区间为1-10。
START TRANSACTION;
-- 选择范围查询,InnoDB将自动在需要的地方使用间隙锁
SELECT * FROM t1 WHERE id BETWEEN 1 AND 10;
-- 进行其他操作...
COMMIT;
这就可以查询出id为2-9的数据行的数据了。
临键锁
- 临键锁:一种由行锁和间隙锁组合而成的锁,可以有效防止幻读和数据完整性。
适用引擎:InnoDB
特点:区间左开右闭
优
防止幻读:在可重复度级别下,Next-Key Locks通过锁定记录和区间,防止在查询区间内的其他事务插入新行。
事务隔离:在串行化级别下,Next-Key Locks通过强制执行串行化访问数据,保证最高级别的事务隔离。
缺
锁竞争:Next-Key Locks与间隙锁一样,提供数据一致性的保障,但高并发场景下,引起锁竞争和死锁的概率更高。
记录锁
- 记录锁:封锁记录,专门用于锁定表中的单个或多个行记录,是实现行级锁的基础,也可以称为行锁。
适用引擎:InnoDB
优
灵活性高:记录锁可以直接应用于锁定单个行,且可以支持共享锁模式或者排他锁模式。
缺
锁升级:大量使用记录锁,InnoDB可能会考虑将其升级为粒度更粗的锁(表锁),以减少锁的数量及管理开销。
保户能力低:在InnoDB默认的可重复度级别下,单独的记录锁不足以防止幻读的产生,InnoDB为防止这种问题的产生,可能会搭配间隙锁或临键锁来扩展锁定的范围,防止幻读的产生。