Optimistic locking you should know - a means to efficiently control thread safety

1. Background

Recently, I have been modifying Seatasome problems of thread concurrency, and I will summarize some of these experiences for you. Briefly describe this problem first Seata. There is a concept of global transaction in this distributed transaction framework. In most cases, the process of global transaction is basically advanced sequentially without concurrency problems, but in some extreme cases, it will Multi-threaded access occurred causing our global transactions to be handled incorrectly. As shown in the following code: In our global transaction commitphase, there is a code as follows:

    if (status == GlobalStatus.Begin) {
        globalSession.changeStatus(GlobalStatus.Committing);
    }

The code is somewhat omitted, that is, first determine whether the status status is the Begin status, and then change the status to Committing.

In our global transaction rollback phase, there is a code as follows:

if (status == GlobalStatus.Begin) {
            globalSession.changeStatus(GlobalStatus.Rollbacking);
        }

Similarly, part of the code is omitted. Here, first determine whether statusthe state is begin, and then change it to Rollbacking. There is Seatano thread synchronization method in the code here. If these two logics are executed at the same time (generally not, but it may occur in extreme cases), our results will have unexpected errors. And what we have to do is to solve the problem of concurrency in this extreme case.

2. Pessimistic lock

For this kind of concurrency problem, I believe that the first thing that everyone thinks of is locking. In Java, we generally use the following two methods to lock:

  • Synchronized
  • ReentrantLock

We can use Synchronizedor ReentrantLockto lock, we can modify the code to the following logic:

synchronized:

synchronized(globalSession){
            if (status == GlobalStatus.Begin) {
                globalSession.changeStatus(GlobalStatus.Rollbacking);
            }
        }

ReentrantLockTo lock:

 reentrantLock.lock();
 try {
    if  (status == GlobalStatus.Begin) {
    globalSession.changeStatus(GlobalStatus.Rollbacking);
        }
    }finally {
            reentrantLock.unlock();
    }

This kind of locking is relatively simple, Seataand Go-Serverit is currently implemented in this way. However, this implementation scenario ignores the situation we mentioned above, that is, in extreme cases, that is, there may be no concurrency problem in 99.9% of the cases, and only the case of %0.1 may cause this concurrency problem. Although our pessimistic lock takes a relatively short time to lock, it is still not enough in this high-performance middleware, so we introduce our optimistic lock.

3. Optimistic locking

When it comes to optimistic locking, many friends will think of optimistic locking in the database. Imagine if the above logic is in the database and does not use optimistic locking to do it, we will have the following pseudo-code logic:

select * from table where id = xxx for update;
if(status == begin){
    //do other thing
    update table set status = rollbacking;
}

The above code can be seen in a lot of our business logic. There are two small problems with this code:

  1. The transaction is large. Since we lock our data as soon as we come up, it must be in a transaction. If some time-consuming logic is interspersed between our query and update, our transaction will be large. Since each of our transactions will occupy a database connection, it is easy to have insufficient database connection pools when the traffic is high.
  2. Locking data takes a long time. In our entire transaction, a row lock is added to this data. If other transactions want to modify this data, it will block and wait for a long time.

So in order to solve the above problems, in many scenarios where there is little competition, we adopt the optimistic locking method. We add a field version to the database to represent the version number, and we modify the code as follows:

select * from table where id = xxx ;
if(status == begin){
    //do other thing
    int result = (update table set status = rollbacking where version = xxx);
    if(result == 0){
        throw new someException();
    }
}

Here, our query statement no longer has for update, and our transaction is only reduced to the update sentence. We judge by the version we queried in the first sentence. If the number of updated rows in our update is 0, then it proves that Other affairs modified him. Here you can throw an exception or do something else.

It can be seen from this that we use optimistic locking to solve both the problems of larger transactions and longer locks, but the corresponding cost is that if the update fails, we may throw an exception or take some other remedial measures, and Our pessimistic locks are all constraining until we execute our business. Therefore, we must use optimistic locking here only when the concurrent processing of a certain piece of data is relatively small.

3.1 Optimistic locking in code

We talked about optimistic locking in the database above. Many people are asking, without a database, how to implement optimistic locking in our code? Familiar synchronizedstudents must know synchronizedthat it has been optimized after Jdk1.6, and a model of lock expansion has been introduced:

  • Biased lock: As the name implies, a lock that is biased towards a thread is suitable for a thread that can acquire the lock for a long time.
  • Lightweight lock: If the biased lock acquisition fails, then CAS spin is used to complete, and the lightweight lock is suitable for threads to alternately enter the critical section.
  • Heavyweight lock: After the spin fails, the heavyweight lock strategy will be adopted, and our thread will block and hang.

In the above level lock model, the threads that are applicable to lightweight locks enter the critical section alternately, which is very suitable for our scenario, because our global transaction is generally not a single thread processing the transaction all the time (of course, it can also be optimized into this model, but the design will be more complicated), in most cases, our global transaction will be alternately entered by different threads to process this transaction logic, so we can learn from the idea of ​​​​the lightweight lock CAS spin to complete our code level. spin lock. Some friends here may ask why not use synchronized? After the actual measurement, the CAS spin performance we implemented by ourselves in the alternate entry into the critical section is the highest, and there is no timeout mechanism for synchronized, which is inconvenient for us to deal with abnormal situations.

 class GlobalSessionSpinLock {
        
        private AtomicBoolean globalSessionSpinLock = new AtomicBoolean(true);

        public void lock() throws TransactionException {
            boolean flag;
            do {
                flag = this.globalSessionSpinLock.compareAndSet(true, false);
            }
            while (!flag);
        }


        public void unlock() {
            this.globalSessionSpinLock.compareAndSet(false, true);
        }
    }
  // method rollback  
  void rollback(){
    globalSessionSpinLock.lock();
    try {
        if  (status == GlobalStatus.Begin) {
        globalSession.changeStatus(GlobalStatus.Rollbacking);
            }
    }finally {
        globalSessionSpinLock.unlock();
    }
  }
 

We have CASimplemented an optimistic lock with a simple method above, but this optimistic lock has a small disadvantage that once a competition occurs, it cannot be expanded to a pessimistic lock blocking waiting, and there is no expiration and timeout, which may take up a lot of ours CPU. We continue to further optimize:

        public void lock() throws TransactionException {
            boolean flag;
            int times = 1;
            long beginTime = System.currentTimeMillis();
             do {
                long restTime -= (GLOBAL_SESSOION_LOCK_TIME_OUT_MILLS - beginTime);
                if (restTime <= 0){
                    throw new TransactionException(TransactionExceptionCode.FailedLockGlobalTranscation);
                }
                // Pause every PARK_TIMES_BASE times,yield the CPU
                if (times % PARK_TIMES_BASE == 0){
                    // Exponential Backoff
                    long backOffTime =  PARK_TIMES_BASE_NANOS << (times/PARK_TIMES_BASE);
                    long parkTime = backOffTime < restTime ? backOffTime : restTime;
                    LockSupport.parkNanos(parkTime);
                }
                flag = this.globalSessionSpinLock.compareAndSet(true, false);
                times++;
            }
            while (!flag);
        }

The above code makes the following optimizations:

  • A timeout mechanism is introduced. Generally speaking, a timeout mechanism must be done well to lock the critical area, especially in this kind of middleware with high performance requirements.
  • The lock expansion mechanism is introduced. If there is no loop for a certain number of times, if the lock cannot be obtained, the thread will be suspended parkTimefor a time. After the suspension, the loop will continue to be obtained. If it cannot be obtained again, we will perform an exponential backoff form for our parkTime. , increasing our suspend time gradually until it times out.

Summarize

From our processing of concurrency control, if we want to achieve a goal, there are various ways to achieve it. We need to choose the appropriate method and the most efficient means to accomplish it according to different scenarios and different conditions. our aim. This article does not explain too much about the principle of pessimistic locking. If you are interested here, you can come down and check the information by yourself. After reading this article, if you can only remember one thing, then please remember to not forget to consider optimism when implementing thread concurrency safety. Lock.

Finally, this article was included in the JGrowingconcurrent programming article, a comprehensive and excellent Java learning route jointly built by the community. If you want to participate in the maintenance of open source projects, you can build it together. The github address is: https://github .com/javagrowing/JGrowing Please give a little star.

If you think this article is helpful to you, your attention and forwarding are the greatest support for me, O(∩_∩)O:

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324134668&siteId=291194637