Infinite recursion caused by MyBatis level-1 cache and mybatis level-1 Cache
Introduction:
Recently, I participated in a special offer collection activity in the project. When multiple users receive the same offer, I used database lock to control concurrency. The initial assumption was: if multiple people receive a token at the same time, the first person who arrives will receive the token successfully, and other people will continue to find whether there are any remaining tokens. If yes, continue to receive the token; otherwise, the receipt will fail. In implementation, I first used a recursive method to search for sequences. In actual tests, we found infinite recursion, only through degug and materials can we find that this is caused by the primary cache of mybatis. We will share with you the following questions.
1. Knowledge reserve
Brief Introduction:
Mybatis
Level-1 cache: enabled by default. The cache is valid in the current session. After the sqlSession commit (), close (), and merge AchE () operations are executed, the cache is cleared.
Level-2 Cache: It must be manually enabled and global cache is related to mapper namespace.
See http://www.mamicode.com/info-detail-890951.html for details
2. Sample Code
The following is an auxiliary method for getting the discount coupon-randomly selects a discount code and calls the public method of this auxiliary method to start the transaction. During the actual test, it is found that infinite recursion occurs when the database has only one coupon and multiple users receive it at the same time. The Code is as follows:
1/** 2 * randomly select a promotional code 3*4 * @ param codePrefix 5 * prefix 6 * @ return promotional Code 9 */10 private String randExtractOneTicketCode (String mobile, string codePrefix) {11 List <String> notExchangeCodeList = yzTicketCodeDaoExt. getTicketCodeList (codePrefix, 12 MobileServiceConstants. TICKET_CODE_STATUS_NOT_EXCHANGE); 13 logger.info ("getting discount coupons >>> available discount coupons {}", CollectionUtils. size (notExchangeCodeList); 14 if (CollectionUtils. I SEmpty (notExchangeCodeList) {15 logger. warn ("get discount >>> discount coupon {} has been received", codePrefix); 16 throw new YzRuntimeException (MobileServiceConstants. TICKET_NOT_REMAINDER); 17} 18 19 int randomIndex = random. nextInt (notExchangeCodeList. size (); // Random Index 20 String ticketCode = notExchangeCodeList. get (randomIndex); // The randomly selected discount code 21 YzTicketCode ticketCodeObj = yzTicketCodeDaoExt. getByCode (ticketCode); 22 if (ticketCodeObj = Null23 | ticketCodeObj. getStatus ()! = MobileServiceConstants. TICKET_CODE_STATUS_NOT_EXCHANGE) {24 // If the discount coupon has been used 25 logger.info ("get discount coupon >>> discount coupon code {} does not exist or is used", ticketCode ); 26 return randExtractOneTicketCode (String mobile, String codePrefix); // recursive search 27} 28/* 29 * update discount code status 30 */31 ticketCodeObj. setExchangeTime (Calendar. getInstance (). getTime (); 32 ticketCodeObj. setStatus (MobileServiceConstants. TICKET_CODE_STATUS_HAD_EXCHANGED); 33 ticketCodeObj. setMobile (mobile); 34 int updateCnt = yzTicketCodeDaoExt. update4Receive (ticketCodeObj); 35 if (updateCnt <= 0) {36 // The optimistic lock does not affect the row, indicating that the update failed, it may be that this token does not exist or 37 logger.info has been used ("get discount coupon >>> discount coupon code {} does not exist or has been used", ticketCode); 38 return randExtractOneTicketCode (String mobile, string codePrefix); // recursive search 39}; 40 return ticketCode; 41}
Debug found that the query results executed in line 1 were cached by mybatis, so a token can be obtained every time, but in fact this token has already been received by other users, resulting in infinite recursion.
3. Solution
1) for a programmatic transaction, obtain the sqlSession through transactionManager, and then clear the first-level cache through the commit AchE () method of the sqlSession.
2) Because Spring declarative transactions are used in the project, and the concurrency is not high, considering the complexity reduction, the system prompts the user to be busy.
/*** Randomly select a coupon code ** @ param codePrefix * prefix * @ return coupon code * @ throws YzRuntimeException * if no coupon is available */private String randExtractOneTicketCode (String mobile, string codePrefix) {List <String> notExchangeCodeList = yzTicketCodeDaoExt. getTicketCodeList (codePrefix, MobileServiceConstants. TICKET_CODE_STATUS_NOT_EXCHANGE); logger.info ("getting discount coupons >>> available discount coupons {}", CollectionUtils. size (notExchangeCodeList); if (CollectionUtils. isEmpty (notExchangeCodeList) {logger. warn ("Get Discount offer >>> Discount offer {} has been received", codePrefix); throw new YzRuntimeException (MobileServiceConstants. TICKET_NOT_REMAINDER);} int randomIndex = random. nextInt (notExchangeCodeList. size (); // Random Index String ticketCode = notExchangeCodeList. get (randomIndex); // The randomly selected discount code YzTicketCode ticketCodeObj = yzTicketCodeDaoExt. getByCode (ticketCode); if (ticketCodeObj = Null | ticketCodeObj. getStatus ()! = MobileServiceConstants. TICKET_CODE_STATUS_NOT_EXCHANGE) {// If the discount coupon has been used logger.info ("get discount coupon >>> discount coupon code {} does not exist or is used", ticketCode); throw new YzRuntimeException (MobileServiceConstants. TICKET_SYSTEM_BUSY);}/** update discount code status */ticketCodeObj. setExchangeTime (Calendar. getInstance (). getTime (); ticketCodeObj. setStatus (MobileServiceConstants. TICKET_CODE_STATUS_HAD_EXCHANGED); ticketCodeObj. setMobile (mobile); int updateCnt = yzTicketCodeDaoExt. update4Receive (ticketCodeObj); if (updateCnt <= 0) {// The optimistic lock does not affect the row, indicating that the update failed, it may be that this token does not exist or logger.info has been used ("get discount coupon >>> discount coupon code {} does not exist or has been used", ticketCode); throw new YzRuntimeException (MobileServiceConstants. TICKET_SYSTEM_BUSY) ;}; return ticketCode ;}
Summary:
Currently, most projects use clusters. it is not suitable to use the concurrency mechanism provided by java to control concurrency. Database locks and Redis operations are commonly used. The above Code uses optimistic locks of databases, compared with the tragic lock, optimistic Locks require the compilation of external algorithms. Wrong external algorithms and abnormal recovery may easily lead to unknown errors. Therefore, careful design and rigorous testing are required.