查看在并发会话环境中运行的代码,在每个原子语句之后,我们需要问“如果另一个会话刚刚打破了我们的假设怎么办?”并据此进行调整。
选项 1。 计数并决定 INSERT 或 UPDATE
declare
v_count int;
begin
SELECT count(1) INTO v_count FROM my_table WHERE ...;
IF v_count = 0 THEN
-- what if another session inserted the same row just before this point?
-- this statement will fail
INSERT INTO my_table ...;
ELSE
UPDATE my_table ...;
END IF;
end;
选项 2. 更新,如果没有更新 - 插入
begin
UPDATE my_table WHERE ...;
IF SQL%COUNT = 0 THEN
-- what if another session inserted the same row just before this point?
-- this statement will fail
INSERT INTO my_table ...;
END IF;
end;
选项 3. 插入,如果失败 - 更新
begin
INSERT INTO my_table ...;
exception when DUP_VAL_ON_INDEX then
-- what if another session updated the same row just before this point?
-- this statement will override previous changes
-- what if another session deleted this row?
-- this statement will do nothing silently - is it satisfactory?
-- what if another session locked this row for update?
-- this statement will fail
UPDATE my_table WHERE ...;
end;
选项 4. 使用 MERGE
MERGE INTO my_table
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED THEN INSERT ...
-- We have no place to put our "what if" question,
-- but unfortunately MERGE is not atomic,
-- it is just a syntactic sugar for the option #1
选项 5. 在 my_table 上使用 DML 接口
-- Create single point of modifications for my_table and prevent direct DML.
-- For instance, if client has no direct access to my_table,
-- use locks to guarantee that only one session at a time
-- can INSERT/UPDATE/DELETE a particular table row.
-- This could be achieved with a stored procedure or a view "INSTEAD OF" trigger.
-- Client has access to the interface only (view and procedures),
-- but the table is hidden.
my_table_v -- VIEW AS SELECT * FROM my_table
my_table_ins_or_upd_proc -- PROCEDURE (...) BEGIN ...DML on my_table ... END;
PROCEDURE my_table_ins_or_upd_proc(pi_row my_table%ROWTYPE) is
l_lock_handle CONSTANT VARCHAR2(100) := 'my_table_' || pi_row.id;
-- independent lock handle for each id allows
-- operating on different ids in parallel
begin
begin
request_lock(l_lock_handle);
-->> this code is exactly as in option #2
UPDATE my_table WHERE ...;
IF SQL%COUNT = 0 THEN
-- what if another session inserted the same row just before this point?
-- NOPE it cannot happen: another session is waiting for a lock on the line # request_lock(...)
INSERT INTO my_table ...;
END IF;
--<<
exception when others then
release_lock(l_lock_handle);
raise;
end;
release_lock(l_lock_handle);
end;
此处不再深入介绍底层细节,请参阅this article 了解如何在 Oracle DBMS 中使用锁。
因此,我们看到选项 1、2、3、4 存在一般情况下无法避免的潜在问题。但如果安全性得到域规则或特定设计约定的保证,则可以应用它们。
选项 5 可靠且快速,因为它依赖于 DBMS 合同。
尽管如此,这将是对简洁设计的奖励,如果my_table 裸露且客户依赖此表上的直接 DML,则无法实施。
我认为性能不如数据完整性重要,但为了完整性,让我们提一下。
经过适当考虑,很容易看出根据“理论”平均性能的选项顺序是:
2 -> 5 -> (1,4) -> 3
当然,性能测量步骤是在获得至少两个正常工作的解决方案之后进行的,并且应该专门针对给定工作负载配置文件下的特定应用程序进行。那是另一个故事。目前无需在一些综合基准测试中关心理论纳秒。
我想目前我们看到不会有魔法。在应用程序的某个地方,需要确保插入到my_table 中的每个id 都是唯一的。
如果 id 值无关紧要(95% 的情况) - 只需使用 SEQUENCE。
否则,在my_table(在 Java 或 DBMS 模式 PL/SQL 中)上创建单点操作并控制那里的唯一性。如果应用程序可以保证一次最多一个会话处理my_table 中的数据,则可以只应用选项#2。