原标题: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 容器
dockerrun -p 6379: 6379-d --name s2redis_test --restart=always redis: 3. 2redis-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
-- ----------------------------
DROPTABLEIFEXISTS`seqno`;
CREATETABLE`seqno`(
`code`varchar( 50) COLLATEutf8mb4_general_ci DEFAULTNULL,
`num`int( 11) DEFAULTNULL
) ENGINE= InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Records of seqno
-- ----------------------------
BEGIN;
INSERTINTO`seqno`VALUES( 'order', 1);
COMMIT;
SETFOREIGN_KEY_CHECKS = 1;
4、在 VS2019 中创建两个控制台项目和一个类库项目,如下图:
单机测试
1、在 SeqNo 类中添加 GetSeqByNoLock 方法
publicstaticstringGetSeqNoByNoLock()
{
stringconnectionStr = "server = localhost; user id = oec2003; password = 123456; database = seqno_test";
stringgetSeqNosql = "select num from seqno where code='order'";
stringupdateSeqNoSql = "update seqno set num=num+1 where code='order'";
varseqNo = MySQLHelper.ExecuteScalar(connectionStr, System.Data.CommandType.Text, getSeqNosql);
MySQLHelper.ExecuteNonQuery(connectionStr, System.Data.CommandType.Text, updateSeqNoSql);
returnseqNo.ToString;
}
2、在 RedisLockConsoleApp1 控制台程序中用多线程来模拟测试
classProgram
{
staticvoidMain(string[] args)
{
Task.Run( =>
{
for( inti = 0; i < 50; i++)
{
Console.WriteLine( $"Thread1:SeqNo:{SeqNo.GetSeqNoByNoLock}");
}
});
Task.Run( =>
{
for( inti = 0; i < 50; i++)
{
Console.WriteLine( $"Thread2:SeqNo:{SeqNo.GetSeqNoByNoLock}");
}
});
Console.ReadLine;
}
}
3、测试结果如下,可以看出在多线程情况下会出现重复的编号
单机环境加锁测试
在 SeqNo 类中添加 GetSeqNoByLock 方法,通过 Monitor.Enter 来解决单机多线程流水号重复问题
publicstaticstringGetSeqNoByLock()
{
stringconnectionStr = "server = localhost; user id = oec2003; password = 123456; database = seqno_test";
stringgetSeqNosql = "select num from seqno where code='order'";
stringupdateSeqNoSql = "update seqno set num=num+1 where code='order'";
varseqNo = 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);
}
returnseqNo.ToString;
}
运行结果如下,可以看出已经没有出现重复的流水号了
多机环境测试
Monitor 只能解决进程内的重复性问题,现在用两个控制台程序来模拟分布式下的多机器运行,在 RedisLockConsoleApp2 控制台程序添加如下代码
staticvoidMain(string[] args)
{
Task.Run( =>
{
for( inti = 0; i < 50; i++)
{
Console.WriteLine( $"Thread1:SeqNo:{SeqNo.GetSeqNoByLock}");
}
});
Task.Run( =>
{
for( inti = 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 方法
publicstaticstringGetSeqNoByRedisLock()
{
stringconnectionStr = "server = localhost; user id = oec2003; password = 123456; database = seqno_test";
stringgetSeqNosql = "select num from seqno where code='order'";
stringupdateSeqNoSql = "update seqno set num=num+1 where code='order'";
varseqNo= 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);
}
returnseqNo;
}
3、测试结果如下:
总结
例子非常简单,提供一种解决问题的思路,如您有更好的方式欢迎讨论。本文的示例代码已上传 Github ,地址如下:
https://github.com/oec2003/StudySamples/tree/master/RedisLockDemo
责任编辑: