一、场景介绍

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

之所以叫小并发:是因为确实是小并发场景的应用模式,一般针对企业的内部系统,比如工厂里面的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

 

.NET 6 在小并发下如何生成唯一单据号的更多相关文章

  1. 用SQL存储过程生成唯一单据号

    用SQL存储过程生成唯一单据号     在一些系统中,经理要生成单据号,为了不使多台客户端生成的单据号重复,一般要在服务端生成这种流水号,本文是在数据库中生成流水号,并且可以生成多种类型的单据号(比如 ...

  2. php生成唯一订单号

    支持更改长度/** * 生成唯一订单号 * */ function build_order_no(){ return date('Ymd').substr(implode(NULL, array_ma ...

  3. PHP生成唯一会员卡号

    我们将0-Z(0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ)分别代表数值0-35,如字母Z代表35.这样的话我要得到一个5位的编号,最大信息量就是36的5次方了,36^5 ...

  4. C#生成唯一订单号

    今天系统出了一个问题,发现生成的订单号存在重复的情况了,这是要命的bug,不马上解决,就会有投诉了 经过改进后的代码我先简单的放一下,后面在慢慢的写清楚整个流程 string key = " ...

  5. php生成唯一订单号的方法

    第一种 $danhao = date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); 第二种 $danhao = date('Ym ...

  6. mysql使用触发器生成唯一订单号,

    需求:订单号唯一,并且期望是时间格式加其他字符串, 实现:采用触发机制,在新增时根据新增id值加1作为订单生成的随机且确定唯一的数,因为id唯一: 遇到问题:新增时不能提前知道id值, 解决:取到当前 ...

  7. php 生成唯一订单号5种方法

    第一种 private function doCreateOrderNumber($time){ $i=1; $dd = date('Ymd',$time); $aa = 'OH'.$dd; $res ...

  8. 基于mysql的单据号生成(前缀+日期+自增id+后缀)

    介绍 本次采用mysql处理,性能不是很好,对于高并发有要求的建议不要采用公司一个小项目,需要生成一个单据号,格式为: 日期 + 每日重新自增号,自己考虑了一下每日自增需要考虑并发和持久问题,两种数据 ...

  9. oracle生成单据号

    --创建单据号存放表 CREATE TABLE BU_TAB( DOC_NUM NUMBER --生成的单据号 ); --单据号 create table cux_doc_num( tab ), -- ...

随机推荐

  1. pytest文档1-环境搭建

    1.安装方法 pip install -U pytest 2.查看安装版本 pip show pytest pytest -version 3.快速开始 新建test开头py文件 打开test_sam ...

  2. MyCms 自媒体 CMS 系统 v2.7,支持自定义页面

    MyCms 是一款基于Laravel开发的开源免费的自媒体博客CMS系统,助力开发者知识技能变现. MyCms 基于Apache2.0开源协议发布,免费且不限制商业使用,欢迎持续关注我们. V2.7 ...

  3. golang中的runtime包

    1. runtime.Gosched  让出CPU时间片,重新等待安排任务 package main import ( "fmt" "runtime" ) fu ...

  4. 003Linux查看文件内容的5个命令姿势

    01 开篇 Linux 中查看文件内容常用的有如下 5 个命令: cat: more: less: tail: head. 02依次看看这些命令的使用姿势 cat 一次性将所有内容输出到屏幕上,方便查 ...

  5. Excel与MySQL数据库的导入与导出

    应用场景 在许多时候,我们希望数据能够很好地在各个系统之间转移,同时便于非专业人员阅读,如果程序员一点点打字导出的话,不知道要打到什么时候,于是我们便采用日常工作中常用的Excel表格来作为媒介,将数 ...

  6. Shell 脚本进阶,经典用法及其案例

    一.条件选择.判断 1.条件选择if (1)用法格式 if 判断条件 1 ; then 条件为真的分支代码 elif 判断条件 2 ; then 条件为真的分支代码 elif 判断条件 3 ; the ...

  7. oracle 相关查询和非相关查询,oracle 去除重复数据,以及oracle的分页查询!

    一.oracle中的相关查询?和非相关查询? 二.oracle去除重复数据 1. 2. 3.oracle 实现分页? 利用rownum的唯一性,和子查询,将rownum从伪列变成实际列!

  8. 布客&#183;ApacheCN 编程/后端/大数据/人工智能学习资源 2020.6

    公告 我们的群共享文件有备份到 IPFS 的计划,具体时间待定. 我们的机器学习群(915394271)正式改名为财务提升群,望悉知. 请关注我们的公众号"ApacheCN",回复 ...

  9. IDE集成git

    目录 简介 Git安装 IDE集成Git IDE集成Git代码的创建分享上传 代码的下载和普通上传 分子的创建以及合并 代码的回滚 查看历史版本 简介 Git 是一个开源的分布式版本控制软件,用以有效 ...

  10. StarUML官网地址 http://staruml.io/

    StarUML官网地址 http://staruml.io/