基于MongoDB实现自增ID

因最近需要有个业务需要实现一个自增的流水号,其中细节值得学习,故记录下,以便反思总结。

因为项目问题,故优先考虑在已存在的技术上进行实现,所以博猪优先想到的是:

==在MongoDB中,使用单独的集合来存放指定key对应的最大值,然后每次生成流水号时默认查询指定key对应的最大值,取出对应的主键的最大值+1,然后更新即可。博猪使用AtomicInteger来进行对应主键更新的原子性操作,但是在多线程测试时发现博猪对应MongoDB的数据操作有问题,造成了幻读现象,所以这个方案PASS掉了。==

最终方案博猪基于了Redis自增后实现的,下面直接上代码。

创建自增ID流水池

定义集合

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Document(collection = "MAKEUP_SERIAL_NUM_POOL")
public class MakeUpSerialNumPool {
@Id
@JsonIgnore
private ObjectId _id;
/** key值,业务组装,保持唯一 */
private String key;
/** 当前基数 */
private Integer countNum = 0;

}

创建DAO

1
2
3
4
5
6
7
8
9
/**
* @ClassName MakeUpSerialNumPoolRepository
* @Description 自增ID记录池
* @Author will
* @Date @2022/2/9 15:48
* @Company
*/
public interface MakeUpSerialNumPoolRepository extends MongoRepository<MakeUpSerialNumPool, ObjectId> {
}

创建Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface MakeUpSerialNumPoolService {

/**
* 保存或更新
* @param key
* @return
*/
Integer getSerialNum(String key);

/**
* 保存或更新
* @param key
* @return
*/
MakeUpSerialNumPool findAndModify(String key);

/**
* 删除
* @param key
*/
void findAndRemove(String key);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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自增工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 自增主键类型
* 业务主键前缀(含表达式)+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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@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();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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

1
2
@Autowired
private KeyGenerator keyGenerator;
1
2
3
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,不会出现查询到的文档没有保存完整的情况。


基于MongoDB实现自增ID
https://github.com/yangxiangnanwill/yangxiangnanwill.github.io/2024/01/03/好好码代码吖/JAVA/工具类/基于MongoDB实现自增ID/
作者
will
发布于
2024年1月3日
许可协议