.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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM