简体   繁体   中英

Service Called Twice At Same Time

I have the following web service written in Java using Spring, I have wrapped it with the @Transactional annotation to ensure we can roll back if required.

It works fine, except for the scenario where the service is called twice, with the second call happening before the first call finishes.

In that scenario, because the first transaction is still running and hasn't yet committed to the DB, the second transaction will go through the full method, inserting a duplicate row, updating the status again, and calling sendAlert().

Here's the pseudo code.

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

How can I prevent the second transaction from going through before the first? Setting the isolation level as read uncommitted is not an option as the database doesn't support it.

You can use the database to do this, possibly even with the status column it seems like maybe you're already using. I'm not trying to handle all the details here...just to convey the idea. The HUGE thing here is that this mechanism works across threads, processes and even machines, any database I've ever come across, and you have nothing else to set up. No Redis, etc. when you get to the point where you want to scale to multiple instances:

You would create another state, and use a test-and-set operation:

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;
        }
    }
}

You would need to have the lock operation not be transactional...the whole point of it is that each op is truly atomic. If you really needed transactional semantics, you'd want the final processing block to be wrapped in a transaction somehow. I'm not trying to get the transactional part of this just right. I'm pointing out an often used way of controlling synchronization and race conditions by using the database itself. This mechanism falls outside of the transaction, which would only start once a thread had gotten true back from testAndUpdateStatus .

The key here is that testAndUpdateStatus() will only set the status and return true if the status passed in as the first parameter is the current status. Otherwise, it will not do anything and will return false . This solves the race condition where one process samples the status but then another process also samples the same value before you can set the status to "PROCESSING" , ending up with two processes handling the same update. One of the two will fail because the database will fail the operation when the status is no longer what it was when that process read the value.

Note that this will work not only in a single process, but across processes, and even machines.

Unfortunately Hibernate rely on the Database functionality in this case. Hibernate is designed to be able to support other services working with the exact same database.

There is a case where we can manualy set the future primary key by modify the generator-strategy. But this will not prevent content-"duplicates".

One solution might be having ReSTful API having a Rest-Entity that "request" the state of the database in a specific state.

In example:

  1. Request A to INSERT a Car-Entity.
  2. At the same time a second request B arrives to INSERT the Car-Entity.
  3. A is executed successfull.
  4. B is executed successfull.
  5. We have a duplicate.

To prevent this we could store a Car-Existence-Request.

  1. Car-Existence-Request A arives the Server.
  2. Car-Existence-Request B arives the Server.
  3. Both are stoed to the database
  4. Car-Existence-Request A finishes.
  5. Car-Existence-Request B finishes.
  6. Server try to store Car A - succeed.
  7. Server try to store Car B - fail -duplicate.
  8. Mark Cart-Existence-Request A as successfull.
  9. Mark Cart-Existence-Request B as failed.

Or just switch to PostGreSQL

From your question, I assume the update(..) is going to be invoked under some degree of concurrency.

I see a few problems with this approach of using an external data store for coordination. For default " Read Committed " isolation you will encounter what you have encountering now, however, even if you can use " Read Un-Committed " you will have a problem where the second transaction having read the dirty "COMPLETE" data, returns but the first transaction may still fail and rollback.

I propose a couple of approaches(I made a lot of assumptions of-course)

  1. Idempotency: By making the database updates idempotent, you don't have to worry about duplicate updates
  2. Compaction: If there is a possibility that latest records are always the correct you can let all the writes pass through but read only the latest records, this is akin to how Kafka does compaction internally

In my case, I changed build configuration to release when build API.

在此处输入图片说明

Both the calls will open different threads and hence have different transactions. Unless you have something at some place else that's independent of the database that can tell you a thread is using this resource (like a file with a flag), the only other way is to synchronize your block of code in my opinion.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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