基于Tablestore的海量保险单查询平台

本文涉及的产品
对象存储 OSS,20GB 3个月
日志服务 SLS,月写入数据量 50GB 1个月
对象存储 OSS,内容安全 1000次 1年
简介: 背景随着人们风险意识的提高与普及,越来越多的人愿意为自己与家人投一份保险,保险行业的飞速发展也带来了许多问题:海量的保险单该如何存储?如何高效地对保险单进行检索?传统的解决方案一般使用MySQL等关系型数据库对数据进行持久化与检索,但是随着数据量的上涨如何进行水平扩展变成了一个问题。

背景

随着人们风险意识的提高与普及,越来越多的人愿意为自己与家人投一份保险,保险行业的飞速发展也带来了许多问题:海量的保险单该如何存储?如何高效地对保险单进行检索?传统的解决方案一般使用MySQL等关系型数据库对数据进行持久化与检索,但是随着数据量的上涨如何进行水平扩展变成了一个问题。近年来使用NoSQL这种分布式架构的存储引擎来存储海量数据越来越流行,Tablestore是阿里云自研的分布式NoSQL服务,有高并发低延迟、易于水平扩展等特点,可以很好地解决保险行业保险单存储的规模以及各种检索等需求。

需求分析

某公司推出一款寿险产品,每一位投保人投保后会在后台系统中产生一条订单数据,同时投保人可以通过平台查看自己的保单;与此同时,保险经纪人也可以通过平台多保单进行检索、追踪以及轻量级分析。具体需求整理如下:

  • 保险用户

    • 在线投保,生成保单
    • 根据保单生效日期、失效日期检索保单
    • 查看自己的保单详情
  • 保险经纪人

    • 根据投保人查询保单
    • 根据保单生效日期、失效日期检索保单
    • 查询某个用户快要失效的保单
    • 根据受益人的受益百分比查询保单
    • 根据保费对保单以及用户进行排序

技术选型

MySQL

作为一款开源的开源关系型数据库,MySQL有高性能、低成本以及可靠性好等特点,用户可以通过JDBC等工具使用SQL语句对数据库进行增删改查。但是MySQL也有他的不足:

  • 随着数据量的膨胀无法水平扩展
  • 对保险单的多维查询需求支持不佳
  • 在大数量的情况下,写入性能较差

对于这些问题,第一条可以通过一些例如分库分表等手段解决,但是用户维护成本较高;第二条的话由于MySQL的索引引擎的实现,MySQL并不适合对数据进行多维检索分析,一般业界也会将数据导入到Elasticsearch、Solr等搜索引擎中对数据进行检索分析,但是这样同样提高了运维的复杂度。

Tablestore

Tablestore是阿里云基于谷歌Bigtable论文研发的一款分布式数据库产品,可以提供超大规模的存储容量,天然的分布式架构也提供了易于横向扩展的特性,理论上可以存储的数据量是不受限制的。在存储引擎上与MySQL相比Tablestore使用的是LSM-Tree,此种数据结构有天然的写入性能优势,特别适合存储保险单这类写多读少的数据。同时用户将数据托管在Tablestore上无需做任何的运维管理,大大减低了开发运维成本。

在数据检索方面,2019年年初Tablestore推出了多元索引(SearchIndex)功能,支持多维查询、GEO查询、分词查询等功能,完全满足保险单数据查询与轻量级分析的需求。多元索引与MySQL索引不同的是,用户只需要在需要索引的列上打开索引,既可以与其他列进行各种组合查询,而无需创建类似MySQL的联合索引。

所以基于Tablestore,我们给出如下的系统架构:
image

表结构设计

我们这边以普通的寿险保单为例,一张保单有如下的几类数据

  1. 保单ID
  2. 产品名称
  3. 用户信息(投保人用户ID、经纪人用户ID)
  4. 投保人信息、被保人信息与收益人信息

同时我们是支持了多受益人,这边我们选用了SearchIndex的NESTED数据类型来满足相关需求。具体Tablestore表与SearchIndex索引设计如下:

列类型 字段名 数据类型 索引数据类型 说明
主键列 policy_id_md5 String 不索引 保单ID的MD5,保证数据打散
属性列 policy_id String KEYWORD 保单ID,全局唯一
product_name String KEYWORD 产品名称
operate_time Long LONG 投保日期,UNIX时间戳,毫秒
effective_time Long LONG 生效日期
expiration_time Long LONG 失效日期
applier_user_id String KEYWORD 投保人用户ID
broker_user_id String KEYWORD 经纪人用户ID
applier_name String KEYWORD 投保人姓名
applier_id String KEYWORD 投保人证件号
applier_gender String KEYWORD 投保人性别
insured_name String KEYWORD 被保人姓名
insured_id String KEYWORD 被保人证件号
insured_gender String KEYWORD 被保人性别
beneficiary_info String NESTED 多受益人信息
premium Long LONG 保费
profit Long LONG 保额

受益人信息在SearchIndex中为NESTED类型,在写入Tablestore表是需要写入一个JSON数组字符串,例如

[
  {
    "name": "Tom",
    "id": "110101199909221111",
    "gender": "male",
    "benifit_percentage": 30
  },
  {
    "name": "Lucy",
    "id": "110102199201031411",
    "gender": "female",
    "benifit_percentage": 70
  }
]

具体的字段类型如下

字段 索引数据类型 说明
name KEYWORD 受益人姓名
id KEYWORD 受益人证件号
gender KEYWORD 受益人性别
benefit_percentage LONG 受益百分比

代码示例

代码可以在github上找到:https://github.com/aliyun/tablestore-examples/tree/master/demos/insurance-policy-management

SDK引入

引入Tablestore SDK 4.11.0

<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>tablestore</artifactId>
    <version>4.11.0</version>
</dependency>

创建Tablestore表与多元索引

创建表

// Create table
TableMeta tableMeta = new TableMeta(tableName);
// Only one primary key column: policy_id_md5 with type String
tableMeta.addPrimaryKeyColumn(POLICY_ID_MD5, PrimaryKeyType.STRING);
// Set TTL to -1, never expire; Set maxVersions 1, only have one version per column
TableOptions tableOptions = new TableOptions(-1, 1);
CreateTableRequest createTableRequest = new CreateTableRequest(tableMeta, tableOptions);
syncClient.createTable(createTableRequest);

创建索引

CreateSearchIndexRequest createRequest = new CreateSearchIndexRequest(tableName, indexName);
IndexSchema indexSchema = new IndexSchema();
indexSchema.addFieldSchema(new FieldSchema(POLICY_ID, FieldType.KEYWORD));
// Other filed ommited

// Create nested Field
List<FieldSchema> beneficiaryInfoSchema = new ArrayList<>();
beneficiaryInfoSchema.add(new FieldSchema(NAME, FieldType.KEYWORD));
beneficiaryInfoSchema.add(new FieldSchema(ID, FieldType.KEYWORD));
beneficiaryInfoSchema.add(new FieldSchema(GENDER, FieldType.KEYWORD));
beneficiaryInfoSchema.add(new FieldSchema(BENEFIT_PERCENTAGE, FieldType.LONG));

indexSchema.addFieldSchema(new FieldSchema(BENEFICIARY_INFO, FieldType.NESTED).setSubFieldSchemas(beneficiaryInfoSchema));

createRequest.setIndexSchema(indexSchema);

syncClient.createSearchIndex(createRequest);

数据写入

本示例仅展示插入一条数据的样例,批量插入可以看demo中的ImportExampleData

RowPutChange rowPutChange = new RowPutChange(tableName);
// primary key
PrimaryKey primaryKey = new PrimaryKey(new PrimaryKeyColumn[]{
  new PrimaryKeyColumn(POLICY_ID_MD5, PrimaryKeyValue.fromString(DigestUtils.md5Hex("POLICY_000000001837128")))
  });
rowPutChange.setPrimaryKey(primaryKey);
// value columns
rowPutChange.addColumn(POLICY_ID, ColumnValue.fromString("POLICY_000000001837128"))
  // Other columns are omitted
  .addColumn(PROFIT, ColumnValue.fromLong(10_000L));
// nested field, must fill with a JSON array, you can use jackson or gson to generate JSON array
rowPutChange.addColumn(BENEFICIARY_INFO, ColumnValue.fromString("[\n" +
                                                                "  {\n" +
                                                                "    \"name\": \"Kelly Evans\",\n" +
                                                                "    \"id\": \"285278192706139313\",\n" +
                                                                "    \"gender\": \"male\",\n" +
                                                                "    \"benefitPercentage\": 14\n" +
                                                                "  },\n" +
                                                                "  {\n" +
                                                                "    \"name\": \"Ida Clark\",\n" +
                                                                "    \"id\": \"418688200511062045\",\n" +
                                                                "    \"gender\": \"male\",\n" +
                                                                "    \"benefitPercentage\": 27\n" +
                                                                "  },\n" +
                                                                "  {\n" +
                                                                "    \"name\": \"Corey King\",\n" +
                                                                "    \"id\": \"909243194601171631\",\n" +
                                                                "    \"gender\": \"female\",\n" +
                                                                "    \"benefitPercentage\": 18\n" +
                                                                "  },\n" +
                                                                "  {\n" +
                                                                "    \"name\": \"Susan Evans\",\n" +
                                                                "    \"id\": \"288912191305043117\",\n" +
                                                                "    \"gender\": \"female\",\n" +
                                                                "    \"benefitPercentage\": 41\n" +
                                                                "  }\n" +
                                                                "]"));
PutRowRequest putRowRequest = new PutRowRequest(rowPutChange);
syncClient.putRow(putRowRequest);

数据查询

通过申请人姓名和保单过期时间搜索

本需求是一个AND查询,在多元索引中,我们只需要将两个Query放到BoolQuery的mustQueries中即可:

SearchQuery searchQuery = new SearchQuery();
searchQuery.setGetTotalCount(true);

BoolQuery boolQuery = new BoolQuery();

TermQuery applierNameQuery = new TermQuery();
applierNameQuery.setFieldName(APPLIER_NAME);
applierNameQuery.setTerm(ColumnValue.fromString("Vernon Richardson"));

RangeQuery expirationTimeQuery = new RangeQuery();
expirationTimeQuery.setFieldName(EXPIRATION_TIME);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date;
try {
  date = simpleDateFormat.parse("2019-03-04");
} catch (ParseException e) {
  throw new RuntimeException(e);
}
expirationTimeQuery.setTo(ColumnValue.fromLong(date.getTime()), true);

// use BoolQuery to combine other queries
boolQuery.setMustQueries(Arrays.asList(
  applierNameQuery,
  expirationTimeQuery
        ));

searchQuery.setQuery(boolQuery);

SearchRequest searchRequest = new SearchRequest(tableName, indexName, searchQuery);
// specify columns to get
SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
// get all columns
columnsToGet.setReturnAll(true);
searchRequest.setColumnsToGet(columnsToGet);
SearchResponse searchResponse = syncClient.search(searchRequest);

通过受益人姓名和受益比例查询

受益人字段我们在多元索引中保存为nested类型,这边我们使用NestedQuery来完成此功能:

SearchQuery searchQuery = new SearchQuery();
searchQuery.setGetTotalCount(true);

NestedQuery nestedQuery = new NestedQuery();
// search for nested field beneficiary_info
nestedQuery.setPath(BENEFICIARY_INFO);
nestedQuery.setScoreMode(ScoreMode.Avg);

BoolQuery boolQuery = new BoolQuery();

TermQuery beneficiaryNameQuery = new TermQuery();
// concat field name with `.`
beneficiaryNameQuery.setFieldName(BENEFICIARY_INFO + "." + NAME);
beneficiaryNameQuery.setTerm(ColumnValue.fromString("Tyrone Lee"));

RangeQuery profitPercentageQuery = new RangeQuery();
// concat field name with `.`
profitPercentageQuery.setFieldName(BENEFICIARY_INFO + "." + BENEFIT_PERCENTAGE);
profitPercentageQuery.setFrom(ColumnValue.fromLong(50), true);

boolQuery.setMustQueries(Arrays.asList(
  beneficiaryNameQuery,
  profitPercentageQuery
        ));

nestedQuery.setQuery(boolQuery);

searchQuery.setQuery(nestedQuery);
SearchRequest searchRequest = new SearchRequest(tableName, indexName, searchQuery);
// specify columns to get
SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
// get all columns
columnsToGet.setReturnAll(true);
searchRequest.setColumnsToGet(columnsToGet);
SearchResponse searchResponse = syncClient.search(searchRequest);

其他示例

其他示例可以参照样例代码中的SearchPolicyExample

总结

通过Tablestore的多元索引功能可以很方便地完成一个海量保单查询平台,用户只需要关注功能开发而不需要关注具体的运维细节,实现了真正的0托管,如果有更多的问题可以加入我们的技术支持群:
image

相关实践学习
消息队列+Serverless+Tablestore:实现高弹性的电商订单系统
基于消息队列以及函数计算,快速部署一个高弹性的商品订单系统,能够应对抢购场景下的高并发情况。
阿里云表格存储使用教程
表格存储(Table Store)是构建在阿里云飞天分布式系统之上的分布式NoSQL数据存储服务,根据99.99%的高可用以及11个9的数据可靠性的标准设计。表格存储通过数据分片和负载均衡技术,实现数据规模与访问并发上的无缝扩展,提供海量结构化数据的存储和实时访问。 产品详情:https://www.aliyun.com/product/ots
目录
相关文章
|
存储 索引
表格存储根据多元索引查询条件直接更新数据
表格存储是否可以根据多元索引查询条件直接更新数据?
111 3
|
NoSQL 开发工具
TableStore表格存储(阿里云OTS)多行数据操作查询,支持倒序,过滤条件和分页
1. 批量读取操作 批量读取操作可以通过多种方式进行,包括: GetRow:根据主键读取一行数据。 BatchGetRow:批量读取多行数据。 GetRange:根据范围读取多行数据。
869 0
|
存储 SQL NoSQL
基于物联网平台 + Tablestore 打造设备元数据管理平台
从场景到实践,分享物联网设备元数据场景的业务特点、技术选型和案例实践。
462 15
|
存储 SQL 消息中间件
基于物联网平台 + Tablestore,如何打造设备元数据管理平台?
基于物联网平台 + Tablestore,如何打造设备元数据管理平台?
245 15
基于物联网平台 + Tablestore,如何打造设备元数据管理平台?
|
SQL 存储 自然语言处理
表格存储最佳实践:使用多元索引加速 SQL 查询
表格存储(Tablestore)在 2022 年 5 月正式发布了 SQL 商业化版本,业务上只需要在数据表上建立映射关系,就可以基于 SQL 引擎方便地对表格存储中的数据进行访问和计算,大大地降低了用户的学习成本。
721 0
|
存储 消息中间件 SQL
基于 EMQX + Tablestore 打造车辆元数据管理平台
车辆网场景中的云端架构分享与案例实践。
723 0
|
存储 SQL 运维
Tablestore 与 Elasticsearch 在数据库查询加速场景使用对比
过去三十年,我们从企业应用开始,经历了 PC 互联网、移动互联网的爆发式发展,到如今的产业互联网。在这些不同时代,一直变化的是应用形态,不变的是核心数据的价值。对于核心数据的存储,首选的方案是使用数据库存储,从互联网初期开始,开源关系型数据库 MySQL 成长成为了数据库存储的第一选择,关系型数据库解决了数据的快速建模,高可靠存储和快速查询,但是关系数据库中的高效查询主要依赖二级索引,如果出现索引
1732 2
Tablestore 与 Elasticsearch 在数据库查询加速场景使用对比
|
SQL 存储 Java
表格存储 SQL 查询多元索引
多元索引是表格存储产品中一个重要的功能,多元索引使用倒排索引技术为表格存储提供了非主键列上的快速检索功能,另外也提供了统计聚合功能。表格存储近期开放了SQL查询功能,SQL引擎默认从原始表格中读取数据,非主键列上的查询需要扫描全表。
表格存储 SQL 查询多元索引
|
SQL 存储 NoSQL
基于 MySQL + Tablestore 分层存储架构的大规模订单系统实践-SQL 查询和分析
前言前面我们介绍了基于 MySQL + Tablestore 分层架构的订单系统。订单数据储存进入 Tablestore 后,用户可以使用 SDK 中的 API 访问数据,也可以继续使用 SQL 访问 Tablestore 中的数据。Tablestore 提供了多种 SQL 的接入方式,客户可以通过 DLA 访问 Tablestore,也可以利用 Tablestore 自身对 SQL 的支持能力,
967 0
基于 MySQL + Tablestore 分层存储架构的大规模订单系统实践-SQL 查询和分析
|
存储 SQL 运维
基于 MySQL + Tablestore 分层存储架构的大规模订单系统实践-基于 DLA 的联邦查询
前言在订单系统中,基于订单数据对客户和商家商品进行画像分析是一种常见的需求。常见的分析需求有:基于主键、分区键数据的条件组合检索,例如获取某用户最近 30 的订单列表。根据非主键列、分区键的条件组合检索工作,例如查询过去一天异常订单列表、查询过去一天成交额最大的10 笔订单。聚合统计类需求,比如统计某店铺过去一个月各商品销售额排名;统计双十一期间销售额前 10 的店铺;统计双十一期间某店铺每天订单
594 0
基于 MySQL + Tablestore 分层存储架构的大规模订单系统实践-基于 DLA 的联邦查询
下一篇
无影云桌面