核心組件:
@TargetDataSource 註解:標記方法/類使用哪個數據源。DynamicDataSourceContextHolder:ThreadLocal 存儲當前線程數據源 Key。DynamicRoutingDataSource extends AbstractRoutingDataSource:根據 Key 路由到實際 DataSource。DataSourceAspect:AOP 切面,方法執行前切換 Key,執行後清理。public enum DataSourceKey { MASTER, SLAVE_1, SLAVE_2 }
public class DynamicDataSourceContextHolder {
private static final ThreadLocal CONTEXT = new ThreadLocal<>();
public static void set(DataSourceKey key) { CONTEXT.set(key); }
public static DataSourceKey get() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); }
}
@Configuration
public class DataSourceConfig {
@Bean @ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() { return DataSourceBuilder.create().build(); }
@Bean @ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() { return DataSourceBuilder.create().build(); }
@Bean @ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource() { return DataSourceBuilder.create().build(); }
@Primary
@Bean
public DataSource routingDataSource() {
DynamicRoutingDataSource ds = new DynamicRoutingDataSource();
Map
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
DataSourceKey value() default DataSourceKey.MASTER;
}
@Aspect @Component @Order(-100) // 高優先級,早於事務切面
public class DataSourceAspect {
@Pointcut("@annotation(com.example.annotation.TargetDataSource)")
public void dsPointcut() {}
@Before("dsPointcut()")
public void switchDataSource(JoinPoint point) {
TargetDataSource ann = getAnnotation(point);
if (ann != null) {
DynamicDataSourceContextHolder.set(ann.value());
log.debug("切換數據源 -> {}", ann.value());
}
}
@After("dsPointcut()")
public void restoreDataSource() {
DynamicDataSourceContextHolder.clear();
log.debug("清理數據源上下文");
}
private TargetDataSource getAnnotation(JoinPoint point) {
MethodSignature sig = (MethodSignature) point.getSignature();
Method method = sig.getMethod();
// 方法級註解優先
TargetDataSource ann = method.getAnnotation(TargetDataSource.class);
if (ann != null) return ann;
// 類級註解兜底
return method.getDeclaringClass().getAnnotation(TargetDataSource.class);
}
}
@Service @RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final UserMapper userMapper;
@TargetDataSource(DataSourceKey.MASTER) // 寫操作走主庫
@Transactional
public Long createOrder(OrderDTO dto) {
Order order = new Order(dto);
orderMapper.insert(order);
return order.getId();
}
@TargetDataSource(DataSourceKey.SLAVE_1) // 讀操作走從庫
public OrderDetail getOrderDetail(Long id) {
Order order = orderMapper.selectById(id);
User user = userMapper.selectById(order.getUserId()); // 同一切面生效
return new OrderDetail(order, user);
}
}
| 優化 | 說明 |
|---|---|
| @Before + @After 替代 @Around | 無需手動 proceed()、異常傳播更自然、代碼更簡潔 |
| @Pointcut 復用 | 表達式定義一次,多通知復用,其他切面亦可引用 |
| @Order(-100) 早於 @Transactional | 確保數據源切換在事務開啟**之前**完成,避免事務綁定錯誤連接 |
| ThreadLocal 清理在 @After | 無論正常/異常返回,線程上下文必定清理,防止線程池污染 |
| 方法級 > 類級註解優先級 | 靈活覆蓋,細粒度控制 |
@ReadOnly,切面自動輪詢從庫(Round Robin / 隨機 / 加權)。spring:
datasource:
master:
url: jdbc:mysql://master:3306/app?useSSL=false
username: root
password: pwd
hikari:
maximum-pool-size: 20
slave1:
url: jdbc:mysql://slave1:3306/app?useSSL=false
username: readonly
password: pwd
slave2:
url: jdbc:mysql://slave2:3306/app?useSSL=false
username: readonly
password: pwd