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

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

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

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

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

    比如你用 select for
    update

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

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

    当然,事务的开启和提交是否都正确?嵌套事务是否存在?
    yc8332
        28
    yc8332   53 天前
    用户不多直接数据库加锁,或者更新的时候校验数据库的库存,和你判断时的值不一样就失败
    willm
        29
    willm   53 天前 via Android
    @renmu 你一定是小米员工
    markgor
        30
    markgor   53 天前
    一个用户不太多的秒杀 還能出現-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   53 天前   ♥ 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   53 天前 via iPhone
    可以先收集请求,然后抽奖。
    比如你预计 3 秒钟秒完,就收集前 5 秒的请求,只记录不处理,然后抽奖。秒杀本来就看运气,用户又不知道你怎么实现的。
    yejianmail
        33
    yejianmail   52 天前 via Android
    @markgor 我觉得你这个方法可行,毕竟项目用户不多,不用整得太复杂
    markgor
        34
    markgor   52 天前
    @yejianmail 你可以做個頁面,一有請求就減庫存,按照我上面那個方法,然後 ab 測試一下,看看會不會出現超售。
    反正超售是肯定可以保證,但是由於執行流程是一個個執行,所以後面的並發會卡在那,此時前端 ajax 設置個超時重試參數。
    另外流量真的大,你直接後台按百分比來放行,比方隨機 1~100,只要是 50 以上的放行進行搶購,50 以下的直接返回失敗。
    markgor
        35
    markgor   52 天前
    你可以參考下:
    前端靜態,丟 CDN。
    兩個前端頁面,一個是發無效請求去百度,一個是發有效請求到 java
    然後有效請求的那個頁面,發送 ajax 去 java 第一輪按百分比返回結果,
    50%以上執行上面搶購邏輯。50%的就直接返回失敗。

    當然,這是很低莊的方法,但一定程度內有效分流,也能保證到超售情況。
    yejianmail
        36
    yejianmail   52 天前 via Android
    @markgor 谢谢大佬,我测试下,ab 没用过,我用 jmeter 试试
    crclz
        37
    crclz   49 天前
    库存为 -1,应该是没有在库存那条记录上面加锁,而不是幻读。
    另外 ReadCommited 足够。

    如果数据高争用,那么就应该用 redis。但是 redis 是内存数据库,Durability 没法保证,所以就 2 台以上 redis 吧。redis 写个 lua 脚本,最小化往返次数。
    (没实际用过 redis,只是提供一个思路)
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   3609 人在线   最高记录 5168   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 28ms · UTC 10:05 · PVG 18:05 · LAX 02:05 · JFK 05:05
    ♥ Do have faith in what you're doing.