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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS MySQL,高可用系列 2核4GB
简介:

下订单减库存的方式

现在,连农村的大姐都会用手机上淘宝购物了,相信电商对大家已经非常熟悉了,如果熟悉电商开发的同学,就知道在买家下单购买商品的时候,是需要扣减库存的,当然有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);
                    if(currSalesInfo.MaxSalesNum<=0) //必须判断,否则可能出现超卖
return 0;
//重新计算扣减后的库存,但是由于整个订单的处理不在当前事务内,还是有可能出现超买 int currStock = currSalesInfo.MaxSalesNum - detail.Quantity; currSalesInfo.MaxSalesNum = currStock; try { int count = productdbContext.DbContext.SaveChanges(); if (count > 0) { //trans.Commit(); //salesInfo.MaxSalesNum = currStock; //网友 Ivan 提示要注释这个 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完全开源》。

 

补充:

在网友 上海-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框架,看你的偏好了。

 



    本文转自深蓝医生博客园博客,原文链接:http://www.cnblogs.com/bluedoctor/p/4294655.html,如需转载请自行联系原作者



相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
5月前
|
缓存 关系型数据库 MySQL
在MySQL中处理高并发和负载峰值的关键技术与策略
采用上述策略和技术时,每个环节都要进行细致的规划和测试,确保数据库系统既能满足高并发的要求,又要保持足够的灵活性来应对各种突发的流量峰值。实施时,合理评估和测试改动对系统性能的影响,避免单一措施可能引起的连锁反应。持续的系统监控和分析将对维护系统稳定性和进行未来规划提供重要信息。
285 15
|
6月前
|
关系型数据库 MySQL 分布式数据库
Super MySQL|揭秘PolarDB全异步执行架构,高并发场景性能利器
阿里云瑶池旗下的云原生数据库PolarDB MySQL版设计了基于协程的全异步执行架构,实现鉴权、事务提交、锁等待等核心逻辑的异步化执行,这是业界首个真正意义上实现全异步执行架构的MySQL数据库产品,显著提升了PolarDB MySQL的高并发处理能力,其中通用写入性能提升超过70%,长尾延迟降低60%以上。
|
9月前
|
消息中间件 NoSQL 关系型数据库
去哪面试:1Wtps高并发,MySQL 热点行 问题, 怎么解决?
去哪面试:1Wtps高并发,MySQL 热点行 问题, 怎么解决?
去哪面试:1Wtps高并发,MySQL 热点行 问题, 怎么解决?
|
SQL 关系型数据库 MySQL
(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?
锁!这个词汇在编程中出现的次数尤为频繁,几乎主流的编程语言都会具备完善的锁机制,在数据库中也并不例外,为什么呢?这里牵扯到一个关键词:高并发,由于现在的计算机领域几乎都是多核机器,因此再编写单线程的应用自然无法将机器性能发挥到最大,想要让程序的并发性越高,多线程技术自然就呼之欲出,多线程技术一方面能充分压榨CPU资源,另一方面也能提升程序的并发支持性。
1198 3
|
存储 SQL 关系型数据库
(二十一)MySQL之高并发大流量情况下海量数据分库分表的正确姿势
从最初开设《全解MySQL专栏》到现在,共计撰写了二十个大章节详细讲到了MySQL各方面的进阶技术点,从最初的数据库架构开始,到SQL执行流程、库表设计范式、索引机制与原理、事务与锁机制剖析、日志与内存详解、常用命令与高级特性、线上调优与故障排查.....,似乎涉及到了MySQL的方方面面。但到此为止就黔驴技穷了吗?答案并非如此,以《MySQL特性篇》为分割线,整个MySQL专栏从此会进入“高可用”阶段的分析,即从上篇之后会开启MySQL的新内容,主要讲述分布式、高可用、高性能方面的讲解。
898 1
|
SQL 关系型数据库 MySQL
(十六)MySQL调优篇:单机数据库如何在高并发场景下健步如飞?
在当前的IT开发行业中,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代中一个炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题。而MySQL作为整个系统的后方大本营,由于是基于磁盘的原因,性能瓶颈往往也会随着流量增大而凸显出来。
1657 0
|
监控 关系型数据库 分布式数据库
【PolarDB开源】PolarDB在电商场景的应用:应对高并发与数据一致性挑战
【5月更文挑战第26天】阿里云PolarDB是为电商解决高并发和数据一致性问题的云原生数据库。它采用读写分离、弹性扩展和分布式缓存策略应对高并发,通过全局时钟、分布式事务和数据复制保证数据一致性。在大型促销活动中,电商平台可提前扩容、启用读写分离、优化索引并设置监控告警来应对挑战。PolarDB助力电商构建高性能、高可用的数据处理系统,赢得市场优势。
526 1
|
监控 应用服务中间件 nginx
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
165 0
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
192 0
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
229 0

推荐镜像

更多