Java Spring Boot注解切换多数据源以及事务问题

完整源码: https://github.com/mucwj/example-multidatasource.git

原理:

使用注解标识要使用的数据源, 利用AOP在执行方法前切换数据源, 数据源是使用的spring提供的多数据源类(org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource)

一、多数据源实现的核心类AbstractRoutingDataSource

源码:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    // 省略
   
    /**
     * Specify the map of target DataSources, with the lookup key as key.
     * The mapped value can either be a corresponding {@link javax.sql.DataSource}
     * instance or a data source name String (to be resolved via a
     * {@link #setDataSourceLookup DataSourceLookup}).
     * 

The key can be of arbitrary type; this class implements the * generic lookup process only. The concrete key representation will * be handled by {@link #resolveSpecifiedLookupKey(Object)} and * {@link #determineCurrentLookupKey()}. */ public void setTargetDataSources(Map targetDataSources) { this.targetDataSources = targetDataSources; } /** * Specify the default target DataSource, if any. *

The mapped value can either be a corresponding {@link javax.sql.DataSource} * instance or a data source name String (to be resolved via a * {@link #setDataSourceLookup DataSourceLookup}). *

This DataSource will be used as target if none of the keyed * {@link #setTargetDataSources targetDataSources} match the * {@link #determineCurrentLookupKey()} current lookup key. */ public void setDefaultTargetDataSource(Object defaultTargetDataSource) { this.defaultTargetDataSource = defaultTargetDataSource; } /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. *

Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ @Nullable protected abstract Object determineCurrentLookupKey(); }

  1. setTargetDataSources方法设置可以切换的数据源, 参数是一个Map, 其中key是determineCurrentLookupKey方法的返回值, value是数据源实例(或者数据源名称)
  2. setDefaultTargetDataSource方法设置默认使用的数据源, 就是没有指定数据源的情况下使用的数据源, 参数是一个数据源实例(或者数据源名称)
  3. determineCurrentLookupKey方法是个抽象方法, 也就是说AbstractRoutingDataSource是需要我们去继承实现的, 后面再说怎么实现, 这个方法就是返回1.中setTargetDataSources中设置的Map的key, 通过这个key可以找到数据源

二、实现

  1. 假设有2个库user/order分别再mysql和postgresql中, 先写个枚举声明下, 免得后续硬编码
/**
 * 数据源标签
 */
public enum DSLabel {
    /**
     * 用户库
     */
    USER,
    /**
     * 订单库
     */
    ORDER
}
  1. 注解类来标识要使用的数据源
/**
 * 切换数据源的注解
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchDataSource {

    /**
     * 切换到指定的数据库
     * @return 数据库标签
     */
    DSLabel value();

}
  1. 创建一个上下文存储当前线程要使用的数据源名称(DSLabel)
/**
 * 动态切换数据源的上下文, 用来修改和获取当前线程所使用的数据源标签
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class DynamicDataSourceContext {

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

    public static void setDataSourceLabel(DSLabel label) {
        contextHolder.set(label);
    }

    public static DSLabel getDataSourceLabel() {
        return contextHolder.get();
    }

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

}
  1. 实现AbstractRoutingDataSource类
public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContext.getDataSourceLabel();
    }

}
  1. 利用AOP来动态切换数据源
/**
 * 利用AOP来动态切换数据源
 */
@Aspect
// 保证该AOP在@Transactional之前执行
@Order(-10)
@Component
public class DynamicDataSourceAspect {

    @Before("@within(SwitchDataSource) || @annotation(SwitchDataSource)")
    public void changeDataSource(JoinPoint point) {
        // 获取方法上的注解
        Method method = ((MethodSignature)point.getSignature()).getMethod();
        SwitchDataSource annotation = method.getAnnotation(SwitchDataSource.class);

        DSLabel value;
        if (Objects.isNull(annotation)) {
            // 方法上没有注解, 获取类上的注解
            annotation = point.getTarget().getClass().getAnnotation(SwitchDataSource.class);
            if (Objects.isNull(annotation)) {
                return;
            }
        }

        // 获取注解值
        value = annotation.value();
        // 切换数据源
        DynamicDataSourceContext.setDataSourceLabel(value);
    }

    @After("@within(SwitchDataSource) || @annotation(SwitchDataSource)")
    public void clean() {
        // 清理数据源的标签
        DynamicDataSourceContext.clearDataSourceLabel();
    }

}
  1. 配置
  • application.yaml
multi-data-source:
  data-source:
    - label: USER
      jdbcUrl: jdbc:mysql://localhost:3306/user?useSSL=false&allowPublicKeyRetrieval=true
      username: username
      password: password
    - label: ORDER
      jdbcUrl: jdbc:postgresql://localhost:5432/postgres?useSSL=false
      username: username
      password: password

  • 读取配置
    读取整个项目配置类
/**
 * 项目配置类
 */
@Data
@ConfigurationProperties(prefix = "multi-data-source")
@Component
public class ProjectProperty {

    /**
     * 多个数据源配置
     */
    private List datasource;

}

读取数据源配置

/**
 * 数据源配置
 */
@Data
public class DataSourceProperty {

    /**
     * 数据源标签名称
     */
    private String label;

    /**
     * jdbc url
     */
    private String jdbcUrl;

    /**
     * 数据库用户名
     */
    private String username;

    /**
     * 数据库密码
     */
    private String password;

}
  • 配置数据源Bean(RoutingDataSource)
@Configuration
public class DataSourceConfig {

    private DataSource createDataSource(DataSourceProperty property) {
        Properties properties = new Properties();
        properties.setProperty("jdbcUrl", property.getJdbcUrl());
        properties.setProperty("username", property.getUsername());
        properties.setProperty("password", property.getPassword());
        return new HikariDataSource(new HikariConfig(properties));
    }

    /**
     * 构造RoutingDataSource数据源
     * @param projectProperty 项目配置
     * @return  RoutingDataSource实例
     */
    @Bean
    public DataSource routingDataSource(ProjectProperty projectProperty) {
        Map targetMap = new HashMap<>();
        for (DataSourceProperty dataSourceProperty : projectProperty.getDatasource()) {
            DataSource dataSource = createDataSource(dataSourceProperty);
            targetMap.put(DSLabel.valueOf(dataSourceProperty.getLabel()), dataSource);
        }

        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setTargetDataSources(targetMap);
        // 设置默认使用的数据源
        routingDataSource.setDefaultTargetDataSource(targetMap.get(DSLabel.USER));
        return routingDataSource;
    }

}

其中routingDataSource方法中就有之前提到的2个方法setTargetDataSources、setDefaultTargetDataSource, 分别是设置所有可切换的数据源和设置默认数据源

三、使用注解切换数据源

比如说访问表tb_user的服务(UserService)是使用的mysql中的user库, 访问tb_order表的服务(OrderService)是使用的postgresql中的order库, 使用方式如下:

  • 标注在类上, 相当于类中的所有方法都是使用此数据源, 注意要写在实现类上
@Service
@SwitchDataSource(DSLabel.USER)
public class UserServiceImpl implements UserService {}

@Service
@SwitchDataSource(DSLabel.ORDER)
public class OrderServiceImpl implements OrderService {}
  • 标注在方法上, 标识此方法使用指定的数据源
    @SwitchDataSource(DSLabel.ORDER)
    public void deleteOrder(int id) {
        orderMapper.deleteByUserId(id);
    }

注意这种使用方式在开启事务的情况下, 如果在同一个事务下跨数据源会切换数据源失败, 原因未知... 后面再看

四、临时解决跨数据源事务问题

解决的思路就是调用跨数据源方法的时候新开一个事务, 也就是说调用方法时事务传播机制改为REQUIRES_NEW, 比如下面代码, 删除一个用户同时把用户的订单也删除, 在删除订单的方法上增加@Transactional(propagation = Propagation.REQUIRES_NEW)即可.
虽然解决切换数据源失败的问题, 但是还有一个事务回滚的问题, 比如说我先删除用户订单, 再删除用户, 删除用户订单成功后, 执行删除用户失败了, 这个时候删除用户订单将不会回滚, 目前只能将调用跨数据源的方法放到最后, 也就是说将删除用户订单放在删除用户后面, 后面再研究一下事务管理

    @Override
    public void delete(int id) {
        userMapper.delete(id);
        // 这里是为了能够触发AOP
        ((UserServiceImpl) AopContext.currentProxy()).deleteOrder(id);
    }

    @SwitchDataSource(DSLabel.ORDER)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deleteOrder(int id) {
        orderMapper.deleteByUserId(id);
    }

你可能感兴趣的:(Java Spring Boot注解切换多数据源以及事务问题)