作者 | 文库App
导读
ID在我们的开发工作和日常生活中使用的非常频繁,几乎只要是在开发就会天天打交道,它的应用场景十分广泛,比如:身份证号,下单生成的订单号,购买的联合会员商品的兑换券码。不同场景对ID生成服务的要求不同,以下我们逐个分析。全文6863字,预计阅读时间18分钟。
01 什么是分布式ID生成服务
在业务开发中,大量场景需要唯一ID来进行标识:用户独一无二的身份认证、超市售卖的商品、微信的即时消息,它们都需要标识来确定唯一性。需要在特定范围内保证ID具备唯一性,这是ID生成服务最基本的要求。
生成ID的方式多种多样,可以使用Redis键自增,UUID,或者基于雪花算法实现的ID生成服务。最常见的基于数据库ID自增的方式,在业务数据量不大的时候,单库单表可以支撑,数据再大一点搞个MySQL主从同步、读写分离也能对付。但随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求。
伴随着业务快速迭代,很多业务都需要生成ID,各自为政会陷入"重复造轮子"的低效劳动中,同时造成服务治理上的混乱,此时一个能够生成全局唯一ID的服务是非常必要的。那么这个全局唯一ID就叫分布式ID生成服务。
02 服务特性
① 唯一性:生成的ID唯一,特定范围不冲突;
② 有序性:生成的ID按某种规则有序,趋势递增,便于入库和查询,但不严格要求;
③ 高可用、高性能:高并发下的具备高可用,确保任何情况能容灾,稳定提供服务;
④ 自主性:分布式环境下不依赖中心认证即可自行生成ID;
⑤ 安全性:脱敏,不暴露系统和业务的信息,如:订单数,用户数。
03 常见的技术实现方式
04 技术为业务服务
技术归根到底是为业务服务,要在业务中体现技术的价值。
网上绝大多数的分布式id生成服务,一般着重于技术原理剖析,很少见到根据具体的业务场景去选型ID生成服务的文章。
本文结合一些使用场景,进一步探讨业务场景中对ID有哪些具体的要求。
4.1 场景一:订单系统
我们在商场买东西一码付二维码,下单生成的订单号,使用到的优惠券码,联合商品兑换券码,这些是在网上购物经常使用到的单号,那么为什么有些单号那么长,有些只有几位数?有些单号一看就知道年月日的信息,有些却看不出任何意义?下面展开分析下订单系统中不同场景的id服务的具体实现。
1、一码付
我们常见的一码付,指的是一个二维码可以使用支付宝或者微信进行扫码支付。
二维码的本质是一个字符串。聚合码的本质就是一个链接地址。用户使用支付宝微信直接扫一个码付钱,不用担心拿支付宝扫了微信的收款码或者用微信扫了支付宝的收款码,这极大减少了用户扫码支付的时间。
实现原理是当客户用APP扫码后,网站后台就会判断客户的扫码环境。(微信、支付宝、QQ钱包、京东支付、云闪付等)。
判断扫码环境的原理就是根据打开链接浏览器的 HTTP header。任何浏览器打开http链接时,请求的header都会有User-Agent(UA、用户代理)信息。
UA是一个特殊字符串头,服务器依次可以识别出客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等很多信息。
各渠道对应支付产品的名称不一样,一定要仔细看各支付产品的API介绍。
- 微信支付:JSAPI支付支付
- 支付宝:手机网站支付
- QQ钱包:公众号支付
其本质均为在APP内置浏览器中实现HTML5支付。
文库的研发同学在这个思路上,做了优化迭代。动态生成一码付的二维码预先绑定用户所选的商品信息和价格,根据用户所选的商品动态更新。这样不仅支持一码多平台调起支付,而且不用用户选择商品输入金额,即可完成订单支付的功能,很丝滑。用户在真正扫码后,服务端才通过前端获取用户UID,结合二维码绑定的商品信息,真正的生成订单,发送支付信息到第三方(qq、微信、支付宝),第三方生成支付订单推给用户设备,从而调起支付。
区别于固定的一码付,在文库的应用中,使用到了动态二维码,二维码本质是一个短网址,ID服务提供短网址的唯一标志参数。唯一的短网址映射的ID绑定了商品的订单信息,技术和业务的深度结合,缩短了支付流程,提升用户的支付体验。
2、订单号
订单号在实际的业务过程中作为一个订单的唯一标识码存在,一般实现以下业务场景:
- 用户订单遇到问题,需要找客服进行协助;
- 对订单进行操作,如线下收款,订单核销;
- 下单,改单,成单,退单,售后等系统内部的订单流程处理和跟进。
很多时候搜索订单相关信息的时候都是以订单ID作为唯一标识符,这是由于订单号的生成规则的唯一性决定的。从技术角度看,除了ID服务必要的特性之外,在订单号的设计上需要体现几个特性:
(1)信息安全
编号不能透露公司的运营情况,比如日销、公司流水号等信息,以及商业信息和用户手机号,身份证等隐私信息。并且不能有明显的整体规律(可以有局部规律),任意修改一个字符就能查询到另一个订单信息,这也是不允许的。
类比于我们高考时候的考生编号的生成规则,一定不能是连号的,否则只需要根据顺序往下查询就能搜索到别的考生的成绩,这是绝对不可允许。
(2)部分可读
位数要便于操作,因此要求订单号的位数适中,且局部有规律。这样可以方便在订单异常,或者退货时客服查询。
过长的订单号或易读性差的订单号会导致客服输入困难且易错率较高,影响用户体验的售后体验。因此在实际的业务场景中,订单号的设计通常都会适当携带一些允许公开的对使用场景有帮助的信息,如时间,星期,类型等等,这个主要根据所涉及的编号对应的使用场景来。
而且像时间、星期这些自增长的属于作为订单号的设计的一部分元素,有助于解决业务累积而导致的订单号重复的问题。
(3)查询效率
常见的电商平台订单号大多是纯数字组成,兼具可读性的同时,int类型相对varchar类型的查询效率更高,对在线业务更加友好。
3、优惠券和兑换券
优惠券、兑换券是运营推广最常用的促销工具之一,合理使用它们,可以让买家得到实惠,商家提升商品销量。常见场景有:
- 在文库购买【文库VIP+QQ音乐年卡】联合商品,支付成功后会得到QQ音乐年卡的兑换码,可以去QQ音乐App兑换音乐会员年卡;
- 疫情期间,部分地方政府发放的消费券;
- 瓶装饮料经常会出现输入优惠编码兑换奖品。
从技术角度看,有些场景适合ID即时生成,比如电商平台购物领取的优惠券,只需要在用户领取时分配优惠券信息即可。有些线上线下结合的场景,比如疫情优惠券,瓶盖开奖,京东卡,超市卡这种,则需要预先生成,预先生成的券码具备以下特性:
1.预先生成,在活动正式开始前提供出来进行活动预热;
2.优惠券体量大,以万为单位,通常在10万级别以上;
3.不可破解、仿制券码;
4.支持用后核销;
5.优惠券、兑换券属于广撒网的策略,所以利用率低,也就不适合使用数据。
库进行存储(占空间,有效的数据有少)
设计思路上,需要设计一种有效的兑换码生成策略,支持预先生成,支持校验,内容简洁,生成的兑换码都具有唯一性,那么这种策略就是一种特殊的编解码策略,按照约定的编解码规则支撑上述需求。
既然是一种编解码规则,那么需要约定编码空间(也就是用户看到的组成兑换码的字符),编码空间由字符a-z,A-Z,数字0-9组成,为了增强兑换码的可识别度,剔除大写字母O以及I,可用字符如下所示,共60个字符:
abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789
之前说过,兑换码要求近可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为12位,而字符空间有60位,那么可以表示的空间范围为60^12=130606940160000000000000(也就是可以12位的兑换码可以生成天量,应该够运营同学挥霍了),转换成2进制:1001000100000000101110011001101101110011000000000000000000000(61位)
兑换码组成成分分析
兑换码可以预先生成,并且不需要额外的存储空间保存这些信息,每一个优惠方案都有独立的一组兑换码(指运营同学组织的每一场运营活动都有不同的兑换码,不能混合使用, 例如双11兑换码不能使用在双12活动上),每个兑换码有自己的编号,防止重复,为了保证兑换码的有效性,对兑换码的数据需要进行校验,当前兑换码的数据组成如下所示:
优惠方案ID + 兑换码序列号i + 校验码
编码方案
- 兑换码序列号i,代表当前兑换码是当前活动中第i个兑换码,兑换码序列号的空间范围决定了优惠活动可以发行的兑换码数目,当前采用30位bit位表示,可表示范围:1073741824(10亿个券码)。
- 优惠方案ID, 代表当前优惠方案的ID号,优惠方案的空间范围决定了可以组织的优惠活动次数,当前采用15位表示,可以表示范围:32768(考虑到运营活动的频率,以及ID的初始值10000,15位足够,365天每天有运营活动,可以使用54年)。
- 校验码,校验兑换码是否有效,主要为了快捷的校验兑换码信息的是否正确,其次可以起到填充数据的目的,增强数据的散列性,使用13位表示校验位,其中分为两部分,前6位和后7位。
深耕业务还会有区分通用券和单独券的情况,分别具备以下特点,技术实现需要因地制宜地思考。
- 通用券:多个玩家都可以输入兑换,然后有总量限制,期限限制。
- 单独券:运营同学可以在后台设置兑换码的奖励物品、期限、个数,然后由后台生成兑换码的列表,兑换之后核销。
4.2 Tracing
1、日志跟踪
在分布式服务架构下,一个 Web 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。这个过程中每个服务之间的通信又是单独的网络请求,无论请求经过的哪个服务出了故障或者处理过慢都会对前端造成影响。
处理一个Web请求要调用的多个服务,为了能更方便的查询哪个环节的服务出现了问题,现在常用的解决方案是为整个系统引入分布式链路跟踪。
在分布式链路跟踪中有两个重要的概念:跟踪(trace)和 跨度( span)。trace 是请求在分布式系统中的整个链路视图,span 则代表整个链路中不同服务内部的视图,span 组合在一起就是整个 trace 的视图。
在整个请求的调用链中,请求会一直携带 traceid 往下游服务传递,每个服务内部也会生成自己的 spanid 用于生成自己的内部调用视图,并和traceid一起传递给下游服务。
2、TraceId 生成规则
这种场景下,生成的ID除了要求唯一之外,还要求生成的效率高、吞吐量大。traceid需要具备接入层的服务器实例自主生成的能力,如果每个trace中的ID都需要请求公共的ID服务生成,纯纯的浪费网络带宽资源。且会阻塞用户请求向下游传递,响应耗时上升,增加了没必要的风险。所以需要服务器实例最好可以自行计算tracid,spanid,避免依赖外部服务。
产生规则:服务器 IP + ID 产生的时间 + 自增序列 + 当前进程号 ,比如:
0ad1348f1403169275002100356696
前 8 位 0ad1348f 即产生 TraceId 的机器的 IP,这是一个十六进制的数字,每两位代表 IP 中的一段,我们把这个数字,按每两位转成 10 进制即可得到常见的 IP 地址表示方式 10.209.52.143,您也可以根据这个规律来查找到请求经过的第一个服务器。
后面的 13 位 1403169275002 是产生 TraceId 的时间。之后的 4 位 1003 是一个自增的序列,从 1000 涨到 9000,到达 9000 后回到 1000 再开始往上涨。最后的 5 位 56696 是当前的进程 ID,为了防止单机多进程出现 TraceId 冲突的情况,所以在 TraceId 末尾添加了当前的进程 ID。
3、SpanId 生成规则
span是层的意思,比如在第一个实例算是第一层, 请求代理或者分流到下一个实例处理,就是第二层,以此类推。通过层,SpanId 代表本次调用在整个调用链路树中的位置。
假设一个 服务器实例 A 接收了一次用户请求,代表是整个调用的根节点,那么A 层处理这次请求产生的非服务调用日志记录spanid的值都是0,A层需要通过 RPC 依次调用 B、C、D 三个服务器实例,那么在 A 的日志中,SpanId 分别是 0.1,0.2 和 0.3,在 B、C、D 中,SpanId 也分别是 0.1,0.2 和 0.3;如果 C 系统在处理请求的时候又调用了 E,F 两个服务器实例,那么 C 系统中对应的spanid是 0.2.1 和 0.2.2,E、F 两个系统对应的日志也是 0.2.1 和 0.2.2。
根据上面的描述可以知道,如果把一次调用中所有的 SpanId 收集起来,可以组成一棵完整的链路树。
spanid的生成本质:在跨层传递透传的同时,控制大小版本号的自增来实现的。
4.3 场景三:短网址
短网址主要功能包括网址缩短与还原两大功能。相对于长网址,短网址可以更方便地在电子邮件,社交网络,微博和手机上传播,例如原来很长的网址通过短网址服务即可生成相应的短网址,避免折行或超出字符限制。
常用的ID生成服务比如:mysql ID自增、 redis键自增、号段模式,生成的ID都是一串数字。短网址服务把客户的长网址转换成短网址,
实际是在dwz.cn域名后面拼接新产生的数字类型ID,直接用数字ID,网址长度也有些长,服务可以通过数字ID转更高进制的方式压缩长度。这种算法在短网址的技术实现上越来越多了起来,它可以进一步压缩网址长度。转进制的压缩算法在生活中有广泛的应用场景,举例:
客户的长网址:https://wenku.baidu.com/ndbus...\_code=PCoperatebanner
ID映射的短网址:https://dwz.cn/2047601319t66
转进制后的短网址:https://dwz.cn/2ezwDJ0
长数字转短字符串的压缩算法,以下为具体实现:
/**
* 10进制转为62进制
*
* @param integer $n 10进制数值
* @return string 62进制
*/
function dec62($n) {
$base = 62;
$index = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$ret = '';
for($t = floor(log10($n) / log10($base)); $t >= 0; $t --) {
$a = floor($n / pow($base, $t));
$ret .= substr($index, $a, 1);
$n -= $a * pow($base, $t);
}
return $ret;
}
端字符串同样支持反解,将高位进制转为10进制。
/**
* 62进制转为10进制
*
* @param integer $n 62进制
* @return string 10进制
*/
function dec10($s) {
$base = 62;
$index = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$ret = 0;
$len = strlen($s) - 1;
for($t = 0; $t <= $len; $t ++) {
$ret += strpos($index, substr($s, $t, 1)) * pow($base, $len - $t);
}
return $ret;
}
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 依次表示0-62,大小写表示不同的进制。10进制的数字采取62进位压缩的方式转为字符串,仅需要6位就可以满足500多亿的网址(568 0023 5583),唯一性得到满足的同时保证长度够短,在具体实现中也可随机跳跃生成,防御撞库攻击。
05 总结
在诸多需要ID服务的业务中,联合商品需要做好信息加密,兑换码、优惠券需要支持可反解或者可验证,订单号需要支持部分信息可读,可以轻松获取日期信息。
纯数字组成的用户uid,订单号出于数据安全,要求是非连续的,但是如果是一些需要计数的ID,则要求严格递增。短url服务中的ID对长度有很高要求,traceid则要求ID可反解,且支持本地生成,保证局部唯一就行。
由此可见,ID生成服务除了具备基础特性,部分场景下还需要满足的特定需求。制定合适的技术方案结合场景去实现,发挥技术优势,才可以更好的支撑业务发展。
———END———