Preface
Recently, I am writing the product bargaining function of the company's APP products, one of which involves concurrent access. During self-testing, pressure testing was performed through the ApiFox interface management tool, and the "lock failed" scenario occurred when landing data. Thank you very much for your help in troubleshooting and solving this problem.
Problem description
In the concurrency interface, the main table data is first read, and after making business judgments, the data of its table is added and modified. In the case where serial execution is supposed to be performed, multiple request threads have read the same main table data, resulting in data processing exceptions. It is exactly what the preface says that "lock is invalid". (The locking operation is valid in actual situation)
Code reproduction
@RequestMapping("/test") @Transactional(rollbackFor = ) public String test() { ("ct_lock"); try { Map<String, Object> resultMap = ("select * from concurrent_read_uncommit"); int num = (("num").toString()); num++; ("update concurrent_read_uncommit set num = " + num); } finally { ("ct_lock"); } return "success"; }
- The minimum code is used for demonstration. The content in the Controller method body should be the code in the Service
- Redission encapsulated in DistributedLock
- By demonstrating the method of reading first and then modifying, the essence of this is to perform such operations, but there will be more business code (not demonstrating new cases)
In ApiFox, the num field value in the concurrent_read_uncommit table is 94 instead of 100.
Troubleshooting
1. Lock failed
Two new simple interfaces are written, the first interface is locked, and the thread sleeps for 30 seconds before releasing the lock. Another interface adds the same lock, and returns directly after printing a statement. Call the first interface first and call the second interface. Debug found that the lock is valid, and there is a lock key in redis. And when accessing the second interface, the thread is blocked in the locked line code.
2. Transaction isolation level
Query the default isolation level of database transactions:
select @@tx_isolation;
result
REPEATABLE-READ
It is the default RR level, which means that the data read multiple times in the same transaction will be the same and will not be read dirty data.
3. Modify Spring transaction propagation configuration
@Transactional(rollbackFor = , propagation = Propagation.REQUIRES_NEW)
It has nothing to do with this, it is impossible to fight. When I was thinking at that time, multiple concurrent requests entered the same transaction and read the data before they were not modified together. Think about it carefully:
- Transaction propagation configuration is generally used to make transaction decisions when calling between different transaction methods, whether to use a common transaction or a newly created transaction, or other methods to process it.
- The test method itself is the root method and no other transaction methods are called, so there is no need to configure transaction propagation configuration.
- Even if it is not in the same transaction, the same data that has not been submitted can still be found.
Solution
Call transaction methods in lock code block instead of locking them in transaction methods.
The reason is: In the concurrency situation, the execution speed is too fast, which is very likely to occur: the requesting thread does not have time to submit the transaction after releasing the lock, and another requesting thread is awakened at the locking point, and then reads the data that the transaction has not submitted. That is, dirty data is read, resulting in the effect of "lock failure".
Correct code:
@RequestMapping("/test2") public String test2() { ConcurrentTransactionalController proxyBean = (()); proxyBean.doTest2(); return "success"; } @Transactional(rollbackFor = ) public void doTest2() { ("ct_lock"); try { Map<String, Object> resultMap = ("select * from concurrent_read_uncommit"); int num = (("num").toString()); num++; ("update concurrent_read_uncommit set num = " + num); (500); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { ("ct_lock"); } }
- Another way to extract the transaction code that needs to be locked
- Locking is performed in the call method, and transaction annotations must be removed
- Because the transaction method is called in a non-transaction method, in order to ensure that the transaction takes effect, it is necessary to call it through the transaction proxy bean
This ensures that data not submitted by the transaction will not be read, and at the same time it has the exclusiveness of locks.
In fact, locks have always been valid. The essential reason is that Spring's transaction proxy bean blocks transaction code. We cannot control it manually, which means you cannot change the order of transaction code. This problem would not exist if the line of code that commits the transaction can be written before the lock is released. Therefore, this problem can also be solved through programming transactions. Spring also has code encapsulation for programming transactions. If it is not passed through programming transactions, it can only be implemented in disguise through the above code.
This is the end of this article about the analysis of the principle of Spring transactions. For more related Spring transaction content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!