在 Java 中利用 redis 實現分佈式架構的全局唯一標識服務

獲取全局唯一標識的方法介紹

在一個IT系統中,獲取一個對象的唯一標識符是一個普遍的需求。在以前的單體應用中,如果數據庫是一個單數據庫的結構。通常可以利用數據庫的自增字段來獲取這個唯一標識。例如,在 Mysql 數據庫中,我們可以通過 sql 語句創建一個自增長的 int 字段類型的表。如下所示。

CREATE TABLE student
(
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(16),
    PRIMARY KEY (id)
)

然後插入兩條數據

INSERT INTO student(name) VALUE('yanggch');
INSERT INTO student(name) VALUE('grace');

通過 SQL 語句查看錶數據

 SELECT * FROM student;

數據庫查詢結果  【加羣】:857565362
可以看到,雖然我們在通過 SQL 插入數據的時候沒有指定 id 字段的值,但是因爲該字段的 AUTO_INCREMENT 自增長的特性,自動的給兩條記錄添加了1和2兩個值。
這個方法有兩個主要問題。一個是如果是一個分庫分表的數據庫結構,那麼在分佈在不同實例中的同一個表中的id是重複的。另一個問題是記錄插入到數據庫裏後,我們在代碼中並不能知道剛剛插入數據庫的記錄的主鍵的值到底是什麼。如果我們的一個業務是要同時插入一條主表記錄一節一系列以這條主表記錄主鍵爲外鍵的子表記錄,我們在插入子表記錄的時候,不知道對應的外鍵的值是多少。導致無法插入。例如如果我們有一個下單業務,要求在訂單表中插入一條訂單記錄,同時在訂單明細中插入多條在這個訂單中購買的商品的詳細信息的記錄。訂單數據插入成功後,我們不知道訂單的主鍵的值,所以我們也就無法正確的插入商品詳細信息記錄了。
另外一個利用數據庫自增字段屬性獲取唯一標識方式是在數據庫中建立一個帶一個自增字段的數據表。每次在表中插入一條記錄,然後將這條記錄的值取出來作爲主鍵值。這個的問題是每次要另外在數據表中插入一條記錄,同時在多用戶使用的環境下,要嚴格保證你取到的記錄就是你插入的記錄。否則會導致主鍵重複。着會讓獲取唯一標識符的速度變得比較慢。同時,這個方式在分庫分表的結構下,也不能讓唯一標識在全局唯一。
還有一些其他的方式。例如用 uuid 算法可以保證全局唯一,也能保證高性能。但是他生成是一個字符串,不能保證順序性,同時也太長了。
所以在分佈式架構中,我們就需要一個滿足如下條件的唯一標識符服務
1.全局唯一
2.高性能
3.具備順序性
4.可以附加其他業務屬性
這裏我們可以用 redis 的 INCR 命令來作爲生成全局的唯一標識符。INCR 命令的語法是

INCR key

根據 redis 的官網的介紹,它是一個原子操作,效果是是將 redis 數據庫中 key 的值加一併且返回這個結果。如果 key 不存在,將在執行加一操作前,將這個 key 的值設置爲0,也就是說執行這個命令的結果是從 1 開始一直累加下去的。
同時我們可以看到這個命令的算法時間複雜度是 O(1),而 redis 的數據是存儲在內存中的,這個命令的執行速度是非常快的。在 redis 服務器爲雙核 16g環境下,通過千兆局域網在另一臺服務器上命令行執行壓力測試

redis-benchmark -h 10.110.2.56 -p 52981 -a hhSbcpotThgWdnxJNhrzwstSP20DvYOldkjf

結果如下
redis INCR 壓力測試結果   【加羣】:857565362
可以看到每秒可以生成5萬個標識。這個可以滿足一般的高性能需求了

通過 Java 和 redis 實現一個全局唯一標識服務
接下來我們來用繼續來在 Java 中利用 redis 來實現一個全局唯一標識的服務。這個服務要滿足如下的需求
1.全局唯一
2.高性能
3.具備順序性
4.可以將日期數字作爲全局唯一標識的前綴
5.可以每天從 1 開始重新計數
6.不同的實體類型可以單獨生成標識。例如訂單標識,會員標識
7.可以在新的一天中從 1 開始計數

定義唯一標識服務接口

package com.x9710.common.redis;

/**
 * 全局唯一標識服務接口
 *
 * @author lemonrel
 * @since 2017-12-10
 */
public interface UUIDService {

/**
 * 每天從 1 開始生成唯一標識
 *
 * @param key     要生成唯一標識的對象
 * @param length  要生成爲唯一標識後綴的長度。不包括需要附加的時間前綴
 *                如果 haveDay = false 或者 length 長度小於標識後綴的長度則無效
 * @param haveDay 是否要附加日期前綴
 * @return 唯一標識
 * @throws Exception 異常
 */
Long fetchDailyUUID(String key, Integer length, Boolean haveDay) throws Exception;

/**
 * 全局從 1 開始生成唯一標識
 *
 * @param key     要生成唯一標識的對象
 * @param length  要生成爲唯一標識後綴的長度。不包括需要附加的時間前綴
 *                如果 haveDay = false 或者 length 長度小於標識後綴的長度則無效
 * @param haveDay 是否要附加日期前綴。
 * @return 唯一標識
 * @throws Exception 異常
 */
Long fetchUUID(String key, Integer length, Boolean haveDay) throws Exception;
}

基於 redis 實現唯一標識服務

package com.x9710.common.redis.impl;

import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.UUIDService;
import redis.clients.jedis.Jedis;

import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;

/**
 * @author lemonrel
 * @since 2017-11-19
 */
public class UUIDServiceRedisImpl implements UUIDService {
private RedisConnection redisConnection;
private Integer dbIndex;

private DateFormat df = new SimpleDateFormat("yyyyMMdd");

public void setRedisConnection(RedisConnection redisConnection) {
    this.redisConnection = redisConnection;
}

public void setDbIndex(Integer dbIndex) {
    this.dbIndex = dbIndex;
}

public Long fetchDailyUUID(String key, Integer length, Boolean haveDay) {
    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        Calendar now = new GregorianCalendar();
        String day = df.format(now.getTime());
        //新的一天,通過新 key 獲取值,每天都能從1開始獲取
        key = key + "_" + day;
        Long num = jedis.incr(key);
        //設置 key 過期時間
        if (num == 1) {
            jedis.expire(key, (24 - now.get(Calendar.HOUR_OF_DAY)) * 3600 + 1800);
        }
        if (haveDay) {
            return createUUID(num, day, length);
        } else {
            return num;
        }
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
}

public Long fetchUUID(String key, Integer length, Boolean haveDay) {
    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        Calendar now = new GregorianCalendar();
        Long num = jedis.incr(key);
        
        if (haveDay) {
            String day = df.format(now.getTime());
            return createUUID(num, day, length);
        } else {
            return num;
        }
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
}

private Long createUUID(Long num, String day, Integer length) {
    String id = String.valueOf(num);
    if (id.length() < length) {
        NumberFormat nf = NumberFormat.getInstance();
        nf.setGroupingUsed(false);
        nf.setMaximumIntegerDigits(length);
        nf.setMinimumIntegerDigits(length);
        id = nf.format(num);
    }
    return Long.parseLong(day + id);
}
}

編寫測試用例
在 Junit4 中不支持多線程測試,所以這裏直接採用了 main 方法中運行測試用例。~

package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.impl.UUIDServiceRedisImpl;

import java.util.Date;

public class RedisUUIDTest {

public static void main(String[] args) {
    for (int i = 0; i < 20; i++) {
        new Thread(new Runnable() {
            public void run() {
                RedisConnection redisConnection = RedisConnectionUtil.create();
                UUIDServiceRedisImpl uuidServiceRedis = new UUIDServiceRedisImpl();
                uuidServiceRedis.setRedisConnection(redisConnection);
                uuidServiceRedis.setDbIndex(15);
                try {
                    for (int i = 0; i < 100; i++) {
                        System.out.println(new Date() + " get uuid = " + 
                              uuidServiceRedis.fetchUUID("MEMBER", 8, Boolean.TRUE) + 
                              " by globle in " + Thread.currentThread().getName());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                RedisConnection redisConnection = RedisConnectionUtil.create();
                UUIDServiceRedisImpl uuidServiceRedis = new UUIDServiceRedisImpl();
                uuidServiceRedis.setRedisConnection(redisConnection);
                uuidServiceRedis.setDbIndex(15);
                try {
                    for (int i = 0; i < 100; i++) {
                        System.out.println(new Date() + " get uuid = " + 
                            uuidServiceRedis.fetchDailyUUID("ORDER", 8, Boolean.TRUE) + 
                            " by daily in " + Thread.currentThread().getName());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
}

執行結果如下

Mon Dec 11 16:14:10 CST 2017 get uuid = 2017121100000003 by member in Thread-32
Mon Dec 11 16:14:10 CST 2017 get uuid = 2017121100000001 by member in Thread-8
Mon Dec 11 16:14:10 CST 2017 get uuid = 2017121100000007 by order in Thread-19
......
Mon Dec 11 16:14:14 CST 2017 get uuid = 2017121100002000 by member in Thread-14
Mon Dec 11 16:14:14 CST 2017 get uuid = 2017121100001999 by member in Thread-16
Mon Dec 11 16:14:14 CST 2017 get uuid = 2017121100001999 by order in Thread-39
Mon Dec 11 16:14:14 CST 2017 get uuid = 2017121100002000 by order in Thread-39

這樣,我們就實現了一個滿足開始七個需求的一個基本的唯一標識服務。只要調用這個模塊的程序連接的 redis 服務器的配置一樣,就能實現在同一個對象高效生成唯一標識的基礎服務。你還可以將這個包裝成爲一個 rest 服務,客戶端不需要直接連接 redis 服務器,直接通過 rest 的http 服務遠程獲取唯一標識即可。
我這兒整理了比較全面的JAVA相關的面試資料,
需要領取面試資料的同學,請加羣:473984645
在這裏插入圖片描述
獲取更多學習資料,可以加羣:473984645或掃描下方二維碼
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章