分布式全局唯一id——Snowflake算法

1 问题引出——为什么会用到全局唯一ID

     一开始我是拒绝学习这个知识的,因为MySQL明明就有自增属性AUTO_INCREMENT,何必多此一举、大费周章,最后就出来一个id,是否舍近求远、舍本逐末?

    答案是:对,也不对。

    说对,是因为一般来讲,用数据库自增字段应付得来了;

    说不对,是因为要考虑到扩展性,一旦数据量太大需要扩容、分库分表的时候就炸了,当然你也可以在分表时从上一个表的末尾值开始自增,但终归是要强依赖数据库,扩展性和性能都不行。

 

2 有哪些方法可以生成全局唯一ID

2.1 UUID(不可行)

性能差、太长、占用空间大、不能反解析出信息、无序性导致B+树索引在写的时候有过多随机写操作。

2.2 基于DB表实现

方法就是上面问题引出里介绍的。

从分布式的角度来说,如果请求量过大,还会导致单表性能问题。解决这个问题的一个好办法是分库分表,每个表从不同的数字开始自增、使用相同的步长。

 

假设我们分出来3张表:

  初始值 步长 生成的id
表1 0 3

0、3、6、9、...

表2 1 3

1、4、7、10、...

表3 2 3

2、5、8、11、...

使用的时候做下负载均衡就好了,常用的负载均衡算法:随机、轮询、最小活跃数、一致性哈希。

 

看起来自增解决了、分布式负载均衡也照顾到了,可是为何仍弃之不用?

因为这里把可提供id的服务节点数固定住了,例如这里只能是这3个表对外提供,再加一个都不行。而且仍然是数据库的实现。

 

3 分布式全局唯一ID

语言:Java

算法:Twitter的Snowflake算法

机器ID:数据库实现

 

算法介绍:

即将生成的分布式全局唯一ID的结构:

分布式全局唯一id——Snowflake算法_第1张图片

 

总长度64位,从低位到高位依次划分为:

1)0~11位(共12bit)表示序列号,最大值2^12=4096,意味着在一个时间单位(我们用毫秒,当然你也可以用秒)内最多可以生成4096个ID;

2)12~21位(共10bit)表示机器id,最大值2^10=1024,意味着可以在1024台机器上部署我们的算法,当然了,像我所在的团队,一个应用能有4台机器就是“富农”了,6台都能成“地主”了,所以丝毫不用担心。

3)22~62位(共41bit)表示时间戳,最大值2^41=2 199 023 255 552(单位:ms),意味着在这么多时间内我们可以肆意妄为地制造ID。是多久呢?一年按365天算,2^41 / 1000 / 3600 / 24 / 365 ≈ 69.7(年)。系统运行之前我们设置一个起始时间,例如“2019-2-21 00:00:00”,然后从此时开始算,差不多能用到2088年。

4) 63位(共1bit)最高位设置为0,不用,说是二进制里面最高位是1的是负数,我们用0表示正数就好了。

3.1 生成分布式全局唯一ID

 

/**
 * 全局唯一id生成器
 * global unique id generator
 */
public interface IdGenerator {
    T generateId();
}
/**
 * 基于workId的全局唯一Id生成器
 */
public abstract class BaseWorkIdIdGenerator implements IdGenerator {

    @Resource(name = "dbWorkIdResolver")
    private WorkIdResolver workIdResolver;

    protected Long getWorkId() {
        return workIdResolver.resolveWorkId();
    }

}
import org.springframework.stereotype.Service;

import java.util.Calendar;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * 

* 产生long类型的唯一id,基于Twitter的snow flake算法实现,单台机器每毫秒支持2^12=4096个id * *

* 第1位为0,符号位。第2-42位表示毫秒数,共41位,当前时间毫秒-2017年04月01日的毫秒数。第43-52位表示workId,即机器id,共10位,能支持1024台机器。第53-64位表示序列号,共12位 */ @Service("commonIdGenerator") public class CommonIdGenerator extends BaseWorkIdIdGenerator { public static final long START_TIME_MILLIS; private static final long SEQUENCE_BITS = 12L; // 12位序列号 private static final long WORKER_ID_BITS = 10L; // 10位workId号 private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1; private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS; private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS; private long sequence; private long lastTime; private Lock lock = new ReentrantLock(); static { Calendar calendar = Calendar.getInstance(); calendar.set(2018, Calendar.FEBRUARY, 21); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); START_TIME_MILLIS = calendar.getTimeInMillis(); // 从2019.02.21开始 } @Override public Long generateId() { // time long currentTime; // seq long seq; // 此处加锁也可以使用synchronized关键字,用来在多线程并发执行时保护lastTime、sequence这两个变量 lock.lock(); try { currentTime = System.currentTimeMillis(); //时钟被回拨,直接拒绝服务 if (currentTime < lastTime) { throw new IllegalStateException("Clock go back, refused generator guid service."); } if (currentTime == lastTime) { //如果1ms内单台机器的4096个序号用完了,等待下一毫秒 if (0L == (sequence = ++sequence & SEQUENCE_MASK)) { lastTime = waitUntilNextMillis(currentTime); } } else { lastTime = currentTime; sequence = 0; } currentTime = lastTime; seq = sequence; }finally { lock.unlock(); } return ((currentTime - START_TIME_MILLIS) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkId() << WORKER_ID_LEFT_SHIFT_BITS) | seq; } private long waitUntilNextMillis(final long fromMills) { long nextMills = System.currentTimeMillis(); while (nextMills <= fromMills) { nextMills = System.currentTimeMillis(); } return nextMills; } }

我们先看看以上3大段代码,暂不去看机器id(workId)的生成方法,等下会单独讲解。

刚开始看到锁这个东西,我愣了一分钟,我记得这东西是在同一个JVM里面才管用,分布式不是要用分布式锁吗?我甚至想了下分布式锁的实现方式有:数据库、Redis、Zookeeper几种。

然后才恍然大悟、拍手叫绝,此处只要保证在单机上线程安全就行了,因为不同机器使用了机器id来区分,所以不同机器不会生成相同的id,分布式一致性得到了保证。

1)59-61行,当前时间小于上一次发号结束时间,那肯定是时钟被回拨了,这里直接抛异常拒绝服务,不然可能会产生重复id,因为回到了历史时间点。

2)65行是判断这个时间段内4096个号发完了,要等下一个时间段才能继续生成。我们看下是怎么判断号有没有发完的:

0L == (sequence = ++sequence & SEQUENCE_MASK)

而 SEENCE_MASK是多少呢,看看代码,计算下,是2^12 - 1 = 4095,转换成二进制:111 111 111 111

最初的时候,currentTime取当前时间,long类型的lastTime值为0,if (currentTime == lastTime)这个条件判断结果是false,所以会走lastTime = currentTime;sequence = 0;将lastTime赋值为当前时间、sequence设为0并返回。

从下次开始,lastTime有值了,就会走if分支,然后里面会校验4096个号码有没有发完(1~4095,0已经发过了)。

(++sequence) 从1开始,1 & SEQUENCE_MASK=1

  000 000 000 001

& 111 111 111 111

---------------------------

000 000 000 001 (1)

一直到4095:

  111 111 111 111

& 111 111 111 111

---------------------------

011 111 111 111 (4095)

到4096的时候又会回到0:

  1 000 000 000 000

&   111 111 111 111

---------------------------

  0 000 000 000 000 (0)

如此一轮下来正好是4096个号(0-4095)。

3)78行,把结果转换成十进制的数字

((currentTime - START_TIME_MILLIS) << TIMESTAMP_LEFT_SHIFT_BITS)

| (getWorkId() << WORKER_ID_LEFT_SHIFT_BITS) | seq

我们知道,起始时间是2019-02-21 00:00:00,START_TIME_MILLIS转换成毫秒就是:1550678400000,假设现在是2019-02-22 13:34:23,则currentTime对应的毫秒是:1550813663000,减去初始值:1550813663000-1550678400000=135263000,转换成二进制:1000000011111111001100011000,然后左移12+10=22位,得到时间戳:

1000000011111111001100011000 0 000 000 000 000 000 000 000

getWorkId() 我们假设当前机器号是1,转成二进制:1,再左移12位:

0 000 000 001 000 000 000 000

seq 假设当前生成的序号是1024,转成二进制:

010 000 000 000

最后进行逻辑或运算:

  1000000011111111001100011000 0 000 000 000 000 000 000 000

|                              0 000 000 001 000 000 000 000

|                                            010 000 000 000

----------------------------------------------------------

  1000000011111111001100011000 0 000 000 001 010 000 000 000

(十进制:567334141957120)

如此,我们就得到了全局唯一ID:567334141957120。

注意下,因为生成ID的算法强依赖于机器时间,所以禁止回拨时钟,我们代码里对于回拨时钟、未回归到拨回时间前的时间段是拒绝服务的。要是先回拨时间、再重启下应用,就没法判断是否回拨了,这样就有可能生成重复id,所以说禁止任何情况下回拨时钟,是基本要求。

 

3.2 获取机器ID

 

对应的数据库表请参考另一篇博客:https://blog.csdn.net/u010266988/article/details/87870647

package service.guid;

/**
 *
 * 本机workId持有者
 */
public interface WorkIdResolver {

    long resolveWorkId();
}
@Service("dbWorkIdResolver")
public class DbWorkIdResolver implements WorkIdResolver {
    private volatile Long workId;

    private static final String KEY = "GUID_WORK_ID";

    @Autowired
    private KeyValuePOMapperExt keyValuePOMapper;

    @Resource
    private WorkIdService workIdService;

    /**
     * 返回0 ~ 1023之间的workId,最大支持1024台机器
     *
     * @return
     */
    @Override
    public long resolveWorkId() {
        return workId;
    }

    @PostConstruct
    private void init() {
        workId = workIdService.generateWorkId();
    }
}
@Service
public class WorkIdService {
    private static final String KEY = "GUID_WORK_ID";

    /**
     * 默认最近使用的workId是0
     */
    private static final Long DEFAULT_LAST_WORK_ID = 0L;

    @Autowired
    private KeyValuePOMapperExt keyValuePOMapper;

    @Transactional
    public Long generateWorkId() {
        String ipAddr = NetwokUtils.getLocalhost();
        KeyValuePOExample keyValuePOExample = new KeyValuePOExample();
        keyValuePOExample.createCriteria().andKeyEqualTo(KEY).andBizTypeEqualTo(KeyValueBizTypeEnum.DEFAULT);
        List keyValuePOList = keyValuePOMapper.selectByExample(keyValuePOExample);

        if (CollectionUtils.isEmpty(keyValuePOList)) {
            // 数据库表里没数据,直接插入
            WorkIdData workIdData = new WorkIdData();
            Map workIdsMap = new HashMap<>(1);
            workIdsMap.put(ipAddr, DEFAULT_LAST_WORK_ID);
            workIdData.setWorkIdsMap(workIdsMap);
            workIdData.setLastWorkId(DEFAULT_LAST_WORK_ID);

            KeyValuePO insertPO = new KeyValuePO();
            insertPO.setKey(KEY);
            insertPO.setBizType(KeyValueBizTypeEnum.DEFAULT);
            insertPO.setValue(JSON.toJSONString(workIdData));
            keyValuePOMapper.insertSelective(insertPO);
            return DEFAULT_LAST_WORK_ID;
        }

        // 数据库表里有数据,先取出来,然后看看有没有保存当前IP地址
        KeyValuePO keyValuePO = keyValuePOList.get(0);
        WorkIdData workIdData =  JSON.parseObject(keyValuePO.getValue(), WorkIdData.class);
        Map workIdsMap = workIdData.getWorkIdsMap();

        if (workIdsMap != null && workIdsMap.containsKey(ipAddr)) {
            // 已经保存了当前ip,直接返回对应的workId
            return workIdsMap.get(ipAddr);
        }

        // 没有保存当前ip,把ip插进去
        if (workIdsMap == null) {
            workIdsMap = new HashMap<>();
        }
        long newLastId = workIdData.getLastWorkId() + 1;
        workIdData.setLastWorkId(newLastId);
        workIdsMap.put(ipAddr, newLastId);
        KeyValuePO updatePO = new KeyValuePO();
        updatePO.setValue(JSON.toJSONString(workIdData));
        keyValuePOExample.getOredCriteria().get(0).andDbUpdateTimeEqualTo(keyValuePO.getDbUpdateTime());
        keyValuePOMapper.updateByExampleSelective(updatePO, keyValuePOExample);
        return newLastId;
    }
}

假设事先塞了两个ip,此时会把本机ip作为新的worker加进去:

分布式全局唯一id——Snowflake算法_第2张图片

4 反解析全局唯一ID

把生成的GUID解析出来:

先定义下GUID结构:

/**
 * @Description 全局唯一id数据结构
 * @Author lilong
 * @Date 2019-02-21 14:44
 */
public class GuidBO {
    /**
     * 生成id的时间戳
     */
    private Timestamp lockTime;

    /**
     * 机器id
     */
    private Long workId;

    /**
     * 机器ip地址
     */
    private String workIpAddr;

    /**
     * 生成的序列号
     */
    private Long sequence;

   // getter/setter
}

反解析GUID:

@Service("commonIdGenerator")
public class CommonIdGenerator extends BaseWorkIdIdGenerator {
    public static final long START_TIME_MILLIS;
    private static final long SEQUENCE_BITS = 12L; // 12位序列号
    private static final long WORKER_ID_BITS = 10L; // 10位workId号
    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
    private static final long WORK_ID_MASK = (1 << WORKER_ID_BITS) - 1; // 10位workId掩码
    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;

    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2017, Calendar.APRIL, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        START_TIME_MILLIS = calendar.getTimeInMillis(); // 从2017.04.01开始
    }

    @Override
    public Long generateId() {
        ...
    }

    @Override
    public GuidBO parseGUID(Long id) {
        GuidBO guidBO = new GuidBO();

        //1.时间戳
        long generateTimeLong = (id >> TIMESTAMP_LEFT_SHIFT_BITS) + START_TIME_MILLIS;
        guidBO.setLockTime(new Timestamp(generateTimeLong));

        //2.机器号
        Long workId = (id >> SEQUENCE_BITS) & WORK_ID_MASK;
        guidBO.setWorkId(workId);

        //3.机器ip
        guidBO.setWorkIpAddr(parseWorkerIp(workId));

        //4.序列号
        guidBO.setSequence(id & SEQUENCE_MASK);

        return guidBO;
    }
}

5 测试

public class CommonIdGeneratorTest extends BaseTest {
    @Resource
    private CommonIdGenerator commonIdGenerator;

    @Test
    public void testGenerateId() {
        long guid = commonIdGenerator.generateId();
        System.out.println("############## guid:" + guid);

        GuidBO guidBO = commonIdGenerator.parseGUID(guid);
        System.out.println("############## guidBO:" + JSON.toJSONString(guidBO));
    }
}

测试结果:

分布式全局唯一id——Snowflake算法_第3张图片

lockTime转换成时间:2019-02-24 03:45:37(时间戳工具:https://tool.lu/timestamp/)

6 其他

美团Leaf开源:https://github.com/Meituan-Dianping/Leaf

介绍:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html

 

 

 

你可能感兴趣的:(分布式)