背景:电商项目中,总会有特价或者抢购类的商品活动,这种情况下一般都会发生高并发的下单行为,但是一旦高并发的话,很容易出现超卖现象,这里将针对高并发下,多种下单的后端处理方式,看看不同方式的处理后的结果和性能差异
这里将模拟一个商品,库存为 100,并发数为 1000 的情况(压测工具为 JMeter,性能结果均为 JMeter 的数据)
一.普通下单方式(商品库存和订单新增的操作使用EF)
public async Task DefaultPayOrder() { var goods = _DBContext.Goods.FirstOrDefault(); if (goods.Num > 0) { goods.Num--; _DBContext.Goods.Update(goods); await _DBContext.Order.AddAsync(new OrderEntity { OrderNo = StringHelper.SnowflakeNo() }); await _DBContext.SaveChangesAsync(); return "ok"; } else { return "nonum"; } }
1.数据结果
库存剩余:0
订单数量:156
是否超卖:是
2.性能
吞吐量:143/sec
最低响应:119ms
最高响应:6432ms
3.结论:可以看出,最后发生了商品超卖现象,在业务场景中是不允许发生的
二.在 (一) 的基础上,使用 Lock 方式锁定线程
private static object lockobj = new object(); public string LockPayOrder() { lock (lockobj) { var goods = _DBContext.Goods.FirstOrDefault(); if (goods.Num > 0) { goods.Num--; _DBContext.Goods.Update(goods); _DBContext.Order.Add(new OrderEntity { OrderNo = StringHelper.SnowflakeNo() }); _DBContext.SaveChanges(); return "ok"; } else { return "nonum"; } } }
1.数据结果
库存剩余:0
订单数量:100
是否超卖:否
2.性能
吞吐量:87/sec
最低响应:452ms
最高响应:11227ms
100条订单处理时间:9s
3.结论:可以看出,虽然是不发生超卖了,但是性能上会有很大的削弱,最高响应达到10s以上,而且一旦并发量再高上去的话,
很容易导致请求超时或无法响应的问题,会造成用户极大的不好体验
三.EF执行数据库的方改为SQL语句
public async Task SQLPayOrder() { string sql = "update Goods set num=num-1 where num>0"; var effectNum = _DBContext.Database.ExecuteSqlCommand(sql); if (effectNum > 0) { await _DBContext.Order.AddAsync(new OrderEntity { OrderNo = StringHelper.SnowflakeNo() }); await _DBContext.SaveChangesAsync(); return "ok"; } else { return "nonum"; } }
1.数据结果
库存剩余:0
订单数量:100
是否超卖:否
2.性能
吞吐量:218/sec
最低响应:558ms
最高响应:4471ms
100条订单处理时间:2s
3.结论:使用SQL的方式,性能上和超卖问题都很好的解决了(数据库中表行锁)
四.队列方式,单纯的将请求插入Redis队列中,请求由另一个模块异步处理
public async Task QueryPayOrder() { var orderid = GetOrderId(); //入队 await RedisHelper.LPushAsync("order", orderid); return orderid; } //订单后台任务处理(需要在 startup 注册,ConfigureServices 中添加 services.AddHostedService< OrderQueryHandle >(); ) public class OrderQueryHandle : BackgroundService { DbContextOptionsBuilder builder = new DbContextOptionsBuilder(); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { string connectionString = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()). AddJsonFile("appsettings.json").Build().GetSection("ConnectionStrings").Get().DefaultConnection; builder.UseMySql(connectionString); while (!stoppingToken.IsCancellationRequested) { var orderNo = RedisHelper.BRPop(1, "order");//阻塞式出队,如果队列没有数据,则返回 null,并且等待1s,避免性能损耗 if (!string.IsNullOrEmpty(orderNo)) { using (PomeloContext db = new PomeloContext(builder.Options)) { var goods = db.Goods.FirstOrDefault(); if (goods.Num > 0) { goods.Num--; db.Goods.Update(goods); await db.Order.AddAsync(new OrderEntity { OrderNo = orderNo }); await db.SaveChangesAsync(); } //添加等待时间,避免高并发下cpu过高,这里等待时间为10ms await Task.Delay(10, stoppingToken); } } } } }
1.数据结果
库存剩余:0
订单数量:100
是否超卖:否
2.性能
吞吐量:288/sec
最低响应:163ms
最高响应:3571ms
100条订单处理时间:10s
3.结论:订单处理的效果上,其实和 lock方式 类似,队列出队都是将订单一个个逐一处理(当然,可多新增线程去出队,提高出队性能),但是优点是,请求的下单接口可以容纳更高的并发量,不易导致超时或服务器无响应的情况,请求下单接口后,再轮询请求获取订单情况的接口,直至订单被处理,将结果返回
综上四种情况,都是常用的订单处理方式,最简单合适的其实是通过 SQL 的方式来更新商品库存,如果有更大的并发量,则建议使用队列的方式来保证服务可用