关于秒杀一般是如何保证库存操作的原子性的 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
yejianmail
V2EX    程序员

关于秒杀一般是如何保证库存操作的原子性的

  •  
  •   yejianmail 2019-11-27 22:45:42 +08:00 5652 次点击
    这是一个创建于 2152 天前的主题,其中的信息可能已经有所发展或是发生改变。
    需要 java 做一个用户不太多的秒杀,springboot 加 mariadb,涉及库存操作的地方已加锁同步,事务隔离级别为 read-commit,偶尔出现剩余库存为-1,初步认为是数据库出现了幻读,这样的抢单一般是用什么技术实现的,如果并发不高,是不是也必须上 redis,有大佬指导下么?
    37 条回复    2019-12-01 19:13:50 +08:00
    renmu
        1
    renmu  
       2019-11-27 22:50:50 +08:00 via Android
    一律返回失败,几秒后返回假数据到前端(狗头保命)
    lhx2008
        2
    lhx2008  
       2019-11-27 22:53:24 +08:00 via Android   1
    读已提交肯定出问题啊,因为他其实是基于 MVCC 的,比如说现在两个事务都看到只有一个库存,他就直接都做减库存操作了。不过你说了加锁,可能加的位置不对吧。
    MeteorCat
        3
    MeteorCat  
       2019-11-27 22:53:44 +08:00 via Android   1
    不要假设并发不高,我今年也是假设并发不高,哪知道不知道哪里天杀直接爆破公司项目接口
    lhx2008
        4
    lhx2008  
       2019-11-27 22:55:31 +08:00 via Android   1
    最简单的方法肯定是 redis 做一个 lua 递减,或者一个分布式锁,程序内的锁,多副本运行就死了
    lhx2008
        5
    lhx2008  
       2019-11-27 22:56:46 +08:00 via Android
    如果是纯 mysql 的话,也可以用版本乐观锁的,但是读已提交还是不一定能生效。
    shoaly
        6
    shoaly  
       2019-11-27 22:58:48 +08:00   2
    跟客户关系好的话...让她多准备几份商品.
    yejianmail
        7
    yejianmail  
    OP
       2019-11-27 23:07:47 +08:00 via Android
    @lhx2008 涉及两个方法一个是加减,一个是查看剩余库存,锁是同一个对象,事务应该是默认的 require
    yejianmail
        8
    yejianmail  
    OP
       2019-11-27 23:08:30 +08:00 via Android
    @renmu 最后一结算交易额为 0
    yejianmail
        9
    yejianmail  
    OP
       2019-11-27 23:11:54 +08:00 via Android
    @lhx2008 我看到网上的一些实现就是用的一个 guava 的工具类来限流,然后用 redis 的递增或者递减来保证库存操作原子性,没看明白退还库存为什么一定要用 lua 脚本
    des
        10
    des  
       2019-11-27 23:29:22 +08:00 via Android
    这种不是有很多讨论的么?
    提前把数据在库里生成好也行,每一个商品算一条记录,删除成功进行后续操作
    用 redis 也行,不过还是建议用 redis

    这种东西不适合直接上锁
    jeffh
        11
    jeffh  
       2019-11-27 23:30:40 +08:00   1
    mariadb 默认不是不重复读级别吗?更新库存的时候可以 update tab set value=value-1 where id=? and value>0;这相当于变相的乐观锁了吧。根据 sql 返回值可以知道是否 sql 执行成功
    hhx
        12
    hhx  
       2019-11-27 23:36:15 +08:00 via Android   1
    秒杀系统设计应该涵盖两个要点,即限流和同步。限流可以采用 controller 层 CAS 结合分布式锁例如 Redis 或 Zookeeper。同步可以采用 service 层锁或 MySQL 乐观锁。你提到了数据库的事务,你确定只将逻辑写入事务就能保证系统的正确性吗?
    yejianmail
        13
    yejianmail  
    OP
       2019-11-27 23:38:16 +08:00 via Android
    @jeffh 默认是可重复读级别,但是要开 binlog 才支持
    mrdemonson
        14
    mrdemonson  
       2019-11-27 23:38:26 +08:00 via Android   2
    一直觉得奇怪,秒杀应该是锁内存数据吧,直接操作内存好了,为啥都要去搞数据库,和锁数据库
    yejianmail
        15
    yejianmail  
    OP
       2019-11-27 23:39:31 +08:00 via Android
    @jeffh 根据 sql 返回值这是个好办法,类似于 ignore into 看插入成功没
    yejianmail
        16
    yejianmail  
    OP
       2019-11-27 23:40:56 +08:00 via Android
    @mrdemonson 最终数据库需要和内存同步吧,这样才有办法结算
    mxT52CRuqR6o5
        17
    mxT52CRuqR6o5  
       2019-11-27 23:44:23 +08:00 via Android
    据说淘宝的双十一秒杀是会超售的,不知道是真是假
    yejianmail
        18
    yejianmail  
    OP
       2019-11-27 23:46:10 +08:00 via Android
    @hhx 如果读取的数据没有脏读和幻读,可以保证业务的正确性
    yejianmail
        19
    yejianmail  
    OP
       2019-11-27 23:47:10 +08:00 via Android
    @mxT52CRuqR6o5 真的么,那我这就不算 bug 了呀,手动滑稽.jpg
    petelin
        20
    petelin  
       2019-11-27 23:50:41 +08:00 via iPhone
    为啥楼上的都不考虑可靠性和稳定性 内存数据库万一挂了呢?实时同步不就退化成...了吗

    我觉得限流加锁完全没问题

    比如你用 select for
    update

    一个人一个人的弄 怎么会有问题
    jeffh
        21
    jeffh  
       2019-11-27 23:57:09 +08:00   1
    我记得在网上看过,淘宝的秒杀是异步的,先在内存中设置一个总量 v,秒杀到的显示排队中 mq 削峰异步处理,同时 v-1,如果 v 小于 0 了,直接返回秒杀结束。
    hhx
        22
    hhx  
       2019-11-28 00:04:43 +08:00 via Android
    @yejianmail 我想知道你是怎么用的
    h123123h
        23
    h123123h  
       2019-11-28 00:07:34 +08:00
    对速度不敏感的话事务+乐观锁就可以了吧
    ljpCN
        24
    ljpCN  
       2019-11-28 00:51:42 +08:00
    消费者队列
    wangyzj
        25
    wangyzj  
       2019-11-28 00:58:29 +08:00
    select for update
    redis nx
    queue 同时保存状态+轮询获取新状态
    imcj
        26
    imcj  
       2019-11-28 00:59:48 +08:00
    如果没有范围读取,read commit 足够,要么设置为 repeatable-read,要么修改代码,避免范围读取。

    当然,事务的开启和提交是否都正确?嵌套是否是否存在?
    imcj
        27
    imcj  
       2019-11-28 01:00:07 +08:00
    如果没有范围读取,read commit 足够,要么设置为 repeatable-read,要么修改代码,避免范围读取。

    当然,事务的开启和提交是否都正确?嵌套事务是否存在?
    yc8332
        28
    yc8332  
       2019-11-28 08:09:16 +08:00
    用户不多直接数据库加锁,或者更新的时候校验数据库的库存,和你判断时的值不一样就失败
    willm
        29
    willm  
       2019-11-28 09:15:02 +08:00 via Android
    @renmu 你一定是小米员工
    markgor
        30
    markgor  
       2019-11-28 12:14:48 +08:00
    一个用户不太多的秒杀 能出-1 的情....


    我通常偷的做法

    start transaction;
    select 1 from item where id = 123 and less > 1;就返回失。
    update item set less = less -1 where less > 1 and id = 123;影=0 返回失
    commit;

    未出超售。了 item 的 less 是不允的。

    另外也 redis ,
    把品加去 redis,
    然後成功 pop 出再去 mysql 扣。
    markgor
        31
    markgor  
       2019-11-28 12:15:42 +08:00   1
    @markgor #30 select 1 from item where id = 123 and less > 0;就返回失
    update item set less = less -1 where less > 0 and id = 123;影=0 返回失

    的,上面了,更改回。
    shenyuzhi
        32
    shenyuzhi  
       2019-11-28 14:45:17 +08:00 via iPhone
    可以先收集请求,然后抽奖。
    比如你预计 3 秒钟秒完,就收集前 5 秒的请求,只记录不处理,然后抽奖。秒杀本来就看运气,用户又不知道你怎么实现的。
    yejianmail
        33
    yejianmail  
    OP
       2019-11-28 18:22:35 +08:00 via Android
    @markgor 我觉得你这个方法可行,毕竟项目用户不多,不用整得太复杂
    markgor
        34
    markgor  
       2019-11-28 18:36:22 +08:00
    @yejianmail 你可以做面,一有求就存,按照我上面那方法,然後 ab 一下,看看不出超售。
    反正超售是肯定可以保,但是由於行流程是一行,所以後面的卡在那,此前端 ajax 置超重。
    另外流量真的大,你直接後台按百分比放行,比方 1~100,只要是 50 以上的放行行,50 以下的直接返回失。
    markgor
        35
    markgor  
       2019-11-28 18:40:41 +08:00
    你可以考下:
    前端, CDN。
    前端面,一是效求去百度,一是有效求到 java
    然後有效求的那面,送 ajax 去 java 第一按百分比返回果,
    50%以上行上面。50%的就直接返回失。

    然,是很低的方法,但一定程度有效分流,也能保到超售情。
    yejianmail
        36
    yejianmail  
    OP
       2019-11-28 20:23:52 +08:00 via Android
    @markgor 谢谢大佬,我测试下,ab 没用过,我用 jmeter 试试
    crclz
        37
    crclz  
       2019-12-01 19:13:50 +08:00
    库存为 -1,应该是没有在库存那条记录上面加锁,而不是幻读。
    另外 ReadCommited 足够。

    如果数据高争用,那么就应该用 redis。但是 redis 是内存数据库,Durability 没法保证,所以就 2 台以上 redis 吧。redis 写个 lua 脚本,最小化往返次数。
    (没实际用过 redis,只是提供一个思路)
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2584 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 02:24 PVG 10:24 LAX 19:24 JFK 22:24
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86