新建GrabbingOrdersController.cs控制器,可以开始制作测试接口了
Demo一共有8个接口,两个只做查询(不在文章里描述后续会上源代码),纯db接口、lock锁、服务器缓存+队列、Redis+队列、Redis_lua、rabbit
开始搬砖了
Controllers文件夹新建控制器(api 控制器 空),引用日志接口和数据库连接类
namespace MessageQueuing.Controllers
{
/// <summary>
/// 抢单
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class GrabbingOrdersController : ControllerBase
{
private readonly ILogger<GrabbingOrdersController> logger;
private readonly DbHelperContext dbHelper;
public GrabbingOrdersController(ILogger<GrabbingOrdersController> logger, DbHelperContext dbHelper)
{
this.logger = logger;
this.dbHelper = dbHelper;
}
}
}
一.纯DB接口:在控制器中直接处理查询和新增订单等业务
业务流程:接口触发->DB查询数据->新增订单\扣除库存
功能分析:
- 响应较慢,高并发下容易出现错误
- 库存扣除出现异常
- 产品存在超卖现象
压力测试:
线程数为10,100,1000三种情况进行测试,Ramp-Up时间空,循环次数为1
代码:
[HttpGet]
[Route("[action]")]
public string SetOrder(string userId, string proId)
{
try
{
var product = dbHelper.SeckillProduct.Where(x => x.productId == proId).FirstOrDefault();
if (product.productStockNum - 1 > 0)
{
product.productStockNum--;
Models.Order order = new Models.Order();
order.id = Guid.NewGuid().ToString("N");
order.userId = userId;
order.orderNum = Guid.NewGuid().ToString("N");
order.productId = product.id;
order.orderTotal = Convert.ToDecimal(product.productPrice);
order.addTime = DateTime.Now;
order.orderStatus = 0;
order.orderPhone = "1565555555";
order.orderAddress = "test";
order.delFlag = 0;
dbHelper.Add(order);
dbHelper.SaveChanges();
return "下单成功";
}
else
{
return "下单成功";
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
测试结果:1000并发下异常为0说明都正常触发了,但如果查看数据会发现库存并没有扣1000
二、lock锁: 使用lock锁,在处理业务时不让其它进程出入,直到进程结束为止,这样能解决库存错误导致多卖的情况
业务流程:
接口触发-> 单线程进入其余等待->DB查询数据->新增订单\扣除库存->业务完成,释放线程->其它线程进入
功能分析:
- 响应很慢,高并发的情况下容易导致请求排队时间过长而后报错
- 解决库存错误
压力测试:
线程数为10,100,1000三种情况进行测试,Ramp-Up时间空,循环次数为1
代码:
首先控制器里建一个静态只读的object
新建SetOrderLock,lock调用建好的(object)
[HttpGet]
[Route("[action]")]
public string SetOrderLock(string userId, string proId)
{
try
{
lock (objlock)
{
var product = dbHelper.SeckillProduct.Where(x => x.productId == proId).FirstOrDefault();
if (product.productStockNum - 1 > 0)
{
product.productStockNum--;
Models.Order order = new Models.Order();
order.id = Guid.NewGuid().ToString("N");
order.userId = userId;
order.orderNum = Guid.NewGuid().ToString("N");
order.productId = product.id;
order.orderTotal = Convert.ToDecimal(product.productPrice);
order.addTime = DateTime.Now;
order.orderStatus = 0;
order.orderPhone = "1565555555";
order.orderAddress = "test";
order.delFlag = 0;
dbHelper.Add(order);
dbHelper.SaveChanges();
return "下单成功";
}
else
{
return "下单成功";
}
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
测试结果:慢于纯db接口,但不会出现库存错误的情况
三、服务器缓存+队列:生产者和消费者模式,接口只负责向计算库存和新增随机订单号然后写入缓存中,会建两个后台任务进行处理插入和查询
业务流程:
接口触发-> 利用lock来处理简单的库存和新增随机订单号->利用后台任务进行查询数据->新增订单\扣除库存
功能分析:
- 同时解决响应慢和库存错误问题
- 如服务器异常容易导致缓存里的信息丢失
压力测试:
线程数为10,100,1000三种情况进行测试,Ramp-Up时间空,循环次数为1
代码:
在项目里新建Common文件夹,然后新建CacheBackService.cs、CustomerService.cs和MyQueue.cs
MyQueue.cs:基于内存的队列
private static ConcurrentQueue<string> _queue = new ConcurrentQueue<string>();
public static ConcurrentQueue<string> GetQueue()
{
return _queue;
}
CacheBackService.cs:用来初始化库存等信息
public class CacheBackService : BackgroundService
{
private IMemoryCache _cache;
private IConfiguration _Configuration;
public CacheBackService(IMemoryCache cache, IConfiguration Configuration)
{
_cache = cache;
_Configuration = Configuration;
}
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
// EFCore的上下文默认注入的请求内单例的,而CacheBackService要注册成全局单例的
// 由于二者的生命周期不同,所以不能相互注入调用,这里手动new一个EF上下文
var optionsBuilder = new DbContextOptionsBuilder<DbHelperContext>();
optionsBuilder.UseSqlServer(_Configuration[string.Join(":", new string[] { "DBConnection", "ConnectionStrings", "Dbconn" })]);
DbHelperContext context = new DbHelperContext(optionsBuilder.Options);
//初始化库存信息,连临时写在这个位置,充当服务器启动的时候初始化
var data = await context.SeckillProduct.Where(u => u.id == "21e86c6cc32b4e7bb80f96c98e4e7994").FirstOrDefaultAsync();
//服务器缓存
_cache.Set<int>($"{data.productId}-sCount", data.productStockNum);
}
}
CustomerService.cs:处理消费业务
public class CustomerService: BackgroundService
{
private IConfiguration _Configuration;
public CustomerService(IConfiguration Configuration)
{
_Configuration = Configuration;
}
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
// EFCore的上下文默认注入的请求内单例的,而CacheBackService要注册成全局单例的
// 由于二者的生命周期不同,所以不能相互注入调用,这里手动new一个EF上下文
var optionsBuilder = new DbContextOptionsBuilder<DbHelperContext>();
optionsBuilder.UseSqlServer(_Configuration[string.Join(":", new string[] { "DBConnection", "ConnectionStrings", "Dbconn" })]);
DbHelperContext context = new DbHelperContext(optionsBuilder.Options);
Console.WriteLine("下面开始执行消费业务");
while (true)
{
try
{
string data = "";
MyQueue.GetQueue().TryDequeue(out data);
if (!string.IsNullOrEmpty(data))
{
List<string> tempData = data.Split('-').ToList();
//1.扣减库存---禁止状态追踪
var sArctile =await context.SeckillProduct.Where(u => u.id == "21e86c6cc32b4e7bb80f96c98e4e7994").FirstOrDefaultAsync();
sArctile.productStockNum = sArctile.productStockNum - 1;
context.Update(sArctile);
//2. 插入订单信息
Order tOrder = new Order();
tOrder.id = Guid.NewGuid().ToString("N");
tOrder.userId = tempData[0];
tOrder.orderNum = Guid.NewGuid().ToString("N");
tOrder.productId = tempData[1];
tOrder.orderTotal = 50;
tOrder.addTime = DateTime.Now;
tOrder.orderStatus = 0;
tOrder.orderPhone = "1565555555";
tOrder.orderAddress = "test";
tOrder.delFlag = 0;
context.Add<Order>(tOrder);
int count = await context.SaveChangesAsync();
//释放一下
context.Entry<SeckillProduct>(sArctile).State = EntityState.Detached;
Console.WriteLine($"执行成功,条数为:{count},当前库存为:{ sArctile.productStockNum}");
}
else
{
Console.WriteLine("暂时没有订单信息,休息一下");
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
catch (Exception ex)
{
Console.WriteLine($"执行失败:{ex.Message}");
}
}
}
}
控制器引入IMemoryCache接口,
private static readonly object objlock = new object();
private readonly ILogger<GrabbingOrdersController> logger;
private readonly DbHelperContext dbHelper;
private readonly IMemoryCache cache;
public GrabbingOrdersController(ILogger<GrabbingOrdersController> logger, DbHelperContext dbHelper, IMemoryCache cache)
{
this.logger = logger;
this.dbHelper = dbHelper;
this.cache = cache;
}
然后新建SetOrderServercache
[HttpGet]
[Route("[action]")]
public string SetOrderServercache(string userId, string proId)
{
try
{
lock (objlock)
{
int count = cache.Get<int>($"{proId}-sCount");
if (count - 1 >= 0)
{
count--;
cache.Set<int>($"{userId}-sCount", count);
var orderNum = Guid.NewGuid().ToString("N");
MyQueue.GetQueue().Enqueue($"{userId}-{proId}-{orderNum}");
return "下单成功";
}
else
{
return "下单成功";
}
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
最后到Startup.cs注册后台任务
//注册后台任务
services.AddHostedService<CacheBackService>();
services.AddHostedService<CustomerService>();
测试结果: