zoukankan      html  css  js  c++  java
  • 如何使用redis生成唯一编号及原理

    在系统开发中,保证数据的唯一性是至关重要的一件事,目前开发中常用的方式有使用数据库的自增序列、UUID生成唯一编号、时间戳或者时间戳+随机数等。
    在某些特定业务场景中,可能会要求我们使用特定格式的唯一编号,比如我有一张订单表(t_order),我需要生成“yewu(ORDER)+日期(yyyyMMdd)+序列号(00000000)”格式的订单编号,比如今天的日期是20200716,那我今天第一个订单号就是ORDER2020071600000001、第二个订单号就是ORDER2020071600000002,明天的日期是20200717,那么明天的第一个订单号就是ORDER2020071700000001、第二个订单号就是ORDER2020071700000002,以此类推。
    今天介绍下如何使用redis生成唯一的序列号,其实主要思想还是利用redis单线程的特性,可以保证操作的原子性,使读写同一个key时不会出现不同的数据。
    以SpringBoot项目为例,添加以下依赖
           <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.1</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>${spring.boot.version}</version>
            </dependency>

    application.properties中配置redis,我本地redis没有设置密码,所以注释了密码这一行

    server.port=9091
    server.servlet.context-path=/
    
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    #spring.redis.password=1234
    spring.redis.database=0

    SequenceService类用于生成特定业务编号

    package com.xiaochun.service;
    
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.support.atomic.RedisAtomicLong;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    
    @Service
    public class SequenceService {
    
        private static Logger logger = LoggerFactory.getLogger(SequenceService.class);
    
        @Resource
        private RedisTemplate redisTemplate;
    
        //用作存放redis中的key
        private static String ORDER_KEY = "order_key";
        
        //生成特定的业务编号,prefix为特定的业务代码
        public String getOrderNo(String prefix){
             return getSeqNo(ORDER_KEY, prefix);
        }
        
        //SequenceService类中公用部分,传入制定的key和prefix
        private String getSeqNo(String key, String prefix)
        {
            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 = generate(redisTemplate, key, expireDate);
            //获取当天的日期,格式为yyyyMMdd
            String date = new SimpleDateFormat("yyyyMMdd").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;
            logger.info("KEY:{}, 序列号生成:{}, 过期时间:{}", key, seqNo, String.format("%tF %tT ", expireDate, expireDate));
            return seqNo;
        }
    
        /**
         * @param key
         * @param expireTime <i>过期时间</i>
         * @return
         */
        public static long generate(RedisTemplate<?,?> redisTemplate,String key,Date expireTime) {
            //RedisAtomicLong为原子类,根据传入的key和redis链接工厂创建原子类
            RedisAtomicLong counter = new RedisAtomicLong(key,redisTemplate.getConnectionFactory());
            //设置过期时间
            counter.expireAt(expireTime);
            //返回redis中key的值,内部实现下面详细说明
            return counter.incrementAndGet();
        }
    
    }
    接下来,启动项目,使用接口的形式访问,或者写Test方法执行,就可以得到诸如ORDER2020071600000001、ORDER2020071600000002的编号,而且在高并发环境中也不会出现数据重复的情况。
     
    实现原理:
    上面生成特定业务编号主要分为三部分,如下图
    前缀和日期部分,没什么需要解释的,主要是redis中的生成的序列号,而这需要依靠RedisAtomicLong实现,先看下上面生成redis序列过程中发生了什么
    • 获取redis中对应业务的key生成过期时间expireTime
    • 获取了RedisTemplate对象,通过该对象获取RedisConnectionFactory对象
    • key,RedisConnectionFactory对象作为构造参数生成RedisAtomicLong对象,并设置过期时间
    • 调用RedisAtomicLong的incrementAndGet()方法
    看下RedisAtomicLong源码,当然只放一部分源码,不会放全部,RedisAtomicLong的结构,主要构造函数,和上面提到过的incrementAndGet()方法
    public class RedisAtomicLong extends Number implements Serializable, BoundKeyOperations<String> {
        private static final long serialVersionUID = 1L;
        //redis中的key,用volatile修饰,获得原子性
        private volatile String key;
        //当前的key-value对象,根据传入的key获取value值
        private ValueOperations<String, Long> operations;
        //传入当前redisTemplate对象,为RedisTemplate对象的顶级接口
        private RedisOperations<String, Long> generalOps;
    
        public RedisAtomicLong(String redisCounter, RedisConnectionFactory factory) {
            this(redisCounter, (RedisConnectionFactory)factory, (Long)null);
        }
        private RedisAtomicLong(String redisCounter, RedisConnectionFactory factory, Long initialValue) {
            Assert.hasText(redisCounter, "a valid counter name is required");
            Assert.notNull(factory, "a valid factory is required");
            //初始化一个RedisTemplate对象
            RedisTemplate<String, Long> redisTemplate = new RedisTemplate();
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new GenericToStringSerializer(Long.class));
            redisTemplate.setExposeConnection(true);
            //设置当前的redis连接工厂
            redisTemplate.setConnectionFactory(factory);
            redisTemplate.afterPropertiesSet();
            //设置传入的key
            this.key = redisCounter;
            //设置当前的redisTemplate
            this.generalOps = redisTemplate;
            //获取当前的key-value集合
            this.operations = this.generalOps.opsForValue();
            //设置默认值,如果传入为null,则key获取operations中的value,如果value为空,设置默认值为0
            if (initialValue == null) {
                if (this.operations.get(redisCounter) == null) {
                    this.set(0L);
                }
            //不为空则设置为传入的值
            } else {
                this.set(initialValue);
            }
        }
        //将传入key的value+1并返回
        public long incrementAndGet() {
            return this.operations.increment(this.key, 1L);
        }
    其实主要还是通过redis的自增序列来实现
    技术交流QQ群:579949017 或者添加个人微信:xieya0126 加入微信交流群
  • 相关阅读:
    发送邮件程序
    T-SQL存储过程、游标
    GPS经纬度换算成XY坐标
    开博了
    你应该知道的 50 个 Python 单行代码
    想提升java知识的同学请进
    adb工具包使用方法
    红米note3刷安卓原生
    hadoop 使用和javaAPI
    django学习——url的name
  • 原文地址:https://www.cnblogs.com/dsxie/p/13324489.html
Copyright © 2011-2022 走看看