分布式唯一Id(雪花算法——snowflake)

给大家分享分布式唯一id——雪花算法以及雪花算法的改进版

1.雪花算法介绍

2.传统分布式雪花算法java版

3.python版本

4.Snowflake 的其他变种

5.异常情况讨论

6.改进版snowflake

 

 

1.雪花算法介绍

Snowflake 生成的 unique ID 的组成 (由高位到低位):

41 bits: Timestamp (毫秒级)
10 bits: 节点 ID (datacenter ID 5 bits + worker ID 5 bits)
12 bits: sequence number
一共 63 bits (最高位是 0)

unique ID 生成过程:

10 bits 的机器号, 在 ID 分配 Worker 启动的时候, 从一个 Zookeeper 集群获取 (保证所有的 Worker 不会有重复的机器号)
41 bits 的 Timestamp: 每次要生成一个新 ID 的时候, 都会获取一下当前的 Timestamp, 然后分两种情况生成 sequence number:
如果当前的 Timestamp 和前一个已生成 ID 的 Timestamp 相同 (在同一毫秒中), 就用前一个 ID 的 sequence number + 1 作为新的 sequence number (12 bits); 如果本毫秒内的所有 ID 用完, 等到下一毫秒继续 (这个等待过程中, 不能分配出新的 ID)
如果当前的 Timestamp 比前一个 ID 的 Timestamp 大, 随机生成一个初始 sequence number (12 bits) 作为本毫秒内的第一个 sequence number
整个过程中, 只是在 Worker 启动的时候会对外部有依赖 (需要从 Zookeeper 获取 Worker 号), 之后就可以独立工作了, 做到了去中心化.



2.传统分布式雪花算法java版

/**

* Twitter_Snowflake<br>

* SnowFlake的结构如下(每部分用-分开):<br>

* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>

* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>

* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)

* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>

* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>

* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>

* 加起来刚好64位,为一个Long型。<br>

* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。

*/

public class SnowflakeIdWorker {

// ==============================Fields===========================================

/** 开始时间截 (2015-01-01) */

private final long twepoch = 1420041600000L;

/** 机器id所占的位数 */

private final long workerIdBits = 5L;

 

/** 数据标识id所占的位数 */

private final long datacenterIdBits = 5L;

 

/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */

private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

 

/** 支持的最大数据标识id,结果是31 */

private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

 

/** 序列在id中占的位数 */

private final long sequenceBits = 12L;

 

/** 机器ID向左移12位 */

private final long workerIdShift = sequenceBits;

 

/** 数据标识id向左移17位(12+5) */

private final long datacenterIdShift = sequenceBits + workerIdBits;

 

/** 时间截向左移22位(5+5+12) */

private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

 

/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */

private final long sequenceMask = -1L ^ (-1L << sequenceBits);

 

/** 工作机器ID(0~31) */

private long workerId;

 

/** 数据中心ID(0~31) */

private long datacenterId;

 

/** 毫秒内序列(0~4095) */

private long sequence = 0L;

 

/** 上次生成ID的时间截 */

private long lastTimestamp = -1L;

 

//==============================Constructors=====================================

/**

* 构造函数

* @param workerId 工作ID (0~31)

* @param datacenterId 数据中心ID (0~31)

*/

public SnowflakeIdWorker(long workerId, long datacenterId) {

if (workerId > maxWorkerId || workerId < 0) {

throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));

}

if (datacenterId > maxDatacenterId || datacenterId < 0) {

throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));

}

this.workerId = workerId;

this.datacenterId = datacenterId;

}

 

// ==============================Methods==========================================

/**

* 获得下一个ID (该方法是线程安全的)

* @return SnowflakeId

*/

public synchronized long nextId() {

long timestamp = timeGen();

 

//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常

if (timestamp < lastTimestamp) {

throw new RuntimeException(

String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));

}

 

//如果是同一时间生成的,则进行毫秒内序列

if (lastTimestamp == timestamp) {

sequence = (sequence + 1) & sequenceMask;

//毫秒内序列溢出

if (sequence == 0) {

//阻塞到下一个毫秒,获得新的时间戳

timestamp = tilNextMillis(lastTimestamp);

}

}

//时间戳改变,毫秒内序列重置

else {

sequence = 0L;

}

 

//上次生成ID的时间截

lastTimestamp = timestamp;

 

//移位并通过或运算拼到一起组成64位的ID

return ((timestamp - twepoch) << timestampLeftShift) //

| (datacenterId << datacenterIdShift) //

| (workerId << workerIdShift) //

| sequence;

}

 

/**

* 阻塞到下一个毫秒,直到获得新的时间戳

* @param lastTimestamp 上次生成ID的时间截

* @return 当前时间戳

*/

protected long tilNextMillis(long lastTimestamp) {

long timestamp = timeGen();

while (timestamp <= lastTimestamp) {

timestamp = timeGen();

}

return timestamp;

}

 

/**

* 返回以毫秒为单位的当前时间

* @return 当前时间(毫秒)

*/

protected long timeGen() {

return System.currentTimeMillis();

}

 

//==============================Test=============================================

/** 测试 */

public static void main(String[] args) {

SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);

for (int i = 0; i < 1000; i++) {

long id = idWorker.nextId();

System.out.println(Long.toBinaryString(id));

System.out.println(id);

}

}

}

========================================================================================

========================================================================================

3.python版本

 

import datetime

import time

import random

 

class SnowflakeId:

def __init__(self):

# twitter's snowflake parameters

self.twepoch = random.randint(1, 100)

self.datacenter_id_bits = 5

self.worker_id_bits = 5

self.sequence_id_bits = 12

# 1 << datacenter_id_bits

self.max_datacenter_id = 32

# 1 << worker_id_bits

self.max_worker_id = 32

# 1 << sequence_id_bits

self.max_sequence_id = 4096

# 1 << (64 - datacenter_id_bits - worker_id_bits - sequence_id_bits)

self.max_timestamp = 4398046511104

 

def make_snowflake(self, timestamp_ms, datacenter_id, worker_id, sequence_id,):

 

sid = ((int(timestamp_ms) - self.twepoch) % self.max_timestamp) << self.datacenter_id_bits << self.worker_id_bits << self.sequence_id_bits

sid += (datacenter_id % self.max_datacenter_id) << self.worker_id_bits << self.sequence_id_bits

sid += (worker_id % self.max_worker_id) << self.sequence_id_bits

sid += sequence_id % self.max_sequence_id

return sid

 

def melt(self, snowflake_id):

sequence_id = snowflake_id & (self.max_sequence_id - 1)

worker_id = (snowflake_id >> self.sequence_id_bits) & (self.max_worker_id - 1)

datacenter_id = (snowflake_id >> self.sequence_id_bits >> self.worker_id_bits) & (self.max_datacenter_id - 1)

timestamp_ms = snowflake_id >> self.sequence_id_bits >> self.worker_id_bits >> self.datacenter_id_bits

# timestamp_ms += self.twepoch

return (timestamp_ms, datacenter_id, worker_id, sequence_id)

 

def local_datetime(self, timestamp_ms):

return datetime.datetime.fromtimestamp(timestamp_ms / 1000.)

 

def handle(self):

t0 = int(time.time() * 1000)

uuid = self.melt(self.make_snowflake(t0, 1, 0, 0))[0]

return uuid

 

 

if __name__ == '__main__':

while 1:

uuid = SnowflakeId().handle()

print(uuid)

 

 

==============================================================================================

==============================================================================================

4.Snowflake 有一些变种, 各个应用结合自己的实际场景对 Snowflake 做了一些改动. 这里主要介绍 3 种.

  1. Boundary flake

变化:

ID 长度扩展到 128 bits:
最高 64 bits 时间戳;
然后是 48 bits 的 Worker 号 (和 Mac 地址一样长);
最后是 16 bits 的 Seq Number
由于它用 48 bits 作为 Worker ID, 和 Mac 地址的长度一样, 这样启动时不需要和 Zookeeper 通讯获取 Worker ID. 做到了完全的去中心化
基于 Erlang
它这样做的目的是用更多的 bits 实现更小的冲突概率, 这样就支持更多的 Worker 同时工作. 同时, 每毫秒能分配出更多的 ID

  1. Simpleflake

Simpleflake 的思路是取消 Worker 号, 保留 41 bits 的 Timestamp, 同时把 sequence number 扩展到 22 bits;

Simpleflake 的特点:

sequence number 完全靠随机产生 (这样也导致了生成的 ID 可能出现重复)
没有 Worker 号, 也就不需要和 Zookeeper 通讯, 实现了完全去中心化
Timestamp 保持和 Snowflake 一致, 今后可以无缝升级到 Snowflake
Simpleflake 的问题就是 sequence number 完全随机生成, 会导致生成的 ID 重复的可能. 这个生成 ID 重复的概率随着每秒生成的 ID 数的增长而增长.

所以, Simpleflake 的限制就是每秒生成的 ID 不能太多 (最好小于 100次/秒, 如果大于 100次/秒的场景, Simpleflake 就不适用了, 建议切换回 Snowflake).

  1. instagram 的做法

先简单介绍一下 instagram 的分布式存储方案:

先把每个 Table 划分为多个逻辑分片 (logic Shard), 逻辑分片的数量可以很大, 例如 2000 个逻辑分片
然后制定一个规则, 规定每个逻辑分片被存储到哪个数据库实例上面; 数据库实例不需要很多. 例如, 对有 2 个 PostgreSQL 实例的系统 (instagram 使用 PostgreSQL); 可以使用奇数逻辑分片存放到第一个数据库实例, 偶数逻辑分片存放到第二个数据库实例的规则
每个 Table 指定一个字段作为分片字段 (例如, 对用户表, 可以指定 uid 作为分片字段)
插入一个新的数据时, 先根据分片字段的值, 决定数据被分配到哪个逻辑分片 (logic Shard)
然后再根据 logic Shard 和 PostgreSQL 实例的对应关系, 确定这条数据应该被存放到哪台 PostgreSQL 实例上
instagram unique ID 的组成:

41 bits: Timestamp (毫秒)
13 bits: 每个 logic Shard 的代号 (最大支持 8 x 1024 个 logic Shards)
10 bits: sequence number; 每个 Shard 每毫秒最多可以生成 1024 个 ID
生成 unique ID 时, 41 bits 的 Timestamp 和 Snowflake 类似, 这里就不细说了.

主要介绍一下 13 bits 的 logic Shard 代号 和 10 bits 的 sequence number 怎么生成.

logic Shard 代号:

假设插入一条新的用户记录, 插入时, 根据 uid 来判断这条记录应该被插入到哪个 logic Shard 中.
假设当前要插入的记录会被插入到第 1341 号 logic Shard 中 (假设当前的这个 Table 一共有 2000 个 logic Shard)
新生成 ID 的 13 bits 段要填的就是 1341 这个数字
sequence number 利用 PostgreSQL 每个 Table 上的 auto-increment sequence 来生成:

如果当前表上已经有 5000 条记录, 那么这个表的下一个 auto-increment sequence 就是 5001 (直接调用 PL/PGSQL 提供的方法可以获取到)
然后把 这个 5001 对 1024 取模就得到了 10 bits 的 sequence number
instagram 这个方案的优势在于:

利用 logic Shard 号来替换 Snowflake 使用的 Worker 号, 就不需要到中心节点获取 Worker 号了. 做到了完全去中心化
另外一个附带的好处就是, 可以通过 ID 直接知道这条记录被存放在哪个 logic Shard 上
同时, 今后做数据迁移的时候, 也是按 logic Shard 为单位做数据迁移的, 所以这种做法也不会影响到今后的数据迁移。

 

 

5.异常情况讨论

在获取当前 Timestamp 时, 如果获取到的时间戳比前一个已生成 ID 的 Timestamp 还要小怎么办? Snowflake 的做法是继续获取当前机器的时间, 直到获取到更大的 Timestamp 才能继续工作 (在这个等待过程中, 不能分配出新的 ID)
从这个异常情况可以看出, 如果 Snowflake 所运行的那些机器时钟有大的偏差时, 整个 Snowflake 系统不能正常工作 (偏差得越多, 分配新 ID 时等待的时间越久)

从 Snowflake 的官方文档 (https://github.com/twitter/snowflake/#system-clock-dependency) 中也可以看到, 它明确要求 "You should use NTP to keep your system clock accurate". 而且最好把 NTP 配置成不会向后调整的模式. 也就是说, NTP 纠正时间时, 不会向后回拨机器时钟.

 

6.改进版的snowflake

1.机器码生成器 MachineIdService设计及其实现:

public interface MachineIdService {
    /**
     * 生成MachineId的方法
     *
     * @return machineId 机器码
     * @throws  MessageIdException 获取机器码可能因为外部因素失败
     */
    Long getMachineId() throws MessageIdException;
}

实现该接口确保一个集群中,每台实例生成不同的machineID,并且MachineID 不能超过(2^10) 1023,具体实现方式,可使用MySQL数据库,文件描述映射,Redis自增等方式,这里我使用了Redis自增的方式(所以在需要用到该ID生成器的地方需要依赖Redis),具体实现方式如下:

public class RedisMachineIdServiceImpl implements MachineIdService {

    private static final String MAX_ID = "MAX_ID";
    private static final String IP_MACHINE_ID_MAPPING = "IP_MACHINE_ID_MAPPING";

    private RedisTemplate<String, String> redisTemplate;


    private String redisKeyPrefix;

    //设置RedisTemplate实例
    public void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 设置redisKey前缀,如果多个业务使用同一个Redis集群,使用不同的Redis前缀进行区分
    public void setRedisKeyPrefix(String redisKeyPrefix) {
        this.redisKeyPrefix = redisKeyPrefix;
    }

    @Override
    public Long getMachineId() throws MessageIdException {
        String host;
        try {
            //获取本机IP地址
            host = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            throw new MessageIdException("Can not get the host!", e);
        }
        if (redisTemplate == null) {
            throw new MessageIdException("Can not get the redisTemplate instance!");
        }
        if (redisKeyPrefix == null) {
            throw new MessageIdException("The redis key prefix is null,please set redis key prefix first!");
        }
        HashOperations<String, String, Long> hashOperations = redisTemplate.opsForHash();
        //通过IP地址在Redis中的映射,找到本机的MachineId
        Long result = hashOperations.get(redisKeyPrefix + IP_MACHINE_ID_MAPPING, host);
        if (result != null) {
            return result;
        }
        //如果没有找到,说明需要对该实例进行新增MachineId,使用Redis的自增函数,生成一个新的MachineId
        Long incrementResult = redisTemplate.opsForValue().increment(redisKeyPrefix + MAX_ID, 1L);
        if (incrementResult == null) {
            throw new MessageIdException("Get the machine id failed,please check the redis environment!");
        }
        //将生成的MachineId放入Redis中,方便下次查找映射
        hashOperations.put(redisKeyPrefix + IP_MACHINE_ID_MAPPING, host, incrementResult);
        return incrementResult;
    }
}

2.MessageIdService设计以及实现

public interface MessageIdService {

    /**
     * 生成一个保证全局唯一的MessageId
     *
     * @return messageId
     */
    long genMessageId();

    /**
     * 初始化方法
     *
     * @throws MessageIdException
     */
    void init() throws MessageIdException;
}
public class MessageIdServiceImpl implements MessageIdService {

    private static final Logger LOGGER = LoggerFactory.getLogger(MessageIdServiceImpl.class);
    //最大的MachineId,1024个
    private static final long MAX_MACHINE_ID = 1023L;
    //AtomicLongArray 环的大小,可保存200毫秒内,每个毫秒数上一次的MessageId,时间回退的时候依赖与此
    private static final int CAPACITY = 200;
    // 时间戳在messageId中左移的位数
    private static final int TIMESTAMP_SHIFT_COUNT = 22;
    // 机器码在messageId中左移的位数
    private static final int MACHINE_ID_SHIFT_COUNT = 12;
    // 序列号的掩码 2^12 4096
    private static final long SEQUENCE_MASK = 4095L;

    //messageId ,开始的时间戳,start the world,世界初始之日
    private static long START_THE_WORLD_MILLIS;
    //机器码变量
    private long machineId;
    // messageId环,解决时间回退的关键,亦可在多线程情况下减少毫秒数切换的竞争
    private AtomicLongArray messageIdCycle = new AtomicLongArray(CAPACITY);
    //生成MachineIds的实例
    private MachineIdService machineIdService;

    static {
        try {
            //使用一个固定的时间作为start the world的初始值
            START_THE_WORLD_MILLIS = SimpleDateFormat.getDateTimeInstance().parse("2018-09-13 00:00:00").getTime();
        } catch (ParseException e) {
            throw new RuntimeException("init start the world millis failed", e);
        }
    }

    public void setMachineIdService(MachineIdService machineIdService) {
        this.machineIdService = machineIdService;
    }

    /**
     * init方法中通过machineIdService 获取本机的machineId
     * @throws MessageIdException
     */
    @Override
    public void init() throws MessageIdException {
        if (machineId == 0L) {
            machineId = machineIdService.getMachineId();
        }
        //获取的machineId 不能超过最大值
        if (machineId <= 0L || machineId > MAX_MACHINE_ID) {
            throw new MessageIdException("the machine id is out of range,it must between 1 and 1023");
        }
    }
    /**
     * 核心实现的代码
     */
    @Override
    public long genMessageId() {
        do {
            // 获取当前时间戳,此时间戳是当前时间减去start the world的毫秒数
            long timestamp = System.currentTimeMillis() - START_THE_WORLD_MILLIS;
            // 获取当前时间在messageIdCycle 中的下标,用于获取环中上一个MessageId
            int index = (int)(timestamp % CAPACITY);
            long messageIdInCycle = messageIdCycle.get(index);
            //通过在messageIdCycle 获取到的messageIdInCycle,计算上一个MessageId的时间戳
            long timestampInCycle = messageIdInCycle >> TIMESTAMP_SHIFT_COUNT;
            // 如果timestampInCycle 并没有设置时间戳,或时间戳小于当前时间,认为需要设置新的时间戳
            if (messageIdInCycle == 0 || timestampInCycle < timestamp) {
                long messageId = timestamp << TIMESTAMP_SHIFT_COUNT | machineId << MACHINE_ID_SHIFT_COUNT;
                // 使用CAS的方式保证在该条件下,messageId 不被重复
                if (messageIdCycle.compareAndSet(index, messageIdInCycle, messageId)) {
                    return messageId;
                }
                LOGGER.debug("messageId cycle CAS1 failed");
            }
            // 如果当前时间戳与messageIdCycle的时间戳相等,使用环中的序列号+1的方式,生成新的序列号
            // 如果发生了时间回退的情况,(即timestampInCycle > timestamp的情况)那么不能也更新messageIdCycle 的时间戳,使用Cycle中MessageId+1
            if (timestampInCycle >= timestamp) {
                long sequence = messageIdInCycle & SEQUENCE_MASK;
                if (sequence >= SEQUENCE_MASK) {
                    LOGGER.debug("over sequence mask :{}", sequence);
                    continue;
                }
                long messageId = messageIdInCycle + 1L;
                // 使用CAS的方式保证在该条件下,messageId 不被重复
                if (messageIdCycle.compareAndSet(index, messageIdInCycle, messageId)) {
                    return messageId;
                }
                LOGGER.debug("messageId cycle CAS2 failed");
            }
            // 整个生成过程中,采用的spinLock
        } while (true);
    }

}

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值