发码平台结构设计
同事接到个任务要做一个发码平台,但产品没说清楚怎么做,也不造要做成什么样子。
虽然跟我没关系,但由于之前就对发码平台的实现逻辑感兴趣,因此基于兴趣进行了业务设计,这里简单记录个人的一些想法。
需求猜测
如果只是为了保证公司内所有的券码的唯一性,那我觉得做一个随机数生成器就足够了,做成平台级的功能没有意义,基于此进行需求猜测:
- 唯一性保证:保证业务平台内券码唯一性
- 券码生成管理:能查看每种优惠券的生成以及使用情况
- 批量生成券码:一次返回限量的券码给业务端
- 券码核销:核销并同步回调业务端。
唯一性保证
缩短雪花位数
这个好说。保证唯一性的算法多得很,听到这个需求就想到使用雪花ID。但产品冒出来说要求券码长度在10位以内,这个需要斟酌一下。
想了一下,雪花ID是64位二进制数,如果把base提高,那么位数就会下降。
随便做个测试:
1 |
|
32进制下只有12个字符了,加把劲把进制弄成64试试:
1 |
|
输出变成十进制了,感觉有问题,进去看看代码:
1 |
|
Long这个类中的toString做了限制,只能将number表示为2-36进制编码的字符串,因为(0-9)+(a-z)刚好36个字符。这里没有考虑到大写字符也可以来做编码,
如果加上A-Z的大写字符,就有62个字符可用,可以将这个long表示为62进制的字符串,字符串内无特殊字符,既好看又不用考虑转义。 测试Base62:
1 |
|
需求满足!
不连续性
这个着实有点头疼,思考了一下把这个拆解为两个部分
业务线隔离
总的来说就是要让同一时间内每个业务线生成的ID不连续,这样可以充分降低每个业务线之间券码冲突的可能性(虽然雪花ID本来就不冲突,只要时间不被NTP回调)。
从SnowFlake类本身着手,由于我们应用跑在k8s内,本身就有全系统唯一的标识,且实例数也不会大于32,所以这里近似的任务SnowFlake中的datacenterId和workId没必要都用,
这里将容器的Id对32取余作为datacenterId(此处可能会导致冲突,后期可以与运维协商为容器编号,这里使用编号即可),再对业务方提供的业务Id对32取余作为workId,这样基本达成了通过业务线隔离Id的要求。
11101010111101100010010110000000000
同一批券码隔离
虽说同一批券码生成之间不冲突,但是由于雪花ID尾数是个序列,同一批券码生成出来尾数是连续的,这可能会导致相邻的券码被盗用,因此同一批券码隔离也是有必要的。
还是打算从SnowFlake类着手,64位数,每一位都有用处,其中时间戳占用的空间比较大,占用了整整11位。要想腾空间,就只有拿时间戳开刀了。 首先我们计算1年的时间用时间戳表示需要多长:
1 |
|
如此这般,便省下来3个二进制位和时间戳前面的符号位一起组成4个二进制位用于放猜解,可以提供16种组合
这里吐槽一下,这个限制10位要求全局唯一要求生成性能高还要抗猜解真的有点过分,当然雪花算法也并不特别适合这种场景,有其他好的方案就更好了。
后续重写Snowflake类,重新设置位移位数并新增抗猜解位数就可以了。
业务以及表结构设计
这个功能说来简单,其实也有不明确的点:
- 一次要生成多少张券?每一批券需不需要记录批次?
- 是即时生成还是仅记录数量用时再取?
- 需要管理什么字段?
就个人想法来说,先假设每个系统会有一个账户,系统收到钱后会生成订单,根据订单数额,找本系统添加对应数量的券码。但是由于生成的数量较多,因此在这里并不会真正地生成券码,只会记录该系统拥有对应数量的券码额度。随后调用获取券码接口时,再生成券码,再通过限制该接口的调用频率和最大券码数来控制性能消耗。
最终交互设计图如下:
生成管理表设计(省略大家都有的字段) 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> |