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

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

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

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

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

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