dotNET Core實現分散式環境下的流水號唯一
- 2019 年 10 月 4 日
- 筆記

業務背景
在管理系統中,很多功能模組都會涉及到各種類型的編號,例如:流程編號、訂單號、合約編號等等。編號各有各自的規則,但通常有一個流水號來確定編號的唯一性,保證流水號的唯一,在不同的環境中實現方式有所不同。本文將介紹在單機和分散式環境中保證流水號唯一的方式。
實現思路
1、在資料庫中創建 seqno 表,每個業務一條數據,存儲業務 code 和流水號的最大值 2、獲取某業務的流水號時,根據業務 code 查詢 seqno 表,獲取流水號返回,並將最大值加一 3、使用 Monitor.Enter 解決單機重複性問題 4、使用 Redis 分散式鎖解決分散式部署的重複性問題
環境
- dotNET Core:2.1
- VS For Mac:2019
- Docker:18.09.2
- MySql:8.0.17,基於Docker構建
- Redis:3.2,基於Docker構建
- CSRedisCore:3.1.5
準備工作
1、執行下面命令構建 Redis 容器
docker run -p 6379:6379 -d --name s2redis_test --restart=always redis:3.2 redis-server --appendonly yes
2、執行下面命令構建 MySql 容器
docker run -d -p 3306:3306 -e MYSQL_USER="oec2003" -e MYSQL_PASSWORD="123456" -e MYSQL_ROOT_PASSWORD="123456" --name s2mysql mysql/mysql-server --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --default-authentication-plugin=mysql_native_password
3、在 MySql 中創建資料庫seqno_test,執行下面 SQL 創建表和測試數據
-- ---------------------------- -- Table structure for seqno -- ---------------------------- DROP TABLE IF EXISTS `seqno`; CREATE TABLE `seqno` ( `code` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL, `num` int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -- ---------------------------- -- Records of seqno -- ---------------------------- BEGIN; INSERT INTO `seqno` VALUES ('order', 1); COMMIT; SET FOREIGN_KEY_CHECKS = 1; 4、在 VS2019 中創建兩個控制台項目和一個類庫項目,如下圖:

單機測試
1、在 SeqNo 類中添加 GetSeqByNoLock 方法
public static string GetSeqNoByNoLock() { string connectionStr = "server = localhost; user id = oec2003; password = 123456; database = seqno_test"; string getSeqNosql = "select num from seqno where code='order'"; string updateSeqNoSql = "update seqno set num=num+1 where code='order'"; var seqNo = MySQLHelper.ExecuteScalar(connectionStr, System.Data.CommandType.Text, getSeqNosql); MySQLHelper.ExecuteNonQuery(connectionStr, System.Data.CommandType.Text, updateSeqNoSql); return seqNo.ToString(); } 2、在 RedisLockConsoleApp1 控制台程式中用多執行緒來模擬測試
class Program { static void Main(string[] args) { Task.Run(() => { for (int i = 0; i < 50; i++) { Console.WriteLine($"Thread1:SeqNo:{SeqNo.GetSeqNoByNoLock()}"); } }); Task.Run(() => { for (int i = 0; i < 50; i++) { Console.WriteLine($"Thread2:SeqNo:{SeqNo.GetSeqNoByNoLock()}"); } }); Console.ReadLine(); } } 3、測試結果如下,可以看出在多執行緒情況下會出現重複的編號

單機環境加鎖測試
在 SeqNo 類中添加 GetSeqNoByLock 方法,通過 Monitor.Enter 來解決單機多執行緒流水號重複問題
public static string GetSeqNoByLock() { string connectionStr = "server = localhost; user id = oec2003; password = 123456; database = seqno_test"; string getSeqNosql = "select num from seqno where code='order'"; string updateSeqNoSql = "update seqno set num=num+1 where code='order'"; var seqNo = string.Empty; try { Monitor.Enter(_myLock); seqNo = MySQLHelper.ExecuteScalar(connectionStr, System.Data.CommandType.Text, getSeqNosql).ToString(); MySQLHelper.ExecuteNonQuery(connectionStr, System.Data.CommandType.Text, updateSeqNoSql); Monitor.Exit(_myLock); } catch { Monitor.Exit(_myLock); } return seqNo.ToString(); } 運行結果如下,可以看出已經沒有出現重複的流水號了

多機環境測試
Monitor 只能解決進程內的重複性問題,現在用兩個控制台程式來模擬分散式下的多機器運行,在 RedisLockConsoleApp2 控制台程式添加如下程式碼
static void Main(string[] args) { Task.Run(() => { for (int i = 0; i < 50; i++) { Console.WriteLine($"Thread1:SeqNo:{SeqNo.GetSeqNoByLock()}"); } }); Task.Run(() => { for (int i = 0; i < 50; i++) { Console.WriteLine($"Thread2:SeqNo:{SeqNo.GetSeqNoByLock()}"); } }); Console.ReadLine(); } 同時運行兩個控制台程式,測試結果如下:

可以看出在每一個控制台程式內沒有重複流水號,但兩個控制台還是會間歇性地出現重複流水號。
要解決這個問題就必須使用分散式鎖。
多機環境分散式鎖測試
分散式鎖又很多實現方式,本例中採用 Redis 來實現,Redis 客戶端使用的是 CSRedisCore ,在 CSRedisCore 最新的版本 3.1.5 中實現了分散式鎖,這讓使用變得非常的方便。
1、在 RedisLockLib 項目中添加 CSRedisCore 包的引用

2、在 SeqNo 類中添加 GetSeqNoByRedisLock 方法
public static string GetSeqNoByRedisLock() { string connectionStr = "server = localhost; user id = oec2003; password = 123456; database = seqno_test"; string getSeqNosql = "select num from seqno where code='order'"; string updateSeqNoSql = "update seqno set num=num+1 where code='order'"; var seqNo=string.Empty; using (_redisClient.Lock("test", 5000)) { seqNo = MySQLHelper.ExecuteScalar(connectionStr, System.Data.CommandType.Text, getSeqNosql).ToString(); MySQLHelper.ExecuteNonQuery(connectionStr, System.Data.CommandType.Text, updateSeqNoSql); } return seqNo; } 3、測試結果如下:

總結
例子非常簡單,提供一種解決問題的思路,如您有更好的方式歡迎討論。本文的示例程式碼已上傳 Github ,地址如下:
https://github.com/oec2003/StudySamples/tree/master/RedisLockDemo
祝大家假期快樂!