服务同时调用两次
Posted
技术标签:
【中文标题】服务同时调用两次【英文标题】:Service Called Twice At Same Time 【发布时间】:2019-04-09 16:49:26 【问题描述】:我有以下使用 Spring 用 Java 编写的 Web 服务,我用 @Transactional 注释将它包装起来,以确保我们可以在需要时回滚。
它工作正常,除了服务被调用两次的情况,第二次调用发生在第一次调用完成之前。
在这种情况下,由于第一个事务仍在运行并且尚未提交到 DB,第二个事务将通过 full 方法,插入重复行,再次更新状态,并调用 sendAlert()。
这是伪代码。
@Transactional
public ServiceResponse update(ServiceRequest serviceRequest)
....
if (myDao.getStatus() == "COMPLETE")
return serviceError;
myDao.insertRow();
myDao.updateStatus("COMPLETE");
sendAlert();
如何防止第二笔交易在第一笔交易之前完成?将隔离级别设置为未提交读不是一个选项,因为数据库不支持它。
【问题讨论】:
你需要做的是同步你的函数,所以第一个调用的执行会阻塞第二个调用的执行。虽然,这完全没有效率。也许你需要做的是防止函数被调用两次。 根据您使用的数据库和技术,可以使用数据库锁,以便您拥有单一访问权限。如果您使用诸如休眠之类的东西,请使用乐观锁定来防止这种情况。此外,您可能希望添加一些唯一索引,以防止插入重复项(作为最后的手段)。 M. Deinum - 迁移到 Hibernate 不是一种选择,因此唯一可用的解决方案是在数据库上添加约束,因此您的答案最接近解决方案。 【参考方案1】:根据您的问题,我假设 update(..)
将在某种程度的并发下被调用。
我发现这种使用外部数据存储进行协调的方法存在一些问题。对于默认的“Read Committed”隔离,您将遇到现在遇到的情况,但是,即使您可以使用“Read Un-Committed”,您也会遇到问题,即第二个事务已读取脏的“COMPLETE”数据,返回但第一个事务仍可能失败并回滚。
我提出了几种方法(当然我做了很多假设)
-
幂等性:通过使数据库更新幂等,您不必担心重复更新
压缩:如果有可能最新记录总是正确的,您可以让所有写入通过但只读取最新记录,这类似于 Kafka 在内部进行压缩的方式
【讨论】:
【参考方案2】:不幸的是,Hibernate 在这种情况下依赖于数据库功能。 Hibernate 旨在能够支持使用完全相同的数据库的其他服务。
有一种情况,我们可以通过修改生成器策略来手动设置未来的主键。但这不会阻止内容“重复”。
一种解决方案可能是让 ReSTful API 具有一个“请求”处于特定状态的数据库状态的 Rest-Entity。
例如:
-
请求 A 插入汽车实体。
同时第二个请求 B 到达 INSERT Car-Entity。
A 执行成功。
B 执行成功。
我们有一个副本。
为了防止这种情况,我们可以存储 Car-Existence-Request。
-
汽车存在请求 A 到达服务器。
Car-Existence-Request B 到达服务器。
两者都存储到数据库中
Car-Existence-Request A 完成。
汽车存在请求 B 完成。
服务器尝试存储汽车 A - 成功。
服务器尝试存储汽车 B - 失败 - 重复。
将 Cart-Existence-Request A 标记为成功。
将 Cart-Existence-Request B 标记为失败。
或者只是切换到 PostGreSQL
【讨论】:
【参考方案3】:您可以使用数据库来执行此操作,甚至可能使用状态列,看起来您可能已经在使用。我不想在这里处理所有细节……只是为了传达这个想法。这里最重要的是,这种机制可以跨线程、进程甚至机器,我遇到过的任何数据库,你没有其他东西可以设置。当您想要扩展到多个实例时,没有 Redis 等:
您将创建另一个状态,并使用测试和设置操作:
public ServiceResponse update(ServiceRequest serviceRequest)
....
while true:
String status = myDao.getStatus();
if (status.equals("COMPLETE"))
return serviceError;
else if (status.equals("PROCESSING"))
// do whatever you want to do if some other process or thread is
// already handling this. Maybe also return an error
return serviceError;
else if (myDao.testAndUpdateStatus(status, "PROCESSING"))
// You would probably want to re-introduce a transaction here, maybe
// by moving this block to its own method. Sorry for cutting a
// corner here trying to just demonstrate the lock idea, which needs
// to not be in a transaction.
try
myDao.insertRow();
myDao.updateStatus("COMPLETE");
sendAlert();
return serviceOK;
catch (Exception ex)
// What you do for the failure case will be very app specific. This
// is mostly to point out that you would want to set the status
// explicitly in the case of an error, to whatever is appropriate
myDao.updateStatus("COMPLETE")
return serviceError;
您需要锁定操作不是事务性的……重点是每个操作都是真正的原子操作。如果您真的需要事务语义,您会希望最终处理块以某种方式包装在事务中。我不想让交易部分恰到好处。我指出了一种通过使用数据库本身来控制同步和竞争条件的常用方法。这种机制不属于事务,只有在线程从testAndUpdateStatus
得到true
后才会启动。
这里的关键是testAndUpdateStatus()
只会设置状态,如果作为第一个参数传入的状态是当前状态,则返回true
。否则,它不会做任何事情并返回false
。这解决了竞争条件,即一个进程对状态进行采样,但随后另一个进程也对相同的值进行采样,然后您才能将状态设置为 "PROCESSING"
,最终两个进程处理相同的更新。两者之一将失败,因为当状态不再是该进程读取值时的状态时,数据库将导致操作失败。
请注意,这不仅适用于单个进程,还适用于跨进程,甚至是机器。
【讨论】:
评论不用于扩展讨论;这个对话是moved to chat。如果讨论取得任何成果(澄清的可能性等),请务必edit相应的答案。【参考方案4】:就我而言,我在构建 API 时将构建配置更改为发布。
【讨论】:
【参考方案5】:这两个调用都将打开不同的线程,因此具有不同的事务。除非你在其他地方有独立于数据库的东西可以告诉你一个线程正在使用这个资源(比如一个带有标志的文件),否则在我看来,唯一的另一种方法是同步你的代码块。
【讨论】:
这只适用于应用程序的单个实例,一旦你扩大规模,同步也无济于事。 是的,这是真的。同步并不能完全解决这个问题。它很可能需要一个单独的指示器来判断某个线程是否正在使用该资源。以上是关于服务同时调用两次的主要内容,如果未能解决你的问题,请参考以下文章