concurrent transaction in grails resulting in data

2019-05-18 11:07发布

问题:

following is my code that is run on an api call.The below code is in a grails service which are transactional by default.But even after locking the row,I am getting this error : Message: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect).There is just one plan in database so for loop is run just once.but there are concurrent calls to api which are causing error.Please help me fix this

def updateAllPlans(def user,def percentComplete,def chapterId){
        def plans = Plan.findAllWhere(user:user)
        def chapter = Chapter.findById(chapterId)
        for(def plan:plans){
                def foundChapters = plan.psubject.ptopics.psubtopics.pchapters.chapter.flatten().contains(chapter)
                if(foundChapters){
                    plan.lock()
                    if(percentComplete=='0'){
                        plan.durationViewComplete = plan.durationViewComplete.plusMillis(chapter.duration.getMillisOfSecond())
                        plan.durationViewComplete = plan.durationViewComplete.plusSeconds(chapter.duration.getSecondOfMinute())
                        plan.durationViewComplete = plan.durationViewComplete.plusMinutes(chapter.duration.getMinuteOfHour())
                        plan.durationViewComplete = plan.durationViewComplete.plusHours(chapter.duration.getHourOfDay())
                    }else{
                        plan.durationComplete = plan.durationComplete.plusMillis(chapter.duration.getMillisOfSecond())
                        plan.durationComplete = plan.durationComplete.plusSeconds(chapter.duration.getSecondOfMinute())
                        plan.durationComplete = plan.durationComplete.plusMinutes(chapter.duration.getMinuteOfHour())
                        plan.durationComplete = plan.durationComplete.plusHours(chapter.duration.getHourOfDay())
                    }

                    plan.save(flush:true, failOnError:true)
                }
        }
    }

回答1:

You only lock the plan after it has been read. So multiple threads can read the plan concurrently. Then one thread locks it and updates it, and the other thread locks it and updates it as well. But they both read it concurrently, so both read the same data, with the same version inside:

thread 1: read plan, with version = 3
thread 2: read plan, with version = 3
thread 1: lock plan
thread 1 : update and save plan. version becomes 4
thread 2 : lock plan
thread 2 : update and save plan. version in memory is 3, but version in database is 4, so an exception is thrown

You need to lock when reading (which is pessimistic locking), as documented:

def airport = Airport.findByName("Heathrow", [lock: true])

Then the second thread will have to wait for the first one to have saved and committed its transaction before reading the same plan.

This has all the disadvantages of pessimistic locking though: the throughput might be reduced because only one transaction at a time can use the plan, which is precisely what optimistic locking tries to avoid. The cost is that you can get exceptions and have to deal with them (by retrying, displaying an error message, or whatever is the best answer depending on the situation)