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

4 sql oracle

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

update mytable set mail_sent = 't' where id = ? and mail_sent = 'f'
Run Code Online (Sandbox Code Playgroud)

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

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

Kir*_*tev 5

有一组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 ;)
Run Code Online (Sandbox Code Playgroud)

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