
好的,我们将对 Oracle 序列(Sequence)的缓存(Caching)机制及其引发的非序列化(Non-sequential)现象进行一场深入而全面的剖析。序列是生成唯一标识符的关键对象,其性能和高可用性设计直接影响系统的扩展能力。
第一部分:官方技术详解
一、核心概念与作用
1. 什么是序列?
Oracle 序列是一个独立的数据库对象,用于生成一组唯一且有序的数字。它不依赖于表,多个表可以共享同一个序列。其主要作用是为主键列或其他需要唯一标识的列提供值。
2. 为什么需要缓存?
如果每次调用 NEXTVAL 都直接更新数据字典(存储在 SYSTEM 表空间中),会产生严重的性能瓶颈:
- 频繁的磁盘 I/O:更新数据字典需要物理写盘(尽管可能被缓冲池延迟,但事务提交仍需保证写入)。
- 序列化争用(Serialization):为了维护序列值的唯一性和顺序性,获取下一个值的操作必须是串行的。这会导致会话等待,在高并发场景下成为致命的性能枷锁。
缓存机制通过“预分配”一组序列值到内存中,极大地减少了访问数据字典的次数和序列化点的争用,是提升并发性能的关键。
二、内部架构与缓存机制
1. 序列的存储与状态
序列的定义(名称、增量、缓存大小等)存储在数据字典表(如 SEG$)中。而其当前值是一个状态,这个状态在内存(SGA)和磁盘上都有体现:
- 磁盘上的状态:记录在数据字典中,是持久化的、已分配出去的最后一个值。例如,如果缓存大小为 20,磁盘上可能记录的是 100,而内存中正在分配 81 到 100。
- 内存中的状态:在 SGA 的共享池(Shared Pool) 中,为每个序列分配了一个内存结构,其中包含了当前缓存的范围(例如
next available number = 81,last available number = 100)。
2. 缓存工作机制详解
假设我们创建一个序列并开始使用:
CREATE SEQUENCE my_seq CACHE 20;
-- Session 1:
SELECT my_seq.NEXTVAL FROM DUAL; -- Returns 1
-- Session 2:
SELECT my_seq.NEXTVAL FROM DUAL; -- Returns 2
-
初始状态:
- 磁盘数据字典中记录的
LAST_NUMBER可能是 21。(因为缓存了 1-20,下一个可用的缓存范围将从 21 开始)。 - 内存中缓存了数字 1 到 20。两个会话的
NEXTVAL请求都从内存中快速获取,无需接触磁盘。
- 磁盘数据字典中记录的
-
缓存耗尽:
- 当第 20 次
NEXTVAL被调用后,内存缓存耗尽。 - 下一个需要
NEXTVAL的会话(例如第 21 次调用)会触发一个缓存更新事件。
- 当第 20 次
-
缓存更新过程(关键内部原理):
- 会话需要获取序列的下一个值,发现内存缓存已用完。
- 会话请求获取一个锁(通常是
enq: SQ - contention),以确保只有一个会话可以更新序列。 - 持有锁的会话执行以下操作:
a. 访问数据字典,将磁盘上的LAST_NUMBER增加CACHE大小(例如,从 21 增加到 41)。
b. 将新的值范围(21 到 41)拉取到内存中,更新 SGA 中的序列缓存结构。
c. 释放锁。 - 所有等待的会话现在可以继续从新的内存缓存(21-41)中获取值。
3. CACHE vs NOCACHE
-
CACHE N:- 性能:极高。
NEXTVAL操作几乎完全是内存访问,速度极快。 - 潜在风险:如果数据库实例意外关闭(如断电),所有已缓存但尚未被使用的序列值将会丢失。下次实例启动后,序列将从磁盘上记录的下一个值开始分配,导致序列值出现“间隔(Gaps)”。这是设计使然,而非故障。
- 性能:极高。
-
NOCACHE:- 行为:每次调用
NEXTVAL都会导致对数据字典的更新(UPDATE)。 - 优点:保证了无间隔的连续值(但在多会话并发环境下,由于回滚等原因,也无法绝对保证无间隔)。
- 缺点:性能极差,会引发严重的
enq: SQ - contention等待,绝对不应用于高并发系统。
- 行为:每次调用
4. ORDER vs NOORDER (尤其在RAC环境中)
-
NOORDER(默认):- 每个数据库实例可以缓存自己的一组序列值(例如,实例1缓存1-20,实例2缓存21-40)。
- 性能最好,因为实例间的协调最少。
- 生成的序列值在全局范围内是唯一的,但不是按请求时间严格排序的。实例1产生的值可能小于实例2产生的值,但实例2的请求可能先于实例1发生。
-
ORDER:- 强制要求序列值严格按照请求发生的顺序分配。
- 在 RAC 中,这意味着所有实例的
NEXTVAL请求必须通过一个全局的同步点(可能会通过全局缓存服务(GCS)进行协调),这会带来显著的性能开销和等待事件。 - 只有在应用程序严格要求时间顺序时才使用。
第二部分:场景、争用、排查与解决
三、性能争用与排查:enq: SQ - contention
1. 场景:高并发序列申请导致争用
- 原理:当缓存大小(
CACHE)设置过小,无法满足系统的并发需求时,缓存会频繁耗尽。如上所述,每次缓存更新都需要一个会话获取独占锁来执行更新操作,在此期间所有其他申请NEXTVAL的会话都会被阻塞,等待enq: SQ - contention事件。 - 影响:应用程序响应时间急剧增加,吞吐量下降。AWR 报告会显示该等待事件名列前茅。
2. 排查与诊断
-
确认等待事件:
-- 查找正在等待序列争用的会话 SELECT s.sid, s.serial#, s.username, s.event, s.p1, s.p2, s.p3, -- 解码P1和P2获取被争用的序列对象ID -- (SELECT object_name FROM dba_objects WHERE object_id = s.p1) AS seq_name, s.sql_id, s.BLOCKING_SESSION FROM v$session s WHERE s.event = 'enq: SQ - contention';P1和P2参数可以用于解码出被争用的序列对象ID。
-
识别有问题的序列:
-- 结合V$SESSION和DBA_OBJECTS来定位序列 SELECT DISTINCT o.owner, o.object_name, o.object_type FROM v$session s, dba_objects o WHERE s.event = 'enq: SQ - contention' AND s.p1 = o.object_id; -
分析序列使用情况:
-- 查看序列的当前定义和缓存设置 SELECT sequence_owner, sequence_name, cache_size, last_number FROM dba_sequences WHERE sequence_name = 'MY_SEQ'; -- 替换为你的序列名 -- 监控序列的使用速率(需要定期采样估算) SELECT * FROM ( SELECT sequence_owner, sequence_name, last_number, (last_number - LAG(last_number) OVER (PARTITION BY sequence_owner, sequence_name ORDER BY NULL)) / (SELECT (EXTRACT(SECOND FROM (systimestamp - startup_time)) + 60 * (EXTRACT(MINUTE FROM (systimestamp - startup_time))) + 3600 * (EXTRACT(HOUR FROM (systimestamp - startup_time)))) FROM v$instance) * 60 AS values_per_min FROM dba_sequences WHERE sequence_name = 'MY_SEQ' ) WHERE values_per_min IS NOT NULL; -- 这个查询可以粗略估算序列值每分钟的增长速度
3. 解决方案
-
增大
CACHE大小:这是最直接、最有效的解决方法。将缓存值从一个很小的数字(如 20)增加到与你的并发事务量相匹配的大小(如 1000, 5000, 甚至 10000)。ALTER SEQUENCE my_seq CACHE 5000;- 权衡:更大的缓存意味着实例重启时可能丢失的值的范围更大(间隔更大)。但对于绝大多数使用序列生成代理主键的场景,性能远比无间隔的连续性重要。
-
使用
NOORDER:如果你在 RAC 环境中且应用程序不严格要求序列值的全局顺序,确保使用NOORDER(这是默认值)。ALTER SEQUENCE my_seq NOORDER; -
考虑其他方案(极端情况下):
- 应用层预分配:在应用层使用一个独立的服务批量获取一组序列值,然后在应用内部分配。这完全消除了数据库层的争用。
- 使用反向键索引(Reverse Key Index):如果序列争用伴随着索引块的热块争用(
buffer busy waitson the primary key index),可以考虑使用反向键索引来分散热点。
四、非序列化现象:间隔(Gaps)
1. 原因
序列值出现间隔是正常现象,原因包括:
- 缓存机制:实例重启后缓存丢失(最常见原因)。
- 事务回滚:一个会话获取了
NEXTVAL,然后该事务被回滚。已获取的序列值不会被“回滚”或重用。 - 批量操作:使用
INSERT INTO ... SELECT ...时,如果语句中途失败,已从序列中获取的值就被消耗掉了。 - RAC 中的
CACHE NOORDER:每个实例独立缓存,实例1缓存了1-20,实例2缓存了21-40。如果实例1因为负载较低而没有用完其缓存,而实例2用完了,那么全局序列值就会是 1, 21, 22, 23, … 2, 3, …,从而产生间隔和乱序。
2. 影响与应对
- 影响:通常没有负面影响。序列的主要职责是提供唯一性,而非连续性。主键本身只需要唯一。
- 应对:如果应用程序确实需要无间隔的编号(如发票号),则不能依赖序列的
CACHE机制。你必须使用NOCACHE并承受其性能代价,或者实现一个自定义的、基于表的发号器(但需要自己处理并发和性能问题)。
五、常用查询与管理 SQL
-
查看所有序列定义:
SELECT sequence_owner, sequence_name, min_value, max_value, increment_by, cycle_flag, order_flag, cache_size, last_number FROM dba_sequences WHERE sequence_owner = 'MY_SCHEMA'; -
获取序列的当前会话值(不推进序列):
SELECT my_seq.CURRVAL FROM DUAL; -- 注意:必须在当前会话中已经调用过 NEXTVAL 后才能使用。 -
重置序列(需要技巧):
-- 注意:直接修改序列需要谨慎,通常需要先删除再重建。 -- 或者使用一个过程,通过 INCREMENT BY 负值来调整 DECLARE l_currval NUMBER; BEGIN -- 不安全的方法,仅演示原理 SELECT my_seq.NEXTVAL INTO l_currval FROM DUAL; EXECUTE IMMEDIATE 'ALTER SEQUENCE my_seq INCREMENT BY -' || (l_currval - 1) || ' MINVALUE 0'; SELECT my_seq.NEXTVAL INTO l_currval FROM DUAL; -- This will return 1 EXECUTE IMMEDIATE 'ALTER SEQUENCE my_seq INCREMENT BY 1 MINVALUE 0'; END; / -- 更安全的方法是 DROP 和 CREATE。 -
监控序列相关的等待(AWR/ASH):
- 在 AWR 报告的 “Enqueue Activity” 部分查看
SQ锁的获取次数和等待时间。 - 在 “Segment Statistics” 部分查看 “logical reads” 和 “row lock waits” 是否集中在序列相关的对象上(但序列争用更多是内存闩锁/锁,而非段I/O)。
- 在 AWR 报告的 “Enqueue Activity” 部分查看
第三部分:通俗易懂的解释
想象一下序列是一个发号机,比如银行柜台或者餐厅排队叫号机。
-
NOCACHE:好比是只有一个号码纸的叫号机。每来一个人(会话),工作人员(数据库)必须:- 拿出唯一的号码纸(访问数据字典)。
- 用笔写上下一个数字(更新磁盘)。
- 把纸撕给你(返回
NEXTVAL)。
- 问题:速度极慢!如果同时来一群人,大家必须排成一队,严格按顺序等待工作人员完成“写号-撕票”这个动作。这就是
enq: SQ - contention等待。
-
CACHE 20:工作人员学聪明了。他提前准备了20张空白小票(内存缓存),并一次性在系统里登记:“我已经把1-20号发出去了!”(更新磁盘上的LAST_NUMBER为21)。现在:- 来一个人,他直接从手边的小票堆里拿一张给你(极快的 memory access)。
- 又来一个人,再给一张。
- …直到第21个人来时,他发现手边的20张票发完了。
- 这时,他需要暂停发号(获取锁),再准备20张空白票(21-40),并再次在系统里登记:“21-40号已发出”(更新磁盘),然后恢复发号。
- 优点:绝大部分时间发号速度飞快。
- 缺点(间隔):如果工作人员突然晕倒(实例崩溃),他手头准备好的但还没发出去的空白小票就作废了。等他醒过来,他会从之前登记的下一个号(21)开始准备新的小票。因此,15,16,17…这些票就永远消失了,造成了号码的间隔。
-
RAC 中的
CACHE NOORDER:现在银行有两个柜台(两个数据库实例),每个柜台都有自己的叫号机和一叠预分配的号码(实例缓存)。1号柜台有1-20号,2号柜台有21-40号。两个柜台同时发号。- 虽然全局来看号码是唯一的,但不保证顺序。一个在2号柜台排队的顾客,可能比在1号柜台晚来的顾客更早拿到号(比如拿到21号),但他的号码更大。
-
RAC 中的
CACHE ORDER:为了保证绝对的时间顺序,两个柜台不能自己发号了。每来一个顾客,柜台必须打电话给一个总协调员(全局锁)询问:“下一个该发多少号?”。总协调员告诉号码,柜台再发。这保证了顺序,但速度慢多了,因为每次发号都要打电话请示。
总结:
Oracle 序列的缓存机制是一种经典的用“空间(可能的值间隔)”换“时间(性能)”的策略。DBA 的核心任务就是根据应用程序的需求(是否能容忍间隔?是否需要绝对顺序?并发多高?),合理地配置 CACHE、ORDER/NOORDER 等选项。对于绝大多数高并发 OLTP 系统,使用一个足够大的 CACHE 配合 NOORDER 是最佳实践。当看到 enq: SQ - contention 等待时,第一反应就是去增大相关序列的缓存大小。
欢迎关注我的公众号《IT小Chen》
2401

被折叠的 条评论
为什么被折叠?



