跳到主要内容

3.7 事务与多数据源

DB.ds 管理数据源注册和线程内切换,DB.tx 管理本地事务。两者职责独立:切换数据源不会自动开启事务。

添加数据源

DataSourceProperty prop = new DataSourceProperty();
prop.setName("slave");
prop.setUrl("jdbc:mysql://slave-host:3306/db_slave");
prop.setUsername("root");
prop.setPassword("slave123");
prop.setDriverClassName("com.mysql.cj.jdbc.Driver");

DB.ds.setDataSource(prop);

切换数据源

// 切换到从库
List<User> users = DB.ds.use("slave", () ->
DB.pojo.selectWrapper(User.class)
.eq(User::getStatus, 1)
.queryBeanList()
);

// Runnable 重载
DB.ds.use("slave", () -> DB.pojo.insert(user));

设置默认数据源

DB.ds.setDefaultDataSource(dataSource);

删除数据源

DB.ds.removeDataSource("slave");

删除前应确认没有正在使用该数据源的请求。可以用 DB.ds.getAllDataSourceNames() 查看当前注册名称,用 DB.ds.testConnection(property) 在不注册的情况下测试连接参数。

编程式事务

默认数据源事务:

DB.tx.run(() -> {
DB.pojo.insert(order);
DB.pojo.insert(orderItem);
});

带返回值:

Long orderId = DB.tx.run(() -> {
DB.pojo.insert(order);
return order.getId();
});

指定数据源:

DB.tx.run("slave", () -> {
DB.jdbc.execute("UPDATE task SET status = ? WHERE id = ?", 1, taskId);
});

DB.tx.run(name, ...) 会在命名数据源作用域内建立本地事务;它不是跨数据源事务。

切换与事务组合

DB.ds.use("report", () ->
DB.tx.run(() -> {
DB.pojo.insert(report);
DB.pojo.insert(reportDetail);
})
);

更直接的写法是:

DB.tx.run("report", () -> {
DB.pojo.insert(report);
DB.pojo.insert(reportDetail);
});

同一线程中 DB.ds.use 会在回调结束后恢复先前的数据源上下文。不要把数据库操作交给新线程后仍假设原上下文有效。

事务边界

  • DB.tx 只承诺单数据源本地事务;
  • 同一数据源已有事务时,适配器采用 REQUIRED 式加入语义;
  • 当前公共事务抽象不提供 REQUIRES_NEWNESTED
  • 切换到另一数据源意味着另一条连接,无法与当前事务原子提交;
  • 回调抛出异常时事务回滚,异常会按框架约定继续向外传播;
  • 批量操作需要全有或全无时,必须显式置于事务中;
  • 数据源上下文和连接绑定是线程本地的,不自动跨线程传播。

框架声明式事务分别见 Spring Boot 完整集成Solon 完整集成

读写分离建议

@Service
public class UserService {
// 写操作 → 主库
public void save(User user) {
DB.tx.run("master", () -> DB.pojo.insert(user));
}

// 读操作 → 从库
public User getById(Long id) {
return DB.ds.use("slave", () ->
DB.pojo.selectWrapper(User.class).eq(User::getId, id).queryBean()
);
}
}

上例只是显式路由示意,不是自动读写分离。真实项目还需要处理从库延迟、故障转移、连接池监控以及读写一致性。