1. 程式人生 > 實用技巧 >如何使用redis生成唯一編號及原理

如何使用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的自增序列來實現