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

祝大家假期快樂!

Tags: