Solve the problem of Spring transaction and lock conflict invalidation

Posted by gdure on Sat, 15 Jan 2022 02:47:52 +0100

background

During business, it is necessary to ensure that a user can only insert one piece of data into the wallet table. When the service adds the synchronize lock and searches before insertion, one day it suddenly finds that multiple pieces of data appear in a short time. Through the log, it is found that there are multiple identical requests in a short time. We guess it is caused by high concurrency of multiple threads.

reflection

We obviously synchronized the lock before inserting it, but the result shows that the lock may have failed. During this period, we changed the synchronized lock to redis, and the distributed lock also failed. So I looked up the data and found that the synchronized lock failed under the spring transaction. Therefore, I understand the spring transaction process.

Execution process of spring transaction

Code execution process: open transaction lock execute business unlock commit transaction

  • Reason for lock invalidation: spring transactions will open the transaction before executing the method, then lock, execute code logic, unlock and commit the transaction. The following occurs:

When the first thread is unlocked, the transaction has not been committed. The second thread has started the transaction and locked. At this time, the data read is not up-to-date, resulting in business errors.

And mysql reads repeatedly by default, so the above problem occurs.

1. Invalidation of spring transaction @ Transactional and synchronized

  • Business code

 @Transactional
    public synchronized void insert(int index) throws InterruptedException {
        System.out.println(index);
        Test test = testMapper.selectById(1);
        System.out.println(test);
        test.setNum(test.getNum()+1);
        testMapper.updateById(test);
        System.out.println("Thread:"+index+" Execution complete");
    }
  • Test code

@Test
public void test() throws InterruptedException {
    CountDownLatch countDownLatch=new CountDownLatch(1);
    for (int i=0;i<100;i++){
        int k=i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatch.await();
                    walletService.insert(k);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
​
            }
        }).start();
    }
    countDownLatch.countDown();
    //The main thread waits for the execution of the child thread to complete
    Thread.currentThread().join();
}
  • test result

background

During business, it is necessary to ensure that a user can only insert one piece of data into the wallet table. When the service adds the synchronize lock and searches before insertion, one day it suddenly finds that multiple pieces of data appear in a short time. Through the log, it is found that there are multiple identical requests in a short time. We guess it is caused by high concurrency of multiple threads.

reflection

We obviously synchronized the lock before inserting it, but the result shows that the lock may have failed. During this period, we changed the synchronized lock to redis, and the distributed lock also failed. So I looked up the data and found that the synchronized lock failed under the spring transaction. Therefore, I understand the spring transaction process.

Execution process of spring transaction

Code execution process: open transaction lock execute business unlock commit transaction

  • Reason for lock invalidation: spring transactions will open the transaction before executing the method, then lock, execute code logic, unlock and commit the transaction. The following occurs:

When the first thread is unlocked, the transaction has not been committed. The second thread has started the transaction and locked. At this time, the data read is not up-to-date, resulting in business errors.

And mysql reads repeatedly by default, so the above problem occurs.

1. Invalidation of spring transaction @ Transactional and synchronized

  • Business code

 @Transactional
    public synchronized void insert(int index) throws InterruptedException {
        System.out.println(index);
        Test test = testMapper.selectById(1);
        System.out.println(test);
        test.setNum(test.getNum()+1);
        testMapper.updateById(test);
        System.out.println("Thread:"+index+" Execution complete");
    }
  • Test code

@Test
public void test() throws InterruptedException {
    CountDownLatch countDownLatch=new CountDownLatch(1);
    for (int i=0;i<100;i++){
        int k=i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatch.await();
                    walletService.insert(k);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
​
            }
        }).start();
    }
    countDownLatch.countDown();
    //The main thread waits for the execution of the child thread to complete
    Thread.currentThread().join();
}
  • test result

Before test

Expected result: num=100

After test:

  • Analysis of test results: it can be seen from the results that the database is repeatedly read under spring transactions. Cause the final business code to become invalid.

  • The reason for failure: the key reason for Synchronized failure: because Synchronized locks the current calling method object, and Spring AOP processes transactions to generate a proxy object, and opens the transaction before the proxy object executes the method and transmits the transaction completed by the method. Therefore, the opening and submission of transactions is not within the scope of Synchronized locking. The reason why the synchronization lock fails is that when a (thread) finishes executing the insertSelective() method, it will release the synchronization lock and commit the transaction. However, before a (thread) has committed the transaction, B (thread) executes the selectById() method and commits the transaction with a (thread) after execution. At this time, thread safety problems will occur.

2. Invalidation of spring transaction and distributed lock

  • Business code

 @Transactional
    public void insertByRedisLock(int index) throws InterruptedException {
        RLock lock = redissonClient.getLock("wxpay");
        lock.lock();
        try {
            System.out.println(index);
            Test test = testMapper.selectById(1);
            System.out.println(test);
            test.setNum(test.getNum()+1);
            testMapper.updateById(test);
            System.out.println("Thread:"+index+" Execution complete");
        }finally {
            lock.unlock();
        }
    }
  • Test code

    The test code is the same as above, basically no difference

  • test result

  • Database results

Summary

Through the synchronized lock and Redis lock of java, the data of the database will be repeatedly read under the spring transaction. The mysql database is repeatedly read by default. However, we found that the Redis lock is better than the synchronized lock, but there are still problems to be solved.

terms of settlement

Through the experimental code, we found that the lock is included in the spring transaction. After the lock is released and before the transaction is committed, the dirty data has been read from the database, resulting in that the read data is not the latest data.

  • The core idea of the solution: since locks cannot be used under transactions, let's separate locks from transactions. This makes it thread safe to include transactions in a lock environment.

    • Method 1: extract the synchronized and Redis locks to the controller layer without any transactions.

    • Method 2: create a new transaction free method under service to extract transaction codes separately. Directly call the transacted method in the non transacted method, which can still ensure thread safety.

synchronized method

@Transactional
public  void insert(int index) throws InterruptedException {
    System.out.println(index);
    Test test = testMapper.selectById(1);
    System.out.println(test);
    test.setNum(test.getNum()+1);
    testMapper.updateById(test);
    System.out.println("Thread:"+index+" Execution complete");
}
​
@Override
public synchronized void noTxinsert(int index) throws InterruptedException {
    this.insert(index);
}
​
  • Result analysis: from the results, the results are in line with expectations. It's exactly 100

Redis distributed lock method

@Override
    @Transactional
    public void insertByRedisLock(int index) throws InterruptedException {
        System.out.println(index);
        Test test = testMapper.selectById(1);
        System.out.println(test);
        test.setNum(test.getNum()+1);
        testMapper.updateById(test);
        System.out.println("Thread:"+index+" Execution complete");
    }
​
    @Override
    public void noTxinsertByRedisLock(int index) throws InterruptedException {
        RLock lock = redissonClient.getLock("wxpay");
        lock.lock();
        try {
            this.insertByRedisLock(index);
        }finally {
            lock.unlock();
        }
    }
  • Test results:

summary

In the case of high concurrency, if the spring transaction contains locks, the read data will not be the latest. Therefore, the lock should contain the whole transaction, so as to ensure thread safety and realize the lock, and there will be no repeated data reading and dirty reading.

Topics: Java Spring