采用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. 獲取的結果為:
    image

由於並發導致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. 獲取的結果為:
    image

一切正常。

參考

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

我的博客地址

我的博客地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM