1. 程式人生 > >採用redis生成唯一且隨機的訂單號

採用redis生成唯一且隨機的訂單號

專案描述

最近做的一個專案有這麼一個需求:需要生成一個唯一的11位的就餐碼(類似於訂單號的概念),就餐碼的規則是:一共是11位的數字,前面6位是日期比如2019年07月20就是190720,後面五位是隨機數且不能是自增的,不然容易讓人看出一天的單量。

解決方案

五位隨機數不能用隨機生成的,不然可能不唯一,所以想到了預生成的方案:
採用redis

  • 隨機數生成

先生成10000~99999共9萬個數(從1萬開始是懶得再前面補0了),然後打亂分別 存入redis的list資料結構 90個key每個key存1000個數。取的時候通過LINDEX進行讀取。

        List<String> numList=new ArrayList<>();
        //90萬個數 每個redis key 1000個數,要存90個key.
        for (int i=10000;i<=99999;i++){
            numList.add(String.valueOf(i));

        }
        //打亂順序
        Collections.shuffle(numList);
        //生成key
        for (int j=10;j<=99;j++){
            String redisKey="qrcode:"+j;
            List<String> newList= test.subList((j-10)*1000,(j-10)*1000 + 1000);
           jedisCluster.rpush(redisKey,newList.toArray(new String[newList.size()]));
        }

這樣每個key的index值就是0~999,key就是qrcode:10/qrcode:11/qrcode:12.../qrcode:99.

  • 計數key

再使用一個key來計數每次生成一個就餐碼就加1,值也從10000開始,計數的前兩位用來表示該取哪個key,後三位代表key的索引。比如現在計數記到12151那就是取上面生成的qrcode:12 key裡索引為151的value,然後當計數到99999時再從10000重新計數,這樣保證一天有9萬個隨機數可以使用且不會取到相同的隨機數。這樣可以解決一天最多9萬單數量級的業務,後面一天百萬級同理可以擴充成6位7位等。

先初始化:

jedisCluster.set(qrcode:incr,9999);

示例

      public String getOneQrCode() {
        Long incr = jedisCluster.incr("qrcode:incr");
        //測試環境生成到19999
        int maxIncr=19999
        //int maxIncr = 99999;
        //後期單量過猛時需要考慮--併發風險導致的就餐碼重複 todo
        if (incr == maxIncr) {
            jedisCluster.set("qrcode:incr", String.valueOf(10000));
           }
        System.out.println("incr:"+incr);
        //取前兩位
        String key = incr.toString().substring(0, 2);
        //取後三位作為list裡的index
        Integer index = NumberUtil.getIntValue(incr.toString().substring(2));
        //獲得5位隨機數
        String qrcode = jedisCluster.lIndex("qrcode:"+ key, index);
        return qrcode;
    }

併發風險

當計數到最大值時,需要重置計數key(qrcode:incr)為10000會有執行緒不安全的問題。
我們先編寫一個併發方法單元測試一下:
測試環境由於只生成10000個隨機數,maxincr=19999,所以
我們先把計數的key設定成接近maxincr來進行併發測試,設定成19997後獲取2個qrcode將進行重置成10000.

jedisCluster.set(qrcode:incr,19997);

開啟5個執行緒併發測試:

private static final int threadNum=5;
    //倒計數器,用於模擬高併發
    private CountDownLatch countDownLatch=new CountDownLatch(threadNum);
    @Test
    public void benchmark() {
        Thread[] threads=new Thread[threadNum];
        for (int i = 0; i <threadNum ; i++) {
            final int j=i;
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println("qrcode"+getOneQrCode());
                }
            });
            threads[i]=thread;
            thread.start();

            countDownLatch.countDown();
        }
        for (Thread thread :threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

5個執行緒併發測試的結果:

  1. 對qrcode:incr進行get返回的結果是10000.
  2. 獲取的結果為:

由於併發導致5個執行緒都先執行到

 Long incr = jedisCluster.incr("qrcode:incr");

最終incr的值分別為19998/19999/20000/20001/20002.所以後面三個計數的key為20,由於測試環境只生成到了qrcode:19,所以返回的是null。

解決

所以判斷到達maxincr並重置成10000時應該是原子操作。所以這裡採用lua指令碼的方式執行。

Redis使用lua指令碼
版本:自2.6.0起可用。
時間複雜度:取決於執行的指令碼。

使用Lua指令碼的好處:

  • 減少網路開銷。可以將多個請求通過指令碼的形式一次傳送,減少網路時延。
    原子操作。redis會將整個指令碼作為一個整體執行,中間不會被其他命令插入。因此在編寫指令碼的過程中無需擔心會出現競態條件,無需使用事務。
  • 複用。客戶端傳送的指令碼會永久存在redis中,這樣,其他客戶端可以複用這一指令碼而不需要使用程式碼完成相同的邏輯。
  • redis會將整個指令碼作為一個整體執行,中間不會被其他命令插入。因此在編寫指令碼的過程中無需擔心會出現競態條件,無需使用事務。

所以對獲取qrcode進行改造:

public String getOneQrcodeLua(){
        String lua="local key = KEYS[1]\n" +
            "local incr=redis.call('incr',key) \n"+
            "if incr == tonumber(ARGV[1]) \n" +
            "then\n" +
            "    redis.call('set',key,ARGV[2])\n" +
            "    return incr\n" +
            "else\n" +
            "    return incr\n" +
            "end";
        List<String> keys = new ArrayList<>();
        keys.add("qrcode:incr");
        List<String> argv = new ArrayList<>();
        argv.add("19999");
        argv.add("10000");
        Object o= jedisCluster.eval(lua,keys,argv);
       // System.out.println("incr"+o);
        //取前兩位
        String key = o.toString().substring(0, 2);
        //取後三位作為list裡的index
        Integer index = NumberUtil.getIntValue(o.toString().substring(2));
        //獲得5位隨機數
        String qrcode = jedisCluster.lIndex("qrcode:"+ key, index);
        return qrcode;
    }

5個執行緒併發測試的結果:

  1. 對qrcode:incr進行get返回的結果是10003.
  2. 獲取的結果為:

一切正常。

參考

https://redisbook.readthedocs.io/en/latest/feature/scripting.html
http://doc.redisfans.com/script/eval.html

我的部落格地址

我的部落格地址