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

  • 2019 年 10 月 3 日
  • 筆記

項目描述

最近做的一個項目有這麼一個需求:需要生成一個唯一的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" +              "thenn" +              "    redis.call('set',key,ARGV[2])n" +              "    return incrn" +              "elsen" +              "    return incrn" +              "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

我的部落格地址

我的部落格地址