基于MongoDB实现自增ID
因最近需要有个业务需要实现一个自增的流水号,其中细节值得学习,故记录下,以便反思总结。
因为项目问题,故优先考虑在已存在的技术上进行实现,所以博猪优先想到的是:
在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,不会出现查询到的文档没有保存完整的情况。