一. Spring Boot + Redisson 生成运单号
<!--整合redission框架start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.12.5</version>
</dependency>
<!--整合redission框架end-->
public abstract class StrategyHandler<T> {
public abstract Response<T> handler(T t);
public abstract T handler();
}
import java.util.HashMap;
import java.util.Map;
public class TransOrderSequenceStrategy extends StrategyHandler<Map<String,String>> {
private static final String SEQUENCE_REDIS_KEY = "trans:order:sequence:%s:%s";
@Override
public Response<Map<String, String>> handler(Map<String, String> map) {
return null;
}
@Override
public Map<String, String> handler() {
Map<String,String> redisValueMap = new HashMap<>();
redisValueMap.put("nameSpace",SEQUENCE_REDIS_KEY);
redisValueMap.put("prefix","D");
return redisValueMap;
}
}
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author wanghong
* @desc: 发号器
* @date 2021/07/22:47
**/
@Service
public class SequenceManagerBusiness {
@Autowired
private RedissonClient redissonClient;
private static Map<String, StrategyHandler<Map<String,String>>> REDIES_PREFIX_MAP;
private static String SEQUENCE_REDIS_KEY;
static {
REDIES_PREFIX_MAP = new HashMap<>();
REDIES_PREFIX_MAP.put("order",new TransOrderSequenceStrategy());
}
public String generateSequence(String key){
StrategyHandler<Map<String,String>> taskStrategy = REDIES_PREFIX_MAP.get(key);
Map<String,String> sequenceMap = taskStrategy.handler();
String prefix = sequenceMap.get("prefix");
SEQUENCE_REDIS_KEY = sequenceMap.get("nameSpace");
return generateSequence0(prefix);
}
private String generateSequence0(String prefix) {
StringBuilder sb = new StringBuilder();
sb.append(prefix);
SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmm");
String day = sdf.format(new Date());
sb.append(day);
Long sequence = getSequence(prefix, day);
String sequenceStr = sequence.toString();
int count;
if (sequenceStr.length() < 5) {
count = 5 - sequenceStr.length();
} else {
return sb.append(sequence).toString();
}
for (int i = 0; i < count; i++) {
sb.append("0");
}
return sb.append(sequence).toString();
}
private Long getSequence(String prefix, String day) {
long sequence;
String key = String.format(SEQUENCE_REDIS_KEY, prefix, day);
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
if (!atomicLong.isExists()) {
sequence = atomicLong.incrementAndGet();
atomicLong.expire(1L, TimeUnit.MINUTES);
} else {
sequence = atomicLong.incrementAndGet();
}
return sequence;
}
}
package com.hong;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.hong.redis.SequenceManagerBusiness;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.*;
/**
* @author wanghong
* @desc:
* @date 2021/07/22:50
**/
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {
private static final int CONCURRENCY_LEVEL = 100;
private static final CountDownLatch cdl = new CountDownLatch(CONCURRENCY_LEVEL);
private static ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("RedisTest-pool-%d").build();
@Autowired
private SequenceManagerBusiness sequenceManagerBusiness;
private static final ExecutorService executorService = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() + 1, 10, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
/**
* 测试递增发号器
*/
@Test
public void testSequence0() throws Exception{
String key = "order";
generateSequence0(key);
}
private void generateSequence0(String key) throws Exception{
long start = System.currentTimeMillis();
for (int i = 0; i < CONCURRENCY_LEVEL; i++) {
// 使用 线程池+CountDowmLatch 模拟多线程并发请求
executorService.submit(() -> {
try {
String seq = sequenceManagerBusiness.generateSequence(key);
System.out.println(Thread.currentThread().getName() + "generate seq=" + seq);
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + "执行异常:" + e.getMessage());
} finally {
// 线程启动后,倒计数器-1,表示有一个线程就绪了
cdl.countDown();
}
});
}
// 主线程一直等待 所有获取序列的线程执行完毕
cdl.await();
System.out.println("发号结束,耗时:" + (System.currentTimeMillis()-start)/100 + "s");
executorService.shutdown();
System.out.println("=========================================================");
}
}
// 打印结果:
RedisTest-pool-5generate seq=D210731090300003
RedisTest-pool-8generate seq=D210731090300099
。。。
RedisTest-pool-1generate seq=D210731090300100
发号结束,耗时:1s
=========================================================
二. 现在运单号规则生成调整
我们做的是一个TMS SAAS平台,引入了租户的概念,但数据库表层面,数据未作物理隔离,只做了逻辑隔离,即表加上了一个tenantId,运单表租户的数据都是在一张表上的,当每次查询时,会从请求的上下文中获取tenantId,作为必须的查询条件,防止数据越权。现在产品需求:运单号生成规则调整:以租户+当天日期为维度,生成的运单号以 YD 为前缀,然后拼接上 yyMMdd,然后最少三位开始的递增序号,示例:YD210731001,超过3位自动向上加1。
public abstract class SequenceStrategy<T> {
public abstract T handle();
public abstract String getMaxSeqFromDB(Long tenantId,String prefix);
}
public class TmsTransOrderSequenceStrategy extends SequenceStrategy<Map<String, String>> {
private static final String SEQUENCE_REDIS_KEY = "tms:saas:trans:order:sequence:%s:%s:%s";
private static final String PREFIX = "YD";
public static final String BIZ_FLAG = "transOrder";
@Override
public Map<String, String> handle() {
Map<String, String> redisValueMap = new HashMap<>();
redisValueMap.put("nameSpace", SEQUENCE_REDIS_KEY);
redisValueMap.put("prefix", PREFIX);
return redisValueMap;
}
@Override
public String getMaxSeqFromDB(Long tenantId, String prefix) {
return SpringUtils.getBean(TmsTransOrderMapper.class).selectMaxSeqFromDB(tenantId, prefix);
}
}
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @description: 递增发号器
* @author: 王宏
* @email hong.wang8@amh-group.com
* @date: 2021/6/25 14:17
* @version: 1.0
*/
@Component
public class SequenceGenerateBusiness {
// SimpleDateFormat非线程安全,使用 java8 的 DateTimeFormatter 替代
private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyMMdd");
@Autowired
private RedissonClient redissonClient;
private static Map<String, SequenceStrategy<Map<String, String>>> REDIES_PREFIX_MAP;
private static String SEQUENCE_REDIS_KEY;
static {
REDIES_PREFIX_MAP = new HashMap<>();
REDIES_PREFIX_MAP.put(TmsTransOrderSequenceStrategy.BIZ_FLAG, new TmsTransOrderSequenceStrategy());
}
/**
* 多租户序列号生成,不同租户并发生成隔离
*/
public String generateSequence(Long tenantId, String key) {
if (Objects.isNull(tenantId)) {
throw new RuntimeException("SequenceGenerateBusiness.generateSequence 请求参数tenantId不能为空");
}
SequenceStrategy<Map<String, String>> taskStrategy = REDIES_PREFIX_MAP.get(key);
Map<String, String> sequenceMap = taskStrategy.handle();
String prefix = sequenceMap.get("prefix");
SEQUENCE_REDIS_KEY = sequenceMap.get("nameSpace");
StringBuilder sb = new StringBuilder(prefix);
LocalDate now = LocalDate.now();
String day = now.format(dtf);
sb.append(day);
Long sequence = getSequence(taskStrategy,tenantId, prefix, day);
String sequenceStr = sequence.toString();
int len = sequenceStr.length();
if (len >= 3) {
return sb.append(sequenceStr).toString();
}
int diffBitCount = 3 - len;
while (diffBitCount > 0) {
sb.append("0");
diffBitCount--;
}
return sb.append(sequenceStr).toString();
}
// 运单号规则调整: YD+2位年+2位月+2位日+3位流水号; 生成运单号的效果: YD210624001 ; 如果超过流水号超过3位,自动加1;例如 YD210624999,自动加1后 YD2106241000;
private Long getSequence(SequenceStrategy<Map<String, String>> taskStrategy,Long tenantId, String prefix, String day) {
Long sequence;
String key = String.format(SEQUENCE_REDIS_KEY, tenantId, prefix, day);
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
if (!atomicLong.isExists()) {
/**
* 增加 redis key被异常删除的情况判断:
* 如果该key被意外删掉了,则会再次进入这里,生成的单号又是从 1开始,如果当天同一租户有创建运单,就会造成运单号重复
* 因为之前设置的单号递增时间格式为 yyMMddHHmm SequenceManagerBusiness.java,时间粒度到分,设置的key有效期为1分钟,
* 即使key被意外删除,也只是影响 key 被删除时的那1分钟内可能有运单号重复;
* 现在的单号时间格式为 yyMMdd,生成的粒度一下子放大到了天,就会造成 key被意外删除的当天都有部分运单号重复;
*
* 比如:在 2021-07-02 00:00:00 ~ 12:00:00 租户 10001L 已经产生了单号 YD210702001,YD210702002,
* 然后在 12:00:01 时 key被意外删除,这时,该租户再来创建运单,就会产生重复的 单号:YD210702001,YD210702002,
* 直到 YD210702003之后才没有问题;
*/
String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);
if (StringUtils.isNotEmpty(maxSeqFromDB)){ // 数据库中已经产生了运单号,但未知原因,又重新从头开始,需要重置序号
String dbSeq = maxSeqFromDB.substring(prefix.length() + day.length());
if (dbSeq.startsWith("0")){
int index = 0;
char[] chars = dbSeq.toCharArray();
for (char c:chars){
if ('0' == c){
index++;
}else {
break;
}
}
dbSeq = dbSeq.substring(index);
}
atomicLong.set(Long.valueOf(dbSeq));
}
sequence = atomicLong.incrementAndGet();
// 设置有效期为 当前时间 到 当天零点的 剩余时间
LocalTime midnight = LocalTime.MIDNIGHT;
LocalDateTime todayMidnight = LocalDateTime.of(LocalDate.now(), midnight);
LocalDateTime tomorrowMidnight = todayMidnight.plusDays(1);
long seconds = TimeUnit.NANOSECONDS.toSeconds(Duration.between(LocalDateTime.now(), tomorrowMidnight).toNanos());
atomicLong.expire(seconds, TimeUnit.SECONDS);
} else {
sequence = atomicLong.incrementAndGet();
}
return sequence;
}
}
<select id="selectMaxSeqFromDB" resultType="java.lang.String">
SELECT max(trans_order_no)
FROM tms_trans_order
WHERE tenant_id=#{tenantId}
AND trans_order_no like concat(#{prefix},'%')
AND TO_DAYS(create_time)=TO_DAYS(NOW())
for update
</select>
三. 并发下是否会出现重复单号?
批量导入运单,同一租户下运单号偶发出现重复。
线上查询到的日志如下:
-- 同一个机器上的日志
trace 2021-08-18 15:39:08.755 INFO [pool-36-thread-2] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init base db,tenantId=100000000001577327,prefix=YD,day=210818,maxSeqFromDB=YD210818005
trace 2021-08-18 15:39:08.638 INFO [pool-36-thread-1] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init base db,tenantId=100000000001577327,prefix=YD,day=210818,maxSeqFromDB=YD210818003
trace 2021-08-18 15:39:08.756 INFO [pool-36-thread-2] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
trace 2021-08-18 15:39:08.638 INFO [pool-36-thread-1] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
trace 2021-08-18 15:39:08.523 INFO [pool-36-thread-3] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
trace 2021-08-18 15:39:08.363 INFO [pool-36-thread-4] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
select d.tenant_id,d.trans_order_id,d.trans_order_no,d.create_time,d.import_id from tms_trans_order d
where d.tenant_id=100000000001577327 and DATE_FORMAT(d.create_time,'%Y-%m-%d')='2021-08-18' order by d.trans_order_no asc;
出现问题的方法就是上面的 SequenceGenerateBusiness.getSequence()。
该方法依赖redis ,考虑到redis本身的可用性,有可能重启或宕机,导致初始化错误而发生运单号重复,所以加了从数据库查询该租户当前最大的运单号,然后手动set到redis中,保证初始化数据的正确性。
但批量导入运单时(批量导入运单为 @Async(“AsyncTaskThreadExecutor”) 异步导入),开启了多线程导入,这样在初始化中就存在并发查库,比如批量导入的运单有8笔,在2021-08-18 15:39:08这1秒内有4个线程同时进入 if (!atomicLong.isExists()) 判断为true,进入初始化代码中,然后内部执行时序:
1. 在 2021-08-18 15:39:08.363 时间点,pool-36-thread-4线程查询 String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);结果为空,sequence = atomicLong.incrementAndGet(); = YD210818001 ,第一条数据入库;
2. 同上,在2021-08-18 15:39:08.523时间点,第二条数据入库;
3. 在2021-08-18 15:39:08.638时间点,pool-36-thread-1线程走到 String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);查询到结果 maxSeqFromDB = YD210818003,说明在2021-08-18 15:39:08.523 ~ 2021-08-18 15:39:08.638 时间段内有线程走到了 if (!atomicLong.isExists()) {},因为之前已初始化过,所以if为false,走到 else{},生成sequence=YD210818003,然后入库第三条数据;
4. 接着pool-36-thread-1在 atomicLong.set(Long.valueOf(dbSeq)); 即 atomicLong.set(3);此时此刻,正好有另外一个线程执行到了 else{。。。},生成运单号 YD210818004;紧接着pool-36-thread-1 执行到sequence = atomicLong.incrementAndGet();也生成运单号 YD210818004,出现第一次重复单号;
5. 在2021-08-18 15:39:08.638~2021-08-18 15:39:08.755时间段内,一条线程走else{...}生成运单号YD210818005,然后入库;
6. 在2021-08-18 15:39:08.755时间点,pool-36-thread-2还在if (!atomicLong.isExists()) {}分支里,并且查询到了maxSeqFromDB=YD210818005;然后接下来的情况就和 3一样,pool-36-thread-2 走到了atomicLong.set(Long.valueOf(dbSeq));即atomicLong.set(5),同一时间点,另一个线程走到了 else{。。。},sequence = atomicLong.incrementAndGet();=YD210818006;紧接着pool-36-thread-2走到if (!atomicLong.isExists()) { 。。。sequence = atomicLong.incrementAndGet(); } = YD210818006,出现了第二次重复;
优化方案:
通过上面的分析,主要原因就是 atomicLong.set(Long.valueOf(dbSeq)); 和 sequence = atomicLong.incrementAndGet(); 无法保证整体操作的原子性,在两个操作之间,并发操作可能会造成生成的序列号重复。为了避免此问题,增加分布式锁控制序号的初始化逻辑:
// 运单号规则调整: YD+2位年+2位月+2位日+3位流水号; 生成运单号的效果: YD210624001 ; 如果超过流水号超过3位,自动加1;例如 YD210624999,自动加1后 YD2106241000;
private Long getSequence(SequenceStrategy<Map<String, String>> taskStrategy, Long tenantId, String prefix, String day) {
Long sequence = null;
String key = String.format(SEQUENCE_REDIS_KEY, tenantId, prefix, day);
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
if (!atomicLong.isExists()) {
/**
* 当并发多个线程同时判断key不存在,需要排队,保证只有一个线程进行初始化
*/
RLock lock = redissonClient.getLock("saas_tms_trans:sequence_generate:" + key);
try {
/**
* 当第一次初始化时,并发多个线程同时进到这里,租户维度上锁
* tryLock(long waitTime, long leaseTime, TimeUnit unit)
* 尝试加锁,最多等待1秒,上锁以后3秒自动解锁
*/
if (lock.tryLock(1, 3, TimeUnit.SECONDS)) {
// 类似单例模式双检锁思想
if (!atomicLong.isExists()) {
String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);
if (StringUtil.isNotEmpty(maxSeqFromDB)) {
String dbSeq = maxSeqFromDB.substring(prefix.length() + day.length());
if (dbSeq.startsWith("0")) {
int index = 0;
char[] chars = dbSeq.toCharArray();
for (char c : chars) {
if ('0' == c) {
index++;
} else {
break;
}
}
dbSeq = dbSeq.substring(index);
}
atomicLong.set(Long.valueOf(dbSeq));
log.info("SequenceGenerateBusiness.getSequence init base db,tenantId={},prefix={},day={},maxSeqFromDB={}", tenantId, prefix, day, maxSeqFromDB);
}
sequence = atomicLong.incrementAndGet();
// 设置有效期为 当前时间 到 当天零点的 剩余时间
LocalTime midnight = LocalTime.MIDNIGHT;
LocalDateTime todayMidnight = LocalDateTime.of(LocalDate.now(), midnight);
LocalDateTime tomorrowMidnight = todayMidnight.plusDays(1);
long seconds = TimeUnit.NANOSECONDS.toSeconds(Duration.between(LocalDateTime.now(), tomorrowMidnight).toNanos());
atomicLong.expire(seconds, TimeUnit.SECONDS);
log.info("SequenceGenerateBusiness.getSequence init finish,tenantId={},prefix={},day={}", tenantId, prefix, day);
} else {
sequence = atomicLong.incrementAndGet();
log.info("SequenceGenerateBusiness.getSequence current thread come into init method but found other thread has already init,tenantId={},prefix={},day={},sequence={}", tenantId, prefix, day, sequence);
return sequence;
}
}
} catch (Exception e) {
log.error("SequenceGenerateBusiness.getSequence init base db error={},tenantId={},prefix={},day={},sequence={}", e, tenantId, prefix, day, sequence);
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
sequence = atomicLong.incrementAndGet();
log.info("SequenceGenerateBusiness.getSequence has already init,tenantId={},prefix={},day={},sequence={}", tenantId, prefix, day, sequence);
}
return sequence;
}