首页 > 其他分享 >库存超发问题

库存超发问题

时间:2022-10-13 10:03:48浏览次数:47  
标签:库存 -- redis 问题 version 超发 id stock


1. 库存超发的原因是什么?

在执行商品购买操作时,有一个基本流程:

库存超发问题_数据库

例如初始库存有3个。

第一个购买请求来了,想买2个,从数据库中读取到库存有3个,数量够,可以买,减库存后,更新库存为1个。

接下来第二个购买请求来了,想买2个,发现库存为1,不够,不可以买了。

这样是没问题的,但在高并发情况下,这2个购买请求很可能是一起来的,他们都读到库存是3,都可以买,就都去减库存,这时超发就发生了,结果库存变成 -1了。

库存超发问题_mysql_02

有多种方案来解决这个问题,我们主要看3种方案:

  • 悲观锁
  • 乐观锁
  • Redis + Lua
    下面分别看一下各个方案的实现思路,和优缺点。

2. 解决方案

2.1 悲观锁
出现超发现象的根本在于共享的数据被多个线程所修改,多个线程交织在一起。

如果一个线程读库存时就将数据锁定,不允许别的线程进行读写操作,直到库存修改完成才释放锁,那么就不会出现超发问题了。

例如:

// 事务开始
select id, product_name, stock, ...
from t_product
where id=#{id} for update

update t_product
set stock = stock - #{quantity}
where id = #{id}
// 事务结束

在查询库存时使用了 for update,这样在事务执行的过程中,就会锁定查询出来的数据,其他事务不能对其进行读写(注意,读也不行),这就避免了数据的不一致,直至事务完成释放锁。

  • 优点
    思路简单,代码实现也非常简单,从数据库层面解决了超发问题。
  • 缺点
    这种独占锁的方式对性能的影响是比较大的。

2.2 乐观锁
悲观锁有效但不高效,为了提高性能,出现了乐观锁方案,不使用数据库锁,不阻塞线程并发。

思路:

给商品记录添加一个 version 字段,读取库存时拿到这个 version 版本,更新库存时要对比这个 version 值,如果版本相同,说明库存没被别人改过,可以更新,同时把 version 值加1,如果版本不同,说明被别人改过了,则取消库存修改操作,购买失败。

库存超发问题_mysql_03

update t_product 
set stock = stock - #{quantity},
version = version +1
where id = #{id} and version = #{version}

通过 version 版本号,就可以知道自己读取的数据在更新时是不是旧的,如果是旧数据,就不能更新了。

这种方式有点像碰运气,运气好,没人和我一起更新,那么就成功;如果运气不好,被别人抢先修改了,那么就失败。

从而可以知道,在并发量很大的时候,失败的概率会比较高。

为了提升成功率,可以引入重试机制,当更新失败后,再走一遍流程(读取、更新),具体重走几遍比较好呢?可以规定一个次数,例如3次,如果重试了3次还是失败,就放弃;还可以规定一个时间段,比如在 100ms 内循环操作,期间如果某次成功了就退出,否则一直重试到时间到为止。

  • 优点
    没有阻塞,性能优于悲观锁。
  • 缺点
    实现思路较悲观锁复杂,增加了 version 的控制,还需要添加重试机制。

2.3 Redis + Lua
在高并发环境中,数据库的方案较慢,如果写入内存的 Redis 就会快很多。

此方案思路与悲观锁类似,都是把查询库存的操作与更新库存的操作绑定在一起,不被其他线程影响,区别在于存储介质,从数据库换为Redis。

Lua 脚本中可以编写逻辑(取库存、判断是否够用、更新库存),Redis 中执行 Lua 时可以保证原子性,所以能够满足我们的需求,而且内存操作非常快,我们也不用担心性能。

Lua 脚本的逻辑:

库存超发问题_mysql_04

示例代码:

-- 获取当前库存
local stock = tonumber(redis.call('hget', product, 'stock'))

-- 如果库存小于购买数量,说明库存不足,返回0(失败)
if stock < quantity then return 0 end

-- 减少库存,得到新的库存数量
stock = stock - quantity

-- 更新库存
redis.call('hset', product, 'stock', tostring(stock))

-- 字符串拼接,生成购买记录
local purchaseRecord = ...

-- 把购买记录保存到 redis
redis.call('rpush', purchaseList, purchaseRecord)

-- 返回1(成功)
return 1

我们的程序接收到用户的购买请求时,就调用 Lua 进行处理。

上面的处理流程中有一步”把购买记录写入 redis“,这是因为 redis 不适合做持久化,我们还是需要把数据同步到数据库中,可以使用一个定时程序,把 redis 中的记录写入数据库。购买记录也可以不放在 redis 中,写入消息队列,然后通过消费者同步到数据库。

优点
性能最优,实现简单。

缺点
增加了辅助工作,需要额外处理数据库的同步,还要保证 redis 本身是高可用的。

3. 小结

库存超发问题_数据_05


标签:库存,--,redis,问题,version,超发,id,stock
From: https://blog.51cto.com/u_15052623/5752247

相关文章

  • 开发过程冷门问题汇总
    1.如何让gif动图每一次都重新加载,达到显示动画效果的目的?问题产生原因:浏览器的缓存机制,为了更快的渲染,浏览第一次加载后会对图片进行缓存解决办法:加时间戳,加随机......
  • Python相对路径导入问题
    如果某个项目的文件结构如上,想要在f1.py中导入pkg包的时候,可能会这样写:from....importpkg但是很遗憾,这样会引发ImportError异常。直接运行f1.py时,异常信息是Import......
  • 企业大数据发展面临问题之存算分离技术思考
    @目录概述背景为何要存算分离优势应用场景存算分离产品技术流派华为JuiceFSHashDataXSKY概述背景Hadoop一出生就是奔存算一体设计,当时设计思想就是存储不动而计算(code......
  • JMeter响应数据中文乱码问题
    解决方法1.打开JMeter安装目录->bin文件夹->jmeter.properties文件2.编辑文件搜索关键字sampleresult.default.encoding删掉注释符#编码替换为UTF-8......
  • ORA-01440: 要减小精度或标度, 则要修改的列必须为空问题修复
    环境准备:使用oracle数据库:createtabletable01(idnumber(16)notnull,namevarchar2(50)defaultnull,moneynumber(12,6)defaultnull,primarykey......
  • 【万能的圈友】SQL Server 磁盘空间不足问题分析
    JZGKCHINA工控技术分享平台技术交流与分享 是剑指工控全部的意义所在不论你在哪里,不论你遇到怎样的技术问题,剑指工控群里总有那么一群带有工控情结的技术人与你一起面对,一......
  • cesium教程9-加载倾斜摄影并解决高度问题
    无人机航拍的倾斜摄影,用照片和视频处理生成三维模型,一般照片都带有坐标信息,所以一般都能定位的比较准确,但是经常会出现高度偏差,这个时候就需要特殊处理了。今天航拍建模的......
  • Python解决千年虫问题
    #避免千年虫(日期bug)问题千年虫:部分计算机程序使用年份后两位作为记录年份,当日期跳转到00时候,默认会解析为1900,造成系统紊乱lst=[45,89,1998,00,75,33,1968,37,1958,90]l......
  • 陆地观测卫星数据服务(CRESDA)订单ftp地址错误—已解决不能下载问题
    陆地观测卫星数据服务订单ftp地址错误问题:本人在陆地观测卫星数据网站上申请GF1-WFV10幅数据,订单完成后返回的FTP地址出现无法连接服务器现象。(数据订单申请已通过)一、情......
  • 阿里云服务器数据盘扩容后,宝塔面板显示旧容量的问题
    发现系统盘不够用了,在阿里云后台扩容后,宝塔面板上看不到新的容量,重启后还是看不见。首先,要备份数据库和重要文件的备份,创建快照,以防在硬盘扩容时出现意外导致数据丢失。......