MongoDB数据源动态切换

项目需要根据业务来分库,比如任务A相关数据入库到MongoDB-A,任务B相关数据入库到MongoDB-B;

网上搜索了下大概方案都是依赖AOP来实现,代码也都千篇一律,BUG百出;于是修改了下,大概的思路如下:

  1. 切面放在了MongoTemplate
@Aspect
@Component
@Slf4j
public class MongodbSpecialSourceAspect {

    private final String POINT_CUT = "execution (* org.springframework.data.mongodb.core.MongoTemplate.*(..))";

    @Around(value = POINT_CUT)
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object o = joinPoint.getTarget();
        CustomMongoTemplate customMongoTemplate = null;
        // 判断是否是自定义的MongoTemplate
        if (o.getClass() == CustomMongoTemplate.class) {
            customMongoTemplate = (CustomMongoTemplate) o;
            // TODO 判断是否切换逻辑
            if(xxxx) {
                customMongoTemplate.switchMongoDbFactory(CustomMongoTemplate.EDU_DB_KEY);
            }
        }
        Object result = joinPoint.proceed();
        if (customMongoTemplate != null) {
            customMongoTemplate.restoreMaster();
        }
        return result;
    }
}
  1. 声明MongoTemplate的子类,重写getMongoDbFactory方法
@Slf4j
public class CustomMongoTemplate extends MongoTemplate {

    // 存放当前的数据源dbFactory
    private ThreadLocal<MongoDbFactory> mongoDbFactoryThreadLocal;

    // 存放初始化多数据源的dbFactory
    private final Map<Integer, MongoDbFactory> mongoDbFactoriesMap;

    public final static int MASTER_DB_KEY = 0;
    public final static int EDU_DB_KEY = 1;

    public CustomMongoTemplate(Map<Integer, MongoDbFactory> mongoDbFactoriesMap, MongoDbFactory mongoDbFactory){
        super(mongoDbFactory);
        if (!mongoDbFactoriesMap.containsKey(0)) {
            throw new BeanInitializationException("mongoDbFactoriesMap must contain an entry with key=0.");
        }
        this.mongoDbFactoriesMap = mongoDbFactoriesMap;
        if(mongoDbFactoryThreadLocal==null) {
            mongoDbFactoryThreadLocal = new ThreadLocal<>();
        }
    }

    public void switchMongoDbFactory(Integer mongoDbFactoryKey){
        if (!mongoDbFactoriesMap.containsKey(mongoDbFactoryKey)) {
            throw new IllegalArgumentException("The entry of the current key cannot be found in mongoDbFactoriesMap.");
        }
        mongoDbFactoryThreadLocal.set(mongoDbFactoriesMap.get(mongoDbFactoryKey));
    }

    public void restoreMaster() {
        mongoDbFactoryThreadLocal.set(mongoDbFactoriesMap.get(MASTER_DB_KEY));
    }

    ///////////////////////
    @Override
    public MongoDbFactory getMongoDbFactory() {
        if (mongoDbFactoryThreadLocal.get() == null) {
            restoreMaster();
        }
        return mongoDbFactoryThreadLocal.get();
    }

    @Override
    protected MongoDatabase doGetDatabase() {
        return MongoDatabaseUtils.getDatabase(getMongoDbFactory(), SessionSynchronization.ON_ACTUAL_TRANSACTION);
    }

    @Override
    public SessionScoped withSession(ClientSessionOptions options) {

        Assert.notNull(options, "ClientSessionOptions must not be null!");

        return withSession(() -> getMongoDbFactory().getSession(options));
    }

    ////////////////////

主要是重写getMongoDbFactory方法来实时改变dbFactory,因为可以查阅源码看到所有的dbFactory都是来源于此方法。当然也有例外,比如

doGetDatabasewithSession还有findDistinct方法,所以我们重写了doGetDatabasewithSession方法,由直接使用私有变量mongoDbFactory改为调用getMongoDbFactory方法来获取mongoDbFactory

findDistinct方法中使用了大量的私有变量,导致无法重写此方法;如果有用到,可以重新实现下此方法;

  1. 声明MongoTemplate的Bean实例
@Configuration
@ConfigurationProperties(prefix = "spring.data.mongodb.file")
public class FileMongoConfig {
    
    private String url;
    private String eduUrl;

    @Bean(name = "fileMongoTemplate")
    public MongoTemplate getMongoTemplate() {
        // 将需要切换的数据源都放到map中
        Map<Integer, MongoDbFactory> mongoDbFactoriesMap = new HashMap<>(1<<3);
        MongoDbFactory mongoDbFactory = this.fileMongoDbFactory();
        mongoDbFactoriesMap.put(CustomMongoTemplate.MASTER_DB_KEY, mongoDbFactory);
        mongoDbFactoriesMap.put(CustomMongoTemplate.EDU_DB_KEY, this.fileEduMongoDbFactory());
        // 这里返回我们自定义的MongoTemplate
        return new CustomMongoTemplate(mongoDbFactoriesMap, mongoDbFactory);
    }

    public MongoDbFactory fileMongoDbFactory() {
        return new SimpleMongoDbFactory(new MongoClientURI(getUrl()));
    }

    public MongoDbFactory fileEduMongoDbFactory() {
        return new SimpleMongoDbFactory(new MongoClientURI(getEduUrl()));
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
    
    public String getEduUrl() {
        return eduUrl;
    }

    public void setEduUrl(String eduUrl) {
        this.eduUrl = eduUrl;
    }
}
  1. 以上三步后,我们准备工作就完成了;此时进行一个mongo查询时,切面拦截,判断是否是自定义的MongoTemplate,如果是则进行逻辑判断是否切换数据源;

    在执行完mongo语句后,切换回主数据源;完成数据源的切换;

缺陷
  1. 此方案缺陷很明显,aop拿不到方法参数,如果场景依赖的是方法的传参来判断是否切换数据源,那么用aop就无法完成;此方案只适合整体切换的场景;

转换思路,如果我需要依赖collectionName来切换数据源或者部分入参来切换数据源,该怎么实现呢?

  • 首先aop肯定是拿不到参数的,就算使用threadlocal传输也很难实现
  • 那么我们直接重写MongoTemplate的方法,在所有方法前都进行逻辑判断是否切换,在方法执行后还原
  1. 删除aspect类
  2. 修改下CustomMongoTemplate,我们仔细筛选MongoTemplate的所有参数有collectionName的方法,大部分底层调用都是一样的。那么我们只需要重写这些底层的方法就可以;最终如下:
@Slf4j
public class CustomMongoTemplate extends MongoTemplate {

    private ThreadLocal<MongoDbFactory> mongoDbFactoryThreadLocal;

    // 用于分区map, 0: master-db   1: edu-db
    private final Map<Integer, MongoDbFactory> mongoDbFactoriesMap;

    public final static int MASTER_DB_KEY = 0;
    public final static int EDU_DB_KEY = 1;

    public CustomMongoTemplate(Map<Integer, MongoDbFactory> mongoDbFactoriesMap, MongoDbFactory mongoDbFactory){
        super(mongoDbFactory);
        if (!mongoDbFactoriesMap.containsKey(0)) {
            throw new BeanInitializationException("mongoDbFactoriesMap must contain an entry with key=0.");
        }
        this.mongoDbFactoriesMap = mongoDbFactoriesMap;
        if(mongoDbFactoryThreadLocal==null) {
            mongoDbFactoryThreadLocal = new ThreadLocal<>();
        }
    }

    public CustomMongoTemplate(Map<Integer, MongoDbFactory> mongoDbFactoriesMap, MongoDbFactory mongoDbFactory, @Nullable MongoConverter mongoConverter){
        super(mongoDbFactory, mongoConverter);
        if (!mongoDbFactoriesMap.containsKey(0)) {
            throw new BeanInitializationException("mongoDbFactoriesMap must contain an entry with key=0.");
        }
        this.mongoDbFactoriesMap = mongoDbFactoriesMap;
        if(mongoDbFactoryThreadLocal==null) {
            mongoDbFactoryThreadLocal = new ThreadLocal<>();
        }
    }

    public void switchMongoDbFactory(Integer mongoDbFactoryKey){
        if (!mongoDbFactoriesMap.containsKey(mongoDbFactoryKey)) {
            throw new IllegalArgumentException("The entry of the current key cannot be found in mongoDbFactoriesMap.");
        }
        mongoDbFactoryThreadLocal.set(mongoDbFactoriesMap.get(mongoDbFactoryKey));
    }

    public void restoreMaster() {
        mongoDbFactoryThreadLocal.set(mongoDbFactoriesMap.get(MASTER_DB_KEY));
    }

    public void checkAndSwitchDbFactory(String collectionName) {
        // TODO 判断业务相关逻辑,切换到相关的DbFactory
        this.switchMongoDbFactory(EDU_DB_KEY);
    }

    ///////////////////////
    @Override
    public MongoDbFactory getMongoDbFactory() {
        if (mongoDbFactoryThreadLocal.get() == null) {
            restoreMaster();
        }
        return mongoDbFactoryThreadLocal.get();
    }

    @Override
    protected MongoDatabase doGetDatabase() {
        return MongoDatabaseUtils.getDatabase(getMongoDbFactory(), SessionSynchronization.ON_ACTUAL_TRANSACTION);
    }

    @Override
    public SessionScoped withSession(ClientSessionOptions options) {

        Assert.notNull(options, "ClientSessionOptions must not be null!");

        return withSession(() -> getMongoDbFactory().getSession(options));
    }

    ////////////////////
    @Override
    public <S, T> List<T> doFind(String collectionName, Document query, Document fields, Class<S> sourceClass, Class<T> targetClass, CursorPreparer preparer) {
        checkAndSwitchDbFactory(collectionName);
        List<T> ts = super.doFind(collectionName, query, fields, sourceClass, targetClass, preparer);
        restoreMaster();
        return ts;
    }

    @Override
    protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler documentCallbackHandler, CursorPreparer preparer) {
        checkAndSwitchDbFactory(collectionName);
        super.executeQuery(query, collectionName, documentCallbackHandler, preparer);
        restoreMaster();
    }

    @Override
    public <T> T execute(String collectionName, CollectionCallback<T> callback) {
        checkAndSwitchDbFactory(collectionName);
        T x = super.execute(collectionName, callback);
        restoreMaster();
        return x;
    }

    @Override
    public MongoCollection<Document> getCollection(String collectionName) {
        checkAndSwitchDbFactory(collectionName);
        MongoCollection<Document> x = super.getCollection(collectionName);
        restoreMaster();
        return x;
    }

    @Override
    public boolean collectionExists(String collectionName) {
        checkAndSwitchDbFactory(collectionName);
        boolean x = super.collectionExists(collectionName);
        restoreMaster();
        return x;
    }

    @Override
    public <T> List<T> findDistinct(Query query, String field, String collectionName, Class<?> entityClass, Class<T> resultClass) {
        checkAndSwitchDbFactory(collectionName);
        List<T> x = super.findDistinct(query, field, collectionName, entityClass, resultClass);
        restoreMaster();
        return x;
    }

    @Override
    public <T> GeoResults<T> geoNear(NearQuery near, Class<?> domainType, String collectionName, Class<T> returnType) {
        checkAndSwitchDbFactory(collectionName);
        GeoResults<T> x = super.geoNear(near, domainType, collectionName, returnType);
        restoreMaster();
        return x;
    }

    @Override
    protected <T> T doInsert(String collectionName, T objectToSave, MongoWriter<T> writer) {
        checkAndSwitchDbFactory(collectionName);
        T x = super.doInsert(collectionName, objectToSave, writer);
        restoreMaster();
        return x;
    }

    @Override
    protected <T> Collection<T> doInsertBatch(String collectionName, Collection<? extends T> batchToSave, MongoWriter<T> writer) {
        checkAndSwitchDbFactory(collectionName);
        Collection<T> x = super.doInsertBatch(collectionName, batchToSave, writer);
        restoreMaster();
        return x;
    }

    @Override
    protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
        checkAndSwitchDbFactory(collectionName);
        T x = super.doSave(collectionName, objectToSave, writer);
        restoreMaster();
        return x;
    }

    @Override
    public <T> List<T> findAll(Class<T> entityClass, String collectionName) {
        checkAndSwitchDbFactory(collectionName);
        List<T> x = super.findAll(entityClass, collectionName);
        restoreMaster();
        return x;
    }

    @Override
    public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, String mapFunction, String reduceFunction, MapReduceOptions mapReduceOptions, Class<T> entityClass) {
        checkAndSwitchDbFactory(inputCollectionName);
        MapReduceResults<T> x = super.mapReduce(query, inputCollectionName, mapFunction, reduceFunction, mapReduceOptions, entityClass);
        restoreMaster();
        return x;
    }

    @Override
    public <T> List<T> mapReduce(Query query, Class<?> domainType, String inputCollectionName, String mapFunction, String reduceFunction, MapReduceOptions mapReduceOptions, Class<T> resultType) {
        checkAndSwitchDbFactory(inputCollectionName);
        List<T> x = super.mapReduce(query, domainType, inputCollectionName, mapFunction, reduceFunction, mapReduceOptions, resultType);
        restoreMaster();
        return x;
    }

    @Override
    public <T> GroupByResults<T> group(Criteria criteria, String inputCollectionName, GroupBy groupBy, Class<T> entityClass) {
        checkAndSwitchDbFactory(inputCollectionName);
        GroupByResults<T> x = super.group(criteria, inputCollectionName, groupBy, entityClass);
        restoreMaster();
        return x;
    }

    @Override
    protected <O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<O> outputType, AggregationOperationContext context) {
        checkAndSwitchDbFactory(collectionName);
        AggregationResults<O> x = super.doAggregate(aggregation, collectionName, outputType, context);
        restoreMaster();
        return x;
    }

    @Override
    protected MongoCollection<Document> doCreateCollection(String collectionName, Document collectionOptions) {
        checkAndSwitchDbFactory(collectionName);
        MongoCollection<Document> x = super.doCreateCollection(collectionName, collectionOptions);
        restoreMaster();
        return x;
    }

    @Override
    protected <T> T doFindOne(String collectionName, Document query, Document fields, Class<T> entityClass) {
        checkAndSwitchDbFactory(collectionName);
        T x = super.doFindOne(collectionName, query, fields, entityClass);
        restoreMaster();
        return x;
    }

    @Override
    protected <S, T> List<T> doFind(String collectionName, Document query, Document fields, Class<S> entityClass, CursorPreparer preparer, DocumentCallback<T> objectCallback) {
        checkAndSwitchDbFactory(collectionName);
        List<T> x = super.doFind(collectionName, query, fields, entityClass, preparer, objectCallback);
        restoreMaster();
        return x;
    }

    @Override
    protected <T> T doFindAndRemove(String collectionName, Document query, Document fields, Document sort, Collation collation, Class<T> entityClass) {
        checkAndSwitchDbFactory(collectionName);
        T x = super.doFindAndRemove(collectionName, query, fields, sort, collation, entityClass);
        restoreMaster();
        return x;
    }

    @Override
    protected <T> T doFindAndModify(String collectionName, Document query, Document fields, Document sort, Class<T> entityClass, Update update, FindAndModifyOptions options) {
        checkAndSwitchDbFactory(collectionName);
        T x = super.doFindAndModify(collectionName, query, fields, sort, entityClass, update, options);
        restoreMaster();
        return x;
    }

    @Override
    protected <T> T doFindAndReplace(String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class<?> entityType, Document replacement, FindAndReplaceOptions options, Class<T> resultType) {
        checkAndSwitchDbFactory(collectionName);
        T x = super.doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, collation, entityType, replacement, options, resultType);
        restoreMaster();
        return x;
    }
}

收工;

posted @ 2021-08-09 16:34  faylinn  阅读(1023)  评论(0编辑  收藏  举报
、、、