zookeeper和snowflake结合生成分布式唯一ID

文章目录

    • 背景
    • 将traceId写入日志
    • 如何获取traceId
      • Java 自带UUID
      • 开源解决方案
    • 如何生成工作机器ID
    • 生成traceId整体逻辑
    • 参考文档

背景

为便于问题定位,最近想通过日志记录每个HTTP请求的链路,即通过记录在日志内的traceId快速查出该HTTP请求对应的所有业务日志。对于争分夺秒的问题定位,依据traceId来定位问题与通过time + threadId相比,可大大解约定位问题时间。

将traceId写入日志

  • 通过拦截器拦截HTTP请求;
  • 在拦截器中通过MDC将traceId放入到ThreadLocal中,并在logBack.xml中配置
MDC.put("traceId", String.valueOf(snowflakeService.getId()));
        
            [%-5p] [%d{yyyy-MM-dd HH:mm:ss}] [%t] %X{traceId} [%C{1}:%M:%L] %m%n
        

如何获取traceId

获取traceId的方法有很多,主要有:

Java 自带UUID

  • 优点
    简单方便,直接使用;无额外依赖,可保证分布式系统生成唯一traceId
String traceId = UUID.randomUUID().toString();
  • 缺点
    通过UUID生成的traceId有36位无规律字符串,在将其中四位’-'剔除后为32位字符串;该traceId一方面无规律,另一方面太长

开源解决方案

  • 美团leaf
  • 百度uid-generator
  • twitter Snowflake(雪花)算法及其开源实现

这些开源的ID生成方案多数借鉴了twitter算法来进行了具体的实现,美团和百度的开源方案比较完备,另一方面也增加了复杂度,对接入不太有好。
zookeeper和snowflake结合生成分布式唯一ID_第1张图片

目前网络上已有的方案多数集中在traceId生成,对于如何生成工作ID描述不多。

如何生成工作机器ID

本文通过zookeeper临时顺序节点的方式生成机器ID,生成机器ID的代码逻辑如下:

    // 每次机器启动时生成机器ID
    private void init() {
        //ZooKeeper客户端
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

        client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
        client.start();
        String nodeName = "/IDMaker/ID-";
        // 每次重新启动服务时才需要重新生成机器ID
        workerId = Long.valueOf(makeId(nodeName));
    }
    
    ## 生成临时顺序节点,从上图看最多客容纳1024台机器,因此需要对获取的节点序号进行模1024取余
    public Long makeId(String nodeName) {
        String str = createSeqNode(nodeName);
        if (null == str) {
            return null;
        }
        int index = str.lastIndexOf(nodeName);
        if (index >= 0) {
            index += nodeName.length();
            str = index <= str.length() ? str.substring(index) : "";
        }
        return Long.valueOf(str) % 1024;
    }
    
    // 使用zookeeper临时顺序节点特定生成递增机器ID
    private String createSeqNode(String pathPefix) {
        try {
            // 创建一个 ZNode 顺序节点
            String destPath = client.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)//避免zookeeper的顺序节点暴增,可以删除创建的顺序节点
                    .forPath(pathPefix);
            return destPath;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

由于本程序对zookeeper有依赖,运行上述程序的前提是需要部署zookeeper服务,只有这样才可以正常生成分布式traceId,部署zookeeper可参考Zookeeper入门

生成traceId整体逻辑

@Service
public class IDMaker implements InitializingBean {

    private final static Logger logger = LoggerFactory.getLogger(IDMaker.class);

    private long workerId;
    private final long epoch = 1575817859683L;   // 时间起始标记点,作为基准,一般取系统的最近时间
    private final long workerIdBits = 10L;      // 机器标识位数
    private long sequence = 0L;                   // 0,并发控制
    private final long sequenceBits = 12L;      //毫秒内自增位

    private final long workerIdShift = this.sequenceBits;                             // 12
    private final long timestampLeftShift = this.sequenceBits + this.workerIdBits;// 22
    private final long sequenceMask = -1L ^ -1L << this.sequenceBits;                 // 4095,111111111111,12位
    private long lastTimestamp = -1L;

    CuratorFramework client = null;

    @Override
    public void afterPropertiesSet() {
        init();
    }

    private void init() {
        //ZooKeeper客户端
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

        client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
        client.start();
        String nodeName = "/IDMaker/ID-";
        workerId = Long.valueOf(makeId(nodeName));
    }

    private String createSeqNode(String pathPefix) {
        try {
            // 创建一个 ZNode 顺序节点
            String destPath = client.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)//避免zookeeper的顺序节点暴增,可以删除创建的顺序节点
                    .forPath(pathPefix);
            return destPath;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public Long makeId(String nodeName) {
        String str = createSeqNode(nodeName);
        if (null == str) {
            return null;
        }
        int index = str.lastIndexOf(nodeName);
        if (index >= 0) {
            index += nodeName.length();
            str = index <= str.length() ? str.substring(index) : "";
        }
        return Long.valueOf(str) % 1024;
    }

    public synchronized long nextId() throws Exception {
        long timestamp = this.timeGen();
        if (this.lastTimestamp == timestamp) { // 如果上一个timestamp与新产生的相等,则sequence加一(0-4095循环); 对新的timestamp,sequence从0开始
            this.sequence = this.sequence + 1 & this.sequenceMask;
            if (this.sequence == 0) {
                timestamp = this.tilNextMillis(this.lastTimestamp);// 重新生成timestamp
            }
        } else {
            this.sequence = 0;
        }

        if (timestamp < this.lastTimestamp) {
            logger.error(String.format("clock moved backwards.Refusing to generate id for %d milliseconds", (this.lastTimestamp - timestamp)));
            throw new Exception(String.format("clock moved backwards.Refusing to generate id for %d milliseconds", (this.lastTimestamp - timestamp)));
        }

        this.lastTimestamp = timestamp;
        return timestamp - this.epoch << this.timestampLeftShift | this.workerId << this.workerIdShift | this.sequence;
    }

    /**
     * 等待下一个毫秒的到来, 保证返回的毫秒数在参数lastTimestamp之后
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    /**
     * 获得系统当前毫秒数
     */
    private static long timeGen() {
        return System.currentTimeMillis();
    }
}

生成的traceId如下图所示:
zookeeper和snowflake结合生成分布式唯一ID_第2张图片

参考文档

  • 分布式唯一 ID 之 Snowflake 算法
  • adyliu/idcenter
  • zookeeper入门

你可能感兴趣的:(web,分布式技术)