news 2026/9/30 3:28:10

Spring Boot 整合 MyBatis 与 PostgreSQL:从配置到性能优化全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 整合 MyBatis 与 PostgreSQL:从配置到性能优化全解析

做 Java 后端这几年,Spring Boot、MyBatis、PostgreSQL 这三样东西几乎成了我项目里的固定搭配。不管是刚入行的新手,还是已经被线上事故磨过几轮的老兵,最终都会发现:一套用得住、讲得清、改得动的数据访问方案,比追着框架版本跑要重要得多。这篇文章就围绕“Spring Boot 整合 MyBatis 与 PostgreSQL”这条主线,把我从零搭环境、写 Mapper、调缓存、排故障的全过程拉一遍,也顺手回答那些你大概率搜过的问题:PostgreSQL 到底下载哪个版本,MyBatis 一级二级缓存怎么才有效,TypeHandler 工作流程是什么,批量插入为什么这么慢。

这不是官方文档的搬运,全是我实际操作过、踩过坑之后沉淀下来的东西。适合三类人看:准备用 Spring Boot 写第一个完整项目的初学者;项目里想从 MySQL 迁到 PostgreSQL 或者正在选型的后端开发;以及马上要面试、想把 MyBatis 底层和 PostgreSQL 常见坑一次讲清楚的候选人。

1. 这套技术栈到底好在哪:选型思路与架构拆解

1.1 为什么是 Spring Boot + MyBatis + PostgreSQL

我帮人看过不少项目,也重构过一些祖传代码,最后得出的结论是:技术选型没有银弹,但 Spring Boot + MyBatis + PostgreSQL 的组合,是绝大多数业务系统里最不容易翻车的一种。

Spring Boot 解决的问题很纯粹:把 Spring 那一大堆 XML 配置、Bean 装配、依赖管理全部收敛成“约定大于配置”。你新建一个项目,加了依赖就能跑,内置 Tomcat,打包成 jar 直接扔服务器执行。它不负责业务,但它把你从环境搭建里解放出来,让你专心写代码。

MyBatis 的存在则有点反主流。当年 JPA/Hibernate 火的时候,很多人觉得“全自动 ORM 才是未来”,但真到了复杂报表、多表 join、分库分表、SQL 调优的时候,Hibernate 的自动 SQL 反而成了约束。MyBatis 的思路是“半自动”:你写 SQL,它帮你在 Java 对象和数据库记录之间做映射。SQL 是你的,执行计划是数据库优化器说了算,映射的细节由 MyBatis 处理。这种“把 SQL 主动权还给开发者”的理念,在性能敏感、SQL 复杂的业务里特别舒服。

PostgreSQL 更是被低估的选手。过去大家默认用 MySQL,但 PostgreSQL 在标准兼容性、JSON 支持、事务隔离、扩展能力上其实更接近 Oracle 这类商业数据库。很多从 Oracle 迁出来的项目,第一站就是 PostgreSQL。它的 JSONB 类型可以直接当文档数据库用,数组类型也让一些复杂业务字段省掉一张子表,再加上出色的 MVCC 并发控制,这套组合扛住中小型系统的全部流量毫无压力。

说白了,这套组合的逻辑是:Spring Boot 提供稳定底座,MyBatis 给你 SQL 控制力,PostgreSQL 提供现代数据库能力。三者各管一段,没有哪个环节特别“黑盒”,出了问题你能自己定位。

1.2 版本选型:Spring Boot 2 还是 3,PostgreSQL 用哪个版本

版本问题是新手最容易纠结的地方,也是线上故障的常见源头。直接给结论。

Spring Boot 目前主流就是 3.x 系列,但它要求 Java 17 以上。如果你还在用 JDK 8,那就老老实实用 Spring Boot 2.7.x,这是 2.x 最后一个大版本,还在维护期内。Spring Boot 3 最大的变化有两个:一是 javax 命名空间换成了 jakarta,二是很多自动配置类的内部实现被重构。如果你的项目要从 2 升 3,需要重新梳理依赖,尤其是 Spring Security 的配置方式,网上搜“spring boot 3 spring security 配置迁移”能看到大量踩坑记录,本质上都是命名空间和配置类位置变了。

MyBatis 官方提供了专门的 Starter,版本对应关系要记牢:

Spring Boot 版本JDK 要求mybatis-spring-boot-starter 推荐版本
2.5.x - 2.7.x8/112.2.x / 2.3.x
3.0.x - 3.2.x17+3.0.x
3.3.x+17+3.0.4+

千万不要用一个很新的 Spring Boot 3.3 去配 mybatis-spring-boot-starter 2.3,那会直接因为版本冲突起不来。Maven 报错一般很明确,但能避免的冲突就别等着报错。

PostgreSQL 版本我建议分场景看。新项目直接用 17,它是当前稳定版,性能、并行查询、逻辑复制都有明显改进。老项目如果依赖了某个版本的行为特性,没有特殊情况就别乱升,PostgreSQL 的大版本升级涉及底层数据文件格式,不能原地覆盖升级,要用 pg_upgrade 或者 dump/restore 迁移。生产环境里我还见过一套项目跑在 PostgreSQL 10 上,都快 EOL 了还在硬撑,这种属于技术债,建议尽快规划迁移。

PostgreSQL JDBC 驱动版本选 42.7.x 这一类新版本就行,它向后兼容 server 9.4 以上的绝大多数版本。驱动版本直接放到 pom.xml 里,Spring Boot 的 dependency management 会帮你管理驱动版本,但你也可以显式指定,避免父 POM 升级时驱动被悄悄换掉。

2. 环境准备:从零把 PostgreSQL 跑起来

2.1 安装 PostgreSQL:Windows 和 Linux 两种最典型场景

PostgreSQL 下载哪个版本这个问题,搜索量一直很高,其实答案很统一:去官网 postgresql.org/download 拿官方安装包,别去第三方站点下什么“优化版”“绿色版”。Windows 上直接下载 EDB 提供的 installer,它是一个图形化安装向导,能装服务端、pgAdmin 图形客户端,还能顺手装 Stack Builder。安装过程中会让你设置 postgres 超级用户的密码,这个密码千万别忘记,后面所有数据库操作都要从它展开。

Windows 装完 PostgreSQL,服务默认是自动启动的。如果“服务启动失败”,最常见的原因是安装时选的端口 5432 已经被占用,或者修改了数据目录权限。排查方法很简单,cmd 里跑一句:

netstat -ano | findstr 5432

看端口到底是哪个进程占用。如果是之前残留的 PostgreSQL 实例,先把旧服务停掉再启动新服务。实在搞不定,检查 Windows 服务管理器里 PostgreSQL 对应的服务状态和日志目录,日志里会写清楚失败原因。

Linux 上我以 CentOS 7.9 为例,因为这是很多企业内部服务器的现状。CentOS 7 自带的软件源里 PostgreSQL 版本很老,所以要用官方 PGDG 仓库:

sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo yum install -y postgresql17-server sudo /usr/pgsql-17/bin/postgresql-17-setup initdb sudo systemctl enable --now postgresql-17

安装后默认会创建一个 postgres 系统用户,以及一个同名的超级数据库用户。切换到 postgres 用户后就可以建库建账号:

sudo -i -u postgres psql -c "CREATE ROLE app_user LOGIN PASSWORD 'YourPassword';" psql -c "CREATE DATABASE mydb OWNER app_user;"

如果是本机开发,这样已经能用了。但如果你是想在另一台电脑上用 Navicat、DBeaver 或者 Spring Boot 应用连这个 PostgreSQL,有两个配置必须要改:监听地址和认证规则。默认 PostgreSQL 只监听本机地址,外部连接会直接超时或者被拒。编辑 postgresql.conf:

listen_addresses = '*'

然后编辑 pg_hba.conf,在文件末尾加上:

host all all 0.0.0.0/0 scram-sha-256

最后重载配置:

systemctl reload postgresql-17

注意:pg_hba.conf 的规则是从上往下匹配的,如果你在之前已经有更严格的规则覆盖了 0.0.0.0/0,要把新规则放在前面,或者直接替换掉之前的下一行规则。生产环境不要用 0.0.0.0/0 这种全开放写法,要精确到内网网段。

2.2 用 Docker 快速起一个开发用 PostgreSQL

日常开发我更喜欢用 Docker 起一个 PostgreSQL 实例。原因很简单:干净、可重复、切换版本方便。一个命令就能得到一个全新的 PG 17 环境,不想用了删掉容器再起一个,数据说丢就丢,不污染本机。

docker run -d --name postgres17 \ -e POSTGRES_PASSWORD=mysecret \ -e POSTGRES_DB=mydb \ -v pgdata:/var/lib/postgresql/data \ -p 5432:5432 \ postgres:17

这个命令里的三个环境变量要注意:POSTGRES_PASSWORD 是超级用户 postgres 的密码;POSTGRES_DB 在首次初始化时自动创建一个空数据库;POSTGRES_USER 如果不写,默认就是 postgres。

-v pgdata:/var/lib/postgresql/data 是数据卷持久化,把数据库文件放到 Docker 卷里,这样容器删了数据还在。如果你本机 5432 端口已经被占,冒号左边改一个端口,比如 -p 5433:5432,连接 URL 里也要相应改成 5433。

容器启动后想进去测试一下:

docker exec -it postgres17 psql -U postgres -d mydb

看到 PostgreSQL 的提示符之后执行 SELECT 1;,能返回一行结果就说明服务端正常。这一步测试非常关键,因为后面 Spring Boot 应用连不上的时候,你要先想办法确认“数据库本身是好的”,再去排查应用配置。

我用 macOS 和 Windows 都跑过这套 Docker 方案,只要 Docker Desktop 能正常启动,PostgreSQL 容器基本不会出乱子。反倒是本机直接装 PostgreSQL 时,不同系统的服务管理方式不同,Windows 的服务管理器、Linux 的 systemd、macOS 的 brew services 各有一套,出问题时的排查路径完全不同。开发环境用 Docker 能把这些差异全部抹掉,强烈建议。

3. Spring Boot 项目搭建与 MyBatis 整合配置

3.1 快速创建第一个 Spring Boot 程序并连上 PostgreSQL

创建项目直接去 start.spring.io,这是官方脚手架,比在 IDE 里点向导更可控。选择 Maven 项目、Java 版本、Spring Boot 版本,依赖部分加上 Spring Web、MyBatis Framework、PostgreSQL Driver、Lombok。如果你是在国内网络环境下访问 start.spring.io 偶尔拉不动,也可以直接用 IDE 内置的 Spring Initializr,效果一样。

生成出来的项目 pom.xml 里,关键依赖是这样:

<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

如果你的 Spring Boot 版本是 3.x,那 mybatis starter 必须用 3.x;如果你用的是 Spring Boot 2.7,那版本就用 2.3.x,这点前面已经强调过,别让 Maven 帮你猜,直接钉死版本。

启动类里加一行 @MapperScan,让 MyBatis 自动扫描 Mapper 接口并生成代理对象:

@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

这里 @MapperScan 的作用是告诉 MyBatis 去哪个包底下找接口。如果不加,每个 Mapper 接口上都要单独标 @Mapper 注解,比较啰嗦。

3.2 数据源与 MyBatis 核心配置详解

所有持久层框架的第一步都是把数据源配对。Spring Boot 2.x 开始默认用 HikariCP 作为连接池,这个选择很聪明,HikariCP 性能极好,而且配置极简。在 application.yml 里这样写:

spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: app_user password: YourPassword driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000

连接 JDBC URL 的格式是固定的:jdbc:postgresql://主机:端口/数据库名。如果本机用 Docker 映射了 5433,这里的 URL 就要写成 jdbc:postgresql://localhost:5433/mydb。

然后配置 MyBatis 自身的参数:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

逐项解释一下:

  • mapper-locations 指定 XML 映射文件放哪。我习惯在 resources 下建一个 mapper 目录,所有 XML 统一放在那里。Spring Boot 默认不会把 src/main/java 底下的 XML 打进 classpath,所以千万别把 XML 放在 Mapper 接口同级目录,除非你额外加资源构建配置。放在 resources/mapper 下是最省心的。
  • type-aliases-package 会让 MyBatis 把该包下所有类注册为短别名。比如实体类 User,在 XML 里可以直接用 resultType="User" 而不写全限定类名。
  • map-underscore-to-camel-case 开启下划线转驼峰。PostgreSQL 里字段命名规范用 user_name,Java 属性用 userName,这个开关一开,resultMap 都不用写字段映射了。
  • log-impl 设成 StdOutImpl,执行 SQL 时会直接把参数和执行结果打印到控制台,开发阶段排查 SQL 简直神器。

配好之后,写个最简单的查询验证整条链路。新建一个表:

CREATE TABLE t_user ( id BIGSERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

然后写一个 Mapper 接口:

public interface UserMapper { List<User> findAll(); }

XML 映射:

<select id="findAll" resultType="User"> SELECT id, name, email, created_at FROM t_user </select>

启动应用看到 SQL 打印出来,说明 Spring Boot、MyBatis、PostgreSQL 三者已经通了。这一步是整个脚手架的地基,地基稳了,后面加 CRUD、加缓存都是锦上添花。

3.3 MyBatis 的初始化工作流程,一次讲透

这个点也是面试高频题。MyBatis 整个初始化工作流程,拆开看是四个阶段。

第一阶段是构建 SqlSessionFactory。MyBatis 启动时要解析全局配置文件和应用里的 Mapper XML,把每一个 SQL 语句、参数映射、结果映射都包装成 MappedStatement 对象,放入 Configuration 对象中。Configuration 是 MyBatis 的“心脏”,里面存了所有运行时需要的元信息。

第二阶段是创建 SqlSessionFactory。通过 SqlSessionFactoryBuilder 读配置,生成一个不可变的 SqlSessionFactory 单例。在 Spring Boot 环境里,mybatis-spring-boot-starter 替我们自动完成了这一个过程,它会在 Spring 容器启动时调用 SqlSessionFactoryBuilder,获取数据源和配置,构建出工厂。

第三阶段是创建 SqlSession。SqlSession 是 MyBatis 执行 SQL 的门面。在 Spring 集成环境下,我们不会直接操作原生 SqlSession,而是使用 SqlSessionTemplate。SqlSessionTemplate 实现了线程安全,每个需要执行 Mapper 方法的时候,从工厂拿到一个 SqlSession,执行完关闭,提交/回滚交给 Spring 的数据库事务管理统一处理。

第四阶段是 Mapper 代理。Mapper 接口本身没有实现类,MyBatis 在启动时通过 JDK 动态代理为每个接口生成代理对象。你调用 userMapper.findAll(),代理对象会找到对应的 MappedStatement,交给 SqlSessionTemplate 执行,最后把 JDBC ResultSet 通过 ResultMap 映射成 Java 对象返回。

这条链路理解了,后面很多坑都能对上号。比如为什么 MyBatis 缓存失效、为什么 Mapper 不能被 Spring 的切面代理处理、为什么一个接口可以没有实现类。这些面试题的本质都是这条链路。

4. 写一个完整的增删改查:动态 SQL、TypeHandler 与分页

4.1 实体类、Mapper 接口与 XML 映射文件的正确对齐方式

建表、建实体、建接口、建 XML,这是 MyBatis 项目的标准四件套。以用户表为例:

@Data public class User { private Long id; private String name; private String email; private LocalDateTime createdAt; private String status; }

PostgreSQL 的 BIGSERIAL 自增主键,在 MyBatis 插入时需要告诉它主键是数据库生成的,以及要把生成的主键回填到对象的 id 属性上:

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_user (name, email, status, created_at) VALUES (#{name}, #{email}, #{status}, #{createdAt}) </insert>

关于参数传递,有个细节值得单独拎出来说:Mapper 接口方法有多个参数时,一定要用 @Param 注解指定参数名。比如:

List<User> findByStatusAndName(@Param("status") String status, @Param("name") String name);

XML 里就写 #{status} 和 #{name}。如果不加 @Param,MyBatis 也可以用参数位置下标,比如 #{param1}、#{param2},但这种写法可读性太差,代码审查时几乎过不了。还有 #{} 和 ${} 的区别,这是 MyBatis 面试必考:#{} 是预编译占位符,会生成 JDBC 的 PreparedStatement 参数,安全;${} 是字符串拼接,有 SQL 注入风险,只用在表名、列名、排序字段这类不能预编译的场景,并且必须做好白名单校验。

4.2 动态 SQL 实战:多条件查询、批量插入、PostgreSQL 的 ON CONFLICT

动态 SQL 是 MyBatis 最实用的能力,没有之一。一个典型场景:用户管理页面要按姓名模糊查、按状态精确查、按创建时间范围查,三个条件可组合也可留空。

<select id="findByCondition" resultType="User"> SELECT id, name, email, status, created_at FROM t_user <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="status != null and status != ''"> AND status = #{status} </if> <if test="startTime != null"> AND created_at &gt;= #{startTime} </if> <if test="endTime != null"> AND created_at &lt;= #{endTime} </if> </where> ORDER BY created_at DESC </select>

标签有两个隐藏功能:自动在前面补 WHERE 关键字;自动把第一个多余的 AND 或 OR 去掉。这两个功能在动态条件查询里至关重要,少了它你得自己在每个 if 里拼表名和 AND,极易出错。

批量插入也是日常高频操作。一个常见错误是把一万条数据放到 for 循环里一条条 insert,速度慢得让人怀疑人生。正确写法是用 foreach:

<insert id="batchInsert"> INSERT INTO t_user (name, email, status, created_at) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.email}, #{item.status}, #{item.createdAt}) </foreach> </insert>

但要记住两个限制:PostgreSQL 对单条 INSERT 语句能携带的多值列表有上限,一般是 65535 个参数,所以一次批量插入的最佳实践是 500 到 1000 条分一批,别一个批次塞几万条;第二个是批量数据量太大的话,SQL 字符串本身可能超出数据库参数上限或者内存限制。

PostgreSQL 还有一个 MySQL 没有的杀手级语法:ON CONFLICT,也就是插入时存在唯一键冲突就执行更新。MyBatis 里可以直接写:

<insert id="upsertUser"> INSERT INTO t_user (id, name, email) VALUES (#{id}, #{name}, #{email}) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email </insert>

这种写法在同步任务、定时抓取场景里特别实用,不用先查再判断插入还是更新,一次 SQL 完成。

4.3 TypeHandler 工作流程与自定义 JSONB 映射

TypeHandler 是 MyBatis 里最容易讲不清楚的概念。它干的事很简单:Java 类型和 JDBC 类型之间的双向转换。

你在 XML 写的 #{name},执行时 MyBatis 会调用对应的 TypeHandler 的 setParameter 方法,把 Java 对象的属性值设置到 PreparedStatement 上;查询返回的 ResultSet,MyBatis 又会调用 TypeHandler 的 getResult 方法,把数据库列的值取出来构造成 Java 对象。所以一个完整的 TypeHandler 类型处理器,包含两个方向的转换逻辑。

MyBatis 内置了很多常用 TypeHandler:String、Integer、Long、LocalDateTime、Map、List 等。但 PostgreSQL 有一些特有类型,比如 jsonb、数组类型,内置 TypeHandler 处理不了,需要自定义。

我在项目里最常用的是把 PostgreSQL 的 jsonb 字段映射成 Jackson 的 JsonNode 对象。做法是继承 BaseTypeHandler 并指定泛型:

@MappedTypes(JsonNode.class) @MappedJdbcTypes(JdbcType.OTHER) public class JsonNodeTypeHandler extends BaseTypeHandler<JsonNode> { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void setNonNullParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.toString()); } @Override public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } @Override public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } @Override public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private JsonNode parse(String json) { try { return MAPPER.readTree(json); } catch (Exception e) { return null; } } }

这里用 setString 的原因很简单:PostgreSQL JDBC 驱动对 jsonb 类型的处理比较特殊,直接以文本形式写入就能被数据库正确识别。而读取时,数据库返回的本身就是 JSON 字符串,交给 Jackson 解析即可。

在 XML 里使用时要指定 typeHandler:

<resultMap id="UserResultMap" type="User"> <result column="profile" property="profile" typeHandler="com.example.demo.handler.JsonNodeTypeHandler"/> </resultMap>

如果你只是处理一个字段,也可以用注解直接用在大字段上。TypeHandler 在面试里被高频问到“工作流程是什么样的”,其实就是 setParameter 和 getResult 这两个方向的转换时机,把这个讲清楚就达标了。

4.4 分页查询:PageHelper 与手写 limit 的正确姿势

分页是后端系统躲不开的需求。集成 PageHelper 是最常见的做法,Maven 加依赖:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>

然后应用类上加配置或者直接在使用时调用:

PageHelper.startPage(pageNum, pageSize); List<User> users = userMapper.findByCondition(query); PageInfo<User> pageInfo = new PageInfo<>(users);

PageHelper 的原理是拦截器。调用 startPage 后会往 ThreadLocal 里存分页参数,接下来执行的第一个 Mapper 查询会被 PageInterceptor 拦截,先在原 SQL 基础上生成 count 统计 SQL,再拼接 limit 实现分页。

但我必须提醒一点:PageHelper 的 count 查询在复杂 SQL 上经常“自作聪明”。比如你的查询里带了 GROUP BY、带了 UNION,自动生成的 count SQL 可能统计错误。所以我现在的项目分页已经不怎么依赖 PageHelper 了,更喜欢手写 limit:

<select id="findByPage" resultType="User"> SELECT id, name, email, status, created_at FROM t_user ORDER BY created_at DESC LIMIT #{pageSize} OFFSET #{offset} </select>

offset 的计算由业务代码完成。这种方式可控、性能可预期,而且在大数据量下更容易做优化。还有一个深层翻页优化的技巧:如果页数很深,直接用 OFFSET 100000 这种写法,数据库要扫描前面十万行才能返回结果,性能会很差。常见做法是改成基于游标的分页方式,用 id > lastMaxId 或者 created_at < lastMaxTime 来切分,这就是另一种思路了。

5. 缓存、日志与性能优化:别等线上才补课

5.1 MyBatis 一级缓存与二级缓存的真实情况

MyBatis 缓存是面试题常客,也是日常开发里最容易产生“我以为它生效了其实没有”的错觉的地方。

一级缓存是 SqlSession 级别的。同一个 SqlSession 中执行相同的查询,第二次会直接走缓存不查库。但在 Spring Boot 整合之后,默认情况下我们每次通过 Mapper 接口执行方法,SqlSessionTemplate 都会新建一个 SqlSession,执行完就关闭。也就是说,不开启事务时一级缓存本质上不生效,因为 SqlSession 生命周期太短。唯一能让一级缓存发挥作用的方式是加 @Transactional,让同一个事务内共享同一个 SqlSession。

二级缓存是 Mapper 级别的,跨 SqlSession 共享。开启方式很直接:在 Mapper XML 里加一行:

<cache/>

然后实体类要实现 Serializable,否则序列化缓存会报错。开启后,同一个 namespace 下所有查询结果都进二级缓存,其他 SqlSession 也能读到。

但是二级缓存在实际生产里问题很多。首先是脏读问题:如果两个 Mapper 操作同一张表,一个开启了缓存、另一个没开或者 namespace 不同,一方更新数据后,另一方缓存里的旧数据照样被读出来。其次是缓存命中率问题:只要有任何更新操作,MyBatis 默认会清空整个 namespace 的缓存,在写多读少的业务里,缓存基本白开。

我的结论是:MyBatis 二级缓存适合那种极其稳定、极少更新的配置型数据,比如数据字典、行政区划表、系统参数表。业务数据表就用业务层缓存,也就是 Spring Cache 或者 Redis,可控性高得多。

5.2 SQL 日志打印与慢 SQL 定位

开发阶段,我强烈建议把 MyBatis SQL 日志打出来。两种方式,一种是前面配置里写过的 StdOutImpl,把 SQL 直接打到 stdout;另一种是按 Mapper 包设置 debug 日志级别:

logging: level: com.example.demo.mapper: debug

这种方式更精细,生产环境也可以保留,配合日志采集系统能记录每个 Mapper 方法的实际 SQL。但要注意:日志里会打印完整的 SQL 参数值,包括用户姓名、手机号这类敏感信息,生产环境一定要做脱敏,或者用占位符方式记录。

慢 SQL 定位也不能只靠应用日志。PostgreSQL 端有两个插件非常有用:auto_explain 和 pg_stat_statements。在 postgresql.conf 里配置:

shared_preload_libraries = 'auto_explain,pg_stat_statements'

重启 PostgreSQL 后,设置:

ALTER SYSTEM SET auto_explain.log_min_duration = '500ms'; ALTER SYSTEM SET pg_stat_statements.track = ALL;

然后执行 pg_reload_conf()。之后所有执行超过 500ms 的 SQL 都会被记录到 PostgreSQL 日志,pg_stat_statements 视图能按总耗时、调用次数、平均耗时排序,一眼找到慢查询。这套组合拳比单纯肉眼扫应用日志高效得多。

5.3 业务级缓存接入:Caffeine 和 Spring Cache

业务数据想加缓存,我首选 Spring Cache 配合 Caffeine,本地缓存方案足够应对大多数单体应用。

配置很简单:

spring: cache: type: caffeine caffeine: spec: maximumSize=10000,expireAfterWrite=5m

然后在 Service 方法上加注解:

@Cacheable(value = "userList", key = "#query.toString()") public List<User> listUsers(UserQuery query) { return userMapper.findByCondition(query); }

缓存 key 的设计是个技术活。最简单粗暴的是把整个查询对象作为 key,但如果对象里有时间戳、页码、随机数这种字段,缓存命中率会非常低。更好的方式是把查询条件拆出来拼成规范化字符串,或者只缓存热点数据,比如按 id 缓存单条记录。Caffeine 的 expireAfterWrite 是写入后过期,这个语义很好理解,适合读多写少的静态数据。

我为什么不用 MyBatis 二级缓存做这件事?核心原因是缓存维度的问题。MyBatis 二级缓存锁死在 Mapper namespace 上,一旦涉及多表关联查询,或者跨 Mapper 的写操作,缓存一致性很难保证。而业务缓存可以控制 Service 层,粒度更粗,也更容易配合失效逻辑。

6. 常见问题排查与避坑实录

6.1 连接类问题:服务起不来、连不上、认证失败

这套组合里,90% 的“数据库连不上”问题出在 PostgreSQL 的配置层面,应用代码反而很少出问题。我按排查路径列一下典型场景。

第一个是服务本身没起来。Linux 上执行 systemctl status postgresql-17,Windows 上打开服务管理器看 PostgreSQL 服务状态,没起来就先看日志。日志里最常见的错误是没有权限访问数据目录。CentOS 上装完 PG 后数据目录默认是 /var/lib/pgsql/17/data,属主必须是 postgres 用户,如果你手动改过目录权限导致属主不对,把属主改回去再重试。

第二个是网络层连不通。远程连不上时先确认两件事:postgresql.conf 里 listen_addresses 是否包含了服务器 IP 或者 *;pg_hba.conf 里是否配置了允许该来源 IP 的认证规则。默认 pg_hba.conf 只允许 local 和 127.0.0.1,外部连接会被直接拒绝。改完后要 reload 配置,记住是 reload 不是 restart,reload 不中断现有连接。

第三个是认证失败。错误信息一般是 password authentication failed for user xxx。这时候要检查 pg_hba.conf 里的认证方式,常见的 scram-sha-256 是加盐哈希,如果之前用明文密码建的用户,在 PG 14 之后默认无法用明文认证。还有一种情况是数据库角色本身密码为空,重新 ALTER ROLE user WITH PASSWORD 一下就好。

第四个是 JDBC 连接参数中的 SSL 报错。PostgreSQL JDBC 驱动默认会尝试 SSL 连接,如果服务端不支持或者配置不对,会报 sslmode 相关错误。开发环境直接在连接串后面加 ?sslmode=disable 就能消除,但生产环境要按安全要求配置 SSL。

6.2 映射类问题:时间字段、NULL 处理、PostgreSQL 特有类型

时间字段映射是 MyBatis 跨数据库时最闹心的问题。在 PostgreSQL 里,TIMESTAMP 映射到 Java 的 LocalDateTime 很顺畅。但有几个细节要注意。

第一,PostgreSQL 的 timestamp with time zone,也就是 timestamptz 类型,映射到 LocalDateTime 时可能会因为时区产生偏差。解决的思路是连接串里显式指定时区:

jdbc:postgresql://localhost:5432/mydb?timezone=Asia/Shanghai

第二,MyBatis 在设置 null 参数时,默认会把它当作 JDBC 的 OTHER 类型,PostgreSQL 对这种歧义类型很敏感,有时会报“无法确定参数的数据类型”。解决办法是在 XML 里写 #{createdAt, jdbcType=TIMESTAMP},传入 null 时指定明确的 JDBC 类型。更全局的做法是在 MyBatis 配置里加 jdbc-type-for-null=NULL,但这个配置在某些场景会引起反向问题,我建议按字段单独处理。

第三,Oracle 用 MyBatis 查询时间映射容易踩的坑是 DATE 类型,Oracle 的 DATE 其实是带时分秒的,跟 MySQL 和 PostgreSQL 的 DATE 语义不一样,映射到 Java 的 java.util.Date 时要小心。如果你维护过 Oracle 迁移到 PostgreSQL 的项目,这一步一定要整理一份字段类型映射对照表,逐个验证。

还有一个热词问题:MyBatis 能支持 Gauss 这类兼容 PostgreSQL 协议国产数据库吗。答案是可以尝试,但必须测试。这类数据库大多兼容 PostgreSQL 的驱动协议,应用层可以换驱动,但具体的 JSON 函数、主键自增方式、特殊类型支持各有差异,MyBatis 的方言和 TypeHandler 可能都需要微调。我的建议是选型阶段拿真实 schema 和 SQL 跑一遍兼容性测试,别只看宣传。

6.3 性能问题:批量插入慢与分页查询慢

批量插入是日常高频坑。同样的代码,插 1000 条数据,MySQL 下还好,PostgreSQL 下单条 insert 循环能给你干到几秒起步。除了用 foreach 多值插入之外,还有两个能明显提性能的点。

第一个是 JDBC URL 加参数:

jdbc:postgresql://localhost:5432/mydb?reWriteBatchedInserts=true

这个参数对使用 PreparedStatement 的 executeBatch 批量插入有效。PostgreSQL 默认批量插入时,驱动还是会逐条发送给服务器,开了这个参数后,驱动会把多条 INSERT 合并成一条多值 INSERT 发送,性能提升非常明显。

第二个是 MyBatis 的 ExecutorType。如果用的是 SqlSessionTemplate 且调用了 insert 太多次,可以考虑把 defaultExecutorType 设成 BATCH。但要注意:Batch 模式下,MyBatis 的更新操作不会立即执行,要等批量刷新才会发送到数据库,所以查询同一个事务里刚插入的数据可能查不到,这种模式适合纯插入场景,不适合混用读写。

分页查询慢的问题,我在前面提过两种方案:一是把 OFFSET 深翻页改成游标分页,二是给排序字段建合适的索引。还有一个细节:LIMIT 和 OFFSET 的值在 MyBatis 里如果用 #{}, 参数类型一定要对,PostgreSQL 的 LIMIT 是 bigint 类型,别传成字符串,否则数据库可能隐式转换导致索引失效。

6.4 其他高频搜索问题速查

结合各类高频搜索热词,我整理了一张常见问题速查表,方便直接对照。

问题可能原因解决思路
PostgreSQL 服务启动失败端口占用 / 数据目录权限不对netstat 查端口,检查目录属主
Spring Boot 启动报 Failed to determine a suitable driver classJDBC URL 配置错误检查 spring.datasource.url 前缀
连接报 no pg_hba.conf entry客户端 IP 没被允许修改 pg_hba.conf 并 reload
执行 SQL 报 column X does not exist大小写问题PostgreSQL 对双引号敏感,SQL 里字段名全小写
插入时主键冲突自增序列不同步检查序列 nextval 值,必要时 setval
MyBatis 查询不到数据但不报错表名或字段大小写不匹配确认实际表结构的字段名
查询字段类型为 jsonb 报错缺少 TypeHandler自定义 JsonNodeTypeHandler
批量插入很慢JDBC URL 缺参数 / 单条插入开 reWriteBatchedInserts,用 foreach
时间字段相差 8 小时时区不一致连接串加 timezone,确认数据库时区
内存缓存不生效一级缓存被每次 SqlSession 重置开启事务或使用业务级缓存

7. 从真实项目场景看这套组合的价值

7.1 典型业务系统里的数据访问模式

搜热词时会看到很多真实项目标题,比如“基于 Spring Boot 的企业办公用品管理系统的设计与实现”“Spring Boot 设计题目商城”“Spring Boot 餐饮 SaaS AI 集成”。这些项目形态各异,但数据访问模式高度相似。

管理系统核心是 CRUD 加审批流。办公用品管理就是用品分类、库存管理、领用申请、审批记录、统计报表这一套。这类系统的复杂 SQL 集中在报表查询和汇总统计上,MyBatis 手写 SQL 的优势在这里非常突出,你可以直接写 PostgreSQL 的窗口函数做分组排名,也可以写物化视图刷报表数据,不用被 ORM 的 API 束缚。

商城类系统的关键在事务。下单时要扣库存、生成订单、扣减账户余额,多个写操作必须在同一个事务里,任何一个失败都要整体回滚。Spring Boot 的 @Transactional 加上 MyBatis 的 SqlSessionTemplate 能保证同一个事务内复用同一个 SqlSession,这正好也是一级缓存能起作用的时候。同时,PostgreSQL 的行级锁、唯一约束在并发下单场景里是库存不超卖的重要保障。

餐饮 SaaS 加 AI 的场景更加现代。菜单、订单、门店信息往往结构多变,PostgreSQL 的 JSONB 字段可以灵活存储;AI 生成菜品推荐或者智能库存预测,最终也要落到结构化查询上。这样的项目特别适合 Spring Boot 做 API 层,MyBatis 做数据层,PostgreSQL 兼顾结构化数据和非结构化数据。

这些项目的共同特点,是把数据库当作核心资产。选 MyBatis 不选 JPA,不是因为 JPA 不行,而是因为当你对 SQL 有强控制需求时,手写 SQL 比生成 SQL 更贴近业务。

7.2 可扩展的方向:代码生成器、多租户与数据同步

这套组合项目走向生产之后,有几个高频扩展方向。

代码生成器是提升团队效率的利器。MyBatis Generator 或者新版 MyBatis Code Generator 可以根据数据库表结构自动生成实体类、Mapper 接口、XML 文件,然后把生成的代码作为起点,手工调整复杂 SQL。配合 MyBatis-Plus 也能自动生成一套基础的 CRUD 方法,如果你不想重复写简单的单表操作,用 MyBatis-Plus 还能进一步简化。但要注意,MyBatis-Plus 的 LambdaQueryWrapper 虽然省代码,但在复杂查询上我还是建议走 XML,可读性和可调优性都好很多。

多租户是 SaaS 项目绕不开的话题。实现方案分两种:PostgreSQL 的 schema 隔离和行级 Tenant 字段隔离。schema 隔离每个租户一张独立表结构,安全隔离性好,但数据迁移和连接管理复杂;行级隔离实现简单,所有租户数据在同一张表里,靠 tenant_id 区分,开发成本低,但需要在所有查询上加过滤条件,防止串租户数据。用 MyBatis 做多租户的话,可以使用拦截器统一添加租户条件,避免业务代码里到处写 tenant_id。

数据同步则是数据库运维层的问题。PostgreSQL 原生的逻辑复制可以把数据实时同步到备库,用于读写分离和容灾。如果需要把数据同步到其他数据库或者消息队列,可以用 Debezium 这类 CDC 工具,它的原理是读取 PostgreSQL 的逻辑复制流。反过来,如果你要做增量同步软件选型,核心要点是:是否支持 PostgreSQL 逻辑复制、是否能处理 DDL 变更、延迟是多少、是否支持数据回放。没有一层不变的最佳方案,只有按业务取舍。

7.3 一些个人实践心得

写到最后,分享几个我自己长期使用这套组合沉淀下来的习惯。

项目初始化时,我永远先把版本组合钉死。Spring Boot 版本、mybatis starter 版本、JDBC 驱动版本、PostgreSQL 大版本,写进 README 的“技术栈清单”里。这个动作看起来简单,但能帮团队省掉大把的依赖冲突排查时间。

数据库 schema 一定要纳入版本管理。就算项目只有一个人开发,也建议用 Flyway 管理 SQL 变更脚本。V1__init.sql、V2__add_user_role.sql 这种文件命名,按顺序执行,每次部署环境一变,数据库结构永远和代码同步。我见过太多项目数据库结构和代码对不上,最后只能靠人肉比对,这是最让人崩溃的维护方式。

还有一个小技巧:开发环境连接串里显式把 log 打开,第一次跑通时把日志里打印的 SQL 复制到 pgAdmin 或者 DBeaver 里执行,确认 SQL 本身没问题。这能快速定位是 SQL 写错还是 MyBatis 配置错误。DBeaver 和 pgAdmin 都是开源免费工具,完全够用,不需要去找什么商业客户端的破解版,一旦用了没保障的东西,后果可能比付费更严重。

这套 Spring Boot + MyBatis + PostgreSQL 的组合,我已经在多个项目里验证过它的稳定性和可维护性。你把它学会之后,不管是你自己的毕设、公司内部管理系统,还是长期迭代的商用产品,都能稳稳拿住。希望这篇文章里的配置、代码和排障经验,能让你少走我当年走过的那些弯路。

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

宠物店管理系统全栈实战:SpringBoot+Vue+uniapp设计与部署

又到毕业设计选题季和各路程序员找项目练手的节点&#xff0c;宠物店管理系统在Java方向的热度一直居高不下。我自己做毕设辅导和全栈项目交付这些年&#xff0c;基于Java SpringBoot/SSM Vue uniapp这套组合的宠物店系统&#xff0c;前前后后落地了不少。这篇文章把这类项目…

作者头像 李华
网站建设 2026/9/30 3:27:32

Cisco 3560三层交换机配置实战:SVI、路由、PBR与安全加固

简介&#xff1a;本资源是一份面向网络工程师、高校通信/计算机专业学生及思科认证备考者的三层交换机实操指南&#xff0c;聚焦Cisco Catalyst 3560-E系列设备的全面配置与应用。内容系统覆盖设备硬件特性&#xff08;如万兆上行、PoE供电、冗余电源&#xff09;、IOS软件操作…

作者头像 李华
网站建设 2026/9/30 3:27:04

东方云权通全开源商城源码:中小企业高并发架构设计与部署实战

1. 项目整体认识与选型拆解1.1 项目定位&#xff1a;中小企业商城系统的“开源答案”先说说我为什么会盯上东方云权通这套东西。做电商系统这行久了&#xff0c;很多朋友问我要一套“能跑起来、能撑住流量、又不至于把预算烧穿”的商城源码&#xff0c;坦白说市面上选择很多——…

作者头像 李华
网站建设 2026/9/30 3:26:45

告别容器数据丢失:Docker数据卷挂载原理与实战

作为一个成天跟容器打交道的开发者&#xff0c;我想先聊一个特别普遍的痛点——很多人第一次用 Docker 跑 MySQL、Redis 或者 Nginx 的时候&#xff0c;容器跑得好好的&#xff0c;数据往里写了一大堆&#xff0c;结果某天一个docker rm或者docker compose down之后&#xff0c…

作者头像 李华
网站建设 2026/9/30 3:26:45

CentOS 7安装Docker CE报错container-selinux依赖:三种解决路径

1. 报错现场&#xff1a;又卡在 container-selinux 上了看到这个报错&#xff0c;我一点都不意外。凡是这几年在 CentOS 7 上手动装过 Docker CE 的运维&#xff0c;大概率都被这条依赖卡过至少一次。当时的情况一般是这样的&#xff1a;你按网上教程配好了 docker-ce 的 yum 源…

作者头像 李华
网站建设 2026/9/30 3:26:37

深入TCP Socket编程:从三次握手到粘包排查实战

引言&#xff1a;从"会调API"到"真懂TCP"&#xff0c;还差一层"深悟"早年学计算机网络的时候&#xff0c;我最大的困惑不是协议本身&#xff0c;而是协议和代码之间到底怎么对上号。教科书告诉你TCP要三次握手&#xff0c;可我拿着connect()和ac…

作者头像 李华