服务同时调用两次

时间:2019-04-09 16:49:27

标签: java spring transactional

我有以下使用Spring用J​​ava编写的Web服务,我用@Transactional注释将其包装起来,以确保在需要时可以回滚。

除了两次调用该服务,第二次调用发生在第一个调用完成之前的情况之外,它都工作正常。

在这种情况下,由于第一个事务仍在运行并且尚未提交给DB,因此第二个事务将通过full方法,插入重复的行,再次更新状态并调用sendAlert()。

这是伪代码。

@Transactional
public ServiceResponse update(ServiceRequest serviceRequest) {
....
if (myDao.getStatus() == "COMPLETE") {
 return serviceError;
}
 myDao.insertRow();
 myDao.updateStatus("COMPLETE");
 sendAlert();
}

如何防止第二笔交易在第一笔交易之前进行?不能将隔离级别设置为“读未提交”,因为数据库不支持隔离级别。

4 个答案:

答案 0 :(得分:0)

您可以使用数据库来执行此操作,即使状态列似乎已经在使用中也可能如此。我并不是要在这里处理所有细节...只是为了传达想法。这里的巨大之处在于,这种机制可以在线程,进程甚至机器,我遇到的任何数据库中运行,而您无需设置其他任何东西。当您想扩展到多个实例时,无需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;
        }
    }
}

您需要使锁定操作不是事务性的...的重点是每个操作都是真正的原子操作。如果您确实需要事务语义,则希望将最终处理块以某种方式包装在事务中。我并不是想让交易中的事情恰到好处。我指出了一种使用数据库本身来控制同步和竞争条件的常用方法。该机制不属于事务处理,事务仅在线程从true返回testAndUpdateStatus后才开始。

此处的关键是testAndUpdateStatus()仅会设置状态,并且如果作为第一个参数传入的状态是当前状态,则返回true。否则,它将不执行任何操作,并返回false。这解决了争用条件,其中一个进程对状态进行采样,然后另一个进程对状态值进行采样,然后再将状态设置为"PROCESSING",最后两个进程处理相同的更新。两者之一将失败,因为当状态不再是该进程读取该值时的状态时,数据库将使操作失败。

请注意,这不仅适用于单个进程,而且适用于跨进程,甚至适用于机器。

答案 1 :(得分:0)

不幸的是,在这种情况下,Hibernate依赖于数据库功能。 Hibernate设计为能够支持使用完全相同的数据库的其他服务。

在某些情况下,我们可以通过修改生成器策略来手动设置将来的主键。但这并不能防止内容“重复”。

一种解决方案可能是使用具有Rest-Entity的ReSTful API,该实体“请求”处于特定状态的数据库状态。

例如:

  1. 请求A插入汽车实体。
  2. 同时,第二个请求B到达插入汽车实体。
  3. A执行成功。
  4. B执行成功。
  5. 我们有一个副本。

为防止这种情况,我们可以存储汽车存在请求。

  1. 汽车存在请求A到达服务器。
  2. 汽车存在请求B到达服务器。
  3. 两者都存储在数据库中
  4. 汽车存在请求A完成。
  5. 汽车存在请求B完成。
  6. 服务器尝试存储Car A-成功。
  7. 服务器尝试存储Car B-重复失败。
  8. 将购物车存在请求A标记为成功。
  9. 将购物车存在请求B标记为失败。

或者只是切换到PostGreSQL

答案 2 :(得分:0)

根据您的问题,我假设将在某种程度的并发性下调用update(..)

我发现使用外部数据存储进行协调的这种方法存在一些问题。对于默认的“ 读取已提交”隔离,您将遇到现在遇到的问题,但是,即使您可以使用“ 读取未提交”,也会遇到问题:已读取脏的“ COMPLETE”数据的第二个事务返回,但是第一个事务仍可能失败并回滚。

我提出了几种方法(我对课程做了很多假设)

  1. 幂等:通过使数据库更新成为幂等,您不必担心重复更新
  2. 压缩:如果有可能最新记录总是正确的,那么您可以让所有写入通过,但只能读取最新记录,这类似于Kafka在内部进行压缩的方式

答案 3 :(得分:-1)

这两个调用将打开不同的线程,因此具有不同的事务。除非您在某个地方有一些独立于数据库的东西,可以告诉您线程正在使用此资源(例如带有标志的文件),否则,我认为唯一的其他方法是同步代码块。

相关问题