分散式環境利用資料庫生成連續唯一序列
阿新 • • 發佈:2020-08-21
生成連續唯一的序列號在很多業務場景都會需要,本文分享一個在分散式環境利用資料庫生成連續唯一序列的例子(按天生成),在併發不是特別高的大型場景還是值得一幹。文中採用版本(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
表