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

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

  •  
  •   yejianmail · 2019-11-27 22:45:42 +08:00 · 4893 次点击
    这是一个创建于 1583 天前的主题,其中的信息可能已经有所发展或是发生改变。
    需要 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   ·   我们的愿景   ·   实用小工具   ·   5288 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 01:27 · PVG 09:27 · LAX 18:27 · JFK 21:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.