导读:本期聚焦于半夏创作的《SQL中如何用触发器实现并发更新同一行数据的版本号校验?》,敬请观看详情。并发更新同一行数据时,丢失更新往往发生在毫秒级的时间窗口内:两个事务都读到了旧版本,后提交的事务覆盖了先提交的修改。应用层惯用的乐观锁做法是在UPDATE语句的WHERE条件里带上版本号,再判断影响行数。但这种方式完全依赖开发者的编码纪律,一旦某个接口忘记写version条件或者漏掉version自增,就会埋下数据覆盖的风险。把版本号校验下沉到数据库层是一个更稳妥的思路,利用BEFORE UPDATE触发器可以在行更新前自动比对版本号,并强制递增版本列。本文通过一个账户余额并发扣减的例子,展示如何创建带版本号的表、编写触发器、执行带版本校验的更新语句,同时分析触发器方案在性能、可移植性和异常处理上的取舍。读完可以判断这种数据库层乐观锁是否适合当前业务。

在数据库并发控制中,同一行数据被多个事务读取并尝试更新时,如果缺少保护机制,后提交的事务会静默覆盖先提交的修改,也就是常说的丢失更新。乐观锁的核心思路不是加锁阻塞,而是通过版本号记录行的修改次数,更新时校验版本号是否仍然是读取时的值。如果版本已经变化,说明有其他事务改过这一行,当前更新必须失败并由应用层决定重试或放弃。最简单的乐观锁写法是在应用代码中执行类似 UPDATE account SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 5 这样的语句,然后判断影响行数是否为0。这种方式可靠,但存在一个明显短板:版本号递增逻辑分散在每个更新入口,开发者一旦遗漏 version = version + 1,乐观锁就会失效。

SQL中如何用触发器实现并发更新同一行数据的版本号校验?

一、丢失更新问题与应用层乐观锁的局限

假设账户表里有一行余额数据,两个事务同时读取到余额为1000、版本号为5。事务A准备扣减100,事务B准备扣减200。如果没有任何并发控制,A先把余额改成900并提交,随后B基于旧余额1000计算后把余额改成800并提交,A的修改就被覆盖了,账户余额凭空多出100。这种问题在并发量上来之后非常隐蔽,测试环境很难复现。

应用层乐观锁的典型做法是在数据表中增加一个整型的 version 列,每次更新都必须带上版本号条件,并且把版本号加1。例如事务B扣减时执行:

UPDATE account
SET balance = balance - 200,
    version = version + 1
WHERE id = 1
  AND version = 5;

如果事务A已经先把version从5改成了6,那么事务B执行这条语句时影响行数为0,应用层就知道发生了并发冲突,可以选择重试或者提示用户。这个方案本身没有问题,但它高度依赖开发规范。只要有一个更新入口忘记在WHERE条件中加 version = ?,或者忘记执行版本号自增,数据覆盖的风险就会重新出现。在团队人数较多、业务迭代频繁的项目中,这种依赖人工纪律的乐观锁很容易出现漏洞。

二、用BEFORE UPDATE触发器实现版本号自动校验

把版本号校验逻辑下沉到数据库层,可以借助行级触发器完成。以MySQL为例,先创建一张带版本号的账户表:

CREATE TABLE account (
    id INT PRIMARY KEY,
    balance DECIMAL(10,2) NOT NULL,
    version INT NOT NULL DEFAULT 0
);

接着创建一个 BEFORE UPDATE 触发器。它的作用是在每一行被实际更新之前,先检查 NEW.version 是否与数据库当前的 OLD.version 一致。如果不一致,说明这一行已经被其他事务修改过,直接抛出异常终止更新。如果一致,则把版本号自动加1,应用层不需要再手动写自增逻辑。

DELIMITER $$

CREATE TRIGGER trg_account_version_check
BEFORE UPDATE ON account
FOR EACH ROW
BEGIN
    IF NEW.version != OLD.version THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'version conflict, row has been modified by another transaction';
    END IF;

    SET NEW.version = OLD.version + 1;
END$$

DELIMITER ;

这里的约定是:应用层读取数据时记录下 version 的值,执行更新时在SET子句中显式传入这个旧版本号。例如读取到余额1000、版本号5,业务准备扣减200时执行:

UPDATE account
SET balance = balance - 200,
    version = 5
WHERE id = 1;

注意这条语句没有在WHERE中写 version = 5,也没有手动写 version = version + 1。应用层把 version 设置为读取时的值5,触发器会拿这个5和 OLD.version 做比较。如果当前数据库里版本号还是5,校验通过,触发器自动把更新后的版本号改成6。如果这期间其他事务已经把它改成了6,那么 NEW.version 为5而 OLD.version 为6,二者不相等,触发器直接报错,后到的事务无法覆盖先提交的结果。

三、并发场景下的执行流程验证

为了验证触发器方案的有效性,可以模拟两个事务并发操作同一行数据。首先客户端A和客户端B同时读取账户余额和版本号,都读到 balance = 1000、version = 5。客户端A先执行更新:

UPDATE account
SET balance = balance - 100,
    version = 5
WHERE id = 1;

由于此时数据库中的 OLD.version 是5,与 NEW.version 一致,触发器校验通过,并将版本号自动修改为6。客户端A提交事务后,这一行的版本号变成6。

紧接着客户端B也提交它的更新,但它传入的仍是读取时的版本号5:

UPDATE account
SET balance = balance - 200,
    version = 5
WHERE id = 1;

此时数据库中 OLD.version 已经变成6,而 NEW.version 是5,触发器会立即抛出 SQLSTATE '45000' 的异常,客户端B的事务被终止。它不会覆盖客户端A的修改,应用层捕获异常后可以重新读取数据并再次尝试。这个过程中,版本号校验和递增都在数据库内部完成,应用层只需要传入读取到的旧版本号,不需要关心加1逻辑,也不会因为遗漏自增而破坏乐观锁。

如果应用层完全忘记在SET子句中传入版本号,触发器会看到 NEW.version 等于 OLD.version,此时校验仍然通过,版本号会正常加1。这在没有并发冲突的情况下不会产生问题,但如果存在并发冲突,由于应用层没有提供可供比对的旧版本号,触发器无法主动发现有人修改过同一行。因此,触发器方案并不能完全消除应用层传版本号的要求,而是把校验和自增步骤合并到了数据库层,降低了遗漏自增的风险。实践中仍然建议将“读取时保存version、更新时回传version”作为统一的编码规范。

四、触发器乐观锁的优缺点与生产环境注意事项

触发器方案最大的优点是版本号递增逻辑集中管理,应用层代码更简洁。开发人员只需要记住更新时把读取到的版本号原样传入,不需要再手动写 version = version + 1,也不容易因为粗心把递增写成固定值或者漏写。另一个优点是数据库层多了一层兜底校验,即使有部分更新入口绕过了WHERE版本条件,只要它在SET中传入了旧版本号,触发器仍然能拦住并发覆盖。

但这种方案也有明显的代价。触发器是在每一行更新前自动执行的,涉及额外的比较和赋值操作,在高并发写入场景下会带来一定的CPU开销。对于更新非常频繁的热点行,触发器可能放大锁持有时间,进一步加剧争用。此外,不同数据库的触发器语法差异较大,MySQL使用 SIGNAL 抛出异常,SQL Server需要使用 RAISERROR,PostgreSQL则使用 RAISE EXCEPTION。一套触发器逻辑很难跨数据库直接移植。

生产环境中使用触发器乐观锁时,需要特别注意几个问题。第一,应用层必须统一约定更新语句中 version 字段的含义为“期望的旧版本号”,不要与自增后的新版本号混淆。第二,触发器中的异常应该被应用层捕获并解析,避免直接暴露给前端用户。第三,如果使用了ORM框架,要确认ORM生成的UPDATE语句是否会默认带上所有字段,否则可能因为 version 未包含在SET子句中而导致触发器无法完成比对。第四,批量更新语句会逐行触发 BEFORE UPDATE 触发器,如果更新行数非常多,触发器开销会被线性放大,需要评估是否仍然适用。

总体而言,触发器实现乐观锁适合那些希望用数据库约束来统一版本号管理、应用层更新入口较多且对代码一致性要求较高的项目。它可以有效降低人工遗漏版本号自增带来的风险,但前提是应用层仍然需要配合传入读取到的旧版本号。如果团队已经建立了完善的ORM和DAO层封装,应用层乐观锁配合统一的更新方法也能达到相同效果,触发器方案则是在数据库层面再加上一道保险。

乐观锁版本号校验SQL触发器修改时间:2026-10-02 18:32:15

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1002/64782.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。