分布式ID生成

几乎所有的业务系统,都会有很多表记录,都有生成一个记录标识的需求,或者直接使用数据自带的自增键,或者自己开发(一般大公司有中间件部门提供组件或服务),作为工程师也是我们要掌握的技能,过往实践中,碰到过不少ID生成场景,如:

  • 数据量不大(数据在千万以下),写入并发未达到数据库上限,建单表,主键使用数据库的auto_increment足矣;
  • 业务日志,数据写入海量(每天可能达到百亿级,如果到家的日志平台-守望者,阿里的鹰眼等),写入并发高,主键适合使用UUID;
  • 数据量大(如订单 交易 积分等),使用了分库分表,ID的生成要求:生成全局唯一,高性能,容灾,可扩展,这里就需要分布式ID;
  • 其他:有脱敏性要求的,防竞品收集商家数量 订单数量 交易笔数等商业机密,要求ID散列,如商家ID,订单ID,交易ID 不宜使用数据库的自增键。

分布式ID,顾名思义在分布式环境下生成全局唯一的ID。有twitter提出的Snowflake(雪花)算法,Flicker算法,有在Flicker思想上进一步的数据库+本地缓存算法。

Snowflake

算法

image.png

算法理解起来还是比较简单:

  1. 41 bits: Timestamp(毫秒级)
  2. 10 bits:机器ID(一般是单元机房ID 5 bits + 机器ID 5 bits),也说是逻辑分片的l
  3. 12 bits: 可单机生成12位bit的自增序列

JAVA实现

/**
 * Created by 登程 on 2018/1/24.
 */
public class Snowflake {
    /**
     * 机器ID占用位数
     */
    private static final int WORK_ID_BIT_SIZE = 10;
    /**
     * 机器ID最大数
     */
    private static final int WORK_ID_MAX_VAL = -1 ^ (-1 << WORK_ID_BIT_SIZE);
    private int workId;
    /**
     * 序列占用最大位数
     */
    private static final int SEQUENCE_BIT_SIZE = 12;
    /**
     * 序列最大值(4098)
     */
    private static final int SEQUENCE_MAX_VAL = -1 ^ (-1 << SEQUENCE_BIT_SIZE);
    private int sequence = 0;
    private long lastTimestamp = 0L;
    /**
     * 最小日期1970-01-01
     */
    private static final long MIN_TIME = 1288834974657L;

    public Snowflake(int workId) {
        if (workId > WORK_ID_MAX_VAL || workId < 0) {
            throw new IllegalArgumentException("workId参数错误");
        }
        this.workId = workId;
    }

    public synchronized long nextId() {
        long currentTime = getTimestamp();
        if (lastTimestamp > currentTime) {
            throw new IllegalArgumentException("time clock is error!");
        }
        if (currentTime == lastTimestamp) {
            this.sequence = (this.sequence + 1) & SEQUENCE_MAX_VAL;
            if (sequence > SEQUENCE_MAX_VAL) {
                throw new IllegalArgumentException("并发量大,sequence溢出!");
            }
        } else {
            this.sequence = 0;
            lastTimestamp = currentTime;
        }

        return ((lastTimestamp - MIN_TIME) << (WORK_ID_BIT_SIZE+SEQUENCE_BIT_SIZE)) | (this.workId << WORK_ID_BIT_SIZE) | sequence;
    }

    private long getTimestamp() {
        return System.currentTimeMillis();
    }
}

测试代码如下

/**
 * Created by 登程 on 2018/1/25.
 */
public class TestSnowflake {
    private static ExecutorService threadPool = Executors.newFixedThreadPool(200);
    private static Snowflake snowflake2 = new Snowflake(11);
    private static Snowflake snowflake1 = new Snowflake(10);
    public static void main(String[] args) {

        for (int i = 0; i < 100000; i++) {
            threadPool.execute(new IdGenThread(snowflake2));
            threadPool.execute(new IdGenThread(snowflake1));
        }
        threadPool.shutdown();
        while (!threadPool.isTerminated()){
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * Created by 登程 on 2018/1/25.
 */
public class IdGenThread implements Runnable {
    private Snowflake snowflake;

    IdGenThread(Snowflake snowflake) {
        this.snowflake = snowflake;
    }

    @Override
    public void run() {
        System.out.println(snowflake.nextId());
    }
}

优点缺点

优点和缺点都明显

优点

  • 生成Long型易操作,有序
  • 性能高效(单机支持4098个并发生成ID,对于大部分业务来说足够了)

缺点

  • 维护成本:维护机器ID(大公司有统一机器部署服务,单元机房和机器很容易读取到,小公司维护起来较为麻烦,可能需要在每台机器上序号)
  • 没有一个全局时钟,难以保证时序

Flicker算法

不同分库设置不同的起始值和步长。Flicker启用了两台数据库服务器来生成ID,通过区分auto_increment的起始值和步长来生成奇偶数的ID。

数据库配置

Sequence Server1:
auto-increment-increment = 2
auto-increment-offset = 1
 
Sequence Server2:
auto-increment-increment = 2
auto-increment-offset = 2
image.png

如何路由到不同的数据库上,做到负载均衡:如果保证自增顺序,可以考虑利用ZK或redis等做分布式锁来保证;不考虑自增顺序,可以考虑RPC类似的负载原理。

  • 优势:利用mysql自增id
  • 缺点:运维成本比较高,数据扩容时需要重新设置步长;数据库的写压力依然很大,每次生成ID都要访问数据库。

基于数据库更新+内存分配

在数据库中维护一个ID,获取下一个ID时,会对数据库进行ID=ID+{步长} WHERE ID=XX,如果更新成功,则表示可以拿到{步长} 个ID,在内存中进行分配,单机最多可生产{步长} 个ID,超时或者达到{步长} 个ID后,则继续对数据库进行获取。
在分布式环境下,容灾和扩容是需要考虑,概括起来就是:生成id的数据库可以是多机,其中的一个或者多个数据库挂了,不能影响id获取,保证高可用.

利用多库的id生成方案: 每个数据库只拿自己的那一段id. 如下图,sample_db_1-sample_db_4是用来生成全局唯一id的4个数据库(一般在业务数据库中建立,可保证同生共死),那么每个数据库对于同一个id有一个起始值,比如步长是1000.

数据库 取值范围(步长:1000)
sample_db_1 0
sample_db_2 1000
sample_db_3 2000
sample_db_4 3000

应用某台服务器获取ID的时候,随机取到了sample_db_2,那么这台机器会拿到2000-2999这一千个id(初始值是2000,在单机内存中将其视为临界资源保证自增),当然而这个时候4个数据库上id起始值会变成下图所示:

数据库 取值范围(步长:1000)
sample_db_1 0
sample_db_2 5000
sample_db_3 2000
sample_db_4 3000

   注意到,下次从sample_db_2上取得的id就变成了5000-5999. 这样,完全避免了多机上取id的重复.比如sample_db_2只会取到1000-1999,5000-5999,9000-9999, 13000-13999…sample_db_1,sample_db_3,sample_db_4其他数据库取值范围也一样,这样就不会相互重叠,数据唯一性得到保证。
   如果扩容,如将数据库扩展成5个(建议设定初始值,避免重试多次),sample_db_2的取值范围就会是1000-1999,6000-6999,11000-11999...一个小段时间存在2台服务器有生成统一ID的可能,建议重启机器并做重试(有重复异常时,取ID),这样问题解决。

优势:高可用
缺点:无法保证自增顺序;需要根据合适的写入QPS来判断步长。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,061评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,407评论 3 404
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,275评论 0 368
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,084评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,091评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,555评论 1 315
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,914评论 3 429
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,900评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,438评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,470评论 3 346
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,596评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,187评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,932评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,361评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,511评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,163评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,671评论 2 366

推荐阅读更多精彩内容