登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 事务隔离级别与锁定读的组合方案

来源:17golang原创

时间:2026-10-31 23:22:41 271浏览 收藏

MySQL 事务隔离级别与锁定读的组合方案

InnoDB 中,隔离级别决定读视图和部分锁策略,锁定读则用于保护随后要依赖或修改的数据。按已存在主键读取库存再扣减,可以在 READ COMMITTED 或 REPEATABLE READ 下使用同一事务中的 FOR UPDATE;只把隔离级别调高,并不能替代清晰的事务边界、索引条件和错误处理。

官方地址:https://dev.mysql.com/doc/refman/8.4/en/。本文以 MySQL 8.4 的 InnoDB 语义为范围,示例使用独立演示库。

先确定要保护哪项库存不变量

假设一个SKU只有一条库存记录,每次预留数量必须为正,成功后可用库存不能为负。这里保护的是单行数量变化,不包括订单创建、跨仓调拨和支付。先把目标缩小,才能判断需要普通读取、共享锁还是排他锁。

如果只是查询页面显示数量,普通SELECT通常更符合读取目的;如果必须先依据数量作决定,再修改同一行,就应考虑FOR UPDATE。若唯一任务是“库存足够就减掉数量”,带数量条件的单条UPDATE也能表达该不变量,不必为了形式完整额外读一次。本文保留锁定读,是为解释读后决策场景。

准备 InnoDB 与主键访问条件

下面的表用主键精确定位SKU。先在独立演示库准备表和虚构数据,再创建过程。DDL与库存预留不放进同一个业务事务,避免混淆事务控制。

-- 在独立演示库中准备表;DDL不放进业务事务。
CREATE TABLE stock_demo (
    sku_id BIGINT PRIMARY KEY,
    available INT NOT NULL,
    CONSTRAINT ck_stock_demo_nonnegative CHECK (available >= 0)
) ENGINE = InnoDB;

-- 使用虚构SKU,避免操作真实库存。
INSERT INTO stock_demo (sku_id, available) VALUES (701, 20);

示例讨论的是已经存在的主键记录。若查询范围、联合索引条件或不存在记录发生变化,锁的覆盖也需要重新判断;不能根据WHERE里写了一个等号就认定永远只锁一条记录。

隔离级别与普通读锁定读的静态组合关系
结构说明图:隔离级别、普通读取与锁定读取是不同设计维度,示例把库存写入建立在同一事务的锁定读上。

按读写目标组合隔离级别

组合适合讨论的目标需要注意
READ COMMITTED与普通SELECT按语句读取已提交数据同一事务多次读取可能不同
REPEATABLE READ与普通SELECT需要一致读视图的读取读视图不是未来修改的排他权
READ COMMITTED与FOR UPDATE锁住当前要修改的记录不能依赖它阻止一般范围内的新行插入
REPEATABLE READ与FOR UPDATE结合当前锁定读与相应索引锁策略范围扫描和唯一精确命中的锁覆盖不同
FOR SHARE保护读取所依赖的记录读后还要升级写锁时可能产生竞争

官方 隔离级别说明区分了两种一致读视图,也说明RC通常不使用间隙锁,但外键和重复键检查有例外。RR的范围锁定不能套用到RC。锁定读使用的状态与RR早先建立的一致读视图也可能不同,不应混用两次查询的值作同一判断。

在一个事务里完成锁定与扣减

下面给出mysql命令行客户端可使用的存储过程示例。它拥有事务,调用时必须确保连接没有活动事务;不要从外层订单事务中调用,以免START TRANSACTION影响已有事务边界。过程只演示单SKU库存扣减,不是完整订单系统。

-- 以下DELIMITER是mysql客户端指令,不是应用驱动SQL。
DELIMITER //
CREATE PROCEDURE reserve_stock_demo(IN p_sku BIGINT, IN p_qty INT)
SQL SECURITY INVOKER
BEGIN
    DECLARE v_available INT DEFAULT NULL;
    -- SQL失败时结束整个事务,并把错误交还调用方。
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_available = NULL;

    -- 本过程拥有事务,只能在无活动事务的连接上调用。
    IF p_sku IS NULL OR p_qty IS NULL OR p_qty = p_qty;

    IF ROW_COUNT()  1 THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'reservation rejected';
    END IF;
    COMMIT;
END//
DELIMITER ;

-- 在无活动事务的同一连接上,为下一事务指定隔离级别。
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
CALL reserve_stock_demo(701, 2);

过程先检查输入,再开启事务。FOR UPDATE取得库存值,数量不足或SKU不存在时抛错;异常处理器回滚并重新抛出,成功路径提交。调用方只有收到成功响应才能确认本次预留结果。代码未在本文制作中执行,不包含实测锁等待或数据库截图。

要改成RR,可在无活动事务的连接上把SET TRANSACTION语句中的READ COMMITTED替换为REPEATABLE READ;其余单行逻辑不必因此改为普通SELECT。此处不使用SET SESSION,避免示例把隔离级别长期留在连接池会话上。应用接入时仍应让设置与事务在同一物理连接执行,并确认驱动的连接复用行为。

库存过程和事务资源的静态归属关系
结构说明图:预留过程拥有事务和数量判断,库存表拥有主键及可用数量;提交或回滚决定事务的结束,不是跨系统业务完成证明。

锁要等事务结束才释放,不要在持锁期间等待支付接口、人工确认或慢速外部请求。FOR UPDATE也不意味着普通一致读全部被阻塞,其他事务仍可能读取其读视图中的版本。具体规则见 锁定读文档。

把权限和错误处理放进事务边界

示例采用SQL SECURITY INVOKER,运行时使用调用者权限,不借过程定义者身份扩大访问范围。创建过程与调用过程使用的账号职责应分开。MySQL 8.4中FOR UPDATE需要SELECT以及UPDATE、DELETE、LOCK TABLES中的至少一种权限;本例本来就要UPDATE库存,不需要为了锁定读另外授予删除能力。

死锁与等待超时必须作为业务失败处理,不要在事务状态不明确时只重发最后一条UPDATE。该过程的SQL异常路径主动回滚;若应用选择重试,应建立新事务、限制次数并采用退避。连接中断或提交响应丢失时,调用方还可能无法判断提交是否生效,因此真实预留接口需要独立业务请求号与幂等记录。本例没有这张记录表,不能直接无条件重试扣减。

日志宜记录业务请求号、SKU标识、请求数量、错误类别和耗时,避免输出连接口令或完整客户数据。锁等待与失败计数有助于定位争用,但不能仅凭接口超时判断“事务一定没提交”。

接入前的检查与常见问题

在自己的演示环境中,可以用两个独立连接请求同一SKU,观察竞争是否遵守预期,并覆盖库存不足、缺失SKU、非法数量和错误回滚路径。接入时还应确认主键访问条件、事务是否过长、存储过程权限和客户端错误处理。这些是读者的接入建议,本文发布流程未运行数据库校验。

能否用FOR SHARE读取后再修改?可以出现锁升级竞争。明确准备修改同一行的业务,优先把排他锁意图写清;不能把共享锁当作没有代价的FOR UPDATE替代品。

SKIP LOCKED可以防止库存接口等待吗?它会排除已锁记录,可能把正在竞争的SKU表现为空结果。适用于允许跳过记录的队列等语义,不应直接替换普通库存查询。

选择RC就不再有死锁吗?不是。隔离级别调整不能取消所有锁竞争;多行业务仍需要一致的锁定顺序、短事务和有界重试。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>