这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 数据访问层的目标不是消除 SQL,而是把连接管理、事务边界、异常转换、资源释放和重复模板代码交给框架,让开发者专注业务模型和 SQL 质量。
11.1 技术选择
| 技术 | 适合场景 |
|---|---|
| Spring JDBC / JdbcClient | 明确 SQL 控制、复杂查询、低抽象成本 |
| Spring Data JPA | 领域模型稳定、CRUD 丰富、对象图清晰 |
| MyBatis / MyBatis-Plus | SQL 可控、团队熟悉、复杂报表多 |
| R2DBC | Reactive 数据访问 |
| JOOQ | 类型安全 SQL |
选择原则:
- 团队能力;
- SQL 复杂度;
- 性能要求;
- 领域模型复杂度;
- 维护周期;
- 招聘成本;
- 现有代码风格。
不要因为某个框架流行就频繁迁移。
11.2 DataSource 与连接池
常用 HikariCP 配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/order?useSSL=false&serverTimezone=Asia/Shanghai
username: order
password: ${DB_PASSWORD}
hikari:
pool-name: order-hikari
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 2000
validation-timeout: 1000
max-lifetime: 1800000
leak-detection-threshold: 10000
连接池不是越大越好。过大可能造成数据库上下文切换和锁等待,过小会造成请求排队。应基于数据库最大连接数、服务实例数和单请求持连接时间计算。
估算:
每实例最大连接 * 实例数 + 运维预留 <= 数据库可承受连接
11.3 JdbcClient
Spring Framework 6.1 引入 JdbcClient,提供更简洁的 JDBC API:
@Repository
public class JdbcOrderRepository {
private final JdbcClient jdbcClient;
public JdbcOrderRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
public Optional<Order> findById(Long id) {
return jdbcClient.sql("select id, user_id, status, amount from orders where id = :id")
.param("id", id)
.query(Order.class)
.optional();
}
public int insert(Order order) {
return jdbcClient.sql("""
insert into orders(id, user_id, status, amount, created_at)
values(:id, :userId, :status, :amount, :createdAt)
""")
.param("id", order.id())
.param("userId", order.userId())
.param("status", order.status())
.param("amount", order.amount())
.param("createdAt", order.createdAt())
.update();
}
}
SQL 必须使用绑定参数,避免字符串拼接导致注入。
11.4 Spring Data JPA
实体:
@Entity
@Table(name = "orders")
public class OrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 64)
private String userId;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private BigDecimal amount;
@CreationTimestamp
private Instant createdAt;
}
Repository:
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
List<OrderEntity> findByUserIdAndStatus(String userId, OrderStatus status, Pageable pageable);
}
适合:
- 领域模型与表结构较接近;
- 常规 CRUD 和简单查询;
- 持久化 dirty tracking 有价值;
- 团队理解 Hibernate 生命周期。
复杂报表、批量大写入、多表优化 SQL 时,应使用投影 DTO、原生 SQL 或 JDBC。
11.5 N+1 问题
错误代码:
List<OrderEntity> orders = repository.findAll();
for (OrderEntity order : orders) {
total += order.getItems().size(); // 每次可能触发一条 SQL
}
解决:
@EntityGraph(attributePaths = "items")
List<OrderEntity> findByUserId(String userId);
或 JPQL fetch join:
@Query("select distinct o from OrderEntity o join fetch o.items where o.userId = :userId")
List<OrderEntity> findWithItems(String userId);
开启 SQL 日志是发现 N+1 的第一步:
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
Hibernate 5 与 6 的绑定日志参数不同,需要按版本调整 logger。
11.6 事务基础
声明式事务:
@Service
public class OrderService {
@Transactional
public Order create(CreateOrderCommand command) {
Order order = Order.create(command);
repository.save(order);
outboxRepository.save(OrderEvent.outbox(order));
return order;
}
}
流程:
TransactionInterceptor
-> 获取连接并关闭自动提交
-> 执行业务
-> 正常提交
-> 异常按回滚规则回滚
-> 释放连接
事务应尽量短:
- 不在事务中调用慢 RPC;
- 不在事务中处理大文件;
- 不在事务中发送不可回滚消息;
- 批量任务分批提交;
- 查询方法按需只读事务。
11.7 传播行为
| 传播行为 | 说明 |
|---|---|
| REQUIRED | 当前有事务则加入,否则新建 |
| REQUIRES_NEW | 挂起当前事务,新建事务 |
| NESTED | 保存点嵌套,依赖驱动支持 |
| SUPPORTS | 有事务则加入,没有则非事务执行 |
| NOT_SUPPORTED | 挂起事务,非事务执行 |
| NEVER | 存在事务则报错 |
| MANDATORY | 必须已有事务,否则报错 |
示例:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAudit(AuditRecord record) {
}
REQUIRES_NEW 会占用第二个连接,高并发下容易造成连接池耗尽。操作日志不一定必须独立事务,可通过异步事件或最终一致方案处理。
11.8 隔离级别
| 隔离级别 | 常见问题 |
|---|---|
| READ UNCOMMITTED | 可能脏读 |
| READ COMMITTED | 避免脏读,可能不可重复读 |
| REPEATABLE READ | 避免不可重复读,幻读行为与数据库实现有关 |
| SERIALIZABLE | 强隔离,代价高 |
Spring 可声明:
@Transactional(isolation = Isolation.READ_COMMITTED)
实际隔离效果还取决于数据库引擎和配置。MySQL InnoDB 的实现与 SQL 标准 isolation name 不完全一一对应,需要结合当前数据库文档分析。
11.9 事务失效场景
常见错误:
@Service
public class OrderService {
@Transactional
public void create(Order order) {
repository.save(order);
this.sendNotification(order); // 自调用
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification(Order order) {
}
}
失效原因:
| 场景 | 原因 |
|---|---|
| 自调用 | 不经过代理 |
| 非 public 方法 | Spring 事务代理默认不支持 |
| 异常被 catch 吞掉 | 拦截器看不到异常 |
| 抛出受检异常 | 默认不回滚,需 rollbackFor |
| Bean 未被 Spring 管理 | 没有代理 |
| 数据源/事务管理器配置错误 | 连接不在事务上下文 |
| 多线程执行 | 事务绑定 ThreadLocal |
正确处理受检异常:
@Transactional(rollbackFor = Exception.class)
public void importOrders(File file) throws IOException {
}
11.10 分布式数据一致性
本地事务不能覆盖多个数据库或多个服务。常见方案:
| 方案 | 适用 |
|---|---|
| 本地消息表 + 异步投递 | 可接受最终一致 |
| Outbox + CDC | 消息可靠性高 |
| TCC | 需要强业务补偿 |
| Saga | 长流程编排 |
| 2PC/XA | 强一致,吞吐和运维成本高 |
Outbox 示例:
本地事务:
写 orders
写 outbox_events
后台任务或 CDC:
读取 outbox
发送 Kafka
标记已发送
不要在本地事务提交前直接发送消息,因为消息发送成功而事务回滚会造成幻象消息。
11.11 数据访问监控
指标:
hikaricp_connections_active
hikaricp_connections_pending
hikaricp_connections_timeout
jdbc_query_seconds
jdbc_query_total
spring_data_repository_seconds
慢 SQL:
- 开启数据库慢日志;
- 应用记录 SQL 耗时;
- 使用 APM 或 Micrometer Tracing;
- 定期审查执行计划;
- 加索引要评估写入成本。
连接池等待告警:
active == maximum
pending > 0
connection timeout count 增长
本章小结
Spring 提供了从 JDBC 到 JPA 的多层次数据访问抽象,核心价值是资源管理、事务边界和异常统一。生产中要控制连接池规模、避免 N+1 和长事务,理解传播行为和事务失效原因。跨库跨服务一致性应引入 Outbox、Saga 或 TCC 等方案,而不是期待 @Transactional 覆盖分布式场景。
思考题
- 如何估算 HikariCP 最大连接数?
- JPA 和 JDBC 各适合什么场景?
REQUIRES_NEW有什么风险?- 哪些常见写法会导致事务失效?
- 为什么推荐 Outbox 而不是事务内直接发消息?