zoukankan      html  css  js  c++  java
  • 采用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]
    " +
                "local incr=redis.call('incr',key) 
    "+
                "if incr == tonumber(ARGV[1]) 
    " +
                "then
    " +
                "    redis.call('set',key,ARGV[2])
    " +
                "    return incr
    " +
                "else
    " +
                "    return incr
    " +
                "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

    我的博客地址

    我的博客地址

    努力学习,每天进步一点点。加油
  • 相关阅读:
    做问答系统是对题目修改的bug
    控件treetable使用
    百度地图API --地理位置定位
    按每十分钟查询数据
    《deetom》项目开发历程<六> 免登陆
    poj 3348
    poj 1556
    poj 1269
    poj 3304
    R 540
  • 原文地址:https://www.cnblogs.com/yalunwang/p/11592194.html
Copyright © 2011-2022 走看看