更新声明是否安全于竞争条件?

时间:2013-09-12 15:30:31

标签: sql oracle

我想确保电子邮件只发送一次,因此我在Oracle SQL中使用以下语句:

update mytable set mail_sent = 't' where id = ? and mail_sent = 'f'

并检查修改的行数。如果没有修改任何行,则另一个进程首先执行相同的操作并发送邮件。如果修改了1行,我发送邮件。 (当然,如果发送邮件失败,我会重置mail_sent。进程崩溃的可能性很小,并且将mail_sent留在't',所以没有邮件发送。我会忍受它。)

我无法说服自己这对竞争条件是安全的(进程1读取'f'并且进程2在进程1写入't'之前读取'f',因此两个进程都认为他们修改了行和2封电子邮件发送。我将隔离级别设置为SERIALIZABLE以避免问题,但这实际上是必要的,还是没有它我是否安全?

3 个答案:

答案 0 :(得分:6)

有一组Tom Kyte关于并发更新期间发生的一些优秀文章,值得一读:

长话短说,如果两个语句进行并发更新,后一个:

  1. 执行一致的读取(从语句开始时刻开始的行版本)
  2. 检查行是否符合更新的where条件
  3. 如果是,则执行当前模式读取 - 获取行的最新提交版本 - 并检查它是否仍然与步骤1(!)中的行相同,因此我们不进行更新我们并不打算
  4. 如果不是,那么该行不会更新,整个更新语句会重新启动,但那是另一个故事。
  5. 因此,如果您的第一次更新提交',则第二次更新将永远不会再次更新此行。您可以使用sql%rowcount进行检查。

    一个简单的测试用例(36和37是这里的两个并发会话):

    -- first session updates, locks the row
    00:41:44 LKU@sandbox(36)> update mail set mail_sent = 't' where id = 1 and mail_sent = 'f';
    
    1 row updated.
    
    Elapsed: 00:00:00.21
    
    -- second session tries to update the same row, it hangs as the row is locked
    00:58:13 LKU@sandbox(37)> update mail set mail_sent = 't' where id = 1 and mail_sent = 'f';
    
    -- first session commits
    00:58:27 LKU@sandbox(36)> commit;
    
    Commit complete.
    
    Elapsed: 00:00:00.00
    
    -- no rows updated in second!
    00:58:13 LKU@sandbox(37)> update mail set mail_sent = 't' where id = 1 and mail_sent = 'f';
    
    0 rows updated.
    
    Elapsed: 00:00:33.12 -- time of me switching between sqlplus tabs and copy-pasting text here ;)
    

    因此,我可以得出结论,如果您在执行更新后检查会话更新的行数 - 您是安全的。

答案 1 :(得分:2)

一种安全的方法是选择要更新的行,该行对该行进行独占锁定,发送电子邮件,然后将记录更新为“t”并提交。

记录的锁定是该方法的精心设计目标。在确认发送电子邮件之前,您不希望表明您已发送电子邮件,否则您需要恢复过程以指示传输实际上已失败。同样,当您开始发送电子邮件的过程时,您不希望其他会话启动该过程。

如果有必要避免长期锁定,那么我建议将该过程分为两个步骤 - 设置一个标志以确认电子邮件传输过程已经开始(实际上我是时间戳),以及再次设置(或设置另一个)以确认传输。这本身并不是一个糟糕的方法,因为它可以监控确认所需的时间,根据我的经验,一些互联网请求可能占应用时间的很大一部分。

答案 2 :(得分:0)

这听起来像交易工作。

BEGIN TRANSACTION

UPDATE mytable 
SET mail_sent = 't' 
WHERE id = @id 
AND mail_sent = 'f'

send the email

IF (@emailSent = 0)
    ROLLBACK TRANSACTION
    RAISERROR('Email not sent', 1, 16);
ELSE
    COMMIT TRANSACTION