分布式ID

分布式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