news 2026/11/2 4:14:08

Spring Boot JDBC 直连 MySQL:版本、连接池与事务避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot JDBC 直连 MySQL:版本、连接池与事务避坑指南

简介:面向Java后端初学者与需要快速搭建数据库访问示例的开发者,围绕Spring Boot通过JDBC连接MySQL这一核心场景,提供可导入运行的工程源码与配套环境安装包,帮助解决依赖配置、数据库连接和基础增删改查落地等常见问题。压缩包共136个文件,约637.42MB,主体由dll运行库、xml配置、class字节码、java源码、sql脚本以及exe、msi安装程序等组成:安装包用于补齐数据库与运行环境,sql脚本便于初始化库表,源码与配置可直接对照调试。目前已有601人学习下载,适合课程实验、毕业设计起步或自学练手。读者可据此完成从环境准备、建库建表到控制器、实体类与启动类协同运行的完整链路,并参考连接参数、依赖配置和基础接口示例,减少反复试错,快速形成可复用的Spring Boot JDBC访问模板。

1. 一个「Connection refused」的下午

接手一个 Spring Boot 项目,启动日志第二行就是HikariPool-1 - Exception during pool initialization,底下一串Communications link failure。翻配置:URL 里库名拼错、驱动坐标还是老的mysql:mysql-connector-java、时间字段取出来差 8 小时。三处问题全在配置层,跟业务代码一行关系没有。

这套资源就是冲这个来的:一套 Spring Boot 用 JDBC 直连 MySQL 的整套可运行方案,源码、建表脚本、驱动 jar、MySQL 安装包放在一处,不引 MyBatis 也不引 JPA,SQL 写在代码里,链路短、报错清楚。适合三类人:要交课程设计但被 ORM 配置绕晕的、要给内部工具快速搭后端的、被层层框架报错折磨过、想看清原生 JDBC 到底怎么走的人。

2. 版本线先对齐:JDK、Spring Boot、驱动三者的兼容矩阵

2.1 三条线任意一条错位,编译期就过不去

JDBC 连 MySQL 这件事,代码本身不到五十行,翻车九成发生在「版本没对齐」。最典型的是把 Spring Boot 3.x 的代码贴进 2.7 的项目里,import javax.persistence.*和import jakarta.persistence.*直接红一片——Spring Boot 3 起全面迁到 jakarta 命名空间,这不是换个包名那么简单,是整个 Servlet / 持久化生态的坐标都换了。

先看一张对齐表,照着选就不会错:

Spring Boot最低 JDK命名空间驱动坐标驱动类
2.7.xJDK 8javax.*mysql:mysql-connector-javacom.mysql.cj.jdbc.Driver
3.0.x 及以上JDK 17jakarta.*com.mysql:mysql-connector-jcom.mysql.cj.jdbc.Driver

驱动坐标在 8.0.31 之后整体迁到了com.mysql这个 groupId 下,老坐标停更。两个坐标同时出现在 pom 里,Maven 会按最近路径选一个,另一个仍在 classpath 上,运行期可能出现No suitable driver或者认证插件不认识的怪问题。只留一个,这是硬规矩。

再说服务端和驱动的兼容。MySQL 5.7 的服务端用 8.x 驱动是能连的,反过来 8.0 服务端用 5.1.x 驱动就不建议——8.0 默认认证插件是caching_sha2_password,5.1.x 驱动不认识,报Unable to load authentication plugin。安装包里给的是 8.0 系列,配 8.x 驱动,别降级。

还有一处很容易踩:spring-boot-starter-jdbc和spring-boot-starter-data-jdbc名字只差一个词,前者是JdbcTemplate+ HikariCP + spring-tx,后者额外带了一套 Repository 抽象。这套方案要的是前者——能直接写 SQL 的那种。引错了不会报错,但你会莫名多出一堆用不上的接口。

2.2 依赖只引两个,用依赖树确认没被顶掉

pom 里的写法很克制,版本全交给 parent 管理:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <!-- 用 3.x 的话这里换版本,JDK 必须 17+ --> </parent> <dependencies> <!-- 自带 JdbcTemplate、HikariCP、spring-tx,不用单独引连接池 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- 驱动声明为 runtime,编译期不需要它的类 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

scope写runtime的理由:业务代码里不会出现com.mysql.cj.jdbc.Driver这个类名(Spring Boot 会根据 URL 自动推断),只有运行期加载驱动时才需要它。这么写还有个副作用——如果有人手贱在代码里写了Class.forName("com.mysql.cj.jdbc.Driver"),编译直接失败,反而逼你去掉这种老掉牙的写法。

引完别急着写代码,先跑两条命令核对:

# 确认驱动只有一个版本,group 是 com.mysql mvn dependency:tree -Dincludes=com.mysql # 确认连接池是 HikariCP,且版本跟 Spring Boot 对齐 mvn dependency:tree -Dincludes=com.zaxxer

输出里如果看到mysql:mysql-connector-java和com.mysql:mysql-connector-j同时存在,说明有别的 starter 传递进来了老坐标,用<exclusions>排掉。同理,出现commons-dbcp2或tomcat-jdbc,说明有依赖带了自己的池子,Spring Boot 的自动配置会按优先级挑一个,你以为在用 Hikari,实际跑的是别的——这种问题排查起来非常费劲,不如一开始就堵死。

还有一个更隐蔽的坑:如果你在配置类里手动new HikariDataSource()并注册成@Bean,Spring Boot 的DataSourceAutoConfiguration会整体退出,application.yml里spring.datasource.hikari.*那一段就成了摆设。要么全自动,要么全手动并用@ConfigurationProperties("spring.datasource.hikari")把 yml 绑到自己的 Bean 上,不要混着来。

2.3 建库建表:字符集、引擎、时间类型三个决定

代码写得再漂亮,表建错了后面全是补丁。这套资源里的脚本我按下面这个结构来,三项都锁死了:

CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE demo_db; CREATE TABLE t_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0, remark VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_status_created (status, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三个决定值得单独说。

字符集必须是utf8mb4。MySQL 里的utf8是utf8mb3的别名,最多 3 字节,emoji 和部分生僻字存不进去,插入时直接报Incorrect string value。这不是「以后再说」的事,改表要重建,数据量大了很痛苦。

引擎必须是InnoDB。MyISAM 不支持事务,@Transactional加在上面等于写了个注释,出异常照样提交,这个坑我在老项目里见过一次,对账时才发现数据多出一半。

时间类型优先DATETIME而不是TIMESTAMP。TIMESTAMP会按会话时区做转换,存进去和读出来经过两次换算,服务端时区、JVM 时区、JDBC 连接时区三者不一致时,差 8 小时是常事。DATETIME原样存取,只受连接时区影响一次,排查成本低很多。范围上TIMESTAMP只到 2038 年,也是个隐患。

amount用DECIMAL不用DOUBLE,金额场景没有任何讨论空间。索引idx_status_created是给「按状态查最近订单」这种列表页准备的,uk_order_no保证幂等插入能靠唯一键冲突兜底。

3. JdbcTemplate 打通增删改查:四个方法覆盖九成场景

3.1 数据源配置与 JdbcTemplate 的注入方式

JdbcTemplate不需要自己 new,starter 检测到 DataSource 就自动配好一个 Bean,构造器注入拿过来即可。配置集中在 yml:

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true username: app_user password: ${DB_PASSWORD:change_me} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1700000 connection-test-query: SELECT 1

URL 后面那一串参数不是装饰,每一个都对应一类故障:

参数作用不写会怎样
useUnicode+characterEncoding声明客户端编码中文写入后变问号,且不一定报错
serverTimezone告诉驱动服务端时区时间差 8 小时,或启动报时区值无法识别
useSSL关闭 TLS 握手无证书环境下每次连接刷一条长警告,连接也变慢
allowPublicKeyRetrieval允许首次认证时取服务端公钥报Public Key Retrieval is not allowed
rewriteBatchedStatements驱动把批量重写成多值 INSERT批量插入仍逐条发送,慢一个数量级

password用${DB_PASSWORD:change_me}这种占位符写法,本地开发有默认值,生产用环境变量覆盖,不要把明文密码提交到仓库。这是个习惯问题,不是技术问题,但出过事的人都不会省这一步。

DAO 层的骨架长这样,构造器注入、字段final,单测时可以直接new OrderDao(jdbcTemplate),不用起 Spring 容器:

@Repository public class OrderDao { private final JdbcTemplate jdbc; public OrderDao(JdbcTemplate jdbc) { this.jdbc = jdbc; } }

3.2 增删改查四个方法的完整骨架

新增要拿自增主键,用KeyHolder而不是再查一次:

public long insert(Order o) { KeyHolder kh = new GeneratedKeyHolder(); jdbc.update(con -> { PreparedStatement ps = con.prepareStatement( "INSERT INTO t_order(order_no, amount, status, remark) VALUES (?,?,?,?)", Statement.RETURN_GENERATED_KEYS); ps.setString(1, o.getOrderNo()); ps.setBigDecimal(2, o.getAmount()); ps.setInt(3, o.getStatus()); ps.setString(4, o.getRemark()); return ps; }, kh); Number key = kh.getKey(); // 联合主键时 getKey() 会返回 null,要用 getKeys() return key == null ? -1L : key.longValue(); }

kh.getKey()在单列自增主键下没问题,多列主键时要换成kh.getKeys()拿 Map。这个差异文档里写得不显眼,踩过一次就记住了。

查询单条我不用queryForObject,因为它查不到会抛EmptyResultDataAccessException,业务里「查不到」是正常分支,用异常表达不合适:

private static final RowMapper<Order> ROW_MAPPER = (rs, rowNum) -> { Order o = new Order(); o.setId(rs.getLong("id")); o.setOrderNo(rs.getString("order_no")); o.setAmount(rs.getBigDecimal("amount")); o.setStatus(rs.getInt("status")); o.setRemark(rs.getString("remark")); o.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); return o; }; public Order findByOrderNo(String orderNo) { String sql = "SELECT id, order_no, amount, status, remark, created_at " + "FROM t_order WHERE order_no = ?"; List<Order> list = jdbc.query(sql, ROW_MAPPER, orderNo); return list.isEmpty() ? null : list.get(0); }

RowMapper显式列名映射,比BeanPropertyRowMapper啰嗦,但字段改名时会立刻报错而不是静默返回 null,长期看更省心。rs.getTimestamp(...).toLocalDateTime()这一步不能省,直接getObject("created_at", LocalDateTime.class)在老驱动上支持不完整。

更新用影响行数做乐观并发控制,比SELECT ... FOR UPDATE轻得多:

public boolean updateStatus(long id, int expected, int target) { int rows = jdbc.update( "UPDATE t_order SET status = ? WHERE id = ? AND status = ?", target, id, expected); return rows == 1; }

rows == 1说明状态确实是从expected改到target的;返回 0 表示被别人抢先改了或者 id 不存在,调用方据此决定重试还是提示。删除同理,DELETE ... WHERE id = ?返回行数判断是否真的删掉。

3.3 占位符的两条边界,绕不过去

?占位符能传值,不能传标识符。ORDER BY ?这种写法不会报错,但会把传进去的字段名当成一个字符串常量排序,结果永远不变。动态排序必须走白名单映射:

private static final Map<String, String> SORTABLE = Map.of( "amount", "amount", "createdAt", "created_at" ); public List<Order> list(String sortField, boolean asc, int limit) { // 只允许映射表里出现过的字段,杜绝拼接注入 String column = SORTABLE.getOrDefault(sortField, "id"); String sql = "SELECT id, order_no, amount, status, created_at FROM t_order " + "ORDER BY " + column + (asc ? " ASC" : " DESC") + " LIMIT ?"; return jdbc.query(sql, ROW_MAPPER, limit); }

IN查询也得自己拼占位符,个数由集合长度决定:

public List<Order> findByIds(Collection<Long> ids) { if (ids == null || ids.isEmpty()) return List.of(); String ph = String.join(",", Collections.nCopies(ids.size(), "?")); String sql = "SELECT id, order_no, amount, status FROM t_order WHERE id IN (" + ph + ")"; return jdbc.query(sql, ROW_MAPPER, ids.toArray()); }

拼的是占位符个数,不是值本身,所以不会注入。要小心ids.toArray()的元素顺序必须和占位符一一对应,用Set的话顺序是不保证的——这个不影响IN的语义,但如果同时还有别的参数,位置就会错。

4. 连接池与事务:HikariCP 每个参数都在管一件事

4.1 池子开多大:一个公式和一张参数表

默认maximum-pool-size是 10,很多人为了「提升并发」直接改成 100,然后数据库max_connections被打满,报Too many connections。池子不是越大越好,连接数超过数据库的处理能力,只会把排队从应用层挪到数据库层,延迟反而更高。

经验公式是连接数 = CPU 核数 × 2 + 磁盘数,8 核机器配一块盘大概 17,取 16 或 20 都行。这个数字对大多数 CRUD 应用足够,真正的瓶颈通常在 SQL 和索引,不在池子大小。

几个参数按重要性排:

参数默认值建议值关键说明
maximum-pool-size108~20所有实例之和必须小于数据库max_connections
minimum-idle等于最大值2~5常驻连接数,设太高等于白占资源
connection-timeout30000ms3000~5000从池里取连接的等待上限,不是 SQL 执行超时
idle-timeout600000ms600000ms超出最小空闲的连接多久回收
max-lifetime1800000ms比wait_timeout小 30s连接的最大存活时间,超时主动重建
leak-detection-threshold0(关闭)测试环境 20000借出超过阈值打印持有堆栈
connection-test-query无不写MySQL 8 驱动支持isValid(),不需要测试语句

connection-timeout这个概念最容易理解错。它管的是「向池子借连接等了多久」,不是「SQL 跑了多久」。日志里出现Connection is not available, request timed out after 30000ms,说明池子被占满了,跟慢查询是两回事。而 SQL 本身的超时要用jdbcTemplate.setQueryTimeout()或Statement.setQueryTimeout()单独设。

max-lifetime与数据库wait_timeout的关系是生产事故高发区。MySQL 默认wait_timeout是 28800 秒(8 小时),连接空闲超过这个时间会被服务端单方面断开,而客户端池子里还认为它活着。第二天早高峰第一波请求打过去,拿到的全是死连接,报Communications link failure。解决办法就是让max-lifetime明显小于wait_timeout(比如 1700000 毫秒,约 28 分钟),连接在服务端动手之前自己先换掉。如果 DBA 把wait_timeout调到 60 秒而应用没跟着改,这个故障会以「每天固定时间抖一下」的形式出现,非常难定位。

leak-detection-threshold平时关着,只在测试环境打开。它会在连接借出超过阈值时打一条带堆栈的警告,直接指到没关连接的那行代码。别在生产开,日志量会失控。

4.2 事务边界:@Transactional 失效的六种情形

事务这块,JDBC 原生写法是setAutoCommit(false)+commit()+rollback(),Spring 里换成注解,但底层逻辑没变。一个正常的写事务:

@Service public class OrderService { private final OrderDao dao; private final JdbcTemplate jdbc; public OrderService(OrderDao dao, JdbcTemplate jdbc) { this.dao = dao; this.jdbc = jdbc; } @Transactional(rollbackFor = Exception.class) public long createAndAudit(Order o) { long id = dao.insert(o); jdbc.update("INSERT INTO t_audit(order_id, action) VALUES (?,?)", id, "CREATE"); return id; } }

rollbackFor = Exception.class不是可选项。Spring 默认只对RuntimeException和Error回滚,受检异常(比如IOException)抛出去事务照样提交。这个默认行为反直觉,写业务代码时统一标上比较安全。

更麻烦的是注解不生效的情况,它们全都不报错,只是「事务没起来」。整理成一张清单:

场景现象原因
同类内部this.method()调用异常抛了但数据没回滚绕过 AOP 代理,注解没被拦截
异常被 catch 住没再抛事务正常提交代理感知不到异常
方法不是public注解被忽略默认代理模式下只拦 public
表引擎是 MyISAM报错也不回滚引擎本身不支持事务
多数据源未指定transactionManager启动报 Bean 不唯一事务管理器有歧义
异常在finally里被吞或return提前部分提交提交动作在方法正常返回时触发

第一条是最高频的。@Transactional加在createAndAudit上,另一个同类方法直接createAndAudit(o)调用,走的是原始对象而不是代理对象,注解完全失效。要么把方法挪到另一个 Bean,要么注入自身代理,要么改用TransactionTemplate显式控制。

传播行为也该心里有数。默认REQUIRED,加入当前事务;写审计日志这类「业务失败也要留痕」的场景用REQUIRES_NEW,它会挂起当前事务、单独开一个。注意REQUIRES_NEW需要再借一条连接,池子只有 1 条连接时必然死锁——这是排查到天亮的那种问题。池子小的时候要算一下:最坏情况同时在跑的事务数 × 每个事务的连接数,不能超过maximum-pool-size。

5. 五个真实翻车现场,从连不上到连接泄漏

5.1 启动就 Communications link failure

现象:应用启动几秒后报HikariPool-1 - Exception during pool initialization,堆栈里是Communications link failure,The last packet sent successfully to the server was 0 milliseconds ago。

原因:0 milliseconds ago说明根本没握上手,问题在网络层而不是 SQL 层。常见四种:MySQL 服务没起、端口不是 3306、bind-address只监听127.0.0.1而应用跑在容器里、防火墙拦了端口。

解决:先撇开 Java 单独验端口,nc -zv 127.0.0.1 3306或直接用mysql -h ... -u ... -p连一次。能连说明是应用侧配置问题,不能连就查服务端:bind-address改成0.0.0.0(或具体网卡地址)、skip-networking必须关掉。容器里连宿主机时 URL 里写localhost一定不通,那是容器自己的回环,要写宿主机实际地址。另外别忽略端口映射,宿主机 3307 映射到容器 3306 是很常见的编排。

5.2 中文写进去变成问号

现象:插入「张三」,查询出来是??,而且不一定报错,静默损坏。

原因:三层编码任意一层不对都会这样——数据库/表的字符集是latin1或utf8mb3、连接 URL 缺characterEncoding、客户端与服务端character_set_client不一致。

解决:先确认库表字符集,SHOW VARIABLES LIKE 'character_set%'逐个看,character_set_database和character_set_connection都要是utf8mb4。表已经是utf8mb3的要ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4,注意这会重建表,大表挑低峰做。URL 里useUnicode=true&characterEncoding=utf8不能少,这里的utf8在驱动里映射到utf8mb4,写utf8mb4反而不认。

5.3 时间差 8 小时与时区告警

现象:数据库里存的是 10:00,Java 里取出来变 18:00;或者启动时刷一行The server time zone value 'xxx' is unrecognized。

原因:驱动需要知道服务端时区才能正确换算TIMESTAMP,没配就退回到 JVM 默认时区猜。容器里 JVM 时区经常是 UTC,一来一回差 8 小时。这属于典型的「玄学」问题,日志上看不出任何异常,只有业务对账时才发现。

解决:三处对齐。URL 里写死serverTimezone=Asia/Shanghai;JVM 启动参数加-Duser.timezone=Asia/Shanghai;字段类型优先用DATETIME避开二次转换。如果历史数据已经乱了,光改配置救不回来,得按业务含义写脚本修数据,改之前务必先备份。

5.4 跑一段时间池子被占满

现象:服务跑几小时后开始大量报Connection is not available, request timed out after 30000ms,重启就好,过几小时复发。

原因:连接泄漏。要么手写了dataSource.getConnection()忘了关,要么流式查询的ResultSet/Statement没关,要么在try块里return导致finally没执行到关闭语句。池子就那么大,泄漏一点耗一点,最后归零。

解决:临时打开leak-detection-threshold: 20000,复现一次就能从堆栈定位到具体行。根治是统一走JdbcTemplate(它内部保证关闭),确实要手写原生 JDBC 的用 try-with-resources:

try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement( "SELECT id, order_no FROM t_order WHERE status = ?")) { ps.setInt(1, 1); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 处理结果 } } } // 三层资源按逆序自动关闭

Connection、PreparedStatement、ResultSet三个都要关,只关最外层在某些驱动下不够干净。

5.5 批量插入慢十倍

现象:循环调insert插一万条要几十秒,rewriteBatchedStatements加了也没快多少。

原因:两个原因叠加。一是逐条insert每次都是一次独立的网络往返加一次磁盘刷,二是不在事务里,每条都自动提交。rewriteBatchedStatements只对batchUpdate生效,对循环调用update完全无效。

解决:改成batchUpdate,外面套事务,URL 加rewriteBatchedStatements=true,一次批 500~1000 条:

@Transactional(rollbackFor = Exception.class) public void batchInsert(List<Order> list, int batchSize) { for (int i = 0; i < list.size(); i += batchSize) { List<Order> chunk = list.subList(i, Math.min(i + batchSize, list.size())); jdbc.batchUpdate( "INSERT INTO t_order(order_no, amount, status) VALUES (?,?,?)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int idx) throws SQLException { Order o = chunk.get(idx); // 注意 idx 是块内下标,不是 list 的下标 ps.setString(1, o.getOrderNo()); ps.setBigDecimal(2, o.getAmount()); ps.setInt(3, o.getStatus()); } @Override public int getBatchSize() { return chunk.size(); } }); } }

setValues里的idx是当前批次内的下标,写list.get(idx)会重复插入前面的数据,这个错误很隐蔽——数据条数对得上,但内容是错的。批次别开太大,2000 以上容易撞max_allowed_packet,报Packet for query is too large。

6. 进阶:把「能连上」变成启动时的一次断言

配置类问题最好的发现时机是启动那一刻,不是上线三小时后。写一个ApplicationRunner,启动即执行一次完整链路验证,失败就让它启动失败:

@Component public class DbStartupChecker implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(DbStartupChecker.class); private final JdbcTemplate jdbc; private final DataSource dataSource; public DbStartupChecker(JdbcTemplate jdbc, DataSource dataSource) { this.jdbc = jdbc; this.dataSource = dataSource; } @Override public void run(ApplicationArguments args) throws Exception { // 1. 真实往返一次,验证账号权限和库是否存在 String version = jdbc.queryForObject("SELECT VERSION()", String.class); // 2. 验证字符集,避免中文静默损坏 String charset = jdbc.queryForObject( "SELECT @@character_set_database", String.class); // 3. 验证连接池实际状态 HikariDataSource hikari = dataSource.unwrap(HikariDataSource.class); log.info("db connected: version={}, charset={}, pool active={}, idle={}, total={}", version, charset, hikari.getHikariPoolMXBean().getActiveConnections(), hikari.getHikariPoolMXBean().getIdleConnections(), hikari.getHikariPoolMXBean().getTotalConnections()); if (!"utf8mb4".equalsIgnoreCase(charset)) { log.warn("database charset is {}, emoji and some CJK chars may fail", charset); } } }

dataSource.unwrap(HikariDataSource.class)这一步能拿到池子的 MXBean,把活跃、空闲、总数三个数打出来。启动时total应该等于minimum-idle,随着请求上来active会涨。上线后如果发现active长期等于maximum-pool-size且idle是 0,说明池子不够或者有慢 SQL 占着不放,这是最直观的容量信号。

上线前我习惯过一张核对表,比事后翻日志便宜太多:

检查项命令期望
端口连通nc -zv host 3306succeeded
账号权限SHOW GRANTS FOR 'app_user'@'%'只有业务库的 DML 权限
库字符集SELECT @@character_set_databaseutf8mb4
时区SELECT @@global.time_zone, @@session.time_zone与 URL 中serverTimezone一致
引擎SHOW TABLE STATUS LIKE 't_order'InnoDB
连接上限SHOW VARIABLES LIKE 'max_connections'大于实例数 × 池大小
空闲超时SHOW VARIABLES LIKE 'wait_timeout'明显大于max-lifetime

有一年我给一个内部工具配max-lifetime: 0,想着「连接永不过期,省得重建」,结果周一早上整片请求失败——周末两天连接全被数据库按wait_timeout清掉了,池子里剩的全是死连接。从那以后我每次上生产都强制走一遍上面这张表,再加一次「挂机 12 小时后打一发请求」的复测,宁可多花半小时,也不想在周一早上被叫起来。

资源里的 pom、yml、建表脚本、DAO 骨架和这个自检类都是配套的,把包名和库名替换掉就能直接跑起来。按第 2 章的顺序对齐版本、第 3 章把 DAO 跑通、第 5 章那五条提前避掉,剩下的事就是写你自己的 SQL 了。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 4:14:08

2026年AI论文平台推荐与高效文献管理技巧

1. 为什么需要关注AI论文网站&#xff1f;作为一名在人工智能领域摸爬滚打多年的研究者&#xff0c;我深知获取前沿学术资源的重要性。2026年的AI研究格局正在发生微妙变化&#xff0c;传统顶会依然重要&#xff0c;但新兴的开放学术平台正在改变知识传播方式。最近我的导师团队…

作者头像 李华
网站建设 2026/11/2 4:13:54

Excel VBA中用ListView控件实现高性能动态数据展示

1. 项目概述&#xff1a;为什么一个Listview控件值得你花两小时重写Excel界面“Excel VBA实战&#xff1a;用Listview控件打造动态数据展示界面&#xff08;附完整代码&#xff09;”——这个标题里藏着三个被绝大多数Excel用户长期忽视的关键事实&#xff1a;第一&#xff0c;…

作者头像 李华
网站建设 2026/11/2 4:13:52

工作流插件节点:自动化工具链的核心构建单元

1. 工作流插件节点核心概念解析工作流插件节点是现代自动化工具链中的基础构建单元&#xff0c;它们如同乐高积木般通过标准化接口相互连接&#xff0c;形成完整的业务处理流水线。以Dify、n8n为代表的低代码平台之所以能实现复杂业务流程的快速搭建&#xff0c;核心就在于这些…

作者头像 李华
网站建设 2026/11/2 4:12:58

英语教学Agent实时架构:WebSocket+FastAPI+React实战

1. 这不是又一个“AI英语课件”&#xff0c;而是一个能实时对话、动态生成场景、自动纠音的英语教学Agent我去年带一个成人英语提升班&#xff0c;发现传统App和录播课最大的问题不是内容不好&#xff0c;而是“它根本不知道你卡在哪”。学员说“I go to school yesterday”&am…

作者头像 李华
网站建设 2026/11/2 4:12:17

1Z0-829 练习册实战指南:从刷题到通过率的备考策略

简介&#xff1a;这份资源是面向准备 OCP Oracle Certified Professional Java SE 17 Developer&#xff08;1Z0-829&#xff09;认证考试的开发者与在校学生的实战练习资料&#xff0c;由 Jeanne Boyarsky 与 Scott Selikoff 编写&#xff0c;John Wiley & Sons 出版。内容…

作者头像 李华
网站建设 2026/11/2 4:12:13

AI智能体在制造业落地:难点与解决方案商能力评估

AI智能体在制造业的落地难点与解决方案商能力这两年"AI智能体"这个概念火到什么程度&#xff1f;圈内圈外都在聊&#xff0c;互联网大厂追着发布Agent框架&#xff0c;创业公司恨不得把产品名字里都带上"智能体"三个字。我是做制造业数字化咨询的&#xff…

作者头像 李华