首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  Java

有懂秒杀的兄弟吗?为什么 mysql 这样会出现超卖的问题

  •  
  •   cyhulk · 337 天前 · 10594 次点击
    这是一个创建于 337 天前的主题,其中的信息可能已经有所发展或是发生改变。
    首先设定一个前提,为了防止超卖现象,所有减库存操作都需要进行一次减后检查,保证减完不能等于负数。(由于 MySQL 事务的特性,这种方法只能降低超卖的数量,但是不可能完全避免超卖)

    update number set x=x-1 where (x -1 ) >= 0;
    为什么这条语句会出现超卖
    第 1 条附言  ·  336 天前
    不好意思,这只是我无意间看网上的文章看到的,不信你百度,秒杀超卖,很多都这样写,所以我就很奇怪,我自己 1000 个并发去操作也没有问题,所以就问问了,还有大家纠结的锁,其实即使是 RC 隔离级别下,也会有 X 锁,也不会出现问题,这个我一会验证下,天下文章一大抄。
    84 回复  |  直到 2018-11-22 13:34:10 +08:00
        1
    lihongjie0209   337 天前
    你的事务级别是什么?
        2
    iloveyou   337 天前
    悲观锁乐观锁
        3
    WeaponXu   337 天前
    取数据的时候要锁一下
        4
    mmdsun   337 天前 via Android
    update number set x=x-1 where x >0 这样写肯定不会超卖的。RR 隔离级别
        5
    opengps   337 天前 via Android
    秒杀是个高并发的东西,不适合用数据库控制
        6
    gejun123456   337 天前 via iPhone
    你这样写肯定不会的
        7
    blue0125   337 天前 via Android
    据说用 Redis 可以
        8
    xavier007   337 天前
    队列!比如小米原来的秒杀,就是有个中间页,然后通过队列限制进入实际购买页。另外数据库更新也可以利用队列。这样限制了更新数据库的并发量
        9
    zjsxwc   337 天前 via Android
    这种 sql 明显会超卖的啊, mysql 默认的事务级别是允许幻读的, 使用队列处理秒杀是最佳实践.
        10
    ilyh   337 天前
    你这样写是不会的, mysql 默认的隔离级别是可重复读, update set 操作是当前读, 会加锁的. 和幻读没有关系
        11
    luozic   337 天前 via iPhone
    事务,你这只是 update,
        12
    heww   337 天前 via iPhone
    用 select for update 来做
        13
    o00o   337 天前 via Android
    艺高人胆大,我这都是在 Java 端用线程锁判断
        14
    limuyan44   337 天前 via Android
    mysql 默认的 rr 级别不会的,另,这个和上面说的幻读有毛关系。。
        15
    Raymon111111   337 天前
    这样写应该不会超卖啊
        16
    mayday526   337 天前
    默认级别 RR,理论上不会,那么问题来了,为什么这样还是会超卖呢
        17
    leriou   337 天前
    秒杀用 redis 分布式锁服务
        18
    insert000   337 天前
    当并发进来的时候 x-1 可能是同时的,并且 x 设置成非负
        19
    Va1n3R   337 天前   ♥ 1
    了解一下安全界的条件竞争漏洞~
        20
    cheeseyu1994   337 天前 via Android
    这样会超卖吗?等答案|ω・)
    是不是没有对 mysql 操作结果进行判断就直接写入订单了?
        21
    mayday526   337 天前
    @insert000 不可能,update 语句不是造成行级锁就是表锁,并发情况下也不可能是负数
        22
    qsbaq   337 天前
    高并发的话建议 redis 或者 memcached,频繁更新数据库会增加负载。
    https://www.jiloc.com/44364.html
        23
    insert000   337 天前
    @mayday526 我说的是 x 设置成非负,没说会成负数,x>0 就造成能行锁了。是可以的
        24
    insert000   337 天前
    用 redis SETNX 当锁拦一道,抢到锁的再操作数据库就好了
        25
    tuntunxiong   337 天前
    redis list, rpush rpop
        26
    linbiaye   337 天前
    这个写法,事务级别为 RR, RC, Serialize 都不应该会出现问题才是,是不是没检查返回值?
        27
    oovveeaarr   337 天前
    的确比较奇怪,我也差不多是这种写法不应该会负数的
        28
    alcarl   337 天前 via Android   ♥ 1
    这跟隔离级别没关系吧又不是 select,update 要先锁记录的,有别的事务锁不了就等着了,因此这条不会出现超卖,但现实中一般不会这么就一个条件就更新,一般 where 里还有别的条件需要先查查 sql 的别的地方
        29
    ppyybb   337 天前 via iPhone
    不讨论秒杀
    就这个语句,不可能超卖:
    默认是 rr 的,即便是 rc 级别,update 也会加行锁,不存在超卖。

    我怀疑你看的教程的正确性
        30
    alex321   337 天前
    redis list len
        31
    37rangers   337 天前
    你给它放到 redis list 里,先到先得每次只能出 list 一个 ,相对会好很多
        32
    jimchen9999   337 天前 via Android
    直接 redis pop 啊 要什么数据库
        33
    cubecube   336 天前
    @ppyybb 我觉得你说得对,帖子里面扯队列,redis 也是够了。就事论事,这个 mysql 语句是基本的 mysql 锁机制。行级别都保证不了一致性的话,mysql 死去算了。楼主其实没说清楚,最后是应用层超卖,还是这个 mysql 控制库存 x 小于 0 了?
        34
    loqixh   336 天前
    没有读 update 影响行数判断是否成功吧?
        35
    myhot21   336 天前 via Android
    并发下,这种 sql 是完全避免不了超卖,加事务也只是降低出现概率,最好的避免方式是改用队列。
        36
    polymerdg   336 天前
    上 redis 很難嘛?
        37
    ljzxloaf   336 天前
    你这是表锁啊,应该不会超卖,但是性能很差
        38
    wmhack   336 天前 via iPhone
    在外层代码里做 x-1 试试
        39
    ccl945   336 天前 via Android
    超卖放 redis,就 mysql 这尿性,没崩就要庆幸了
        40
    irgil   336 天前
    有人能分析一下为何会超卖吗?
        41
    cqu1980   336 天前
    这个只是保证减库存后,库存数不会变成负数,和超卖是两个概念....
        42
    CSDreamer   336 天前
    秒杀请求走队列,消费者去消费,就不出现这种情况,不能完全基于 mysql 做秒杀
        43
    juneszh   336 天前
    难道楼主没有用 ROW_COUNT()来获取 UPDATE 语句的执行结果,而是直接用 EXEC()的返回值?
        44
    cqu1980   336 天前
    估计没做 43 楼的操作~~~~~~
        45
    awanabe   336 天前 via iPhone
    事务搞到串行当然可以避免的 那双十一的订单可能要几年才能处理完吧?
        46
    springGun   336 天前
    为什么不是 where x>0
        47
    ikaros   336 天前
    放在内存里用互斥锁操作,或者先锁表
        48
    kismetX   336 天前
    如果是 innodb,并且加了事务的话,是不会出现负数的情况的,每行数据是都有一个事务版本号的,修改前 innodb 引擎是会检查事务版本号再更改的,这个就是解决前面部分说幻读的机制,而且这也是行级锁,讲道理,如果判断了返回的影响行数,不仅不会成为负数,也不会比库存多订单
        49
    tabris17   336 天前
    我猜 lz 用的 MyISAM 引擎,大家散了吧
        50
    shenhhd   336 天前
    坐等大神终结此问题~~~~
        51
    cyhulk   336 天前
    @tabris17 不是,这只是我无意间看网上的文章看到的,不信你百度,秒杀超卖,很多都这样写,所以我就很奇怪,1000 个并发去操作也没有问题,所以就问问了,还有大家纠结的锁,其实即使是 RC 隔离级别下,也会有 X 锁,也不会出现问题,这个我一会验证下。
        52
    amon   336 天前
    如果是 InnoDB,加了合适的事务的话,不会有问题吧。
    另外秒杀这种还是走队列。
        53
    linxy   336 天前
    按 LZ 的说法,搜了一下来源,原来是别人博客里的。。。。。
        54
    cyhulk   336 天前
    @amon 我知道这个,这个只是网上的文章,我也只是质疑别人的结论。
        55
    cyssxt   336 天前 via iPhone
    通过数据库锁并不是好的办法 需要在最上层杜绝 比如 200 的的秒杀量 10000 的访问量 后面的人 99800 的请求应该直接返回失败 个人觉得最好的办法是用队列去处理 前端轮回处理秒杀结果
        56
    cyssxt   336 天前 via iPhone
    @cyssxt 错了 是 9800 数学老师要活过来
        57
    tingfang   336 天前
    不可能超卖的吧?
        58
    karllynn   336 天前
    这个不会超卖啊,这跟事务也没啥关系,这是数据库的基本保证吧

    另外为啥不直接写`where x > 0`…
        59
    grandpa   336 天前
    有检查 affected rows 吗
        60
    cyhulk   336 天前
    @linxy 我就是看别人的博客出现的疑虑
        61
    cyhulk   336 天前
    @karllynn 这个没事的,可能是为了逻辑清晰,而且数据库的优化器会帮你查处理成 x > 0 的
        62
    realpg   336 天前
    事务呢……
        63
    mineqiqi   336 天前
    上 redis 分布式锁是比较好的解决方案
        64
    saltxy   336 天前
    网上的博客都是到处抄,连写错的地方都是一模一样~这条语句顶多就是高并发扛不住,库存是不会减到负数的
        65
    jzmws   336 天前
    数据库把 库存的数量设置为 unsigned 的类型 数据库做个最后的拦截
        66
    Exceptions   336 天前
    这个和事务没关系,update 不管在 innodb 下是行锁还在 myisam 下是表锁, 都不会造成负数的情况。网上博客一大抄,看看就行了,别较真, 真较真起来能气死自己....
        67
    HamQ   336 天前
    @xavier007 小米不是到预售时间直接改文字变售罄嘛 不需要秒杀这么高级的玩意的
        68
    mnhkahn   336 天前
    你们秒杀没量吗?居然数据库减
        69
    micean   336 天前
    贴一下超卖的博客看一下?
        70
    lixikei   336 天前
    秒杀这种场景,直接减数据库,总会有意想不到的惊喜。
        71
    lixikei   336 天前
    秒杀开始前,先将库存信息存到 redis、memcache 里,减库存直接内存减,订单付款结束更新数据库。
        72
    xiaoxiaoan317   336 天前
    高并发,首先要做上游拦截,然后使用内存队列,最后异步处理,mysql 就可以轻轻松松应对了
        73
    qilishasha   336 天前
    设计模式可以看一下……不要相信那些博客上愚蠢的办法(我朝当真是人才济济)…… 12306 坑那么久也是没得说了,可以多看看 12306 的解决方案,不要浪费时间在数据库这种东西的操作上,不要感谢我,我的名字叫雷锋!
        74
    weizhen199   336 天前
    大哥啊,用队列啊,数据库🔒吃不消啊
        75
    cyhulk   336 天前
    @Exceptions 是行锁没问题,但是不同隔离级别的下的锁类型可能也不一样,X 锁还是 S 锁,这个我也不是特别清楚,其实一直想找个 DBA 问下
        76
    Exceptions   336 天前
    @cyhulk 底层用到的锁肯定不止这两种,排它锁共享锁间隙锁范围锁意向锁等等,事务下每次更新数据,都会用到特定的锁去锁定,然后修改数据存到 redo 日志,旧数据存到回滚段的 undo 日志里,读取都是不加锁的快照读,直接读取 undo 日志的数据,所以 innodb 才适合高并发的互联网应用
        77
    cyhulk   336 天前
    @Exceptions 不考虑 RC 和 RR 级别,这两个都是严格的 X 锁,RR 还要加间隙锁,我唯一不确定的就是 Read uncommit 级别下的是否是 S 锁,如果是 S 锁,确实可能会存在,但是我实践上没有这个问题
        78
    wentaoliang   336 天前
    为啥上面一堆人在扯事务的隔离级别, 这个事务隔离级别有毛的关系,我觉得这和 update number set x=x-1 where x >= 1 没区别, 会把 x >= 1 的全部加上行锁,所以不会超发
        79
    he583899772   336 天前
    我以前也出现超卖了,后来我用 redis 链表实现的,活动前吧库存缓存到 redis 列表,放在下订单逻辑的前面,卖一个出一个,为空就报错了,不让创建订单,前端也搞搞几层缓解一下并发
        80
    cyhulk   336 天前   ♥ 1
    @he583899772 为啥要用列表呢?直接数字 incrby 不也可以吗?
        81
    he583899772   336 天前
    @cyhulk 都行吧,当初萌新也是摸索过河,只要反正利用 redis 原子性操作就行了,不过订单没付款,自动取消的时候还要反补 redis 活动库存吧
        82
    zerozerone   336 天前 via Android
    老哥找到答案记得帮忙艾特我一下,我也想知道
        83
    cyhulk   336 天前   ♥ 1
    @zerozerone 我自己做过 1000 个并发,不会初心超卖,那个博客说的是错的
        84
    agostop   335 天前
    1、这语句肯定是全表锁,全表遍历
    2、既然是全表加锁遍历,当然是不会出现超卖
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1966 人在线   最高记录 5043   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 28ms · UTC 16:22 · PVG 00:22 · LAX 09:22 · JFK 12:22
    ♥ Do have faith in what you're doing.