雪花算法生成订单ID

一、用数据库主键自增生成订单ID

数据库主键顺序自增,每天有多少订单量被竞争对手看得一清二楚,商业机密都暴露了。 况且单机MySQL只能支持几百量级的并发,如果系统每天千万订单量,完全hold不住。

二、用数据库集群

数据库集群,自增ID起始值按机器编号,步长等于机器数量。比如有两台机器,第一台机器生成的ID是1、3、5、7,第二台机器生成的ID是2、4、6、8。性能不行就加机器,这并发量一下就上去了。

但实现百万级的并发,大概就需要2000台机器,这还只是用来生成订单ID,公司再有钱也经不起这么造。

三、号段模式

然MySQL的并发量不行,我们是不是可以提前从MySQL获取一批自增ID,加载到本地内存中,然后从内存中并发取。这种叫号段模式。并发量是上去了,但是自增ID还是不能作为订单ID的。

四、用Java自带UUID

import java.util.UUID;

/**
 * @author yideng
 * @apiNote UUID示例
 */
public class UUIDTest {
    public static void main(String[] args) {
        String orderId = UUID.randomUUID().toString().replace("-", "");
        System.out.println(orderId);
    }
}

输出结果:

58e93ecab9c64295b15f7f4661edcbc1

也不行。32位字符串会占用更大的空间,无序的字符串作数据库主键,每次插入数据库的时候,MySQL为了维护B+树结构,需要频繁调整节点顺序,影响性能。况且字符串太长,也没有任何业务含义,pass。

五、生成订单ID要满足的条件

  • 全局唯一:如果订单ID重复了,肯定要完蛋。

  • 高性能:要做到高并发、低延迟。生成订单ID都成为瓶颈了,那还得了。

  • 高可用:至少要做到4个9,别动不动就宕机了。

  • 易用性:如果为了满足上述要求,搞了几百台服务器,复杂且难以维护,也不行。

  • 数值且有序递增:数值占用的空间更小,有序递增能保证插入MySQL的时候更高性能。

  • 嵌入业务含义:如果订单ID里面能嵌入业务含义,就能通过订单ID知道是哪个业务线生成的,便于排查问题。

六、雪花算法生成订单ID

一种流传已久的分布式、高性能、高可用的订单ID生成算法—雪花算法,完全能满足你的上述要求。雪花算法生成ID是Long类型,长度64位。

雪花算法
  • 第 1 位: 符号位,暂时不用。
  • 第 2~42 位:共41位,时间戳,单位是毫秒,可以支撑大约69年
  • 第 43~52 位:共10位,机器ID,最多可容纳1024台机器
  • 第 53~64 位:共12位,序列号,是自增值,表示同一毫秒内产生的ID,单台机器每毫秒最多可生成4096个订单ID

代码实现:

package com.ac.member.config.mybatis;

import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

@Slf4j
public class SnowFlake {

    /** 初始偏移时间戳 */
    private static final long OFFSET = 1546300800L;

    /** 机器id (0~15 保留 16~31作为备份机器) */
    private static final long WORKER_ID;
    /** 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32)*/
    private static final long WORKER_ID_BITS = 5L;
    /** 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = 65536) */
    private static final long SEQUENCE_ID_BITS = 16L;
    /** 机器id偏移位数 */
    private static final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS;
    /** 自增序列偏移位数 */
    private static final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS;
    /** 机器标识最大值 (2^5 / 2 - 1 = 15) */
    private static final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1;
    /** 备份机器ID开始位置 (2^5 / 2 = 16) */
    private static final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1;
    /** 自增序列最大值 (2^16 - 1 = 65535) */
    private static final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1;
    /** 发生时间回拨时容忍的最大回拨时间 (秒) */
    private static final long BACK_TIME_MAX = 1000L;

    /** 上次生成ID的时间戳 (秒) */
    private static long lastTimestamp = 0L;
    /** 当前秒内序列 (2^16)*/
    private static long sequence = 0L;
    /** 备份机器上次生成ID的时间戳 (秒) */
    private static long lastTimestampBak = 0L;
    /** 备份机器当前秒内序列 (2^16)*/
    private static long sequenceBak = 0L;

    static {
        // 初始化机器ID
        long workerId = getWorkId();
        if (workerId < 0 || workerId > WORKER_ID_MAX) {
            throw new IllegalArgumentException(String.format("cmallshop.workerId范围: 0 ~ %d 目前: %d", WORKER_ID_MAX, workerId));
        }
        WORKER_ID = workerId;
    }

    private static Long getWorkId(){
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for(int b : ints){
                sums += b;
            }
            return (long)(sums % WORKER_ID_MAX);
        } catch (UnknownHostException e) {
            // 如果获取失败,则使用随机数备用
            return RandomUtils.nextLong(0,WORKER_ID_MAX-1);
        }
    }


    /** 私有构造函数禁止外部访问 */
    private SnowFlake() {}

    /**
     * 获取自增序列
     * @return long
     */
    public static long nextId() {
        return nextId(SystemClock.now() / 1000);
    }

    /**
     * 主机器自增序列
     * @param timestamp 当前Unix时间戳
     * @return long
     */
    private static synchronized long nextId(long timestamp) {
        // 时钟回拨检查
        if (timestamp < lastTimestamp) {
            // 发生时钟回拨
            log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp);
            return nextIdBackup(timestamp);
        }

        // 开始下一秒
        if (timestamp != lastTimestamp) {
            lastTimestamp = timestamp;
            sequence = 0L;
        }
        if (0L == (++sequence & SEQUENCE_MAX)) {
            // 秒内序列用尽
//            log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp);
            sequence--;
            return nextIdBackup(timestamp);
        }

        return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence;
    }

    /**
     * 备份机器自增序列
     * @param timestamp timestamp 当前Unix时间戳
     * @return long
     */
    private static long nextIdBackup(long timestamp) {
        if (timestamp < lastTimestampBak) {
            if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) {
                timestamp = lastTimestampBak;
            } else {
                throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak));
            }
        }

        if (timestamp != lastTimestampBak) {
            lastTimestampBak = timestamp;
            sequenceBak = 0L;
        }

        if (0L == (++sequenceBak & SEQUENCE_MAX)) {
            // 秒内序列用尽
//            logger.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp);
            return nextIdBackup(timestamp + 1);
        }

        return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak;
    }


    /**
     * 并发数
     */
    private static final int THREAD_NUM = 30000;
    private static volatile CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);

    public static void main(String[] args) {
        ConcurrentHashMap<Long,Long> map = new ConcurrentHashMap<>(THREAD_NUM);
        List<Long> list = Collections.synchronizedList(new LinkedList<>());

        for (int i = 0; i < THREAD_NUM; i++) {
            Thread thread = new Thread(() -> {
                // 所有的线程在这里等待
                try {
                    countDownLatch.await();

                    Long id = SnowFlake.nextId();
                    list.add(id);
                    map.put(id,1L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });

            thread.start();
            // 启动后,倒计时计数器减一,代表有一个线程准备就绪了
            countDownLatch.countDown();
        }

        try{
            Thread.sleep(50000);
        }catch (Exception e){
            e.printStackTrace();
        }

        System.out.println("listSize:"+list.size());
        System.out.println("mapSize:"+map.size());
        System.out.println(map.size() == THREAD_NUM);
    }
}

与mybatis-plus结合

import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class CustomIdGenerator implements IdentifierGenerator {
    
    @Override
    public Long nextId(Object entity) {
        return SnowFlake.nextId();
    }
}

接入非常简单,不需要搭建服务集群。代码逻辑非常简单,同一毫秒内,订单ID的序列号自增。同步锁只作用于本机,机器之间互不影响,每毫秒可以4百万的订单ID,非常强悍。

生成规则不是固定的,可以根据自身的业务需求调整。如果你不需要那么大的并发量,可以把机器标识位拆出一部分,当作业务标识位,标识是哪个业务线生成的订单ID。

6.1 雪花算法优化

雪花算法严重依赖系统时钟。如果时钟回拨,就会生成重复ID。

比如美团的Leaf(美团自研一种分布式ID生成系统),为了解决时钟回拨,引入了zookeeper,原理也很简单,就是比较当前系统时间跟生成节点的时间。

有的对并发要求更高的系统,比如双十一秒杀,每毫秒4百万并发还不能满足要求,就可以使用雪花算法和号段模式相结合,比如百度的UidGenerator、滴滴的TinyId。想想也是,号段模式的预先生成ID肯定是高性能分布式订单ID的最终解决方案。

转载自:面试官竟然问我订单ID是怎么生成的?难道不是MySQL自增主键?

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

推荐阅读更多精彩内容