分布式环境下redis自增值&定时失效

需求&业务场景

  没有需求或者业务场景,去谈技术就是空中楼阁~

前置条件

● 分布式部署
● 多实例

业务需求

● 不同业务,有该业务标识且自增的单号。
● 单号规则 业务标识+日期+4位自增数字
● 4位自增数字是表示当天的,凌晨清零

构思

   因为有多个实例,所以在操作自增数字的时候需要用到分布式锁,同时需要当天凌晨清零,很容易想到redis,缓存一个key值,失效时间是到凌晨。同时,redis提供原子操作的自增指令。至于分布式锁,考虑用reddsion的红锁。
另外一个需要考虑的点就是凌晨失效的那个点的那一刻,并发问题。
● reddsion的红锁解决分布式,多个实例操作问题
● 给key设置一个到凌晨的失效时间
● 考虑凌晨失效时候的并发问题
● 保证自增的原子操作

实现

获取到第二天凌晨的毫秒数

 public Long getNowToNextDayMilliseconds() {
        //获取当前时间
        Calendar calendar = Calendar.getInstance();
        //当前天+1
        calendar.add(Calendar.DAY_OF_YEAR, 1);
        //将时分秒毫秒都设为0
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        //减去当前时间获取插值
        return (calendar.getTimeInMillis() - System.currentTimeMillis());
    }

格式化字符串

  最终的输出格式是type+YYYYMMDD+4位自增数字

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
    private String getCode(String type, String number) {
        String date = sdf.format(new Date());
        StringBuffer buffer = new StringBuffer();
        buffer.append(type)
              .append(date);
        for (int i = number.length(); i < 4; i++) {
            buffer.append("0");
        }
        buffer.append(number);
        return buffer.toString();
    }

核心逻辑

  public String getOrderCode(String key) {
        Object value = redisTemplate.opsForValue().get(key);
        //如果值存在,直接自增返回        @第一个if   
        if (null != value) {
            return getCode(key, redisTemplate.opsForValue().increment(key).toString());
        }
        //拿不到值,说明可能到了零点,自增值失效,需要重新set 一个0进去
        //需要考虑分布式和本地并发问题,分布式通过redssion lock解决
        //本地并发可以通过lock 和 tryLock两种方案,lock是把while(true)放里面,本方案采用tryLock
        //while(true)里面两个分支,第一个分支是拿到锁,进去后需要考虑,执行成功后,临界区的线程进来所以需要先判空
        //第二个分支是拿不到锁,判断一下是否已经被拿到锁的线程set值成功,如果成功,直接返回
        RLock lock = redissonClient.getLock(CommonConstant.ORDER_CODE_LOCK_KEY + key);
        try {
            while (true) {
                //拿到锁就初始化值和失效时间,没拿到锁就继续获取key的值  
                if (lock.tryLock(CommonConstant.INTEGER_FIVE, TimeUnit.MICROSECONDS)) { // @第二个if  
                    if (null == redisTemplate.opsForValue().get(key)) {  // @第三个if  
                        redisTemplate.opsForValue().set(key, "0", getNowToNextDayMilliseconds(), TimeUnit.MILLISECONDS);
                    }
                    return getCode(key, redisTemplate.opsForValue().increment(key).toString());
                } else {
                    value = redisTemplate.opsForValue().get(key);
                    if (null != value) {// @第四个if  
                        return getCode(key, redisTemplate.opsForValue().increment(key).toString());
                    }
                }
            }
        } catch (InterruptedException e) {
            throw new BizException(BasicDataExceptionEnum.ORDER_CODE_CREATE_FAIL);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }

    }

  对于这段代码的解读,其实关注4个if就可以了

第一个if

  如果值存在,直接自增返回,redis的incr本身是原子操作,且redis是单线程的,可以保证线程安全,同时也能保证多进程情况下,拿到的值是唯一的。

第二个if

  当值是不存在的时候,需要去set值了。这个操作不是原子性的,而且分布式的情况下,A实例的set可能把B实例set的值覆盖掉。这个时候需要一个分布式锁。redssion的lock实现了AQS的接口,可以通过tryLock去尝试获取分布式锁。如果获取锁成功,则执行下一步。

第三个if

  即使获取分布式锁成功,也需要考虑本地并发问题,主要是需要考虑临界区的线程问题,第一个拿到锁的执行完了,会释放锁,这个时候临界区等待的线程就可以拿到锁了,也会进到这段逻辑,所以需要在判空操作一下。

第四个if

  如果没有获取到锁,也没有必要继续去循环获取锁,因为这个时候,可能拿到锁的线程已经把初始值set进去了。所以这里再次判空操作一下。

测试

  要保证代码的严谨性,需要设计一个并发场景的测试

@Test
    public void get_order_code_multi_thread_test()throws Exception {
        //删掉key,模拟key不存在的情况下并发操作
        redisTemplate.opsForValue().getOperations().delete("IS");
        CyclicBarrier barrier = new CyclicBarrier(100);
        CountDownLatch latch = new CountDownLatch(100);
        Set<String> result= new HashSet<>(100);
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                String code = commonBiz.getOrderCode("IS");
                System.out.println(code);
                result.add(code);
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println(result.size());
        Assert.assertTrue(result.size()==100);
    }

  这里模拟了100个线程。通过CyclicBarrier来保证100个线程同时掉获取单号的操作。然后通过CountDownLatch保证100个线程都执行完,在判断执行的结果,获取的订单编号放到一个set里面,如果最终set的size是100个,说明100个线程在并发情况下,获取的单号没有重复,执行成功。

总结

  这个需求的难点其实是在凌晨这一刻,key失效的时候,多进程,同时同线程来set的问题。多进程通过分布式锁来保证只有一个进程操作,set不是操作,主要原因是get判断值为空和set一个0值进去,不是原子操作,其实有些集合提供的有putIfAbsent()此类的原子操作方法,因此只能通过锁来保证原子性。这里又复用了分布式锁的阻塞性来保证getAndSet的原子性,同时需要考虑临界区的问题,不能只关注第一个拿到锁的线程,还得考虑第一个线程释放锁后,第二个线程拿到锁的情况。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值