Skip to content
第 51 章 后端 ⏱ 10 分钟阅读

第 51 章:数据源与事务 ​

学习目标 ​

  • 配置多数据源
  • 掌握 @Transactional 用法
  • 避开事务失效的坑

一、HikariCP(默认连接池) ​

💡 连接池干嘛的? 每次新建连接要走 TCP + 鉴权,开销很大。连接池预先开一批连接放着,业务要查 SQL 时借一个、用完还回去,避免每次都重建。

Spring Boot 2.x 默认用 HikariCP,业界最快,spring-boot-starter-jdbc / data-jpa 都自带,不用额外引依赖。

yaml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/myapp
    username: root
    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20        # 最大连接数(默认 10)
      minimum-idle: 5              # 最小空闲
      connection-timeout: 30000    # 拿连接超时 30s
      idle-timeout: 600000         # 空闲 10min 回收
      max-lifetime: 1800000        # 连接最长 30min(必须小于 DB 的 wait_timeout)
      pool-name: MyHikariPool

连接数怎么算?

maximum-pool-size = (核心数 * 2) + 有效硬盘数

数据库 1000 并发,100 个连接够用,再多会拖垮 DB。

二、多数据源 ​

💡 多数据源 = 一个应用连多个数据库。常见两种场景:业务拆分(用户库 + 订单库)、读写分离(主库写 + 从库读)。配置思路:两个 DataSource bean + @MapperScan 把 mapper 包绑到对应数据库,代码看着一样,底层走不同库。

场景:用户库 + 订单库分开。

java
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.user")
    public DataSource userDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.order")
    public DataSource orderDataSource() {
        return DataSourceBuilder.create().build();
    }
}
yaml
spring:
  datasource:
    user:
      url: jdbc:mysql://localhost:3306/user_db
      username: root
      password: xxx
    order:
      url: jdbc:mysql://localhost:3306/order_db
      username: root
      password: xxx

Mapper 扫描不同包:

java
@Configuration
@MapperScan(basePackages = "com.example.mapper.user", sqlSessionTemplateRef = "userSqlSessionTemplate")
public class UserDataSourceConfig { }

@Configuration
@MapperScan(basePackages = "com.example.mapper.order", sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class OrderDataSourceConfig { }

⚠️ 坑 1:多数据源下事务默认不跨数据源。@Transactional 只对一个数据库生效,你这边 orderMapper.insert() 提交成功、那边 userMapper.insert() 失败回滚,两边独立,会出现"订单成功 + 用户失败"的不一致。

要跨库事务考虑:

  • 业务规避(优先):下单成功发消息,异步扣库存 / 积分,各管各的本地事务
  • 分布式事务(复杂场景):Seata,见后续章节

三、@Transactional 基础 ​

java
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderMapper orderMapper;
    private final StockMapper stockMapper;

    @Transactional    // 整个方法在一个事务里
    public void createOrder(Order order) {
        orderMapper.insert(order);
        stockMapper.decrement(order.getSkuId(), order.getCount());  // 一起成功 / 一起失败
    }
}

7 种传播行为(最常用的 3 种):

传播行为含义
REQUIRED(默认)有事务就加入,没有就新建
REQUIRES_NEW无论有没有,都开新事务(挂起当前)
NESTED嵌套事务(子事务回滚不影响父)
java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation() {    // 无论如何,日志独立提交
    logMapper.insert(log);
}

⚠️ 坑 2:@Transactional 抛 RuntimeException 才回滚。受检异常不回滚!

java
@Transactional
public void create() throws IOException {    // 抛 IOException 不回滚!
    orderMapper.insert(order);
    throw new IOException();    // ❌ 不会回滚
}

// ✅ 显式指定
@Transactional(rollbackFor = Exception.class)
public void create() throws Exception { }

四、事务失效的 6 个坑 ​

坑 1:自调用 ​

java
@Service
public class OrderService {
    @Transactional
    public void createOrder() { }

    public void createOrderWithLog() {
        this.createOrder();   // ❌ 走 this,不走代理,事务失效
    }
}

// ✅ 注入自己
@Service
public class OrderService {
    @Autowired private OrderService self;   // 自己注入自己

    public void createOrderWithLog() {
        self.createOrder();   // 走代理,生效
    }
}

坑 2:异常被吞 ​

java
@Transactional
public void create() {
    try {
        orderMapper.insert(order);
    } catch (Exception e) {
        log.error("失败", e);   // ❌ 异常被吞,事务不会回滚
    }
}

坑 3:方法不是 public ​

java
@Transactional
private void create() { }   // ❌ private 不生效

坑 4:数据库引擎不支持 ​

MySQL MyISAM 没有事务,必须用 InnoDB。

坑 5:没被 Spring 管理 ​

java
// ❌ new 出来的对象
new UserService().create();   // 事务不生效

坑 6:多线程调用 ​

java
@Transactional
public void create() {
    new Thread(() -> orderMapper.insert(order)).start();   // ❌ 子线程不受当前事务控制
}

五、事务隔离级别 ​

MySQL 默认 REPEATABLE READ,Spring 默认跟随数据库。

java
@Transactional(isolation = Isolation.READ_COMMITTED)
public void read() { }
隔离级别脏读不可重复读幻读
READ_UNCOMMITTED✅✅✅
READ_COMMITTED❌✅✅
REPEATABLE_READ❌❌✅
SERIALIZABLE❌❌❌

生产推荐 READ_COMMITTED(行锁 + 间隙锁少,幻读用业务补偿)。

六、本章小结 ​

要点关键
HikariCP最大连接数 = (核心数 × 2) + 硬盘数
事务@Transactional
失效坑自调用/异常被吞/private/非 Spring 管理
跨库事务业务规避 / Seata

动手练习 ​

  1. 写 @Transactional 转账(扣 A 加 B),故意抛异常验证回滚
  2. 故意自调用,看事务是否生效

下一章:第 52 章:数据库设计与索引 →

本站基于 VitePress 构建 · 由 StackHub 团队维护