Oracle 序列(Sequence)的缓存(Caching)与非序列化现象

在这里插入图片描述
好的,我们将对 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 次调用)会触发一个缓存更新事件
  • 缓存更新过程(关键内部原理)

    1. 会话需要获取序列的下一个值,发现内存缓存已用完。
    2. 会话请求获取一个(通常是 enq: SQ - contention),以确保只有一个会话可以更新序列。
    3. 持有锁的会话执行以下操作:
      a. 访问数据字典,将磁盘上的 LAST_NUMBER 增加 CACHE 大小(例如,从 21 增加到 41)。
      b. 将新的值范围(21 到 41)拉取到内存中,更新 SGA 中的序列缓存结构。
      c. 释放锁。
    4. 所有等待的会话现在可以继续从新的内存缓存(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';
    
    • P1P2 参数可以用于解码出被争用的序列对象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 waits on 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)。

第三部分:通俗易懂的解释

想象一下序列是一个发号机,比如银行柜台或者餐厅排队叫号机。

  • NOCACHE:好比是只有一个号码纸的叫号机。每来一个人(会话),工作人员(数据库)必须:

    1. 拿出唯一的号码纸(访问数据字典)。
    2. 用笔写上下一个数字(更新磁盘)。
    3. 把纸撕给你(返回 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 的核心任务就是根据应用程序的需求(是否能容忍间隔?是否需要绝对顺序?并发多高?),合理地配置 CACHEORDER/NOORDER 等选项。对于绝大多数高并发 OLTP 系统,使用一个足够大的 CACHE 配合 NOORDER 是最佳实践。当看到 enq: SQ - contention 等待时,第一反应就是去增大相关序列的缓存大小。

欢迎关注我的公众号《IT小Chen

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值