我有Java / JDBC应用程序维护,其中包括两个SQL数据库表:
MESSAGES (primary key MSG_ID)
RECIPIENTS (primary key USER_ID, foreign key MSG_ID)
RECIPIENTS中的记录指向MESSAGES.MSG_ID。当收件人解除该邮件时,应删除其收件人中的(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将在DELETE FROM RECIPIENTS之后相信它不是最后剩下的收件人(看到B仍然是A快照中的剩余收件人); B同样会认为它不是最后一个收件人(看到A仍然是B快照中的剩余收件人)。
参考他们的快照,A和B都会看到他们自己的DELETE FROM RECIPIENTS的影响,但不会看到并发事务的影响。对于A和B,SELECT COUNT(*)将返回1,A和B都不会尝试执行DELETE FROM MESSAGES。
这个问题的解决方案是否通用,即独立于特定的数据库引擎,而不依赖于数据库外部的锁定?
我更愿意(如果可能的话)避免必须创建一个事务隔离级别较低的单独连接池,以解决此问题。
答案 0 :(得分:0)
没有任何事情发生在同一时间"当事务隔离级别是可序列化的
时
我不会那么肯定。它通常的工作方式是通过数据库引擎检测两个COMMITS之间的写冲突,例如通过比较事务的前映像和数据库存储之间的版本号,如果版本不匹配则失败COMMIT,使其保持不变应用程序从一开始就重新尝试整个事务。
然而,重点是在这种情况下没有写入冲突。 RECIPIENTS.A和RECIPIENTS.B的记录是不同的记录,并且(或可以)是独立的版本标记(例如,如果标记是基于行的;或者如果它是基于页面的,但记录属于不同的DB页面)。 / p>
应用程序根据只读SELECT数据访问做出决定(是否删除MESSAGE),不会发生写入冲突,这个决定是在数据库引擎之外进行的,后者不知道。
答案 1 :(得分:0)
您要在此处执行的操作是基于一组元组(RECIPIENTS)建立一致性规则。但是您的应用程序只锁定一个特定的元组。
在这种情况下,没有隔离级别会为您提供正确的协议。
或者,您可以在MSG_ID
中锁定与RECIPIENTS
匹配的所有元组,运行DELETE
命令,检查剩余的匹配元组数量并运行第二个{如果总剩余计数为零,则为{1}}。
协议会像这样工作 Tx A:
1)锁定属于MSG_ID的所有当前记录
2)删除有问题的记录
3)计算剩余记录
4)如果count = 0则删除消息记录
5)COMMIT / ROLLBACK
Tx B (在Tx A开始后的任何时间运行):
1)锁定属于MSG_ID的所有当前记录
DELETE
2)删除有问题的记录
3)计算剩余记录
4)如果count = 0则删除消息记录
5)COMMIT / ROLLBACK
此架构涵盖RECIPIENTS表的所有 - 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
/ DELETE
个交易
唯一可能的问题是,通过仅锁定与感兴趣的消息ID相关的记录,我们不会涵盖在UPDATE
执行后可以插入新收件人的情况。
为了避免这种情况,必须锁定整个表以避免count(*)
。但是,这会使等待也无法处理对指定消息ID无效的线程。