本文立题来源于Tablestore的日常线上支持,为解刚上手Tablestore的同学对“主键列自增”这个功能的使用疑惑。
一个日常咨询:
X同学:我想修改一行数据,用UpdateRow接口,但是我的表里用了自增列,我不知道自增主键的值是什么,PrimaryKey怎么填写呢?
技术支持:您要修改这行需要先读出来,拿到主键信息,再做修改。
X同学:是用GetRow接口读吗?那GetRow的PrimaryKey怎么填写?
技术支持:没有完整的主键信息不能使用GetRow接口读,您可以尝试用GetRange加上过滤条件找到要修改的行。如果有单行读和单行修改的操作,建议在插入行的时候选择返回主键信息,做一下记录供以后操作,可以参考文档。
X同学:...(内心感觉很不好用)
自增列-“主键列自增”,是Tablestore很有特色的一个功能,和常见的k-v存储相比,我们支持创建含有一个“自增列主键”的表。若设置某一列主键为自增列,在写入一行数据时,这一列主键无需填值,表格存储会自动生成这一主键列的值。该值在分区键上保证唯一,且严格递增。
要想知道自增列主键的用途,不妨了解下它被创造的背景,自增列主键最早开发是为了给某商务沟通软件用的,目标是高效支持一个IM系统,如果您正是要找IM系统的实现方案,可以直接参考Tablestore主键列自增功能在IM系统中的应用。
社交IM的基本需求是:将源用户发送的消息及时、准确地更新给该目的用户。一条消息的基本元素 {接收方的用户ID,消息内容};为了保证所有的消息能够按照正确的顺序被接收端处理,添加消息ID,一条消息的结构变为 {接收方的用户ID,消息ID,消息内容},保证消息ID严格递增。
实现上以“接收方用户ID”和“消息ID”做主键,“消息内容”为属性,一条消息存为一行数据;接收端根据“接收方用户ID”和“消息ID”的范围批量获取并处理对应用户的消息。
通过GetRange接口读取最近的消息。message_id这一列PK的起始位置是上一条消息的message_id+1,结束位置是INF_MAX,这样每次都可以读出最新的消息,然后发送给客户端。代码实现参考
自增列分区键级别唯一,严格递增但不保证连续
分区键(第一个主键)级别唯一,从上述背景可看到,相同的“接收方用户ID”的消息在同一个分区键内,所以分区键级别唯一且严格递增的“消息ID”已经可以保证消息的顺序性。为什么不是全表唯一呢,Tablestore的底层实现决定,有“相同的分区键的行”会被放在“同一个分区”,一个分区同一时刻唯一被一台机器加载;所以简单理解“分区键级别”是同一台机器上的操作,而“表级别”则是跨机器的操作,出于性能和可扩展性保证,我们提供部分扩展操作都是分区键级别的:自增列、局部事务等。
综上可看,自增列的一个主流用法是:“同一个ID”有序的存储一批数据,单条或批量写入,然后批量的读取和处理。自增列的主要作用是“保序”,而不是作为唯一定位一行的主键部分的主要“身份区分”元素。一来自增列的并值不是全表唯一的,二来在写入时就开始记录自增主键的值有一定维护成本。
一个用的不好的例子
- 某用户做一个全国食品连锁店的店铺信息统计,把自增主键作为区分同名连锁店的唯一标识:
PK1 |
PK2 |
PK3 |
col1 |
col2 |
col3 |
...... |
北京 |
KFC |
1578490808029000 |
地址:朝阳区 |
电话:xxx |
营业时间:xxx |
|
上海 |
M记 |
1578490791443000 |
地址:徐汇区 |
电话:xxx |
营业时间:xxx |
|
杭州 |
必胜客 |
1578490823817000 |
地址:西湖区 |
电话:xxx |
营业时间:xxx |
|
杭州 |
必胜客 |
1578490833204000 |
地址:滨江区 |
电话:xxx |
营业时间:xxx |
|
杭州的两家必胜客,插入数据时没有问题,自增的主键只填一个占位符,补充其他信息即可。存储时也没有问题,自增主键保证了主键的唯一性,但是查询信息的时候就头疼了,插入行的时候并没有保留自增列PK3的信息,没有完整的主键信息既没办法读取单行信息,也不能对单行信息进行修改。
同样的主键结构换个场景则适用:
- 统计不同地区“不同品牌”连锁店的,高低峰就餐时间段不同桌型的就座率。
PK1 |
PK2 |
PK3 |
col1 |
col2 |
...... |
北京 |
KFC |
1578490808029000 |
桌型:两人桌 |
新增就餐人数:1 |
|
上海 |
M记 |
1578490791443000 |
桌型:四人桌 |
结束就餐人数:3 |
|
杭州 |
必胜客 |
1578490823817000 |
桌型:六人桌 |
新增就餐人数:5 |
|
杭州 |
必胜客 |
1578490823817800 |
桌型:六人桌 |
结束就餐人数:5 |
|
杭州 |
必胜客 |
1578490833204000 |
桌型:四人桌 |
新增就餐人数:4 |
|
每隔时间周期,批量获取一次某品牌不同桌型新增/结束就餐人数信息做增量处理,统计出实时的就座率。这里的自增列PK3保证了同一桌人开始就餐和结束就餐的顺序不会错乱,就座率始终为正。和上例的不同点:1.把“杭州-必胜客”看为一个“同一个ID” 2.行的属性信息是一个待处理的队列性质信息。
- 如果还是要实现原来的需求店铺信息统计,那么改一下表结构使主键唯一且用户可知:
PK1 |
PK2 |
col1 |
col2 |
col3 |
...... |
北京 |
KFC |
地址:朝阳区 |
电话:xxx |
营业时间:xxx |
|
上海 |
M记 |
地址:徐汇区 |
电话:xxx |
营业时间:xxx |
|
杭州 |
必胜客(西湖区) |
地址:西湖区 |
电话:xxx |
营业时间:xxx |
|
杭州 |
必胜客(滨江区) |
地址:滨江区 |
电话:xxx |
营业时间:xxx |
|
- 如果只想借Tablestore的主键自增功能做一个“唯一ID生成器”的话:
建议用一张表专门生层“唯一ID”,先插入一条数据到这张表拿到返回的主键信息,再写入业务表。
PK1 |
PK2 |
col1 |
ID生成 |
1578490808029000 |
000 |
ID生成 |
1578490791443000 |
000 |
ID生成 |
1578490823817000 |
000 |
ID生成 |
1578490833204000 |
000 |
PK1 |
PK2 |
col1 |
col2 |
col3 |
col4 |
...... |
北京 |
KFC |
1578490808029000 |
地址:朝阳区 |
电话:xxx |
营业时间:xxx |
|
上海 |
M记 |
1578490791443000 |
地址:徐汇区 |
电话:xxx |
营业时间:xxx |
|
杭州 |
必胜客(西湖区) |
1578490823817000 |
地址:西湖区 |
电话:xxx |
营业时间:xxx |
|
杭州 |
必胜客(滨江区) |
1578490833204000 |
地址:滨江区 |
电话:xxx |
营业时间:xxx |
|
不推荐这样使用自增列属性,因为若想保证全表唯一,生成ID的表的分区键是固定的。即全表只有一个分区键,落在一个分区一台机器上,且分区无法再分裂,一个分区下的数据不宜过多,应该控制在10GB以内,当行数到千万级及以上时可能就会到达瓶颈。
本文主要讲Tablestore的自增列最为普遍的使用方式,大家如果用在了更多新场景解锁了更多新姿势,欢迎进群交流讨论。钉钉群:“表格存储公开交流群”,群内提供免费的在线专家服务,欢迎扫码加入,群号:23307953