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.