背景
我有很多不同的“事物”(特定于域的项目/实体/主题),这些东西对“事物”所有者(人类)可见。所有者将用数字标识他们的“事物”。我要显示一个较小的数字(最好是从1开始的序列),而不是显示一个较大的“随机”数字,这对人类来说比较容易。业主很乐于谈论“我的富人37”和“她的酒吧128”。那里的“序列”可以有间隔,但附加的数目必须在“事物”实例的生存期内保持不变。因此,我需要一种生成“事物” +所有者特定ID(当前称为“可见ID”)的方法。
“事物” +所有者组合的数量为10k +。当前无法动态生成新的“事物”,但所有者可以生成。
每个所有者一个“事物”实例的数量相对较小,每个所有者大约数十个,但是没有可以从业务规则中得出的硬性上限。经常创建和删除新的“事物”实例。
考虑的选项
我在一个SO问题Oracle分区序列中找到了一个很好的讨论,该问题几乎解决了我所遇到的相同问题。
到目前为止,我已经考虑了以下选项:
max(visible_id) + 1
问题
还有其他解决此问题的方法,还是应该开始动态创建序列?如果序列是答案,请详细说明可能存在的陷阱(例如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;
由于差距尚可,因此您应该实现“选项2”的变体。允许间隔意味着您可以快速完成同步:竞争的会话仅检查并继续,而不必等待其他会话是否提交或回滚。
如果Oracle提供了一个INSERT INTO..NOWAIT选项,这将很容易。实际上,我可能会参与其中DBMS_LOCK。这是我对您的API外观的看法。
INSERT INTO..NOWAIT
DBMS_LOCK
它对您拥有的最大可见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;