dotNET Core实现分布式环境下的流水号唯一

640?wx_fmt=jpeg

业务背景

在管理系统中,很多功能模块都会涉及到各种类型的编号,例如:流程编号、订单号、合同编号等等。编号各有各自的规则,但通常有一个流水号来确定编号的唯一性,保证流水号的唯一,在不同的环境中实现方式有所不同。本文将介绍在单机和分布式环境中保证流水号唯一的方式。

实现思路

1、在数据库中创建 seqno 表,每个业务一条数据,存储业务 code 和流水号的最大值

环境

  • 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 中创建两个控制台项目和一个类库项目,如下图:

640?wx_fmt=png

单机测试

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、测试结果如下,可以看出在多线程情况下会出现重复的编号

640?wx_fmt=png

单机环境加锁测试

在 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();
}

运行结果如下,可以看出已经没有出现重复的流水号了

640?wx_fmt=png

多机环境测试

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();
}

同时运行两个控制台程序,测试结果如下:

640?wx_fmt=png

可以看出在每一个控制台程序内没有重复流水号,但两个控制台还是会间歇性地出现重复流水号。

要解决这个问题就必须使用分布式锁。

多机环境分布式锁测试

分布式锁又很多实现方式,本例中采用 Redis 来实现,Redis 客户端使用的是 CSRedisCore ,在 CSRedisCore 最新的版本 3.1.5 中实现了分布式锁,这让使用变得非常的方便。

1、在 RedisLockLib 项目中添加 CSRedisCore 包的引用

640?wx_fmt=png

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、测试结果如下:

640?wx_fmt=png

总结

例子非常简单,提供一种解决问题的思路,如您有更好的方式欢迎讨论。本文的示例代码已上传 Github ,地址如下:

https://github.com/oec2003/StudySamples/tree/master/RedisLockDemo

祝大家假期快乐!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了实现基于ZooKeeper的分布式锁,您可以使用ZooKeeperNetEx nuget包安装ZooKeeper客户端,并使用以下代码实现分布式锁: ``` using System; using System.Threading.Tasks; using org.apache.zookeeper; namespace ZooKeeperLock { public class DistributedLock : IDisposable { private readonly ZooKeeper _zooKeeper; private readonly string _lockPath; private string _currentLockPath; public DistributedLock(string connectionString, string lockPath) { _zooKeeper = new ZooKeeper(connectionString, TimeSpan.FromSeconds(10), null); _lockPath = lockPath; } public async Task AcquireLock() { while (true) { try { _currentLockPath = await _zooKeeper.createAsync(_lockPath + "/lock-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); var locks = await _zooKeeper.getChildrenAsync(_lockPath, false); var currentSeq = long.Parse(_currentLockPath.Substring(_lockPath.Length + "/lock-".Length)); var minSeq = long.MaxValue; foreach (var lockNode in locks.Children) { var seq = long.Parse(lockNode.Substring("lock-".Length)); if (seq < minSeq) { minSeq = seq; } } if (currentSeq == minSeq) { return; } await _zooKeeper.deleteAsync(_currentLockPath); } catch (KeeperException.NoNodeException) { await _zooKeeper.createAsync(_lockPath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } } } public void Dispose() { _zooKeeper?.deleteAsync(_currentLockPath); _zooKeeper?.closeAsync(); } } } ``` 您可以使用以下方式使用分布式锁: ``` using System; namespace ZooKeeperLock { class Program { static void Main(string[] args) { using (var lockObject = new DistributedLock("localhost:2181", "/mylock")) { lockObject.AcquireLock().Wait(); Console.WriteLine("This code is protected by distributed lock."); Console.ReadLine(); } } } } ``` 请注意,此实现基于ZooKeeper 3.4.0版本,可以使用其他版本的ZooKeeper和ZooKeeperNetEx进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值