DEADLOCK in MYSQL batch insert concurrent scenario

1. Background

On the morning of 2023-10-12 (Thursday), Yamei in the group reported that a user-generated label reported a deadlock exception.

2. Check exception logs

Find the error log reported at that time

The specific exception information is as follows

server-provider-info-2023-10-12.0.log:2023-10-12 09:40:50.593 [TID:bf623bded189486cbb0b6a64d81b64b4.357.16970748504097047] [4ed7b7943a8a47de912e4b644d70285e] [SimpleAsyncTaskExecutor-5080] INFO com. emax.user.user.provider.UserTagAPIImpl:? - Signing completed, user tag information updated abnormally, user id[1712282059297107970]org .apache.ibatis.exceptions.PersistenceException:
server-provider-info-2023-10-12.0.log-### Error flushing statements. Cause: org.apache.ibatis.executor.BatchExecutorException: com.emax.user.user.mapper.UserTagMapper.insert (batch index # 1) failed. Cause: java.sql.BatchUpdateException: Deadlock found when trying to get lock; try restarting transaction
server-provider-info-2023-10-12.0.log-### Cause: org.apache.ibatis.executor.BatchExecutorException: com.emax.user.user.mapper.UserTagMapper.insert (batch index #1) failed. Cause: java.sql.BatchUpdateException: Deadlock found when trying to get lock; try restarting transaction
server-provider-info-2023-10-12.0.log- at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
server-provider-info-2023-10-12.0.log- at org.apache.ibatis.session.defaults.DefaultSqlSession.flushStatements(DefaultSqlSession.java:254)
server-provider-info-2023-10-12.0.log- at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.saveBatch(ServiceImpl.java:127)
server-provider-info-2023-10-12.0.log- at com.baomidou.mybatisplus.extension.service.IService.saveBatch(IService.java:58)
server-provider-info-2023-10-12.0.log- at com.baomidou.mybatisplus.extension.service.IService$$FastClassBySpringCGLIB$$f8525d18.invoke(<generated>)
server-provider-info-2023-10-12.0.log- at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
server-provider-info-2023-10-12.0.log- at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
server-provider-info-2023-10-12.0.log- at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
server-provider-info-2023-10-12.0.log- at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294)
server-provider-info-2023-10-12.0.log- at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
server-provider-info-2023-10-12.0.log- at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
server-provider-info-2023-10-12.0.log- at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
server-provider-info-2023-10-12.0.log- at com.emax.user.user.service.UserTagManager$$EnhancerBySpringCGLIB$$67ca0067.saveBatch(<generated>)
server-provider-info-2023-10-12.0.log- at com.emax.user.user.provider.UserTagAPIImpl.addUserTagByUser(UserTagAPIImpl.java:236)
server-provider-info-2023-10-12.0.log- at com.emax.user.user.provider.UserTagAPIImpl.addTagAfterSign(UserTagAPIImpl.java:116)
server-provider-info-2023-10-12.0.log- at com.emax.user.user.provider.UserTagAPIImpl$$FastClassBySpringCGLIB$$2547ce26.invoke(<generated>)
server-provider-info-2023-10-12.0.log- at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
server-provider-info-2023-10-12.0.log- at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
--
server-provider-info-2023-10-12.0.log- at io.seata.rm.datasource.StatementProxy.lambda$executeBatch$9(StatementProxy.java:129)
server-provider-info-2023-10-12.0.log- at io.seata.rm.datasource.exec.ExecuteTemplate.execute(ExecuteTemplate.java:73)
server-provider-info-2023-10-12.0.log- at io.seata.rm.datasource.exec.ExecuteTemplate.execute(ExecuteTemplate.java:51)
server-provider-info-2023-10-12.0.log- at io.seata.rm.datasource.StatementProxy.executeBatch(StatementProxy.java:129)
server-provider-info-2023-10-12.0.log- at sun.reflect.GeneratedMethodAccessor1346.invoke(Unknown Source)
server-provider-info-2023-10-12.0.log- at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
server-provider-info-2023-10-12.0.log- at java.lang.reflect.Method.invoke(Method.java:498)
server-provider-info-2023-10-12.0.log- at org.apache.ibatis.logging.jdbc.PreparedStatementLogger.invoke(PreparedStatementLogger.java:78)
server-provider-info-2023-10-12.0.log- at com.sun.proxy.$Proxy501.executeBatch(Unknown Source)
server-provider-info-2023-10-12.0.log- at com.baomidou.mybatisplus.core.executor.MybatisBatchExecutor.doFlushStatements(MybatisBatchExecutor.java:132)
server-provider-info-2023-10-12.0.log- ... 71 common frames omitted
server-provider-info-2023-10-12.0.log-Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
server-provider-info-2023-10-12.0.log- at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123)
server-provider-info-2023-10-12.0.log- at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
server-provider-info-2023-10-12.0.log- at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
server-provider-info-2023-10-12.0.log- at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970)
server-provider-info-2023-10-12.0.log- at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1109)
server-provider-info-2023-10-12.0.log- at com.mysql.cj.jdbc.ClientPreparedStatement.executeBatchSerially(ClientPreparedStatement.java:849)
server-provider-info-2023-10-12.0.log- ... 89 common frames omitted
server-provider-info-2023-10-12.0.log-

3. Code location

View the code scene where Deadlock occurred.

code show as below:

 /**
     * Add user tags
     * @param user user information
     * @param tagNameList tag collection
     */
    private void addUserTagByUser(User user, List<String> tagNameList){
        log.info("User adds tag input parameter information, current user id[{}], tag information[{}]", user.getUserId(), JSON.toJSONString(tagNameList));
        Set<String> existTags = new HashSet<>();
        final List<UserTag> userTagList = userTagManager.listUserTagByUserId(user.getUserId());
        if(CollectionUtils.isNotEmpty(userTagList)){
            existTags = userTagList.stream().map(UserTag::getTagName).collect(Collectors.toSet());
        }
        //Traverse the collection. If the old data does not exist, add it directly. If it exists, determine whether the current data is in the old data.
        List<UserTag> newInsertList = new ArrayList<>();
        for(String name : tagNameList){
            if (StringUtils.isNotBlank(name) & amp; & amp; existTags.add(name)) {
                newInsertList.add(userTagManager.buildEntity(user.getUserId(), user.getIdcardNo(), name));
            }
        }
        userTagManager.saveBatch(newInsertList);
    }

Observing the code, the general logic is to first query all tag information based on the user ID, calculate the user tags that need to be added, and store them in batches. Among them, the user_tag table stores user tag data. The two key fields user_id (user id) and tag_name (tag) have unique indexes, and the inserted data is mainly these two fields.

It is not easy to tell from this code how a deadlock occurs. It is speculated that multiple threads operate the same data at the same time and concurrent insertion causes deadlock.

4. Concurrent operation log

Query method input log on elk. Sure enough, at the time node when the exception occurred, two logs inserting the same data were found. At the same time point, two traceIds show that concurrent calls occurred at that time.

I found the operation log of another thread at that time. There was no exception in the log and the data was successfully entered into the database. See screenshot below.

5. Database deadlock log verification speculation

After going to work that afternoon, I asked the operation and maintenance brother for help to find the deadlock log.

Execute the command show ENGINE INNODB status

The information obtained is as follows

==================== ==================
2023-10-12 13:49:17 0x7fea5ae87700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 61 seconds
------------------
BACKGROUND THREAD
------------------
srv_master_thread loops: 3123131 srv_active, 0 srv_shutdown, 7715544 srv_idle
srv_master_thread log flush and writes: 10838675
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 3905404
OS WAIT ARRAY INFO: signal count 27617015
RW-shared spins 0, rounds 21456656, OS waits 2772950
RW-excl spins 0, rounds 41132727, OS waits 487320
RW-sx spins 227170, rounds 4363782, OS waits 66152
Spin rounds per wait: 21456656.00 RW-shared, 41132727.00 RW-excl, 19.21 RW-sx
--------------------------
LATEST DETECTED DEADLOCK
--------------------------
2023-10-12 09:40:50 0x7fecedb1e700
*** (1) TRANSACTION:
TRANSACTION 404898333, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 11375251, OS thread handle 140644580488960, query id 1031478425 10.128.0.160 emax_base update
INSERT INTO emax_user_tag (user_id,
id_card_no,
tag_name,
create_time,
update_time ) VALUES ( 1712282059297107970,
'A978B69811B82F4B08F929D26B360F2D980CDCB5F419F1DD46527FB90BE1E5A9',
'Marketing',
'2023-10-12 09:40:50.481',
'2023-10-12 09:40:50.481' )
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1155 page no 65295 n bits 312 index uniq_user_id_tag_name of table `emax_base`.`emax_user_tag` trx id 404898333 lock mode S waiting
Record lock, heap no 242 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 97c33f75fbadb002; asc ?u ;;
 1: len 12; hex e5b882e59cbae68ea8e5b9bf; asc ;;
 2: len 8; hex 000000000044c0da; asc D ;;

*** (2) TRANSACTION:
TRANSACTION 404898332, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 11375224, OS thread handle 140655576868608, query id 1031478426 10.128.0.160 emax_base update
INSERT INTO emax_user_tag (user_id,
id_card_no,
tag_name,
create_time,
update_time ) VALUES ( 1712282059297107970,
'A978B69811B82F4B08F929D26B360F2D980CDCB5F419F1DD46527FB90BE1E5A9',
'Information Technology Services',
'2023-10-12 09:40:50.481',
'2023-10-12 09:40:50.481' )
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 1155 page no 65295 n bits 312 index uniq_user_id_tag_name of table `emax_base`.`emax_user_tag` trx id 404898332 lock_mode X locks rec but not gap
Record lock, heap no 242 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 97c33f75fbadb002; asc ?u ;;
 1: len 12; hex e5b882e59cbae68ea8e5b9bf; asc ;;
 2: len 8; hex 000000000044c0da; asc D ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1155 page no 65295 n bits 312 index uniq_user_id_tag_name of table `emax_base`.`emax_user_tag` trx id 404898332 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 242 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 97c33f75fbadb002; asc ?u ;;
 1: len 12; hex e5b882e59cbae68ea8e5b9bf; asc ;;
 2: len 8; hex 000000000044c0da; asc D ;;

*** WE ROLL BACK TRANSACTION (1)
<br>Some non-key information lines are omitted here
----------------------------
END OF INNODB MONITOR OUTPUT
============================

The log contains a lot of information, and several non-critical information lines have been omitted. We mainly focus on the information related to the last deadlock.

6. Deadlock recurrence

sql preparation

INSERT INTO emax_user_tag ( user_id, id_card_no, tag_name, create_time, update_time ) VALUES ( 1712282059297107970, ‘A978B69811B82F4B08F929D26B360F2D980CDCB5F419F1DD46527FB90BE1E5A9’, ‘Market Promotion’, ‘2023-10-12 09:40:50.481’, ‘2023-10-12 09:40: 50.481’ );

INSERT INTO emax_user_tag ( user_id, id_card_no, tag_name, create_time, update_time ) VALUES ( 1712282059297107970, ‘A978B69811B82F4B08F929D26B360F2D980CDCB5F419F1DD46527FB90BE1E5A9’, ‘Information Technical Services’, ‘2023-10-12 09:40:50.481’, ‘2023-10-12 09:40 :50.481’ );

Create a local database emax_user_tag that is the same as the production one. Similarly, create a unique index on the user_id and tag_name fields.

Operation Step 1

Execute in window one

begin;

INSERT INTO emax_user_tag ( user_id, id_card_no, tag_name, create_time, update_time ) VALUES ( 1712282059297107970, ‘A978B69811B82F4B08F929D26B360F2D980CDCB5F419F1DD46527FB90BE1E5A9’, ‘Market Promotion’, ‘2023-10-12 09:40:50.481’, ‘2023-10-12 09:40: 50.481’ );

Operation Step 2

Execute in window two

begin;

INSERT INTO emax_user_tag ( user_id, id_card_no, tag_name, create_time, update_time ) VALUES ( 1712282059297107970, ‘A978B69811B82F4B08F929D26B360F2D980CDCB5F419F1DD46527FB90BE1E5A9’, ‘Market Promotion’, ‘2023-10-12 09:40:50.481’, ‘2023-10-12 09:40: 50.481’ );

Operation Step 3

Execute in window one

INSERT INTO emax_user_tag ( user_id, id_card_no, tag_name, create_time, update_time ) VALUES ( 1712282059297107970, ‘A978B69811B82F4B08F929D26B360F2D980CDCB5F419F1DD46527FB90BE1E5A9’, ‘Information Technical Services’, ‘2023-10-12 09:40:50.481’, ‘2023-10-12 09:40 :50.481’ );

commit;

Operation Step 4

Switch back to window two

Operation Step 5

Execute the command show ENGINE INNODB status to view the deadlock log. Same situation as above.

======================================
2023-10-13 14:52:43 0x7fe759575700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 24 seconds
------------------
BACKGROUND THREAD
------------------
srv_master_thread loops: 137590 srv_active, 0 srv_shutdown, 2010475 srv_idle
srv_master_thread log flush and writes: 2148065
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 6627679
OS WAIT ARRAY INFO: signal count 14339066
RW-shared spins 0, rounds 22104735, OS waits 1859432
RW-excl spins 0, rounds 258795010, OS waits 2238669
RW-sx spins 6551822, rounds 136672247, OS waits 1786313
Spin rounds per wait: 22104735.00 RW-shared, 258795010.00 RW-excl, 20.86 RW-sx
--------------------------
LATEST DETECTED DEADLOCK
--------------------------
2023-10-13 14:50:31 0x7fe892ad0700
*** (1) TRANSACTION:
TRANSACTION 293171186, ACTIVE 10 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 1266048, OS thread handle 140631613069056, query id 35172347 192.168.48.179 develop update
/* ApplicationName=DBeaver 22.2.1 - SQLEditor <Script-4.sql> */ INSERT INTO emax_user_tag ( user_id, id_card_no, tag_name, create_time, update_time ) VALUES ( 1712282059297107970, 'A978B69811B82F4B08F929D26B360F2D9 80CDCB5F419F1DD46527FB90BE1E5A9', 'Marketing', '2023-10 -12 09:40:50.481', '2023-10-12 09:40:50.481' )
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14171 page no 4 n bits 72 index user_id_tag_name of table `dbsyncer_mf`.`emax_user_tag` trx id 293171186 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 97c33f75fbadb002; asc ?u ;;
 1: len 12; hex e5b882e59cbae68ea8e5b9bf; asc ;;
 2: len 8; hex 80000000000358d0; asc X ;;

*** (2) TRANSACTION:
TRANSACTION 293171182, ACTIVE 19 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 1266036, OS thread handle 140636869953280, query id 35172492 192.168.48.179 develop update
/* ApplicationName=DBeaver 22.2.1 - SQLEditor <Script-1.sql> */ INSERT INTO emax_user_tag ( user_id, id_card_no, tag_name, create_time, update_time ) VALUES ( 1712282059297107970, 'A978B69811B82F4B08F929D26B360F2D9 80CDCB5F419F1DD46527FB90BE1E5A9', 'Information Technology Services', '2023- 10-12 09:40:50.481', '2023-10-12 09:40:50.481' )
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 14171 page no 4 n bits 72 index user_id_tag_name of table `dbsyncer_mf`.`emax_user_tag` trx id 293171182 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 97c33f75fbadb002; asc ?u ;;
 1: len 12; hex e5b882e59cbae68ea8e5b9bf; asc ;;
 2: len 8; hex 80000000000358d0; asc X ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14171 page no 4 n bits 72 index user_id_tag_name of table `dbsyncer_mf`.`emax_user_tag` trx id 293171182 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 97c33f75fbadb002; asc ?u ;;
 1: len 12; hex e5b882e59cbae68ea8e5b9bf; asc ;;
 2: len 8; hex 80000000000358d0; asc X ;;

*** WE ROLL BACK TRANSACTION (1)

View Code

7. Deadlock analysis

Analyze based on deadlock logs.

When performing step one:

Transaction 1 acquired the insertion intention lock

When performing step two:

Transaction 2: Upgrade the intention lock of the transaction to a unique index exclusive lock, and try to obtain a unique index shared lock (the shared lock has not been obtained yet, and is queued)

When performing step three:

Transaction 1: Try to acquire the insertion intention gap lock

Transaction two waits for transaction one to release the exclusive lock

Transaction one waits for transaction two to release the shared lock

A mutual waiting relationship is formed, which is a deadlock.

8. Summary

1. Batch insert operations try to ensure data orderliness

2. You can use higher-performance redis for concurrent interception or synchronous processing control

3. Set the parameter innodb_deadlock_detect to on, and rollback will be performed in case of deadlock (the database is enabled by default)

4. Business code needs to consider database exceptions

5. The current mysql database version is 5.7.28-log. After careful verification by Hong Jie, mysql-8.0 version has been optimized for this scenario and no deadlock will occur.

Literature reference https://blog.csdn.net/minghao0508/article/details/129093202