業務背景
在管理系統中,很多功能模塊都會涉及到各種類型的編號,例如:流程編號、訂單號、合同編號等等。編號各有各自的規則,但通常有一個流水號來確定編號的唯一性,保證流水號的唯一,在不同的環境中實現方式有所不同。本文將介紹在單機和分佈式環境中保證流水號唯一的方式。
實現思路
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
祝大家假期快樂!