【问题标题】:An "entity" specific sequence“实体”特定序列
【发布时间】:2016-12-19 04:32:42
【问题描述】:

背景

我有很多不同的“事物”(特定领域的项目/实体/主题),这些“事物”所有者(人类)是可见的。业主将用数字标识他们的“东西”。而不是显示一个大的“随机”数字,我想向他们显示一个对人类来说更容易的小数字(最好是从 1 开始的序列)。业主很乐意谈论“my foo 37”和“her bar 128”。 “序列”可能有间隙,但附加的数字在“事物”实例的生命周期内必须保持不变。所以我需要一种方法来生成“事物”+所有者特定的 id(目前称为“可见 id”)。

“事物”+所有者组合的数量为 10k+。目前新的“事物”不能动态生成,但所有者可以。

每个所有者一个“事物”实例的数量相对较少,每个所有者大约几十个,但是没有可以从业务规则中得出的硬性上限。经常创建和删除新的“事物”实例。

考虑的选项

我在 SO question Oracle Partitioned Sequence 中找到了一个很好的讨论,它解决了我遇到的几乎相同的问题。

到目前为止,我已经考虑了以下选项:

  1. 我认为标准数据库序列会非常好,但这需要我动态创建大量“事物”+ 所有者特定序列,并在插入过程中解析序列名称。 (并在所有者离开时删除序列。)我不确定创建大量序列是否是一种好习惯(对我来说,10k+ 数据库对象是一个巨大的数字)。
  2. 我也认为是臭名昭著的 max(visible_id) + 1,但我们会遇到正常的并发问题,所以这是不行的。
  3. 根本不将所有者特定的 ID 保存到数据库中,而是在 suggested by Adam Musch 的选择中生成它。这是一个绝妙的主意,但不幸的是,在“事物”实例的生命周期内,id 必须是相同的。
  4. 通过让所有者命名“事物”来避免整个问题。但他们根本不喜欢这个主意 - “我为什么要打扰,说 foo 16 就是这么容易。”

问题

是否有其他方法可以解决此问题,或者我应该开始动态创建序列?如果序列是答案,请详细说明可能存在哪些陷阱(例如 DDL 中的隐式提交)。

我对 Oracle 11gR2 和 12c 解决方案都感兴趣(如果它们不同的话)。

说明问题的伪代码

create table foo (
 id number primary key -- the key for computers
,owner_id number
,visible_id number -- the key for humans
,data_ varchar2(20)
);

create constraint foo_u1 unique foo(owner_id, visible_id);

-- primary key sequence
create sequence foo_id_seq;

insert into foo values(
 foo_id_seq.nextval
,1
,1 -- what to put here ?
,'lorem ipsum'
);

insert into foo values(
 foo_id_seq.nextval
,2
,1 -- what to put here ?
,'dolor sit amet'
);

select visible_id, data_ from foo where owner = 2 order by visible_id;

【问题讨论】:

  • 为什么选项 3 不适用?事物实例可能会被删除?如果将它们标记为已删除(例如通过附加列)怎么办?
  • 我不明白为什么当 MAX(visible_id) 被限制为特定的 ownerid 时 #2 会产生并发问题(你绝对不希望整个表的最大值)。所有者是否可能会同时从多个应用程序实例更新他们的项目?听起来您需要表上的主键,ownerid,thingid。 Thingid 必须由您生成,因此我建议您使用存储过程进行更新/插入/删除,以便您可以根据需要管理序列,而不是直接调用 dml 操作
  • @KonstantinSorokin 嗯......因为,错误......我可能只是因为我对那个选项视而不见。是的,我知道soft delete,是的,我的第一个想法是这可能非常适合这里。我需要更多地思考这个想法以及利弊。
  • @Matt 你是对的,所有访问都将在 PL/SQL 接口之后(我的问题中没有明确表示)。此外,owner_idvisible_id 将是由约束强制执行的唯一组合。然而,即使我们对所有者有一个明确的概念,不幸的是它也不排除来自不同来源的多个同时插入 - 所有者并不总是可以生成新“事物”实例的唯一来源。如果可能的话,我不想故意引入序列化点(即使它是由 PL/SQL API 控制的,并且冲突可能只会“很少”发生)。感谢您挑战我的想法!
  • 创建和删除 10k+ 个 oracle 序列需要几秒钟。但我认为选项 1 可能会在某种程度上减慢插入速度,因为为每个所有者的序列动态创建了硬解析语句。从这个角度来看,通过带有包 API 的表(以提供自治事务和缓存)维护自助序列看起来还不错。但我还是更喜欢选项 3。

标签: sql oracle


【解决方案1】:

由于间隙是可以的,您应该实施“选项 2”的变体。允许间隙意味着您可以快速完成同步:竞争会话只需检查并继续,而不必等待查看其他会话是否提交或回滚。

如果 Oracle 提供INSERT INTO..NOWAIT 选项,这将很容易。事实上,我可能会涉及DBMS_LOCK。这是我对您的 API 外观的看法。

它对您拥有的最大可见 ID 做了一些假设,因为您在原始帖子中做出了这些假设。

CREATE OR REPLACE PACKAGE foo_api AS
  PROCEDURE create_foo (p_owner_id NUMBER, p_data VARCHAR2);
END foo_api;

CREATE OR REPLACE PACKAGE BODY foo_api AS
  -- We need to call allocate_unique in an autonomous transaction because
  -- it commits and the calling program may not want to commit at this time
  FUNCTION get_lock_handle (p_owner_id NUMBER, p_visible_id NUMBER)
    RETURN VARCHAR2 IS
    PRAGMA AUTONOMOUS_TRANSACTION;
    l_lock_handle   VARCHAR2 (128);
  BEGIN
    DBMS_LOCK.allocate_unique (
      lockname  => 'INSERT_FOO_' || p_owner_id || '_' || p_visible_id,
      lockhandle => l_lock_handle
    );
    COMMIT;
    RETURN l_lock_handle;
  END;


  PROCEDURE create_foo (p_owner_id NUMBER, p_data VARCHAR2) IS
    -- This is the highest visible ID you'd ever want.
    c_max_visible_id   NUMBER := 1000;
  BEGIN
   <<id_loop>>
    FOR r_available_ids IN (SELECT a.visible_id
                            FROM   (SELECT ROWNUM visible_id
                                    FROM   DUAL
                                    CONNECT BY ROWNUM <= c_max_visible_id) a
                                   LEFT JOIN foo
                                     ON foo.owner_id = p_owner_id
                                     AND foo.visible_id = a.visible_id
                            WHERE  foo.visible_id IS NULL) LOOP
      -- We found a gap
      -- We could try to insert into it.  If another session has already done so and
      -- committed, we'll get an ORA-00001.  If another session has already done so but not 
      -- yet committed, we'll wait.  And waiting is bad.
      -- We'd like an INSERT...NO WAIT, but Oracle doesn't provide that.
      -- Since this is the official API for creating foos and we have good application 
      -- design to ensure that foos are not created outside this API, we'll manage 
      -- the concurrency ourselves.
      --
      -- Try to acquire a user lock on the key we're going to try an insert.
      DECLARE
        l_lock_handle       VARCHAR2 (128);
        l_lock_result       NUMBER;
        l_seconds_to_wait   NUMBER := 21600;
      BEGIN
        l_lock_handle := get_lock_handle (
          p_owner_id => p_owner_id,
          p_visible_id => r_available_ids.visible_id
        );

        l_lock_result := DBMS_LOCK.request (lockhandle => l_lock_handle,
                                            lockmode   => DBMS_LOCK.x_mode,
                                            timeout    => 0, -- Do not wait
                                            release_on_commit => TRUE);

        IF l_lock_result = 1 THEN
          -- 1 => Timeout -- this could happen.
          -- In this case, we want to move onto the next available ID.
          CONTINUE id_loop;
        END IF;

        IF l_lock_result = 2 THEN
          -- 2 => Deadlock (this should never happen, but scream if it does).
          raise_application_error (
            -20001,
               'A deadlock occurred while trying to acquire Foo creation lock for '
            || p_owner_id
            || '_'
            || r_available_ids.visible_id
            || '.  This is a programming error.');
        END IF;

        IF l_lock_result = 3 THEN
          -- 3 => Parameter error (this should never happen, but scream if it does).
          raise_application_error (
            -20001,
               'A parameter error occurred while trying to acquire Foo creation lock for '
            || p_owner_id
            || '_'
            || r_available_ids.visible_id
            || '.  This is a programming error.');
        END IF;

        IF l_lock_result = 4 THEN
          -- 4 => Already own lock (this should never happen, but scream if it does).
          raise_application_error (
            -20001,
               'Attempted to create a Foo creation lock and found lock already held by session for '
            || p_owner_id
            || '_'
            || r_available_ids.visible_id
            || '.  This is a programming error.');
        END IF;

        IF l_lock_result = 5 THEN
          -- 5 => Illegal lock handle (this should never happen, but scream if it does).
          raise_application_error (
            -20001,
               'An illegal lock handle error occurred while trying to acquire Foo creation lock for '
            || p_owner_id
            || '_'
            || r_available_ids.visible_id
            || '.  This is a programming error.');
        END IF;
      END;

      -- If we get here, we have an exclusive lock on the owner_id / visible_id 
      -- combination.  Attempt the insert
      BEGIN
        INSERT INTO foo (id,
                         owner_id,
                         visible_id,
                         data_)
        VALUES (foo_id_seq.NEXTVAL,
                p_owner_id,
                r_available_ids.visible_id,
                p_data);

        -- If we get here, we are done.
        EXIT id_loop;
      EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN
          -- Unfortunately, if this happened, we would have waited until the competing 
          -- session committed or rolled back.  But the only way it
          -- could have happened if the competing session did not use our API to create 
          -- or update the foo.
          -- TODO: Do something to log or alert a programmer that this has happened, 
          -- but don't fail.
          CONTINUE id_loop;
      END;
    END LOOP;
  END create_foo;
END foo_api;

【讨论】:

  • 我今天才看到这个答案。这看起来像一个完美的匹配,我将以这种方式实现实体特定的序列。作为奖励,如果我遇到这样的要求,这可能会被用来限制实体的数量。谢谢!
猜你喜欢
  • 1970-01-01
  • 2016-10-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-23
  • 2010-12-31
  • 2017-10-24
  • 1970-01-01
相关资源
最近更新 更多