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 = 10;//如果出现更新的并发冲突,尝试一定次数
            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 > 0)
                    {
                                salesInfo.MaxSalesNum = currStock;
                                return count;
                    }
                    System.Threading.Thread.Sleep(1000);
                }
            }
            while (--retry > 0);

        }
        return 0;
    }
}

上面的程序中,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 = 0;
                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);

                    //重新计算扣减后的库存,但是由于整个订单的处理不在当前事务内,还是有可能出现超买
                    int currStock = currSalesInfo.MaxSalesNum - skuInOrder.Quantity;
                    currSalesInfo.MaxSalesNum = currStock;

                    try
                    {
                        int count = productdbContext.DbContext.SaveChanges();
                        if (count > 0)
                        {
                            //trans.Commit();
                            salesInfo.MaxSalesNum = currStock;
                            retry = false;
                            return count;
                        }
                    }
                    catch (DbUpdateConcurrencyException ex)
                    {
                        retry = true;
                        ex.Entries.Single().Reload();
                    }
                    retrycount++;
                    if (retrycount > 100)
                        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=99,
  MaxSalesNum=1 //要预扣的库存数
};
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完全开源》。

时间: 2024-10-01 20:28:47

EF+MySQL乐观锁控制电商并发下单扣减库存,在高并发下的问题的相关文章

亿级流量电商详情页系统实战-缓存架构+高可用服务架构+微服务架构第二版视频教程

14套java精品高级架构课,缓存架构,深入Jvm虚拟机,全文检索Elasticsearch,Dubbo分布式Restful 服务,并发原理编程,SpringBoot,SpringCloud,RocketMQ中间件,Mysql分布式集群,服务架构,运 维架构视频教程 14套精品课程介绍: 1.14套精 品是最新整理的课程,都是当下最火的技术,最火的课程,也是全网课程的精品: 2.14套资 源包含:全套完整高清视频.完整源码.配套文档: 3.知识也 是需要投资的,有投入才会有产出(保证投入产出比是

电商 APP 下单页(俗称车2) 业务流程概要设计

购物车是电商APP的一个关键功能点,一般购物车包含 3 个页面,分别是: 1.购物车的商品列表页 2.商品下单页 3.订单付款页面 4.订单付款成功页面 由于现有购物车逻辑相对混乱,这里重新整理一下商品下单页的业务流程设计 1.生成订单 这里在业务层面把订单的生命周期划分为4个阶段,分别是: 订单的初始阶段 订单的完备阶段 订单的支付阶段 订单的服务阶段 1.1 订单的初始阶段 订单的初始阶段是在 购物车商品列表页开始的,用户在购物车商品页面,确定 订单的 商品的种类和各个商品的数量. 商品的种

MySQL 插入与自增主键值相等的字段 与 高并发下保证数据准确的实验

场景描述: 表t2 中 有 自增主键 id  和 字段v  当插入记录的时候 要求 v与id 的值相等(按理来说这样的字段是需要拆表的,但是业务场景是 只有某些行相等 ) 在网上搜的一种办法是 先获取自增ID SELECT max(id)+1 from t2 然后给v字段插入获取到的值 但是这样的做法在有删除行+调整过自增值的表中是不准确的 于是换个思路 从 information_schema 下手 读取表的信息 INSERT INTO `t2` VALUES ( NULL, ( SELECT

电商商品秒杀系统架构分析与实战

网址:http://my.oschina.net/xianggao/blog/524943 0 系列目录 1 秒杀业务分析 2 秒杀技术挑战 3 秒杀架构原则 4 秒杀架构设计 4.1 前端层设计 4.2 站点层设计 4.3 服务层设计 4.4 数据库设计 4.4.1 基本概念 4.4.2 设计思路 5 大并发带来的挑战 5.1 请求接口的合理设计 5.2 高并发的挑战:一定要“快” 5.3 重启与过载保护 6 作弊的手段:进攻与防守 6.1 同一个账号,一次性发出多个请求 6.2 多个账号,一

电商 秒杀系统 设计思路和实现方法

电商 秒杀系统 设计思路和实现方法 2017年05月26日 00:06:35 阅读数:3662 1 秒杀业务分析 正常电子商务流程 (1)查询商品:(2)创建订单:(3)扣减库存:(4)更新订单:(5)付款:(6)卖家发货 秒杀业务的特性 (1)低廉价格:(2)大幅推广:(3)瞬时售空:(4)一般是定时上架:(5)时间短.瞬时并发量高: 2 秒杀技术挑战 假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有: 对现有网站业务造

ES6+ 开发电商网站的账号体系 JS SDK

详情请咨询  QQ  709639943 01.ES6+ 开发电商网站的账号体系 JS SDK 02.Python3 全网最热的Python3入门+进阶 比自学更快上手实际开发 03.Python3.6 强力Django+杀手级Xadmin打造上线标准的在线教育平台 04.python_进阶强化 05.Java秒杀系统方案优化 高性能高并发实战 06.企业级刚需Nginx入门,全面掌握Nginx配置+快速搭建高可用架构 07.快速上手Linux 玩转典型应用 08.全面系统讲解CSS 工作应用+

Kotlin打造完整电商APP 模块化+MVP+主流框架

详情请交流  QQ  709639943 01.Kotlin打造完整电商APP 模块化+MVP+主流框架 02.Kotlin系统入门与进阶 03.Node.js入门到企业Web开发中的应用 04.精通高级RxJava 2响应式编程思想 05.Java秒杀系统方案优化 高性能高并发实战 06.Java深入微服务原理改造房产销售平台 07.快速上手Linux 玩转典型应用 08.快速上手Ionic3 多平台开发企业级问答社区 09.Java Spring Security开发安全的REST服务 10

电商网站产品数据库设计

1.最近在自学java,想用java+mysql做一个小小的电商项目实例,首先设计出产品相关的数据表,如下图 2.平常设计的话都会把产品的属性放到产品表,比如颜色.规格.尺码等.不过我把属性单独拿出两张表,一张表存属性名,一张表存属性值. 3.本来考虑要品牌表的,但是后来想想品牌可以放到属性表里,把产品当成一个属性存进P_Property表,然后把品牌对应的值存到属性值表中. 4.不知道这么设计有没有欠缺的地方,再此抛砖引玉,希望能有电商方面的大神前来指导.

【完整版】七千字长文揭秘万达电商

本人17年工作经验,在加盟万达电商之前做过技术.销售.咨询,就企业性质而言私企.国企.外企都干过.与此前的经历相比,万达电商有很多值得八卦的地方. 大时代提供的机遇--揭秘万达电商(1) 不期而至的选择 2013年5月,猎头推荐工作机会--万达电商,开始我兴趣不大,原因你懂的--经过几轮沟通,我于2014年初,结束手中200万的咨询项目正式加盟. 至今都有不少朋友质疑,"听说这家公司很乱啊!人员不稳定,方向不明确!你胆儿还挺大--" 客观讲,万达电商能给出的薪水处于市场较高水平,极具诱