分布式ID需求
全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。
UUID
优点:
1.性能高,本地生成,没有网络开销。
缺点:
1.不易存储,32位字符串。
2.信息不安全,基于 MAC 地址生成的 UUID,容易暴露出机器的 MAC 地址。
3.生成的ID无序,无法保证趋势递增。
snowflake算法
将64为的bit的long划分成多个部分,用来保证唯一性
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 00000 - 000000000000
1-bit为符号位都是0
41-bit的时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;
10-bit机器可以分别表示1024台机器
12-bit部分,支持同一毫秒内同一个节点可以生成4096个ID;
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
// 相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
// 同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
// 不同毫秒内,序列号置为0
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT // 时间戳部分
| datacenterId << DATACENTER_LEFT // 数据中心部分
| machineId << MACHINE_LEFT // 机器标识部分
| sequence; // 序列号部分
}
优点:
1、毫秒数在高位,自增序列在低位,整个ID都是趋势递增的
2、不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
3、可以根据自身业务特性分配bit位,非常灵活
缺点
1、强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态
因为如果修改了机器的时间就有可能导致时间戳重复
由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。
要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警
2、生成唯一标示较长
机器ID生产策略,可以利用redis 启动的时候设置唯一的ID,机器停机的时候再删除,这样就支持机器横向扩展了
for (int i = 1; i <= maxMachineId; i++) {
if (!jedis.exists(UNIQ_MACHINE_KEY + i)) {
jedis.set(UNIQ_MACHINE_KEY + i, i + "");
machineId = i;
log.info("add :{}", machineId);
break;
}
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
jedis.del(UNIQ_MACHINE_KEY + machineId);
log.info("del :{}", machineId);
}
});
数据库自增长
优点:
1、非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
2、ID号单调自增,可以实现一些对ID有特殊要求的业务。
缺点:
1、强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。
主从切换时的不一致可能会导致重复发号。
2、ID发号性能瓶颈限制在单台MySQL的读写性能。
Leaf-segment
在使用数据库的方案上,做了如下改变
1、原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力
2、各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行
+-------------+--------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| biz_tag | varchar(128) | NO | PRI | | |
| max_id | bigint(20) | NO | | 1 | |
| step | int(11) | NO | | NULL | |
| desc | varchar(256) | YES | | NULL | |
| update_time | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+
biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。
原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。
那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step
例如:
test_tag在第一台Leaf机器上是1~1000的号段,
第二台机器就是1000~2000号段
第三台机器就是2000~3000号段
当第一台机器号段用完时,会去加载另一个长度为step=1000的号段,假设另外两台号段都没有更新,
这个时候第一台机器新加载的号段就应该是3001~4000。同时数据库对应的biz_tag这条数据的max_id会从3000被更新成4000,更新号段的SQL语句如下:
Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit
优点:
1、容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
缺点:
1、TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺
双buffer优化
DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,
即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。
这样做就可以很大程度上的降低系统的TP999指标
采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。
当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复
参考
https://tech.meituan.com/2017/04/21/mt-leaf.html