ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JDBC 连 MySQL 增删改查:从能跑到扛住高并发的实战避坑指南

JDBC 连 MySQL 增删改查:从能跑到扛住高并发的实战避坑指南 简介这份PDF资料面向Java初学者与需要巩固数据库基础的开发者系统讲解如何通过JDBC连接MySQL并完成增删改查操作。内容从环境准备入手涵盖JDK、Eclipse、MySQL与Navicat的配置逐步演示建库建表、编写DBUtil工具类加载驱动并获取连接、定义Goddess实体类以及基于DAO层实现插入、查询、更新、删除的完整流程并强调PreparedStatement防注入的实践要点。资源包共1个PDF文件大小约326KB篇幅紧凑、代码示例完整便于按章节对照练习。目前已有5619人学习下载适合希望理解Hibernate、MyBatis等ORM框架底层原理、打牢JDBC基础的读者参考也可作为课程实验与课程设计的辅助材料。1. JDBC 连 MySQL 做增删改查为什么你写的 CRUD 一上并发就翻车很多人第一次用 Java 连 MySQL都是照着一段模板代码抄Class.forName加载驱动、DriverManager.getConnection拿连接、Statement拼 SQL、ResultSet遍历结果。本地跑一遍增删改查全绿于是以为这块已经拿下了。可一旦放到真实业务里几十个请求同时进来连接数暴涨、Too many connections报错、SQL 注入被扫、事务回滚不干净问题全冒出来。JDBC 连接 MySQL 实现增删改查这件事难点从来不在「能不能跑通」而在「怎么写得扛得住、查得安全、改得干净」。这篇笔记面向正在做 Java 后端、需要手写数据访问层或准备面试的工程师把驱动加载、连接管理、预编译、事务、批处理、连接池这几件事按落地顺序讲透每一步都给能直接抄的命令和代码也把血泪踩过的坑标出来。看完你应该能自己判断什么时候该用原生 JDBC什么时候该上连接池什么时候干脆交给 MyBatis-Plus 这类通用 CRUD 框架。2. 从驱动到连接JDBC 打通 MySQL 的最小可运行链路2.1 驱动依赖与 MySQL 版本对齐动手之前先把依赖和数据库版本对齐这一步翻车的人比想象中多。MySQL 5.7 和 8.0 的驱动类名、连接串参数、时区处理都不一样。常见做法是用 Maven 引入驱动版本跟服务端大版本匹配。!-- pom.xmlMySQL 8.x 用 8.x 驱动5.7 用 5.1.x 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency逻辑说明mysql-connector-java是官方 JDBC 驱动实现它把 Java 的java.sql.*接口翻译成 MySQL 的通信协议。参数说明groupId和artifactId固定version必须和服务端匹配——8.0 服务端配 8.x 驱动5.7 服务端配 5.1.x 驱动混用最常见的后果是Unknown system variable query_cache_size或时区报错。如果你用 Gradle把上面换成implementation mysql:mysql-connector-java:8.0.33即可。提示MySQL 8.0 之后驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver老代码里写死旧类名会直接抛ClassNotFoundException。2.2 连接串参数怎么设才不踩时区和 SSL 的坑连接串是 JDBC 里最容易被忽视、又最容易出问题的地方。下面这条是我在 8.0 环境里常用的最小可用串// 8.0 推荐连接串显式指定时区、关闭不必要的 SSL 告警 String url jdbc:mysql://127.0.0.1:3306/demo ?useUnicodetrue characterEncodingutf8 serverTimezoneAsia/Shanghai useSSLfalse allowPublicKeyRetrievaltrue;逻辑说明useUnicode和characterEncoding保证中文不乱码serverTimezone解决 8.0 驱动默认时区导致的The server time zone value is unrecognized报错useSSLfalse在本地和内网环境关掉 SSL 握手避免WARN: Establishing SSL connection without servers identity verification这类噪音allowPublicKeyRetrievaltrue是 8.0 默认加密插件caching_sha2_password在非 SSL 连接下必须开的开关不开会报Public Key Retrieval is not allowed。参数怎么改生产环境如果走内网且没配证书useSSLfalse可以保留如果强制 SSL把它改成true并配好trustCertificateKeyStoreUrl。serverTimezone按服务器实际时区填别照抄Asia/Shanghai到 UTC 机器上。2.3 用 DriverManager 跑通第一条查询先把最小链路跑通再谈封装。下面这段是纯 JDBC 查询不依赖任何框架import java.sql.*; public class JdbcDemo { public static void main(String[] args) throws Exception { String url jdbc:mysql://127.0.0.1:3306/demo?useUnicodetrue characterEncodingutf8serverTimezoneAsia/Shanghai useSSLfalseallowPublicKeyRetrievaltrue; // 8.0 驱动可省略 Class.forNameSPI 会自动加载显式写更稳 Class.forName(com.mysql.cj.jdbc.Driver); try (Connection conn DriverManager.getConnection(url, root, 123456); PreparedStatement ps conn.prepareStatement( SELECT id, name, age FROM user WHERE age ?)) { ps.setInt(1, 18); // 参数下标从 1 开始 try (ResultSet rs ps.executeQuery()) { while (rs.next()) { System.out.println(rs.getLong(id) rs.getString(name) rs.getInt(age)); } } } } }逻辑说明Class.forName触发驱动类静态块注册到DriverManagergetConnection建立物理连接PreparedStatement预编译 SQL 并绑定参数ResultSet是游标next()逐行推进。参数说明ps.setInt(1, 18)的下标从 1 开始不是 0这是新手最常见的越界来源rs.getLong(id)用列名取值比用下标更抗表结构变更。try-with-resources保证连接、语句、结果集按逆序自动关闭漏掉它就会连接泄漏。注意DriverManager.getConnection每次调用都新建一条物理连接用完关闭。它没有池化单机压测几十并发就会打满 MySQL 的max_connections所以它只适合脚本和验证不适合线上服务。3. 增删改查四类操作预编译、批处理与主键回填3.1 插入拿到自增主键和批量插入的正确姿势单条插入要拿回自增 ID靠的是Statement.RETURN_GENERATED_KEYSString sql INSERT INTO user(name, age) VALUES(?, ?); try (PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, 张三); ps.setInt(2, 25); int rows ps.executeUpdate(); // 影响行数 try (ResultSet keys ps.getGeneratedKeys()) { if (keys.next()) { long id keys.getLong(1); // 自增主键 System.out.println(新记录 id id); } } }逻辑说明executeUpdate返回受影响行数插入成功通常是 1getGeneratedKeys拿回数据库生成的主键。参数说明第二个参数RETURN_GENERATED_KEYS必须显式传否则getGeneratedKeys返回空结果集。批量插入不要循环单条执行用addBatchexecuteBatchString sql INSERT INTO user(name, age) VALUES(?, ?); conn.setAutoCommit(false); // 关自动提交攒批 try (PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i 1000; i) { ps.setString(1, user i); ps.setInt(2, 20 i % 30); ps.addBatch(); if (i % 500 0) { // 每 500 条刷一次防内存膨胀 ps.executeBatch(); ps.clearBatch(); } } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; }逻辑说明addBatch把参数攒在客户端executeBatch一次性发给服务端减少网络往返。参数说明setAutoCommit(false)让整批要么全成要么全滚每 500 条刷一次是经验值太大内存吃紧太小失去批处理意义。连接串上加rewriteBatchedStatementstrue能让驱动把多条 INSERT 合并成一条多值语句吞吐能再上一个台阶。3.2 删除与更新影响行数是你唯一的后悔药删除和更新写法几乎一样核心是永远带 WHERE 条件并且检查影响行数String sql UPDATE user SET age ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, 30); ps.setLong(2, 1001L); int rows ps.executeUpdate(); if (rows 0) { // 没有匹配行可能是 id 不存在也可能是并发下已被别人改走 throw new IllegalStateException(更新失败目标记录不存在); } }逻辑说明executeUpdate对 UPDATE/DELETE 返回实际改动行数。参数说明rows 0不代表报错但业务上往往意味着数据状态和预期不符必须显式处理否则会出现「接口返回成功但数据没变」的玄学问题。删除同理DELETE FROM user WHERE id ?绝不要写没有 WHERE 的 DELETE——那是删库跑路的标准开场。提示MySQL 的 UPDATE 如果新值和旧值完全相同默认返回的影响行数是 0除非连接串加useAffectedRowstrue改成匹配行数语义。排查「更新成功却返回 0」时先想到这一点。3.3 查询结果集映射与分页的正确写法查询的坑集中在结果集映射和分页。先看映射String sql SELECT id, name, age, created_at FROM user WHERE age BETWEEN ? AND ? ORDER BY id DESC LIMIT ?, ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, 18); ps.setInt(2, 60); ps.setInt(3, 0); // offset ps.setInt(4, 20); // pageSize try (ResultSet rs ps.executeQuery()) { ListUser list new ArrayList(); while (rs.next()) { User u new User(); u.setId(rs.getLong(id)); u.setName(rs.getString(name)); u.setAge(rs.getInt(age)); u.setCreatedAt(rs.getTimestamp(created_at).toLocalDateTime()); list.add(u); } } }逻辑说明ORDER BY必须和分页一起用否则LIMIT返回的顺序不确定rs.getTimestamp(...).toLocalDateTime()把 JDBC 时间类型转成 Java 8 时间 API。参数说明LIMIT ?, ?第一个是偏移量、第二个是条数深分页offset 很大性能会急剧下降常见优化是用「上一页最大 id」做游标WHERE id ? ORDER BY id DESC LIMIT 20。排序相关的热搜词这里也能对上ORDER BY后面跟的列如果有索引排序就走索引没索引会触发Using filesort数据量大时是性能杀手。用EXPLAIN看执行计划Extra列出现Using filesort就要考虑加索引或改排序字段。3.4 事务手动提交、回滚与隔离级别的边界多条写操作要么全成要么全滚靠事务Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 开启事务 conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // MySQL 默认 // ... 多条 update / insert conn.commit(); } catch (SQLException e) { if (conn ! null) conn.rollback(); // 出错回滚 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); // 归还池前恢复 conn.close(); } }逻辑说明setAutoCommit(false)后所有语句在同一事务里commit落盘、rollback撤销。参数说明TRANSACTION_REPEATABLE_READ是 MySQL InnoDB 默认隔离级别能防脏读和不可重复读但防不住幻读要靠间隙锁或串行化。事务里不要做远程调用、不要等用户输入否则长事务会锁住行、拖垮并发。4. 连接池与通用 CRUD从能跑到扛得住的最后一公里4.1 为什么必须上连接池HikariCP 怎么配DriverManager每次新建物理连接TCP 握手 认证开销在毫秒级高并发下直接压垮数据库。连接池预先建好一批连接循环复用这是线上服务的标配。HikariCP 是当前主流选择配置如下HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://127.0.0.1:3306/demo?useUnicodetrue characterEncodingutf8serverTimezoneAsia/Shanghai useSSLfalseallowPublicKeyRetrievaltruerewriteBatchedStatementstrue); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(20); // 最大连接数 config.setMinimumIdle(5); // 最小空闲连接 config.setConnectionTimeout(30000); // 拿连接超时 30s config.setIdleTimeout(600000); // 空闲回收 10min config.setMaxLifetime(1800000); // 连接寿命 30min小于 MySQL wait_timeout HikariDataSource ds new HikariDataSource(config);逻辑说明maximumPoolSize是并发上限不是越大越好——连接数超过数据库 CPU 核数太多反而因上下文切换变慢经验值是CPU 核数 * 2 磁盘数。参数说明maxLifetime必须小于 MySQL 的wait_timeout默认 28800 秒否则连接被服务端单方面掐断客户端拿到的是失效连接报Communications link failure。connectionTimeout是拿不到连接时的等待上限设太小高峰期直接抛异常设太大请求全堵住。注意连接池的close()不是真关闭物理连接而是归还池中。所以业务代码里conn.close()照常写别因为「怕关掉」而漏关漏关才是真的把池耗干。4.2 用 MyBatis-Plus 的通用 CRUD 省掉重复代码手写 JDBC 的增删改查在表多、字段多时是纯体力活。通用 CRUD 服务基于 MyBatis-Plus 的Db工具类或BaseMapper能把单表操作压到一行// 基于 MyBatis-Plus 的 Db 静态工具类无需注入 Mapper User user new User(); user.setName(李四); user.setAge(28); Db.save(user); // 插入 User loaded Db.getById(1001L); // 按主键查 loaded.setAge(29); Db.updateById(loaded); // 按主键更新 Db.removeById(1001L); // 按主键删除 ListUser list Db.lambdaQuery(User.class) .gt(User::getAge, 18) .orderByDesc(User::getId) .last(LIMIT 20) .list(); // 条件查询逻辑说明Db是无状态工具类底层复用 MyBatis-Plus 的SqlSession省去 Mapper 注入lambdaQuery用方法引用代替字符串字段名编译期就能发现字段写错。参数说明last(LIMIT 20)直接拼 SQL 片段只用于确定安全的场景别把用户输入拼进去。这套方案适合单表 CRUD 占多数的业务复杂多表关联还是老老实实写 XML 或手写 SQL。4.3 原生 JDBC、MyBatis-Plus、JPA 怎么选方案适用场景上手成本灵活度主要代价原生 JDBC脚本、批处理、极致性能调优低最高样板代码多易漏关资源MyBatis-Plus单表 CRUD 为主的后台系统中高复杂 SQL 仍需手写Spring Data JPA领域模型清晰、以对象为中心中高中复杂查询易生成低效 SQL选型逻辑如果项目里 80% 是单表增删改查MyBatis-Plus 的通用 CRUD 能省掉大量重复代码如果对 SQL 有极致控制需求比如批量导入、复杂报表原生 JDBC 或 MyBatis XML 更合适JPA 适合领域驱动设计、实体关系复杂的场景但要对它生成的 SQL 保持警惕必要时用Query接管。5. 避坑与排查JDBC 连 MySQL 最常见的 5 个翻车现场5.1 时区报错The server time zone value is unrecognized现象启动或首次查询时抛java.sql.SQLException: The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因MySQL 8.0 驱动要求明确时区而服务端time_zone是SYSTEM且系统时区名驱动识别不了。解决连接串加serverTimezoneAsia/Shanghai按实际时区填或登录 MySQL 执行SET GLOBAL time_zone 8:00后重启连接。5.2 连接泄漏Too many connections 越跑越多现象服务跑一段时间后报Too many connectionsSHOW PROCESSLIST看到大量Sleep连接。原因某处Connection/Statement/ResultSet没关或异常路径跳过了close。解决全部改用try-with-resources上连接池并设maxLifetime用SHOW STATUS LIKE Threads_connected监控连接数趋势持续上涨就是泄漏。5.3 SQL 注入字符串拼接的 Statement 是重灾区现象登录接口输入 OR 11直接绕过校验。原因用Statement拼接用户输入SQL 结构被篡改。解决一律用PreparedStatement加?占位符参数通过setXxx绑定。预编译不仅防注入还能让数据库缓存执行计划重复执行更快。排序字段、表名这类不能参数化的位置用白名单校验别直接拼。5.4 中文乱码characterEncoding 没设或库表字符集不一致现象插入中文后查出来是???或乱码。原因连接串没设characterEncodingutf8或库/表/列的字符集是latin1。解决连接串加useUnicodetruecharacterEncodingutf8建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ciutf8mb4才能存 emoji 和生僻字。用SHOW VARIABLES LIKE character%核对服务端字符集。5.5 批处理失效executeBatch 比循环单条还慢现象写了addBatch/executeBatch性能却没提升。原因连接串没开rewriteBatchedStatementstrue驱动仍逐条发送或批太大导致内存溢出。解决连接串加rewriteBatchedStatementstrue每批控制在 5001000 条executeBatch后调clearBatch释放参数缓存。用SHOW GLOBAL STATUS LIKE Com_insert对比批处理前后的语句数验证效果。6. 进阶技巧用 EXPLAIN 和慢查询日志把 CRUD 性能钉死写到能跑只是及格能定位慢在哪才算过关。我一般用两个工具配合EXPLAIN看单条 SQL 的执行计划慢查询日志抓整体瓶颈。先看EXPLAIN怎么读EXPLAIN SELECT id, name, age FROM user WHERE age 18 ORDER BY id DESC LIMIT 20;重点看四列type越靠左越好system const eq_ref ref range index ALL出现ALL就是全表扫描key是实际用的索引为NULL说明没走索引rows是预估扫描行数越大越慢Extra里Using filesort表示额外排序、Using temporary表示用了临时表这两个都是优化信号。给age加索引后type从ALL变成rangerows从几十万降到几千效果立竿见影。慢查询日志用来抓漏网的-- 开启慢查询日志超过 1 秒的记录 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL slow_query_log_file /var/log/mysql/slow.log;参数说明long_query_time按业务容忍度设线上一般 0.51 秒日志文件路径要有写权限。开完之后用mysqldumpslow -s t /var/log/mysql/slow.log按耗时排序找出最拖后腿的几条 SQL再回到EXPLAIN逐条分析。还有一个容易被忽略的点连接池参数和数据库参数要联动调。HikariCP 的maxLifetime必须小于 MySQL 的wait_timeoutmaximumPoolSize乘以应用实例数不能超过 MySQL 的max_connections。我踩过一次坑三个服务实例各配 20 连接MySQLmax_connections默认 151加上其他客户端直接打满报Too many connections。后来把单实例池降到 15并调大max_connections才稳住。最后说个验证方法写完一套 CRUD别只看功能通不通用SHOW STATUS LIKE Com_select、Com_insert、Com_update、Com_delete看语句计数是否符合预期用SHOW STATUS LIKE Threads_connected看连接数是否稳定。功能测试只能证明「能跑」这些计数才能证明「跑得干净」。我自己现在的习惯是任何一段 JDBC 代码合入前先过一遍try-with-resources有没有漏、PreparedStatement有没有用、影响行数有没有判、连接池参数有没有和数据库对齐。这四条守住增删改查基本不会出大问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表