1. 程式人生 > 其它 >基於MongoDB實現自增ID

基於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,不會出現查詢到的文件沒有儲存完整的情況。