前言

我们的数据库在设计时一般有两个ID,自增的id为主键,还有一个业务ID使用UUID生成。自增id在需要分表的情况下做为业务主键不太理想,所以我们增加了uuid作为业务ID,有了业务id仍然还存在自增id的原因具体我也说不清楚,只知道和插入的性能以及db的要求有关。

我个人一直想将这两个ID换成一个字段来处理,所以要求这个id是数字类似的,且是趋抛增长的,这样mysql创建索引以及查询时性能会比较好。于时网上找到了雪花算法.关于雪花算法大家可以看一下我后面引用的资料。

ID生成器代码:

从网上抄的,自己改的,目前我还没有应用到实际项目中,如需应用,请先进行严格自测


import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

/**
 * <p>
 *  在雪花算法基础生稍做改造生成Long Id
 *  https://www.jianshu.com/p/d3881a6a895e
 * </p>
 * 1 - 41位 - 10位 - 12位
 * 0 - 41位 - 10位 - 12位
 * <p>
 * <PRE>
 * <BR>    修改记录
 * <BR>-----------------------------------------------
 * <BR>    修改日期         修改人          修改内容
 * </PRE>
 *
 * @author cuiyh9
 * @version 1.0
 * @Date Created in 2018年11月29日 20:46
 * @since 1.0
 */
public final class ZfIdGenerator {

    /**
     * 起始的时间戳
     */
    private static final long START_TIME_MILLIS;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long WORKID_BIT = 10;   //机器标识占用的位数

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

    /**
     * 每一部分向左的位移
     */
    private final static long WORKID_SHIFT = SEQUENCE_BIT;
    private final static long TIMESTMP_SHIFT = WORKID_SHIFT + WORKID_BIT;

    private long sequence = 0L; //序列号
    private long lastStmp = -1L;

    /** workId */
    private long workId;

    static {
        String startDate = "2018-01-01 00:00:00";
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime localDateTime = LocalDateTime.parse(startDate, df);
        START_TIME_MILLIS = localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();

    }




    /**
     * 获取分部署式发号器
     * @param workId 每台服务需要传一个服务id
     * @return
     */
    public static synchronized ZfIdGenerator getDistributedIdGenerator(long workId) {
        return new ZfIdGenerator(workId);
    }

    public static synchronized ZfIdGenerator getStandAloneIdGenerator() {
        long workId = MAX_WORK_NUM;
        return new ZfIdGenerator(workId);
    }


    private ZfIdGenerator(long workId) {
        if (workId > MAX_WORK_NUM || workId <= 0) {
            throw  new RuntimeException("workdId的值设置错误");
        }
        this.workId = workId;
    }

    /**
     * 生成id
     * @return
     */
    public synchronized long nextId() {
        long currStmp = System.currentTimeMillis();
        if (currStmp < START_TIME_MILLIS) {
            throw new RuntimeException("机器时间存在问题,请注意查看");
        }

        if (currStmp == lastStmp) {
            sequence =  (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0L) {
                currStmp = getNextMillis(currStmp);
            }
        } else {
            sequence = 0L;
        }
        lastStmp = currStmp;

        return ((currStmp - START_TIME_MILLIS) << TIMESTMP_SHIFT)
                | (workId << WORKID_SHIFT)
                | (sequence);
    }

    public long getNextMillis(long currStmp) {
        long millis = System.currentTimeMillis();
        while (millis <= currStmp) {
            millis = System.currentTimeMillis();
        }
        return millis;
    }

    /**
     * 获取最大的工作数量
     * @return
     */
    public static long getMaxWorkNum() {
        return MAX_WORK_NUM;
    }

    public static void main(String[] args) {
        ZfIdGenerator idGenerator1 = ZfIdGenerator.getDistributedIdGenerator(1);
//        ZfIdGenerator idGenerator2 = ZfIdGenerator.getDistributedIdGenerator(2);
        for (int i = 0; i < 1000000; i++) {
            System.out.println(idGenerator1.nextId());
        }

//        System.out.println(idGenerator2.nextId());



    }

}

分布式情况

上面的ID生成器在单机情况下使用没有问题,但如果在分布下使用,就需要分配不同的workId,如果workId相同,可能会导致生成的id相同。

解决方案:

1、使用java环境变量,人为通过-D预先设置workid.这种方案简单,不会出现重复情况,但需要每个服务的启动脚本不同.

2、使用sharding-jdbc中的算法,使用IP后几位来做workId,这种方案也很简单,不需要修改服务的启动脚本,但在某些情况下会出现生成重复ID的情况,详细见我下面的参考资料

3、使用zk,在启动时给每个服务分配不同的workId,缺点:多了依赖,需要zk,优点:不会出现重复情况,且不需要修改服务的启动脚本。这个是我个人使用的方案,实现思路为,系统启动时创建一个永久性的结点(zookeeper保证原子性),然后在这个永久性的节点下,遍历workId去zookeeper创建临时结点,zookeeper会保证相同路径只会有一个可能创建成功,如果创建失败继续遍历即可。详细可看一下代码

实例化ID生成器如下(Spring boot项目):


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;

/**
 * <p>TODO</p>
 * <p>
 * <PRE>
 * <BR>    修改记录
 * <BR>-----------------------------------------------
 * <BR>    修改日期         修改人          修改内容
 * </PRE>
 *
 * @author cuiyh9
 * @version 1.0
 * @Date Created in 2018年11月30日 16:37
 * @since 1.0
 */
@Slf4j
@SpringBootConfiguration
public class IdGeneratorConfig {

    @Autowired
    private ZkClient zkClient;

    @Value("${idgenerator.zookeeper.parent.path}")
    private String IDGENERATOR_PARENT_PATH;

    @Bean
    public ZfIdGenerator idGenerator() {
        boolean flag = zkClient.createParent(IDGENERATOR_PARENT_PATH);
        if (!flag) {
            throw new RuntimeException("创建发号器父节点失败");
        }

        // 获取workId
        long workId = 0;
        long maxWorkNum = ZfIdGenerator.getMaxWorkNum();
        for (long i = 1; i < maxWorkNum; i++) {
            String workPath = IDGENERATOR_PARENT_PATH + "/" + i;
            flag = zkClient.createNotExistEphemeralNode(workPath);
            if (flag) {
                workId =  i;
                break;
            }
        }

        if (workId == 0) {
            throw new RuntimeException("获取机器id失败");
        }
        log.warn("idGenerator workId:{}", workId);
        return ZfIdGenerator.getDistributedIdGenerator(workId);

    }
}

 

ZkClient代码(基于apache curator)

注意apache curator版本,我最初使用的是4.x版本,程序执行到forPath()方法就会阻塞,后来查到是与zookeeper版本不匹配导致.


import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * <p>TODO</p>
 * <p>
 * <PRE>
 * <BR>    修改记录
 * <BR>-----------------------------------------------
 * <BR>    修改日期         修改人          修改内容
 * </PRE>
 *
 * @author cuiyh9
 * @version 1.0
 * @Date Created in 2018年11月30日 16:36
 * @since 1.0
 */
@Slf4j
@Component
public class ZkClient {

    @Autowired
    private CuratorFramework client;

    /**
     * 创建父节点,创建成功或存在都返回成功
     * @param path
     * @return
     */
    public boolean createParent(String path) {
        try {
            client.create().creatingParentsIfNeeded().forPath(path);
            return true;
        } catch (KeeperException.NodeExistsException e) {
            return true;
        } catch (Exception e) {
            log.error("createParent fail path:{}", path, e);
        }
        return false;
    }

    /**
     * 创建不存在的节点。如果存在或创建失败,返回false
     * @param path
     * @throws Exception
     */
    public boolean createNotExistEphemeralNode(String path) {
        try {
            client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
            return true;
        } catch (KeeperException.NodeExistsException e) {
            return false;
        }  catch (Exception e) {
            log.error("createNotExistNode fail path:{}", path, e);
        }
        return false;
    }
}

 

参考资料:

1、https://blog.csdn.net/x5fnncxzq4/article/details/79549514
2、https://segmentfault.com/a/1190000011282426#articleHeader2
3、https://www.cnblogs.com/relucent/p/4955340.html
4、https://blog.csdn.net/boshuzhang/article/details/72677454
5、https://www.jianshu.com/p/d3881a6a895e
6、https://blog.csdn.net/u012557538/article/details/53318436
7、https://segmentfault.com/a/1190000011282426#articleHeader2

Logo

更多推荐