Mysql中的共享锁、排他锁、悲观锁、乐观锁等及使用场景

Mysql中的共享锁、排他锁、悲观锁、乐观锁等及使用场景

一、背景与问题

在高并发系统中,数据一致性是核心挑战之一。当多个事务同时访问同一数据时,会出现读-写冲突写-写冲突,导致数据不一致或业务逻辑错误。MySQL通过锁机制来控制并发访问,其中共享锁(Shared Lock)、排他锁(Exclusive Lock)是行级锁的基础,而悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)是锁策略的两种范式。

核心问题在于:

  • 如何在读写操作中保证事务的隔离性
  • 如何在高并发场景下避免死锁锁等待
  • 如何在不同业务场景中选择合适的锁机制

本文将从底层原理出发,结合真实业务场景,深入探讨这些锁机制的实现方式、使用场景及注意事项。


二、基本原理

1. 锁的分类

MySQL的锁机制分为共享锁(Shared Lock, S Lock)和排他锁(Exclusive Lock, X Lock),它们是行级锁的基础:

锁类型读操作写操作适用场景
共享锁(S Lock)允许不允许读取数据时防止写入
排他锁(X Lock)不允许允许更新数据时防止读写

事务隔离级别决定了锁的持有时间和冲突处理方式,例如在REPEATABLE READ隔离级别下,共享锁和排他锁会持续到事务结束。

2. 悲观锁与乐观锁

  • 悲观锁:假设冲突频繁,在读取数据时即加锁,典型实现是SELECT ... FOR UPDATE
  • 乐观锁:假设冲突较少,在更新时检查版本号或时间戳,典型实现是version字段。

三、环境准备

1. 数据库准备

创建测试表并插入数据:

-- 创建库存表
CREATE TABLE inventory (
    id INT PRIMARY KEY,
    product_name VARCHAR(50),
    stock INT,
    version INT DEFAULT 1
);

-- 插入初始数据
INSERT INTO inventory (id, product_name, stock, version) VALUES
(1, 'Laptop', 100, 1),
(2, 'Phone', 200, 1);

2. 连接工具

使用mysql命令行客户端或Navicat等工具执行SQL,确保数据库隔离级别为REPEATABLE READ(默认)。


四、核心实现

1. 共享锁与排他锁

共享锁通过SELECT ... FOR SHARE实现,允许其他事务读取但禁止写入;排他锁通过SELECT ... FOR UPDATE实现,禁止所有并发操作。

-- 事务1: 使用共享锁
START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR SHARE;
-- 等待事务2执行
COMMIT;

-- 事务2: 使用排他锁
START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR UPDATE;
-- 此时事务1的共享锁会阻塞事务2的排他锁
COMMIT;

关键点

  • 共享锁和排他锁的持有时间取决于事务的提交/回滚。
  • 当事务1持有共享锁时,事务2的排他锁会等待,直到事务1提交或回滚。

2. 悲观锁实现(库存扣减)

在电商系统中,库存扣减需要保证原子性:

-- 事务1: 悲观锁实现
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
SET @stock = 100;
SET @version = 1;
UPDATE inventory 
SET stock = @stock - 10, version = @version + 1 
WHERE id = 1 AND version = @version;
COMMIT;

关键点

  • FOR UPDATE会立即加锁,防止其他事务修改数据。
  • 适用于高并发写操作的场景,但可能导致锁等待

3. 乐观锁实现(库存扣减)

通过版本号控制更新:

-- 事务1: 乐观锁实现
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1;
SET @stock = 100;
SET @version = 1;
UPDATE inventory 
SET stock = @stock - 10, version = @version + 1 
WHERE id = 1 AND version = @version;
COMMIT;

关键点

  • 只在更新时检查版本号,避免了锁等待。
  • 适用于读多写少的场景,但需要确保版本号更新逻辑正确。

五、完整案例

1. 电商库存扣减系统

业务场景:两个事务同时尝试扣减同一商品库存,需确保最终库存正确。

悲观锁实现

-- 事务1: 悲观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
SET @stock = 100;
SET @version = 1;
UPDATE inventory 
SET stock = @stock - 10, version = @version + 1 
WHERE id = 1 AND version = @version;
COMMIT;

-- 事务2: 悲观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
SET @stock = 90;
SET @version = 2;
UPDATE inventory 
SET stock = @stock - 10, version = @version + 1 
WHERE id = 1 AND version = @version;
COMMIT;

输出结果:库存变为80,版本号为3。

性能分析:悲观锁在高并发时可能导致锁等待,但能保证数据一致性。

乐观锁实现

-- 事务1: 乐观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1;
SET @stock = 100;
SET @version = 1;
UPDATE inventory 
SET stock = @stock - 10, version = @version + 1 
WHERE id = 1 AND version = @version;
COMMIT;

-- 事务2: 乐观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1;
SET @stock = 90;
SET @version = 2;
UPDATE inventory 
SET stock = @stock - 10, version = @version + 1 
WHERE id = 1 AND version = @version;
COMMIT;

输出结果:库存变为80,版本号为3。

性能分析:乐观锁避免了锁等待,但需要处理更新失败的场景(如版本号不匹配)。


六、源码解析

1. MySQL锁机制源码

MySQL的锁机制核心在trx0sys.cc中实现,核心逻辑如下:

void trx_lock_wait_for_lock(trx_t *trx, lock_t *lock) {
    // 等待锁的获取
    if (lock->type == LOCK_X) {
        // 排他锁
        lock_wait_for_lock(trx, lock, LOCK_WAIT_FOREVER);
    } else {
        // 共享锁
        lock_wait_for_lock(trx, lock, LOCK_WAIT_FOREVER);
    }
}

关键点

  • 锁的等待机制通过lock_wait_for_lock实现。
  • 排他锁会阻塞所有其他事务的读写操作。

2. InnoDB行锁实现

InnoDB的行锁通过lock_rec_lock函数实现,核心逻辑如下:

void lock_rec_lock(
    trx_t *trx,                /*!< transaction */
    ulint mode,                /*!< lock mode */
    ulint type,                /*!< lock type */
    ulint index_id,            /*!< index id */
    const byte *rec,           /*!< record */
    ulint rec_version)         /*!< record version */
{
    // 获取锁
    if (mode == LOCK_X) {
        // 排他锁
        lock_rec_get_lock(trx, rec, mode, type, index_id, rec_version);
    } else {
        // 共享锁
        lock_rec_get_lock(trx, rec, mode, type, index_id, rec_version);
    }
}

关键点

  • 行锁的获取与释放由lock_rec_get_locklock_rec_unlock控制。
  • 锁的粒度决定了并发性能。

七、进阶使用

1. 锁的超时设置

通过innodb_lock_wait_timeout参数控制锁等待时间:

SET innodb_lock_wait_timeout = 50; -- 设置为50秒

适用场景:避免长时间等待导致的死锁。

2. 锁的粒度控制

  • 行锁:精确到单行,适合高并发写操作。
  • 表锁:适合批量操作,但会降低并发性能。

示例

LOCK TABLES inventory WRITE; -- 表锁
UNLOCK TABLES;

适用场景:数据导入导出等批量操作。

3. 乐观锁的版本号策略

  • 自增版本号:适合简单场景。
  • 时间戳:适合需要记录更新时间的场景。

示例

ALTER TABLE inventory ADD COLUMN update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP;

八、性能与工程实践

1. 性能优化

场景优化策略
高并发写使用排他锁,但控制事务粒度
高并发读使用共享锁,避免锁等待
锁等待设置合理的锁等待超时
死锁使用SHOW ENGINE INNODB STATUS排查

2. 安全风险

  • 死锁:事务A持有锁1,事务B持有锁2,两者相互等待。
  • 锁丢失:未在事务中使用锁,导致数据不一致。
  • 锁粒度过大:导致并发性能下降。

解决方案

  • 使用SELECT ... FOR UPDATE控制写锁。
  • 使用version字段实现乐观锁。
  • 使用SHOW ENGINE INNODB STATUS监控死锁。

3. 异常处理

BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;

    START TRANSACTION;
    SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
    -- 更新逻辑
    COMMIT;
END;

九、常见问题与踩坑

1. 锁等待导致的性能瓶颈

错误示例

START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR UPDATE;
-- 长时间未提交

问题:其他事务会等待该锁,导致阻塞。

解决办法:在事务中尽快完成操作,或使用SET innodb_lock_wait_timeout限制等待时间。

2. 乐观锁版本号不匹配

错误示例

UPDATE inventory SET stock = 90 WHERE id = 1 AND version = 1;

问题:如果版本号已更新,更新会失败。

解决办法:在事务中重新获取版本号并更新。

3. 未正确处理锁的事务提交

错误示例

START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR UPDATE;
-- 未提交事务,导致锁一直存在

问题:事务未提交,锁会一直持有,影响并发。

解决办法:确保事务在完成后正确提交或回滚。


十、最佳实践

1. 选择锁策略的依据

场景推荐策略
高并发写悲观锁(排他锁)
高并发读乐观锁(版本号)
业务逻辑复杂混合使用共享锁和排他锁
批量操作表锁(需谨慎)

2. 锁的粒度控制

  • 行锁:适合高并发写操作。
  • 乐观锁:适合读多写少的场景。
  • 避免大范围锁:如SELECT * FROM table FOR UPDATE

3. 死锁预防

  • 按顺序加锁:所有事务按相同顺序加锁。
  • 避免长事务:事务应在最短时间完成。
  • 监控死锁:定期使用SHOW ENGINE INNODB STATUS

十一、总结

MySQL的锁机制是保障数据一致性的重要工具,但需要根据业务场景选择合适的策略。共享锁和排他锁是行级锁的基础,而悲观锁和乐观锁是两种不同的锁策略。

在实际开发中:

  • 悲观锁适合写多读少的场景,但可能导致锁等待;
  • 乐观锁适合读多写少的场景,但需要处理版本号不匹配的异常;
  • 死锁是必须避免的问题,需通过监控和锁顺序控制;
  • 性能优化需要结合事务粒度、锁等待时间和锁粒度进行调整。

掌握这些锁机制的核心原理和使用场景,是构建高并发、高可靠系统的基石。在实际项目中,需要根据业务需求权衡选择合适的锁策略,同时注意异常处理和性能优化。

最后修改于:2026年09月18日 14:35

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日