事务未真正开启时,UPDATE和INSERT实际各自独立提交,因MySQL默认autocommit=1,每条语句立即落盘;PostgreSQL和SQLite同理,必须显式执行BEGIN才能建立事务上下文。

事务没真正开启,UPDATE和INSERT根本不在同一个事务里
最常见的情况是:你以为自己在事务中,其实每条语句都是独立提交的。MySQL 默认 autocommit=1,UPDATE 执行完立刻落盘;等后面的 INSERT 报错回滚时,UPDATE 早已不可逆。PostgreSQL 和 SQLite 同理——不显式执行 BEGIN,就没有事务上下文。
实操建议:
- Python(sqlite3):必须调用
conn.execute("BEGIN"),别只设isolation_level = None就以为开启了事务 - Java(JDBC):
connection.setAutoCommit(false)必须在第一条 SQL 前设置,且不能漏掉 - Node.js(pg):用
client.query("BEGIN")+client.query("COMMIT")或client.query("ROLLBACK"),别用pool.query()直接发修改语句 - Django:包裹关键逻辑在
transaction.atomic()中,确保所有操作共享同一连接
跨连接操作让“原子性”失效
Web 应用里最容易踩的坑:扣库存、记订单、发通知这三步看似在一个事务里,但发通知如果走 HTTP 调用再连一次 DB,那它用的是全新连接,完全不受主事务控制。哪怕主事务回滚了,通知服务那条 INSERT 依然生效。
实操建议:
- 所有 DML 操作必须复用同一个数据库连接对象(如 Go 的
*sql.Tx、PHP PDO 的同一$pdo实例) - 避免在事务块内发起外部 HTTP 请求后顺手写 DB —— 这种操作应拆到事务外,用最终一致性补偿
- 检查中间件、日志模块是否悄悄 new 了一个连接去写 audit_log 表,它会自动 commit
ROLLBACK 没手动触发,事务“挂着”没结束
多数驱动不会在异常时自动回滚。你 INSERT 失败抛出异常,UPDATE 却没被撤销,是因为连接还开着,事务状态仍是 active。这个连接如果被放回连接池,下次被复用时可能带着未结束的事务,导致锁表或数据错乱。
实操建议:
- Python:用
try/except/finally,finally里判断conn.in_transaction再决定是否rollback() - Node.js:Promise 链中必须
.catch()并显式执行client.query("ROLLBACK"),不能只throw - PostgreSQL:可设
SET statement_timeout = '30s'防止长事务卡死
触发器或存储过程内部隐式提交
某些数据库(如 MySQL 在特定模式下)允许触发器里执行 CREATE TABLE、ALTER 等 DDL,这会隐式触发 COMMIT,导致前面的 UPDATE 提交,后续 INSERT 失败也无济于事。SQL Server 的某些系统过程也有类似行为。
实操建议:
- 避免在事务中调用含 DDL 的存储过程或触发器
- MySQL 中检查是否启用了
autocommit或implicit_commit相关变量 - PostgreSQL 中注意
TRUNCATE是 DDL,会强制提交当前事务
事务不是开关,是连接+显式 BEGIN+显式 COMMIT/ROLLBACK 的组合。最容易被忽略的是:你以为的“一个事务”,其实是多个连接、多个自动提交语句、多个没清理的 pending 状态。










