孙宇的技术专栏 大数据/机器学习

分布式ID生成

2015-07-09

阅读:


自增ID是数据库中最熟悉不过的功能了。在小型应用中,一台数据库承托业务,没有问题。在中小型业务里采用读写分离,主/从结构(一主一从,一主多从,甚至带中继的主从),单表的自增ID逻辑上没问题,但性能上就受制于单台写入机器,扩展性差,容错性也不理想。但在大型应用中,通常要用到分库,分表。如何让各库各表的ID在时间上是递增的,且不重复。这就成为一个课题。

DB自增

数据库自身就有 auto_increment 的功能。它能保证全局的唯一性和递增性质。而且保证每次递增的步长一致。但受制于该机器的性能,也不易扩展。这时候可以有两种方法:

按范围水平切分

将分水平切分成 N 个,比如 表 1 的ID的初始值设置为 0, 表 2 的值设为 1 亿,表 3 为 2 亿。 这样,保证了ID的唯一性,但从整体来看ID 不是递增的。因为表 2 中的ID肯定都比表 1 中的大,而且是大不少。

按步长水平切分

第二种方式是分成 N 个表,但表的递增步长设置为 N。比如有 3 个表,则将步长设置为 3。这样表1 中的ID可能是:1, 4, 7…表2 中则是:2, 5, 8…;表3 中是: 3,6,9…。

这样也是保证了全局的唯一,同时在一定程度上保证了递增性。但在局步上ID的递增性不那么严谨。而且如果要扩展,也不那么方便。需要重新设置步长。

自建服务

由于使用数据库自身的自增功能有这么多限制。则可以考虑自建ID生成服务。我们自己创建,维护全局的ID,这里又有几种方法:

利用自增

单独建一张表,专门用来管理ID。但表里只存一个值,即当前ID的最大值。如果现在有服务要插入数据,先请求ID;从该表中得知当前ID的最大值是 100。这时候ID生成器生成 N 个ID,如 101, 102—110,共 10 个ID,并将它记在内存中。然后返回 101 给服务,并将表里的最大值设置为 110。下一个请求过来时则直接从内存中返回 102 。当ID用完后,再次查库,并生成新一批ID。

这种做法的好处是对数据库的请求要少一些。因为使用数据库自增ID时,每次生成ID,它都会有一次数据库的请求。这里利用了ID池的做法,减少了请求。

但该方法的问题在于如果ID生成的服务器崩溃了,提前生成放在池中的ID就消失了。下次请求的时候会漏掉一些ID。比如现在池中有 102,103,104,105,106。ID表中记的最大值是 106。如果这时候服务器挂了,再申请ID时,就会从 106 往上分配。而 102 - 106 则漏了。但这种情况对业务的影响不大。

为了解决ID生成器的单点故障情况,可以使用主备的结构。如使用 keepalive 。

时间毫秒数

如果不利用数据库来辅助生成ID,可以自己设计一种自增的,且唯一的算法。

显然和时间相关的算法是最合适的。当前的时间肯定是递增的,但要保证唯一性,就需要时间尽量精确,所以这里用了毫秒。而且时间可以表现成数字,正好用来做ID,用来做索引的查询效率很高,而且在本地就可以生成ID,不用额外请求ID生成服务。

但在同一毫秒下如果还有多次请求(每秒超过 1000 次请求),那就需要在毫秒数后再额外加一些识别的值,以减少重复的机率。比如增加 N 位的随机数字等。但比较有名的是 twitter 开源的 snowflake 算法。

snowflake

twitter 的做法是用毫秒再加上许多其它额外的数据进行拼接。它使用的是一个 Long 型的整数,一共 64 位。其中 第 1 位不用,后 41 位存毫秒数,10 位是机器编号,12 位作为毫秒内的序列号。

将毫秒数放在最高位,保证生成的ID是趋势递增的。在局部来看,ID也不是绝对的递增。比如在同一秒内如果有多次请求,这一秒内的ID可能不是时间上的先后对应ID的大小。

这种算法,41 位的毫秒数,可以存储 241 个值,即 2.19亿亿个。而我们以 50 年为限,需要的ID是 50 * 365 * 24 * 3600 * 1000 = 1,576,800,000,000。光靠 41 位的毫秒数就足够生成 50 年用的ID了。10 位的机器编码则表示可以有 210 = 1024 台机器。 12 位的毫秒内序列号表示同一毫秒内可以支持 212个ID。通常是不会有这么多的。

注意:通常在分表时,我们会用ID取模来决定分在哪个表。为了分表时的均匀,ID生成的算法产生的ID的最后一位要足够的平均分布。所以上面的逻辑中,把毫秒内序列号放在最后。甚至我们可以在后面再加一个随机 0-9 的值,专门用来应对分库分表。

参照代码:

class IdWork
{

    //开始时间,固定一个小于当前时间的毫秒数即可
    const twepoch = 1420070400000;//2015/01/01 0:0:0

    //机器标识占的位数
    const workerIdBits = 10;

    //毫秒内自增数点的位数
    const sequenceBits = 12;

    protected $workId = 0;

    //要用静态变量
    static $lastTimestamp = -1;
    static $sequence = 0;


    function __construct($workId)
    {
        //机器ID范围判断
        $maxWorkerId = -1 ^ (-1 << self::workerIdBits);
        if ($workId > $maxWorkerId || $workId < 0) {
            throw new Exception("机器编号不能大于 " . maxWorkerId . " 或小于 0");
        }
        //赋值
        $this->workId = $workId;
    }

    //生成一个ID
    public function nextId()
    {
        $timestamp = $this->timeGen();
        $lastTimestamp = self::$lastTimestamp;
        //判断时钟是否正常
        if ($timestamp < $lastTimestamp) {
            throw new Exception("时光无法倒流,机器上时间错误!");
        }
        //生成唯一序列
        if ($lastTimestamp == $timestamp) {
            $sequenceMask = -1 ^ (-1 << self::sequenceBits);
            // 同一微秒中的序列号
            self::$sequence = (self::$sequence + 1) & $sequenceMask;
            if (self::$sequence == 0) {
                $timestamp = $this->tilNextMillis($lastTimestamp);
            }
        } else {
            self::$sequence = 0;
        }
        self::$lastTimestamp = $timestamp;
        //
        //时间毫秒/机器ID,要左移的位数
        $timestampLeftShift = self::sequenceBits + self::workerIdBits;
        $workerIdShift = self::sequenceBits;
        //组合3段数据返回: 时间戳.工作机器.序列
        $nextId = (($timestamp - self::twepoch) << $timestampLeftShift) | ($this->workId << $workerIdShift) | self::$sequence;
        return $nextId;
    }

    //取当前时间毫秒
    protected function timeGen()
    {
        $timestramp = (float)sprintf("%.0f", microtime(true) * 1000);
        return $timestramp;
    }

    //取下一毫秒
    protected function tilNextMillis($lastTimestamp)
    {
        $timestamp = $this->timeGen();
        while ($timestamp <= $lastTimestamp) {
            $timestamp = $this->timeGen();
        }
        return $timestamp;
    }

}

$work = new IdWork(1023);
for ($i = 0; $i < 10; $i++) {
    $id = $work->nextId();
    echo $id . PHP_EOL;
}

JAVA 版本:

/**
 * 描述: Twitter的分布式自增ID雪花算法snowflake (Java版)
 *
 **/
public class SnowFlake {

    /**
     * 起始的时间戳
     */
    private final static long START_STMP = 1480166465631L;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     *
     * @return
     */
    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;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

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

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(2, 3);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            System.out.println(snowFlake.nextId());
        }

        System.out.println(System.currentTimeMillis() - start);


    }
}

Similar Posts

下一篇 nginx+lua

评论