长事务是Oracle数据库里最容易被忽视却又杀伤力极大的隐患之一。一个事务哪怕只是执行了一条很小的update语句,只要没有提交或回滚,它就会持续持有行级锁,并且阻止undo表空间的回收。时间一长,undo暴涨、其他会话被锁阻塞、ORA-01555快照过旧等问题就会接连出现。本文将从长事务的成因、监控方法、终止手段和预防措施四个方面展开,给出一套可以直接落地的处理方案。

长事务为什么危险:从undo和锁说起
理解长事务的危害,首先要明白Oracle的事务机制。当一个事务开始后,Oracle会在undo表空间里记录数据块修改前的镜像,用于回滚和一致性读。事务只要不结束,它所产生的undo数据就无法被回收复用。假如一个事务从早上开始一直运行到晚上,期间产生的所有undo都会被保留,即使是自动管理的undo表空间(undo_retention参数也拦不住),最终结果就是undo表空间被撑爆,或者频繁报ORA-01555错误。
第二个危害是锁竞争。事务未提交前,它修改过的所有行都会持有TX锁。其他会话想修改这些行就必须排队等待,表现为业务卡顿、会话大量挂起。可以通过下面的查询观察锁等待情况:
-- 查看锁等待链,找出阻塞源头
SELECT LPAD(' ', 2 * (LEVEL - 1)) || s.sid blocked_session,
s.serial#,
s.username,
s.machine,
s.event,
s.seconds_in_wait
FROM v$session s
WHERE s.blocking_session IS NOT NULL
START WITH s.blocking_session IS NULL
CONNECT BY PRIOR s.sid = s.blocking_session;
如果某个会话被大量会话层层等待,那么它就是阻塞源头,大概率是一个长事务在作怪。常见的长事务来源包括:应用代码里忘了写commit、大批量DML没有分批提交、开发人员在工具里手动改数据后离开工位、还有定时任务中一条超大事务的处理逻辑。
如何监控长事务:核心SQL与视图
监控长事务的核心视图是v$transaction,它记录了当前所有活动事务的信息。其中start_time是事务开始时间,used_ublk是事务占用的undo块数,used_urec是undo记录数。把它和v$session关联,就能拿到完整的会话信息。下面是日常排查最常用的一条SQL:
-- 查询运行超过10分钟的长事务及其会话信息
SELECT s.sid,
s.serial#,
s.username,
s.osuser,
s.machine,
s.program,
s.status,
t.start_time,
ROUND((SYSDATE - TO_DATE(t.start_time, 'MM/DD/YY HH24:MI:SS')) * 24 * 60, 1) AS run_minutes,
t.used_ublk * 8 / 1024 AS undo_mb,
t.used_urec
FROM v$transaction t
JOIN v$session s ON t.ses_addr = s.saddr
WHERE (SYSDATE - TO_DATE(t.start_time, 'MM/DD/YY HH24:MI:SS')) * 24 * 60 > 10
ORDER BY run_minutes DESC;
这条语句能直接看出每个长事务运行了多少分钟、占用了多少MB的undo、来自哪台机器哪个程序。used_ublk乘以8是因为默认块大小是8KB,如果你的数据库块大小不是8KB,需要相应调整。如果发现program是plsqldev、sqlplus这类工具且username是某个开发人员账号,基本可以判断是人工操作后忘记提交。
除了实时查询,建议部署定时监控脚本,每隔几分钟扫描一次v$transaction,发现超过阈值的事务就记录日志或告警。也可以结合v$session_longops观察长时间运行的操作,但要注意它只记录走了并行或全表扫描等统计口径的操作,不能替代v$transaction。对于被锁阻塞的会话,还可以查询dba_dml_locks或v$lock来确认阻塞的具体对象。
如何终止长事务:杀会话的几种方式与风险控制
确认某个长事务必须终止后,最常用的方式是使用ALTER SYSTEM KILL SESSION语句。在单机环境下执行:
-- 杀掉sid为1023、serial#为45678的会话 ALTER SYSTEM KILL SESSION '1023,45678' IMMEDIATE;
执行这条命令后,会话会被标记为killed状态,事务开始回滚。这里有个非常关键的知识点:杀会话不等于立即结束,回滚还需要时间,而且回滚期间undo依然被占用,相关的锁也依然存在。很多新手看到会话状态变成KILLED就以为万事大吉,结果等了半天锁还在,就是因为回滚没完成。估算回滚时间可以观察used_urec的下降速度,每秒采样一次,用两次的差值推算剩余时间。
如果会话状态是KILLED但迟迟不回滚结束,或者进程已经僵死,就需要在操作系统层面杀进程。先查出对应的操作系统进程号,再执行kill:
-- 查询会话对应的操作系统进程 SELECT s.sid, s.serial#, p.spid, s.program FROM v$session s JOIN v$process p ON s.paddr = p.addr WHERE s.sid = 1023;
拿到spid后在数据库服务器上执行kill -9 进程号(Windows环境下使用orakill命令)。操作系统层面杀进程后,PMON进程会接管并完成实例级的恢复和回滚。要注意绝对不能误杀后台进程,所以执行前务必确认spid对应的进程是oracle的server process而不是smon、pmon等核心进程。
RAC环境下情况更复杂一些,杀会话时需要指定实例号,或者使用ALTER SYSTEM DISCONNECT SESSION直接断开连接。disconnect会立即终止客户端连接,事务同样会回滚,适合处理那些客户端已经断线但事务还挂着的僵尸会话。无论用哪种方式,操作前都应该做好三件事:确认事务来源避免误杀核心业务、通知相关责任人、记录操作时间和会话信息以备事后追溯。
如何预防长事务:从根源减少风险
事后处理永远不如事前预防。首先在应用层面,大批量DML一定要分批提交,比如每处理一万行就commit一次,避免单个事务累积海量undo。可以结合rownum或rowid分片来控制每批的数据量。其次,检查应用代码中事务边界的设计,确认没有在循环里累积事务、没有在事务中间调用外部接口导致长时间等待。
在数据库层面,可以通过profile限制会话的资源消耗,或者开启resumable session让undo不足时会话暂停而不是直接失败。另外要合理设置undo表空间大小,为监控脚本设定告警阈值,比如undo使用率超过80%或事务运行超过30分钟就触发告警。对于开发测试环境,可以设置idle_time限制空闲会话,避免开发人员在工具里开了事务就去吃饭的情况。
最后建议把长事务监控纳入日常巡检脚本,形成固定机制。监控的意义不只是出问题时排查,更是通过持续观察发现应用设计上的缺陷。一个健康的系统里,长事务应该是极少数的异常事件,如果监控发现长事务天天出现,那就要认真审视应用的事务设计是否合理了。掌握监控加终止加预防这套组合拳,长事务就不再是让人头疼的难题。