一、MySQL数据结构选择
二、MySQL性能优化explain关键字详解
三、MySQL索引优化
四、MySQL事务
五、MySQL锁机制
六、MySQL多版本并发(MVCC)机制
MySQL的锁机制是数据库并发控制的重要组成部分,目的是防止多个事务对数据的并发访问
导致数据不一致或其他问题,区别于JUC并发编程中的锁,从不同的角度,MySQL的锁通常分为:
锁的两个重要概念是粒度
与冲突
,锁的粒度指的是锁作用的范围,粒度越小,锁的冲突概率越低,系统的并发能力越强;但管理锁的开销也会增加。锁冲突是指两个或多个事务试图以不兼容的方式访问相同的数据。例如,一个事务试图加排他锁,而另一个事务已经持有该数据的共享锁。
锁的相关命令:
和JUC的悲观锁思想一样,“假设会发生冲突”,因此在数据操作前,它会主动获取锁,防止其他事务并发地修改数据。悲观锁适用于冲突概率较高的场景,特别是在对数据一致性要求较高的情况下。
通常会在对数据进行操作之前显式地加锁,并且操作完成之后才释放锁。锁定的时间长,贯穿整个操作的过程。
MySQL的InnoDB存储引擎通过行级锁来实现悲观锁,使用SELECT FOR UPDATE
或LOCK IN SHARE MODE
语句来加锁数据。
BEGIN;
SELECT * FROM account WHERE account_id = 1 FOR UPDATE; -- 获取排他锁
UPDATE account SET balance = balance - 100 WHERE account_id = 1;
COMMIT;
上述的sql语句,在执行更新操作时,account_id = 1
的行被锁定,其他事务无法同时对该行数据进行修改。
乐观锁的思想在于,“假设不会发生冲突”,也就是在对于数据的操作时不会显式地加锁,而是在操作完成后,对原始数据
进行比对,确定本事务在操作的过程中,数据没有被其他数据修改。如果原始数据
已经被修改,则放弃本次更新,再次重试。(CAS机制)。
乐观锁通常通过版本号或时间戳来实现:
-- 事务A读取数据
SELECT id, name, price, version FROM products WHERE id = 1;
-- 事务A尝试更新数据,检查版本号是否一致
UPDATE products
SET price = price + 10, version = version + 1
WHERE id = 1 AND version = 1; -- version = 1 是事务A读取时的版本号
如果事务A读取时的版本号和更新时的版本号不一致,表示数据已被其他事务修改,事务A将回滚或重试。
乐观锁和悲观锁相比,减少了显式加锁的开销,但是乐观锁重试的过程对于cpu性能也有一定的影响。适用于冲突概率不高的场景。
特性 | 悲观锁 (Pessimistic Lock) | 乐观锁 (Optimistic Lock) |
---|---|---|
锁的策略 | 事务操作时会加锁,假设有冲突 | 事务操作时不加锁,假设没有冲突,提交时检查冲突 |
锁的粒度 | 锁的粒度较大,通常为行级锁或表级锁 | 锁的粒度较小,通常基于版本号或时间戳字段来实现 |
并发性能 | 可能会导致较高的锁竞争,性能下降 | 性能较好,适用于读多写少的场景,但存在冲突时需要重试 |
使用场景 | 适用于冲突概率较高,数据一致性要求较高的场景 | 适用于冲突概率较低,数据读取较多,写入较少的场景 |
实现复杂度 | 相对简单,直接通过数据库加锁实现 | 实现较为复杂,需要额外的字段(如版本号或时间戳)来支持冲突检测 |
锁的持续时间 | 锁持有时间较长,直到事务提交或回滚 | 锁持有时间较短,事务提交时才检查冲突 |
表锁指的是,对于表中某一行数据的修改,会将整张表锁定。MyISAM存储引擎默认使用表锁。其开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低,表锁可以通过 LOCK TABLES 命令显式地锁定:
LOCK TABLES 表名 WRITE;
通常是在对于数据库表迁移时使用。
和表锁相对应,行锁则是需要修改表中的哪一行数据,就对于该行数据上锁。InnoDB存储引擎默认使用行锁。相较于表锁,力度小,并发性能高,上文中悲观锁的SELECT FOR UPDATE
或LOCK IN SHARE MODE
语句便可以显式加行锁。
页锁是介于表锁和行锁之间的锁,锁定的是数据页。数据页是数据库中存储数据的最小单位,MySQL的InnoDB存储引擎将数据组织在大小为16KB的页面中。相当于行锁和表锁的折中方案。锁粒度适中,适用于中等并发的场景。
特性 | 表锁 (Table-level Lock) | 行锁 (Row-level Lock) | 页锁 (Page-level Lock) |
---|---|---|---|
锁定粒度 | 锁定整个表 | 锁定某一行 | 锁定某一数据页(通常为16KB) |
并发性 | 最差,多个事务对同一表的操作会阻塞其他事务 | 最好,多个事务可以并发操作不同的行 | 中等,多个事务可以并发操作不同的页,但同一页内的操作会发生阻塞 |
锁管理开销 | 最小,锁定粒度大,管理开销较小 | 最大,锁管理较为复杂 | 中等,管理开销大于表锁但小于行锁 |
适用场景 | 适用于低并发、操作简单的场景 | 适用于高并发、高频繁更新的场景 | 适用于中等并发和批量操作的场景 |
死锁概率 | 最低 | 较高,尤其是多个事务竞争同一行数据时 | 中等,死锁发生的概率低于行锁,但高于表锁 |
读锁是控制共享访问
的锁,即多个事务可以对同样的数据进行读操作
,但是任何事务都不可进行写操作
。
-- 事务A读取数据,获取共享读锁
BEGIN;
SELECT * FROM account WHERE account_id = 1;
-- 事务A在此时可以读取数据,但无法进行更新
写锁是控制单独访问
的锁,即只有持有写锁的事务,可以对数据进行写操作
,同时阻止其他事务对该数据进行读写操作
。
-- 事务B更新数据,获取排他写锁
BEGIN;
UPDATE account SET balance = balance - 100 WHERE account_id = 1;
-- 事务B执行期间,其他事务无法读取或修改该行数据
意向锁本身并不实际锁定数据,而是用来表明事务的意图
。当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有行锁了,其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不
该加表锁。
意向锁有助于协调不同粒度的锁(如表锁和行锁),保证事务在锁的管理中不会发生冲突,可以分为
假设我们有一个 account 表,事务A想要对某行数据加行级锁,但首先会加意向锁。
-- 事务A对表加意向排他锁,表示打算对某行加排他锁
LOCK TABLES account WRITE;
-- 事务A对具体的行加排他锁
SELECT * FROM account WHERE account_id = 1 FOR UPDATE;
锁类型 | 描述 | 并发情况 | 应用场景 |
---|---|---|---|
读锁(共享锁) | 允许多个事务共享读取数据,但不能修改数据。 | 多个事务可以同时加读锁,互不影响。 | 读多写少的场景,如报告查询、数据统计等。 |
写锁(排他锁) | 只允许一个事务对数据进行修改,其他事务无法读取或修改该数据。 | 会阻塞其他事务,直到持锁事务完成。 | 数据更新、插入、删除等操作。 |
意向锁 | 用于表明事务打算对某些行加锁的意图,不实际锁定数据。 | 不会阻塞其他事务,但会影响对数据的锁定操作。 | 多粒度锁的场景,主要用于协调表锁和行锁。 |
间隙锁锁定的是范围数据中的间隙,并且是在可重复读
隔离级别下才会生效,假设目前表中的数据如下,有(4,10),(10,15),(15,正无穷)的三个区间
执行如下的sql:
select * from account WHERE id = 11 for UPDATE