没有需求或者业务场景,去谈技术就是空中楼阁~
● 分布式部署
● 多实例
● 不同业务,有该业务标识且自增的单号。
● 单号规则 业务标识+日期+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就可以了
如果值存在,直接自增返回,redis的incr本身是原子操作,且redis是单线程的,可以保证线程安全,同时也能保证多进程情况下,拿到的值是唯一的。
当值是不存在的时候,需要去set值了。这个操作不是原子性的,而且分布式的情况下,A实例的set可能把B实例set的值覆盖掉。这个时候需要一个分布式锁。redssion的lock实现了AQS的接口,可以通过tryLock去尝试获取分布式锁。如果获取锁成功,则执行下一步。
即使获取分布式锁成功,也需要考虑本地并发问题,主要是需要考虑临界区的线程问题,第一个拿到锁的执行完了,会释放锁,这个时候临界区等待的线程就可以拿到锁了,也会进到这段逻辑,所以需要在判空操作一下。
如果没有获取到锁,也没有必要继续去循环获取锁,因为这个时候,可能拿到锁的线程已经把初始值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的原子性,同时需要考虑临界区的问题,不能只关注第一个拿到锁的线程,还得考虑第一个线程释放锁后,第二个线程拿到锁的情况。