
明明开启了事务转账为什么还能少100元两个人各有1000元从账户1转100元到账户2。代码已经写了START TRANSACTION和COMMIT最后却查到900与1000总额从2000变成1900。不是转账手续费也不是数据库算错减法。扣款完成了预期的收款没有完成程序却仍然提交了这份结果。为什么事务没有把它自动救回来因为有两件事必须分开数据库能让一组操作整体提交或撤销不代表数据库知道怎样才算业务成功。常见教程说“中间一条SQL失败整个事务自动回滚”省掉了最重要的条件哪种失败谁决定回滚这次在独立MySQL 8.0.46实例上测试五个场景。账户1和2各有1000元转账100元。服务使用专用数据目录和Unix socket关闭TCP监听没有连接既有数据库。先区分三种证据问题需要检查数据库有没有拒绝语句错误码与异常有没有完成期望的业务操作受影响行数、业务条件整个事务有没有撤销回滚结果、后续查询没有SQL错误不代表完成了收款某条语句被撤销也不代表此前成功的语句一起撤销。图是本次实验结果的简洁排版不是数据库界面截图。初始化只在专用测试库里运行以下语句新建实验库。同名库若已存在不要复用业务库改一个未使用的名字并同步改后续USE。后面的DELETE会清空实验表所以只能对这张专用表执行。CREATE DATABASE transfer_lab; USE transfer_lab; CREATE TABLE account ( id INT PRIMARY KEY, balance DECIMAL(10,2) NOT NULL ) ENGINEInnoDB;每个场景在自动提交开启的独立连接中执行先重置余额再进入显式事务。事务中的语句必须使用同一个连接。交互式mysql客户端报错后继续逐句输入批处理复现故意报错的场景需要--force否则客户端可能提前终止。此选项只用于这次负例实验不是生产错误处理策略。五组实际结果场景账户1账户2总额扣款、重复键错误、继续提交90010001900扣款、重复键错误、主动回滚100010002000扣款、收款人不存在、仍提交90010001900正常扣款与收款90011002000收款人不存在、主动回滚100010002000语句报错不代表整笔扣款撤销第一组故意在扣款之后插入重复主键制造1062错误。这不是正常收款逻辑而是用来观察错误范围的故障注入。错误后同一连接仍能看到900与1000继续COMMIT后这组余额保留下来。USE transfer_lab; DELETE FROM account; INSERT INTO account VALUES(1,1000.00),(2,1000.00); START TRANSACTION; UPDATE account SET balancebalance-100 WHERE id1; INSERT INTO account VALUES(2,999.00); SELECT after_error; SELECT CONCAT(id,:,balance) FROM account ORDER BY id; SELECT SUM(balance) FROM account; COMMIT; SELECT CONCAT(id,:,balance) FROM account ORDER BY id; SELECT SUM(balance) FROM account;INSERT报ERROR 1062 (23000)后续查询的标准输出为after_error 1:900.00 2:1000.00 1900.00 1:900.00 2:1000.00 1900.00第二组使用同样错误但主动ROLLBACK余额恢复1000与1000。USE transfer_lab; DELETE FROM account; INSERT INTO account VALUES(1,1000.00),(2,1000.00); START TRANSACTION; UPDATE account SET balancebalance-100 WHERE id1; INSERT INTO account VALUES(2,999.00); ROLLBACK; SELECT CONCAT(id,:,balance) FROM account ORDER BY id; SELECT SUM(balance) FROM account;同样出现1062但ROLLBACK后的输出为1:1000.00 2:1000.00 2000.00MySQL官方InnoDB错误处理说明区分语句和事务回滚普通重复键错误回滚该语句死锁回滚整个事务。本文实测的是重复键不能将死锁或锁超时也说成已验证。不报错也可能没完成收款第三组把收款账户写成不存在的id99。UPDATE合法但ROW_COUNT()返回0。不检查就提交会把前面的扣款保存下来。USE transfer_lab; DELETE FROM account; INSERT INTO account VALUES(1,1000.00),(2,1000.00); START TRANSACTION; UPDATE account SET balancebalance-100 WHERE id1; UPDATE account SET balancebalance100 WHERE id99; SELECT ROW_COUNT(); COMMIT; SELECT CONCAT(id,:,balance) FROM account ORDER BY id; SELECT SUM(balance) FROM account;输出第一行的0就是收款UPDATE的受影响行数0 1:900.00 2:1000.00 1900.00第四组给扣款加balance100条件两次UPDATE都影响一行余额为900与1100。这里只验证固定正金额、主键匹配和单会话结果不是完整生产转账实现。USE transfer_lab; DELETE FROM account; INSERT INTO account VALUES(1,1000.00),(2,1000.00); START TRANSACTION; UPDATE account SET balancebalance-100 WHERE id1 AND balance100; SELECT ROW_COUNT(); UPDATE account SET balancebalance100 WHERE id2; SELECT ROW_COUNT(); COMMIT; SELECT CONCAT(id,:,balance) FROM account ORDER BY id; SELECT SUM(balance) FROM account;本次观察到两次UPDATE均影响1行1 1 1:900.00 2:1100.00 2000.00SELECT ROW_COUNT()只显示结果不会替应用自动判断或回滚。实际代码必须把异常和业务条件失败接到ROLLBACK路径不能打印出0以后仍然提交。受影响行数还需要结合具体语句及连接选项解释不能推广成任何UPDATE都必须返回1。看到0以后真正执行回滚会怎样第五组还是扣款后给id99收款但观察到0后执行ROLLBACKUSE transfer_lab; DELETE FROM account; INSERT INTO account VALUES(1,1000.00),(2,1000.00); START TRANSACTION; UPDATE account SET balancebalance-100 WHERE id1; UPDATE account SET balancebalance100 WHERE id99; SELECT ROW_COUNT(); ROLLBACK; SELECT CONCAT(id,:,balance) FROM account ORDER BY id; SELECT SUM(balance) FROM account;本次结果0 1:1000.00 2:1000.00 2000.00这组SQL是手动选择回滚的对照不是自动判断行数的程序。它说明一旦选择整笔撤销前面的扣款也可以撤销但谁检查0、谁选择撤销仍需要明确写出来。应用应该在哪一步决定提交对这里固定正金额、唯一账户id的例子可以把提交条件写成下面的流程。这是逻辑提要不是可以直接执行的SQL也不是完整支付系统在同一个连接中开启事务 扣款账户存在且余额足够执行后立即检查受影响行数为1 收款账户存在执行后立即检查受影响行数为1 提交前任一步SQL异常或业务检查失败 - 回滚并报告失败 两步都满足条件 - 尝试提交 提交响应不确定 - 查询业务流水确认不直接重复扣款数据库能判断SQL是否合法、约束有没有违反却不知道id99是否是你写错的收款人。一个合法的零行UPDATE在数据库层可以成功执行在转账业务里却是失败。另外总额2000也只是这组实验的一项证据。如果钱加到了错误账户总额仍可能正确。所以验证同时检查每个账户的余额不能只靠“总额没变”证明业务正确。事务提供整体提交或撤销的能力但“哪些结果值得提交”仍然是业务逻辑的责任。写转账教程时最值得展示的往往不是成功的两条UPDATE而是失败以后究竟留下了什么。附录验证程序的设计每组先重置相同余额再在一个客户端连接内完整执行。程序同时校验输出、错误1062、余额和总额而不是仅看进程退出码。为暴露“报错后仍然提交”的反例客户端使用--force继续执行后续语句。这仅用于实验不是生产建议。客户端停止并断开连接可能使事务回滚不能把这种回滚误认成重复键错误自动回滚了整笔事务。验证时直接提取本文六段SQL一段建表、五段场景。预期结果独立固定在程序中不从待验证输出反推答案随后再比较本文五段text输出与实际结果。这样既能发现理解错误也能发现写稿时把结果抄错。程序结束时仅停止自己创建的mysqld。五组断言通过服务正常结束socket已移除。上面的SQL已经完整列出读者不需要我们的私有脚本或源码包也能独立复现。这次没有做并发、崩溃恢复、提交响应丢失或硬件持久性测试。生产还需幂等流水、金额与账户校验、锁顺序及死锁重试提交响应不确定时应先确认流水状态不能直接重复扣款。官方事务说明也不能替代这些业务设计。