• .NET 6 在小并发下如何生成唯一单据号


      一、场景介绍

                小并发下要解决生成单据号的问题,会碰到哪些问题呢?,接下来让我们一探究竟【这是小并发的解决方案,大家有更好的做好可以一起讨论分享】。

                之所以叫小并发:是因为确实是小并发场景的应用模式,一般针对企业的内部系统,比如工厂里面的WMS,MES,QMS需要单据号生成的系统。 

                单据号的一般组成:业务类型+YYYYMMDD+流水号【五位】,每天重新从1开始。

                根据单据号的组成规则,一般数据库表设计如下:

                       1、业务类型和YYYYMMDD 统一称为前缀 prefix,存到我们数据库中。

                       2、另外一个当前值表达的是当前序号已经到多少了。

                       并且一般会根据Prefix和一些其他业务字段组成,建立一个唯一索引,避免插入重复数据。

                大概表的设计如下(部分逻辑):

    create table SFC_BARCODE_SEQUENCE
    (
      id                VARCHAR2(36 CHAR) default sys_guid() not null,
      datetime_created  DATE default sysdate not null,
      user_created      VARCHAR2(80 CHAR) default 'SYS' not null,
      datetime_modified DATE,
      user_modified     VARCHAR2(80 CHAR),
      state             CHAR(1) default 'A' not null,
      enterprise_id     VARCHAR2(36 CHAR) default '*' not null,
      org_id            VARCHAR2(36 CHAR) not null,
      barcode_category  VARCHAR2(80 CHAR) not null,
      prefix            VARCHAR2(80 CHAR) not null, --前缀
      current_value     NUMBER(22) not null, --当前序号  
      barcode_rule      VARCHAR2(80 CHAR) not null
    )
    
    --创建一个唯一索引,建这个唯一索引是避免在高并发场景下,插入重复数据。
    create unique index IX_SFC_BARCODE_SEQUENCE on SFC_BARCODE_SEQUENCE (ENTERPRISE_ID, ORG_ID, BARCODE_CATEGORY, PREFIX)
      tablespace WMSD
      pctfree 10
      initrans 2
      maxtrans 255
      storage
      (
        initial 64K
        next 1M
        minextents 1
        maxextents unlimited
      );

              接着我们就开始根据业务逻辑写一个生成单号的逻辑,如下所示:

      public static string GenerateBillNo(string billTypeCode, string OrgId, string EnterpriseId, string CurrentUserName)
            {
                using (var db = DbContext.GetInstance())
                {
                    DateTime dbTime = DateTime.Now;
                    var prefix = billTypeCode + DateTime.Now.ToString("yyyyMMdd");
                    string barcodeCategory = billTypeCode + "TEST_BILL_CATEGORY";
    
                    string newBillNo = prefix + "0001";
                    //当前序号
                    var currentSeq = db.Queryable<SFC_BARCODE_SEQUENCE>()
                        .Where(x => x.BARCODE_CATEGORY == barcodeCategory && x.PREFIX == prefix)
                        .Where(x => x.STATE == "A" && x.ORG_ID == OrgId && x.ENTERPRISE_ID == EnterpriseId)
                        .ToList()
                        .FirstOrDefault();
                    if (currentSeq == null)
                    {
                        SFC_BARCODE_SEQUENCE model = new SFC_BARCODE_SEQUENCE();
                        model.ID = Guid.NewGuid().ToString("N").ToUpper();
                        model.DATETIME_CREATED = dbTime;
                        model.USER_CREATED = CurrentUserName;
                        model.STATE = "A";
                        model.ORG_ID = OrgId;
                        model.ENTERPRISE_ID = EnterpriseId;
                        model.BARCODE_CATEGORY = barcodeCategory;
                        model.PREFIX = prefix;
                        model.CURRENT_VALUE = 1;
                        model.BARCODE_RULE = $"检验单据类型({billTypeCode}) + 年月日(yyyyMMdd) + 4位流水";
    
                        db.Insertable(model).ExecuteCommand();
                    }
                    else
                    {
                        db.Updateable<SFC_BARCODE_SEQUENCE>()
                            .Where(x => x.ID == currentSeq.ID)
                            .SetColumns(t => new SFC_BARCODE_SEQUENCE()
                            {
                                USER_MODIFIED = CurrentUserName,
                                DATETIME_MODIFIED = dbTime,
                                CURRENT_VALUE = (currentSeq.CURRENT_VALUE + 1)
                            }).ExecuteCommand();
    
                        newBillNo = prefix + (currentSeq.CURRENT_VALUE + 1).ToString().PadLeft(4, '0');
                    }
                    return newBillNo;
                }
            }

      上面这种方式,存在的问题:高并发下,容易产生重复单号等问题,如下分析

            

            这是我们认为模拟50个并发进行操作,会导致重复单据数据产生。

            现在代码存了重复数据产生,也有以下的问题点:

            

            问题一、多个并发同时过来的时候,开启数据库连接池很慢,这一步需要对数据库连接池进行调优设置,并且还要配上连接预热的功能【这里不展开讲】

            问题二、高并发插入的时候,在数据库层面设置唯一索引,但是也不能让报异常了就把当前线程给拒绝了【某个线程插入报异常,说明有线程插入成功了,这个时候,应该要接着走下面的更新单号的逻辑】

            问题三、更新单号的时候,重复覆盖的问题,导致获取到重复单号。

           下面说说具体的解决方案

      二、各种实现方式

      基础工作

             1、数据库连接池预热

             之所以要建立数据库连接池预热是因为在高并发的情况下,很多建立连接这个操作都会非常耗时,所以先预热数据库连接池,在高并发情况下,只需要去数据库连接池获取连接即可,而不需要重新连接,连接池里的连接也不是越多好,连接越多就要频繁的进行线程切换,对性能也不好。

             连接池预热的简单代码【获取一下数据库的最新时间等方式,可用开启独立的定时任务来干这事情,数据库连接池要多少连接,要根据服务器的CPU核数等有关】

    //这个连接池里面的连接数量,可以通过数据库连接字符串的连接参数进行设置。
    
            //Console.WriteLine("连接池预热开始");
            //for (var i = 0; i < ThreadCount; i++)
            //{
            //    BarcodeProvider.GetDbNow();
            //}
            //Console.WriteLine("连接池预热结束");

      2、高并发插入的时候【因为我们单号是按天开始,每天都要重新从1开始,而不是序列号一直累积的那种,所以每天刚刚开始的时候,都要进行一次插入操作】,也有一个比较巧的设计思路,如下所示:

               

         红色部分:休息300毫秒,抛出异常,不一定是违法了数据库唯一健的异常。

              黄色部分:即使你是插入失败,你也要考虑是不是其他线程会插入成功,因为你已经等了300毫秒。

              整个逻辑只执行三次的原因:如果我们数据库出问题了,这个逻辑不能一直无限循环下次。

      1、悲观锁

              优点:

                1、实现简单
                 2、百分百能保证成功
              缺点:
                  1、悲观锁的效率不高,扛不住高并发的场景,不过一般的场景也够用了。

                   其实有些场景,推荐使用悲观锁,一般企业内部的系统都可以用这种方式

               实现方式

       

                事务一,先开启事务,执行到update 语句时候,事务2,也开始事务,但是会在红色部分update语句卡住。

                只有等事务一提交(绿色部分)了,这样事务2才能继续执行下去。

               2、乐观锁(版本号机制)

        优点:能够高并发,其实代码也相对简单
        缺点:可能会有失败的情况

                  实现方式:采用版本号类似的字段,刚刚好序号表的顺序序号就是这种类似于版本号的,自增字段加上即可。

                 

                  如果多个线程同时读取,那么更新的时候,就只会有一个线程更新成功,其他返回失败。

                  乐观锁还有一种实现方式是CAS

        两种方式的比较

         乐观锁:不需要直接去给锁定某一块,这样相对来说并发会更好,但是不能保证每次都成功。

                   悲观锁:开启事务,先update 再select 方式,其实也可以接受,并且没有返回失败,在并发情况不大下,悲观锁也是OK的。

                   选择:根据业务场景来定,如果用户不接受返回失败,那直接就悲观锁:事务里面 update 再select 方式在一般的系统也足够用了,如果你要上分布式锁这些东东,也是等业务发展到一定程度再来考虑,毕竟大部分系统都到不了那个时候,尤其是企业内部应用系统。

                   乐观锁:如果用户能够接受偶尔返回失败,并且并发量也比较大的话,可以考虑使用这种方式。

      三、案例程序

                涉及技术:.NET 6 控制台+Sqlsguar+Oracle;

                代码演示效果:我未来演示效果【这个效果是我加了Thread.Sleep,所以耗时不用太关注】

                

                代码地址:https://github.com/gdoujkzz/NET6GenerateBillNoDemo/tree/master

     
    终极目标:世界大同
  • 相关阅读:
    C++各种进制的转换
    C++获取当前目录
    【转】Caffe初试(十)命令行解析
    libsvm下的windows版本中的工具的使用
    libsvm的数据格式及制作
    【转】Windows下使用libsvm中的grid.py和easy.py进行参数调优
    【转】Caffe初试(九)solver及其设置
    【转】Caffe初试(八)Blob,Layer和Net以及对应配置文件的编写
    【转】Caffe初试(七)其它常用层及参数
    Ubuntu 14.04 安装 Sublime Text 3
  • 原文地址:https://www.cnblogs.com/gdouzz/p/15685564.html
Copyright © 2020-2023  润新知