跳到主要内容

数据访问

本章将介绍 Spring Boot 中数据访问的核心技术,包括 Spring Data JPA、MyBatis 集成和事务管理。

数据源配置

添加依赖

<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<!-- JDBC 起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

数据源配置

spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver

# HikariCP 连接池配置(Spring Boot 默认)
hikari:
minimum-idle: 5 # 最小空闲连接数
maximum-pool-size: 20 # 最大连接数
idle-timeout: 30000 # 空闲连接超时时间(毫秒)
max-lifetime: 1800000 # 连接最大存活时间(毫秒)
connection-timeout: 30000 # 连接超时时间(毫秒)
pool-name: MyHikariCP # 连接池名称

多数据源配置

@Configuration
public class DataSourceConfig {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.primary")
@Primary
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}

@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}

Spring Data JPA

添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

JPA 配置

spring:
jpa:
hibernate:
ddl-auto: update # 自动更新表结构
show-sql: true # 显示 SQL
properties:
hibernate:
format_sql: true # 格式化 SQL
dialect: org.hibernate.dialect.MySQLDialect

ddl-auto 选项说明

说明
none不做任何操作
validate验证表结构与实体是否一致
update自动更新表结构(保留数据)
create每次启动创建新表(删除旧数据)
create-drop启动创建,关闭删除

实体类

@Entity
@Table(name = "t_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50)
private String name;

@Column(unique = true, nullable = false, length = 100)
private String email;

@Column(name = "phone_number", length = 20)
private String phone;

private Integer age;

@Enumerated(EnumType.STRING)
private Gender gender;

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_at", updatable = false)
private Date createdAt;

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "updated_at")
private Date updatedAt;

@PrePersist
public void prePersist() {
this.createdAt = new Date();
this.updatedAt = new Date();
}

@PreUpdate
public void preUpdate() {
this.updatedAt = new Date();
}
}

public enum Gender {
MALE, FEMALE, OTHER
}

常用 JPA 注解

注解作用
@Entity标识为实体类
@Table指定表名
@Id标识主键
@GeneratedValue主键生成策略
@Column字段映射
@Enumerated枚举类型映射
@Temporal日期时间类型映射
@Transient忽略字段(不映射)
@OneToOne一对一关联
@OneToMany一对多关联
@ManyToOne多对一关联
@ManyToMany多对多关联

Repository 接口

// 基础 Repository
public interface UserRepository extends JpaRepository<User, Long> {

// 根据方法名自动生成查询
User findByName(String name);

List<User> findByAgeGreaterThan(Integer age);

List<User> findByAgeBetween(Integer min, Integer max);

List<User> findByNameContaining(String name);

User findByEmail(String email);

// 使用 @Query 自定义查询
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);

// 原生 SQL 查询
@Query(value = "SELECT * FROM t_user WHERE age > :age", nativeQuery = true)
List<User> findByAgeGreaterThanNative(@Param("age") Integer age);

// 更新操作
@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
int updateName(@Param("id") Long id, @Param("name") String name);

// 分页查询
Page<User> findByAgeGreaterThan(Integer age, Pageable pageable);
}

关键字查询

关键字示例SQL 片段
AndfindByNameAndAgeWHERE name = ? AND age = ?
OrfindByNameOrAgeWHERE name = ? OR age = ?
BetweenfindByAgeBetweenWHERE age BETWEEN ? AND ?
LessThanfindByAgeLessThanWHERE age < ?
GreaterThanfindByAgeGreaterThanWHERE age > ?
LikefindByNameLikeWHERE name LIKE ?
ContainingfindByNameContainingWHERE name LIKE %?%
StartingWithfindByNameStartingWithWHERE name LIKE ?%
EndingWithfindByNameEndingWithWHERE name LIKE %?
InfindByAgeInWHERE age IN (?)
IsNullfindByNameIsNullWHERE name IS NULL
NotNullfindByNameNotNullWHERE name IS NOT NULL
OrderByfindByAgeOrderByNameWHERE age = ? ORDER BY name
NotfindByNameNotWHERE name <> ?

分页查询

// Service 层
public Page<User> findByPage(int page, int size, String sort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort).descending());
return userRepository.findAll(pageable);
}

// Controller 层
@GetMapping
public Result<Page<User>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sort
) {
return Result.success(userService.findByPage(page, size, sort));
}

分页响应结构

{
"content": [...],
"totalElements": 100,
"totalPages": 10,
"size": 10,
"number": 0,
"first": true,
"last": false
}

MyBatis 集成

添加依赖

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>

MyBatis 配置

mybatis:
mapper-locations: classpath:mapper/**/*.xml # Mapper XML 文件位置
type-aliases-package: com.example.entity # 实体类包路径
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Mapper 接口

@Mapper
public interface UserMapper {

/**
* 查询所有用户
*/
List<User> findAll();

/**
* 根据 ID 查询
*/
User findById(Long id);

/**
* 插入用户
*/
int insert(User user);

/**
* 更新用户
*/
int update(User user);

/**
* 删除用户
*/
int deleteById(Long id);

/**
* 条件查询
*/
List<User> findByCondition(@Param("name") String name, @Param("age") Integer age);
}

Mapper XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.UserMapper">

<!-- 结果映射 -->
<resultMap id="UserResultMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="email" column="email"/>
<result property="phone" column="phone_number"/>
<result property="age" column="age"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>

<!-- 查询所有 -->
<select id="findAll" resultMap="UserResultMap">
SELECT * FROM t_user ORDER BY created_at DESC
</select>

<!-- 根据 ID 查询 -->
<select id="findById" resultMap="UserResultMap">
SELECT * FROM t_user WHERE id = #{id}
</select>

<!-- 插入 -->
<insert id="insert" parameterType="com.example.entity.User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user (name, email, phone_number, age, created_at, updated_at)
VALUES (#{name}, #{email}, #{phone}, #{age}, NOW(), NOW())
</insert>

<!-- 更新 -->
<update id="update" parameterType="com.example.entity.User">
UPDATE t_user
SET name = #{name},
email = #{email},
phone_number = #{phone},
age = #{age},
updated_at = NOW()
WHERE id = #{id}
</update>

<!-- 删除 -->
<delete id="deleteById">
DELETE FROM t_user WHERE id = #{id}
</delete>

<!-- 条件查询 -->
<select id="findByCondition" resultMap="UserResultMap">
SELECT * FROM t_user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
</mapper>

注解方式

@Mapper
public interface UserMapper {

@Select("SELECT * FROM t_user WHERE id = #{id}")
User findById(Long id);

@Select("SELECT * FROM t_user ORDER BY created_at DESC")
List<User> findAll();

@Insert("INSERT INTO t_user (name, email, phone_number, age, created_at, updated_at) " +
"VALUES (#{name}, #{email}, #{phone}, #{age}, NOW(), NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);

@Update("UPDATE t_user SET name = #{name}, email = #{email}, " +
"phone_number = #{phone}, age = #{age}, updated_at = NOW() WHERE id = #{id}")
int update(User user);

@Delete("DELETE FROM t_user WHERE id = #{id}")
int deleteById(Long id);
}

事务管理

声明式事务

@Service
@Transactional
public class UserServiceImpl implements UserService {

@Autowired
private UserRepository userRepository;

@Autowired
private OrderRepository orderRepository;

/**
* 创建用户并初始化订单
* 整个方法在事务中执行
*/
public User createUserWithOrder(UserDTO userDTO) {
User user = userRepository.save(convertToEntity(userDTO));
// 创建初始订单
Order order = new Order();
order.setUserId(user.getId());
orderRepository.save(order);
return user;
}

/**
* 只读事务
*/
@Transactional(readOnly = true)
public List<User> findAll() {
return userRepository.findAll();
}

/**
* 指定回滚异常
*/
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
userRepository.deleteById(id);
}

/**
* 不回滚特定异常
*/
@Transactional(noRollbackFor = BusinessException.class)
public void update(User user) {
userRepository.save(user);
}
}

事务传播行为

传播行为说明
REQUIRED(默认)有事务则加入,无则新建
SUPPORTS有事务则加入,无则非事务执行
MANDATORY必须在事务中,否则抛异常
REQUIRES_NEW总是新建事务,暂停当前事务
NOT_SUPPORTED非事务执行,暂停当前事务
NEVER非事务执行,有事务则抛异常
NESTED嵌套事务(保存点)
@Service
public class OrderService {

/**
* 新事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderLog(Order order) {
// 即使外层事务回滚,此日志也会保存
orderLogRepository.save(new OrderLog(order));
}
}

事务隔离级别

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED
READ_COMMITTED×
REPEATABLE_READ(MySQL 默认)××
SERIALIZABLE×××
@Transactional(isolation = Isolation.READ_COMMITTED)
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}

数据库迁移

Flyway

添加依赖

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>

配置

spring:
flyway:
enabled: true
locations: classpath:db/migration # 迁移脚本位置
baseline-on-migrate: true # 首次迁移时创建基线
validate-on-migrate: true # 迁移前验证

迁移脚本命名规则

V{版本号}__{描述}.sql

示例:
V1__init_schema.sql
V2__add_user_table.sql
V3__add_order_table.sql

迁移脚本示例

-- V1__init_schema.sql
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
phone_number VARCHAR(20),
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Liquibase

添加依赖

<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>

配置

spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml

变更日志

# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: db/changelog/changes/001-init-schema.yaml
# db/changelog/changes/001-init-schema.yaml
databaseChangeLog:
- changeSet:
id: 1
author: author
changes:
- createTable:
tableName: t_user
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
- column:
name: name
type: VARCHAR(50)
constraints:
nullable: false

HikariCP 连接池深度优化

HikariCP 是 Spring Boot 默认的数据库连接池,以其高性能和低延迟著称。正确配置连接池对应用性能至关重要。

连接池工作原理

┌─────────────────────────────────────────────────────────────────────┐
│ 连接池工作原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 应用请求连接 ──> 从池中获取空闲连接 ──> 使用连接执行SQL │
│ │ │ │
│ 连接池管理 归还连接 │
│ ┌─────┴─────┐ │ │
│ ▼ ▼ ▼ │
│ 空闲连接池 活跃连接池 重新放入空闲池 │
│ (等待使用) (正在使用) │
│ │
│ 连接池优势: │
│ - 避免频繁创建/销毁连接的开销 │
│ - 控制数据库连接数量,防止连接耗尽 │
│ - 连接复用,提高性能 │
│ │
└─────────────────────────────────────────────────────────────────────┘

核心配置参数详解

spring:
datasource:
hikari:
# 连接池大小配置
maximum-pool-size: 20 # 最大连接数
minimum-idle: 5 # 最小空闲连接数

# 超时配置
connection-timeout: 30000 # 获取连接超时时间(毫秒)
idle-timeout: 600000 # 空闲连接超时时间(毫秒)
max-lifetime: 1800000 # 连接最大存活时间(毫秒)

# 连接验证配置
validation-timeout: 5000 # 连接验证超时时间(毫秒)
leak-detection-threshold: 60000 # 连接泄露检测阈值(毫秒)

# 连接池名称
pool-name: MyHikariCP

# 初始化配置
initialization-fail-timeout: 1 # 初始化失败超时
connection-init-sql: SELECT 1 # 连接初始化SQL

参数详解

参数默认值说明
maximum-pool-size10连接池最大连接数,超过此数量的请求将阻塞等待
minimum-idle与maximum相同最小空闲连接数,建议设置为与最大值相同以避免动态调整开销
connection-timeout30000ms客户端等待连接的最大毫秒数,超时将抛出异常
idle-timeout600000ms空闲连接在池中保留的最长时间,仅在minimum-idle < maximum-pool-size时生效
max-lifetime1800000ms连接的最大存活时间,建议比数据库的wait_timeout小几秒
leak-detection-threshold0(禁用)连接泄露检测阈值,如果连接未归还超过此时间,将记录警告日志

连接池大小计算

连接池大小不是越大越好,需要根据实际场景计算:

公式

连接数=CPU核心数×2+有效磁盘数平均查询时间占比\text{连接数} = \frac{\text{CPU核心数} \times 2 + \text{有效磁盘数}}{\text{平均查询时间占比}}

实践经验

# 低并发场景(内部系统)
spring:
datasource:
hikari:
maximum-pool-size: 5-10
minimum-idle: 2-5

# 中等并发场景(Web应用)
spring:
datasource:
hikari:
maximum-pool-size: 10-20
minimum-idle: 5-10

# 高并发场景(互联网应用)
spring:
datasource:
hikari:
maximum-pool-size: 20-50
minimum-idle: 10-20

注意:连接池大小需要根据数据库服务器的最大连接数限制来设置,确保所有应用实例的总连接数不超过数据库的限制。

连接泄露检测

连接泄露是指应用获取连接后忘记归还,会导致连接池耗尽:

spring:
datasource:
hikari:
# 启用连接泄露检测(开发环境推荐)
leak-detection-threshold: 60000 # 60秒未归还则记录警告

日志示例

WARN - Connection leak detection triggered for connection on thread http-nio-8080-exec-1

常见泄露场景

// 错误:忘记关闭连接
public void badExample() throws SQLException {
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
// 异常发生时连接未归还
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
}

// 正确:使用 try-with-resources
public void goodExample() throws SQLException {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
// 自动关闭资源
}
}

JMX 监控

启用 JMX 监控可以实时查看连接池状态:

spring:
datasource:
hikari:
register-mbeans: true # 注册 JMX MBean

使用 JConsole 或 VisualVM 连接后,可以监控

指标说明
ActiveConnections当前活跃连接数
IdleConnections当前空闲连接数
TotalConnections总连接数
ThreadsAwaitingConnection等待获取连接的线程数
ConnectionCreationMillis创建连接的平均耗时

生产环境最佳配置

spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
# 连接池大小(根据实际情况调整)
maximum-pool-size: 20
minimum-idle: 10

# 超时配置
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000

# 连接验证
validation-timeout: 5000
leak-detection-threshold: 60000

# 连接池名称(方便监控识别)
pool-name: ProductionPool

# 初始化SQL(验证连接有效性)
connection-init-sql: SELECT 1

# 注册JMX监控
register-mbeans: true

JPA 性能优化

JPA 虽然简化了数据访问,但不当使用会导致严重的性能问题。本节介绍常见的性能陷阱和优化方案。

N+1 查询问题

N+1 问题是 JPA 最常见的性能陷阱,会导致大量不必要的数据库查询。

问题示例

@Entity
public class Author {
@Id
private Long id;
private String name;

// 一对多关系,默认懒加载
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
}

@Entity
public class Book {
@Id
private Long id;
private String title;

@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
}

触发 N+1 的代码

// 查询所有作者(1次查询)
List<Author> authors = authorRepository.findAll();

for (Author author : authors) {
// 每个作者访问 books 集合时,各执行一次查询(N次查询)
System.out.println(author.getName() + ": " + author.getBooks().size());
}

// 总共执行:1 + N 次查询(N = 作者数量)

执行过程分析

-- 第1次查询:获取所有作者
SELECT * FROM author;

-- 第2次查询:获取第1个作者的书籍
SELECT * FROM book WHERE author_id = 1;

-- 第3次查询:获取第2个作者的书籍
SELECT * FROM book WHERE author_id = 2;

-- ... 继续 N 次

解决方案一:JOIN FETCH

使用 JPQL 的 JOIN FETCH 一次性加载关联数据:

public interface AuthorRepository extends JpaRepository<Author, Long> {

/**
* 使用 JOIN FETCH 同时加载作者和书籍
*/
@Query("SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books")
List<Author> findAllWithBooks();

/**
* 按需加载指定作者的书籍
*/
@Query("SELECT a FROM Author a LEFT JOIN FETCH a.books WHERE a.id = :id")
Optional<Author> findByIdWithBooks(@Param("id") Long id);
}

生成的 SQL

SELECT DISTINCT a.*, b.* 
FROM author a
LEFT JOIN book b ON a.id = b.author_id;

使用示例

// 只执行1次查询,获取所有作者和书籍
List<Author> authors = authorRepository.findAllWithBooks();

for (Author author : authors) {
// 不会再执行额外查询
System.out.println(author.getName() + ": " + author.getBooks().size());
}

解决方案二:EntityGraph

@EntityGraph 提供了更灵活的关联加载方式,可以在运行时决定加载策略:

@Entity
@NamedEntityGraph(
name = "Author.withBooks",
attributeNodes = @NamedAttributeNode("books")
)
public class Author {
// ... 实体定义
}

public interface AuthorRepository extends JpaRepository<Author, Long> {

/**
* 使用 EntityGraph 加载关联数据
*/
@EntityGraph(value = "Author.withBooks", type = EntityGraph.EntityGraphType.LOAD)
List<Author> findAll();

/**
* 动态 EntityGraph
*/
@EntityGraph(attributePaths = {"books"})
Optional<Author> findById(Long id);
}

EntityGraph 类型说明

类型说明
LOAD懒加载的属性变为急加载,其他保持原样
FETCH只加载指定的属性,其他懒加载属性保持懒加载

解决方案三:批量获取

配置批量获取可以减少查询次数:

spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100 # 批量获取大小
batch_fetch_style: PADDED # 批量获取风格
@Entity
public class Author {
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@BatchSize(size = 20) // 每次批量获取20个作者的书籍
private List<Book> books;
}

解决方案四:使用 DTO 投影

对于只需要部分字段的场景,使用 DTO 投影避免加载整个实体:

/**
* 接口投影
*/
public interface AuthorBookCount {
Long getId();
String getName();
Long getBookCount();
}

public interface AuthorRepository extends JpaRepository<Author, Long> {

@Query("SELECT a.id as id, a.name as name, COUNT(b) as bookCount " +
"FROM Author a LEFT JOIN a.books b " +
"GROUP BY a.id, a.name")
List<AuthorBookCount> findAuthorBookCounts();
}

/**
* 类投影(推荐)
*/
public record AuthorDTO(Long id, String name, List<String> bookTitles) {}

@Query("SELECT new com.example.dto.AuthorDTO(a.id, a.name, " +
"(SELECT GROUP_CONCAT(b.title) FROM Book b WHERE b.author = a)) " +
"FROM Author a")
List<AuthorDTO> findAuthorDTOs();

懒加载最佳实践

原则:默认使用懒加载,按需急加载。

@Entity
public class Order {
@Id
private Long id;

// 默认懒加载
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;

// 默认懒加载
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;

// 少量数据且总是需要,可以急加载
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "shipping_address_id")
private ShippingAddress shippingAddress;
}

避免懒加载初始化异常

// 方式一:在事务内加载
@Transactional(readOnly = true)
public OrderDTO getOrderDetail(Long id) {
Order order = orderRepository.findByIdWithCustomerAndItems(id);
return convertToDTO(order);
}

// 方式二:使用 JOIN FETCH
@Query("SELECT o FROM Order o " +
"LEFT JOIN FETCH o.customer " +
"LEFT JOIN FETCH o.items " +
"WHERE o.id = :id")
Optional<Order> findByIdWithDetails(@Param("id") Long id);

查询优化技巧

1. 只查询需要的字段

// 不推荐:查询所有字段
@Query("SELECT u FROM User u WHERE u.status = :status")
List<User> findByStatus(@Param("status") String status);

// 推荐:只查询需要的字段
@Query("SELECT new com.example.dto.UserDTO(u.id, u.name, u.email) " +
"FROM User u WHERE u.status = :status")
List<UserDTO> findDTOByStatus(@Param("status") String status);

2. 使用分页避免大量数据加载

// 推荐:使用分页
Page<User> findByStatus(String status, Pageable pageable);

// 使用示例
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<User> page = userRepository.findByStatus("ACTIVE", pageable);

3. 避免使用 SELECT *

// 不推荐
@Query("SELECT p FROM Product p")
List<Product> findAllProducts();

// 推荐:明确指定字段
@Query("SELECT p.id, p.name, p.price FROM Product p WHERE p.active = true")
List<Object[]> findActiveProducts();

多数据源完整配置

在企业应用中,经常需要同时访问多个数据库。Spring Boot 支持灵活的多数据源配置。

场景分析

┌─────────────────────────────────────────────────────────────────────┐
│ 多数据源常见场景 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 读写分离:主库写入,从库读取 │
│ │
│ 应用 ──写入──> 主库(Master) │
│ │ │
│ └──读取──> 从库(Slave1, Slave2...) │
│ │
│ 2. 业务分库:不同业务使用不同数据库 │
│ │
│ 订单模块 ──> 订单数据库 │
│ 用户模块 ──> 用户数据库 │
│ 日志模块 ──> 日志数据库 │
│ │
│ 3. 多租户:不同租户使用不同数据库 │
│ │
│ 租户A ──> 租户A数据库 │
│ 租户B ──> 租户B数据库 │
│ │
└─────────────────────────────────────────────────────────────────────┘

基础多数据源配置

步骤一:配置文件

spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/primary_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
secondary:
jdbc-url: jdbc:mysql://localhost:3306/secondary_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 2

jpa:
hibernate:
ddl-auto: update
show-sql: true

步骤二:主数据源配置

package com.example.config;

import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

/**
* 主数据源配置
* 管理用户相关的实体和 Repository
*/
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.primary", // Repository 所在包
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDataSourceConfig {

/**
* 配置主数据源
*/
@Bean
@Primary // 标记为主数据源
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}

/**
* 配置主数据源的 EntityManager
*/
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
DataSource primaryDataSource) {

LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(primaryDataSource);
em.setPackagesToScan("com.example.entity.primary"); // 实体类所在包
em.setPersistenceUnitName("primary");

HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);

// JPA 属性配置
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.show_sql", true);
properties.put("hibernate.format_sql", true);
em.setJpaPropertyMap(properties);

return em;
}

/**
* 配置主数据源的事务管理器
*/
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager(
LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(primaryEntityManagerFactory.getObject());
return transactionManager;
}
}

步骤三:次数据源配置

package com.example.config;

import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

/**
* 次数据源配置
* 管理订单相关的实体和 Repository
*/
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.secondary",
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDataSourceConfig {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}

@Bean
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
DataSource secondaryDataSource) {

LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(secondaryDataSource);
em.setPackagesToScan("com.example.entity.secondary");
em.setPersistenceUnitName("secondary");

HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);

Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.show_sql", true);
em.setJpaPropertyMap(properties);

return em;
}

@Bean
public PlatformTransactionManager secondaryTransactionManager(
LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(secondaryEntityManagerFactory.getObject());
return transactionManager;
}
}

步骤四:实体类组织

src/main/java/com/example/
├── entity/
│ ├── primary/ # 主数据源实体
│ │ └── User.java
│ └── secondary/ # 次数据源实体
│ └── Order.java
└── repository/
├── primary/ # 主数据源 Repository
│ └── UserRepository.java
└── secondary/ # 次数据源 Repository
└── OrderRepository.java

跨数据源事务

当需要在一个事务中操作多个数据源时,需要使用分布式事务或链式事务:

方式一:JTA 分布式事务

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
import org.springframework.transaction.annotation.Transactional;
import com.example.service.primary.UserService;
import com.example.service.secondary.OrderService;

@Service
public class BusinessService {

private final UserService userService;
private final OrderService orderService;

/**
* JTA 分布式事务
* 保证跨数据源操作的原子性
*/
@Transactional
public void createUserAndOrder(UserDTO userDTO, OrderDTO orderDTO) {
// 操作主数据源
userService.createUser(userDTO);

// 操作次数据源
orderService.createOrder(orderDTO);

// 任何一步失败,两个数据源都会回滚
}
}

方式二:编程式事务

@Service
public class BusinessService {

private final PlatformTransactionManager primaryTransactionManager;
private final PlatformTransactionManager secondaryTransactionManager;
private final UserService userService;
private final OrderService orderService;

/**
* 使用 TransactionTemplate 手动管理事务
*/
public void createUserAndOrder(UserDTO userDTO, OrderDTO orderDTO) {
TransactionTemplate primaryTx = new TransactionTemplate(primaryTransactionManager);
TransactionTemplate secondaryTx = new TransactionTemplate(secondaryTransactionManager);

primaryTx.execute(primaryStatus -> {
try {
userService.createUser(userDTO);

secondaryTx.execute(secondaryStatus -> {
orderService.createOrder(orderDTO);
return null;
});

return null;
} catch (Exception e) {
primaryStatus.setRollbackOnly();
throw e;
}
});
}
}

读写分离实现

读写分离是提升数据库性能的常见方案:

/**
* 动态数据源路由
*/
public class RoutingDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}

/**
* 数据源上下文持有者
*/
public class DataSourceContextHolder {

private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

public static final String MASTER = "master";
public static final String SLAVE = "slave";

public static void setDataSourceType(String type) {
contextHolder.set(type);
}

public static String getDataSourceType() {
return contextHolder.get();
}

public static void clearDataSourceType() {
contextHolder.remove();
}
}

/**
* 自定义注解标记读写操作
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}

/**
* AOP 切面自动切换数据源
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {

@Before("@annotation(readOnly)")
public void beforeReadOnly(ReadOnly readOnly) {
DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.SLAVE);
}

@After("@annotation(readOnly)")
public void afterReadOnly(ReadOnly readOnly) {
DataSourceContextHolder.clearDataSourceType();
}
}

/**
* 配置读写分离数据源
*/
@Configuration
public class ReadWriteDataSourceConfig {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}

@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}

@Bean
@Primary
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {

RoutingDataSource routingDataSource = new RoutingDataSource();

Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);

routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource);

return routingDataSource;
}
}

使用示例

@Service
public class UserService {

// 读操作使用从库
@ReadOnly
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}

@ReadOnly
public List<User> findAll() {
return userRepository.findAll();
}

// 写操作使用主库(默认)
public User save(User user) {
return userRepository.save(user);
}
}

数据库性能优化最佳实践

1. 索引优化

@Entity
@Table(name = "t_user", indexes = {
@Index(name = "idx_user_email", columnList = "email"),
@Index(name = "idx_user_status_created", columnList = "status, created_at")
})
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String email;

@Column(nullable = false)
private String status;

@Column(name = "created_at")
private LocalDateTime createdAt;
}

索引设计原则

  • 为经常用于查询条件的字段创建索引
  • 为排序和分组字段创建索引
  • 避免为低选择性字段(如性别)创建索引
  • 复合索引注意字段顺序(最左前缀原则)

2. 批量操作优化

配置批量操作

spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 50 # 批量大小
batch_versioned_data: true
order_inserts: true # 排序插入
order_updates: true # 排序更新

批量插入示例

@Transactional
public void batchInsert(List<User> users) {
for (int i = 0; i < users.size(); i++) {
entityManager.persist(users.get(i));

// 每50条刷新一次
if (i % 50 == 0) {
entityManager.flush();
entityManager.clear();
}
}
}

3. 连接池监控与调优

@Component
@Slf4j
public class DataSourceMonitor {

@Autowired
private DataSource dataSource;

@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void monitorConnectionPool() {
if (dataSource instanceof HikariDataSource) {
HikariPoolMXBean pool = ((HikariDataSource) dataSource).getHikariPoolMXBean();

log.info("连接池状态 - 活跃: {}, 空闲: {}, 等待: {}, 总数: {}",
pool.getActiveConnections(),
pool.getIdleConnections(),
pool.getThreadsAwaitingConnection(),
pool.getTotalConnections());

// 告警:如果活跃连接超过80%
double usageRate = (double) pool.getActiveConnections() / pool.getTotalConnections();
if (usageRate > 0.8) {
log.warn("连接池使用率过高: {}%", usageRate * 100);
}
}
}
}

4. 查询超时配置

全局配置

spring:
jpa:
properties:
hibernate:
query:
timeout: 30 # 查询超时秒数

单查询配置

@QueryHints(@QueryHint(name = "javax.persistence.query.timeout", value = "30000"))
List<User> findActiveUsers();

5. 二级缓存配置

启用二级缓存

spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
use_query_cache: true
region:
factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
javax:
persistence:
sharedCache:
mode: ALL

实体类缓存配置

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Dictionary {
// 字典等很少变化的数据适合缓存
}

6. 数据库连接健康检查

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

@Autowired
private DataSource dataSource;

@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
// 执行简单查询验证连接
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1")) {
if (rs.next()) {
return Health.up()
.withDetail("database", "MySQL")
.withDetail("validationQuery", "SELECT 1")
.build();
}
}
} catch (SQLException e) {
return Health.down()
.withException(e)
.withDetail("error", e.getMessage())
.build();
}
return Health.unknown().build();
}
}

小结

本章我们学习了:

  1. 数据源配置:单数据源和多数据源配置
  2. Spring Data JPA:实体映射、Repository 接口、分页查询
  3. MyBatis 集成:Mapper 接口、XML 配置、注解方式
  4. 事务管理:声明式事务、传播行为、隔离级别
  5. 数据库迁移:Flyway 和 Liquibase 的使用
  6. HikariCP 连接池优化:核心参数、连接泄露检测、JMX 监控
  7. JPA 性能优化:N+1 问题解决、懒加载最佳实践、EntityGraph 使用
  8. 多数据源配置:完整的多数据源配置、跨数据源事务、读写分离
  9. 数据库性能优化:索引优化、批量操作、缓存配置

练习

  1. 配置一个 MySQL 数据源并使用 JPA 创建实体类
  2. 实现一个完整的 CRUD Repository
  3. 使用 MyBatis 实现动态 SQL 查询
  4. 实现一个涉及多表操作的事务方法
  5. 使用 Flyway 管理数据库版本
  6. 配置多数据源并实现跨数据源事务
  7. 解决 JPA 的 N+1 查询问题
  8. 实现读写分离配置