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}).
     * <p>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<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    /**
     * Specify the default target DataSource, if any.
     * <p>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}).
     * <p>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.
     * <p>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<DSLabel> 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<DataSourceProperty> 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<Object, Object> 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);
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,363评论 6 343
  • 蝴蝶飞 我在梦里追 泪在飞 如细雨轻雷 风儿吹 不憔悴,不可摧 花儿成堆 愿有它相陪 有浪来回 不必问,为谁 不知...
    文山鹿阅读 154评论 51 18
  • 有人说大脑就像是操作系统,可以通过学习去进行升级,当我们的认知越来越高级时,工作能力自然就水涨船高了。 最近在看《...
    良知良智阅读 381评论 0 0
  • 经常听周围人说,最讨厌朋友圈里做微商推销产品的,打开一看全是广告占屏,特别影响心情。可我想说,请尊重并理解他们,净...
    一笑而过2023阅读 298评论 0 0