dotNET Core實現分佈式環境下的流水號唯一

業務背景

在管理系統中,很多功能模塊都會涉及到各種類型的編號,例如:流程編號、訂單號、合同編號等等。編號各有各自的規則,但通常有一個流水號來確定編號的唯一性,保證流水號的唯一,在不同的環境中實現方式有所不同。本文將介紹在單機和分佈式環境中保證流水號唯一的方式。

實現思路

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

祝大家假期快樂!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章