唯一序列生成器 starter

作者: javacoo | 来源:发表于2021-05-22 22:21 被阅读0次

    sequence-spring-boot-starter

    唯一序列生成器 starter

    介绍

    说明

    工程提供扩展接口(扩展点):

             接口->com.javacoo.sequence.client.api.Sequence
    

    基于xkernel 提供的SPI机制,结合SpringBoot注解 ConditionalOnBean,ConditionalOnProperty实现。

    类关系图

    Sequence.png

    项目结构

    sequence-spring-boot-starter
     └── src
        ├── main  
        │   ├── java  
        │   │   └── com.javacoo 
        │   │   ├────── sequence
        │   │   │         ├──────client
        │   │   │         │         ├── api
        │   │   │         │         │     ├── client
        │   │   │         │         │     │      └── Sequence 唯一编码生成服务
        │   │   │         │         │     ├── config
        │   │   │         │         │            └── SequenceConfig 唯一编码生成服务配置
        │   │   │         │         └── internal 接口内部实现
        │   │   │         │               ├── redis
        │   │   │         │               │      └── RedisSequenceImpl 唯一编码生成服务redis实现
        │   │   │         │               ├── snowflake
        │   │   │         │                      ├── SnowflakeConfig 配置类
        │   │   │         │                      └── SnowflakeSequenceImpl 唯一编码生成服务雪花算法实现
        │   │   │         └──────starter
        │   │   │                   ├── SequenceAutoConfiguration 自动配置类
        │   │   │                   └── SequenceHolder 锁接口对象持有者
        │   └── resource  
        │       ├── META-INF
        │             └── ext
        │                  └── internal
        │                          └── com.javacoo.sequence.client.api.Sequence
        └── test  测试
    

    如何使用

    1. pom依赖:

      <dependency>
         <groupId>com.javacoo</groupId>
         <artifactId>sequence-spring-boot-starter</artifactId>
         <version>1.0.0</version>
      </dependency>
      
    2. 配置参数,如果使用默认实现,则无需配置,如要扩展则需要,配置如下:

      #sequence是否可用,默认可用
      sequence.enabled = true
      #sequence实现,默认内部实现
      sequence.impl = default
      
    3. 使用,如:

      
      @Resource
      private Sequence sequence;
      
      sequence.generate("APPLY","GJJ")
      
      

    SPI扩展

    基于xkernel 提供的SPI机制,扩展非常方便,大致步骤如下:

    1. 实现唯一编码生成服务接口:如 com.xxxx.xxxx.MySequencempl

    2. 配置唯一编码生成服务接口:

      • 在项目resource目录新建包->META-INF->services

      • 创建com.javacoo.sequence.client.api.Sequence文件,文件内容:实现类的全局限定名,如:

        mySequence=com.xxxx.xxxx.MySequencempl
        
      • 修改配置文件,添加如下内容:

      #sequence实现,默认内部实现
      sequence.impl = mySequence
      

    默认实现

    1、唯一编码生成服务接口:

    /**
     * 唯一编码生成服务
     * <li></li>
     *
     * @author: duanyong
     * @since: 2021/4/28 10:27
     */
    @Spi(SequenceConfig.DEFAULT_IMPL)
    public interface Sequence {
        /**
         * 生成申请号
         * <li></li>
         * @author duanyong
         * @date 2021/4/28 10:31
         * @param module: 模块
         * @param prefix: 前缀
         * @return: java.lang.String 唯一编码
         */
        String generate(String module,String prefix);
    }
    

    2、实现唯一编码生成服务接口,如默认实现:

    /**
     * 唯一编码生成服务
     * <li>基于redis</li>
     *
     * @author: duanyong
     * @since: 2021/4/28 10:32
     */
    @Slf4j
    public class RedisSequenceImpl implements Sequence {
        /**缓存KEY前缀*/
        private static final String CACHE_KEY_PREFIX = "KEY:JAVACOO-APPLICATION:%s";
        /**时间格式*/
        private static final String FORMAT = "yyyyMMdd";
        /**redis模板*/
        @Resource
        private RedisTemplate redisTemplate;
        /**
         * 生成唯一编码
         * <li></li>
         *
         * @param module: 模块
         * @param prefix : 前缀
         * @author duanyong
         * @date 2021/4/28 10:31
         * @return: java.lang.String 唯一编码
         */
        @Override
        public String generate(String module,String prefix) {
            log.info("开始生成唯一编码,模块:{},前缀:{}",module,prefix);
            String seq = doGenerate(String.format(CACHE_KEY_PREFIX, module), String.format("%s_", prefix),FORMAT);
            log.info("生成唯一编码结束:{}",seq);
            return seq;
        }
    
        /**
         * 执行生成唯一编码
         * <li></li>
         * @author duanyong
         * @date 2021/4/28 10:53
         * @param key: 键
         * @param prefix: 前缀
         * @param format: 格式化
         * @return: java.lang.String 唯一编码
         */
        private String doGenerate(String key, String prefix, String format) {
            Calendar calendar = Calendar.getInstance();
            calendar.set(Calendar.HOUR_OF_DAY, 23);
            calendar.set(Calendar.MINUTE, 59);
            calendar.set(Calendar.SECOND, 59);
            calendar.set(Calendar.MILLISECOND, 999);
            //设置过期时间,这里设置为当天的23:59:59
            Date expireDate = calendar.getTime();
            //返回当前redis中的key的最大值
            Long seq = generateSeq(key, expireDate);
            //获取当天的日期,格式为yyyyMMdd
            String date = new SimpleDateFormat(format).format(expireDate);
            //生成八为的序列号,如果seq不够八位,seq前面补0,
            //如果seq位数超过了八位,那么无需补0直接返回当前的seq
            String sequence = StringUtils.leftPad(seq.toString(), 8, "0");
            if (prefix == null) {
                prefix = "";
            }
            //拼接业务编号
            String seqNo = prefix + date + sequence;
            log.info("KEY:{}, 序列号生成:{}, 过期时间:{}", key, seqNo, String.format("%tF %tT ", expireDate, expireDate));
            return seqNo;
        }
        /**
         * 生成序列号
         * <li></li>
         * @author duanyong
         * @date 2021/4/28 10:50
         * @param key: 键
         * @param expireTime: 过期时间
         * @return: long 序列号
         */
        private long generateSeq(String key, Date expireTime) {
            try{
                //RedisAtomicLong为原子类,根据传入的key和redis链接工厂创建原子类
                RedisAtomicLong counter = new RedisAtomicLong(key,redisTemplate.getConnectionFactory());
                //设置过期时间
                counter.expireAt(expireTime);
                //返回redis中key的值
                return counter.incrementAndGet();
            }catch (Exception e){
                log.error("生成序列号异常:",e);
                String no = StringUtils.right(String.valueOf(System.currentTimeMillis()),4)+ (Math.round((Math.random()+1) * 1000));
                log.info("异常后生成序列号:{}",no);
                return Long.parseLong(no);
            }
        }
    
    }
    

    3、雪花算法配置

    **
     * 雪花算法配置
     * <li></li>
     *
     * @author: duanyong
     * @since: 2021/5/21 14:56
     */
    @ConfigurationProperties(prefix = SequenceConfig.PREFIX+SnowflakeConfig.PREFIX)
    public class SnowflakeConfig {
        /** 前缀 */
        public static final String PREFIX = ".snowflake";
        /** 工作机器ID,默认值*/
        public static final long DEFAULT_WORKER_ID = 0L;
        /** 数据中心ID:,默认值*/
        public static final long DEFAULT_DATACENTER_ID= 0L;
        /** 工作机器ID(0~31) */
        private long workerId = DEFAULT_WORKER_ID;
    
        /** 数据中心ID(0~31) */
        private long datacenterId = DEFAULT_DATACENTER_ID;
    
        public long getWorkerId() {
            return workerId;
        }
    
        public void setWorkerId(long workerId) {
            this.workerId = workerId;
        }
    
        public long getDatacenterId() {
            return datacenterId;
        }
    
        public void setDatacenterId(long datacenterId) {
            this.datacenterId = datacenterId;
        }
    }
    
    

    4、唯一编码生成服务,基于Twitter_Snowflake,算法实现来自网络

    /**
     * 唯一编码生成服务
     * <li>基于Twitter_Snowflake</li>
     * <li>算法实现来自网络</li>
     * SnowFlake的结构如下(每部分用-分开):<br>
     * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <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位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
     * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
     * 加起来刚好64位,为一个Long型。<br>
     * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
     * @author: duanyong
     * @since: 2021/5/21 14:30
     */
    @Slf4j
    public class SnowflakeSequenceImpl implements Sequence {
        /** 开始时间截 (2015-01-01) */
        private final long twepoch = 1420041600000L;
    
        /** 机器id所占的位数 */
        private final long workerIdBits = 5L;
    
        /** 数据标识id所占的位数 */
        private final long datacenterIdBits = 5L;
    
        /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
        private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    
        /** 支持的最大数据标识id,结果是31 */
        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~31) */
        private long workerId;
    
        /** 数据中心ID(0~31) */
        private long datacenterId;
    
        /** 毫秒内序列(0~4095) */
        private long sequence = 0L;
    
        /** 上次生成ID的时间截 */
        private long lastTimestamp = -1L;
    
        @Autowired
        private SnowflakeConfig snowflakeConfig;
    
        @PostConstruct
        public void init() {
            if (snowflakeConfig.getWorkerId() > maxWorkerId || snowflakeConfig.getWorkerId() < 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (snowflakeConfig.getDatacenterId() > maxDatacenterId || snowflakeConfig.getDatacenterId() < 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            this.workerId = snowflakeConfig.getWorkerId();
            this.datacenterId = snowflakeConfig.getDatacenterId();
        }
        /**
         * 生成申请号
         * <li></li>
         *
         * @param module : 模块
         * @param prefix : 前缀
         * @author duanyong
         * @date 2021/4/28 10:31
         * @return: java.lang.String 唯一编码
         */
        @Override
        public String generate(String module, String prefix) {
            log.info("Snowflake开始生成唯一编码,前缀:{}",prefix);
            String seq = String.format("%s_", prefix)+nextId();
            log.info("Snowflake生成唯一编码结束:{}",seq);
            return seq;
        }
        /**
         * 获得下一个ID (该方法是线程安全的)
         * @return SnowflakeId
         */
        private 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();
        }
    }
    

    5、Sequence接口对象持有者

    /**
     * Sequence接口对象持有者
     * <li></li>
     * @author duanyong
     * @date 2021/5/21 9:26
     */
    public class SequenceHolder {
        /** Sequence对象*/
        static Sequence sequence;
    
        public static Optional<Sequence> getSequence() {
            return Optional.ofNullable(sequence);
        }
    }
    

    6、自动配置。

    /**
     * 自动配置类
     * <li></li>
     *
     * @author: duanyong
     * @since: 2021/5/21 9:19
     */
    @Slf4j
    @Configuration
    @EnableConfigurationProperties(value = SequenceConfig.class)
    @ConditionalOnClass(Sequence.class)
    @ConditionalOnProperty(prefix = SequenceConfig.PREFIX, value = SequenceConfig.ENABLED, matchIfMissing = true)
    public class SequenceAutoConfiguration {
        @Autowired
        private SequenceConfig sequenceConfig;
        @Bean
        @ConditionalOnMissingBean(Sequence.class)
        public Sequence createSequence() {
            log.info("初始化唯一编码生成器,实现类名称:{}",sequenceConfig.getImpl());
            SequenceHolder.sequence = ExtensionLoader.getExtensionLoader(Sequence.class).getExtension(sequenceConfig.getImpl());
            log.info("初始化唯一编码生成器成功,实现类:{}",SequenceHolder.sequence);
            return SequenceHolder.sequence;
        }
    }
    

    一些信息

    路漫漫其修远兮,吾将上下而求索
    码云:https://gitee.com/javacoo
    QQ群:164863067
    作者/微信:javacoo
    邮箱:xihuady@126.com
    

    相关文章

      网友评论

        本文标题:唯一序列生成器 starter

        本文链接:https://www.haomeiwen.com/subject/qlnojltx.html