
刚工作那两年我也被同样的问题问住过执行完 delete 之后到底要不要 commit当时带我的主管瞄了一眼代码淡淡地说了句“你先把事务概念理清楚再来改”。后来我在生产环境看到有人因为没有理解 delete 的提交行为几万条订单数据被删光后只能靠备份恢复那种无力感至今记得。其实“delete 会自动提交事务吗”这个问题没有一个放之四海皆准的答案。这不是打太极而是因为数据库的默认会话模式、你是否开启了显式事务、你用的 ORM 框架有没有给你包一层事务注解都会改变 delete 的行为。把这几个变量梳理清楚你就不会再犯“删了想反悔却来不及”的低级错误。1. 从一条“删了就撤不回”的 DELETE 说起先还原一个我在项目里真实遇到的场景。当时同事接到一个需求说要清理测试环境里某个业务表的脏数据他在 SQL Server 的查询窗口里执行了一条 delete看到“影响行数 36275”然后继续往下处理业务。过了几分钟他发现筛选条件写错了该保留的保留错、该删除的没删对他立刻执行 rollback结果查询窗口提示“没有活动事务”可以回滚。他当场就懵了。这个现象背后的原因非常简单也很容易被新手忽略在执行 delete 的单条语句时如果没有被包进某个显式事务里数据库会按“自动提交”的规则来执行这条语句。也就是说这条 delete 成功后就已经提交了后续 rollback 自然无从谈起。这里的“自动提交”并不是 delete 语句自带的能力而是当前会话的控制行为决定的这个控制开关在不同数据库里有不同的名字和默认值。另外提醒一句很多人看到“delete”这个单词还会联想到 C 里的new/delete []配对释放内存这完全是两码事。我们这里讨论的是数据库的数据操纵语言 DML也就是DELETE语句跟 C 的堆内存释放没有半点关系。把场景铺开之后就可以进入真正的问题核心了一条 delete 到底什么时候自动提交什么时候必须手动提交什么时候明明开了事务却依然没有回滚成功。2. 一条 DELETE 的事务边界到底由谁决定2.1 没有显式事务语句级“自动提交”的真面目以 MySQL 为例默认情况下autocommit的值为 1这意味着每一条 DML 语句都被当作一个独立的事务执行语句执行成功就自动提交执行失败就自动回滚。你执行delete from orders where id 1如果没有提前执行start transaction或begin这条 delete 就是“自给自足”的要么删掉并且永久生效要么报错并且什么都不发生。它很省心但也意味着你失去了后悔的余地。在 PostgreSQL 里也是一样的逻辑。没有显式BEGIN时每条语句都运行在一个隐式事务里语句结束即提交。这也是我之前遇到过的一个线上事故的根源有同事用小脚本逐条删除一边跑一边打印结果跑了一个多小时后发现规则有问题想停掉脚本再 rollback却发现单独拉起的新连接根本回滚不了这个脚本所在会话已经提交掉的操作。SQL Server 稍微有点绕。它也有隐式事务和显式事务两套概念但很多 DBA 习惯把没有写 BEGIN TRAN 的语句都直接称为“自动提交事务”。如果你在 SSMS 的新查询窗口里执行一条 delete没有写BEGIN TRAN这条语句默认也会在它自己的事务里完成成功即提交。有个记忆方法单个语句的自动提交本质是“这条语句自己和自己组了一个事务”而不是说 delete 这个动词自带提交权限。2.2 有显式事务delete 并不会提交事务当你在外面包了一层显式事务时情况就完全变了。比如在 SQL Server 里写BEGIN TRAN; DELETE FROM orders WHERE order_status CANCELLED;这条 delete 执行完之后数据其实还没有真正“定案”其他外部会话默认情况下也看不到未提交的删除结果取决于隔离级别。只有在执行COMMIT时删除才算生效执行ROLLBACK时删除就会完全撤销被删掉的行会恢复原样。MySQL 和 PostgreSQL 也一样只要用BEGIN/START TRANSACTION开启了一个事务那么这个事务里的所有 delete 都绑定在同一条 tx 内。你在事务里执行完 delete 后不提交哪怕再去启动一个连接去查也可能查不到该事务的执行结果直到事务被提交或回滚。这里有个很重要的实操建议凡是要做“先看效果、再把关”的敏感数据操作一定要养成先开事务的习惯。我一个老同事做数据订正有一条不成文的规定DELETE 和 UPDATE 这类高危操作执行前先BEGIN TRAN在执行完后先跑一条 COUNT 做对比验证确认无误再COMMIT一旦发现数量不对立刻ROLLBACK。这套习惯帮他避免了好几次凌晨被叫起来处理事故的窘境。2.3 一个不能忽略的特例DDL 里的隐式提交聊到这儿很多读者可能会想只要我包了事务就万事大吉了吗并不完全是。在不少数据库里你的事务中间一旦混入了 DDL 语句比如DROP TABLE、TRUNCATE TABLE、ALTER TABLE可能就会触发隐式提交导致事务里之前做的 delete 操作也跟着被一起提交掉。MySQL 的隐式提交尤其明显很多 DDL 语句都会让当前事务无法回滚。我印象很深的一次教训是同事在一个事务里先删了一批订单明细然后为了“回收表空间”顺手执行了TRUNCATE TABLE结果这条 DDL 触发了隐式提交前面的 delete 想回滚也回滚不了了。最后只能从备份里找回数据。所以这里必须明确delete 的事务边界并不能简单归结为“有事务就能回滚”还要看你有没有在事务里做了会隐式提交的操作尤其是 DDL 类语句。3. 事务日志被 DELETE 撑满一个真实排障现场3.1 看一条让 SQL Server 直接罢工的报错每个人大概都有过对着报错信息发呆的时刻。之前碰到过一个客户环境应用端疯狂报错数据库错误日志里不断出现下面这行信息数据库 ais20221123194008 的事务日志已满。若要了解无法分配日志空间的原因请参阅 sys.databases 中的 log_reuse_wait_desc 列。 消息 9002级别 17状态 2第 1 行这个报错的级别是 17意味着已经属于资源类错误。看到这个报错之后如果不赶紧处理数据库很快会停止一切需要写日志的操作后续的 delete、update 甚至一部分 insert 都会失败。事后查下来就是有人在事务里玩了一个“大动作”一次性 delete 了几百万行而且事务一直没提交导致事务日志只能不断增长、不能截断。3.2 为什么大 DELETE 会把日志撑爆很多刚接触数据库的人有一个误解觉得 delete 只是把行标记一下删掉应该不会产生太多日志。实际上为了保证事务可以回滚数据库必须把“删除前”的原始数据记录到事务日志里。也就是说你删掉的每一行数据在日志里至少要留一份“被删除之前的完整状态”事务提交前这些日志都无法释放。放大到一个超大事务里情况就更糟糕了你在一个显式事务里执行 delete所有被删除行的前镜像都堆积在日志文件里事务不结束日志缓冲区里的内容也一直不能被循环利用。如果日志文件的自动增长配置了固定上限或者磁盘空间不足它就会撞到上限出现上文那条 9002 报错。可以说这个大事务本身就是一个不断膨胀的定时炸弹。3.3 遇到日志满时的正确救援动作出现日志满报错时第一反应千万不要是无脑收缩日志文件。想收缩日志文件前提是先把日志里可以清除的历史部分清掉也就是把不再需要的日志备份出去或者先看能不能截断。我曾经见过有人一看到日志满了就执行DBCC SHRINKFILE结果长时间卡住因为没有先解决“日志为什么不能被截断”的根因。正确的排查顺序大概是这样的先查询数据库当前的日志重用等待原因SELECT name, log_reuse_wait_desc FROM sys.databases WHERE name ais20221123194008;如果log_reuse_wait_desc显示的是 ACTIVE_TRANSACTION就说明有长事务占着日志。找到这个事务对应的会话评估是等它跑完还是回滚。确认没有长事务后立刻做一次事务日志备份。只要备份完成日志中已备份的部分就可以被截断日志文件就有了可复用空间。最后才是根据业务增长趋势设置一个合理的日志文件大小和增长上限避免每次都是“意外撑爆”之后再救火。这个案例里最终定位到一个已经运行了两个多小时的删除事务。由于业务对这个事务没有硬性要求必须一次干完我直接让应用侧把它拆成了分批删除逐步提交日志空间瞬间就稳定下来了。3.4 避免日志炸弹分批删除的正确写法如果你要在生产环境清理一张大表最稳妥的方式不是一条 delete 把数百万行一起删掉而是用分批删除控制每个事务的大小。简单来说就是每一批只删除几千行每批结束后立刻提交让日志可以及时截断同时也减少了行锁长时间占用的问题。下面这个例子是 SQL Server 里常用的循环批次删除思路SET NOCOUNT ON; DECLARE BatchSize INT 5000; DECLARE DeletedRows INT 1; WHILE DeletedRows 0 BEGIN DELETE TOP (BatchSize) FROM orders WHERE create_time 2023-01-01; SET DeletedRows ROWCOUNT; COMMIT; END;这样每个批次的删除都在独立的小事务里完成即使中途出了故障损失也可以控制在当前批次范围内日志文件也不会像滚雪球一样越滚越大。同样的思路在 MySQL 里可以用LIMIT来实现在 PostgreSQL 里可以用ctid排序加分页来实现。这里有一个需要权衡的点如果需求要求“要么全部删除、要么一条不删”那就不能用分批提交必须保留一个大事务。两个方向各有代价需要在业务一致性面前做取舍。4. 在 Spring 里再聊 “delete 自动提交”很多 Java 开发同学对数据库事务的理解是从Transactional注解开始的。这时候“delete 会自动提交吗”这个问题会变成另一副面孔我在 Service 方法里直接调用了deleteByXxx事务到底有没有生效4.1 Transactional 改变了 delete 的默认提交行为先看一个最简单的代码Transactional(rollbackFor Exception.class) public void removeOrderAndStock(OrderDeleteDTO dto) { orderMapper.deleteByOrderId(dto.getOrderId()); stockMapper.deleteByOrderId(dto.getOrderId()); }当 Spring 容器带着Transactional进入这个方法时它会从数据源拿到一个连接主动关闭这个连接的自动提交模式也就是conn.setAutoCommit(false)。此时第一条 delete 执行完以后不会自动提交而是滞留在这个事务上下文里直到方法体全部执行完毕Spring 的事务管理器才会提交事务。如果方法内抛出了运行时异常并且事务配置允许回滚那么两条 delete 都会一起回滚数据保持一致。从这个角度看我们可以给面试题一个相对完整的答复在没有显式事务、ORM 也没有帮我们开启事务的情况下单条 delete 默认可能是自动提交的一旦进入了明确的事务边界比如手动begin/commit或者 Spring 的Transactionaldelete 就不会自动提交它只是在等待事务边界统一提交。4.2 四个让事务“失效”的常见坑实际项目里翻车最多的不是不懂自动提交而是以为自己写了Transactional就一定生效。我总结过四类高频踩坑点同类内部调用。在同一个类里一个方法调用另一个方法如果走的是this调用就没法让 Spring 代理去触发事务增强。正确做法是把需要事务控制的方法拆到另一个被 Spring 管理的 Bean 里或者通过AopContext.currentProxy()调用代理对象。异常被吞掉。事务回滚依赖异常向外抛出。如果我们在 catch 里把异常吃掉或者只 log 不抛出Spring 并不知道方法执行失败自然也就不会回滚。传播行为被设置成NOT_SUPPORTED或REQUIRES_NEW之后误用。NOT_SUPPORTED会把当前事务挂起让方法在无事务模式下执行delete 就又变回“跑完就自己提交”的状态。代理没生效。自己 new 出来的 Service 对象、没走 Spring 容器管理的对象、或者使用到了不支持动态代理的私有方法都会让Transactional形同虚设。4.3 怎么验证连接上的事务到底开没开如果你实在不确定当前上下文是“自动提交”还是“手动提交”最直接的办法就是拿到 JDBC 连接打印它的自动提交状态Connection connection dataSource.getConnection(); System.out.println(autocommit connection.getAutoCommit());在事务方法里执行时输出通常是autocommit false。如果某些场景下输出是true哪怕你在方法上写了Transactional也说明事务没被正确开启下一步就可以按上面四个坑逐一排查。还有更简单的办法开启底层 SQL 日志看事务管理器是否输出了“Setting autocommit to false”之类的日志。在 MyBatis 的日志配置中当你看到 execute 之前有JDBC Connection的事务参数变化也能辅助判断。5. 当 DELETE 变成分布式事务里的“双刃剑”5.1 订单删除与库存的连锁反应业务系统发展到一定规模之后订单和库存很可能已经不在同一个数据库里。比如订单服务负责订单表的删除库存服务负责库存表的扣减或释放。你在订单库里删掉了订单紧接着想同步在库存库里恢复库存。这两个动作如果还沉浸在“数据库 delete 自动提交”的原始认知里就会陷入非常尴尬的境地。举个最常见的例子用户取消订单。如果订单库里的订单删除成功而库存服务的库存恢复失败用户一看库存没回来立刻就会投诉。反过来如果库存先恢复成功订单删除却失败那系统里就会多出一个看不到的幽灵订单。这种场景不是单数据库事务能解决的因为事务边界默认只在同一个数据库内有效。5.2 为什么需要“事务消息”分布式环境下大家通常不会再去纠结某个 delete 是不是自动提交而是会转向“最终一致性”的解法。一个比较经典的手段是本地消息表加事务消息。基本思路是把“删除订单”的业务操作和“发送一条提醒库存服务的数据变更消息”放在同一个本地事务里。订单删除成功消息记录就写入本地消息表订单删除失败消息也不会发出。两个核心步骤可以这样理解订单服务执行 delete 后在同一事务中写入一条待发送消息消息状态为“待发送”批量任务定时扫描“待发送”消息把消息投递到 MQ库存服务消费后进行库存恢复。这套方案里的订单 delete 和消息写入在同一个数据库事务内它们之间保持着本地一致性。库存服务那边只需要保障消费接口的幂等性即便消息重复投递也不会让库存被重复恢复。最终订单删除和库存恢复会在某个时间点达成一致这也解释了为什么在分布式事务里前端的“自动提交”观念要让位于“业务状态机 事务消息 幂等补偿”的组合拳。5.3 幂等设计是分布式 DELETE 的安全网不管是订单删除还是库存恢复只要消息可能被重试就一定要做幂等控制。比如库存恢复接口可以记录一份操作流水以订单编号加操作类型作为唯一约束重复消息过来时直接返回成功避免把库存加两次。这也是我在实际项目里反复强调的一点分布式事务从来不是一个“神奇中间件”就能解决的事更重要的还是业务设计里对重复、乱序、超时的容忍能力。6. 我的几个实操心得每次聊到 delete 和事务的关系我最后都会分享几条自己真实踩过坑之后沉淀下来的经验。第一条生产环境高危操作前先开明确事务执行后先验证影响行数和数据快照再提交。这条适用于所有关系型数据库不管 SQL Server 还是 MySQL 都一样。第二条大范围 delete 尽量分批提交除非业务明确要求原子性。这是减少锁竞争、避免日志膨胀、降低主从复制延迟最有效的土办法没有之一。第三条在 Spring 项目里判断事务是否生效时不要只盯着注解看。先确认方法是不是被 Spring 代理再确认有没有捕获异常最后再看连接上 autocommit 的真实值三步走完基本不会冤枉“delete 不听话”。最后一个属于细节技巧只要涉及删除订单、删除用户这类偏核心的业务操作我都会想办法在代码里增加软删除标记或者操作日志表而不是物理删除。这样即便事务回滚没赶上数据至少还有一个可追溯的恢复入口。这个习惯帮我避免过不止一次线上灾难也希望你们不要等到出事之后才后悔。