Java连接MySQL全流程详解:JDBC原理、连接池、事务与性能优化

只要你的 Java 项目叫“业务系统”,就逃不开 MySQL。这句话我说了快十年,带过的新人没有一个能反驳。很多人第一次连数据库,都是在搜索引擎里复制一段三行代码,跑通了就丢到生产环境,然后被时区报错、中文乱码、连接超时、数据库连接数被打满这些事按在地上反复摩擦。

这篇我把“Java 连接 MySQL 并实现数据交互”从头到尾拆开讲,不只有能跑的 demo,还包括驱动选型、连接 URL 参数、CRUD 实操、事务与批处理、连接池配置、高频报错排查、性能优化。新手看完能独立接库里最简单的读写需求,写过一阵子但一直靠复制粘贴的人,也能把那些“为啥别人不出错就我出错”的疑惑一次性理清。

1. JDBC 核心原理与驱动选型

1.1 JDBC 到底解决了一个什么问题

Java 要操作 MySQL,绕不开一个标准:JDBC(Java Database Connectivity)。它本质上是一套接口规范,定义了一套统一的操作数据库的方式。你写代码的时候面向这套接口,而真正跟 MySQL 方言打交道的是 MySQL 官方提供的驱动实现。

你可以把 JDBC 类比成 USB 接口。电脑不关心你插的是 U 盘还是手机,只要对方遵守 USB 协议就能通信。JDBC 就是这个协议,MySQL 驱动就是那个把 MySQL 特有的网络协议翻译成 JDBC 接口语言的“转接头”。

这套设计带来的实际好处是:你学会用 JDBC 连 MySQL,将来换 PostgreSQL、Oracle,代码结构基本不用改,只换驱动包和连接 URL 就行。可不要小看这个抽象,实际项目里很多团队做多数据库支持,靠的就是这一层。

从执行流程看,一次 JDBC 交互无非五步:

  1. 加载并注册驱动(8.0 之后其实可以省略显式加载这一步,后面讲)。
  2. 通过 DriverManager 或 DataSource 获取连接 Connection。
  3. 创建 Statement 或 PreparedStatement,携带 SQL。
  4. 执行 SQL,拿到 ResultSet 结果集或影响行数。
  5. 依次关闭 ResultSet、Statement、Connection(按创建顺序的反向关闭)。

这五步贯穿整篇,后文所有实操代码都是这条主线上长出来的。

1.2 驱动选择:版本和坐标,一步都不能错

选驱动是第一道坎,也是第一个很容易踩坑的地方。MySQL 驱动有两个大版本线:5.1.x 系列和 8.x 系列。如果你的 MySQL 是 5.7,用 5.1.49 没问题;如果是 8.0,最好用 8.0 系列驱动,因为 8.0 驱动完整支持 MySQL 8 的默认认证插件 caching_sha2_password,还会自动协商更优的字符集和功能。

这里有个细节值得强调:MySQL 5.x 驱动和 8.x 驱动的包路径前缀不一样。老驱动是com.mysql.jdbc.Driver,8.0 之后统一改成com.mysql.cj.jdbc.Driver。网上大量老文章还在用老路径,直接复制到新工程就会报ClassNotFoundException。所以看到任何教程,先确认它用的驱动版本和包路径是否匹配你项目里的 jar。

另外,Maven 坐标也有变化。较新的驱动坐标是这样:

<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.4.0</version> </dependency>

注意 artifactId 是mysql-connector-j,不是早年常见的mysql-connector-java。后者虽然还在仓库里,但新版本已经改名为前者。至于要不要用最新版,我的建议是:不要盲目追新,选一个已经在大规模生产环境验证过的稳定版本,比如 8.0.33。数据库驱动这东西,稳定压倒一切。

提示:驱动版本与 MySQL 服务端版本不必完全一致,但差距不要跨大版本。用 8.4 驱动连 5.7 库通常没问题,反过来用 5.1 驱动连 MySQL 8.0 就可能遇到认证插件不兼容。

2. 环境准备与基础工程搭建

2.1 数据库侧的准备:库、账号、权限

很多初学者直接在 root 用户下写代码,这个习惯非常不好。生产环境最好不要用 root 跑业务应用,权限过大,误操作或注入漏洞时影响面不可控。我会单独建一个业务账号,只给它需要的最小权限。

登录 MySQL 后执行:

CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'demo_user'@'%' IDENTIFIED BY 'DemoPass123'; GRANT SELECT, INSERT, UPDATE, DELETE ON demo_db.* TO 'demo_user'@'%'; FLUSH PRIVILEGES;

这里有两个细节很多人踩过:第一,字符集一定要用utf8mb4,不是utf8。MySQL 的utf8最多只能存 3 个字节,像 emoji 表情和部分生僻字存不进去,utf8mb4是真正的 4 字节 UTF-8,兼容性完整。第二,如果你用 MySQL 8.0 默认创建用户,默认认证插件是caching_sha2_password,Java 驱动连接时如果没在 URL 里配置allowPublicKeyRetrieval=true,可能会在握手阶段报错,这个我放在后面的问题排查部分详细讲。

数据库里顺手建一张测试表,我们后面所有演示都基于它:

USE demo_db; CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL, email VARCHAR(128) DEFAULT NULL, age INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;

选择InnoDB引擎不用犹豫,它是目前 MySQL 默认且最成熟的存储引擎,支持事务、行级锁和崩溃恢复。除非你做全文检索或临时缓存,否则别动引擎。

2.2 Maven 工程与依赖配置

项目骨架我会用一个最精简的 Maven 工程,不用 Spring Boot,先把 JDBC 底层的原理摸透。这很重要——一旦你理解了原生 JDBC,再去看 Spring 的 JdbcTemplate、MyBatis,会发现它们不过是对这套流程的封装。

pom.xml里除了驱动依赖,我还会加一个日志依赖用来演示排查思路:

<dependencies> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.36</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> </dependencies>

为什么不直接用 Spring Boot?因为 Spring Boot 内置的数据源自动配置会帮你把连接池、驱动加载、事务管理都处理好,好用的同时也会掩盖很多底层细节。一旦出问题,排查成本会更高。先用原生 JDBC 把每一步跑通,再上框架,是最稳妥的学习路径,也是遇到问题能快速定位的基础。

工程结构建议按职责分包:config放连接配置,dao放数据访问代码,entity放与表结构对应的实体类,util放工具方法。别看业务小,从第一天开始就养成分层习惯,后面维护会省很多事。

3. 核心代码实录:连接、CRUD 与事务

3.1 连接 URL 的五段式拆解

拿到连接的第一步是写对 URL。JDBC URL 的格式是:

jdbc:mysql://主机地址:端口/数据库名?参数1=值1&参数2=值2

逐项拆开看:

  • jdbc:mysql://:协议头,驱动靠它识别这是 MySQL。
  • 主机地址:可以是localhost、IP,也可以是云数据库的内网地址。
  • 端口:默认3306,如果你改过,这里必须对应。
  • 数据库名:就是你要连的那个库,比如demo_db。注意 URL 里的库和连接账号的权限是两码事,账号必须有该库的权限才能连上。
  • ?后的参数:核心配置都在这里。

一段生产环境可用的 URL 长这样:

jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true

逐个解读参数:

参数作用说明
useUnicode=true&characterEncoding=utf8保证客户端与服务端之间的字符集一致缺失会导致中文乱码
useSSL=false关闭 SSL 连接本地开发与内网环境建议关闭,省去证书配置成本;公网传输再按需开启
serverTimezone=Asia/Shanghai指定服务器时区高版本驱动必须显式设置,否则可能报时区错误或时间差 8 小时
allowPublicKeyRetrieval=true允许客户端自动获取服务端公钥MySQL 8 默认认证插件下常见需要此参数
rewriteBatchedStatements=true开启批处理语句重写批量插入性能提升的关键,后面会讲

第一次连库我建议先不加任何参数,看看驱动默认行为,再加上这些参数逐个体会差异。很多时候你只知道“加参数解决报错”,但不知道为什么,换台机器换个环境照样抓瞎。

获取连接的代码写出来非常简单:

public class DBConfig { private static final String URL = "jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "demo_user"; private static final String PASSWORD = "DemoPass123"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

注意 8.0 驱动里Class.forName("com.mysql.cj.jdbc.Driver")可以省略,因为 JDBC 4.0 之后驱动通过META-INF/services自动注册。但很多老项目还保留这行代码,保留也不会错。你只需要确保引入的 jar 包路径正确即可。

3.2 Statement 与 PreparedStatement:老生常谈但值得再聊一次

获取连接之后,实战里第一个分歧点来了:用Statement还是PreparedStatement?

很多初学者因为Statement写起来短,就顺手用了。但Statement有两个致命问题:SQL 注入风险和 SQL 编译开销。

SQL 注入的原理是拼接字符串。假设登录 SQL 是这样的:

String sql = "SELECT * FROM t_user WHERE username = '" + name + "' AND password = '" + pwd + "'";

如果name传入admin' --,拼出来的 SQL 就变成了:

SELECT * FROM t_user WHERE username = 'admin' --' AND password = '...'

--是 MySQL 的注释符,它后面的校验条件全部失效,攻击者直接就拿到了 admin 账号。这不是危言耸听,这是真实世界里反复出现的攻击手法。

PreparedStatement用占位符预编译 SQL,参数作为单独数据传递,数据库层会做元数据解析和类型校验,从机制上杜绝了拼接注入。同时它还有缓存执行计划的好处,同一句 SQL 反复执行时性能更好。

标准的查询写法:

public User findByUsername(String username) { String sql = "SELECT id, username, email, age, created_at FROM t_user WHERE username = ?"; try (Connection conn = DBConfig.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getLong("id")); u.setUsername(rs.getString("username")); u.setEmail(rs.getString("email")); u.setAge(rs.getInt("age")); u.setCreatedAt(rs.getTimestamp("created_at")); return u; } } } catch (SQLException e) { log.error("query user failed, username={}", username, e); } return null; }

这段代码里有两个细节要说:

第一,try-with-resources。这是 Java 7 引入的语法糖,能自动关闭实现了AutoCloseable的资源。比起在 finally 里手动关闭漏写一行,这个方式又安全又干净。注意内部打开的 ResultSet 也会在 try 结束时被关闭。

第二,setXxx 方法必须与列类型对应。setString对应字符串列,setInt对应整数列,如果用错,数据库可能隐式转换,性能下降不说,还可能出现不可预期的结果。ResultSet读取时也一样,getTimestamp拿时间列,getLong拿 BIGINT 列。

插入数据的写法:

public int insertUser(User u) { String sql = "INSERT INTO t_user(username, email, age) VALUES (?, ?, ?)"; try (Connection conn = DBConfig.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, u.getUsername()); ps.setString(2, u.getEmail()); ps.setInt(3, u.getAge()); int rows = ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { u.setId(keys.getLong(1)); } } return rows; } catch (SQLException e) { log.error("insert user failed", e); } return 0; }

这里第二个参数Statement.RETURN_GENERATED_KEYS是关键。你在 MySQL 里自增的 id,执行完 insert 后数据库才生成,通过getGeneratedKeys()才能拿到。不想要这个值可以不传,但实际业务里拿回主键的场景非常多。

3.3 事务提交与批处理写入

默认情况下,连接处于自动提交模式,每执行一条 SQL 就立即提交。这在单条 SQL 写入时没问题,但遇到“多个步骤必须一起成功,一起失败”的场景就不行了。

最常见的例子是转账:扣减 A 账户余额 + 增加 B 账户余额,两步必须绑定成一个原子操作。中间任何一步失败,整个操作都要回滚,否则账就平不了。

手动事务的标准写法:

public boolean transfer(Long fromId, Long toId, BigDecimal amount) { String updateSql = "UPDATE t_account SET balance = balance - ? WHERE id = ?"; String addSql = "UPDATE t_account SET balance = balance + ? WHERE id = ?"; Connection conn = null; try { conn = DBConfig.getConnection(); conn.setAutoCommit(false); try (PreparedStatement psFrom = conn.prepareStatement(updateSql); PreparedStatement psTo = conn.prepareStatement(addSql)) { psFrom.setBigDecimal(1, amount); psFrom.setLong(2, fromId); psFrom.executeUpdate(); // 模拟中间异常 // if (true) throw new RuntimeException("mock error"); psTo.setBigDecimal(1, amount); psTo.setLong(2, toId); psTo.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); log.error("transfer failed, rollback", e); return false; } } catch (SQLException e) { log.error("get connection failed", e); return false; } finally { if (conn != null) { conn.setAutoCommit(true); // 恢复自动提交,避免连接归还连接池时状态异常 conn.close(); } } }

事务的几个要点:

  • setAutoCommit(false)必须在拿到连接后、执行业务 SQL 前调用。
  • 只要出现SQLException或业务异常,就调用rollback(),把已执行但未提交的操作全部回滚。
  • finally 里必须把连接状态恢复为自动提交,再归还连接池。这是很多踩坑点,连接池里的连接对象是复用的,如果上一次事务没恢复自动提交,下一个人拿到同一个连接就全部失效了。
  • 事务尽量短,别在事务里执行网络请求、文件读写等耗时的外部操作。锁持有得越久,并发冲突概率越高。

批处理是另一个能明显提升性能的关键。它解决的问题是:一次循环插入一万条数据,如果每条都走一次网络往返,慢到你怀疑人生。批处理把这些 SQL 攒到一定量再发到数据库,网络开销直接从一万次变成几十次。

public void batchInsert(List<User> userList) { String sql = "INSERT INTO t_user(username, email, age) VALUES (?, ?, ?)"; try (Connection conn = DBConfig.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); int count = 0; for (User u : userList) { ps.setString(1, u.getUsername()); ps.setString(2, u.getEmail()); ps.setInt(3, u.getAge()); ps.addBatch(); count++; if (count % 500 == 0) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit(); } catch (SQLException e) { log.error("batch insert failed", e); } }

这里有三点经验送给你:

第一,批次大小建议控制在 500 到 1000 条左右。太小的话批处理优势不明显,太大会导致单次提交的 SQL 包过大,数据库解析压力陡增,甚至内存溢出。

第二,URL 要加rewriteBatchedStatements=true。不加这个参数,MySQL 驱动只是简单地把多条 SQL 依次发给服务端,并没有真正合并。加了之后驱动会把同一条 SQL 的多组参数重写为 MySQL 原生的VALUES (...),(...)多值语法,实测下来批量插入性能提升 5 到 10 倍是常事。

第三,批量操作也建议走手动事务,一批一提交。既可以避免中途失败导致全部回滚,又可以减少提交次数。

4. 连接池:从 demo 走向生产的必修课

4.1 DriverManager 直连为什么撑不住真实场景

前文所有示例都在用DriverManager.getConnection()。这种方式在学习的 demo 里没问题,但它有个致命问题:每次调用都会创建一个全新的物理连接。而 MySQL 建立连接的过程涉及 TCP 握手、认证、权限校验,耗时往往在几十毫秒到几百毫秒之间。高并发场景下,大量请求同时创建连接,数据库服务器很快就会被拖垮。

打个比方,直连模式就像每次吃饭都从零开始搭灶台、生火、做饭。连接池就好比一个常年开火的餐厅后厨,灶台提前备好,客人点菜直接炒,吃完把锅放回炉边,下个客人继续用。

生产环境必须使用连接池。连接池的核心思路是:初始化时创建一批连接,放在池子里复用;应用需要时从池里借,用完归还;池子空了就排队等待,而不是无限新建。

连接池选型上,我会优先推荐 HikariCP。知名连接池 DBCP、C3P0 各有拥趸,但 HikariCP 性能指标几乎全面领先,字节码优化做到极致,Spring Boot 2.x 以后也把它作为默认连接池。另一个选择是阿里巴巴的 Druid,它的监控面板更丰富,适合需要可视化运维和数据源统计的业务场景。如果对监控没有特别需求,就用 HikariCP,轻量而且够快。

4.2 HikariCP 参数配置实录

在 Maven 里加入依赖后,用HikariDataSource替换掉原来的DriverManager:

<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.1.0</version> </dependency>

配置代码:

public class DataSourceConfig { public static HikariDataSource createDataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl(URL); config.setUsername("demo_user"); config.setPassword("DemoPass123"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setIdleTimeout(600000); config.setConnectionTimeout(30000); config.setMaxLifetime(1800000); config.setPoolName("DemoHikariPool"); return new HikariDataSource(config); } }

这些参数有一个基本逻辑:

  • maximumPoolSize:池子能容纳的最大连接数。不是越大越好。每个连接背后都是一个 TCP 连接和一个 MySQL 服务端线程,连接数超过数据库极限反而会引发宕机。一个常用的估算口径是:并发峰值 × (单请求平均耗时 / 1000),再乘以一个余量系数。假设峰值并发 100,平均请求耗时 200ms,那么池子大小约为100 × 0.2 = 20。
  • minimumIdle:池子至少保留的空闲连接数。设置 0 也可以,但通常保留几个能降低冷启动延迟。
  • connectionTimeout:应用从池子拿连接的等待超时。30 秒是相对保守的数值,具体要看你的最大请求耗时。如果业务 SQL 平均只需几十毫秒,却要等 30 秒才报错,那对用户体验来说太久了。
  • maxLifetime:连接的最大存活时间。MySQL 服务端默认的wait_timeout是 8 小时,池子里长期不用的连接可能被服务端断开,客户端却不知道,下次调用时就会报“连接失效”。把 maxLifetime 设得小于服务端超时时间就能规避。

配置好之后,业务代码里获取连接只需要dataSource.getConnection(),用完close()归还连接池而不是真正关闭。注意前面说的事务恢复问题,在这里同样适用:连接归还前必须把autoCommit恢复成 true,否则污染连接状态。

5. 高频问题与排查思路实录

5.1 六个现场报错速查表

下面这六个错误,是我在不同项目里见过最频繁的,每一个都能单独写一篇事故复盘。

报错关键信息根本原因解决方案
ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动 jar 未引入,或包路径不对检查 pom.xml 依赖,确认驱动版本对应的路径
Access denied for user 'xxx'@'localhost'用户名、密码错误,或账号没有对目标库的访问权限用客户端工具登录验证;检查 GRANT 语句
Communications link failure网络不通、端口未开放、MySQL 服务未启动ping主机,telnet 主机 3306测试端口,查看 MySQL 进程
The server time zone value '...' is unrecognized驱动版本与服务器时区不匹配URL 加serverTimezone=Asia/Shanghai
Public Key Retrieval is not allowedMySQL 8 的 caching_sha2_password 认证机制URL 加allowPublicKeyRetrieval=true
Unknown character set: 'utf8mb4'或中文乱码数据库、表、连接三侧字符集不一致统一为 utf8mb4,URL 加characterEncoding=utf8

这里要单独展开讲一下时区问题和Public Key Retrieval。

时区报错的完整信息往往带着一堆时区候选值,新手看了就懵。本质是:MySQL 8.0 驱动默认要求客户端提供准确的时区,用来把数据库的TIMESTAMP类型换算成本地时间。如果你不指定,服务端时区信息又不明确,驱动就直接抛异常。就算你运气好没报错,连接参数不规范也容易导致时间字段差 8 小时——那是另一个隐蔽坑,调了它连接让你怀疑人生。

Public Key Retrieval is not allowed只要出现在 MySQL 8.0 以上版本。原因是 MySQL 8 默认认证插件caching_sha2_password在首次认证时要求交换 RSA 公钥,客户端默认出于安全考虑不允许自动获取,于是握手失败。内网开发环境加allowPublicKeyRetrieval=true是最省事的解法,但生产环境如果对安全要求高,可以考虑先通过 SSL 连接或者预置公钥。

5.2 排查工具与方法

遇到连接问题,我的排查序列是固定的,不会盲目改代码:

第一步,用数据库客户端验证账号和密码本身是否可用。很多所谓“代码连接失败”,其实是密码里带了特殊字符被 URL 解析搞坏,或者账号只在某个 host 下授权了。用客户端能连上,再看代码。

第二步,检查网络层。ping一下主机看通不通,telnet 主机 3306看端口是否可达。云数据库尤其容易在安全组层面挡掉端口,这是本地环境根本复现不出来的问题。

第三步,打开驱动日志。JDBC 驱动本身支持调试日志,Java 端可以通过-Djava.util.logging.config.file或接入 SLF4J 查看驱动输出的连接过程,能看到它在哪个阶段出错。比如 SSL 握手失败、认证类型不支持这些,日志里都会留下明确线索。

第四步,测试最小可复现用例。排除框架干扰,写一个只有 JDBC 连接的 Java 类直接跑。很多时候问题出在 Spring 配置叠加了一层,或者 MyBatis 的拦截器搞了鬼。最小用例能让问题迅速归因。

6. 数据交互层面的性能优化

6.1 永远先检查 SQL 与索引

连接池优化解决的是“连接”层面的瓶颈。但真实系统里,大多数慢查询的根因其实在 SQL 本身。一次全表扫描和一次索引命中的差距,在百万行数据上可能是几十毫秒和几秒的差别。

排查慢 SQL 最直接的命令:

EXPLAIN SELECT * FROM t_user WHERE username = 'demo';

看type那一列:如果出现ALL,就是全表扫描,数据量大时会铺开读磁盘;如果是ref、const这类索引命中的类型,说明查询用上了索引。username如果不在任何索引上,就要考虑加索引:

ALTER TABLE t_user ADD INDEX idx_username (username);

注意加索引不是无脑加。索引会占用额外空间,每次插入、更新都要维护索引结构,写多读少的表加太多索引反而拖慢写入。一个经验法则是:索引优先加在 WHERE 条件频繁出现的列、JOIN 关联列和 ORDER BY 排序列上。

另一个常见问题是SELECT *。在真实业务里,一个表几十个字段,你只需要其中三个,结果集全拉回来,传输和内存开销全部浪费。写查询时只选择需要的列,这个习惯带来的收益常被低估。在数据量大的场景下,从SELECT *改成指定列,响应时间往往肉眼可见地下降。

6.2 写入侧优化:批处理与事务粒度

写入方向上,最大的优化空间在批处理和事务的合理组合。前面演示过addBatch,这里再补充一个更常见的现实问题:一条一条地 insert 一万行数据。

不优化版:一万次网络往返 + 一万次事务提交,耗时可能几十秒甚至几分钟。优化版:分批 500 条一组,每组一个事务,配合rewriteBatchedStatements=true,同样一万条数据,实测可以压到 1 秒内到 2 秒左右。

批处理还有一个隐藏价值:它天然限制了事务粒度。每批 500 条单独成一事务,中途失败了,最多回滚这 500 条,不会影响前面已经成功提交的数据。这在数据导入场景非常实用,你不需要手动写断点续传,只要每批独立提交,失败后从失败批次重跑即可。

事务粒度上还有一个反向陷阱:有人在批处理时把所有数据放在一个事务里,想着“反正分批插入,失败就全部回滚”。结果到了千万级数据导入,单事务持锁时间过长,锁等待和回滚日志膨胀会带来更大问题。事务做小一点,风险边界也就小一点。

最后忠告:连接池参数、批次大小这些数值,不要在网上抄完就完事。参数是跟业务特征绑定的,读多写少的系统、批量导入的系统、实时交易系统,最优配置完全不同。上线前做一轮简单的压测,把连接池最大值、最小空闲从当前值上下浮动几个档位,观察响应时间和服务端连接数,再定最终值。数据交互这块没有银弹,只有不停地测量和调整。

我个人这几年带项目的体会是:JDBC 这块内容虽然“老”,但它是一切 ORM 框架的地基。你懂了连接 URL 每个参数的意义,再看 MyBatis 的dataSource配置就是透明的;你懂了事务提交和连接归还的生命周期,再复杂的分布式事务方案也能分辨它到底在哪个环节做补偿。地基打不牢,上面盖什么都慌。

相关新闻

从零实现轻量级图表编辑器:Canvas渲染与数据驱动架构解析

从零实现轻量级图表编辑器:Canvas渲染与数据驱动架构解析

图表设计工具这类项目&#xff0c;名字叫 "diagram-design" 的其实蛮多的&#xff0c;但真正能让人愿意截图发出来的产品很少。我这次想聊的&#xff0c;是我自己从零搭起来的一个轻量级图表设计方案——一个跑在浏览器里的流程图、架构图编辑器。核心思路很简单&…

2026/10/19 18:04:45 阅读更多 →
从性能瓶颈到Canvas渲染:Vue 3打造高性能Markdown编辑器

从性能瓶颈到Canvas渲染:Vue 3打造高性能Markdown编辑器

直接从标题说开去。我最近在技术社区里看到这个开源项目时&#xff0c;第一反应是"又一个Canvas编辑器轮子"&#xff0c;但仔细看完性能和实现思路之后&#xff0c;忍不住动手折腾了一把。基于 Vue 3 Canvas 的高性能 Markdown 编辑器&#xff0c;宣称性能提升 10-4…

2026/10/19 18:04:45 阅读更多 →
ESP32量产级固件基座:OTA+BLE Mesh+低功耗唤醒一体化方案

ESP32量产级固件基座:OTA+BLE Mesh+低功耗唤醒一体化方案

简介&#xff1a;本资源是小智源码项目1.5.6_2版本的完整嵌入式开发包&#xff0c;面向ESP32系列&#xff08;含ESP32-S3、ESP32-C3&#xff09;硬件平台的IoT开发者与嵌入式学习者&#xff0c;聚焦于LCD驱动适配、触摸屏交互、多设备兼容性支持及低功耗感知应用开发。压缩包共…

2026/10/19 18:04:45 阅读更多 →

最新新闻

开源代码审查工具open-code-review:基于diff的自动化规则引擎实践

开源代码审查工具open-code-review:基于diff的自动化规则引擎实践

搞代码审查这么多年&#xff0c;我一直觉得这事不该靠人海战术硬扛。团队一忙起来&#xff0c;review 就成了走过场&#xff0c;可线上出了问题&#xff0c;背锅的还是 review 环节。所以我花了不少业余时间做了一个开源的代码审查辅助工具&#xff0c;名字就叫 open-code-revi…

2026/10/19 19:41:00 阅读更多 →
AI辅助教材写作:从初稿到低查重的完整方法论

AI辅助教材写作:从初稿到低查重的完整方法论

上个月接了一本专业教材的编写任务&#xff0c;时间紧、资料杂、章节多&#xff0c;刚开始我直接用AI批量生成初稿&#xff0c;效率确实拉满&#xff0c;但第一轮内部查重就傻了眼——好几个章节重复率直接超标。后来我花了两周时间磨合出一套完整的干活流程&#xff1a;用AI辅…

2026/10/19 19:41:00 阅读更多 →
AI辅助教材编写:低查重工具选型与实操指南

AI辅助教材编写:低查重工具选型与实操指南

教材编写这活儿&#xff0c;干过的人都懂。资料摞了一桌子&#xff0c;键盘前坐了一下午&#xff0c;真正落到文档里的字可能还不到两千。不是不会写&#xff0c;是每个知识点都得反复琢磨怎么表达才准确、怎么组织才符合教学逻辑&#xff0c;尤其还要躲开一个隐形红线——查重…

2026/10/19 19:41:00 阅读更多 →
交换机堆叠技术实战:原理、配置与运维全解析

交换机堆叠技术实战:原理、配置与运维全解析

1. 堆叠技术到底解决了什么痛点第一次接触堆叠这个概念&#xff0c;是在一个园区网的改造项目里。当时接入层有六台交换机&#xff0c;每台都单独跑生成树协议&#xff0c;结果网络里一半的链路被阻塞&#xff0c;带宽白白浪费&#xff0c;而且任意一台设备宕机&#xff0c;下面…

2026/10/19 19:41:00 阅读更多 →
万可750/753系列PROFINET GSDML-V2.33文件导入与组态实战指南

万可750/753系列PROFINET GSDML-V2.33文件导入与组态实战指南

简介&#xff1a;这份资源是德国万可&#xff08;WAGO&#xff09;系列750与753数字输入输出扩展模块的GSD硬件配置文件&#xff0c;版本为GSDML-V2.33&#xff0c;发布日期2021年1月15日&#xff0c;面向工业自动化领域的电气工程师、系统集成人员及PLC编程调试人员。GSD文件用…

2026/10/19 19:40:59 阅读更多 →
DAG-GNN:从结构学习到因果发现,用图神经网络学有向无环图

DAG-GNN:从结构学习到因果发现,用图神经网络学有向无环图

最近我在整理结构学习这块的资料时&#xff0c;又把 DAG-GNN 从头到尾认真看了一遍。很多人对它的第一印象就是“给结构学习套了个图神经网络的壳”&#xff0c;这个说法不算全错&#xff0c;但会让人忽略它真正的价值。DAG-GNN 解决的核心问题是&#xff1a;给定一批观测数据&…

2026/10/19 19:38:58 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介&#xff1a;本资源为电源技术方向的学术研究文档&#xff0c;聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略&#xff0c;面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集&#xff0c;共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →