一、场景介绍

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

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

 

最新文章

  1. 7Z命令行详解
  2. 解决 scroll() position:fixed 抖动、导航菜单固定头部(底部)跟随屏幕滚动
  3. java 模拟消息的发送功能
  4. HTML表格标签
  5. 一次JQuery性能优化实战
  6. Python 第八篇:异常处理、Socket语法、SocketServer实现多并发、进程和线程、线程锁、GIL、Event、信号量、进程间通讯
  7. T-SQL基础(1) - T-SQL查询和编程基础
  8. amazeui tab 监听当前选项
  9. js原生设计模式——8单例模式之简约版属性样式方法库
  10. CodeForces 722A
  11. Nest客户端的基本使用方法
  12. 发现----Android Demo
  13. [PHP]算法-队列结构的PHP实现
  14. Oralce sql (+) 补充
  15. open-falcon ---安装Dashboard时候报错&quot;SSLError: The read operation timed out&quot;
  16. JVM jstack 详解
  17. 第3章:Hadoop分布式文件系统(1)
  18. Centos7下python3安装ipython
  19. SpringBoot日记——编码配置篇
  20. Javascript富文本编辑器

热门文章

  1. echarts的通用属性的介绍
  2. C++构造函数语义学(二)(基于C++对象模型)
  3. 记一次org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only异常
  4. java多态成员变量、成员函数(非静态)、静态函数特点
  5. Sping简介
  6. mac下复制文件路径
  7. Java继承的概念与实现
  8. 【webpack4.0】---webpack的基本使用(四)
  9. Java线上问题排查神器Arthas实战分析
  10. 海康NVR设备上传人脸图片到人脸库