微服务下雪花算法生成方案

背景:微服务架构,需要有全局唯一的分布式id,使用UUID性能太差,可读性太差,数据存储无规律,替换成snowflakes。网上的实现方法也有,我现在是基于redis生成了一套暂时可用的生成器,同时也是参考了其他朋友的代码。

正题:

SnowFlakeProperties,读取配置属性信息

SnowFlake:雪花算法生成类

MachineIdConfig:机器id生成配置类


@Slf4j
@Configuration
@AutoConfigureBefore(SnowFlakeProperties.class)
@ConditionalOnClass({SnowFlakeProperties.class,SnowFlake.class})
public class MachineIdConfig {
    @Resource
    private RedisTemplate<Object,Object> redisTemplate;

    private SnowFlakeProperties properties;

    /**
     * 机器id
     */
    public static Integer machineId;
    /**
     * 本地ip地址
     */
    private static String localIp;

    /**
     * 获取ip地址
     *
     * @return
     * @throws UnknownHostException
     */
    private String getIPAddress() {
        return NetUtil.getLocalhostStr();
    }

    @ConditionalOnBean(SnowFlakeProperties.class)
    @Bean
    public SnowFlakeProperties getProperties(){
        return new SnowFlakeProperties();
    }

    /**
     * hash机器IP初始化一个机器ID
     */
    @ConditionalOnBean(SnowFlakeProperties.class)
    @Bean
    public SnowFlake initMachineId() {
        properties = getProperties();
        localIp = getIPAddress();
        long  ipHash = Math.abs(localIp.hashCode()) + properties.getPort();
        machineId = (int) ipHash % 32;
        // 创建一个机器ID
        createMachineId();
        log.info("初始化 machine_id :{}", machineId);
        return new SnowFlake(machineId, properties.getDataCenterId());
    }


    /**
     * 容器销毁前清除注册记录
     */
    @PreDestroy
    public void destroyMachineId() {
        try  {
            redisTemplate.delete(properties.getAppName() + properties.getDataCenterId() + machineId);
        }catch (Exception ex){
            log.error("destroyMachineId failed");
        }
    }


    /**
     * 主方法:首先获取机器 IP 并 % 32 得到 0-31
     * 使用 业务名 + 组名 + IP 作为 Redis 的 key,机器IP作为 value,存储到Redis中
     *
     * @return
     */
    public Integer createMachineId() {
        try {
            // 向redis注册,并设置超时时间
            log.info("注册一个机器ID到Redis " + machineId + " IP:" + localIp);
            Boolean flag = registerMachine(machineId, localIp);
            // 注册成功
            if (flag) {
                // 启动一个线程更新超时时间
                updateExpTimeThread();
                // 返回机器Id
                log.info("Redis中端口没有冲突 " + machineId + " IP:" + localIp);
                return machineId;
            }
            // 注册失败,可能原因 Hash%32 的结果冲突
            if (!checkIfCanRegister()) {
                // 如果 0-31 已经用完,使用 32-64之间随机的ID
                getRandomMachineId();
                createMachineId();
            } else {
                // 如果存在剩余的ID
                log.warn("Redis中端口冲突了,使用 0-31 之间未占用的Id " + machineId + " IP:" + localIp);
                createMachineId();
            }
        } catch (Exception e) {
            // 获取 32 - 63 之间的随机Id
            // 返回机器Id
            log.error("Redis连接异常,不能正确注册雪花机器号 " + machineId + " IP:" + localIp, e);
            log.warn("使用临时方案,获取 32 - 63 之间的随机数作为机器号,请及时检查Redis连接");
            getRandomMachineId();
            return machineId;
        }
        return machineId;
    }

    /**
     * 检查是否被注册满了
     *
     * @return
     */
    private Boolean checkIfCanRegister() {
        // 判断0~31这个区间段的机器IP是否被占满
        boolean flag = true;
        try {
            for (int i = 0; i < 32; i++) {
                flag = redisTemplate.hasKey(properties.getAppName() + properties.getDataCenterId() + i);
                // 如果不存在。设置机器Id为这个不存在的数字
                if (!flag) {
                    machineId = i;
                    break;
                }
            }
            return !flag;
        }catch (Exception ex){
            log.error("checkIfCanRegister failed");
        }
        return true;
    }

    /**
     * 1.更新超時時間
     * 注意,更新前检查是否存在机器ip占用情况
     */
    private void updateExpTimeThread() {
        // 开启一个线程执行定时任务:
        // 每50s更新一次超时时间
        new Timer(localIp).schedule(new TimerTask() {
            @Override
            public void run() {
                // 检查缓存中的ip与本机ip是否一致, 一致则更新时间,不一致则重新获取一个机器id
                Boolean b = checkIsLocalIp(String.valueOf(machineId));
                if (b) {
                    log.info("IP一致,更新超时时间 ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                    redisTemplate.expire(properties.getAppName() + properties.getDataCenterId() + machineId, Duration.ofSeconds(properties.getDuration()));
                } else {
                    // IP冲突
                    log.info("重新生成机器ID ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                    // 重新生成机器ID,并且更改雪花中的机器ID
                    getRandomMachineId();
                    // 重新生成并注册机器id
                    createMachineId();
                    // 更改雪花中的机器ID
                    SnowFlake.setWorkerId(machineId);
                    // 结束当前任务
                    log.info("Timer->thread->name:{}", Thread.currentThread().getName());
                    this.cancel();
                }
            }
        }, 10 * 1000, properties.getTimer());
    }

    /**
     * 获取32-63随机数
     */
    public void getRandomMachineId() {
        machineId = (int) (Math.random() * 31) + 31;
    }


    /**
     * 检查Redis中对应Key的Value是否是本机IP
     *
     * @param mechineId
     * @return
     */
    private Boolean checkIsLocalIp(String mechineId) {
        String ip = (String) redisTemplate.opsForValue().get(properties.getAppName() + properties.getDataCenterId() + mechineId);
        log.info("checkIsLocalIp->ip:{}", ip);
        return localIp.equals(ip);
    }

    /**
     * 1.注册机器
     * 2.设置超时时间
     * @param machineId 取值为0~31
     * @return
     */
    private Boolean registerMachine(Integer machineId, String localIp) throws Exception {
        // key 业务号 + 数据中心ID + 机器ID value 机器IP
        String key = properties.getAppName() + properties.getDataCenterId() + machineId;
        // 如果Key存在,判断Value和当前IP是否一致,一致则返回True
        if(redisTemplate.hasKey(key)){
            String value = (String) redisTemplate.opsForValue().get(key);
            if(localIp.equals(value)){
                redisTemplate.expire(properties.getAppName() + properties.getDataCenterId() +
                        machineId, Duration.ofSeconds(properties.getDuration()));
                return true;
            }
            return false;
        }else {
            redisTemplate.opsForValue().setIfAbsent(properties.getAppName() + properties.getDataCenterId() +
                    machineId, localIp,Duration.ofSeconds(properties.getDuration()));
            return true;
        }

    }
}
/**
 * 功能:分布式ID生成工具类
 */
public class SnowFlake implements IdProducer {
    /**
     * 服务一旦运行过之后不能修改。会导致ID生成重复
     */
    private final long twepoch = 1288834974657L;


    /**
     * 机器Id所占的位数 0 - 64
     */
    private final long workerIdBits = 6L;

    /**
     * 工作组Id所占的位数 0 - 16
     */
    private final long dataCenterIdBits = 4L;

    /**
     * 支持的最大机器id,结果是63 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /**
     * 支持的最大数据标识id,结果是15
     */
    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~63)
     */
    private static long workerId;

    /**
     * 数据中心ID(0~16)
     */
    private long datacenterId;

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

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

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

    /**
     * 构造函数
     *
     * @param workerId     工作ID (0~63)
     * @param datacenterId 数据中心ID (0~15)
     */
    public SnowFlake(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("机器ID必须小于 %d 且大于 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("工作组ID必须小于 %d 且大于 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    /**
     * 构造函数
     *
     */
    public SnowFlake() {
        this.workerId = 0;
        this.datacenterId = 0;
    }

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

    public long getWorkerId() {
        return workerId;
    }

    public static void setWorkerId(long workerId) {
        SnowFlake.workerId = workerId;
    }

    public long getDatacenterId() {
        return datacenterId;
    }

    public void setDatacenterId(long datacenterId) {
        this.datacenterId = datacenterId;
    }
}
/**
 * 使用说明:
 * 1.配置文件添加dataCenterId、port、appName、enable等关键配置信息
 * 2.在需要引入的地方,注入SnowFlake类或者IdProducer接口
 * 3.调用nextId()方法,获取id
 *
 * 配置文件如下:
 produce:
   config:
   # 分布式雪花ID不同机器ID自动化配置
   snowFlake:
     enable: true # 是否开启雪花算法
     dataCenter: 1 # 数据中心的id,范围0~15
     duration: 60000 #到期时间,单位毫秒
     timer: 50000 #定时检测过期时间
 */
@Getter
@Setter
@Configuration
@ConditionalOnProperty(prefix = "produce.config.snowFlake",name = "enable",havingValue = "true")
public class SnowFlakeProperties {

    @Value("${produce.config.snowFlake.dataCenter}")
    private Integer dataCenterId;

    @Value("${produce.config.snowFlake.duration}")
    private Long duration;

    @Value("${produce.config.snowFlake.timer}")
    private Long timer;

    @Value("${server.port}")
    private Integer port;

    @Value("${spring.application.name}")
    private String appName;
}
public interface IdProducer {
    long nextId();
}

以上是我这边改良的代码,算是原创吧,如有问题,欢迎大家指正。

代码是可以开箱即用的!!!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值