采用redis生成唯一且随机的订单号

项目描述

最近做的一个项目有这么一个需求:需要生成一个唯一的11位的就餐码(类似于订单号的概念),就餐码的规则是:一共是11位的数字,前面6位是日期比如2019年07月20就是190720,后面五位是随机数且不能是自增的,不然容易让人看出一天的单量。

解决方案

五位随机数不能用随机生成的,不然可能不唯一,所以想到了预生成的方案:

采用redis

  • 随机数生成

先生成10000~99999共9万个数(从1万开始是懒得再前面补0了),然后打乱分别 存入redis的list数据结构 90个key每个key存1000个数。取的时候通过LINDEX进行读取。

 
  1. List<String> numList=new ArrayList<>();
  2. //90万个数 每个redis key 1000个数,要存90个key.
  3. for (int i=10000;i<=99999;i++){
  4. numList.add(String.valueOf(i));
  5. }
  6. //打乱顺序
  7. Collections.shuffle(numList);
  8. //生成key
  9. for (int j=10;j<=99;j++){
  10. String redisKey="qrcode:"+j;
  11. List<String> newList= test.subList((j-10)*1000,(j-10)*1000 + 1000);
  12. jedisCluster.rpush(redisKey,newList.toArray(new String[newList.size()]));
  13. }

这样每个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位等。

先初始化:

 
  1. jedisCluster.set(qrcode:incr,9999);

示例

 
  1. public String getOneQrCode() {
  2. Long incr = jedisCluster.incr("qrcode:incr");
  3. //测试环境生成到19999
  4. int maxIncr=19999
  5. //int maxIncr = 99999;
  6. //后期单量过猛时需要考虑--并发风险导致的就餐码重复 todo
  7. if (incr == maxIncr) {
  8. jedisCluster.set("qrcode:incr", String.valueOf(10000));
  9. }
  10. System.out.println("incr:"+incr);
  11. //取前两位
  12. String key = incr.toString().substring(0, 2);
  13. //取后三位作为list里的index
  14. Integer index = NumberUtil.getIntValue(incr.toString().substring(2));
  15. //获得5位随机数
  16. String qrcode = jedisCluster.lIndex("qrcode:"+ key, index);
  17. return qrcode;
  18. }

并发风险

当计数到最大值时,需要重置计数key(qrcode:incr)为10000会有线程不安全的问题。

我们先编写一个并发方法单元测试一下:

测试环境由于只生成10000个随机数,maxincr=19999,所以

我们先把计数的key设置成接近maxincr来进行并发测试,设置成19997后获取2个qrcode将进行重置成10000.

 
  1. jedisCluster.set(qrcode:incr,19997);

开启5个线程并发测试:

 
  1. private static final int threadNum=5;
  2. //倒计数器,用于模拟高并发
  3. private CountDownLatch countDownLatch=new CountDownLatch(threadNum);
  4. @Test
  5. public void benchmark() {
  6. Thread[] threads=new Thread[threadNum];
  7. for (int i = 0; i <threadNum ; i++) {
  8. final int j=i;
  9. Thread thread=new Thread(new Runnable() {
  10. @Override
  11. public void run() {
  12. try {
  13. countDownLatch.await();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println("qrcode"+getOneQrCode());
  18. }
  19. });
  20. threads[i]=thread;
  21. thread.start();
  22. countDownLatch.countDown();
  23. }
  24. for (Thread thread :threads) {
  25. try {
  26. thread.join();
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }

5个线程并发测试的结果:

  1. 对qrcode:incr进行get返回的结果是10000.
  2. 获取的结果为:
     

由于并发导致5个线程都先执行到

 
  1. 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进行改造:

 
  1. public String getOneQrcodeLua(){
  2. String lua="local key = KEYS[1]\n" +
  3. "local incr=redis.call('incr',key) \n"+
  4. "if incr == tonumber(ARGV[1]) \n" +
  5. "then\n" +
  6. " redis.call('set',key,ARGV[2])\n" +
  7. " return incr\n" +
  8. "else\n" +
  9. " return incr\n" +
  10. "end";
  11. List<String> keys = new ArrayList<>();
  12. keys.add("qrcode:incr");
  13. List<String> argv = new ArrayList<>();
  14. argv.add("19999");
  15. argv.add("10000");
  16. Object o= jedisCluster.eval(lua,keys,argv);
  17. // System.out.println("incr"+o);
  18. //取前两位
  19. String key = o.toString().substring(0, 2);
  20. //取后三位作为list里的index
  21. Integer index = NumberUtil.getIntValue(o.toString().substring(2));
  22. //获得5位随机数
  23. String qrcode = jedisCluster.lIndex("qrcode:"+ key, index);
  24. return qrcode;
  25. }

5个线程并发测试的结果:

  1. 对qrcode:incr进行get返回的结果是10003.
  2. 获取的结果为:
     

一切正常。

参考

Lua 脚本 — Redis 设计与实现

EVAL — Redis 命令参考

我的博客地址

来源:采用redis生成唯一且随机的订单号

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值