Daizc
count.articles52
count.tags25
count.categories3
发码平台结构设计

发码平台结构设计

同事接到个任务要做一个发码平台,但产品没说清楚怎么做,也不造要做成什么样子。

虽然跟我没关系,但由于之前就对发码平台的实现逻辑感兴趣,因此基于兴趣进行了业务设计,这里简单记录个人的一些想法。

需求猜测

如果只是为了保证公司内所有的券码的唯一性,那我觉得做一个随机数生成器就足够了,做成平台级的功能没有意义,基于此进行需求猜测:

  • 唯一性保证:保证业务平台内券码唯一性
  • 券码生成管理:能查看每种优惠券的生成以及使用情况
  • 批量生成券码:一次返回限量的券码给业务端
  • 券码核销:核销并同步回调业务端。

唯一性保证

缩短雪花位数

这个好说。保证唯一性的算法多得很,听到这个需求就想到使用雪花ID。但产品冒出来说要求券码长度在10位以内,这个需要斟酌一下。

想了一下,雪花ID是64位二进制数,如果把base提高,那么位数就会下降。

随便做个测试:

1
2
3
4
public static void main(String[] args) {
System.out.println(Long.toString(SnowflakeIdUtil.generateId(), 32));
}
// output => gjnk3gf62400

32进制下只有12个字符了,加把劲把进制弄成64试试:

1
2
3
4
public static void main(String[] args) {
System.out.println(Long.toString(SnowflakeIdUtil.generateId(), 64));
}
// output => 598684081116483584

输出变成十进制了,感觉有问题,进去看看代码:

1
2
3
4
5
6
7
8
java.util.Long:
public static String toString(long i, int radix) {
// Look Here! MIN_RADIX = 2 MAX_RADIX = 36
if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
radix = 10;
if (radix == 10)
return toString(i);
......

Long这个类中的toString做了限制,只能将number表示为2-36进制编码的字符串,因为(0-9)+(a-z)刚好36个字符。这里没有考虑到大写字符也可以来做编码,
如果加上A-Z的大写字符,就有62个字符可用,可以将这个long表示为62进制的字符串,字符串内无特殊字符,既好看又不用考虑转义。 测试Base62:

1
2
3
4
5
使用hutool的Base62:
public static void main(String[] args) {
System.out.println(Base62.encode(Longs.toByteArray(nextId)));
}
// output => iDzwMuZe76

需求满足!

不连续性

这个着实有点头疼,思考了一下把这个拆解为两个部分

业务线隔离

总的来说就是要让同一时间内每个业务线生成的ID不连续,这样可以充分降低每个业务线之间券码冲突的可能性(虽然雪花ID本来就不冲突,只要时间不被NTP回调)。

从SnowFlake类本身着手,由于我们应用跑在k8s内,本身就有全系统唯一的标识,且实例数也不会大于32,所以这里近似的任务SnowFlake中的datacenterId和workId没必要都用,
这里将容器的Id对32取余作为datacenterId(此处可能会导致冲突,后期可以与运维协商为容器编号,这里使用编号即可),再对业务方提供的业务Id对32取余作为workId,这样基本达成了通过业务线隔离Id的要求。
11101010111101100010010110000000000

同一批券码隔离

虽说同一批券码生成之间不冲突,但是由于雪花ID尾数是个序列,同一批券码生成出来尾数是连续的,这可能会导致相邻的券码被盗用,因此同一批券码隔离也是有必要的。

还是打算从SnowFlake类着手,64位数,每一位都有用处,其中时间戳占用的空间比较大,占用了整整11位。要想腾空间,就只有拿时间戳开刀了。 首先我们计算1年的时间用时间戳表示需要多长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
毫秒*秒*分*时*天
1000*60*60*24*365
= 31536000000
= 0b11101010111101100010010110000000000
1年时间需要35个二进制位表示,剩下6个二进制位,可以提供出0b111111(64)个位置

31536000000*3
= 0b1011000000111000100111000010000000000
3年的时间需要37个二进制位来表示,剩下4个二进制位,可以提供处0b1111(16)个位置给用户猜测

31536000000*5
= 0b10010010110110011101011101110000000000
5年的时间需要38个二进制位来表示,剩下3个二进制位,可以提供处0b111(8)个位置给用户猜测

感觉差不多了,就用38个二进制位:

38个二进制位
0b11111111111111111111111111111111111111
= 274877906943
274877906943/1000/3600/24 约 3181.46天 合 8.7年

如此这般,便省下来3个二进制位和时间戳前面的符号位一起组成4个二进制位用于放猜解,可以提供16种组合

这里吐槽一下,这个限制10位要求全局唯一要求生成性能高还要抗猜解真的有点过分,当然雪花算法也并不特别适合这种场景,有其他好的方案就更好了。

后续重写Snowflake类,重新设置位移位数并新增抗猜解位数就可以了。

业务以及表结构设计

这个功能说来简单,其实也有不明确的点:

  1. 一次要生成多少张券?每一批券需不需要记录批次?
  2. 是即时生成还是仅记录数量用时再取?
  3. 需要管理什么字段?

就个人想法来说,先假设每个系统会有一个账户,系统收到钱后会生成订单,根据订单数额,找本系统添加对应数量的券码。但是由于生成的数量较多,因此在这里并不会真正地生成券码,只会记录该系统拥有对应数量的券码额度。随后调用获取券码接口时,再生成券码,再通过限制该接口的调用频率和最大券码数来控制性能消耗。

最终交互设计图如下:

发码平台交互图

生成管理表设计(省略大家都有的字段) generate_manage

field
(驼峰/下划线脑内自行转换)
type function
id varchar(32) -
systemId varchar(32) 具体业务线Id
systemName varchar(128) 业务系统名冗余在此
避免远程查表
type varchar(64) 这批券码的生成方式(SNOWFLAKE_LITE/UUID)
total bigint 该批次拥有的券码总数
current bigint 该批次已生成的券码总数(保证原子性)
systemName varchar(128) 业务系统名冗余在此
避免远程查表
businessTypeTemplate varchar(64) [模板]提供给业务方存储业务上的类型
在获取券码时可以覆盖该值
可能是以后做查询的条件
businessDataTemplate json [模板]提供给业务方存储该批次券码的公有属性
在获取券码时可以覆盖该值
券码被消费时将返给业务方
callbackTemplate json [模板]券码被消费回调地址
在获取券码时可以覆盖该值
createTime date(3) -

生成操作记录表设计(省略大家都有的字段) generate_operation

field
(驼峰/下划线脑内自行转换)
type function
id varchar(32) -
manageId varchar(32) 关联generate_manage
total bigint 该批次拥有的券码总数
original bigint 消费前数量
consumption bigint 本次消费数量
remaining bigint 剩余消费数量
createTime date(3) -

券码表设计 coupon

field
(驼峰/下划线脑内自行转换)
type function
id varchar(32) -
manageId varchar(32) 关联 generate_manage
operationId varchar(32) 关联 generate_operation
systemId varchar(32) [冗余以避免连表]具体业务线Id
systemName varchar(128) [冗余以避免连表]业务系统名冗余在此
避免远程查表
generateTime date(3) [冗余以避免连表]券码生成时间
code varchar(32) 这个才是券码
businessType varchar(32) 提供给业务方存储业务上的类型
可能是以后做查询的条件
businessData json 提供给业务方存储该批次券码的公有属性
券码被消费时将返给业务方
callback json 券码被消费回调地址,格式为map为升级预留空间
Map<String callbackUrl,String callbackVersion>
createTime date(3) -

生成/获取券码API

request

field
(驼峰/下划线脑内自行转换)
type function
manageId String 关联 generate_manage
businessType String
businessData JSONObject
callback Map<String,String>

response

field
(驼峰/下划线脑内自行转换)
type function
operation GenerateOperation
coupons List<Coupon>

copyright.author:Daizc
copyright.permalink:https://note.bequick.run/%E5%8F%91%E7%A0%81%E5%B9%B3%E5%8F%B0%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可