[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查

这篇具有很好参考价值的文章主要介绍了[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景说明

当前业务场景我们使用原生SpringBoot整合Hikari数据源连接池提供服务,但是近期业务迭代需要使用动态多数据源,很自然想到dynamic-source,结果一系列惨案离奇发生。。。

蒙蔽双眼

原生SpringBoot整合HikariCp数据源连接池配置【这个是没问题的配置】

spring.datasource.hikari.allow-pool-suspension = true
spring.datasource.hikari.connection-timeout = 10000
spring.datasource.hikari.pool-name = HikariPool
spring.datasource.hikari.idle-timeout = 60000
spring.datasource.hikari.maximum-pool-size = 300
spring.datasource.hikari.max-lifetime = 120000
spring.datasource.hikari.minimum-idle = 30

spring.datasource.type = com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://a.com:4000/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username = xx
spring.datasource.password = sx

而升级后的动态多数据源配置如下:【有严重问题】


spring.datasource.dynamic.primary = tidb-payment
spring.datasource.dynamic.strict = false
spring.datasource.dynamic.hikari.idle-timeout = 60000
spring.datasource.dynamic.hikari.max-lifetime = 120000
spring.datasource.dynamic.hikari.connection-timeout = 10000
spring.datasource.dynamic.hikari.minimum-idle = 30
spring.datasource.dynamic.hikari.maximum-pool-size = 300



spring.datasource.url = jdbc:mysql://a.com:4000/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username = xxx
spring.datasource.password = xxx
spring.datasource.type = com.zaxxer.hikari.HikariDataSource

mysql-payment.username = root
mysql-payment.password = xxx
mysql-payment.url = jdbc:mysql://xxx:3306/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai

mysql-cashier.username = xxx
mysql-cashier.password = xx
mysql-cashier.url = jdbc:mysql://xxx:3306/cashier?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai

spring.datasource.dynamic.primary = tidb-payment
spring.datasource.dynamic.datasource.tidb-payment.url = ${spring.datasource.url}
spring.datasource.dynamic.datasource.tidb-payment.username = ${spring.datasource.username}
spring.datasource.dynamic.datasource.tidb-payment.password = ${spring.datasource.password}
spring.datasource.dynamic.datasource.tidb-payment.type = ${spring.datasource.type}

spring.datasource.dynamic.datasource.mysql-payment.url = ${mysql-payment.url}
spring.datasource.dynamic.datasource.mysql-payment.username = ${mysql-payment.username}
spring.datasource.dynamic.datasource.mysql-payment.password = ${mysql-payment.password}
spring.datasource.dynamic.datasource.mysql-payment.type = ${spring.datasource.type}

spring.datasource.dynamic.datasource.mysql-cashier.url = ${mysql-cashier.url}
spring.datasource.dynamic.datasource.mysql-cashier.username = ${mysql-cashier.username}
spring.datasource.dynamic.datasource.mysql-cashier.password = ${mysql-cashier.password}
spring.datasource.dynamic.datasource.mysql-cashier.type = ${spring.datasource.type}


来,无论几年经验的道友看看此配置有什么问题?刚使用的童鞋很难发现,因为没有一定的并发量, 几乎很难发现其中 很致命的2个问题

  1. 全局配置是各自独享,不是共享
  2. 当前配置的最大活跃连接数和最小活跃连接数实际运行都是10,即配置是错误的

实话说,我也是遇到我人生第一个职业滑铁卢:

  1. 只要服务一发版,消息服务一直处于积压状态,而这个服务业务逻辑又很单一就是消费数据写TIDB,加上匮乏的测试人员,非生产环境根本看不出任何问题
  2. 只要一回滚就正常
  3. 服务消息积压根本没有任何错误
    这期间一直怀疑是新升级代码过多创建线程,但是几经确认是规范的创建线程池,自信注释掉所有可能过多创建线程地方,发布后继续消息积压,几经尝试无果

最搞笑的是,在期间做的修补策略还因为看不到异常,而引入一个新的问题:

WARN com.baomidou.dynamic.datasource.DynamicRoutingDataSource [240] - dynamic-datasource initial loaded [0] datasource,Please add your primary datasource or check your configuration

当你第一次看到这个警告切记不要忽略,因为此时服务虽然只是启动告警,但是只要一尝试sql连接,直接异常:Caused by: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource
本来我不需要单独讲,因为自测是基本的素养,但是因为在当时上线修补过程中是缺少测试【过于自信】,所以任务服务发版没问题忽略,而异常还是我后来从rocketmq_client.log找到,还不是自身配置logback-spring.xml对应日志文件,所以一直没在意,关键RocketMQ还吃掉了异常,直接当回滚处理.


口说无凭

修补引发的新问题

首先对着回滚前最后一次修补代码分支先直接在本地压测,瞬间发现baomidou.dynamic.datasource.exception.CannotFindDataSourceException:ception
但问题来了,线上为什么没有这个异常,搜遍了日志无果,后来想到当前直接监听RocketMQ消费,统一在consumeMessage方法处理,如下[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查,前车之鉴,spring boot,rocketmq,Hikari,数据库连接池
坑啊,当时没发现是因为程序没有任何错误还傻傻以为是程序处理正常,只是线程积压了

话说回来,这个错误算比较低级了,因为引入了dynamic-datasource 数据源但是却没有配置好数据源,而默认引入依赖就会在业务的sql操作中使用改配置数据源连接池【当时回滚代码逻辑是不清晰的,只回滚配置注释代码是不够的,要么基于老分支直接重写逻辑本地验证后再试,要么所有新代码一起移除,包括mave依赖】

  <dependency>
          <groupId>com.baomidou</groupId>
           <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
           <version>3.4.0</version>
 </dependency>

这里可以推理得到:既然这个错误是RocketMQ捕获了,那么自然打在了RocketMQ配置的日志文件中:rockemq_client.log 注意这个不配置就自动生成,关键还只保留8小时,最终本地验证是找到了,生产因为过了一天所以看不到

解决配置问题

现在我们回来看看配置两个问题是怎么回事,这个比较隐晦了,我加好了数据源后拷贝生产一份配置到本地,开始debug定位发现,配置最大活跃连接、最小活跃连接数首先是-1 然后在校验合法性时改成了默认值10
[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查,前车之鉴,spring boot,rocketmq,Hikari,数据库连接池
what?没生效本能想到这不可能,因为生产一直这么使用的,甚至怀疑生产一直是错误的,但是生产让SRE查询监控信息确认是正确的,瞬间再次怀疑自己,索性仔细比对生产老配置发现和源代码排查
才知道maximum-pool-size minimum-idle在升级使用dynamic-source是不对的,属性名发生了变更分别变成了max-pool-size 和 min-idle , 本以为原路拷贝即可谁知在dynamic-datasource源码中配置HikariCp做了替换,真的坑爹
[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查,前车之鉴,spring boot,rocketmq,Hikari,数据库连接池
这里就可以解释,线上是并发比较高的,所以很快把10个连接占满,甚至已经抛出了连接不可用的异常由于被RocketMQ捕获,所以很难发现,于是修正了属性值再次Debug正常设置成功。

修正了属性值还不够,接下来有第二个问题,请回到开头再次观察连接池配置是全局配置,最初也是没有好好看源码以为是三个数据源共享配置,直到我在调试过程中看到源码确实是独自设置,我才恍然
[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查,前车之鉴,spring boot,rocketmq,Hikari,数据库连接池

是否允许全局独享取决你的业务场景,如果你的数据库的所在数据源都是独立部署的那么 共享除了失去定制的灵活性没啥性能问题,但是如果你的本质是一个数据源多个数据库 这么配置会撑爆数据库连接,使用时需要谨慎!

有人要问了谁叫你不看文档,这里要diss 一下 dynamic-source官方文档说明这一块是真的黑心
[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查,前车之鉴,spring boot,rocketmq,Hikari,数据库连接池

所以经过上面分析最正确的配置模版如下,注意我只保证属性一定设置生效,但是value数值需要各自工业实践结果:

spring.datasource.url = jdbc:mysql://a.com:4000/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username = xxx
spring.datasource.password = xxx
spring.datasource.type = com.zaxxer.hikari.HikariDataSource

mysql-payment.username = root
mysql-payment.password = xxx
mysql-payment.url = jdbc:mysql://xxx:3306/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai

mysql-cashier.username = xxx
mysql-cashier.password = xx
mysql-cashier.url = jdbc:mysql://xxx:3306/cashier?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai


spring.datasource.dynamic.primary = tidb-payment
spring.datasource.dynamic.datasource.tidb-payment.url = ${spring.datasource.url}
spring.datasource.dynamic.datasource.tidb-payment.username = ${spring.datasource.username}
spring.datasource.dynamic.datasource.tidb-payment.password = ${spring.datasource.password}
spring.datasource.dynamic.datasource.tidb-payment.type = ${spring.datasource.type}
spring.datasource.dynamic.datasource.tidb-payment.hikari.max-pool-size = 50
spring.datasource.dynamic.datasource.tidb-payment.hikari.min-idle = 4
spring.datasource.dynamic.datasource.tidb-payment.hikari.max-lifetime = 120000
spring.datasource.dynamic.datasource.tidb-payment.hikari.connection-timeout = 10000
spring.datasource.dynamic.datasource.tidb-payment.hikari.idle-timeout = 60000
spring.datasource.dynamic.datasource.tidb-payment.hikari.allow-pool-suspension = true

spring.datasource.dynamic.datasource.mysql-payment.url = ${mysql-payment.url}
spring.datasource.dynamic.datasource.mysql-payment.username = ${mysql-payment.username}
spring.datasource.dynamic.datasource.mysql-payment.password = ${mysql-payment.password}
spring.datasource.dynamic.datasource.mysql-payment.type = ${spring.datasource.type}
spring.datasource.dynamic.datasource.mysql-payment.hikari.max-pool-size = 25
spring.datasource.dynamic.datasource.mysql-payment.hikari.min-idle = 4
spring.datasource.dynamic.datasource.mysql-payment.hikari.max-lifetime = 120000
spring.datasource.dynamic.datasource.mysql-payment.hikari.connection-timeout = 10000
spring.datasource.dynamic.datasource.mysql-payment.hikari.idle-timeout = 60000
spring.datasource.dynamic.datasource.mysql-payment.hikari.allow-pool-suspension = true

spring.datasource.dynamic.datasource.mysql-cashier.url = ${mysql-cashier.url}
spring.datasource.dynamic.datasource.mysql-cashier.username = ${mysql-cashier.username}
spring.datasource.dynamic.datasource.mysql-cashier.password = ${mysql-cashier.password}
spring.datasource.dynamic.datasource.mysql-cashier.type = ${spring.datasource.type}
spring.datasource.dynamic.datasource.mysql-cashier.hikari.max-pool-size = 25
spring.datasource.dynamic.datasource.mysql-cashier.hikari.min-idle = 3
spring.datasource.dynamic.datasource.mysql-cashier.hikari.max-lifetime = 120000
spring.datasource.dynamic.datasource.mysql-cashier.hikari.connection-timeout = 10000
spring.datasource.dynamic.datasource.mysql-cashier.hikari.idle-timeout = 60000
spring.datasource.dynamic.datasource.mysql-cashier.hikari.allow-pool-suspension = true

本地监控佐证

至此问题排查和解决已经确定,但是这么debug修改我还是不太放心,比较之前自信修改的教训让我历历在目,有了解到SpringBoot自带监控肯定有关于数据源连接池的信息,如果能看到自己期望的结果,那么一定不会有问题了

所以这里参考网上如何打开本地健康检查【不推荐生产环境使用】:Springboot整合Prometheus本地监控多数据源 ,这一篇不仅给出了方案,还发现了SpringBoot监控多数据源的bug,即只监控到一个问题:配置之后把之前的流程走一遍确实走到了默认值10
[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查,前车之鉴,spring boot,rocketmq,Hikari,数据库连接池

不用不知道,我又陷入另一个自我怀疑阶段:在本地和测试环境启动参数、apollo配置、代码完全一致的情况,都使用错误的数据连接池配置后, 测试和本地展现两种不同的数据源监控结果:云服务器是-1,而本地一直都是10,详情分析请看这一篇:【沉淀之华】SpringBoot使用HikariCP数据源两次初始化过程 & 服务器与本地完全一致却不同数据源结果定位


万法归元

从上面坎坷的排查过程看,需要注意3点

  1. 平时迭代一定要尽可能做好自测,甚至是压测
  2. 不要定式思维,按技术文档或者源码配置【无奈官方文档都成了资本手下,只恨无开源精神】
  3. 不要让RocketMQ去处理我们的业务异常,一定要手动捕获处理,否则很多未知的问题很难定位发现

持续分享,持续输出…文章来源地址https://www.toymoban.com/news/detail-790513.html

到了这里,关于[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于注解切换、Hikari实现的SpringBoot动态数据源(支持JNDI)

    先说效果,要实现方法级别注解切换当前数据源,不设置注解时走默认数据源,同时支持JNDI源。 Spring框架中存在一个抽象类 AbstractRoutingDataSource ,他是一个可以动态选择当前DataSource的路由类,我们就是要从这里入手,重新实现数据源的切换选择逻辑。然后借助注解和切面,

    2024年02月08日
    浏览(79)
  • springboot中Hikari连接池常用参数含义(一)

    yml 配置 minimum-idle:10 该参数限制数据库连接池保持数据连接的最小数量,如下当我们启动服务时,不做任何请求,服务就会默认建立10个数据库连接。等到有需求的时候就可以及时使用。 我们启动服务不做任何请求可以看到数据库与服务的连接数就是10. 且状态都为sleep. 这里

    2024年02月05日
    浏览(52)
  • 【Spring Boot】Spring Boot 配置 Hikari 数据库连接池

    数据库连接池是一个提高程序与数据库的连接的优化,连接池它主要作用是提高性能、节省资源、控制连接数、连接管理等操作; 程序中的线程池与之同理,都是为了优化、提高性能。

    2024年02月11日
    浏览(53)
  • Springboot项目使用原生Websocket

    2024年02月11日
    浏览(42)
  • rocketMq消息队列原生api使用以及rocketMq整合springboot

    使用RocketMQ的原生API开发是最简单也是目前看来最牢靠的方式。这里用SpringBoot来搭建一系列消息生产者和消息消费者,来访问之前搭建的RocketMQ集群。 首先创建一个基于Maven的SpringBoot工程,引入如下依赖: RocketMQ的官网上有很多经典的测试代码,这些代码虽然依赖的版本比较

    2024年02月12日
    浏览(47)
  • springboot整合neo4j-使用原生cypher

    该文的实现有更简单的方式,详见我的另一篇博客springboot整合neo4j–采用Neo4jClient和Neo4jTemplate方式 Neo4j 提供 JAVA API 以编程方式执行所有数据库操作。它支持三种类型的API: 1、Neo4j 原生的 Java API 原生 Java API 是一种低级别的纯 JAVA API,用于执行数据库操作。 2、Neo4j Cypher Jav

    2024年02月12日
    浏览(50)
  • springboot整合neo4j-使用原生cypher Java API

    该文的实现有更简单的方式,详见我的另一篇博客springboot整合neo4j–采用Neo4jClient和Neo4jTemplate方式 Neo4j 提供 JAVA API 以编程方式执行所有数据库操作。它支持三种类型的API: 1、Neo4j 原生的 Java API 原生 Java API 是一种低级别的纯 JAVA API,用于执行数据库操作。 2、Neo4j Cypher Jav

    2024年02月09日
    浏览(52)
  • ASP.NET Core MVC 从入门到精通之鉴权授权基础

    随着技术的发展,ASP.NET Core MVC也推出了好长时间,经过不断的版本更新迭代,已经越来越完善,本系列文章主要讲解ASP.NET Core MVC开发B/S系统过程中所涉及到的相关内容,适用于初学者,在校毕业生,或其他想从事ASP.NET Core MVC 系统开发的人员。 经过前几篇文章的讲解,初步

    2024年02月08日
    浏览(39)
  • 使用原生AJAX请求数据

    AJAX英文全称 Asynchronous Javascript And XML(异步的JavaScript和XML),是指一种创建交互式网页应用的网页开发技术,用于浏览器和服务器之间进行数据交互。AJAX在浏览器与Web服务器之间使用异步数据传输(HTTP请求),这样就可使网页从服务器请求少量的信息,而不是整个页面。

    2024年02月11日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包