公共工具类设计之《MongoDB 动态分表组件》

在实际的各种业务中,存储分表的需求是比较容易涉及到的,在MongoDB的场景中,如何设计一个动态表组件,满足下面的要求呢?

  1. 灵活支持各种的的分表策略,例如后缀,前缀模式,以及支持分组、哈希等各种分表规则。
  2. 兼容已有的业务,实现无感知的切换,无需修改任何业务侧的代码。

话不多说,直入主题。

整体方案

  1. 基于Properties 配置动态表参数,使用spel表达式实现自定义要求。
  2. 继承 MongoTemplate,实现各SQL方法,添加分表逻辑,业务层无需做任何改动。

分表配置

使用spring.properties进行配置,支持spel表达式。 按照示例的分表配置,订单表site_order,使用后缀策略,分表的规则使用id字段取模

  • id = 1的数据将存到 site_order_1表
  • id = 5的数据将存到site_order_2表

商品表使用后缀策略,按照租户字段,单独进行分组操作

  • tenantId = 2 的数据将存到 product_2表
  • tenantId = 8的数据将存到product_8表中
yaml 复制代码
spring:  
  data:  
    mongodb:  
      uri: mongodb://root:123456@localhost:27017/admin
      dynamic-collections:  
        - collection: site_order  
          strategy: SUFFIX  
          field: "#root.id % 3"  #基于id哈希分组进行分表
        - collection: product  
          strategy: SUFFIX  
          field: "tenantId"  #每个租户ID单独使用一个表

DynamicProperties 读取分表配置

java 复制代码
@Configuration  
@EnableConfigurationProperties  
@ConfigurationProperties(prefix = "spring.data.mongodb")  
public class DynamicMongoProperties {  

	private List<DynamicCollection> dynamicCollections = new ArrayList<>();  
	  
	private volatile Map<String, DynamicCollection> dynamicCollectionMap = new HashMap<>();
  
    public void setDynamicCollections(List<DynamicCollection> dynamicCollections) {  
        this.dynamicCollections = dynamicCollections;  
    }  


	public Map<String, DynamicCollection> getDynamicCollections() {  
	    if (dynamicCollections == null || dynamicCollections.isEmpty()) {  
	        return dynamicCollectionMap;  
	    }  
	    if (dynamicCollectionMap.isEmpty()) {  
	        synchronized (dynamicCollectionMap) {  
	            if (dynamicCollectionMap.isEmpty()) {  
	                dynamicCollectionMap = dynamicCollections.stream()  
	                        .collect(Collectors.toMap(DynamicCollection::getCollection, c -> c));  
	            }  
	        }  
	    }  
	    return dynamicCollectionMap;  
	}
  
    @Data  
    public static class DynamicCollection {  
        private String collection;  
        private Strategy strategy = Strategy.SUFFIX;  
        private String field;  
    }  
  
}

public enum Strategy {  
    SUFFIX  
}

DynamicMongoTemplate 实现 MongoTemplate

需要重写MongoTemplate里sql相关的操作,但是这里有一个限制,只能重写参数包含obj对象或者Query对象的,因为要获取动态表名必须要知道分表键的值,像findById这样的只传入了一个id,对于获取动态表名来说信息不够。 下面只给了insert 、findById 、 findOne的实现,其他的方法都可以参考这3个进行实现。

java 复制代码
  
public class DynamicMongoTemplate extends MongoTemplate {  
  
    private DynamicMongoProperties dynamicMongoProperties;  
  
    public DynamicMongoTemplate(MongoDatabaseFactory mongoDatabaseFactory,    
                                DynamicMongoProperties dynamicMongoProperties) {  
        super(mongoDatabaseFactory);  
        this.dynamicMongoProperties = dynamicMongoProperties;  
    }  
  
    @Override  
    public String getCollectionName(Class<?> entityClass) {  
        DynamicCollection dynamicCollection = this.getDynamicCollection(entityClass);  
        return super.getCollectionName(entityClass) + "_" + dynamicCollection.getField();  
    }  
  
    @Override  
    public <T> T findById(Object id, Class<T> entityClass) {  
        throw new RuntimeException("DynamicMongoTemplate unsupported findById");  
    }  
  
    @Override  
    public <T> T findOne(Query query, Class<T> entityClass) {  
        String dynamicCollectionName = this.getDynamicCollectionName(query, entityClass);  
        return super.findOne(query, entityClass, dynamicCollectionName);  
    }  
  
    @Override  
    public <T> T insert(T objectToSave) {  
        String dynamicCollectionName = this.getDynamicCollectionName(objectToSave);  
        return super.insert(objectToSave, dynamicCollectionName);  
    }  
  
    private <T> String getDynamicCollectionName(Query query, Class<T> entityClass) {  
        DynamicCollection dynamicCollection = getDynamicCollection(entityClass);  
        if (dynamicCollection == null) {  
            throw new RuntimeException("unknown entityClass " + entityClass.getName());  
        }  
        String collection = dynamicCollection.getCollection();  
        String field = dynamicCollection.getField();  
        String fieldValue = "";  
        try {  
            fieldValue = getFieldValue(query, field, entityClass);  
        } catch (InstantiationException | IllegalAccessException e) {  
            throw new RuntimeException("entityClass (" + entityClass.getName()  
                    + ") cant be instantiated");  
        }  
        return generateCollectionName(collection, fieldValue, dynamicCollection.getStrategy());  
    }  
  
    private <T> String getDynamicCollectionName(T obj) {  
        DynamicCollection dynamicCollection = getDynamicCollection(obj.getClass());  
        if (dynamicCollection == null) {  
            return null;  
        }  
        String collection = dynamicCollection.getCollection();  
        String field = dynamicCollection.getField();  
        String fieldValue = getFieldValue(obj, field);  
        return generateCollectionName(collection, fieldValue, dynamicCollection.getStrategy());  
    }  
  
    private <T> String getFieldValue(Query query, String field, Class<T> entityClass) throws InstantiationException, IllegalAccessException {  
        T t = entityClass.newInstance();  
  
        BeanWrapperImpl beanWrapper = new BeanWrapperImpl(t);  
        query.getQueryObject().entrySet().forEach(entry ->  
                beanWrapper.setPropertyValue(entry.getKey(), entry.getValue()));  
  
        return getFieldValue(t, field);  
    }  
  
    private String getFieldValue(Object obj, String field) {  
        ExpressionParser parser = new SpelExpressionParser();  
        Expression exp = parser.parseExpression(field);  
        EvaluationContext context = new StandardEvaluationContext(obj);  
        return exp.getValue(context, String.class);  
    }  
  
    private String generateCollectionName(String originCollectionName,  
                                          String fieldValue, Strategy strategy) {  
        if (!StringUtils.hasText(fieldValue)) {  
            throw new RuntimeException(  
                    "dynamicCollection field missing. collection = " + originCollectionName);  
        }  
        switch (strategy) {  
            case SUFFIX -> {  
                return originCollectionName + "_" + fieldValue;  
            }  
            default -> throw new RuntimeException("unknown dynamicCollection strategy " + strategy);  
        }  
    }  
  
    private DynamicCollection getDynamicCollection(Class<?> entityClass) {  
        return dynamicMongoProperties.getDynamicCollections()  
                .get(super.getCollectionName(entityClass));  
    }  
  
}

DynamicMongoConfig

最后一步要做的就是提供给业务侧使用动态表名逻辑了,本质就是使用DynamicMongoTemplate覆盖MongoTemplate,业务侧无感知的进行切换。这里可以使用Configuration类配置:

java 复制代码
@Configuration  
public class DynamicMongoConfig {  
  
    @Bean  
    public DynamicMongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory,  
                                              DynamicMongoProperties dynamicMongoProperties) {  
        return new DynamicMongoTemplate(mongoDatabaseFactory, dynamicMongoProperties);  
    }  
  
}

添加这3个类后,项目就已经具备动态表名的能力了。

相关推荐
阿伟*rui2 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj4 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck4 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei4 小时前
java的类加载机制的学习
java·学习
码农小旋风5 小时前
详解K8S--声明式API
后端
Peter_chq5 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml46 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~6 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616886 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7896 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot