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的整体架构分为三层: 1.基础支持层 该层包括:数据源模块、事务管理模块、缓存模块、Binding模块、反射模块、类型转换模块、日志模块、资源加载模块、解析器模块 2.核心处理层 该层包括:配置解析、参数映射、SQL解析、SQL执行、结果集映射、插件 3.接口层 该层包括:SqlSession 基础支持层 该层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。

Spring+MyBatis+jeasyui 功能树列表

java代码@EnablePaging@RequestMapping(value = "/queryFunctionList.html")@ResponseBodypublic Map<String, Object> queryFunctionList() {String parentId = "";List<FunctionDisplay> tables = query(parent

Mybatis中的like查询

<if test="templateName != null and templateName != ''">AND template_name LIKE CONCAT('%',#{templateName,jdbcType=VARCHAR},'%')</if>

JavaWeb【day09】--(Mybatis)

1. Mybatis基础操作 学习完mybatis入门后,我们继续学习mybatis基础操作。 1.1 需求 需求说明: 根据资料中提供的《tlias智能学习辅助系统》页面原型及需求,完成员工管理的需求开发。 通过分析以上的页面原型和需求,我们确定了功能列表: 查询 根据主键ID查询 条件查询 新增 更新 删除 根据主键ID删除 根据主键ID批量删除

MyBatis 切换不同的类型数据库方案

下属案例例当前结合SpringBoot 配置进行讲解。 背景: 实现一个工程里面在部署阶段支持切换不同类型数据库支持。 方案一 数据源配置 关键代码(是什么数据库,该怎么配就怎么配) spring:datasource:name: test# 使用druid数据源type: com.alibaba.druid.pool.DruidDataSource# @需要修改 数据库连接及驱动u

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

java线程深度解析(六)——线程池技术

http://blog.csdn.net/Daybreak1209/article/details/51382604 一种最为简单的线程创建和回收的方法: [html]  view plain copy new Thread(new Runnable(){                @Override               public voi

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

java线程深度解析(二)——线程互斥技术与线程间通信

http://blog.csdn.net/daybreak1209/article/details/51307679      在java多线程——线程同步问题中,对于多线程下程序启动时出现的线程安全问题的背景和初步解决方案已经有了详细的介绍。本文将再度深入解析对线程代码块和方法的同步控制和多线程间通信的实例。 一、再现多线程下安全问题 先看开启两条线程,分别按序打印字符串的