需求&业务场景
??没有需求或者业务场景,去谈技术就是空中楼阁~
前置条件
● 分布式部署 ● 多实例
业务需求
● 不同业务,有该业务标识且自增的单号。 ● 单号规则 业务标识+日期+4位自增数字 ● 4位自增数字是表示当天的,凌晨清零
构思
?? 因为有多个实例,所以在操作自增数字的时候需要用到分布式锁,同时需要当天凌晨清零,很容易想到redis,缓存一个key值,失效时间是到凌晨。同时,redis提供原子操作的自增指令。至于分布式锁,考虑用reddsion的红锁。 另外一个需要考虑的点就是凌晨失效的那个点的那一刻,并发问题。 ● reddsion的红锁解决分布式,多个实例操作问题 ● 给key设置一个到凌晨的失效时间 ● 考虑凌晨失效时候的并发问题 ● 保证自增的原子操作
实现
获取到第二天凌晨的毫秒数
public Long getNowToNextDayMilliseconds() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, 1);
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 (null != value) {
return getCode(key, redisTemplate.opsForValue().increment(key).toString());
}
RLock lock = redissonClient.getLock(CommonConstant.ORDER_CODE_LOCK_KEY + key);
try {
while (true) {
if (lock.tryLock(CommonConstant.INTEGER_FIVE, TimeUnit.MICROSECONDS)) {
if (null == redisTemplate.opsForValue().get(key)) {
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) {
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 {
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的原子性,同时需要考虑临界区的问题,不能只关注第一个拿到锁的线程,还得考虑第一个线程释放锁后,第二个线程拿到锁的情况。
|