MongoDB分表与分片选择的一次实践

背景:

最近公司在开发一款应用,由于应用的数据敏感,在假设客户端为安全的前提下,为避免由于有权限访问数据库的任何人及网络传输过程的泄密,用户的各业务类型数据均转成json然后由前端加密保存于后端,而后端返回给前端的数据也均为加密数据,前端通过不保存于系统的用户自定义密钥来进行加解密。

设计方案:

  • 后端的数据存储使用MongoDB,使用Spring Data进行数据访问。
  • 该内容的表设计大概就仅有_id、业务类型、密文、修改日期等字段。
  • 前端每次请求仅针对一个业务类型,因此每个Api均有业务类型的传入。
  • 为了在批量CUD时,能够在响应中标志具体每笔记录的操作结果,因此主键_id由前端生成。
  • 各业务类型数据均保存于一张表中,各业务类型数据对后端来说是一样的,都使用同一套REST Api,只有前端对数据解密后才有不一样的处理,同时该设计方案可以很灵活的增加业务类型,增加类型时仅需要在类型的枚举类中添加元素即可。
  • 后续可以考虑以[业务类型,id]为组合进行分片。

问题:

开发完成后,前端基于其业务规则,不同的业务类型会使用同一个id,这使得原来的设计方案是无法使用的,而同时老大又偏向于使用分表的方案,即各业务类型使用不同的Collection保存数据。

理由主要有下:

  • 用于分片路由的所使用的索引本身就占用很大的空间(分片必须增加以分片键为左前缀的索引)。
  • 各业务类型数据从业务角度来看是很独立的,不应该揉在一起。

查看代码,Repository层的数据库操作都是继承自MongoRepository<业务Domain>,如findOne(id),而非像MongoOperations.findOne(query,Class,collectionName)可以自行指定Collection,因此如果要改为分表,将会涉及很大的改动,按计划又得马上交差,于是各种找方法。

改造思路

  • 思路1:保持原来的所有Api不变,将前端设置的id当作一个业务id字段(biz_id),后端使用ObjectId()作为真实id,在返回给前端时,指定用业务id来填充id字段。该思路的缺点是相当明显的,不符合所见即所得,对于一个刚开始的项目就如此,会给后面维护增加难度。

以下两个是分表的思路。

  • 思路2:查看官方文档,有如下文字

Rich mapping support is provided by the MongoMappingConverter. MongoMappingConverter has a rich metadata model that provides a full feature set of functionality to map domain objects to MongoDB documents.

看似好像可以通过覆盖MongoMappingConverter来实现将对同一个实体的CRUD根据自定义路由规则至指定Collection,然看完该Reference都没发现有对该种应用场景的介绍,再注意文中描述map domain objects to MongoDB documents,断定仅有对Collumn的映射。

  • 思路3:刚好其它项目的同事有对同一个Domain在保存时决定实际保存于哪个Collection,使用SPEL,于是借鉴过来,具体如下:

    • 1、Domain使用注解@Document(collection=”#{@collectionNameProvider.getCollectionName()}”),在进行CRUD操作时,Spring Data底层会调用collectionNameProvider.getCollectionName()来取得Collection。

    • 2、在Repository层之上,如Service层中调用collectionNameProvider.setType()方法将类型值设置于上下文中。

    • 3、创建表名决策器

@Component("collectionNameProvider")
public class CollectionNameProvider {

    public static final String DEFAULT_COLLECTION_NAME = "default_collection";
    private static ThreadLocal typeThreadLocal = new ThreadLocal<>();

    public static void setType(String type) {
        typeThreadLocal.set(type);
    }

    public String getCollectionName() {
        String type = typeThreadLocal.get();
        if (StringUtils.isNotBlank(type)) {
            String collectionName = doMapper(type);
            return collectionName;
        } else {
            return DEFAULT_COLLECTION_NAME;
        }
    }

    private String doMapper(String type) {
        //TODO 这里自定义通过Type映射至Mapper的逻辑
        return null;
    }
}

通过上面的叙述,可以看出,该思路具有侵入性,Repository层的逻辑侵入至Service,但该思路相对较为成熟,同时可以灵活的增加业务类型,因此使用该思路进行改造。

注:这里要注意的是注入的依赖问题,在服务启动时,Spring Data MongoDB会扫描所有的@Document,会调用collectionNameProvider.getCollectionName(),而此时的collectionNameProvider还未实例化入ApplicationContext,会造成启服不成功,因此可以手动给MongoMappingContext注入ApplicationContext的Bean,这样子保证collectionNameProvider已经实例化后再去扫描@Document。

总结:

  • 对于这一次的设计,后续经过自己的思量,个人觉得应当使用分片来实现,而非分表(分表特指这里的思路3的方式,下同),分表是具有代码侵入性的,而目前无论是针对传统关系型数据库的DAL(Data Access Layer)亦或是其它具有P(CAP中的P)特性的NoSql,从设计哲学均有考虑减少数据存储层对上层(包括持久层)代码的侵入,数据存储尽量做到对上层透明,因此使用分表方式有点走老路的感觉。

  • MongoDB如果unique index是sharing key,则唯一索引失效,因此分片方案在这里是可行的,可以实现不同的业务类型使用相同的id。 而对于分表方案的两个理由做如下解释:

    • 用于分片路由的所使用的索引本身就占用很大的空间,原本为了查询效率,就需要增加[业务类型,id]的索引,但分片确实需要增加额外的路由表空间,但此空间不会太大,是在可容忍的范围内。

    • 各业务类型数据从业务角度来看是很独立的,不应该揉在一起,这个原因相对较为主观,从另外一个角度看,其实可以放在一起的。

  • 在分表方案中,当一种业务类型的数据增加到一定程度,同样需要进行分片,同时每增加一种业务类型,均需要增加一个表(程序可以自动创建),但其实每个表的结构均是一样,不一样的仅是表名,如果放在其它业务场景,过多的表将不利于管理。

你可能感兴趣的:(mongodb)