6

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

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

并检查修改的行数。如果没有行被修改,那么另一个进程首先做同样的事情并发送邮件。如果修改了 1 行,我发送邮件。(当然,如果发送邮件失败,我会重置 mail_sent。进程崩溃并将 mail_sent 留在“t”的可能性很小,因此不会发送任何邮件。我会忍受的。)

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

4

3 回答 3

6

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

长话短说,如果有两条语句同时更新,后一条:

  1. 执行一致的读取(语句开始时的行版本)
  2. 检查该行是否符合更新的 where 条件
  3. 如果是,它会执行当前模式读取 - 获取该行的最新提交版本 - 并检查它是否仍然与步骤 1 中的同一行(!),所以我们不会更新我们不打算更新的东西
  4. 如果不是,则该行不会更新,整个更新语句会重新启动,但那是另一回事了。

因此,如果您的第一次更新提交 't',则第二次更新将永远不会再次更新该行。你可以用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 ;)

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

于 2013-09-12T17:02:16.557 回答
2

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

记录的锁定是这种方法的一个深思熟虑的设计目标。在确认发送电子邮件之前,您不想表明您已经发送了它,否则您需要一个恢复过程来表明传输实际上失败了。同样,当您开始发送电子邮件的过程时,您不希望另一个会话开始该过程。

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

于 2013-09-12T18:11:53.623 回答
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
于 2013-09-12T15:35:18.147 回答