1. 从一次“卡死”的排查说起:Oracle 锁到底在锁什么
很多人第一次真正意识到 Oracle 锁的存在,不是因为读了文档,而是因为某个下午业务系统突然“卡死”了。一个原本几百毫秒的更新语句,跑了十几分钟还没返回;前端页面转圈转到用户开始打电话;DBA 群里有人甩出一句“是不是有锁”。这时候你去查v$lock、v$session,看到一堆BLOCKED状态的会话,才明白锁不是书本上的概念,而是真能让你加班到深夜的东西。
Oracle 的锁机制,本质上是数据库为了保证数据一致性和并发可控而设计的一套协调机制。多个会话同时读写同一份数据时,如果没有锁,就会出现脏读、丢失更新、幻读等问题。Oracle 的做法和很多数据库不太一样:它尽量做到“读不阻塞写、写不阻塞读”,这背后靠的是undo 段 + SCN(系统改变号)构建的一致性读(Consistent Read)能力。也就是说,普通查询根本不需要加锁去等别人,它读的是数据在某个时间点的快照。
但写操作就不一样了。两个会话要改同一行,必须排队,这就产生了行级锁(TX 锁)。Oracle 的行级锁不是锁在数据块上,而是锁在数据行的事务槽(ITL,Interested Transaction List)里,通过事务槽指向 undo 段来标记“这行正在被某个事务修改”。这个设计非常巧妙:锁信息不占用额外的大块空间,而是和数据行本身绑定在一起,锁的粒度和事务的粒度天然对齐。
理解这一点很关键,因为它解释了为什么 Oracle 里“锁”往往不是问题,“阻塞”才是问题。锁是正常的、必要的,一个健康的系统每秒都在加锁释放锁;真正让人头疼的是某个事务拿了锁迟迟不放手,导致后面的会话全部排队。所以这篇文章不会只给你罗列锁的类型,而是从“锁是怎么产生的、怎么找到它、怎么判断它是不是问题、怎么处理”这条实战链路讲清楚。
适合读这篇内容的人:写过 Oracle SQL 的后端开发、需要处理线上阻塞问题的运维或 DBA、以及面试前想系统梳理锁机制的同学。我会尽量用生活化的类比把原理讲透,同时给出可以直接复制执行的排查 SQL,让你下次遇到“卡死”时不再只会重启实例。
2. Oracle 锁的分类:别被 DML、DDL、TX、TM 这些缩写绕晕
2.1 按操作类型分:DML 锁、DDL 锁、内部锁
Oracle 的锁如果按“谁触发的”来分,大致有三类。
第一类是DML 锁,也就是你执行INSERT、UPDATE、DELETE、MERGE时产生的锁。它又细分为行级锁(TX)和表级锁(TM)。行级锁锁住你改的那几行,表级锁则是在表上放一个“意图锁”,告诉别人“这张表里有人正在改数据,你别做会冲突的结构操作”。
第二类是DDL 锁,比如CREATE、ALTER、DROP、TRUNCATE。DDL 锁的级别通常更高,因为它要改的是对象结构,必须保证没有人在读写。你执行ALTER TABLE ... ADD COLUMN时如果一直卡住,多半是有人在这张表上有未提交的事务,DDL 在等一个排他锁。
第三类是内部锁(Internal Lock),比如闩锁(Latch)、互斥锁(Mutex)。这些是 Oracle 内部保护内存结构的机制,普通开发者一般接触不到,但它们是“latch free”等待事件的来源。闩锁的特点是持有时间极短,通常以微秒计,如果看到大量 latch 等待,往往是 CPU 争用或热点块问题,而不是业务逻辑锁。
用一个类比:DML 锁像是图书馆里借书,你借走一本,别人就不能同时借同一本;DDL 锁像是要改造整个书架,必须等所有人都把书还回来;内部锁则像是管理员整理卡片抽屉的那一瞬间,动作极快,但抽屉只有一个,谁都想用就会排队。
2.2 按锁模式分:共享与排他,以及六种表级锁
Oracle 的锁模式核心就两种:共享(S)和排他(X)。共享锁之间兼容,排他锁和任何锁都不兼容。在此基础上,表级锁(TM)细分为六种模式,这个在面试和排查中经常出现:
| 锁模式 | 缩写 | 典型触发场景 | 兼容性特点 |
|---|---|---|---|
| 行共享 | RS | SELECT ... FOR UPDATE | 与 RS、RX、S 兼容 |
| 行排他 | RX | INSERT/UPDATE/DELETE | 与 RS、RX 兼容,与 S、SRX、X 冲突 |
| 共享 | S | LOCK TABLE ... IN SHARE MODE | 与 RS、RX、S 兼容 |
| 共享行排他 | SRX | LOCK TABLE ... IN SHARE ROW EXCLUSIVE MODE | 仅与 RS 兼容 |
| 排他 | X | LOCK TABLE ... IN EXCLUSIVE MODE | 与所有锁冲突 |
| 行共享排他 | SRX | 部分 DDL 场景 | 兼容性最差之一 |
实际开发中最常见的是 RX 锁。你执行一条UPDATE,Oracle 会在表上加 RX 锁,在行上加 TX 锁。RX 和 RX 是兼容的,所以两个人可以同时改同一张表的不同行,互不影响。但如果有人要ALTER TABLE,就需要 S 或 X 锁,这时就会和 RX 冲突,DDL 只能等待。
这里有个容易踩的坑:很多人以为“我改的是不同行就不会锁”,但如果你在事务里先UPDATE了 A 行,又去UPDATEB 行,而另一个会话先锁了 B 行再锁 A 行,就会形成死锁(Deadlock)。Oracle 的死锁检测机制会在几秒内发现并回滚其中一个会话,报ORA-00060。死锁不是“锁等待”,它是循环等待,Oracle 必须牺牲一个事务来打破循环。
2.3 锁的粒度:为什么 Oracle 没有“锁升级”
SQL Server 有锁升级的概念,行锁太多时会升级成表锁。Oracle 没有锁升级,因为它的行锁不占用独立的内存结构,而是存在数据块的 ITL 槽里。一个数据块能容纳的事务槽数量有限(受INITRANS和MAXTRANS影响),但 Oracle 通过动态分配和事务槽复用,避免了“行锁太多要升级”的问题。
这个设计带来的好处是:即使你更新一百万行,也不会突然把整张表锁死。但坏处是,如果并发事务集中在少数几个数据块上,就会出现热点块(Hot Block),表现为buffer busy waits或gc buffer busy等待。这时候问题不在行锁本身,而在块级别的争用。
所以排查锁问题时,不能只看v$lock,还要结合v$session_wait看等待事件。如果等待事件是enq: TX - row lock contention,那是行锁争用;如果是buffer busy waits,那是块争用;如果是enq: TM - contention,那是表级锁冲突,通常和 DDL 或外键有关。
3. 锁等待的排查链路:从“卡住”到定位到具体会话
3.1 第一步:确认是不是锁等待,而不是慢 SQL
业务反馈“卡死”时,先别急着查锁。很多时候只是 SQL 写得烂,全表扫描加排序,跑得慢而已。区分方法很简单:看会话的等待事件。如果等待事件是db file scattered read、db file sequential read,那是 I/O 问题;如果是enq: TX - row lock contention,那才是锁等待。
查当前会话等待事件的 SQL:
SELECT s.sid, s.serial#, s.username, s.status, s.event, s.seconds_in_wait, s.sql_id FROM v$session s WHERE s.status = 'ACTIVE' AND s.event LIKE 'enq%' ORDER BY s.seconds_in_wait DESC;如果event列出现enq: TX - row lock contention,基本可以确定是行锁等待。seconds_in_wait越大,说明等得越久。这时候你需要找到“谁持有锁”,而不是只看“谁在等”。
3.2 第二步:用 v$lock 和 dba_blockers 找到阻塞源头
Oracle 提供了一个非常方便的视图dba_blockers,它直接列出当前正在阻塞别人的会话 SID。查询:
SELECT blocking_session, sid, serial#, username, event, seconds_in_wait FROM v$session WHERE blocking_session IS NOT NULL;blocking_session就是阻塞者的 SID。你可以顺着这个 SID 去查它正在执行什么 SQL:
SELECT s.sid, s.serial#, s.username, s.sql_id, q.sql_text FROM v$session s JOIN v$sql q ON s.sql_id = q.sql_id WHERE s.sid = &blocking_sid;但要注意,v$session.sql_id可能为空,因为阻塞者可能处于INACTIVE状态——它执行完 SQL 但没提交事务。这时候你要查它最近执行的 SQL:
SELECT sql_id, sql_text FROM v$sql WHERE last_active_time > SYSDATE - 1/24 AND parsing_schema_name = '&username' ORDER BY last_active_time DESC;更直接的方式是查v$transaction,因为锁的本质是事务没结束:
SELECT t.addr, t.sid, t.status, t.start_time, t.used_ublk, t.used_urec FROM v$transaction t WHERE t.sid = &blocking_sid;used_ublk是占用的 undo 块数,used_urec是 undo 记录数。如果这两个值很大,说明这个事务改了很多数据但一直没提交,风险很高。
3.3 第三步:判断阻塞者是在“干活”还是“挂起”
找到阻塞者后,别急着 kill。先看它的状态。如果它是ACTIVE,说明它正在执行 SQL,可能只是 SQL 慢;如果它是INACTIVE,说明它没在执行任何语句,但事务没提交——这通常意味着应用代码有问题,比如忘了 commit,或者连接池里的连接被借走后一直没归还。
我遇到过最典型的情况:某个批处理任务在循环里逐条 update,每改一条就commit一次,但中间有一段逻辑抛了异常,异常被捕获后没有回滚也没有提交,连接被放回池里,事务一直挂着。后面所有改同一行的请求全部排队。这种问题从数据库层面看就是“一个 INACTIVE 会话持有 TX 锁”,但根因在应用代码。
判断阻塞者是否“挂起”的另一个方法是看它的last_call_et:
SELECT sid, serial#, status, last_call_et, event, sql_id FROM v$session WHERE sid = &blocking_sid;last_call_et是会话最后一次调用至今的秒数。如果 status 是 INACTIVE 且last_call_et很大(比如几千秒),基本可以确定它挂在那里很久了。
3.4 第四步:处理阻塞——kill session 还是 kill transaction
确认阻塞者确实有问题后,处理方式有两种:ALTER SYSTEM KILL SESSION和ALTER SYSTEM DISCONNECT SESSION。前者是“标记杀死”,会话不会立即消失,要等它自己检测到;后者是“立即断开”,更暴力。
-- 标记杀死,等待 PMON 清理 ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE; -- 立即断开连接 ALTER SYSTEM DISCONNECT SESSION 'sid,serial#' IMMEDIATE;但这里有个重要经验:kill session 不一定能立即释放锁。如果会话正在执行一个大的 DML,或者处于回滚过程中,kill 之后它还要回滚未提交的事务,回滚可能比原来的操作还慢。这时候你会看到会话状态变成KILLED,但锁还在。正确的做法是先确认它的事务有多大(看v$transaction.used_ublk),如果 undo 量巨大,kill 之后要有心理准备等它回滚完。
另一个经验是:优先 kill 阻塞者,而不是等待者。有些新手看到一堆等待会话,就把等待的会话全 kill 了,结果阻塞者还在,新的请求继续排队。治标不治本。
4. 不同场景下的锁问题:行锁、表锁、死锁、DDL 锁
4.1 行锁争用:最常见的“锁等待”
行锁争用的典型等待事件是enq: TX - row lock contention。它通常发生在高并发更新同一行数据的场景,比如秒杀库存扣减、账户余额更新、订单状态流转。
排查这类问题时,除了找到阻塞者,还要看热点行是哪一行。Oracle 没有直接给出“哪一行被锁”的视图,但可以通过v$session的row_wait_obj#、row_wait_file#、row_wait_block#、row_wait_row#定位:
SELECT sid, row_wait_obj#, row_wait_file#, row_wait_block#, row_wait_row# FROM v$session WHERE event = 'enq: TX - row lock contention';拿到row_wait_obj#后,可以查dba_objects找到表名:
SELECT owner, object_name, object_type FROM dba_objects WHERE object_id = &row_wait_obj#;但要注意,row_wait_row#是行在块内的相对位置,不是主键值。要找到具体是哪一行,需要结合dbms_rowid包:
SELECT dbms_rowid.rowid_create(1, &obj#, &file#, &block#, &row#) AS rowid FROM dual;然后用这个 rowid 去查表:
SELECT * FROM &table_name WHERE rowid = '&rowid';这套流程在排查“到底哪条记录被锁”时非常有用。我曾在一次库存扣减问题中,通过这个方法定位到某几个 SKU 的行被反复争用,后来改成用 Redis 预扣减 + 异步落库,锁争用直接消失。
4.2 表锁冲突:TM 锁和 DDL 的纠缠
表级锁冲突的等待事件是enq: TM - contention。它通常发生在两种场景:一是 DDL 操作等待 DML 事务提交;二是外键约束导致的表锁。
先说 DDL。你执行ALTER TABLE orders ADD COLUMN remark VARCHAR2(200),如果orders表上有未提交的 DML,DDL 会等待。因为加列需要表级排他锁,而 DML 持有 RX 锁,两者冲突。这时候查v$lock会看到 DDL 会话在等 TM 锁,而 DML 会话持有 TM 锁。
处理方式:找到持有 TM 锁的会话,让它提交或回滚。如果找不到,可以用v$locked_object:
SELECT l.object_id, o.object_name, l.session_id, l.oracle_username, l.locked_mode FROM v$locked_object l JOIN dba_objects o ON l.object_id = o.object_id;locked_mode的值对应锁模式:3 是 RX,6 是 X,4 是 S。如果看到某个会话对目标表持有 mode 3,而 DDL 在等 mode 6,那就是它了。
再说外键。如果子表有外键指向父表,更新父表的主键时,Oracle 会在子表上加锁,防止子表插入不存在的父键。这种锁有时候会出乎意料地阻塞其他操作。解决办法是给外键列建索引,减少锁的范围。这个经验很实用:外键列没索引,不仅查询慢,锁的范围也大。
4.3 死锁:ORA-00060 的完整解读
死锁的报错是ORA-00060: deadlock detected while waiting for resource。Oracle 检测到死锁后,会回滚其中一个事务,并在告警日志里生成一个 trace 文件。trace 文件里会详细列出两个会话各自持有和等待的锁。
死锁的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。Oracle 能打破的是循环等待,通过回滚一个事务来释放锁。但死锁本身是应用逻辑问题,数据库只能兜底。
常见的死锁场景:
- 两个会话以不同顺序更新同一组行。比如会话 A 先更新 id=1 再更新 id=2,会话 B 先更新 id=2 再更新 id=1。
- 一个会话在更新父表,另一个在更新子表,外键导致锁顺序交叉。
- 位图索引(Bitmap Index)上的并发 DML。位图索引的锁粒度是块级,两个会话改不同行但同一块,也会死锁。
避免死锁的经验:统一访问顺序。如果所有业务代码都按主键升序更新,死锁概率会大幅降低。另外,尽量缩短事务,不要在事务里做远程调用或等待用户输入。
4.4 DDL 锁:为什么加个字段把系统搞挂了
DDL 锁的问题在于它往往“看起来无害”。开发觉得加个字段而已,几秒钟的事。但如果表很大,或者有长事务,DDL 可能等很久,而它等待时会阻塞后续的 DDL 和部分 DML。
更麻烦的是,某些 DDL 操作在等待时会持有“队列锁”,导致后面的会话即使不冲突也要排队。这就像超市只有一个收银台,前面的人卡住了,后面所有人都动不了。
处理 DDL 锁的经验:
- 尽量在业务低峰期做 DDL。
- 使用
ONLINE选项(如CREATE INDEX ... ONLINE),减少锁范围。 - 设置
DDL_LOCK_TIMEOUT,让 DDL 等待超时后自动失败,而不是无限等待:
ALTER SESSION SET ddl_lock_timeout = 10;这样 DDL 等 10 秒拿不到锁就报错,不会一直挂着阻塞别人。
5. 锁相关的动态性能视图与实用脚本
5.1 v$lock、v$locked_object、v$transaction 的分工
这三个视图是排查锁问题的核心,但用途不同:
| 视图 | 主要用途 | 关键字段 |
|---|---|---|
| v$lock | 查看当前所有锁和等待 | TYPE、ID1、ID2、LMODE、REQUEST |
| v$locked_object | 查看被锁的对象 | OBJECT_ID、SESSION_ID、LOCKED_MODE |
| v$transaction | 查看活跃事务 | SID、STATUS、START_TIME、USED_UBLK |
v$lock里的TYPE字段很关键:TX是事务锁(行锁),TM是表锁,UL是用户锁。LMODE是持有模式,REQUEST是请求模式。如果REQUEST > 0且LMODE = 0,说明这个会话在等待锁。
一个经典的锁等待查询:
SELECT l1.sid AS waiting_sid, l1.type, l1.id1, l1.id2, l1.lmode AS waiting_lmode, l1.request AS waiting_request, l2.sid AS blocking_sid, l2.lmode AS blocking_lmode FROM v$lock l1 JOIN v$lock l2 ON l1.id1 = l2.id1 AND l1.id2 = l2.id2 AND l1.type = l2.type WHERE l1.request > 0 AND l2.lmode > 0 AND l1.sid != l2.sid;这个查询能直接告诉你“谁在等谁”。
5.2 一个完整的锁排查脚本
把前面的步骤串起来,我平时用的排查脚本大致是这样:
-- 1. 找阻塞者 SELECT blocking_session AS blocker, sid AS waiter, serial#, username, event, seconds_in_wait, sql_id FROM v$session WHERE blocking_session IS NOT NULL ORDER BY seconds_in_wait DESC; -- 2. 查阻塞者在干什么 SELECT s.sid, s.serial#, s.status, s.last_call_et, s.sql_id, q.sql_text FROM v$session s LEFT JOIN v$sql q ON s.sql_id = q.sql_id WHERE s.sid IN (SELECT blocking_session FROM v$session WHERE blocking_session IS NOT NULL); -- 3. 查阻塞者的事务 SELECT t.sid, t.status, t.start_time, t.used_ublk, t.used_urec FROM v$transaction t WHERE t.sid IN (SELECT blocking_session FROM v$session WHERE blocking_session IS NOT NULL); -- 4. 查被锁对象 SELECT l.object_id, o.object_name, l.session_id, l.locked_mode FROM v$locked_object l JOIN dba_objects o ON l.object_id = o.object_id;这套脚本在大多数锁等待场景下都能快速定位。注意v$session的blocking_session在 10g 之后才有,如果是更老的版本,需要用v$lock自己关联。
5.3 用 ASH 报告看历史锁等待
如果问题已经过去,当前没有锁等待,但你想知道“刚才发生了什么”,可以用 AWR 或 ASH 报告。ASH(Active Session History)每秒采样一次活跃会话,能还原历史等待。
查 ASH 的 SQL:
SELECT session_id, session_serial#, event, COUNT(*) AS samples, MIN(sample_time) AS first_seen, MAX(sample_time) AS last_seen FROM v$active_session_history WHERE sample_time BETWEEN TO_DATE('2024-01-01 14:00', 'YYYY-MM-DD HH24:MI') AND TO_DATE('2024-01-01 15:00', 'YYYY-MM-DD HH24:MI') AND event LIKE 'enq: TX%' GROUP BY session_id, session_serial#, event ORDER BY samples DESC;这个查询能告诉你哪个会话在哪个时间段大量等待 TX 锁。结合dba_hist_active_sess_history可以查更久远的历史。
6. 应用层如何减少锁争用:从代码到架构
6.1 事务尽量短,提交尽量快
这是最有效的一条。锁的持有时间和事务长度成正比。一个事务里如果包含远程调用、文件 IO、用户交互,锁就会被长时间持有。我见过最夸张的案例:一个事务里调用了第三方支付接口,超时 30 秒,导致数据库行锁被持有 30 秒,后面所有请求全部排队。
正确做法:把非数据库操作移到事务外。先做数据库更新并提交,再调远程接口;或者用异步消息解耦。如果业务必须保证一致性,考虑用补偿事务(Saga 模式)而不是长事务。
6.2 统一更新顺序,避免死锁
前面提过,死锁往往是因为更新顺序不一致。解决办法是在应用层约定:所有涉及多行更新的操作,都按主键升序执行。比如批量更新订单,先ORDER BY order_id再逐条更新。这样两个并发事务的加锁顺序一致,就不会形成循环等待。
另一个技巧是用SELECT ... FOR UPDATE提前锁定所有需要的行,并且按固定顺序。但要注意FOR UPDATE本身也会加锁,如果锁的范围太大,反而增加争用。
6.3 热点行优化:从行锁到无锁
秒杀场景下,单行库存扣减是典型的锁争用点。优化思路有几种:
- 拆分热点:把一行库存拆成多行,比如 100 个库存拆成 10 行每行 10 个,请求分散到不同行,减少单行争用。
- 异步化:用消息队列缓冲请求,数据库只做最终扣减,前端快速返回“排队中”。
- 乐观锁:用版本号或
WHERE stock >= 1的条件更新,避免SELECT FOR UPDATE的悲观锁。但乐观锁在高并发下失败率高,需要重试机制。 - Redis 预扣减:在缓存层做原子扣减,数据库只做异步落库。这个方案需要处理缓存和数据库的一致性,适合对一致性要求不是强实时的场景。
6.4 索引与外键:容易被忽略的锁放大器
外键列没索引,更新父表时会锁子表更多行。因为 Oracle 需要扫描子表来检查引用完整性,没有索引就全表扫,锁的范围自然大。给外键列建索引,不仅提升查询性能,还能减少锁冲突。
同样,更新语句的WHERE条件如果没有索引,会导致全表扫描并锁住扫描到的所有行。虽然 Oracle 的行锁只锁实际更新的行,但扫描过程中可能产生块级争用。所以UPDATE语句一定要走索引。
7. 几个真实场景的锁问题复盘
7.1 批量任务导致的 TX 锁堆积
某次夜间批处理,一个存储过程在循环里更新订单表,每 1000 条提交一次。但循环中间有一个EXCEPTION WHEN OTHERS THEN NULL,把异常吞掉了。结果某次更新违反约束,异常被吞,事务没提交也没回滚,继续往下跑。后面的更新全部在同一个事务里,undo 越来越大,锁也一直不释放。等到白天业务高峰,所有改订单的请求全部排队。
复盘结论:异常处理不能吞掉异常后不处理事务。要么ROLLBACK,要么COMMIT,不能什么都不做。另外,批处理任务要有监控,事务超过一定时间要告警。
7.2 连接池配置不当引发的“幽灵锁”
某系统用连接池,最大连接数 50。一个请求从池里拿到连接后,执行了一条UPDATE,然后因为业务逻辑判断,直接return了,没有 commit 也没有 close。连接被池回收后,事务还挂在数据库上。后续请求拿到这个连接,继续在同一个事务里操作,锁越积越多。
这种问题的排查难点在于:从数据库看,持有锁的会话是 INACTIVE 的,last_call_et很大,但sql_id为空。你需要结合应用日志和连接池监控才能定位。
解决办法:连接池配置autoCommit=false时,必须在 finally 块里显式 commit 或 rollback。或者用 Spring 的@Transactional注解,让框架管理事务边界。
7.3 DDL 等待引发的连锁阻塞
某次上线,开发要加一个字段,执行ALTER TABLE时卡住了。原因是有一个长事务在查这张表。更糟的是,这个 DDL 等待时持有队列锁,导致后面另一个ALTER TABLE也在等,而第二个 DDL 又阻塞了后续的 DML。整个系统像多米诺骨牌一样倒下去。
复盘结论:DDL 前先检查有没有长事务和锁等待。可以用ddl_lock_timeout设置超时,避免无限等待。另外,DDL 尽量用ONLINE选项,减少锁范围。
8. 锁监控与预防:把问题挡在发生之前
8.1 建立锁等待的日常监控
不要等业务反馈才去查锁。可以写一个定时任务,每隔几分钟检查一次锁等待,超过阈值就告警:
SELECT COUNT(*) AS lock_wait_count FROM v$session WHERE blocking_session IS NOT NULL AND seconds_in_wait > 30;如果lock_wait_count > 0,说明有会话等待超过 30 秒,值得关注。可以进一步查是哪个用户、哪张表、哪条 SQL。
8.2 设置合理的等待超时
Oracle 允许设置DISTRIBUTED_LOCK_TIMEOUT和DDL_LOCK_TIMEOUT。对于 DDL,建议设置ddl_lock_timeout,避免无限等待。对于分布式事务,distributed_lock_timeout默认 60 秒,可以根据业务调整。
另外,应用层可以设置语句超时。比如 JDBC 的setQueryTimeout,让长时间等待的 SQL 自动失败,而不是一直挂着。
8.3 定期检查长事务和未提交事务
长事务是锁问题的温床。可以定期查v$transaction,找出运行时间超过阈值的事务:
SELECT s.sid, s.serial#, s.username, s.program, t.start_time, t.used_ublk, t.used_urec, ROUND((SYSDATE - t.start_time) * 24 * 60, 2) AS minutes FROM v$transaction t JOIN v$session s ON t.sid = s.sid WHERE (SYSDATE - t.start_time) * 24 * 60 > 10 ORDER BY minutes DESC;这个查询能找出运行超过 10 分钟的事务。如果发现某个事务长时间不提交,就要去查应用代码,看是不是漏了 commit 或者逻辑卡住了。
8.4 锁问题的应急处理清单
最后给一个应急处理清单,遇到锁问题时按顺序执行:
- 确认是锁等待,不是慢 SQL。查
v$session.event。 - 找到阻塞者。查
v$session.blocking_session。 - 看阻塞者在干什么。查
v$session.status、last_call_et、sql_id。 - 看阻塞者的事务有多大。查
v$transaction.used_ublk。 - 如果阻塞者确实有问题,优先
KILL SESSION,但注意回滚时间。 - 如果阻塞者是应用连接池的“幽灵连接”,需要从应用层排查。
- 事后复盘,看是代码问题、配置问题还是设计问题。
这套流程我在多次线上问题中都用过,基本能在几分钟内定位到根因。锁问题不可怕,可怕的是不知道从哪里下手。希望这篇内容能让你下次遇到“卡死”时,心里有底,手上有招。