jpa与mybatis混用引起线程卡死

2023-10-14 17:59

本文主要是介绍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,然后在两次查询前中后打印了一些连接数信息。

  1. 注释掉@Transactional注解

请求接口,发现请求卡住,利用jdk1.8的jvisualvm程序,查看此时程序的线程状态:
http-nio-8082-exec-1
发现http-nio-8082-exec-1线程处于WAITING状态,卡住了,查看线程dump:
dump
queryUser
卡住的方法正好是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查询没有释放连接,导致获取不到,线程卡死。

  1. 加上@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混用引起线程卡死的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/212191

相关文章

Linux线程同步/互斥过程详解

《Linux线程同步/互斥过程详解》文章讲解多线程并发访问导致竞态条件,需通过互斥锁、原子操作和条件变量实现线程安全与同步,分析死锁条件及避免方法,并介绍RAII封装技术提升资源管理效率... 目录01. 资源共享问题1.1 多线程并发访问1.2 临界区与临界资源1.3 锁的引入02. 多线程案例2.1 为

破茧 JDBC:MyBatis 在 Spring Boot 中的轻量实践指南

《破茧JDBC:MyBatis在SpringBoot中的轻量实践指南》MyBatis是持久层框架,简化JDBC开发,通过接口+XML/注解实现数据访问,动态代理生成实现类,支持增删改查及参数... 目录一、什么是 MyBATis二、 MyBatis 入门2.1、创建项目2.2、配置数据库连接字符串2.3、入

MyBatis-Plus 自动赋值实体字段最佳实践指南

《MyBatis-Plus自动赋值实体字段最佳实践指南》MyBatis-Plus通过@TableField注解与填充策略,实现时间戳、用户信息、逻辑删除等字段的自动填充,减少手动赋值,提升开发效率与... 目录1. MyBATis-Plus 自动赋值概述1.1 适用场景1.2 自动填充的原理1.3 填充策略

mybatis中resultMap的association及collectio的使用详解

《mybatis中resultMap的association及collectio的使用详解》MyBatis的resultMap定义数据库结果到Java对象的映射规则,包含id、type等属性,子元素需... 目录1.reusltmap的说明2.association的使用3.collection的使用4.总

mybatis-plus QueryWrapper中or,and的使用及说明

《mybatis-plusQueryWrapper中or,and的使用及说明》使用MyBatisPlusQueryWrapper时,因同时添加角色权限固定条件和多字段模糊查询导致数据异常展示,排查发... 目录QueryWrapper中or,and使用列表中还要同时模糊查询多个字段经过排查这就导致只要whe

Java中的xxl-job调度器线程池工作机制

《Java中的xxl-job调度器线程池工作机制》xxl-job通过快慢线程池分离短时与长时任务,动态降级超时任务至慢池,结合异步触发和资源隔离机制,提升高频调度的性能与稳定性,支撑高并发场景下的可靠... 目录⚙️ 一、调度器线程池的核心设计 二、线程池的工作流程 三、线程池配置参数与优化 四、总结:线程

SpringBoot集成MyBatis实现SQL拦截器的实战指南

《SpringBoot集成MyBatis实现SQL拦截器的实战指南》这篇文章主要为大家详细介绍了SpringBoot集成MyBatis实现SQL拦截器的相关知识,文中的示例代码讲解详细,有需要的小伙伴... 目录一、为什么需要SQL拦截器?二、MyBATis拦截器基础2.1 核心接口:Interceptor

WinForm跨线程访问UI及UI卡死的解决方案

《WinForm跨线程访问UI及UI卡死的解决方案》在WinForm开发过程中,跨线程访问UI控件和界面卡死是常见的技术难题,由于Windows窗体应用程序的UI控件默认只能在主线程(UI线程)上操作... 目录前言正文案例1:直接线程操作(无UI访问)案例2:BeginInvoke访问UI(错误用法)案例

MyBatis-Plus通用中等、大量数据分批查询和处理方法

《MyBatis-Plus通用中等、大量数据分批查询和处理方法》文章介绍MyBatis-Plus分页查询处理,通过函数式接口与Lambda表达式实现通用逻辑,方法抽象但功能强大,建议扩展分批处理及流式... 目录函数式接口获取分页数据接口数据处理接口通用逻辑工具类使用方法简单查询自定义查询方法总结函数式接口

Linux线程之线程的创建、属性、回收、退出、取消方式

《Linux线程之线程的创建、属性、回收、退出、取消方式》文章总结了线程管理核心知识:线程号唯一、创建方式、属性设置(如分离状态与栈大小)、回收机制(join/detach)、退出方法(返回/pthr... 目录1. 线程号2. 线程的创建3. 线程属性4. 线程的回收5. 线程的退出6. 线程的取消7.