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

相关文章

MyBatis分页查询实战案例完整流程

《MyBatis分页查询实战案例完整流程》MyBatis是一个强大的Java持久层框架,支持自定义SQL和高级映射,本案例以员工工资信息管理为例,详细讲解如何在IDEA中使用MyBatis结合Page... 目录1. MyBATis框架简介2. 分页查询原理与应用场景2.1 分页查询的基本原理2.1.1 分

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

mybatis映射器配置小结

《mybatis映射器配置小结》本文详解MyBatis映射器配置,重点讲解字段映射的三种解决方案(别名、自动驼峰映射、resultMap),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定... 目录select中字段的映射问题使用SQL语句中的别名功能使用mapUnderscoreToCame

mybatis-plus如何根据任意字段saveOrUpdateBatch

《mybatis-plus如何根据任意字段saveOrUpdateBatch》MyBatisPlussaveOrUpdateBatch默认按主键判断操作类型,若需按其他唯一字段(如agentId、pe... 目录使用场景方法源码方法改造首先在service层定义接口service层接口实现总结使用场景my

MyBatis ParameterHandler的具体使用

《MyBatisParameterHandler的具体使用》本文主要介绍了MyBatisParameterHandler的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参... 目录一、概述二、源码1 关键属性2.setParameters3.TypeHandler1.TypeHa

MyBatis-plus处理存储json数据过程

《MyBatis-plus处理存储json数据过程》文章介绍MyBatis-Plus3.4.21处理对象与集合的差异:对象可用内置Handler配合autoResultMap,集合需自定义处理器继承F... 目录1、如果是对象2、如果需要转换的是List集合总结对象和集合分两种情况处理,目前我用的MP的版本

Java中如何正确的停掉线程

《Java中如何正确的停掉线程》Java通过interrupt()通知线程停止而非强制,确保线程自主处理中断,避免数据损坏,线程池的shutdown()等待任务完成,shutdownNow()强制中断... 目录为什么不强制停止为什么 Java 不提供强制停止线程的能力呢?如何用interrupt停止线程s

python 线程池顺序执行的方法实现

《python线程池顺序执行的方法实现》在Python中,线程池默认是并发执行任务的,但若需要实现任务的顺序执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录方案一:强制单线程(伪顺序执行)方案二:按提交顺序获取结果方案三:任务间依赖控制方案四:队列顺序消

Python多线程应用中的卡死问题优化方案指南

《Python多线程应用中的卡死问题优化方案指南》在利用Python语言开发某查询软件时,遇到了点击搜索按钮后软件卡死的问题,本文将简单分析一下出现的原因以及对应的优化方案,希望对大家有所帮助... 目录问题描述优化方案1. 网络请求优化2. 多线程架构优化3. 全局异常处理4. 配置管理优化优化效果1.

MyBatis-Plus 与 Spring Boot 集成原理实战示例

《MyBatis-Plus与SpringBoot集成原理实战示例》MyBatis-Plus通过自动配置与核心组件集成SpringBoot实现零配置,提供分页、逻辑删除等插件化功能,增强MyBa... 目录 一、MyBATis-Plus 简介 二、集成方式(Spring Boot)1. 引入依赖 三、核心机制