基於MongoDB實現自增ID
阿新 • • 發佈:2022-02-14
因最近需要有個業務需要實現一個自增的流水號,其中細節值得學習,故記錄下,以便反思總結。
因為專案問題,故優先考慮在已存在的技術上進行實現,所以博豬優先想到的是:
在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,不會出現查詢到的文件沒有儲存完整的情況。