因最近需要有個業務需要實現一個自增的流水號,其中細節值得學習,故記錄下,以便反思總結。
因為項目問題,故優先考慮在已存在的技術上進行實現,所以博豬優先想到的是:
在MongoDB中,使用單獨的集合來存放指定key對應的最大值,然后每次生成流水號時默認查詢指定key對應的最大值,取出對應的主鍵的最大值+1,然后更新即可。博豬使用
AtomicInteger來進行對應主鍵更新的原子性操作,但是在多線程測試時發現博豬對應MongoDB的數據操作有問題,造成了幻讀現象,所以這個方案PASS掉了。最終方案博豬基於了Redis自增后實現的,下面直接上代碼。
創建自增ID流水池
定義集合
@Data
@Document(collection = "MAKEUP_SERIAL_NUM_POOL")
public class MakeUpSerialNumPool {
@Id
@JsonIgnore
private ObjectId _id;
/** key值,業務組裝,保持唯一 */
private String key;
/** 當前基數 */
private Integer countNum = 0;
}
創建DAO
/**
* @ClassName MakeUpSerialNumPoolRepository
* @Description 自增ID記錄池
* @Author will
* @Date @2022/2/9 15:48
* @Company
*/
public interface MakeUpSerialNumPoolRepository extends MongoRepository<MakeUpSerialNumPool, ObjectId> {
}
創建Service
public interface MakeUpSerialNumPoolService {
/**
* 保存或更新
* @param key
* @return
*/
Integer getSerialNum(String key);
/**
* 保存或更新
* @param key
* @return
*/
MakeUpSerialNumPool findAndModify(String key);
/**
* 刪除
* @param key
*/
void findAndRemove(String key);
}
@Service
public class MakeUpSerialNumPoolServiceImpl implements MakeUpSerialNumPoolService {
@Autowired
private MakeUpSerialNumPoolRepository makeUpSerialNumPoolRepository;
@Autowired
private MongoTemplate mongoTemplate;
@Override
public Integer getSerialNum(String key) {
Query query = new Query(Criteria.where("key").is(key));
Update update = new Update();
update.inc("countNum", 1);
FindAndModifyOptions options = new FindAndModifyOptions();
options.upsert(true);
options.returnNew(true);
MakeUpSerialNumPool pool = mongoTemplate.findAndModify(query, update, options, MakeUpSerialNumPool.class);
return pool.getCountNum();
}
@Override
public MakeUpSerialNumPool findAndModify(String key) {
Query query = new Query(Criteria.where("key").is(key));
Update update = new Update();
update.inc("countNum", 1);
FindAndModifyOptions options = new FindAndModifyOptions();
options.upsert(true);
options.returnNew(true);
MakeUpSerialNumPool pool = mongoTemplate.findAndModify(query, update, options, MakeUpSerialNumPool.class);
return pool;
}
@Override
public void findAndRemove(String key) {
Query query = new Query(Criteria.where("key").is(key));
mongoTemplate.findAndRemove(query, MakeUpSerialNumPool.class);
}
}
封裝ID自增工具類
/**
* 自增主鍵類型
* 業務主鍵前綴(含表達式)+length為自增
*/
@Data
public class AutoIncSeqType {
/* 前綴表達式 */
private String keyPrefix;
/* 序列長度 */
private int length;
/* 日期格式化 */
private String format;
public AutoIncSeqType(String keyPrefix, int length, String format) {
this.keyPrefix = keyPrefix;
this.length = length;
this.format = format;
}
}
@Component
@Slf4j
public class KeyGenerator {
/*【"SN:", "FBDZ{yyyyMM}", 4, "yyyyMM", RedisExpireTypeEnum.NON】*/
public static final String YYMM = "yyMM";
public static final String YYYYMM = "yyyyMM";
public static final String YYYYMMDD = "yyyyMMdd";
@Autowired
private MakeUpSerialNumPoolService makeUpSerialNumPoolService;
/**
* @param incrSeqType
* @return
*/
public String getIncrSeq(AutoIncSeqType incrSeqType) {
return getIncrSeq("", incrSeqType, "");
}
/**
*
* @param incrSeqType
* @param orgCode
* @return
*/
public String getIncrSeq(AutoIncSeqType incrSeqType, String orgCode) {
return getIncrSeq("", incrSeqType, orgCode);
}
/**
* 生成日期 自增序號
* @param prefix 前綴,為空則不加
* @param incrSeqType 業務配置
* @param orgCode 經銷商、機構等代碼
* @return
*/
public String getIncrSeq(String prefix, AutoIncSeqType incrSeqType, String orgCode) {
String dateInfo = DateUtils.formatDate(new Date(), incrSeqType.getFormat());
String key = incrSeqType.getKeyPrefix().replaceAll("\\{" + incrSeqType.getFormat() + "\\}", dateInfo);
key = key.replaceAll("\\{orgCode\\}", orgCode);
String keyInfo = StringUtils.isNotEmpty(prefix) ? prefix + key : key;
try {
Integer incr = getIncr(keyInfo);
if(incr == 0) {
incr = getIncr(keyInfo);//從001開始
}
return keyInfo.replace(":","") + String.format("%0" + incrSeqType.getLength() +"d", incr);
} catch (Exception e) {
e.printStackTrace();
log.error("MongoDB生成自增異常:", e);
/* 異常時自動生成隨機序列號,E結尾*/
return keyInfo + RandomUtils.getRandomNumbers(incrSeqType.getLength()) + "E";
}
}
public Integer getIncr(String key) {
MakeUpSerialNumPool makeUpSerialNumPool = makeUpSerialNumPoolService.findAndModify(key);
String month = key.split(":")[1];
String currentMonth = String.valueOf(DateUtil.format(new Date(), YYYYMM));
if (makeUpSerialNumPool == null || !month.equals(currentMonth)) {
makeUpSerialNumPoolService.findAndRemove(key);
}
return makeUpSerialNumPool.getCountNum();
}
}
public class RandomUtils {
private static char[] codeSequence = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
private static char[] numSequence = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
private static SecureRandom random = new SecureRandom();
public RandomUtils() {
}
public static String getRandomChars() {
Random random = new Random();
StringBuffer sBuffer = new StringBuffer();
for(int i = 0; i < 14; ++i) {
sBuffer.append(codeSequence[random.nextInt(62)]);
}
return sBuffer.toString();
}
public static String getRandomChars(int length) {
Random random = new Random();
StringBuffer sBuffer = new StringBuffer();
if (length < 1) {
length = 14;
}
for(int i = 0; i < length; ++i) {
sBuffer.append(codeSequence[random.nextInt(62)]);
}
return sBuffer.toString();
}
public static String getRandomNumbers(int length) {
Random random = new Random();
StringBuffer sBuffer = new StringBuffer();
if (length < 1) {
length = 14;
}
for(int i = 0; i < length; ++i) {
sBuffer.append(numSequence[random.nextInt(10)]);
}
return sBuffer.toString();
}
public static String generateRandomString(int numBytes) {
if (numBytes < 1) {
throw new IllegalArgumentException(String.format("numBytes argument must be a positive integer (1 or larger)", (long)numBytes));
} else {
byte[] bytes = new byte[numBytes];
random.nextBytes(bytes);
return Hex.encodeHexString(bytes);
}
}
}
使用Demo
@Autowired
private KeyGenerator keyGenerator;
String key = agentCode + ":" + currentMonth;
AutoIncSeqType autoIncSeqType = new AutoIncSeqType(key, 4, dateFormat);
String incrSeq = keyGenerator.getIncrSeq(null, autoIncSeqType, agentCode);
心得
上述方法博豬本地測試了一下單次循環,5k的線程沒有問題,由於博豬電腦配置較低就沒有再進行深入的測試,反正使用是沒有太大的問題。
下面說一下博豬的心得:
上面的方法其實和博豬第一的思考方式是一樣的,但是博豬之前考慮的是從Java層面解決並發導致的事務問題,所以沒有仔細的研究MongoDB
mongodb不支持事務,所以,在你的項目中應用時,要注意這點。無論什么設計,都不要要求mongodb保證數據的完整性。但是mongodb提供了許多原子操作,比如文檔的保存,修改,刪除等,都是原子操作。
所謂原子操作就是要么這個文檔保存到Mongodb,要么沒有保存到Mongodb,不會出現查詢到的文檔沒有保存完整的情況。
