介紹
本次采用mysql處理,性能不是很好,對於高並發有要求的建議不要采用
公司一個小項目,需要生成一個單據號,格式為: 日期 + 每日重新自增號,自己考慮了一下每日自增需要考慮並發和持久問題,兩種數據庫redis和mysql由於項目較小,所以沒有redis因為這個增加一個redis好像有點不值得,所以采用mysql作為持久化處理,一下思路也是借鑒了網上的許多想法
源碼
源碼查看規則
源碼位置: blog-study:module-utils:work-no
歡迎大家隨時批評指正
思路
- 首先根據需求,需要這樣一個格式: 20200101 + 00001 的單據號
- 分析日期只需要獲取當前時間格式化即可簡單,
自增號:
1)需要考慮每日重新開始
2)宕機數據可以持久化(存庫即可)
3)並發獲取單據號唯一
以上四個問題只需要考慮 每日重新開始計數和並發問題 - 每日重新開始,在數據庫中加一個時間標志,每次獲取都與當前時間做對比然后做相應處理, 並發問題我能想到的就是加鎖(編程鎖或者數據庫鎖,我這里采用lock鎖)
- 以上基本可以解決項目問題了,然后思考是否可以用於其它業務號的生成
1)於是想可以增加業務標識、前綴和后綴來滿足不同業務,其中業務標識不參與單據號組成只是標識某一類單據號,這樣可以保證不同業務可以使用相同的單據號,前綴是根據業務需要標識業務單據號的參與單據號生成, 后綴的作用是干擾隨機數,可以盡可能防止他人直接看出來業務單據每日的生成數量
格式: GX + 20200101 + 00001 + 23
2) 每日重新開始計數,是否可以改為可選擇的,每月重新計數,每年重新計數,或者一直不需要重新計數 - 思考是否可以封裝為組件,引入依賴直接就可以使用,想了一下自己目前還不具備這種能力吧,以后還要多學多了解,也希望大家可以多多指點,小子將不勝感激。
功能介紹
- 數據庫結構(使用實體類代替)
/**
* 業務唯一標識(只是作為唯一標識,並不參與單據號的生成)
*/
private String workNo;
/**
* 業務碼前綴
*/
private String prefix;
/**
* 序號
*/
private Integer serialNum;
/**
* 序號長度
*/
private Integer serialLength;
/**
* 序號之后的隨機數長度(干擾串)
*/
private Integer randomLength;
/**
* 重置模式(0:無需重置,1: 按天重置, 2: 按月重置, 3: 按年重置)
*/
private Integer restType;
/**
* 重置時間(檢測是否需要重置使用)
*/
private LocalDate restTime;
/**
* 創建時間
*/
private LocalDateTime createTime;
/**
* 修改時間
*/
private LocalDateTime updateTime;
/**
* 備注
*/
private String remark;
- 業務號格式使用枚舉規定好,每次啟動項目自動識別處理
/**
* 為了保證每次生成的單號一致性,所以初始化作為第一次初始化成功后不再修改
*/
ORDER_NO("XG", "XG", 5, 2, 1, "小郭測試單據號");
/**
* 業務唯一標識(只是作為唯一標識,並不參與單據號的生成)
*/
private String workNo;
/**
* 業務碼前綴
*/
private String prefix;
/**
* 序號長度
*/
private Integer serialLength;
/**
* 序號之后的隨機數長度(干擾串)0代表沒有干擾
*/
private Integer randomLength;
/**
* 重置模式(0:無需重置,1: 按天重置, 2: 按月重置, 3: 按年重置)
*/
private Integer restType;
/**
* 備注
*/
private String remark;
項目啟動初始化:
@Configuration
public class WorkNoInit implements ApplicationRunner {
@Autowired
private WorkNoDao workNoDao;
/**
* 單據號初始化
*
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
WorkNoInfo workNoInfo = null;
for (WorkInfoEnum workInfoEnum : WorkInfoEnum.values()) {
workNoInfo = new WorkNoInfo(workInfoEnum);
//查詢是否已有該業務單據號(沒有初始化)
if (workNoDao.isHaveWorkNo(workNoInfo.getWorkNo()) == 0) {
workNoDao.addOrderNumInfo(workNoInfo);
}
}
}
}
- 獲取單據號:
@Component
public class WorkNoService {
@Autowired
private WorkNoDao workNoDao;
private static Lock lock = new ReentrantLock();
/**
* 獲取業務序列碼
*
* 流程:
* 1. 加鎖保證線程安全
* 2. 查詢數據庫中的業務信息
* 3. 判斷重置模式修改數據庫中相應的數據
* 4. 返回業務碼
*
* @param workInfoEnum 枚舉
* @return 業務序列碼
*/
public String getOrderNo(WorkInfoEnum workInfoEnum) {
lock.lock();
LocalDateTime localDateTime = LocalDateTime.now();
try {
WorkNoInfo workNoInfo = workNoDao.queryByWorkNo(workInfoEnum.getWorkNo());
if (workNoInfo == null) {
throw new ErrorCodeException(500, "查詢生成業務碼基礎數據失敗!");
}
//序號
Integer serialNum = workNoInfo.getSerialNum();
//判斷是否需要重置序號
if (workNoInfo.getRestType() == 1) {
String dayTime = localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String dataBaseTime = workNoInfo.getRestTime().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
if (!dataBaseTime.equals(dayTime)) {
serialNum = 0;
}
} else if (workNoInfo.getRestType() == 2) {
String monthTime = localDateTime.format(DateTimeFormatter.ofPattern("yyyyMM"));
String dataBaseTime = workNoInfo.getRestTime().format(DateTimeFormatter.ofPattern("yyyyMM"));
if (!dataBaseTime.equals(monthTime)) {
serialNum = 0;
}
} else if (workNoInfo.getRestType() == 3) {
String yearTime = localDateTime.format(DateTimeFormatter.ofPattern("yyyy"));
String dataBaseTime = workNoInfo.getRestTime().format(DateTimeFormatter.ofPattern("yyyy"));
if (!dataBaseTime.equals(yearTime)) {
serialNum = 0;
}
}
workNoInfo.setSerialNum(serialNum + 1);
workNoInfo.setRestTime(localDateTime.toLocalDate());
//更新數據庫
workNoDao.updateOrderNo(workNoInfo);
return workNoInfo.getNo();
} catch (Exception e) {
throw new ErrorCodeException(500, "生成業務碼失敗" + e.getMessage());
} finally {
lock.unlock();
}
}
}