0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

库存扣减和锁常见的实现方案代码

Android编程精选 来源:CSDN博客 作者:北京-小北 2021-11-06 14:48 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

先说场景:

物品W现在库存剩余1个, 用户P1,P2同时购买.则只有1人能购买成功.(前提是不允许超卖)

秒杀也是类似的情况, 只有1件商品,N个用户同时抢购,只有1人能抢到..

这里不谈秒杀设计,不谈使用队列等使请求串行化,就谈下怎么用锁来保证数据正确.

常见的实现方案有以下几种:

  1. 代码同步, 例如使用 synchronized ,lock 等同步方法
  2. 不查询,直接更新 update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0
  3. 使用CAS, update table set surplus = aa where id = xx and version = y
  4. 使用数据库锁, select xx for update
  5. 使用分布式锁(zookeeper,redis等)

下面就针对这几种方案来分析下;

1.代码同步, 例如使用 synchronized ,lock 等同步方法

面试的时候,我经常会问这个问题,很大一部分人都会回答用这个方案来实现.

伪代码如下:

publicsynchronizedvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校验...
//校验剩余数量
Productproduct=从数据库查询出记录;
if(product.getSurplus< buyQuantity) {
  return"库存不足";
}

//set新的剩余数量
product.setSurplus(product.getSurplus()-quantity);
//更新数据库
update(product);
//记录日志...
//其他业务...
}

在方法声明加上synchronized关键字,实现同步,这样2个用户同时购买,到buy方法时候同步执行,第2个用户执行的时候,会库存不足.

嗯.. 看着挺合理的,以前我也是这么干的. 所以现在碰到别人这样回答,我就会在心里默默的想.小伙子你是没踩过这坑啊.

先说下这个方案的前提配置:

  • 使用spring 声明式事务管理
  • 事务传播机制使用默认的(PROPAGATION_REQUIRED)
  • 项目分层为controller-service-dao 3层, 事务管理在service层

这个方案不可行,主要是因为以下几点:

1).synchronized 作用范围是单个jvm实例, 如果做了集群,分布式等,就没用了

2).synchronized是作用在对象实例上的,如果不是单例,则多个实例间不会同步(这个一般用spring管理bean,默认就是单例)

3).单个jvm时,synchronized也不能保证多个数据库事务的隔离性. 这与代码中的事务传播级别,数据库的事务隔离级别,加锁时机等相关.

3-1).先说隔离级别,常用的是 Read Committed 和 Repeatable Read ,另外2种不常用就不说了

3-1-1)RR(Repeatable Read)级别.mysql默认的是RR,事务开启后,不会读取到其他事务提交的数据

根据前面的前提,我们知道在buy方法时会开启事务.

假设现在有线程T1,T2同时执行buy方法.假设T1先执行,T2等待.

spring的事务开启和提交等是通过aop(代理)实现的,所以执行buy方法前,就会开启事务.

这时候T1,T2是两个事务,当T1执行完后,T2执行,读取不到T1提交的数据,所以会出问题.

3-1-2).RC(Read Committed)级别.事务开启后,可以读取到其他事务提交的数据

看起来这个级别可以解决上面的问题.T2执行时,可以读取到T1提交的结果.

但是问题是,T2执行的时候, T1的事务提交了吗?

事务和锁的流程如下

  1. 开启事务(aop)
  2. 加锁(进入synchronized方法)
  3. 释放锁(退出synchronized方法)
  4. 提交事务(aop)

可以看出是先释放锁,再提交事务.所以T2执行查询,可能还是未读到T1提交的数据,还会出问题

3-2).根据3-1中的问题,发现主要矛盾是事务开启和提交的时机与加锁解锁时机不一致.有小伙伴们可能就想到了解决方案.

3-2-1).在事务开启前加锁,事务提交后解锁.

确实是可以,这相当于事务串行化.抛开性能不谈,来谈谈怎么实现.

如果使用默认的事务传播机制,那么要保证事务开启前加锁,事务提交后解锁,就需要把加锁,解锁放在controller层.

这样就有个潜在问题,所有操作库存的方法,都要加锁,而且要是同一把锁,写起来挺累的.

而且这样还是不能跨jvm.

3-2-2).将查询库存,扣减库存这2步操作,单独提取个方法,单独使用事务,并且事务隔离级别设置为RC.

这个其实和上面的3-2-1异曲同工,最终都是讲加解锁放在了事务开启提交外层.

比较而言优点是入口少了. controller不用处理.

缺点除了上面的不能跨jvm,还有就是 单独的这个方法,需要放到另外的service类中.

因为使用spring,同一个bean的内部方法调用,是不会被再次代理的,所以配置的单独事务等需要放到另外的service bean 中

2.不查询,直接更新

看完第一种方案,有小伙伴就说了. 你说的那么复杂,那么多问题,不就是因为查询的数据不是最新的吗?

我们不查询,直接更新不就行啦.

伪代码如下:

publicsynchronizedvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校验...
int影响行数=updatetablesetsurplus=(surplus-buyQuantity)whereid=1;
if(result< 0){
return"库存不足";
}
//记录日志...
//其他业务...
}

测试后发现库存变成-1了, 继续完善下

publicsynchronizedvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校验...
int影响行数=updatetablesetsurplus=(surplus-buyQuantity)whereid=1and(surplus-buyQuantity)>0;
if(result< 0){
return"库存不足";
}
//记录日志...
//其他业务...
}

测试后,功能OK;

这样确实可以实现,不过有一些其他问题:

  • 不具备通用性,例如add操作
  • 库存操作一般要记录操作前后的数量等,这样没法记录
  • 其他...

但是根据这个方案,可以引出方案3.

3.使用CAS, update table set surplus = aa where id = xx and yy = y

CAS是指compare/check and swap/set 意思都差不多,不必太纠结是哪个单词

我们将上面的sql修改一下:

int影响行数=updatetablesetsurplus=newQuantitywhereid=1andsurplus=oldQuantity;

这样,线程T1执行完后,线程T2去更新,影响行数=0,则说明数据被更新, 重新查询判断执行.伪代码如下:

publicvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校验...
Productproduct=getByDB(productName);
int影响行数=updatetablesetsurplus=(surplus-buyQuantity)whereid=1andsurplus=查询的剩余数量;
while(result==0){
product=getByDB(productName);
if(查询的剩余数量>buyQuantity){
影响行数=updatetablesetsurplus=(surplus-buyQuantity)whereid=1andsurplus=查询的剩余数量;
}else{
return"库存不足";
}
}

//记录日志...
//其他业务...
}

看到重新查询几个字,小伙伴们应该就又想到事务隔离级别问题了.

没错,所以上面代码中的getByDB方法,必须单独事务(注意同一个bean内单独事务不生效哦),而且数据库的事务隔离级别必须是RC,

否则上面的代码就会是死循环了.

上面的方案,可能会出现一个CAS中经典问题. ABA的问题.

ABA是指:

  • 线程T1 查询,库存剩余 100
  • 线程T2 查询,库存剩余 100
  • 线程T1 执行 sub update t set surplus = 90 where id = x and surplus = 100;
  • 线程T3 查询, 库存剩余 90
  • 线程T3 执行add update t set surplus = 100 where id = x and surplus = 90;
  • 线程T2 执行sub update t set surplus = 90 where id = x and surplus = 100;

这里线程T2执行的时候,库存的100已经不是查询到的100了,但是对于这个业务是不影响的.

一般的设计中CAS会使用version来控制.

updatetsetsurplus=90,version=version+1whereid=xandversion=oldVersion;

这样,每次更新version在原基础上+1,就可以了.

使用CAS要注意几点,

  • 失败重试次数,是否需要限制
  • 失败重试对用户是透明的

4.使用数据库锁, select xx for update

方案3种的cas,是乐观锁的实现, 而select for udpate 则是悲观锁. 在查询数据的时候,就将数据锁住.

伪代码如下:

publicvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校验...
Productproduct=select*fromtablewherename=productNameforupdate;
if(查询的剩余数量>buyQuantity){
影响行数=updatetablesetsurplus=(surplus-buyQuantity)wherename=productName;
}else{
return"库存不足";
}

//记录日志...
//其他业务...
}

线程T1 进行sub , 查询库存剩余 100

线程T2 进行sub , 这时候,线程T1事务还未提交,线程T2阻塞,直到线程T1事务提交或回滚才能查询出结果.

所以线程T2查询出的一定是最新的数据.相当于事务串行化了,就解决了数据一致性问题.

对于select for update,需要注意的有2点.

  1. 统一入口:所有库存操作都需要统一使用 select for update ,这样才会阻塞, 如果另外一个方法还是普通的select, 是不会被阻塞的

  2. 加锁顺序:如果有多个锁,那么加锁顺序要一致,否则会出现死锁.

5.使用分布式锁(zookeeper,redis等)

使用分布式锁,原理和方案1种的synchronized是一样的.只不过synchronized的flag只有jvm进程内可见,而分布式锁的flag则是全局可见.方案4种的select for update 的flag 也是全局可见.

分布式锁的实现方案有很多:基于redis,基于zookeeper,基于数据库等等.

需要注意,使用分布式锁和synchronized锁有同样的问题,就是锁和事务的顺序,这个在方案1里面已经讲过.不再重复.

做个简单总结:

  • 方案1: synchronized等jvm内部锁不适合用来保证数据库数据一致性,不能跨jvm
  • 方案2: 不具备通用性,不能记录操作前后日志
  • 方案3: 推荐使用.但是如果数据竞争激烈,则自动重试次数会急剧上升,需要注意.
  • 方案4: 推荐使用.最简单的方案,但是如果事务过大,会有性能问题.操作不当,会有死锁问题
  • 方案5: 和方案1类似,只是能跨jvm
责任编辑:haq
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据
    +关注

    关注

    8

    文章

    7315

    浏览量

    94001
  • 源代码
    +关注

    关注

    96

    文章

    2953

    浏览量

    69681

原文标题:实践角度,谈谈库存扣减和锁

文章出处:【微信号:AndroidPush,微信公众号:Android编程精选】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    实时库存同步接口技术详解

    常见挑战及解决方案,帮助开发者构建高效可靠的接口。 1. 接口的核心概念 实时库存同步接口基于事件驱动架构,通过API(如RESTful或GraphQL)实现数据交换。关键目标是保证
    的头像 发表于 10-10 14:33 303次阅读
    实时<b class='flag-5'>库存</b>同步接口技术详解

    京东商品 SKU 信息接口技术干货:数据拉取、规格解析与字段治理(附踩坑总结 + 可运行代码

    本文详解京东商品SKU接口对接技术,涵盖核心参数、权限申请、签名生成、规格解析及常见坑点解决方案,结合可运行代码与实战经验,助力开发者高效集成SKU数据,实现
    的头像 发表于 09-29 11:56 335次阅读
    京东商品 SKU 信息接口技术干货:数据拉取、规格解析与字段治理(附踩坑总结 + 可运行<b class='flag-5'>代码</b>

    唯品会:对接商品库存API,实现限时特卖活动库存动态预警,避免超卖

    。唯品会作为领先的特卖电商平台,通过高效对接商品库存API,实现库存动态预警系统,有效避免了超卖风险。本文将逐步解析这一解决方案,帮助读者理解其原理和实施过程。 一、超卖风险的根源与
    的头像 发表于 09-10 16:30 487次阅读

    借助唯品会 API,唯品会店铺运动商品库存管理优化

    的 API(应用程序编程接口),帮助店铺实现智能化库存管理。本文将逐步介绍如何借助唯品会 API 优化运动商品库存,包括技术实现、核心算法和实际效益。 1.
    的头像 发表于 09-03 17:11 608次阅读
    借助唯品会 API,唯品会店铺运动商品<b class='flag-5'>库存</b>管理优化

    ​一文了智能门锁常见的几种语音芯片方案

    智能是一种成熟且稳定的产品类型,它对语音芯片的要求,大致集中于以下几点: 使用简单且好打样:由于智能大多依托方案公司开发,购买 pcba 回来组装,产品类型丰富,语音需求多样,频繁打样会影响开发
    的头像 发表于 08-01 17:31 867次阅读

    淘宝API一键同步库存,销量翻倍轻松实现

    功能,您可以实时更新商品库存,避免手动操作的繁琐和错误,从而显著提升销售转化率。许多成功店铺的经验表明,合理利用此功能,销量翻倍并非遥不可及的目标。本文将一步步引导您如何实现这一过程,确保操作真实可靠。 什么是
    的头像 发表于 07-28 14:48 334次阅读
    淘宝API一键同步<b class='flag-5'>库存</b>,销量翻倍轻松<b class='flag-5'>实现</b>!

    API驱动的大型电商平台库存优化

    实现系统间的无缝集成和数据实时交换,为库存优化提供了强大支持。本文将逐步探讨API如何驱动库存优化,包括其原理、关键技术和实际应用,帮助您理解并实施高效策略。 一、API在库存管理中的
    的头像 发表于 07-15 14:42 376次阅读
    API驱动的大型电商平台<b class='flag-5'>库存</b>优化

    苏宁易购电商 API 接口,家电库存管理智能方案

    的智能库存管理方案,专为家电品类设计。本文将逐步解析这一方案的核心要素、实施路径及实际效益,帮助企业高效优化库存运营。 一、电商库存管理的痛
    的头像 发表于 07-07 14:59 406次阅读
    苏宁易购电商 API 接口,家电<b class='flag-5'>库存</b>管理智能<b class='flag-5'>方案</b>

    联世界,智启万家,华普微蓝牙智能解决方案

    密码、指纹、刷卡与多模组合等主流门锁形态,支持与手机APP、小程序、智能中控网关等多终端互联,助力客户快速实现智能门锁产品的量产落地
    的头像 发表于 06-26 15:46 509次阅读
    <b class='flag-5'>锁</b>联世界,智启万家,华普微蓝牙智能<b class='flag-5'>锁</b>解决<b class='flag-5'>方案</b>

    自动螺丝机低成本远程维护方案:PLC 数据采集 + 云端协同实现运维成本下降 40%

    自动螺丝机PLC远程维护管理系统方案
    的头像 发表于 06-25 10:41 354次阅读
    自动<b class='flag-5'>锁</b>螺丝机低成本远程维护<b class='flag-5'>方案</b>:PLC 数据采集 + 云端协同<b class='flag-5'>实现</b>运维成本下降 40%

    RFID在服装库存中的应用

    RFID是一种通过无线电波进行数据读写和物体识别的技术。它由电子标签、读写器和天线组成,能够实现非接触式的数据交互。相比传统的条形码技术,RFID具有更高的效率、更强的抗污染能力和更远的读取距离
    的头像 发表于 04-09 16:18 583次阅读
    RFID在服装<b class='flag-5'>库存</b>中的应用

    电路常见故障及解决方法

    继电器、按钮、限位开关等组成。当电路中的某个条件被满足时,继电器的常闭触点会断开,而常开触点会闭合,从而保持电路的状态,即使初始条件不再满足。 常见故障 1. 电路无法自 故障原因 继电器损坏或触点粘连。 按钮或限位开关
    的头像 发表于 01-18 10:05 3488次阅读

    电路怎么实现自动控制

    在现代电子技术中,自动控制是实现智能化和自动化的关键。自电路作为一种基本的自动控制电路,因其简单、可靠和易于实现的特点,被广泛应用于各种自动控制系统中。 1. 自电路的工作原理 自
    的头像 发表于 01-18 10:04 2025次阅读

    电路的类型和特点

    在电子工程领域,自电路是一种常见的设计,它能够使电路在没有持续的触发信号的情况下保持其状态。这种电路的设计对于实现自动化控制和减少人为干预至关重要。 一、自电路的类型 自
    的头像 发表于 01-18 10:03 1648次阅读

    电路如何设计

    电路的设计旨在实现电路在按下开关后能自动保持持续通电,直到按下其他开关使之断路为止的功能。以下是自电路设计的基本步骤和要点: 一、基本设计步骤 接入电源 : 将零线接入电路的指定端子(如十一号
    的头像 发表于 01-18 09:56 2858次阅读