繁体   English   中英

使用SQL ISOLATION LEVEL SERIALIZABLE进行锁定和引用计数

[英]Locking and reference counting with SQL ISOLATION LEVEL SERIALIZABLE

除其他事项外,我有Java / JDBC应用程序维护两个SQL数据库表:

    MESSAGES (primary key MSG_ID) 
    RECIPIENTS (primary key USER_ID, foreign key MSG_ID)

RECIPIENTS中的记录指向MESSAGES.MSG_ID。 当收件人撤消该邮件时,应删除其在RECIPIENTS中的(USER_ID,MSG_ID)记录,如果他是该MSG_ID的最后一个剩余收件人,则也应从MESSAGES中删除由RECIPIENTS.MSG_ID指示的消息记录。

然后用伪代码写下的简化逻辑基本上是这样的:

    GET DB CONNECTION
    BEGIN TRANSACTION

    // interlock reference counting for this MSG_ID for SELECT COUNT(*) below
    SELECT * FROM MESSAGES WHERE MSG_ID='...' FOR UPDATE

    DELETE FROM RECIPIENTS WHERE USER_ID='...' AND MSG_ID='...'

    IF (SELECT COUNT(*) FROM RECIPIENTS WHERE MSG_ID='...') == 0
    THEN DELETE FROM MESSAGES WHERE MSG_ID='...'

    COMMIT

由于应用程序的核心逻辑原因,数据库连接池设置为TRANSACTION_SERIALIZABLE模式。

问题是当两个用户试图同时消除该消息时,如何避免竞争状况。

用户A和B可能启动并发事务,这意味着(取决于确切的数据库引擎实现)两者都可以在事务开始时获得类似于MVCC的数据库内容快照。 如果是这样,那么A在从收件人中删除后将相信它不是最后一个剩余的收件人(在B的快照中将B视为仍然是剩余的收件人); 并且B同样会认为它也不是最后一个接收者(将A视为B快照中的剩余接收者)。

参照它们的快照,A和B都将看到其自己的“从收件人删除”的效果,但不会看到并发事务的效果。 因此,对于A和B来说,SELECT COUNT(*)将返回1,并且A和B都不会尝试执行DELETE FROM MESSAGES。

是否有一个通用的解决方案,即独立于特定的数据库引擎,而不依赖数据库外部的锁定?

我非常希望(如果可能的话)避免为了解决此问题而不必创建具有较低事务隔离级别的单独连接池。

事务隔离级别可序列化时,“同时”不会发生任何事情

我不会那么确定。 它通常的工作方式是通过数据库引擎检测两个COMMITS之间的写冲突,例如通过比较事务的前映像和DB存储之间的版本号,如果版本不匹配则使COMMIT失败,然后由应用程序重新确定-从一开始就尝试整个交易。

但是,最重要的是,在这种情况下,没有写冲突。 RECIPIENTS.A和RECIPIENTS.B的记录是不同的记录,并且可以(或可以被)对版本进行独立标记(例如,如果标记是基于行的;或者如果是基于页面的,但是记录属于不同的DB页面)。

该应用程序基于不会产生写冲突的只读SELECT数据访问来做出决定(是否删除MESSAGE),并且该决定是在DB引擎之外做出的,后者是未知的。

您在这里要做的是基于一组元组(RECIPIENTS)建立一致性规则。 但是您的应用程序仅锁定一个特定的元组。

在这种情况下,没有隔离级别可以为您提供正确的协议。

或者,您可以将所有MSG_ID匹配的元组锁定在RECIPIENTS ,运行DELETE命令,检查剩余多少个匹配的元组,并在总的剩余计数为零的情况下运行第二个DELETE

该协议将像这样工作
Tx A:

1)锁定所有属于MSG_ID的当前记录
2)删除相关记录
3)计算剩余记录
4)如果count = 0删除消息记录
5)提交/回滚

Tx B (在Tx A启动后的任何时间运行):
1)锁定所有属于MSG_ID的当前记录

 - wait until Tx A released lock via COMMIT/ROLLBACK  
 - once Tx B gets the lock, Tx A has finished all processing  
 - Tx B does not see any record from before Tx A's end

2)删除相关记录
3)计算剩余记录
4)如果count = 0删除消息记录
5)提交/回滚

该架构涵盖了RECIPIENTS表的所有DELETE / UPDATE事务。
唯一可能的问题是,仅通过锁定与感兴趣的消息ID相关的记录,我们就无法解决在执行count(*)之后就可以插入新收件人的情况。

为了避免这种情况,必须锁定整个表以避免INSERTS 但是,这也会使等待的线程无法在指定的消息ID上工作。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM