Ponieważ luki są w porządku, powinieneś zaimplementować odmianę „opcji 2”. Dopuszczenie przerw oznacza, że synchronizację można wykonać szybko:konkurujące sesje po prostu sprawdzają i poruszają się dalej, zamiast czekać, aby zobaczyć, czy inni zatwierdzą lub wycofają.
Jeśli Oracle zaoferował INSERT INTO..NOWAIT
opcja, to byłoby łatwe. W rzeczywistości prawdopodobnie włączyłbym DBMS_LOCK
. Oto moje zdanie na temat wyglądu Twojego interfejsu API.
Robi pewne założenia dotyczące maksymalnego widocznego identyfikatora, który miałbyś, ponieważ przyjąłeś te założenia w swoim oryginalnym poście.
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;