一、前言
之前做个一个购票项目,在这里记录下,生成订单号的代码,以备后续参考.
思路:订单号是不能重复的,每个订单加1,且不能重复。因为多用户可能同时创建订单,导致订单号重复,所以用redis的自增incr。
二、环境
- jdk 1.8
- redis
三、枚举类
枚举类配置各种单号(订单号、退款单号、对账单号)等,之后按需添加即可。
/**
* 单号生成类型枚举
*
* @author qiao
* @since 2022-03-18
*/
@Getter
public enum NumberTypeEnum {
/**
* 订单号:
* 固定前缀:ORDER
* 流水号长度:7
* 随机数长度:3
*/
ORDER_NUMBER("ORDER", "ls:bus:order",7, 3),
/**
* 退款单号:
* 固定前缀:REFUND
* 流水号长度:7
* 随机数长度:3
*/
REFUND_NUMBER("REFUND" , "ls:bus:refund",7, 3),
/**
* 子订单票号
* 固定前缀:TICKET
* 流水号长度:7
* 随机数长度:3
*/
TICKET_NUMBER("TICKET" , "ls:bus:ticket",7, 3),
/**
* 对账单号
* 固定前缀:RECONCILIATION
* 流水号长度:7
* 随机数长度:3
*/
RECONCILIATION_NUMBER("RECONCILIATION" , "ls:bus:reconciliation",7, 3);
/**
* 单号前缀
* 为空时填""
*/
private String prefix;
/**
* redis key
*/
private String redisKey;
/**
* 流水号长度 redis自增 不够补0
*/
private Integer serialLength;
/**
* 随机数长度
*/
private Integer randomLength;
NumberTypeEnum(String prefix, String redisKey, Integer serialLength, Integer randomLength) {
this.prefix = prefix;
this.redisKey = redisKey;
this.serialLength = serialLength;
this.randomLength = randomLength;
}
}
四、具体实现
1、接口
public interface IBusOrderService extends IService<BusOrder> {
/**
* 生成单号
*
* @param numberTypeEnum 单号生成规则
* @return
*/
String generateOrderNumber(NumberTypeEnum numberTypeEnum);
}
2、实现类
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class BusOrderServiceImpl extends ServiceImpl<BusOrderMapper, BusOrder> implements IBusOrderService {
@Autowired
private RedisUtil redisUtil;
/**
* 生成订单号
*
* @param numberTypeEnum
* @return
*/
@Override
public String generateOrderNumber(NumberTypeEnum numberTypeEnum) {
Calendar instance = Calendar.getInstance();
instance.setTime(new Date());
int day = instance.get(Calendar.DAY_OF_YEAR);
String dayFormat = String.format("%1$03d", day);
String incrKey = numberTypeEnum.getRedisKey().concat(dayFormat);
long incr = redisUtil.incr(incrKey, 1);
redisUtil.expire(incrKey, BusConstants.THREE_DAY_SECONDS);
String orderPrefix = BusUtils.completionPrefix(numberTypeEnum.getPrefix());
String random = BusUtils.completionRandom(numberTypeEnum.getRandomLength());
String serial = BusUtils.completionSerial(incr, numberTypeEnum.getSerialLength());
return MessageFormat.format("{0}{1}{2}", orderPrefix, random, serial);
}
}
3、RedisUtils.java
@Component
public class RedisUtil {
/**
* 递增
*
* @param key 键
* @param by 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
4、BusUtils.java
public class BusUtils {
/**
* 生成订单号前缀
* @param prefix
* @return
*/
public static String completionPrefix(String prefix){
Calendar instance = Calendar.getInstance();
instance.setTime(new Date());
int year = instance.get(Calendar.YEAR);
int day = instance.get(Calendar.DAY_OF_YEAR);
int hour = instance.get(Calendar.HOUR_OF_DAY);
String dayFormat = String.format("%1$03d", day);
String hourFormat = String.format("%1$02d", hour);
return MessageFormat.format("{0}{1}{2}{3}", prefix, (year - 2000), dayFormat, hourFormat);
}
/**
* 补全随机数
* @param length 长度
* @return
*/
public static String completionRandom(int length) {
StringBuffer sb = new StringBuffer();
//随机数长度
if (length > 0) {
Random random = new Random();
for (int i = 0; i < length; i++) {
//十以内随机数补全
sb.append(random.nextInt(10));
}
}
return sb.toString();
}
/**
* 补全流水号
* @param incrementalSerial redis当日自增数
* @param total 长度
* @return
*/
public static String completionSerial(Long incrementalSerial, int total){
String express = MessageFormat.format("%1$0{0}d", total);
String dayFormat = String.format(express, incrementalSerial);
return dayFormat;
}
}
5、调用
String orderNumber = this.generateOrderNumber(NumberTypeEnum.ORDER_NUMBER);
6、测试并发单号是否重复
这里实在其他大佬哪里找的代码,其实也可以用 jemter 测试并发
/**
* 生产单号并发测试
*/
@Test
public void testLoad() {
// 1. 定义闭锁来拦截线程
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(100);
ConcurrentLinkedQueue<String> strings = new ConcurrentLinkedQueue<>();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
try {
startGate.await();
String s = busOrderService.generateOrderNumber(NumberTypeEnum.ORDER_NUMBER);
// ORDER2207612480000001
// System.out.println(s);
strings.add(s);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
});
thread.start();
}
// 3. 线程统一放行,并记录时间!
long start = System.nanoTime();
startGate.countDown();
try {
endGate.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("cost times :" + (end - start));
strings.forEach(System.out::println);
List<String> collect = strings.stream().distinct().collect(Collectors.toList());
log.info("去重结果:{}", collect.size() == strings.size());
}
总结
之前总是项目要用被动去学、去抄,过了一段时间,再用到,又会遗忘,再次浏览大佬博客。自己想着改变现状,便有了复盘,将自己用过的技术点,变成帖子,加深记忆,学习思维逻辑,方便以后查阅。
学海无涯