ARTICLE DETAIL

资讯详情

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

JDBC从原理到实战:驱动、增删改查、事务与连接池一网打尽

JDBC从原理到实战:驱动、增删改查、事务与连接池一网打尽 1. 抛开术语JDBC到底在解决什么问题这几年带过不少刚入行的同学发现大部分人学JDBC时卡住的点不是代码有多难而是一堆概念绕来绕去驱动是什么Class.forName是什么意思为什么连接串那么长我印象最深的一次是给一个实习生布置任务让他用Java把用户信息存进MySQL他盯着编辑器半天最后问了我一句JDBC是不是就是一个数据库的客户端软件所以这篇文档我就用大白话把JDBC讲清楚。目标是看完之后你能自己从零写一个JDBC增删改查遇到最常见的连接异常能自己排查顺手把事务、PreparedStatement、连接池这些你迟早要碰的东西也理解掉。适合刚学完Java语法、准备接触数据库编程的同学也适合那些用过框架但没写过原生JDBC的开发者——说实话把这一层补上之后你再看MyBatis、Spring Data JPA思路会通透得多。1.1 驱动才是翻译官JDBC只是统一插口标准JDBC的全称是Java Database Connectivity直译就是Java数据库连接。它解决的问题其实特别朴素Java程序要操作数据库但市面上的数据库太多了MySQL、Oracle、PostgreSQL、SQL Server、SQLite……每家的通信协议都不一样。如果没有一个统一标准你换一个数据库就要重新学一套完全不同的API代码里全是不同厂商的私有方法那场面想想就头疼。JDBC干的事就是给Java程序定了一套统一的接口不管底层连的是什么数据库你写的代码都是DriverManager获取连接、Connection建立会话、Statement执行SQL、ResultSet读取结果。至于数据库内部怎么和服务器握手、怎么解析请求JDBC完全不关心那是数据库厂商该做的事。这里有个好用的类比JDBC就像USB接口。你不需要关心U盘内部是闪存颗粒还是主控芯片只要它遵守USB标准插上电脑就能用。数据库厂商提供的JDBC驱动就是把自己的数据库包装成USB设备——它负责把JDBC接口发来的指令翻译成自己数据库能听懂的协议。所以说驱动才是真正的翻译官JDBC只是那个统一尺寸的插口。1.2 四个关键成员DriverManager、Connection、Statement、ResultSet大白话认识一下JDBC里的四位主角你后面所有代码都是围绕它们转的。DriverManager司机负责根据连接串找到合适的驱动类帮你创建出一个数据库连接。它就像一个出租车调度中心你说要去哪它给你安排一辆车。Connection连接会话你与数据库之间的一条通道。一个连接相当于一条电话线所有SQL都通过这条线送过去结果也从这条线传回来。Statement信使你把SQL写在纸条上交给它它沿着电话线把SQL送到数据库执行。简单的理解就是一个SQL搬运工。ResultSet结果集查询返回的虚拟表格。它不是一次性把全部数据塞给你而是像Excel表格一样你拿一个光标一行一行往下走。这四者是有顺序的DriverManager拿到ConnectionConnection创建StatementStatement执行查询后得到ResultSet。理解了这条链路你再看任何JDBC代码骨架其实都差不多。还有一点值得说透JDBC只是接口规范它本身没有实现。真正干活的jar包是mysql-connector-j、ojdbc、mssql-jdbc这些驱动包。网上有人困惑我明明引入了JDBC依赖怎么还报找不到驱动多半就是把接口和实现搞混了——你引入的应该是MySQL的驱动实现而不是JDBC本身。2. 跑通第一个JDBC增删改查从依赖到插入用户数据很多同学学JDBC死磕理论结果一行代码没跑起来过。这一章咱们直接动手。网上经常能搜到一个说法叫jdbc插入用户数据或者第1关jdbc插入用户数据其实指的就是最经典的那个练习往数据库用户表里插入一条记录。这也是我建议所有新手要亲手打一遍的代码别复制一个字一个字敲。2.1 先把运行环境准备好MySQL与驱动Jar包动手之前本地得先有一个MySQL实例。装MySQL这块我不展开现在Docker一条命令就能跑起来docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 -e MYSQL_DATABASElearn_jdbc mysql:8.0先建库建表我们就用最简单的一张用户表CREATE DATABASE IF NOT EXISTS learn_jdbc DEFAULT CHARACTER SET utf8mb4; USE learn_jdbc; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, age INT NOT NULL );然后是引入MySQL驱动。我建议用Maven管理别去网上手动下载jar包后面麻烦事一堆。在pom.xml里加这么一段dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency注意较新版本的groupId已经从mysql变成com.mysql了老版本是mysql:mysql-connector-java。如果你用的是8.x驱动推荐用新的坐标。版本号不用刻意追求最新稳定版就行。2.2 读懂连接串jdbc:mysql://localhost:3306/learn_jdbc 是怎么拆解的新手最容易对着连接串发呆因为它长得像一串乱码。其实拆开看特别简单jdbc:mysql://localhost:3306/learn_jdbc?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8jdbc:mysql协议头告诉DriverManager这是MySQL的JDBC连接。localhost数据库服务器地址写成127.0.0.1也行。3306MySQL默认端口。改了端口就写你自己的端口。learn_jdbc要连接的数据库名。?后面的都是参数useSSLfalse表示关闭SSL本地开发没必要加密serverTimezoneAsia/Shanghai指定时区characterEncodingutf8统一编码。我见过太多人因为没加时区参数连接时报错或者插入中文变成乱码。所以这里多说一句连接串上的每个参数都有存在的理由先照抄理解别乱删。2.3 六步流程与一份能直接跑的插入代码JDBC标准用法网上都叫六步走其实就是加载驱动类JDBC 4.0之后可以省略但建议保留便于理解。用DriverManager.getConnection拿到连接。创建PreparedStatement。填充参数并执行SQL。如果是查询处理ResultSet。释放资源。直接上一份完整的插入用户数据代码import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class JdbcInsertDemo { public static void main(String[] args) { String url jdbc:mysql://localhost:3306/learn_jdbc?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String username root; String password 123456; // 1. 加载驱动MySQL 8.x 驱动类路径是 com.mysql.cj.jdbc.Driver try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } String sql INSERT INTO user(name, age) VALUES (?, ?); // 使用 try-with-resources连接、语句对象都会自动关闭 try (Connection conn DriverManager.getConnection(url, username, password); PreparedStatement ps conn.prepareStatement(sql)) { // 2. 给占位符 ? 赋值索引从1开始 ps.setString(1, 张三); ps.setInt(2, 25); // 3. 执行更新返回影响行数 int rows ps.executeUpdate(); System.out.println(插入成功影响行数 rows); } catch (Exception e) { e.printStackTrace(); } } }这段代码里有几个点我要专门解释一下第一我故意用了PreparedStatement而不是Statement。网上很多老教程还在用Statement写字符串拼接比如INSERT INTO user(name, age) VALUES ( name , age )。这种写法能跑但会让你养成坏习惯实际项目中早就不这么干了。原因后面第5章细说你现在只要记住新代码就用PreparedStatement准没错。第二try-with-resources是Java 7以后的语法Connection和PreparedStatement实现了AutoCloseable接口代码块结束时自动关闭不用再手动写finally去关资源。这样不仅代码简洁还能杜绝连接忘记关闭导致连接数耗尽的经典事故。2.4 查询、更新、删除别把ResultSet的坑踩进去插入跑通之后增删改查基本就通了一半。查、改、删语法上非常接近唯一的全新知识点是ResultSet怎么读。查询的代码长这样String sql SELECT id, name, age FROM user WHERE age ?; try (Connection conn DriverManager.getConnection(url, username, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, 18); // 查询用 executeQuery返回结果集 ResultSet rs ps.executeQuery(); // rs.next() 每次把游标向下移动一行返回 false 表示没有更多数据了 while (rs.next()) { int id rs.getInt(id); String name rs.getString(name); int age rs.getInt(age); System.out.println(id id , name name , age age); } } catch (Exception e) { e.printStackTrace(); }ResultSet有四个容易踩的坑我一个个说游标默认在“第一行之前”所以必须先调一次next()才能读第一行。很多新手上来就rs.getString(name)直接报Before start of result set就是这个原因。按列名取值比按下标取值更安全。rs.getInt(1)也能取第一列但SQL里列的先后顺序一变代码就悄悄出错。用列名取值SQL怎么变都不怕。getXxx的类型必须和列类型匹配拿getString去取INT列虽然大部分数据库能自动转换但对上类型永远是最稳妥的。ResultSet也要关闭。虽然finally或者try-with-resources里rs也会被自动关闭但如果你是在长连接里手动管理资源千万记得rs.close()。查询大结果集时不及时关闭内存是肉眼可见地往上涨。更新和删除就是把SQL换个关键字的事再补一句executeUpdate()既支持INSERT、也支持UPDATE和DELETE返回的都是影响行数。你用executeUpdate()去执行查询语句它不会报错但返回值毫无意义所以经验就是——查询用executeQuery其余全用executeUpdate别混。3. 事务管理JDBC里最容易翻车的环节3.1 两条SQL之间的要么全部成功要么全部失败先说一个经典场景转账。A账户扣1000B账户加1000。这两个操作是两条独立的UPDATE语句。如果第一条执行成功第二条执行失败那A的钱没了B的钱也没加账就平不上了。数据库解决这个问题的手段叫事务Transaction。一个事务里可以包多条SQL它们要么全部成功要么全部失败不存在中间状态。网上经常搜到jdbc transaction这个关键词说明大家实操时确实会在事务上卡壳。事务有个大白话ACID版本原子性Atomicity事务里的SQL像一个整体原子不可再分。要么全成要么全败。一致性Consistency事务执行前和执行后数据库都得是合法的状态。比如转账前后两个账户的总额不变。隔离性Isolation两个事务同时操作数据时彼此不能互相干扰。要么排队要么合理安排不能让对方看到中间值。持久性Durability事务一旦提交数据就永久落盘不会因为断电、宕机就丢。3.2 三件套setAutoCommit(false)、commit、rollbackJDBC里默认每条SQL执行完就自动提交autoCommittrue也就是说事务边界是一条SQL一个事务。要做真正的多SQL事务必须手动关闭自动提交然后在合适的地方commit或rollback。代码套路是固定的三件套Connection conn null; try { conn DriverManager.getConnection(url, username, password); // 第一件关闭自动提交开启事务 conn.setAutoCommit(false); // 这里执行两条SQL updateAccount(conn, A, -1000); updateAccount(conn, B, 1000); // 第二件全部执行成功提交事务 conn.commit(); System.out.println(转账成功); } catch (Exception e) { // 第三件任何一步出错回滚全部改动 if (conn ! null) { conn.rollback(); } e.printStackTrace(); } finally { if (conn ! null) { conn.setAutoCommit(true); // 恢复默认方便连接池复用 conn.close(); } }这套代码里有三个特别容易翻车的地方我挨个提醒第一setAutoCommit(false)必须在执行第一条SQL之前调用。如果你已经执行了一条SQL才手动关闭自动提交那这条SQL已经悄悄提交了你想回滚它也回滚不掉——因为它根本没进事务。我见过有人把这个放在循环里面结果每条SQL还是独立事务白忙一场。第二rollback()和commit()都必须在Connection还开着的时候调。如果你在try-with-resources里把Connection放在try后面的参数区等进入catch块时连接可能已经被自动关闭了。虽然MySQL驱动在连接断开时会自动回滚未提交的事务但这属于隐式行为不靠谱。我一般会手动管理Connection或者用工具类把它包起来。第三连接池场景下别忘了恢复setAutoCommit(true)。这条坑是在项目里才会遇到的连接从连接池拿出来时如果上一个使用者忘了恢复自动提交状态下一个拿到连接的人就掉进手动事务的坑里。所以你在finally里恢复默认状态是一个职业习惯。3.3 隔离级别脏读、不可重复读、幻读是怎么回事事务隔离级别是JDBC面试高频题也是实际开发里调参的源头。JDBC定义了四个隔离级别对应Connection常量隔离级别对应常量脏读不可重复读幻读读未提交TRANSACTION_READ_UNCOMMITTED可能可能可能读已提交TRANSACTION_READ_COMMITTED不会可能可能可重复读TRANSACTION_REPEATABLE_READ不会不会可能MySQL InnoDB实际可避免串行化TRANSACTION_SERIALIZABLE不会不会不会大白话解释三个读脏读事务A改了数据但还没提交事务B读到了这条没提交的数据。然后A回滚了B就拿着一条不存在的脏数据在那分析结果全错。不可重复读事务A第一次查某行的值是100事务B改了这行的值并提交A第二次查变成90。A在同一事务里两次读到不同的值这就是不可重复读。侧重点是同一行记录被改了。幻读事务A按条件查了一批数据事务B插入了一条新记录并提交A再按同一个条件查多出来一行幽灵数据。侧重点是行数变化。设置方法是conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED)必须在事务开始前设置才有意义。那实际开发选哪个别盲目追求串行化它性能最低因为它把所有并发访问变成串行排队。MySQL默认是REPEATABLE_READOracle和SQL Server默认是READ_COMMITTED。新手一般保持数据库默认即可等你真正遇到并发数据不一致的问题再根据场景去调。最典型的场景是报表统计要求同一事务内多次查询结果一致那就得用REPEATABLE_READ如果只要求读到已提交的数据性能优先READ_COMMITTED就够用。4. 连接异常的现场复盘从IDEA下载失败到驱动版本不匹配这一章聊点实际会遇到的问题。很多新手不是不会写JDBC代码而是被各种奇怪的报错劝退。我把平时后台收到最多的问题挑四个典型场景来一次现场复盘。4.1 IDEA自动下载Maven依赖失败换个镜像源一般就解决热词里有intellij idea sqlserver jdbc 自动下载 download from maven failed这个我太熟了。在IDEA里引入mssql-jdbc依赖一刷新Maven右下角弹Download from Maven failed项目里一片飘红。原因绝大多数不是IDEA坏了而是Maven默认走的中央仓库在国外访问速度极不稳定。解决思路就是配置国内镜像。找到Maven的settings.xml在mirrors节点里加mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror改完之后在IDEA里点一下Maven面板的刷新按钮让依赖重新解析。如果还报失败多半是你本地仓库里已经缓存了失败的空文件把~/.m2/repository下对应目录删掉再刷一次。另外一个小经验IDEA自带的Maven和命令行Maven可能会用不同settings.xml。如果你命令行能下载成功、IDEA里不行去IDEA的Settings - Build, Execution, Deployment - Build Tools - Maven确认User settings file指向的就是你改过的那份文件。4.2 驱动版本与数据库不匹配报错信息其实已经把话说完了热词里还有一句很典型的报错this version of the jdbc driver is only compatible with elasticsearch versio。这是Elasticsearch官方JDBC驱动的版本兼容错误。这类问题在MySQL、SQL Server、PostgreSQL上同样常见报错形式一般是不支持此驱动版本或者握手阶段直接断开。驱动版本和数据库版本是有对应关系的不是越新越好而是越匹配越好。以Elasticsearch为例ES 7.x配ES 7.x的JDBC驱动ES 8.x配8.x驱动你把8.x的驱动拿去连7.x的ES连接握手时版本号对不上直接抛only compatible with异常。另外要注意Elasticsearch的SQL访问有两种方式一种是官方JDBC驱动一种是REST API前者对版本号卡得很死后者宽松很多。排查思路按这个顺序来读完整错误信息找出关键字compatible、version。去数据库官网查驱动兼容矩阵确认当前驱动适用于哪个数据库版本区间。检查Maven依赖里的版本号是否写死成了某个不匹配的版本。如果项目是多模块检查有没有在别的地方覆盖了同一个依赖的版本。这类问题的本质是接口标准统一了但厂商实现各有各的脾气。JDBC这层已经帮你挡掉了99%的差异剩下的版本兼容属于厂商自由发挥区你得认。4.3 Flink JDBC连接器异常的排查链路大数据场景里flink的jdbc连接器异常也是高频搜索词。Flink通过JDBC连接器读写MySQL这类关系型数据库时常见的坑有几个我按出现频率排序第一驱动类没打包进作业Jar。你在本地IDEA里跑Flink作业好好的一提交到集群就报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是本地IDEA的classpath有驱动但提交作业时打的Jar里没带。解决方法是打包时用maven-shade-plugin把驱动打进去plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId executions execution phasepackage/phase goalsgoalshade/goal/goals /execution /executions /plugin第二并行子任务把数据库连接池打爆。Flink的每个并行子任务去写数据库时都会建立JDBC连接。如果并行度设成50而你的MySQLmax_connections只有20那必然报Too many connections。解决办法是把并行度降下来或者换成支持连接池的Sink比如HBase这类更适配大数据写入的存储。这里要理解一个本质JDBC连接是稀缺资源不是每行数据一个连接而是每个并行任务一个连接多出来的连接要排队复用。第三写入吞吐上不去。Flink每秒几万条数据直接走JDBC一条一条插数据库根本扛不住。常规做法是开启JDBC连接器的批量写入参数或者干脆用JdbcBatchStatement配合分批提交。大数据场景下JDBC不是银弹什么时候用、怎么用心里要有数。4.4 入门阶段常见的连接失败原因汇总除了上面三个大场景还有几个入门阶段高频出现的连接报错一张表给你总结掉报错关键内容实际含义常规解法Access denied for user rootlocalhost用户名或密码错误或该用户没有远程访问权限检查密码改MySQL用户Host为%Communications link failure连不上服务器通常是IP/端口/防火墙问题检查3306端口是否开放MySQL是否启动Unknown database xxx连接串里的数据库名不存在先SHOW DATABASES确认库名Public Key Retrieval is not allowedMySQL 8.0的caching_sha2_password认证插件问题连接串加allowPublicKeyRetrievaltrueThe server time zone value XXX is unrecognized时区没配对连接串加serverTimezoneAsia/ShanghaiNo suitable driver found没找到驱动多半是驱动jar没引入或驱动类名拼错检查依赖和Class.forName的类路径我个人排查连接问题一直遵循一个笨办法先把连接串放到命令行客户端里试一遍。比如用mysql -h localhost -P 3306 -u root -p123456 learn_jdbc直接连如果命令行能连上问题在Java这边如果命令行也连不上问题在MySQL配置或网络。这一步能把排查范围缩小一半比盯着IDEA报错猜来猜去高效多了。5. 进阶的必要性PreparedStatement、连接池与框架的关系最后聊点进阶内容。你要是在实际项目里用过JDBC一定会发现没人会在代码里直接写DriverManager.getConnection然后去拼SQL。大家都用框架、用连接池。但这不代表JDBC可以跳过。恰恰相反框架只是把JDBC又包了一层你把底层这层搞懂了上层学起来才有底气。5.1 为什么项目里几乎不用Statement拼SQL前面我说过网上老教程爱用Statement拼字符串。为什么现在不推荐有两大原因。第一个原因是SQL注入漏洞。用户输入的name如果直接拼接进SQL比如输入; DROP TABLE user; --拼接出来就变成SELECT * FROM user WHERE name ; DROP TABLE user; --轻则查询错误重则整张表被删。这不是危言耸听真实世界里因为SQL注入删库跑路的案例太多了。而PreparedStatement用?占位参数的赋值走的是驱动层转义用户输入永远只被当成值看待不会变成SQL关键字。第二个原因是预编译带来的性能优势。PreparedStatement执行之前数据库会把SQL先解析、优化成执行计划。如果同一条SQL要执行100遍用Statement就得解析100遍用PreparedStatement只解析一次后面99次直接复用执行计划。在循环插入数据的场景里这个差距能被放大到几倍甚至几十倍。写起来也不麻烦就是把参数位置换成?再用setXxx赋值。所以我强烈建议从现在起写JDBC一律用PreparedStatement不给坏习惯留机会。5.2 连接池先有池子才有性能DriverManager.getConnection每次都是新建一条物理连接。一条连接的建立要经过TCP握手、MySQL认证、初始化会话这个过程轻则几十毫秒重则几百毫秒。如果系统每来一个请求就新建一次连接数据库迟早被拖垮。连接池的思路非常朴素提前建好一批连接放在池子里谁要用谁取用完归还不够用再临时创建空闲太久就回收。这不就是银行柜台的意思嘛——柜员连接提前坐在那里等着客户请求来了直接办业务而不是每次营业前先去招募一批新柜员。Java生态里最常用的连接池是HikariCPSpring Boot从2.0开始默认就用它。基本配置长这样spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000几个参数的白话理解maximum-pool-size池里最多同时有多少个连接超过就得等。minimum-idle池里至少保持多少个空闲连接。connection-timeout客户端最多等多久拿不到连接就报错。idle-timeout空闲连接存活多久后被回收。新手容易把maximum-pool-size设得很大以为连接越多越快。其实数据库并发处理能力是有上限的连接太多反而让数据库上下文切换开销变大。经验值是核心并行度 少量余量一般10到20个足够。5.3 那些不用写JDBC的框架底层照样是JDBC你现在去看招聘要求几乎都写熟悉Spring Boot、MyBatis。有人就误以为JDBC过时了不值得学。这里我要唱个反调框架只是把JDBC换了个更省事的外包装包装纸撕掉里面还是那六步。拿MyBatis举例你写一个Mapper接口和一个XML文件以为和JDBC没关系。但实际上MyBatis底层就是先拿到Connection用PreparedStatement填充参数执行SQL再用反射把ResultSet映射成你定义的实体类。你在Mapper里配的driverClassName、url、username、password最后全都交还给了DriverManager和连接池。Spring的JdbcTemplate更明显它把Connection的获取、PreparedStatement的参数绑定、ResultSet的行映射全部封装成一行代码但本质仍然是那套JDBC流程。所以我的建议顺序是先老老实实写一个月原生JDBC搞懂每一个环节然后再去用MyBatis、JdbcTemplate或者Spring Data JPA。有了底层概念打底你调试框架问题时不会瞎猜报错里出现PreparedStatementCallback、SQLException时你能立刻想到它真正卡在哪一步。最后分享一个我自己的排查技巧不管用什么框架遇到SQL相关的诡异问题先把框架配置里的show-sql或者logImpl打开让日志把框架实际执行的SQL打出来。你会发现90%的问题是SQL本身写错了不是框架出了鬼也不是JDBC不兼容。能做这一点的基础就是你已经理解JDBC的SQL是怎么生成、怎么送到数据库去的。这个习惯我一直留到现在几乎每次都能帮我在五分钟内定位到问题根源。
返回列表