業務背景
在管理系統中,很多功能子產品都會涉及到各種類型的編号,例如:流程編号、訂單号、合同編号等等。編号各有各自的規則,但通常有一個流水号來确定編号的唯一性,保證流水号的唯一,在不同的環境中實作方式有所不同。本文将介紹在單機和分布式環境中保證流水号唯一的方式。
實作思路
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
祝大家假期快樂!
微信公衆号:不止dotNET
作者: oec2003
出處: http://oec2003.cnblogs.com/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結,否則 保留追究法律責任的權利。