表格存储

一、项目介绍

column-store-orm是一个列式存储orm框架。通过注解的方式实现了对象—实体映射,屏蔽了底层存储系统的差异。目前支持tablestore,hbase,mongo,dynamo四种存储系统。

二、使用场景

当项目需要进行多云部署或者私有化部署时,如果相应的环境使用的存储系统跟当前系统有差异,则不可避免的需要进行代码层面的修改。如果使用了云中立SDK,则只需要修改配置文件即可支持多环境的部署。

三、功能列表

功能项 方法签名 TableStore HBase Mongo
根据主键获取单条记录 T get(Object... primaryKeyColumns) 支持 支持 支持
根据主键获取单条记录 T get(PrimaryKey primaryKey) 支持 支持 支持
根据主键获取单条记录的指定列 T get(PrimaryKey primaryKey, Set columnsToGet) 支持 支持 支持
写入一条记录(如果记录不存在则插入;如果记录存在则覆盖所有列) void put(T t) 支持 支持 支持
更新一条记录(如果记录不存在则插入;如果记录存在,则根据请求的内容新增、修改或者删除指定列的值) void update(T t) 支持 支持 支持
更新一条记录,并根据条件删除动态列 void update(T t, boolean deleteDynamicColumns) 支持 支持 支持
根据主键删除单条记录 void delete(Object... primaryKeyColumns) 支持 支持 支持
根据主键删除单条记录 void delete(PrimaryKey primaryKey) 支持 支持 支持
根据主键批量获取记录 List batchGet(List primaryKeys) 支持 支持 支持
根据主键批量获取记录的指定列 List batchGet(List primaryKeys, Set columnsToGet) 支持 支持 支持
批量写入记录 void batchPut(List ts) 支持 支持 支持
批量更新记录 void batchUpdate(List ts) 支持 支持 支持
批量更新记录,并根据条件删除动态列 void batchUpdate(List ts, boolean deleteDynamicColumns) 支持 支持 支持
根据主键批量删除记录 void batchDelete(List primaryKeys) 支持 支持 支持
根据主键进行范围查找 List rangeGet(RangePrimaryKey rangePrimaryKey) 支持 支持 支持
根据主键进行返回查找,返回指定列 List rangeGet(RangePrimaryKey rangePrimaryKey, Set columnsToGet) 支持 支持 支持
根据主键进行返回查找,返回指定列,同时限制返回记录数 List rangeGet(RangePrimaryKey rangePrimaryKey, Set columnsToGet, int count) 支持 支持 支持
根据主键进行范围查找,并对返回的每条记录执行consumer 方法 void rangeConsume(RangePrimaryKey rangePrimaryKey, Consumer consumer) 支持 支持 支持
根据指定列获取记录 List columnGet(SearchColumn searchColumn) 支持 支持 支持
根据指定列获取记录,并进行分页,排序 List columnGet(SearchColumn searchColumn, int offset, int count, Boolean asc) 支持 支持 支持

四、快速接入

1.版本说明

由于column-store-orm-mongo使用到了spring-data-mongodb,因此与spring存在版本兼容性问题。


column-store-orm-mongo版本 spring-data-mongodb spring版本 spring-boot-dependencies版本
1.0.7.SB1_5 1.10.6.RELEASE 4.3.10.RELEASE 1.5.6.RELEASE
1.0.7.SB2_3 3.0.1.RELEASE 5.2.7.RELEASE 2.3.1.RELEASE

2.maven settings.xml添加仓库地址

<!-- 云中立虚拟仓库,关联了云中立实仓 -->
<repository>
    <id>maven-neutralcloud</id>
    <name>maven-neutralcloud</name>
    <url>http://packages.glodon.com/artifactory/maven-neutralcloud-public/</url>
</repository>

3.项目中添加pom依赖

<dependencies>
    <dependency>
        <groupId>com.glodon.paas.foundation</groupId>
        <artifactId>column-store-orm-mongo</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.glodon.paas.foundation</groupId>
            <artifactId>column-store-orm</artifactId>
            <!-- 根据实际情况修改版本号 -->
            <version>${version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

4.初始化

初始化仅需创建一个TableStoreDao对象,传入tableStore的config和一个实体类。

TableStoreMongoConfig config = TableStoreMongoConfig.builder()
                .hosts("localhost:27017")
                .database("mongo-test")
                .build();
TableStoreDAO<BookEntity> dao = new TableStoreDAOMongoImpl<>(config, BookEntity.class);

实体类到TableStore的PrimaryKeyName和ColumnName的自动映射是通过注解实现的,BookEntity的代码如下:

@Data
@TSTable(name = "ut_book")
public class BookEntity {

    @TSPKColumn
    private String floor = "";

    @TSAttrColumn
    private int ibsn;

    @TSAttrColumn
    private String author = "";

    @TSAttrColumn(dynamicColumnNames = true)
    private List<String> readerIds = new ArrayList<>(10);

    public BookEntity() {
    }

    public BookEntity(String floor, int ibsn, String author) {
        this.floor = floor;
        this.ibsn = ibsn;
        this.author = author;
    }

}

主要用到了三个注解,TSTable和TSPKColumn、TSAttrColumn。

TSTable用于实体类到TableStore中表的映射:

  • name,指定表名;

TSPKColumn用于实体类中属性到TableStore中PrimaryKeyName的映射:

  • name,主键名
  • order,当有多个属性做主键时,安装order顺序拼接

TSAttrColumn用于实体类中属性到TableStore中PrimaryKeyName的映射:

  • name,PrimaryKeyName或者ColumnName,当dynamicColumnNames为true时,指ColumnName的前缀;
  • dynamicColumnNames,是否为动态属性列,默认false;表格存储中列数与列名是不固定的,把此选项设为true来定义变化的列名,暂时不支持列值的定义;

5.单条记录操作

单条记录有put、get、update、delete四种操作;

get和delete有两种调用方式,第一种为按照顺序依次传入primaryKeyValue,第二种先用buildPrimaryKey方法构造出PrimaryKey,再传入get、delete方法;

// put
BookEntity entity = new BookEntity("f1", 12345, "Jon");
entity.setReaderIds(new ArrayList<>(Arrays.asList("100", "101")));
dao.put(entity);

// get
PrimaryKey primaryKey = PrimaryKey.builder()
        .addColumn("floor", "f1")
        .build();
BookEntity bookEntityGet = dao.get(primaryKey);
Assert.assertEquals("Jon", bookEntityGet.getAuthor());
Assert.assertEquals(2, bookEntityGet.getReaderIds().size());

// update
entity.setAuthor("Json_updated");
entity.getReaderIds().add("102");
dao.update(entity);
BookEntity bookEntityUpdate = dao.get(primaryKey);
Assert.assertEquals("Json_updated", bookEntityUpdate.getAuthor());
Assert.assertEquals(3, bookEntityUpdate.getReaderIds().size());

// delete
dao.delete(primaryKey);
BookEntity bookEntityDelete = dao.get(primaryKey);
Assert.assertNull(bookEntityDelete);

6.batch操作

batch有put、get、update、delete四种操作;

List<Integer> ids = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// putRow
dao.batchPut(ids.stream()
        .map(id -> {
            BookEntity bookEntity = new BookEntity("f" + id + "_batch", id, "author" + id + "_batch");
            for (int i = 1; i <= id; i++) {
                bookEntity.getReaderIds().add(String.valueOf(10000 + i));
            }
            return bookEntity;
        })
        .collect(Collectors.toList()));

// get
List<PrimaryKey> pks = ids.stream()
        .map(id -> PrimaryKey.builder()
                .addColumn("floor", "f" + id + "_batch")
                .build())
        .collect(Collectors.toList());
dao.batchGet(pks).forEach(bookEntity -> {
    String floor = bookEntity.getFloor();
    int start = "f".length();
    int end = floor.indexOf("_batch");
    int index = Integer.valueOf(floor.substring(start, end));
    Assert.assertEquals("author" + index + "_batch", bookEntity.getAuthor());
    Assert.assertEquals(index, bookEntity.getReaderIds().size());
});

// update
dao.batchUpdate(ids.stream()
        .map(id -> {
            BookEntity bookEntity = new BookEntity("f" + id + "_batch", id + 9000, "author" + id + "_batch_updated");
            bookEntity.getReaderIds().add("999");
            return bookEntity;
        })
        .collect(Collectors.toList()));
dao.batchGet(pks).forEach(bookEntity -> {
    String floor = bookEntity.getFloor();
    int start = "f".length();
    int end = floor.indexOf("_batch");
    int index = Integer.valueOf(floor.substring(start, end));
    Assert.assertEquals("author" + index + "_batch_updated", bookEntity.getAuthor());
    Assert.assertEquals(index + 1, bookEntity.getReaderIds().size());
});

// update delete dynamic columnValues
dao.batchUpdate(ids.stream()
        .map(id -> {
            BookEntity bookEntity = new BookEntity("f" + id + "_batch", id + 8000, "author" + id + "_batch_updated_2");
            bookEntity.getReaderIds().add("999");
            return bookEntity;
        })
        .collect(Collectors.toList()), true);
dao.batchGet(pks).forEach(bookEntity -> {
    String floor = bookEntity.getFloor();
    int start = "f".length();
    int end = floor.indexOf("_batch");
    int index = Integer.valueOf(floor.substring(start, end));
    Assert.assertEquals("author" + index + "_batch_updated_2", bookEntity.getAuthor());
    Assert.assertEquals(index, bookEntity.getReaderIds().size());
});

// delete
dao.batchDelete(pks);
List<BookEntity> ret = dao.batchGet(pks);
Assert.assertTrue(ret == null || ret.isEmpty());

7.range get操作

range操作有range get和range consume类接口,前者返回结果,后者直接指定对结果的操作;
RangePrimaryKey有四种指定方式:

  • Fixed,固定的值,一般位于最开始的主键列;
  • FullRange,全部范围,一般位于最后的主键列;
  • SubRangeString,String类型的指定范围的主键列,一般位于Fixed和FullRange之间;
// put first
List<Integer> ids = new ArrayList<>(100);
for (int i = 1; i <= 100; i++) {
    ids.add(i);
}

// putRow first
dao.batchPut(ids.stream()
        .map(id -> {
            BookEntity bookEntity = new BookEntity("f" + id + "_range", id, "author" + id + "_range");
            for (int i = 1; i <= id; i++) {
                bookEntity.getReaderIds().add(String.valueOf(20000 + i));
            }
            return bookEntity;
        })
        .collect(Collectors.toList()));

RangePrimaryKey rangePrimaryKey = RangePrimaryKey.builder()
        .addSubRangeStringValue("floor", "f20", "f50")
        .build();
Assert.assertEquals(dao.rangeGet(rangePrimaryKey).size(), 33);

dao.rangeConsume(RangePrimaryKey.builder().addFullRangePrimaryKeyColumn("floor").build(), bookEntity -> {
    System.out.println(bookEntity.getAuthor());
    dao.delete(PrimaryKey.builder().addColumn("floor", bookEntity.getFloor()).build());
});

8.column get操作

column get为按照一个或多个列值进行匹配,操作有两个接口 columnGet(SearchColumn searchColumn)
columnGet(SearchColumn searchColumn, int offset, int count, Boolean asc),前者默认升序列出满足条件的前十条,后者可以自定义。

// 查询ibsn为12345,author为Jon的数据
List<BookEntity> bookEntities = dao.columnGet(SearchColumn.builder()
        .addColumn("ibsn", "12345")
        .addColumn("author", "Jon").build());
List<BookEntity> bookEntities = dao.columnGet(SearchColumn.builder()
        .addColumn("ibsn", "12345")
        .addColumn("author", "Jon").build(), 0, 10, true);
  • HBase与MongoDB可以直接使用
  • 阿里云TableStore需要额外在类要查询的列上声明@TSAttrColumn注解,每次启动都会重新建立索引,建立多元索引需要30秒左右时间。
@Data
@TSTable(name = "ut_book")
public class BookEntity {

    @TSPKColumn
    private String floor = "";

    @TSAttrColumn
    @TSIndexColumn
    private int ibsn;

    @TSAttrColumn
    @TSIndexColumn
    private String author = "";

    @TSAttrColumn(dynamicColumnNames = true)
    private List<String> readerIds = new ArrayList<>(10);
}

五、架构设计

tmp表格存储架构设计.png

表格存储整体架构并不复杂:

  • 框架层

    • Annotation:框架提供的注解。

      • @TSTable:实体类到表的映射。

        • name:表名。
      • @TSIndexColumn:实体类属性到索引列的映射。

        • name:索引列名。
      • @TSPKColumn:实体类属性到主键的映射。

        • name:主键名。
        • order:当有多个属性做主键时,按照order顺序拼接。
      • @TSAttrColumn:实体类属性到列的映射。

        • name:属性名。
        • dynamicColumnNames:是否为动态属性列,默认 false;表格存储中列数与列名是不固定的,把此选项设为 true 来定义变化的列名,暂时不支持列值的定义。
    • Model:框架中定义的模型。

      • TableMeta:表的元数据,每个实体对应一个。注解处理类会读取实体类的相关注解,将元数据保存到TableMeta实例。
      private String tableName; // 表名
      private List<PKColumnMeta> pkColumnMeta; // 主键
      private List<AttrColumnMeta> attrColumnMeta; // 属性
      private List<IndexMeta> indexMeta; //索引
      
      • PrimaryKey:主键。
      • RangePrimaryKey:范围主键,在接入文档中有详细描述。
      • Column:列。
      • SearchColumn:当进行搜索时,用来构造相关的搜索列。
      • Row:用来表示一条记录,其中包含一个PrimaryKey和多个Column
    • TableStoreDAO:这是一个接口,定义了表格存储支持的功能。

      • 单行操作(基于主键):单条记录的CRUD操作。
      • 批量操作(基于主键):批量的CRUD操作。
      • 搜索(基于列):根据指定列进行搜索。
    • AbstractTableStoreDAO:这是一个抽象类,它有两个职责:

      • 继承了TableStoreDAO,并定义了相关的模板方法,为具体实现类提供了一个基类。

      • 作为注解处理类,实现了对实体类注解的解析。

  • 实现层

    实现层以框架层为基础,提供了对具体存储组件的实现。

    • TableStore
      • TableStoreAliyunConfig:表格存储的配置类。
      • TableStoreDAOAliyunImpl:基于AbstractTableStoreDAO的模板方法,提供了对阿里云表格存储的实现。
    • HBase
      • TableStoreHBaseConfig:HBase的配置类。
      • TableStoreDAOHBaseImpl:基于AbstractTableStoreDAO的模板方法,提供了对HBase的实现。
    • Mongo
      • TableStoreMongoConfig:Mongo的配置类。
      • TableStoreDAOMongoImpl:基于AbstractTableStoreDAO的模板方法,提供了对Mongo的实现。
  • 应用层

    应用层以实现层为基础,基于springboot提供了自动装配功能。

    • AliyunAutoConfiguration:阿里云表格存储的自动配置。
    • HBaseAutoConfiguration:HBase的自动配置。
    • MongoDBAutoConfiguration:Mongo的自动配置。