EF+MySQL乐观锁控制电商并发下单扣减库存,在高并发下的问题
下订单减库存的方式
现在,连农村的大姐都会用手机上淘宝购物了,相信电商对大家已经非常熟悉了,如果熟悉电商开发的同学,就知道在买家下单购买商品的时候,是需要扣减库存的,当然有2种扣减库存的方式,
一种是预扣库存,相当于锁定库存,
一种是直接扣减库存。
我们采用的是预扣库存的方式,预扣库存的时候,在SalesInfo表中,将最大可售数量MaxSalesNum减去购买数量,用一条SQL语句来表示这个业务,就是下面这个样子的:
update salesinfo set MaxSalesNum=MaxSalesNum-@BuyNum where Id=@ID
这是SqlServer的SQL语句格式,其它数据库大同小异。
下面讨论如何在高并发下实现这个扣减库存的问题。
初试:EF手工版乐观锁
我们用的EF(Entity Framework)+MySQL,很不幸,在 EF 中没法直接实现这个效果,它的DbContext数据上下文决定了要完成这种情况下的修改,得先查询到指定的数据到EF缓存,然后修改数据,最后保存数据, 更新可售库存的程序看起来是下面这个样子的(第一版的代码):
protected override int ChangeStock(SalesInfo salesInfo, OrderDetail detail)
{
using (var productdbContext = new UnitContextProducts())
{
using (var c = productdbContext.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
{
int retry = ;//如果出现更新的并发冲突,尝试一定次数
do
{
//查询最新的商品可售数量,由于EF 没法使用更新锁 forupdate,所以需要取时间戳用乐观锁
var currSalesInfo = (from p in productdbContext.Repository<dalProductModel.SalesInfo>().Entities
where p.Id == salesInfo.Id
select new
{
p.ModifiedTime,
p.SkuId,
p.MaxSalesNum,
p.Id
}).FirstOrDefault();
if (currSalesInfo != null)
{
//重新计算扣减后的库存,但是由于整个订单的处理不在当前事务内,还是有可能出现超买
int currStock = currSalesInfo.MaxSalesNum - detail.Quantity;
//加上时间戳进行更新判断,乐观锁,处理扣减库存的并发问题
productdbContext.Repository<dalProductModel.SalesInfo>().Update(p =>
p.Id == currSalesInfo.Id &&
p.MaxSalesNum == currSalesInfo.MaxSalesNum &&
p.ModifiedTime == currSalesInfo.ModifiedTime,
p => new dalProductModel.SalesInfo
{
MaxSalesNum = currStock,
ModifiedTime = DateTime.Now,
});
c.Commit();
int count = productdbContext.Commit();
if (count > )
{
salesInfo.MaxSalesNum = currStock;
return count;
}
System.Threading.Thread.Sleep();
}
}
while (--retry > ); }
return ;
}
}
上面的程序中,detail.Quantity 表示本次要购买的某个商品数量,currSalesInfo 是当前根据商品ID查询出来的数据,
int currStock = currSalesInfo.MaxSalesNum - detail.Quantity;
这个语句表示计算得到的预扣库存后的新库存,Update 方法是我们对EF进行的一个封装,第一个参数是要更新的条件,第二个参数是要更新的数据。
这里采用商品表的 ModifiedTime 字段来表示自上一次查询以后,看本次修改的时候有没有另外一个人先修改了,所以这里用 ModifiedTime 作修改的附加条件,相当于是一个“乐观锁”。
但是,经过简单压力测试,上面这个程序会出现“超买”,没有控制到并发修改库存的问题,于是尝试用“EF乐观锁”来解决这个扣减库存的问题,
进阶:EF乐观锁
参考了2篇文章《EF在MySQL中对记录的乐观并发控制(原创)》,《MySQL 实现 EF Code First TimeStamp/RowVersion 并发控制》,由于我们也是EF CodeFirst,所以着重参考了第二篇文章的做法,并且将ModifiedTime 字段改造成Timespan 类型,并添加触发器以便每次修改数据的时候自动更新该字段值,与支持EF的乐观锁,具体做法过程请参考第二篇文章内容。
下面是改写的代码(改写第二版):
//using (var trans = productdbContext.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
//{
//如果出现更新的并发冲突,尝试一定次数
bool retry = false;
int retrycount = ;
do
{
var currSalesInfo = (from p in productdbContext.DbContext.Set<dalProductModel.SalesInfo>()
where p.Id == salesInfo.Id
select p).FirstOrDefault();
if (currSalesInfo == null)
throw new Exception("没有找到指定的SalesInfo 记录: " + salesInfo.Id);
if(currSalesInfo.MaxSalesNum<=0) //必须判断,否则可能出现超卖
return 0;
//重新计算扣减后的库存,但是由于整个订单的处理不在当前事务内,还是有可能出现超买
int currStock = currSalesInfo.MaxSalesNum - detail.Quantity;
currSalesInfo.MaxSalesNum = currStock; try
{
int count = productdbContext.DbContext.SaveChanges();
if (count > )
{
//trans.Commit();
//salesInfo.MaxSalesNum = currStock; //网友 Ivan 提示要注释这个
retry = false;
return count;
}
}
catch (DbUpdateConcurrencyException ex)
{
retry = true;
ex.Entries.Single().Reload();
}
retrycount++;
if (retrycount > )
break;
}
while (retry);
// }//end using
注:为了避免我们对EF封装可能代码的问题,这里完全使用了EF最原始的方式来编写代码。
满怀希望的开始了测试,在每秒5次并发的时候,就出现了多扣减库存的问题。
结果不令人满意,还是会出现多扣减库存的问题。
进而反复改进事务的隔离级别,结果发现没有改善。
将代码仔细对比了原来博客文章,还有MSDN关于检测EF并发的文章,确认代码是正确的!
无奈:EF的ESQL
最后,又去国外技术论坛找了很久,无果,没有看到有这方面的说明,例子大部分都是SqlServer的,莫非这个并发功能对MySQL支持不好?
无赖之下,只有手写SQL上了,于是用ESQL,改写成下面的代码(第三版):
protected override int ChangeStock(SalesInfo salesInfo, OrderDetail detail)
{
var productdbContext = new UnitContextProducts();
string sql = string.Format("update salesinfo set MaxSalesNum=MaxSalesNum-{0} where Id={1}", detail.Quantity, salesInfo.Id);
int count1 = productdbContext.DbContext.Database.ExecuteSqlCommand(sql);
return count1;
}
OK,成功解决问题,原来问题解决起来如此简单,就是一条SQL语句:
update salesinfo set MaxSalesNum=MaxSalesNum-{0} where Id={1}
但是EF没有这种更新的时候,字段自增自减的功能。
问题虽然解决了,发现前面几个版本的代码好臃肿,但这样写,可能会引起新的问题,SQL语句的移植性降低了,不同数据库对表名字段名的格式要求可能会不同,比如Linux上的MySQL严格区分表名大小写,而Windows上的MySQL没有这个要求。
品尝 “SOD框架”的小菜
如果是SOD 框架,这个问题其实很好解决,用OQL的字段自更新语句即可:
SalesinfoEntity salesinfo=new SalesinfoEntity()
{
ID=,
MaxSalesNum= //要预扣的库存数
};
var q=OQL.From(salesinfo)
.UpdateSelf('-',salesinfo.MaxSalesNum)
.Where(salesinfo.ID)
.END;
EntityQuery<SalesinfoEntity>.Instance.ExecuteOql(q);//假设只有一个连接字符串配置
SOD框架式PDF.NET框架的数据开发框架,它简化了各种数据操作,其中的OQL是框架的ORM查询语言,这个字段自更新功能的更多信息,可以查看这篇文章《ORM查询语言(OQL)简介--实例篇》 2.1.2,UpdateSelf 字段自更新
如果你觉得EF在某些方面束缚了你的拳脚,可以选择SOD框架试试看,相信你选择它没错,尤其在金融和电商领域,目前框架已经有很多成功案例,请点击链接。
SOD框架已经全面开源,参见《[置顶]一年之计在于春,2015开篇:PDF.NET SOD Ver 5.1完全开源》。
补充:
在网友 上海-Ival的帮助下,他告诉我主要是 默认情况下MySQL DateTime 数据精度不够,需要使用精度更高的 timestamp 类型,并指定数据更新的时候地默认值,采用下面类似的SQL语句修改当前列的类型:
ALTER TABLE `test2`.`salesinfo`
CHANGE COLUMN `ModifiedTime` `ModifiedTime`
timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) ;
注意要指定精度为6。
实体类属性 ModifiedTime不用修改,仍然使用DateTime 类型。
但是需要指定属性为并发标记,代码如下:
public class ProductdbContext : DbContext
{
public DbSet<SalesInfo> SalesInfoes{get;set;} protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder); modelBuilder.Entity<SalesInfo>()
.Property(p => p.ModifiedTime)
.IsConcurrencyToken();
}
}
经过这样改进后,EF+MySQL终于可以处理并发更新了,非常感谢网友 上海-Ival 的帮助!
PS:虽然解决了本文的问题,但是EF这种并发处理方案,在代码编写上还是略显麻烦,是否使用ESQL或者其它ORM框架,看你的偏好了。
EF+MySQL乐观锁控制电商并发下单扣减库存,在高并发下的问题的更多相关文章
- 调用顺丰API实现电商专递下单和获取面单功能
参考文章:https://www.cnblogs.com/zhangxiaoyong/p/8317229.html 顺丰需求文档: 链接:https://pan.baidu.com/s/16EEaph ...
- 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存
原文:http://blog.csdn.net/heyewu4107/article/details/71009712 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存 问 ...
- redis分布式锁扣减库存弊端: 吞吐量低, 解决方法:使用 分段锁 分布式分段锁并发扣减库存--代码实现
package tech.codestory.zookeeper.aalvcai.ConcurrentHashMapLock; import lombok.AllArgsConstructor; im ...
- MySQL乐观锁为什么可以防止并发
问题引入 本文介绍的是最常用的也是mysql默认的innoDB引擎 Read committed隔离级别下事物的并发.这种情况下的事物特点是 读:在一个事物里面的select语句 不会受到其他事物(不 ...
- Java电商项目,秒杀,抢购等高并发场景的具体场景和一些概念以及处理思路
这里我借鉴了网上其他大佬的观点: 一:高并发带来的挑战 原因:秒杀抢购会经常会带来每秒几万的高并发场景,为了更快的返回结果给用户. 吞吐量指标QPS(每秒处理请求数),假设一个业务请求响应耗时为100 ...
- ElasticSearch内部基于_version乐观锁控制机制
1.悲观锁与乐观锁机制 为控制并发问题,我们通常采用锁机制.分为悲观锁和乐观锁两种机制. 悲观锁:很悲观,所有情况都上锁.此时只有一个线程可以操作数据.具体例子为数据库中的行级锁.表级锁.读锁.写锁等 ...
- 电商 APP 下单页(俗称车2) 业务流程概要设计
购物车是电商APP的一个关键功能点,一般购物车包含 3-4 个页面,分别是: 1.购物车的商品列表页 2.商品下单页 3.订单付款页面 4.订单付款成功页面 由于现有购物车逻辑相对混乱,这里重新整理一 ...
- MySQL 插入与自增主键值相等的字段 与 高并发下保证数据准确的实验
场景描述: 表t2 中 有 自增主键 id 和 字段v 当插入记录的时候 要求 v与id 的值相等(按理来说这样的字段是需要拆表的,但是业务场景是 只有某些行相等 ) 在网上搜的一种办法是 先获取 ...
- 使用mysql乐观锁解决并发问题
案例说明: 银行两操作员同时操作同一账户.比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交.最后实际账户余额为1000 ...
随机推荐
- HTML基础笔记-02
---恢复内容开始--- 学习网站:W3School 一.HTML的认识 纯文本语言:只显示内容,不显示样式,也不能描述语义的文档,但是也不会乱码 语义:数据的含义就是语义,数据是符号,在这表示标签 ...
- python的继承
继承是面向对象的重要特征之一,继承是两个类或者多个类之间的父子关系,子进程继承了父进程的所有公有实例变量和方法.继承实现了代码的重用.重用已经存在的数据和行为,减少代码的重新编写,python在类名后 ...
- C++标准库vector类型详解
Vector简介 vector是定义在C++标准模板库,它是一个多功能.能够操作多种数据结构和算法的模板类(关于模板类我们后面会介绍,如何创建自己的模板类).vector是一个容器,能够像容器一样存放 ...
- WPF自定义控件与样式(5)-Calendar/DatePicker日期控件自定义样式及扩展
一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 日历控 ...
- Nginx配置网站适配PC和手机
考虑到网站的在多种设备下的兼容性,有很多网站会有手机版和电脑版两个版本.访问同一个网站URL,当服务端识别出用户使用电脑访问,就打开电脑版的页面,用户如果使用手机访问,则会得到手机版的页面. 1.判断 ...
- Cocos2d-x 3.2 学习笔记(四)学习打包Android平台APK!
从cocos2dx 3.2项目打包成apk安卓应用文件,搭建安卓环境的步骤有点繁琐,但搭建一次之后,以后就会非常快捷! (涉及到3.1.1版本的,请自动对应3.2版本,3.x版本的环境搭建都是一样的) ...
- Android基于mAppWidget实现手绘地图(十二)–显示当前用户位置
若显示当前用户位置,你需要执行以下步骤: 1.添加ACCESS_COARSE_LOCATION 和ACCESS_FINE_LOCATION权限在AndroidManifest.xml中 2.用mapW ...
- d
1.sql题1表名:成绩表姓名 课程 分数张三 语文 81张三 数学 75李四 语文 56李四 数学 9 ...
- 【转载】CSS Sticky Footer: 完美的CSS绝对底部
下面是我找到的一个比较完美的方法,来自国外的设计达人,纯CSS,可以实现: 当正文内容很少时,底部位于窗口最下面.当改变窗口高度时,不会出现重叠问题. <div id="wrap&qu ...
- 百度地图API实现地图定位
1.引用JS: <script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0& ...