java 抢单 缓存服务器_第二节:抢单流程优化1(小白写法→lock写法→服务器缓存+队列→redis缓存+队列)...
一. 小白写法
1.设计思路
纯DB操作
DB查库存→判断库存→(DB扣减库存+DB创建订单)
2.分析
A.响应非常慢,导致大量请求拿不到结果而报错
B.存在超卖现象
C.扣减库存错误
3.压测结果
前提:原库存为10000,这里统计2s内可处理的并发数,以90%百分位为例,要求错误率为0。
代码分享:
///
///原始版本-纯DB操作///
/// 用户编号
/// 商品编号
/// 订单总额
///
public string POrder1(string userId, string arcId, stringtotalPrice)
{try{//1. 查询库存
var sArctile = _baseService.Entities().Where(u => u.articleId ==arcId).FirstOrDefault();if (sArctile.articleStockNum - 1 > 0)
{//2. 扣减库存
sArctile.articleStockNum--;//3. 进行下单
T_Order tOrder = newT_Order();
tOrder.id= Guid.NewGuid().ToString("N");
tOrder.userId=userId;
tOrder.orderNum= Guid.NewGuid().ToString("N");
tOrder.articleId=arcId;
tOrder.orderTotalPrice=Convert.ToDecimal(totalPrice);
tOrder.addTime=DateTime.Now;
tOrder.orderStatus= 0;
_baseService.Add(tOrder);
_baseService.SaveChange();return "下单成功";
}else{//卖完了
return "卖完了";
}
}catch(Exception ex)
{throw newException(ex.Message);
}
}
View Code
测试结果:
(1). 100并发,需要1788ms,订单数量插入正确,但库存扣减错误。
(2). 200并发,需要4453ms,订单数量插入正确,但库存扣减错误。
二. lock写法
1.设计思路
纯DB操作的基础上Lock锁
Lock { DB查库存→判断库存→(DB扣减库存+DB创建订单) }
2.分析
A. 解决超卖现象
B. 响应依旧非常慢,导致大量请求拿到结果而报错
3.压测结果
前提:原库存为10000,这里统计2s内可处理的并发数,以90%百分位为例,要求错误率为0。
代码分享:
///
///02-纯DB操作+Lock锁///
/// 用户编号
/// 商品编号
/// 订单总额
///
public string POrder2(string userId, string arcId, stringtotalPrice)
{try{lock(_lock)
{//1. 查询库存
var sArctile = _baseService.Entities().Where(u => u.articleId ==arcId).FirstOrDefault();if (sArctile.articleStockNum - 1 > 0)
{//2. 扣减库存
sArctile.articleStockNum--;//3. 进行下单
T_Order tOrder = newT_Order();
tOrder.id= Guid.NewGuid().ToString("N");
tOrder.userId=userId;
tOrder.orderNum= Guid.NewGuid().ToString("N");
tOrder.articleId=arcId;
tOrder.orderTotalPrice=Convert.ToDecimal(totalPrice);
tOrder.addTime=DateTime.Now;
tOrder.orderStatus= 0;
_baseService.Add(tOrder);
_baseService.SaveChange();return "下单成功";
}else{//卖完了
return "卖完了";
}
}
}catch(Exception ex)
{throw newException(ex.Message);
}
}
View Code
(1). 30并发,需要2132ms,订单数量插入正确,库存扣减正确。
(2). 100并发,需要9186ms,订单数量插入正确,库存扣减正确。
三. 服务器缓存+队列
1.设计思路
生产者和消费者模式→流量削峰(异步的模式平滑处理请求)
A. Lock{ 事先同步DB库存到缓存→缓存查库存→判断库存→订单相关信息服务端队列中 }
B. 消费者从队列中取数据批量提交信息,依次进行(DB扣减库存+DB创建订单)
2.分析
A. 接口中彻底干掉了DB操作, 并发数提升非常大
B. 服务宕机,原队列中的下单信息全部丢失
C. 生产者和消费者必须在一个项目及一个进程内
3.压测结果
前提:原库存为10000,这里统计2s内可处理的并发数,以90%百分位为例,要求错误率为0。
代码分享:
初始化库存到内存缓存中
///
///后台任务-初始化库存到缓存中///
public classCacheBackService : BackgroundService
{privateIMemoryCache _cache;privateStackExchange.Redis.IDatabase _redisDb;privateIConfiguration _Configuration;publicCacheBackService(IMemoryCache cache,RedisHelp redisHelp, IConfiguration Configuration)
{
_cache=cache;
_redisDb=redisHelp.GetDatabase();
_Configuration=Configuration;
}protected async overrideTask ExecuteAsync(CancellationToken stoppingToken)
{//EFCore的上下文默认注入的请求内单例的,而CacheBackService要注册成全局单例的//由于二者的生命周期不同,所以不能相互注入调用,这里手动new一个EF上下文
var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr"));
ESHOPContext context= newESHOPContext(optionsBuilder.Options);
IBaseService _baseService= newBaseService(context);//初始化库存信息,连临时写在这个位置,充当服务器启动的时候初始化
var data = await _baseService.Entities().Where(u => u.id == "300001").FirstOrDefaultAsync();//服务器缓存
_cache.Set($"{data.articleId}-sCount", data.articleStockNum);
}
}
View Code
队列定义和下单接口
///
///基于内存的队列///
public static classMyQueue
{private static ConcurrentQueue _queue = new ConcurrentQueue();public static ConcurrentQueueGetQueue()
{return_queue;
}
}///
///03-服务端缓存+队列版本+Lock///
/// 用户编号
/// 商品编号
/// 订单总额
///
public string POrder3(string userId, string arcId, stringtotalPrice)
{try{lock(_lock)
{//1. 查询库存
int count = _cache.Get($"{arcId}-sCount");if (count - 1 >= 0)
{//2. 扣减库存
count = count - 1;
_cache.Set($"{arcId}-sCount", count);//3. 将下单信息存到消息队列中
var orderNum = Guid.NewGuid().ToString("N");
MyQueue.GetQueue().Enqueue($"{userId}-{arcId}-{totalPrice}-{orderNum}");//4. 把部分订单信息返回给前端
return $"下单成功,订单信息为:userId={userId},arcId={arcId},orderNum={orderNum}";
}else{//卖完了
return "卖完了";
}
}
}catch(Exception ex)
{throw newException(ex.Message);
}
}
View Code
基于内存的消费者
///
///后台任务--基于内存队列的消费者(已经测试)///
public classCustomerService : BackgroundService
{privateIConfiguration _Configuration;publicCustomerService(IConfiguration Configuration)
{
_Configuration=Configuration;
}protected async overrideTask ExecuteAsync(CancellationToken stoppingToken)
{//EFCore的上下文默认注入的请求内单例的,而CacheBackService要注册成全局单例的//由于二者的生命周期不同,所以不能相互注入调用,这里手动new一个EF上下文
var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr"));
ESHOPContext context= newESHOPContext(optionsBuilder.Options);
IBaseService _baseService= newBaseService(context);
Console.WriteLine("下面开始执行消费业务");while (true)
{try{string data = "";
MyQueue.GetQueue().TryDequeue(outdata);if (!string.IsNullOrEmpty(data))
{
List tempData = data.Split('-').ToList();//1.扣减库存---禁止状态追踪
var sArctile = context.Set().AsNoTracking().Where(u => u.id == "300001").FirstOrDefault();
sArctile.articleStockNum= sArctile.articleStockNum - 1;
context.Update(sArctile);//2. 插入订单信息
T_Order tOrder = newT_Order();
tOrder.id= Guid.NewGuid().ToString("N");
tOrder.userId= tempData[0];
tOrder.orderNum= tempData[3];
tOrder.articleId= tempData[1];
tOrder.orderTotalPrice= Convert.ToDecimal(tempData[2]);
tOrder.addTime=DateTime.Now;
tOrder.orderStatus= 0;
context.Add(tOrder);int count = awaitcontext.SaveChangesAsync();//释放一下
context.Entry(sArctile).State =EntityState.Detached;
Console.WriteLine($"执行成功,条数为:{count},当前库存为:{ sArctile.articleStockNum}");
}else{
Console.WriteLine("暂时没有订单信息,休息一下");await Task.Delay(TimeSpan.FromSeconds(1));
}
}catch(Exception ex)
{
Console.WriteLine($"执行失败:{ex.Message}");
}
}
}
}
View Code
(1). 1000并发,需要600ms,订单数量插入正确,库存扣减正确。
(2). 2000并发,需要1500ms,订单数量插入正确,库存扣减正确。
四. Redis缓存+队列
1.设计思路
生产者和消费者模式→流量削峰(异步的模式平滑处理请求)
思路同上,缓存和队列改成基于Redis的。
2. 分析
A. 引入Redis缓存和消息队列代替基于内存的缓存和队列,数据可以持久化解决了丢失问题。
B. Redis是单线程的,利用api自身的原子性,从而可以干掉lock锁。
C. 引入进程外的缓存Redis,从而可以把生产者和消费者解耦分离,可以作为两个单独的服务运行。
3. 压测结果
前提:原库存为10万,这里统计2s内可处理的并发数,以90%百分位为例,要求错误率为0。
代码分享:
初始化库存到redis缓存中
///
///后台任务-初始化库存到缓存中///
public classCacheBackService : BackgroundService
{privateIMemoryCache _cache;privateStackExchange.Redis.IDatabase _redisDb;privateIConfiguration _Configuration;publicCacheBackService(IMemoryCache cache,RedisHelp redisHelp, IConfiguration Configuration)
{
_cache=cache;
_redisDb=redisHelp.GetDatabase();
_Configuration=Configuration;
}protected async overrideTask ExecuteAsync(CancellationToken stoppingToken)
{//EFCore的上下文默认注入的请求内单例的,而CacheBackService要注册成全局单例的//由于二者的生命周期不同,所以不能相互注入调用,这里手动new一个EF上下文
var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr"));
ESHOPContext context= newESHOPContext(optionsBuilder.Options);
IBaseService _baseService= newBaseService(context);//初始化库存信息,连临时写在这个位置,充当服务器启动的时候初始化
var data = await _baseService.Entities().Where(u => u.id == "300001").FirstOrDefaultAsync();//Redis缓存
_redisDb.StringSet($"{data.articleId}-sCount", data.articleStockNum);
}
}
View Code
下单接口
///
///04-Redis缓存+队列///
/// 用户编号
/// 商品编号
/// 订单总额
///
public string POrder4(string userId, string arcId, stringtotalPrice)
{try{//1. 直接自减1
int iCount = (int)_redisDb.StringDecrement($"{arcId}-sCount", 1);if (iCount >= 0)
{//2. 将下单信息存到消息队列中
var orderNum = Guid.NewGuid().ToString("N");
_redisDb.ListLeftPush(arcId, $"{userId}-{arcId}-{totalPrice}-{orderNum}");//3. 把部分订单信息返回给前端
return $"下单成功,订单信息为:userId={userId},arcId={arcId},orderNum={orderNum}";
}else{//卖完了
return "卖完了";
}
}catch(Exception ex)
{throw newException(ex.Message);
}
}
View Code
基于redis队列的消费者
{
Console.WriteLine("下面开始执行消费业务");using (ESHOPContext db = newESHOPContext())
{
RedisHelp redisHelp= new RedisHelp("localhost:6379");var redisDB =redisHelp.GetDatabase();while (true)
{try{var data = (string)redisDB.ListRightPop("200001");if (!string.IsNullOrEmpty(data))
{
List tempData = data.Split('-').ToList();
{//1.扣减库存 --去掉状态追踪
var sArctile = db.Set().AsNoTracking().Where(u => u.id == "300001").FirstOrDefault();
sArctile.articleStockNum= sArctile.articleStockNum - 1;
db.Update(sArctile);//2. 插入订单信息
T_Order tOrder = newT_Order();
tOrder.id= Guid.NewGuid().ToString("N");
tOrder.userId= tempData[0];
tOrder.orderNum= tempData[3];
tOrder.articleId= tempData[1];
tOrder.orderTotalPrice= Convert.ToDecimal(tempData[2]);
tOrder.addTime=DateTime.Now;
tOrder.orderStatus= 0;
db.Add(tOrder);int count =db.SaveChanges();//释放一下--否则报错
db.Entry(sArctile).State =EntityState.Detached;
Console.WriteLine($"执行成功,条数为:{count},当前库存为:{ sArctile.articleStockNum}");
}
}else{
Console.WriteLine("暂时没有订单信息,休息一下");
Thread.Sleep(1000);
}
}catch(Exception ex)
{
Console.WriteLine($"执行失败-{ex.Message}");
}
}
}
}
View Code
(1). 1000并发,需要600ms,订单数量插入正确,库存扣减正确。
(2). 2000并发,需要1560ms,订单数量插入正确,库存扣减正确。
!
作 者 : Yaopengfei(姚鹏飞)
声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
java 抢单 缓存服务器_第二节:抢单流程优化1(小白写法→lock写法→服务器缓存+队列→redis缓存+队列)...相关推荐
- iHRM 人力资源管理系统_第9章_文件上传与PDF报表入门_第二节_PDF报表入门
iHRM 人力资源管理系统_第9章_文件上传与PDF报表入门_第二节_PDF报表入门 文章目录 iHRM 人力资源管理系统_第9章_文件上传与PDF报表入门_第二节_PDF报表入门 PDF报表入门 3 ...
- 代驾APP_第一章_项目环境搭建_第二节
代驾APP_第一章_项目环境搭建_第二节 文章目录 代驾APP_第一章_项目环境搭建_第二节 1-11 创建bff-driver服务 一.创建项目 二.配置pom.xml文件 三.编写YML配置文件 ...
- linux搭建mcpe服务器_在Linux的各个版本上安装、配置Minecraft服务器版
Minecraft是一个流行的沙箱独立游戏,由瑞典程序员Markus "Notch" Perssion首先创造,后来由Mojang开发并发布.这是一款关于打碎和放置砖块的游戏.首先 ...
- minecraft服务器_如何使用Minecraft领域设置简单的无压力Minecraft服务器
minecraft服务器 There are a lot of ways to go about hosting a Minecraft game but it's tough to beat the ...
- 配置iscsi服务器_在Windows Server 2016上安装和配置iSCSI目标服务器
配置iscsi服务器 In this article, I am going to explain how we can install and configure the iSCSI Target ...
- 终身 服务器_阿里云VS腾讯云618年中活动云服务器价格对比哪个更优惠?
国内两大云商 2020 年 618 年中大促优惠活动都已经开始了,详见『2020 年阿里云 618 优惠活动详细攻略』和『2020 年腾讯云 618 优惠活动详细攻略』,那么对于这次 618 活动中, ...
- 【项目优化01】使用Git管理项目及使用redis缓存短信验证码,菜品以及套餐数据
文章目录 1. 使用Git管理项目 2. redis缓存 2.1 使用redis缓存短信验证码 2.2 使用redis缓存菜品数据 2.3 使用Spring Cache缓存套餐数据 1. 使用Git管 ...
- mybatisplus修改单个属性_第二节 官封弼马温——类的属性
<西游记>第2集 官封弼马温 美猴王战胜了混世魔王,花果山上喜气洋洋,小猴们每日操演武艺,十分快乐.悟空闯入东海龙宫,向龙王索取镇海神针--如意金箍棒.这棒虽重一万三千五百斤,却大可撑天着 ...
- java语言编写计算器_第二次作业利用java语言编写计算器进行四则运算
随着第一次作业的完成,助教 牛老师又布置了第二次作业:用java语言编写一个程序然后进行四则运算用户用键盘输入一个字符来结束程序显示统计结果.一开始看到这个题目我也着实吓了一跳 因为不知道如何下手而且 ...
最新文章
- java phantomjs 2.1.1_Java之网络爬虫WebCollector2.1.2+selenium2.44+phantomjs2.1.1
- executeQuery,executeUpdate,execute区别
- LeetCode--495
- oracle-rman-list命令收集
- Python超越Java语言,跃居世界编程语言第2位了!你却还在犹豫学不学Python?
- Nginx日志和http模块相关变量
- MVC登陆认证简单设置
- 将日期格式格式化为XXXX/XX/XX
- SpringBoot整合ureport2
- 【HDU2825】AC自动机+状压DP
- java里oop思想_Java OOP 思想解析
- 逻辑谬误_“完成”谬误
- Avoid passing null as the view root (needed to resolve layout parameters on the inflated layout's ro
- Linux v4l2 一 应用层
- 最新!CCF-A类顶会WWW2020最佳论文出炉!OSU最佳论文
- mq中消息消费的几种方式
- POI 导入、导出Excel
- ffmpegguitool下载不了_FFmpeg GUI Tool下载-FFmpeg GUI Tool(视频处理)下载v1.2.4 安卓版-西西软件下载...
- 算法设计课第十周作业
- shell脚本中的export和PWD的作用