分布式全局唯一ID生成器(支持多种注册中心)的实现

分布式全局唯一ID生成器(支持多种注册中心)的实现在订单、支付的业务场景中,单号的生成规则与生成方式十分重要,实现有很多种,最简单的是基于mysql自增主键实现,方案优劣不多说,大家都清楚。我们今天实现一种分布式的、可扩展的并且在高并发场景能保证高性能的全局唯一ID生成方案(基于twitter的snowflake原理进行改编和扩展)。不多说直接上代码。packagecom.zxm.adapter;importorg.apache.z…

大家好,又见面了,我是你们的朋友全栈君。

在订单、支付的业务场景中,单号的生成规则与生成方式十分重要,实现有很多种,最简单的是基于mysql自增主键实现,方案优劣不多说,大家都清楚。我们今天实现一种分布式的、可扩展的并且在高并发场景能保证高性能的全局唯一ID生成方案(基于twitter的snowflake原理进行改编和扩展)。不多说直接上代码。

package com.zxm.adapter;

import org.apache.zookeeper.KeeperException;

/**
 * @Author zxm
 * @Description 注册适配器
 * @Date Create in 上午 9:45 2019/4/12 0012
 */
public interface RegistryAdapter {
    long getWorkerId() throws KeeperException, InterruptedException;
}
package com.zxm.adapter;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;

import java.io.IOException;

/**
 * @Author zxm
 * @Description 基于zk的注册适配器
 * @Date Create in 上午 9:44 2019/4/12 0012
 */
@Slf4j
public class ZkRegistryAdapter implements RegistryAdapter {
    private static final int DEFAULT_SESSION_TIMEOUT = 3000;
    private static final String ROOT_NODE = "/idWorker";

    private static final int DEFAULT_MOD_VALUE = 1024;
    private ZooKeeper zkClient;

    public ZkRegistryAdapter(String connectString) throws Exception {
        this(connectString, DEFAULT_SESSION_TIMEOUT);
    }

    public ZkRegistryAdapter(String connectString, int sessionTimeOut) throws Exception {
        try {
            zkClient = new ZooKeeper(connectString, sessionTimeOut, watchedEvent -> log.info("path:{}, state:{}", watchedEvent.getPath(), watchedEvent.getState()));
            initRootNode(zkClient);
        } catch (IOException e) {
            log.error("zookeeper connect error,url:{},errorMsg:{}", connectString, e.getMessage());
            throw e;
        }
    }

    private void initRootNode(ZooKeeper zkClient) throws KeeperException, InterruptedException {
        if (zkClient.exists(ROOT_NODE, false) == null) {
            String path = zkClient.create(ROOT_NODE, "idWorker".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            if (StringUtils.isNotBlank(path)) {
                log.info("root node init success,path:{}", path);
            }
        }
    }

    @Override
    public long getWorkerId() throws KeeperException, InterruptedException {
        String path = zkClient.create(ROOT_NODE + "/_", "idWorker".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        if (StringUtils.isNotBlank(path)) {
            log.info("node create success,path:{}", path);
            return Long.valueOf(path.substring(ROOT_NODE.length() + 2, path.length())) % DEFAULT_MOD_VALUE;
        }
        return -1;
    }
}
package com.zxm.core;

import com.zxm.adapter.RegistryAdapter;
import lombok.extern.slf4j.Slf4j;

/**
 * Id_Worker<br>
 * Id_Worker的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 0000000000 - 0000000 <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位的机器位(最多支持1023台机器)<br>
 * 12位序列,毫秒内的计数,7位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 */
@Slf4j
public class IdWorker {

    /**
     * 开始时间截 (2018-01-01 00:00:00)
     */
    private static final long twepoch = 1514736000000L;

    /**
     * 机器id所占的位数
     */
    private static final long workerIdBits = 10L;

    /**
     * 序列在id中占的位数
     */
    private static final long sequenceBits = 12L;


    private static final long workerIdShift = sequenceBits;

    /**
     * 时间截向左移22位(12+10)
     */
    private static final long timestampLeftShift = sequenceBits + workerIdBits;

    /**
     * 生成序列的掩码,这里为4095
     */
    private static final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /**
     * 工作机器ID(0~1023)
     */
    private static long workerId;

    /**
     * 毫秒内序列(0~4095)
     */
    private static long sequence = 0L;

    /**
     * 上次生成ID的时间截
     */
    private static long lastTimestamp = -1L;

    public IdWorker(RegistryAdapter registryAdapter) throws Exception {
        if (null == registryAdapter) {
            throw new Exception("registryAdapter init fail");
        }
        workerId = registryAdapter.getWorkerId();
        log.info("GLOBAL_WORkER_ID INIT:" + workerId);
    }

    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return id
     */
    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) //
                | (workerId << workerIdShift)
                | exchangeSequence(sequence);
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected static long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     *
     * @return 当前时间(毫秒)
     */
    protected static long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * 打乱自增序列
     *
     * @param sequence
     * @return
     * @since
     */
    protected static long exchangeSequence(long sequence) {
        String tmp = Long.toBinaryString(sequence | (1 << 12));
        StringBuffer sb = new StringBuffer(tmp.substring(1));
        long sqr = Long.parseLong(sb.reverse().toString(), 2) & sequenceMask;
        return sqr;
    }
}

测试生成结果:

package com.zxm;

import com.zxm.adapter.RegistryAdapter;
import com.zxm.adapter.ZkRegistryAdapter;
import com.zxm.core.IdWorker;

/**
 * Hello world!
 */
public class App {
    public static void main(String[] args) throws Exception {
        RegistryAdapter registryAdapter = new ZkRegistryAdapter("10.10.4.17:2181", 3000);
        Thread.sleep(10 * 1000);
        IdWorker idWorker = new IdWorker(registryAdapter);

        for (int i = 0; i < 10; i++) {
            System.out.println(idWorker.nextId());
        }
    }
}

分布式全局唯一ID生成器(支持多种注册中心)的实现

在spring项目中使用时配置如下:

@Configuration  
 public class IdWorkerConfig {  
     @Bean
     public RegistryAdapter registryAdapter() throws Exception{
         return new ZkRegistryAdapter("10.10.4.4:2181", 3000);
     }
 
     @Bean
     public IdWorker idWorker() throws Exception{
         return new IdWorker(registryAdapter());
     }
 }  

测试用例:

public class IdWorkerTest{
    @Autowire
    private IdWorker idWorker;
    
    @Test
    public void test(){
        for (int i = 0; i < 10; i++) {
            System.out.println(idWorker.nextId());
        }
    }
}

执行结果同上。

项目地址:https://github.com/zhangxiaomin1993/id-worker

项目后续会逐渐完善,代码仅供大家学习参考!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/151708.html原文链接:https://javaforall.net

赞 (0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 解决Mplayer播放srt、sub字幕乱码的方法

    解决Mplayer播放srt、sub字幕乱码的方法网上看到解决Mplayer播放srt、sub字幕乱码的方法,试了下,srt可以,效果也不错,虽然字体有点大。sub还没有试,可能还是不行,记得以前摆弄过。1.srt字幕要设置正确编码才行右键菜单—-属性设置—–字幕和OSD,在字幕一栏选择编码:简体中文(CP936),再在下面的多选框中选上”unicode字幕”这项.再在属性设置—–字体下编码选unico

    2022年7月11日
  • 中级JAVA:简单介绍下Ajax「建议收藏」

    中级JAVA:简单介绍下Ajax「建议收藏」首先Ajax是AsynchronousJavaScriptandXML的全称,Asynchronous是异步的意思,这跟传统的web不同。什么是同步什么是异步?同步是发送方发送数据之后,必须等接收方接收数据做出回应之后,才可以进行下一步。异步则是发送方发送数据之后不需等接收方做出回应,可以进行下一步操作。.

    2022年8月26日
  • Anaconda–Pycharm安装教程[通俗易懂]

    Anaconda–Pycharm安装教程[通俗易懂]因为不想每次下载安装的时候都要去找博客,干脆就参考别人的博客,把自己需要的写下来了。1、Anaconda的安装从官网https://www.anaconda.com/download/下载Anaconda的安装程序,在该页面选择你电脑所对应的系统(Windows、macOSorLinux)以及操作系统位数(64位or32位)。选择Python3.8,等待下载完成,之后便会得到一个anaconda的exe可执行文件。![在这里插入图片描述](https://img-blog.csd

    2022年8月28日
  • html5表格内容怎么居中_html表格上下居中

    html5表格内容怎么居中_html表格上下居中回答:IE6/7及IE8混杂模式中,text-align:center可以使块级元素也居中对齐。其他浏览器中,text-align:center仅作用于行内内容上。解决这个问题比较好的方式,就是为所有需要相对父容器居中对齐的块级元素设置“margin-left:Auto;margin-right:Auto”。但这个方式IE6/IE7/IE8的混杂模式中不支持,所以还要设置父容器的”text…

    2022年9月14日
  • ant-design-vue文档_antdesign原型设计

    ant-design-vue文档_antdesign原型设计antdesignofvue文件上传action设置上传的地址headers设置上传的请求头部fileList接收已经上传的文件列表(受控)可以控制文件数量事件change记录上传文件改变时的状态,当status为‘done’时将文件列表的地址存到表单中,随表单提交至后台数据库具体用法参考antdesign官网:https://www.antdv.com/components/upload-cn/…

    2022年8月15日
  • Thread.IsBackground

    Thread.IsBackground解释:  获取或设置一个值,该值指示某个线程是否为后台线程。  后台线程不会防止应用程序的进程被终止掉。        注意:主线程中创建了一个线程,线程的IsBackground默认是设置为FALSE的。主线程退出时,IsBackground=FALSE线程继续执行下去,直到线程执行结束。IsBackground=TRUE的线程才会随着主线程的退出…

    2022年10月17日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号