等待事件是 Oracle 性能诊断的"症状表",但症状不等于病因。很多 DBA 看到 buffer busy waits 就加 DBWR,看到 latch free 就加 CPU,结果南辕北辙。这篇文章把等待事件的分类、Buffer Busy Waits 的四种场景、Latch 和 Mutex 的区别、以及一个我通过等待事件定位到热块的案例拆清楚。
1 等待事件不是敌人,是诊断线索
Oracle 的等待事件分三类:Idle(空闲等待,比如 SQL*Net message from client,可以忽略)、System I/O(系统 I/O,比如 db file parallel write)、User I/O(用户 I/O,比如 db file sequential read)、Concurrency(并发等待,比如 latch free、buffer busy waits)、Application(应用等待,比如 enq: TX - row lock contention)。AWR 里的 Top 5 Timed Events,重点看 Concurrency 和 Application 类,因为它们是瓶颈;User I/O 高可能是 SQL 写得烂,也可能是存储慢。
2 Buffer Busy Waits:热块的四种面孔
Buffer Busy Waits 发生在多个会话同时访问同一个数据块,且该块不在内存中时。注意:它不是块在内存里的锁争用(那是 cache buffers chains latch),而是块正在被读入内存(或者正在被修改,其他会话要等)。四种常见场景:
场景一:数据块热块。大量会话同时 INSERT 到同一个数据块的末尾(高并发插入,同一个表的最后一个块)。解决办法:用 ASSM(自动段空间管理)替代 MSSM(手动段空间管理),ASSM 用位图管理空闲空间,多个会话可以插入到不同块。
场景二:索引块热块。大量会话同时 INSERT,索引的右叶块(最大键值)成为热点。解决办法:反向键索引(REVERSE)或哈希分区索引。
场景三:UNDO 头块热块。大量并发事务同时需要读 UNDO 头(事务表),导致热块。解决办法:增加 UNDO 表空间的回滚段数量(通常自动管理,但可调整)。
场景四:数据文件头热块。大量会话同时扩展段(allocate extent),要读数据文件头。解决办法:预分配 extent(ALLOCATE EXTENT),或者用大 uniform size 的表空间。
3 Latch Free vs Mutex:轻量级锁的两代产品
Latch 是 Oracle 用来保护 SGA 共享数据结构(如 hash bucket、LRU 链)的轻量级锁。Latch 的获取是 spin(自旋)+ sleep(休眠):进程先 spin 几千次(消耗 CPU),如果还拿不到,就 sleep 几毫秒再试。所以 latch 争用会导致 CPU 飙升。常见 latch:cache buffers chains(保护 buffer cache hash 链)、library cache(保护 SQL 解析结果)、shared pool(保护内存分配)、redo allocation(保护 redo log buffer)。
Mutex 是 10g 以后引入的,比 Latch 更轻量,用来保护 Library Cache 里的 cursor。Mutex 的获取也是 spin,但开销更小。V$LATCH 看 latch,V$MUTEX_SLEEP 看 mutex。
4 实验:模拟热块与 Latch 争用
实验环境:Oracle 19c,ASSM 表空间。测试表 hot_block_test,高并发插入。
-- 建表,用 MSSM 更容易制造热块(测试用)
CREATE TABLE hot_block_test (
id NUMBER,
val VARCHAR2(100)
) TABLESPACE users; -- 假设 users 是 MSSM
-- 高并发插入(用多个会话同时执行)
INSERT INTO hot_block_test VALUES (seq.NEXTVAL, 'TEST');
-- 监控 Buffer Busy Waits
SELECT event, p1 (file#), p2 (block#), p3 (class#), count(*)
FROM v$session_wait
WHERE event = 'buffer busy waits'
GROUP BY event, p1, p2, p3;
-- 确认热块对象
SELECT owner, segment_name, segment_type
FROM dba_extents
WHERE file_id = &p1 AND &p2 BETWEEN block_id AND block_id + blocks - 1;
-- 监控 Latch 争用
SELECT name, gets, misses, round(misses/gets*100,2) miss_pct, spin_gets, sleep1
FROM v$latch
WHERE name LIKE 'cache buffers chains'
ORDER BY miss_pct DESC;
-- 改用 ASSM 后对比
CREATE TABLE hot_block_test2 (
id NUMBER,
val VARCHAR2(100)
) TABLESPACE users_auto; -- ASSM 表空间
-- 同样高并发插入,观察 buffer busy waits 是否减少
实验结果:MSSM 表空间下,100 会话并发插入,buffer busy waits 每秒 3000 次,热块集中在表的最后一个数据块(P1=4, P2=12345)。Latch cache buffers chains 的 miss ratio 涨到 5%,CPU 占用 80%。改用 ASSM 后,buffer busy waits 降到每秒 200 次,miss ratio 降到 0.3%,CPU 占用 45%。
5 那个索引热块把系统搞瘫的案例
一个流水表,每天插入 5000 万行,主键是自增 ID。高并发时,所有 INSERT 都往索引的最右叶块插,那个块成为超级热块,buffer busy waits 占 DB Time 的 40%,插入 TPS 只有 1200。DBA 一开始加 CPU,从 32 核加到 64 核,没用——因为热块是串行访问,CPU 再多也解决不了。后来把主键索引改成反向键索引(REVERSE),键值分散到各个叶块,热块消失,TPS 涨到 8500。代价是范围查询不能走这个索引了(因为键值反向了),但流水表本来就不需要范围查主键,完美解决。
【踩坑笔记】Buffer Busy Waits 的 P3 参数(class#)很重要:1=data block,2=sort block,3=undo header,4=undo block,5=extent map。看 P3 就知道热块类型,针对性解决。Latch 争用不要盲目加 CPU,先定位是哪个 latch,再用 v$latch_children 找到具体子 latch,然后定位保护的数据结构。
6 总结:等待事件诊断的三板斧
第一板斧:看 AWR Top 5,确定瓶颈大类(I/O、并发、应用)。第二板斧:看 ASH 细粒度采样,定位具体 SQL 和具体块。第三板斧:看 V$SESSION_WAIT 的 P1/P2/P3,翻译出文件号、块号、锁类型,找到物理对象。三板斧砍完,80% 的等待事件能定位到根因。最后送一句话:等待事件是数据库的"疼痛信号",止疼药(加硬件)只能缓解,找到病灶(热块、坏 SQL、设计缺陷)才能根治。