秒杀 超卖 sql

文章讨论内容

转自mysql商品库存扣减问题总结

秒杀类的问题一直都是web领域比较热点的问题,一个超高并发的网站需要考虑从产品、前端优化、站点部署及后端服务等等所有环节进行考虑。mysql所能抗住的写压力是一定的,高并发的web站点,你需要在数据持久化之前控制好压力,而不是把所有的请求都落到数据服务这一层。今天我不在这篇文章里讨论秒杀整体设计的问题(我也没这个资格),我们讨论的是如何在流速已经得到控制的情况下,如何利用mysql更安全、高效的解决这个问题。

从网上可以看到各种各样的实现方案,现在针对这些方案及其优缺点和理解误区进行讨论。

常见写法安全性及效率分析

假设我们的商品表的schema是下面这样的:

CREATE TABLE `goods` (  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增id',  `name` varchar(256) NOT NULL DEFAULT '' COMMENT '商品名称',  `available` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '库存剩余量',  `stock` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '总库存量',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表'

设置为字段无符号解决

num = select available from goods where id = xx ;if(num > 0){   affectRows = udpate goods set available = available - 1 where id = xx ;   if(affectRows == 1){       return ok ;   }else{       return fatal ;   }}

解法释义

这种做法大家的想法是我们将库存字段设置成无符号类型,这样当库存字段在sql执行时候被置为负数的时候mysql就会报错,那么affectRow就会是0或者可以捕获到这个异常,从而实现并发下的数据安全。

实际上这段代码是危险的,因为在不同版本的mysql和配置下,这段代码的表现完全不同。具体的情况会出现3种不同的结果:

  • 1.代码正常运行,执行update的时候报错
  • 2.代码最终执行结果出现了 -1
  • 3.最终update操作之后,available变成了一个很大的数目

为什么会出现这三种情况呢?

我想在学习开始学习计算机的时候都讲过计算机的加减法计算方法。

思考一下,无符号2 减去 无符号3 在计算机中的运算是什么样的?2 - 3 = 2 + (-3)假设我们的计算机是4位的,2的补码表示:0010,-3的补码表示为1011那么加和的结果是00101011 + ------1111 =1111解释为有符号数是多少呢? -11111解释为无符号数是多少呢? 15

所以呢?
如果mysql不做任何处理的话,你的无符号数减法的结果不会报错,最终你算出来的库存还是一个非常大的值(可怕)。
但是幸运的是mysql 后来的版本帮你做了这件事情(具体哪个版本我也不清楚),所以如果是mysql做了无符号检测的话,如果减出的结果是负值,会报错,这是大多数人期待的结果。
-1这种情况是需要你设置一下sqlmode的,这也是会出现的情况。

解法总结

  • 这个办法很多人用的时候没问题,那只能说明可能是机缘巧合,但是对于业务代码而言,不能靠碰运气,需要消除不确定性、缩小迁移成本。
  • 如果你想采用这种办法,辛苦你把你们msyql相应的版本及配置搞清楚,确定无符号在你所在的版本会出现什么结果。

select for update

解法释义

读取时候就开始加排他锁也是网上常见的办法之一,具体实现如下:

begin tran ;num = select avaliable from goods where id = xxx for update;if (num >= 0){   affectNum = udpate goods set available = available - 1 where id = xx ;   commit ;   return affectNum ;}else{   rollback ;}

该解法在用户读取的时候对相应的数据加排他锁,保证自己在更新的时候该行的数据不会被别的进程更改.所有写请求及排他锁加锁都会被阻塞。
想想这样的情况,A进程执行过程中,出现死机的情况导致commit/rollback请求没有被发送到mysqlserver,那么所有请求都会锁等待。

解法总结

  • 低流量可以采用这种办法来保证数据的安全性
  • 性能低下

    ,平均需要发送4次mysql请求,同时会造成所有同类请求锁等待。

    常见问题

  • select for udpate 需要在显式的指定在事务代码块执行,不然不会起作用。很多网友都理所当然的人为select for update直接就可以加排他锁
  • 排他锁的释放是在rollback/commit 动作完成才会释放,不是在update操作之后。mysql innodb执行两段锁协议,加锁阶段只加锁,解锁阶段只解锁。

采用事务,先查后写再查,确保没问题

解法释义

这时候的available设置为有符号类型,解决方案一的问题

begin tran ;num = select available from goods where id = xx ;if(num > 0){   //实际需要关心这里的返回值,这里不考虑   udpate goods set available = available - 1 where id = xx ;   num_afterupdate = select available from goods where id = xx ;   if(num_afterupdate < 0 ){       rollback ;   }else{       commit ;   }}

这种解法区分于第一种的办法在于,加了事务、available类型更改、采用了更新后确认的形式,尝试解决问题。

我们都知道数据库的事务隔离级别有4种:
RU,RC,RR,Serializable。
我们常见的innodb中RR模式是可以保证可重复读,意思是在同一个事务内部,多次读取的结果是一致的。那么最后一次的读取对于RR隔离级别实际上是无效的。
RC模式下,这个代码是可用的,每次请求可以确保自己的进程不会超发。

解法总结

  • RR、RC模式下结果不一致.RR下不可保证安全、RC可以。
  • 性能不高,一次业务请求到mysql的转化为 1 : 5。
  • 这种解法就像老奶奶锁门,总是不放心自己到底锁了没有,走了几步再回来看看,实际上有些时候是徒劳。

update语句增加available查询条件

解法释义

udpate goods set available = available - 1 where id = xx and available - 1 >= 0 ;

这个语句如果可以的话,那么他的性能必然是上面锁提到的方法中最优的.
问题的关键就集中在怎么证明这句的安全性的。
我们都知道update操作对于id为主键索引的情况下,是会对数据加行锁。
其实update操作在mysql内部也是一个先查后改的过程,这个过程如果是原子的,那么可以保证update语句是串行的,那我们就来看一下update语句在mysql内部的执行过程。

大家有的另一个误区是单条语句不是事务,实际上单条sql也是一个事务。
那么对于上面这个语句,一样遵循两段锁协议。
update执行的过程,会去查询满足条件的行并加锁,这个加锁是innodb做的,那么就可以保证别的事务必须等到该事务执行完了之后才能获得锁,此时拿到最新数据。

解法总结

  • 语句安全、效率最优(我的认知里)

采用设置库存而不是扣减库存

这几天我把类似的文章几乎翻了一遍,唯一看到批评我的上一条做法的是我的那个做法是不具备幂等性的。

  • 所谓幂等性就是,同一个用户对同一连接的访问不会产生副作用。比如上一条的方案,如果记录用户的操作和扣减库存不是原子操作的话,就有可能出现的问题是,库存扣减成功了,但是用户记录失败了,那么用户重复请求,就会出现多次减库存的问题。

那么他们的解法是这样的,采用设置而不是扣减,代码如下:

num_old = select available from goods where id = xx and available >= 1 ;num_new = num_old - 1 ;update goods set num=num_new where id=xx and num=num_old ;

这段代码也是安全的,采用的是乐观所的理念来完成的操作。

总结

  • 上面的做法,最后两个是相对安全的,但是你的库存字段还是要设置为无符号,关于是否幂等,要看结合请求看,不是单个扣减块代码。
  • 较真是一个学习的过程,只有较真才能把这些概念搞清楚。如果你需要完全弄懂这些内容,可能你需要对mysql锁、事务、mvcc这些概念都做一下预习。
  • 感谢工作过程中小伙伴们的努力,让我们把问题追查的更清楚。

原文地址:https://www.cnblogs.com/hitomeng/p/12369961.html

时间: 2024-10-20 02:58:17

秒杀 超卖 sql的相关文章

大型车祸现场,电商秒杀超卖,这个锅到底有谁来背?

背景 小明在一家在线购物商城工作,最近来了一个新需求,需要他负责开发一个商品秒杀模块,而且需求很紧急,老板要求必须尽快上线. 方案 小明一开始是这么做的,直接用数据库锁进行控制,获取秒杀商品数量并加锁,如果数量大于零则成功,否则秒杀失败. @Override @Transactional public Result startSeckilDBPCC_ONE(long seckillId, long userId) { //获取秒杀商品数量并加锁 String nativeSql = "SELEC

解决redis秒杀超卖的问题

我们再使用redis做秒杀程序的时候,解决超卖问题,是重中之重.以下是一个思路. 用上述思路去做的话,我们再用户点击秒杀的时候,只需要检测,kucun_count中是否能pop出数据,如果能pop出来则证明还有库存,且秒杀成功.而且pop是原子性的,即使很高的并发, 同时有很多用户访问,也是排队一个一个解决(并行转串行). 这样的话,就解决了超卖的问题.至于存入磁盘,我的上一篇文章中有介绍.有需要的朋友可以去看. 这是一个思路,具体的秒杀程序应该还有很多细节需要完善,但是核心问题已经解决了哈.

[转] 基于MySQL的秒杀核心设计(减库存部分)-防超卖与高并发

商品详情页面的静态化,varnish加速,秒杀商品库独立部署服务器这种就略过不讲了.只讨论库存部分的优化 mysql配置层面的优化可以参考我的这篇文章 <关于mysql innodb引擎性能优化的一点心得> 重点设计在数据库层面. 2张表: 第一张:判重表(buy_record),该用户有没秒杀过该商品 字段: id, uid, goods_id, addtime 第二张表:商品表 goods 字段: goods_id   goods_num 方案1: start transaction; s

秒杀的性能和超卖

一.秒杀带来了什么? 秒杀或抢购活动一般会经过[预约][抢订单][支付]这3个大环节,而其中[抢订单]这个环节是最考验业务提供方的抗压能力的. 抢订单环节一般会带来2个问题: 1.高并发 比较火热的秒杀在线人数都是10w起的,如此之高的在线人数对于网站架构从前到后都是一种考验. 2.超卖 任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难题. 二.如何解决? 首先,产品解决方案我们就不予讨论了.我们只讨论技术解决方案 1.前端 面对高并发的

关于秒杀和超卖的性能问题

一.秒杀带来了什么? 秒杀或抢购活动一般会经过[预约][抢订单][支付]这3个大环节,而其中[抢订单]这个环节是最考验业务提供方的抗压能力的. 抢订单环节一般会带来2个问题: 1.高并发 比较火热的秒杀在线人数都是10w起的,如此之高的在线人数对于网站架构从前到后都是一种考验. 2.超卖 任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难题. 二.如何解决? 首先,产品解决方案我们就不予讨论了.我们只讨论技术解决方案 1.前端 面对高并发的

如何解决秒杀的性能问题和超卖的讨论

如何解决秒杀的性能问题和超卖的讨论 最近业务试水电商,接了一个秒杀的活.之前经常看到淘宝的同行们讨论秒杀,讨论电商,这次终于轮到我们自己理论结合实际一次了. ps:进入正文前先说一点个人感受,之前看淘宝的ppt感觉都懂了,等到自己出解决方案的时候发现还是有很多想不到的地方其实都没懂,再次验证了“细节是魔鬼”的理论.并且一个人的能力有限,只有大家一起讨论才能想的更周全,更细致.好了,闲话少说,下面进入正文. 一.秒杀带来了什么? 秒杀或抢购活动一般会经过[预约][抢订单][支付]这3个大环节,而其

秒杀与超卖的 性能解决之路

一.秒杀带来了什么? 秒杀或抢购活动一般会经过[预约][抢订单][支付]这3个大环节,而其中[抢订单] 这个环节是最考验业务提供方的抗压能力的. 抢订单环节一般会带来2个问题: 1.高并发 比较火热的秒杀在线人数都是10w起的,如此之高的在线人数对于 网站架构从前到后都是一种考验. 2.超卖 任何商品都会有数量上限,如何避免成功下订单买到商品的人数不 超过商品数量的上限,这是每个抢购活动都要面临的难题. 二.如何解决? 首先,产品解决方案我们就不予讨论了.我们只讨论技术解决方案 1.前端 面对高

使用 redis 减少 秒杀库存 超卖思路

由于数据库查询的及插入的操作 耗费的实际时间要耗费比redis 要多, 导致 多人查询时库存有,但是实际插入数据库时却超卖 redis 会有效的减少相关的延时,对于并发量相对较少的 可以一用 1 public function buy($goods_id = 0){ 2 if(!$goods_id){ 3 die("商品不存在!"); 4 } 5 $redis = new Redis(); 6 $redis->connect('127.0.0.1',6379); 7 $stock

秒杀踩坑记:库存超卖

本案例发生在别人身上,觉得有学习借鉴的意义特转载过来记录一下. PM 说有一个类似于抢购的小需求,我们第一反应就想到是典型的防止库存超卖场景,于是理所因当地选用了 Redis 方案.只要保证是原子操作,即可防止库存超卖,自然想到使用 Incr/Decr 这类原子操作. 查看 PHP 的 Redis 扩展关于 Incr 方法的说明: /** * Increment the number stored at key by one. * * @param string $key * @return i