最近遇到一个死锁问题,我整理了一下,写了一个例子。
国籍表:
CREATE TABLE `nationality` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`type` VARCHAR ( 64 ) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`desc` VARCHAR ( 255 ) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`count` INT ( 11 ) DEFAULT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
用户表:
CREATE TABLE `person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`nationality_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `nationality_fk` (`nationality_id`),
CONSTRAINT `nationality_fk` FOREIGN KEY (`nationality_id`) REFERENCES `nationality` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
下面是一个很简单的功能。
功能如下:新增一个用户,并且往把该用户所在国籍的人数加1。
新增国籍人数的sql:
update nationality
set `count` = `count` + #{increase}
where id = #{id}
创建用户的sql:
insert into
person
(`name`,`desc`,`nationality_id`)
values(
#{name},
#{desc},
#{nationalityId})
创建用户的service代码:
@Service
public class PersonService {
private static final Integer CHINA_ID = 1;
@Autowired
private PersonMapper personMapper;
@Autowired
private NationalityMapper nationalityMapper;
@Transactional
public void create(String name, String desc) {
Person person = new Person();
person.setName(name);
person.setDesc(desc);
person.setNationalityId(CHINA_ID);
personMapper.create(person);
try {
//这里加了一个延迟,方便测试
Thread.sleep(5000L);
} catch (Exception e) {
throw new RuntimeException(e);
}
nationalityMapper.updateCount(CHINA_ID, 1);
}
}
模拟并发创建用户:
@SpringBootApplication
@MapperScan("com.example.bootmybatis.dao")
public class BootmybatisApplication {
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(BootmybatisApplication.class, args);
PersonService personService = applicationContext.getBean(PersonService.class);
new Thread(() -> personService.create("测试1", "无")).start();
new Thread(() -> personService.create("测试2", "无")).start();
}
}
控制台报错如下:
Exception in thread "Thread-155" org.springframework.dao.DeadlockLoserDataAccessException:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: update nationality set `count` = `count` + ? where id = ?
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:267)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
at com.sun.proxy.$Proxy57.insert(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:278)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:58)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy59.updateCount(Unknown Source)
at com.example.bootmybatis.service.PersonService.create(PersonService.java:37)
at com.example.bootmybatis.service.PersonService$$FastClassBySpringCGLIB$$c378fac9.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:366)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at com.example.bootmybatis.service.PersonService$$EnhancerBySpringCGLIB$$85b28c3f.create()
at com.example.bootmybatis.BootmybatisApplication.lambda$main$0(BootmybatisApplication.java:16)
at java.lang.Thread.run(Thread.java:745)
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)
at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:370)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3051)
at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(FilterEventAdapter.java:440)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3049)
at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.execute(PreparedStatementProxyImpl.java:167)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:498)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:46)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.update(RoutingStatementHandler.java:74)
at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:50)
at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)
at com.sun.proxy.$Proxy69.update(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
... 19 more
查看数据库只有demo2创建成功了。
查看mysql死锁日志如下,(可以通过SHOW ENGINE INNODB STATUS 查看最近死锁日志)
------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-06-11 15:55:25 0x3d8c
*** (1) TRANSACTION:
TRANSACTION 164396, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 20, OS thread handle 17124, query id 341 localhost 127.0.0.1 root updating
update nationality
set `count` = `count` + 1
where id = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 436 page no 3 n bits 72 index PRIMARY of table `boot`.`nationality` trx id 164396 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000002822a; asc *;;
2: len 7; hex 3f000001f521c4; asc ? ! ;;
3: len 5; hex 4348494e41; asc CHINA;;
4: len 6; hex e4b8ade59bbd; asc ;;
5: len 4; hex 80000002; asc ;;
*** (2) TRANSACTION:
TRANSACTION 164397, ACTIVE 5 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 21, OS thread handle 15756, query id 340 localhost 127.0.0.1 root updating
update nationality
set `count` = `count` + 1
where id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 436 page no 3 n bits 72 index PRIMARY of table `boot`.`nationality` trx id 164397 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000002822a; asc *;;
2: len 7; hex 3f000001f521c4; asc ? ! ;;
3: len 5; hex 4348494e41; asc CHINA;;
4: len 6; hex e4b8ade59bbd; asc ;;
5: len 4; hex 80000002; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 436 page no 3 n bits 72 index PRIMARY of table `boot`.`nationality` trx id 164397 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000002822a; asc *;;
2: len 7; hex 3f000001f521c4; asc ? ! ;;
3: len 5; hex 4348494e41; asc CHINA;;
4: len 6; hex e4b8ade59bbd; asc ;;
5: len 4; hex 80000002; asc ;;
*** WE ROLL BACK TRANSACTION (2)
根据日志可以看到事务1在等待获取X锁,事务2持有S锁,在等待X锁。其实事务1和事务2都持有S锁,都在等待X锁,因为两个事务S锁都不释放,所以
都无法获取X锁,所以就死锁了。删除外键后就没有问题了。