1. 程式人生 > 實用技巧 >分散式環境利用資料庫生成連續唯一序列

分散式環境利用資料庫生成連續唯一序列

生成連續唯一的序列號在很多業務場景都會需要,本文分享一個在分散式環境利用資料庫生成連續唯一序列的例子(按天生成),在併發不是特別高的大型場景還是值得一幹。文中採用版本(version)機制,結合自旋鎖 + 樂觀鎖生成連續的唯一的數字。

直接上程式碼

1. 建立表test_no

CREATE TABLE `test_no` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `date` varchar(10) NOT NULL COMMENT '格式yyyyMMdd',
  `number` bigint(20) unsigned DEFAULT NULL COMMENT '序列號',
  `version` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '版本',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_date` (`date`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;

date欄位建有唯一索引。

2. MyBaties持久層程式碼TestNoMapper

package com.mingo.exp.generate_number.single_db_cas;

import com.mingo.exp.generate_number.single_db_cas.dto.TestNoDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * @author Doflamingo
 */
public interface TestNoMapper {

    /**
     * 插入一條資料。主要用於初始值插入
     *
     * @param noDO
     */
    @Insert("INSERT INTO test_no(`date`,number,version) VALUES(#{date}, #{number}, #{version})")
    void insert(TestNoDO noDO);

    /**
     * 查詢資料。date 具有唯一鍵
     *
     * @param date
     * @return
     */
    @Select("SELECT date,number,version FROM test_no WHERE `date` = #{date}")
    TestNoDO select(@Param("date") String date);

    /**
     * 只有與當前版本號一樣才能更新
     *
     * @param date
     * @param version
     * @return
     */
    @Update("UPDATE test_no SET number = number + 1, version = version + 1 WHERE `date` = #{date} AND version = #{version}")
    int update(@Param("date") String date, @Param("version") Integer version);
}

3. 生成連續唯一序列程式碼DistributeSerialNumber

package com.mingo.exp.generate_number.single_db_cas;

import com.mingo.exp.generate_number.single_db_cas.dto.TestNoDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 分散式環境利用資料庫生成連續唯一序列
 *
 * @author Doflamingo
 */
@Component
public class DistributeSerialNumber {

    @Autowired
    private TestNoMapper testNoMapper;

    /**
     * 查詢唯一號碼
     *
     * @param date 格式 yyyyMMdd
     * @return
     */
    public int get(String date) {
        TestNoDO testNoDO = this.selectOrInsert(date);
        boolean flag = this.update(testNoDO);
        if (flag) {
            return testNoDO.getNumber();
        }
        // 自旋鎖 + 樂觀鎖
        while (!flag) {
            testNoDO = testNoMapper.select(date);
            // 更新number
            flag = this.update(testNoDO);
        }
        return testNoDO.getNumber();
    }

    /**
     * 這裡主要用於當前首次查詢和插入資料,保證直插入一條資料,date建有唯一索引;
     * 併發度不大也可以不用雙重校驗鎖,直接插入,異常再查詢即可
     *
     * @Param date yyyyMMdd
     */
    private TestNoDO selectOrInsert(String date) {
        TestNoDO testNoDO = testNoMapper.select(date);
        if (null == testNoDO) {
            try {
                testNoDO = new TestNoDO(date, 1, 0);
                synchronized (this) {
                    TestNoDO testNoDO2 = testNoMapper.select(date);
                    if (null != testNoDO2) {
                        return testNoDO2;
                    }
                    testNoMapper.insert(testNoDO);
                }
            } catch (Exception e) {
                // 插入失敗,其他機器已經插入了
                testNoDO = testNoMapper.select(date);
            }
        }
        return testNoDO;
    }

    /**
     * 按版本號更新資料
     *
     * @param testNoDO
     * @return true 即 成功
     */
    private boolean update(TestNoDO testNoDO) {
        return 1 == testNoMapper.update(testNoDO.getDate(), testNoDO.getVersion());
    }
}

4. 測試類DistributeSerialNumberTest

為了測試效果,我用20執行緒生成序列號

package com.mingo.exp.generate_number.single_db_cas;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@SpringBootTest
@RunWith(SpringRunner.class)
public class DistributeSerialNumberTest {

    @Autowired
    private DistributeSerialNumber serialNumber;

    @Test
    public void test() throws Exception {
        System.out.println("\n\n==============================\n");

        // 20個執行緒
        ExecutorService executorService =
                new ThreadPoolExecutor(
                        20,
                        20,
                        0,
                        TimeUnit.MILLISECONDS,
                        new LinkedBlockingDeque<>()
                );

        for (int i = 30; i > 0; i--) {
            // 啟動
            executorService.execute(() -> {
                int number = serialNumber.get("20200608");
                System.out.println("獲得的數:" + number);
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.SECONDS);
    }
}

 

5. 測試結果

獲得的數:2
獲得的數:1
獲得的數:3
獲得的數:4
獲得的數:6
獲得的數:5
獲得的數:13
獲得的數:8
獲得的數:10
獲得的數:14
獲得的數:9
獲得的數:11
獲得的數:7
獲得的數:12
獲得的數:15
獲得的數:18
獲得的數:17
獲得的數:16
獲得的數:20
獲得的數:26
獲得的數:24
獲得的數:21
獲得的數:25
獲得的數:27
獲得的數:28
獲得的數:23
獲得的數:19
獲得的數:22
獲得的數:30
獲得的數:29