本文主要是介绍jpa与mybatis混用引起线程卡死,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
- 背景
- 实验
- 总结
背景
最近生产环境上出现了一个问题:某台服务器节点出现不工作的情况,观察当时的详细日志,发现有很多线程在请求了某个接口后就再没在日志中出现,假设请求为/query,说明线程在请求了/query后卡死了。于是查看当时的jstack文件,发现有很多线程都处于waiting状态,而waiting的原因是正在从c3p0连接池获取数据库连接,从栈信息看出线程此时执行的方法是methodA,统计了一下methodA出现的次数,正好是100次,这个数字正好等于配置的数据库连接池最大连接数,那么可以判断出,是大量请求都在获取数据库连接,连接数不断增大至最大连接数,而没有连接被释放,导致大量线程卡死。
这里有两个问题:
1.为什么线程等待获取数据库连接会一直卡着?因为生产上没有配置下面这一项:
# 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出
# SQLException,如设为0则无限期等待。单位毫秒。Default: 0
c3p0.checkoutTimeout=1000
所以线程会一致等待获取数据库连接而不会超时并抛出异常;
2. 为什么数据库连接一直没有释放?
在请求/query中,我们混用了jpa和mybatis,先进行了jpa查询,后进行mybatis查询,而等待数据库连接而卡死这一现象正好出现在mybatis查询方法methodA中,难道混用jpa和mybatis对连接数有影响?这个问题我排查了很久,下面详细说明。
实验
准备新建一个boot项目复现生产上的情况,项目同时引入jpa和tk mybatis,连接池选用c3p0,和生产保持一致,简单贴下所需依赖:
<!--整合hibernate和jpa--><!--整合hibernate和jpa--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.32</version></dependency><!-- tk mybatis --><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>4.2.2</version></dependency><!-- c3p0 --><dependency><groupId>com.mchange</groupId><artifactId>c3p0</artifactId><version>0.9.5.4</version></dependency>
下面是c3p0连接池配置,最大连接数、最小连接数、初始连接数都设置为1,便于一次请求就能复现生产上的情况。
c3p0.jdbcUrl=jdbc:mysql://localhost:3306/wuxia?useSSL=false
c3p0.user=root
c3p0.password=123456
c3p0.driverClass=com.mysql.cj.jdbc.Driver
c3p0.minPoolSize=1
c3p0.maxPoolSize=1
# 最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0
c3p0.maxIdleTime=60
#当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3
c3p0.acquireIncrement=1
c3p0.maxStatements=1000
c3p0.initialPoolSize=1
#每60秒检查所有连接池中的空闲连接。Default: 0
c3p0.idleConnectionTestPeriod=60
#定义在从数据库获取新连接失败后重复尝试的次数。Default: 30
c3p0.acquireRetryAttempts=30
#两次连接中间隔时间,单位毫秒。Default: 1000
c3p0.acquireRetryDelay=1000
# 获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常。但是数据源仍有效
# 保留,并在下次调用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试
# 获取连接失败后该数据源将申明已断开并永久关闭。Default: false
c3p0.breakAfterAcquireFailure=false
#因性能消耗大请只在需要的时候使用它。如果设为true那么在每个connection提交的
#时候都将校验其有效性。建议使用idleConnectionTestPeriod或automaticTestTable
#等方法来提升连接测试的性能。Default: false
c3p0.testConnectionOnCheckout=false
#如果设为true那么在取得连接的同时将校验连接的有效性。Default: false
c3p0.testConnectionOnCheckin=true
# 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出
# SQLException,如设为0则无限期等待。单位毫秒。Default: 0
c3p0.checkoutTimeout=0
注意c3p0.checkoutTimeout
配置为0。
简单贴下代码:
@GetMapping("/user/{id}")
// @Transactionalpublic ResponseEntity findUser(@PathVariable("id") Long id) throws InterruptedException, SQLException {System.out.println("numbusy before:" + dataSource.getNumBusyConnections());System.out.println("numidle before:" + dataSource.getNumIdleConnections());System.out.println("numtotal before:" + dataSource.getNumConnections());
// Resume resume = resumeDao.findById(id).orElseThrow(() -> new RuntimeException("no resume"));Resume resume = resumeService.findById(id);System.out.println("numbusy in:" + dataSource.getNumBusyConnections());System.out.println("numidle in:" + dataSource.getNumIdleConnections());System.out.println("numtotal in:" + dataSource.getNumConnections());User user = userService.queryUser(id);
// User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("no user"));
// User user = queryUser(id);System.out.println("numbusy after:" + dataSource.getNumBusyConnections());System.out.println("numidle after:" + dataSource.getNumIdleConnections());System.out.println("numtotal after:" + dataSource.getNumConnections());Map result = new HashMap();result.put("user", user);result.put("resume", resume);return ResponseEntity.ok(result);}
非常简单,就是进行了两次查询,第一用jpa,第二次用mybatis,然后在两次查询前中后打印了一些连接数信息。
- 注释掉
@Transactional
注解
请求接口,发现请求卡住,利用jdk1.8的jvisualvm程序,查看此时程序的线程状态:
发现http-nio-8082-exec-1线程处于WAITING状态,卡住了,查看线程dump:
卡住的方法正好是mybatis查询方法queryUser
,再查看此时的控制台日志:
numbusy before:0
numidle before:1
numtotal before:1
Hibernate: selectresume0_.id as id1_3_0_,resume0_.address as address2_3_0_,resume0_.name as name3_3_0_,resume0_.phone as phone4_3_0_ fromtb_resume resume0_ whereresume0_.id=?
numbusy in:1
numidle in:0
numtotal in:1
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32d321bc] was not registered for synchronization because synchronization is not active
可以看出执行完jpa查询后,紧接着进行mybatis查询,需要创建SqlSession,而此时没有加上@Transactional
注解,jdbc连接不能被spring管理,需要从连接池获取新的连接,而前面的jpa查询没有释放连接,导致获取不到,线程卡死。
- 加上
@Transactional
注解
请求接口,接口正常返回,打印日志:
numbusy before:1
numidle before:0
numtotal before:1
Hibernate: selectresume0_.id as id1_3_0_,resume0_.address as address2_3_0_,resume0_.name as name3_3_0_,resume0_.phone as phone4_3_0_ fromtb_resume resume0_ whereresume0_.id=?
numbusy in:1
numidle in:0
numtotal in:1
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b30043d]
JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@5533cf8f [wrapping: com.mysql.cj.jdbc.ConnectionImpl@5769ce21]] will be managed by Spring
==> Preparing: SELECT id,user_name FROM user WHERE id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name
<== Row: 1, wuxia
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b30043d]
numbusy after:1
numidle after:0
numtotal after:1
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b30043d]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b30043d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b30043d]
由于加上了@Transactional
注解,jdbc连接能够被spring管理,于是mybatis查询复用了前面jpa的连接,能够正常执行查询。
- 去掉
@Transactional
注解,在配置中加上:
spring.jpa.open-in-view=false
关于此项配置,可以参考文章:
链接: https://www.jianshu.com/p/c856799a42a4
简单来说:
- Spring会帮忙在request的一开始就打开Hibernate Session。
- 每当App需要一个Session的时候,就会重用这个Session。
- 在Request结束的时候,会帮忙关闭该Session。
那么,前面说的为什么为什么数据库连接一直没有释放这个问题就清楚了,正是因为默认spring.jpa.open-in-view=true
,所以session会一直保持到请求结束,会一直占用着连接,又因为没加事务,不能复用连接,导致后面的mybatis查询获取不到新连接,进而导致线程卡死。
请求接口,正常返回,日志打印:
numbusy before:0
numidle before:1
numtotal before:1
Hibernate: selectresume0_.id as id1_3_0_,resume0_.address as address2_3_0_,resume0_.name as name3_3_0_,resume0_.phone as phone4_3_0_ fromtb_resume resume0_ whereresume0_.id=?
numbusy in:0
numidle in:1
numtotal in:1
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@64f58863] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@76ea2d06 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@3396bad0]] will not be managed by Spring
==> Preparing: SELECT id,user_name FROM user WHERE id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name
<== Row: 1, wuxia
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@64f58863]
numbusy after:1
numidle after:0
numtotal after:1
可以看到虽然jdbc连接不被spring管理,但是由于配置了spring.jpa.open-in-view=false
,所以jpa查询完成后关闭了jpa的session,释放了连接,所以mybatis可以获取到新连接。
总结
关于生产上的问题,还有一点补充:线上数据库线程池最大连接数是100,只有所有的连接都卡在请求/query中的方法methodA,才会导致连接释放不了,因为但凡有一个连接释放了,某个请求中的methodA方法的mybatis查询都能拿到连接,执行完整个方法,进而释放连接,那么渐渐的,这些卡住的等待获取连接的线程,都能整个执行完方法methodA而不再卡住。那么什么情况下,才能导致这种情况呢?我们发现出现这种现象总是在Full GC之后,这就是原因,Full GC之后的停顿,堆积了大量的/query请求,GC后,大量的/query请求迅速把数据库连接池占满,进而卡住。
总而言之,正是以下条件导致了大量线程卡死:
- 代码中混用jpa和mybatis,且没有开启事务,导致连接不能复用;
- 没有配置
c3p0.checkoutTimeout
,获取连接没有超时; - 默认开启了
spring.jpa.open-in-view=true
,延长了hibernate session的生命周期,导致连接没有及时释放; - Full GC后大量混用jpa和mybatis的并发请求进入。
这篇关于jpa与mybatis混用引起线程卡死的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!