以前的订单服务和代付服务都是自己生成订单ID,这样是很不好的。先不说设计的耦和,一个很大的问题是多机器多进程部署的时候造成订单号重复,那么就必须重试,造成订单服务流程的混乱。如果需要其他格式或者长度的单号又得在代码中重新生成一次,很不利于后期维护。于是就由我来做一个专门生成唯一ID的服务。
设计目标
-
可以多机器多进程部署
这样可以保证通过多进程和扩展机器的方式来提高qps。可以通过HA或者LVS来对外提供唯一的ip和端口,方便访问。 -
保证不产生重复ID
这个是最基本的要求。因为是基于时间加随机数的方式生成的ID,在多进程和多机器部署的情况下就必须加入其它的数据来保证唯一性。我想到两种办法:- 通过加入网卡号来区分机器,加入进程号来区分进程。不过这种方式虽然可以满足这个要求,但是会让订单号变得特别长。
- 为每个进程配置一个work_id,在订单号中加入这个work_id,就可以区分不同的机器和进程产生的ID了。
-
性能上要求:单进程1000qps
因为订单服务可以做到1000tps,这样可以保证即使单进程也可以满足需求。
格式
800 | 20170628180306247 | 94834688 | 05 | 01 |
---|---|---|---|---|
固定的头部 | 年月日时分秒毫秒 | 随机数 | 表标识 | 库标识 |
- 库标识和表标识
因为我们的数据库是多库多表的,所以需要一个标记一下,方便后期通过订单号查询数据。 - 随机数的生成 随机数的生成采用snowflake算法,下面是我写的详细注释。
// 注意:bit、Byte和数字的移动位数的不一样的 uint64_t get_unique_id() { uint64_t uniqueId=0; // nowtime 是当前的秒数sec*1000+当前的微秒数usec/1000,usec最大1000000, // 相当于sec右移10bit位,然后把usec/1000放在末尾的10 bit位 // 此时nowtime 的前22 bit位是0,后42 bit位是有效数据 uint64_t nowtime = get_curr_ms(); // uniqueId的前42位是有效数据,后22位是0 uniqueId = nowtime<<22; // g_info.workid是work_id = 1,自己配置的。0x3ff是低10位为1,高位为0. // g_info.workid&0x3ff是保留workid的低10位,其他位置为0. 接着<<12 后高位为0,中间10位是有效数据,低12位为0. // |= 操作后uniqueId的后22位无效数据的前10位变成有效数据了,低12位仍然是0 uniqueId |=(g_info.workid&0x3ff)<<12; // 确保时间是向前走的,后退就可能造成重复数据 if (nowtime <g_info.last_stamp) { perror("error"); exit(-1); } // 在一个毫秒内 if (nowtime == g_info.last_stamp) { // sequenceMask=( -1L ^ (-1L << 12L)) 也就是低12位为1,高位为0 // seqid原子自增1后和sequenceMask进行与操作后,seqid低12位保留,高位置0 g_info.seqid = atomic_incr(g_info.seqid)& sequenceMask; if (g_info.seqid == 0) { // 每毫秒产生的id极限是12个1,也就是4095 nowtime = wait_next_ms(g_info.last_stamp); } } else { g_info.seqid = 0; } g_info.last_stamp = nowtime; // 或操作后uniqueId的低12位变成seqid,其他位不变。 uniqueId |=g_info.seqid; return uniqueId; }
返回的这个uint_64类型的uniqueId的数据如下
42bit | 10bit | 12bit |
---|---|---|
当前时间的毫秒数,多余的高位数据被截断 | work_id | 每一毫秒都从一开始递增 |
那么我们可以计算出该算法单进程每秒产生的随机数最大个数是:1000*4095=409.5万。
我只取后9位也就是一个int类型的长度(大约),完全满足一毫秒内不重复的要求。
当然限于CPU和网络带宽,极限性能是不可能达到的。本地单进程测试可以达到2000qps,完全满足设计要求。