V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
MySQL 5.5 Community Server
MySQL 5.6 Community Server
Percona Configuration Wizard
XtraBackup 搭建主从复制
Great Sites on MySQL
Percona
MySQL Performance Blog
Severalnines
推荐管理工具
Sequel Pro
phpMyAdmin
推荐书目
MySQL Cookbook
MySQL 相关项目
MariaDB
Drizzle
参考文档
http://mysql-python.sourceforge.net/MySQLdb.html
sheldondai
V2EX  ›  MySQL

mysql update 库存问题

  •  2
     
  •   sheldondai · 2016-02-01 22:33:51 +08:00 · 7007 次点击
    这是一个创建于 3199 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Mysql 5.6
    最近在做一些类似秒杀的业务场景,遇到了库存问题,按我之前的做法我们都没有直接使用 mysql 的update -1操作,因为担心超卖,但是一位同事说只要把需要 update 的字段设为无符号型就可以解决超卖的问题,我试了下,确实如此。但这里的疑惑是网上所以的文章都是 mysql 在高并发 update -1 的情况下会超卖。
    比如这篇 http://www.cnblogs.com/billyxp/p/3701124.html 里面写到

    II : 其次,超卖的根结在于减库存操作是一个事务操作,需要先 select ,然后 insert ,最后 update -1 。最后这个-1 操作是不能出现负数的,但是当多用户在有库存的情况下并发操作,出现负数这是无法避.免的。

    25 条回复    2016-10-22 17:22:54 +08:00
    Sunyanzi
        1
    Sunyanzi  
       2016-02-01 22:40:11 +08:00
    一个提示 ... Redis 的所有命令都是原子性的 ... 其中包括 INCRBY ...

    今天净碰到商城相关的问题是为什么 ...
    yangqi
        2
    yangqi  
       2016-02-01 22:45:12 +08:00
    你给的文章也说明白了, mysql 就是一个数据库, 高并发的问题解决不了,也不应该让 mysql 来解决,只能把逻辑提出来处理。
    sheldondai
        3
    sheldondai  
    OP
       2016-02-01 22:49:17 +08:00
    @yangqi 现在的问题是同事问我说他只要把字段设为 unsigned int 就不会出现库存为负情况,我测试了下确实如此,没有出现超卖
    yangqi
        4
    yangqi  
       2016-02-01 22:56:48 +08:00
    @sheldondai 你这个只能保证库存不为负。但是如果库存为 1 的时候,多个人同时下单成功的情况你怎么处理?
    sheldondai
        5
    sheldondai  
    OP
       2016-02-01 23:01:38 +08:00
    @yangqi 测试结果是只有一个可以 update 成功
    realpg
        6
    realpg  
       2016-02-01 23:03:08 +08:00   ❤️ 1
    如果纯在 mysql 的场景下操作,不用内存 key-value 系统,我更倾向于用另外一种模型处理这种竞争抢购的逻辑。

    “需要先 select ,然后 insert ,最后 update -1 。最后这个-1 操作是不能出现负数的”

    我可以分享一下我的思路,以及我的做法。

    首先,楼主以及这个文章都提到超售的问题,如果你的系统设计上能出现超售,那说明你的逻辑太差。就这种 select insert update-1 的模型,在抢购的瞬间是怎么操作的呢?我们分析一下:

    首先 用户 submit_order, 先去 select 库存,判断有,然后 insert 一个 order 表数据,然后 update 库存减 1

    在 MYSQL 并发非常高的瞬间,这三部操作,都可能出现大坑,只要三步中一个操作被锁坑了,整体就坑了。

    然后我们再抽象:假设抢购 50000 件商品

    抢购前,库存表一条记录 值 50000 ,订单表 0 条记录
    抢购后,订单表 50000 条记录,库存表 1 条记录,值 0

    在抢购过程中(假设非常火, 5 秒钟抢完)
    中间 select 次数远大于 50000 (抢购失败的也得查询库存), update50000 次库存表-1 , insert50000 个订单表 这么多查询吧。这三步是一个顺序逻辑,任何一步出问题整个操作都失败。

    我的抢购系统,绝对不会设计的!

    我有 50000 件商品,我在抢购开始前一个小时到抢购前 5 分钟, 55 分钟的时间里,写入订单表 50000 条记录。其中 orders 表的 uid 做 unique 约束。 UID 使用-1~-50000 预先填写好
    我有 55 分钟时间插入 50000 条记录,不用 5 秒钟, MYSQL 无负载无压力

    开始抢购,所有人就开放一个接口,拼命地往数据库里提交一个查询
    update ignore `orders` set uid = '你的 UID' where uid<0 and item=3 limit 1
    不用等执行结果 完全非阻塞的只接受一个指令 就行了

    整个抢购过程,就是一群人发一个查询往里去,等管理端发现 50000 个记录确认都有主了,抢购结束标志设置好,关闭抢购接口,完成抢购。大家可以访问我的订单看抢购结果了。
    sheldondai
        7
    sheldondai  
    OP
       2016-02-01 23:10:14 +08:00
    @realpg 兄弟这个做法和我们之前的做法类似,确实可以保证不会超发。不过今天跑到 V2EX 上主要是想解决我同学问我的问题,在 mysql 性能够用的情况下,对一个无符号的 int 进行 updte-1 也可以保证不超发
    moro
        8
    moro  
       2016-02-01 23:15:21 +08:00
    begin
    select .... for update;
    update .... where ....;
    commit;
    yangqi
        9
    yangqi  
       2016-02-01 23:15:50 +08:00
    @sheldondai 你库存和订单存在一个表里?你得先说清楚你的操作流程,不然不好判断
    realpg
        10
    realpg  
       2016-02-01 23:16:52 +08:00
    @sheldondai
    不只是超售的问题,超售属于算法 0 分不及格。
    性能差,抗压能力差两个量级。
    skydiver
        11
    skydiver  
       2016-02-01 23:30:15 +08:00
    不能小于 0 ,所以改成无符号整数,然后库存变成了 4294967295 ……
    sheldondai
        12
    sheldondai  
    OP
       2016-02-01 23:30:42 +08:00
    @yangqi 是这样的,我们不是专门做电商公司。是现在有个小活动让用户抽奖,有个同事的方法是这样,有个表保存库存量
    ```
    1. 有用户请求,检查库存是否大于 0
    2.大于 0 的话,则 update-1 ,如果 update 成功就算抽奖成功
    ```
    当然中间计算概率的在这省略了。


    @realpg 感谢,我们现在并发更高的项目里用的是另外两种方法,一种和你们的类似,另一种是使用了 redis 。效果也都不错。今天主要是想问下 update-1 的问题
    sheldondai
        13
    sheldondai  
    OP
       2016-02-01 23:31:13 +08:00
    @skydiver 我们测试不会出现这种情况, mysql5.6
    wy315700
        14
    wy315700  
       2016-02-01 23:34:44 +08:00
    @sheldondai
    提前把每个货物分开
    使用事务,有请求的时候,先把库存表里锁住一个货物,然后处理完改状态,
    yangqi
        15
    yangqi  
       2016-02-01 23:44:11 +08:00
    @sheldondai 这样自然是可行的,不过有个前提就是 mysql 的 SQL_MODE 不能为'NO_UNSIGNED_SUBTRACTION', 否则 update 不会出错,只会返回 warning.

    你这个和秒杀类的不一样,要简单多了
    sheldondai
        16
    sheldondai  
    OP
       2016-02-01 23:58:37 +08:00
    @yangqi 我们用的阿里云的 RDS ,刚查了下是空。但执行 sql 确实报错了
    BIGINT UNSIGNED value is out of range in '(`yao_admin`.`article`.`status` - 1)'。

    这个设置之前不知道,学习了。那网上很多文章里说的库存减为负的是怎么出现的呢
    yangqi
        17
    yangqi  
       2016-02-02 00:10:59 +08:00
    @sheldondai 那些自然是字段没有设为 unsigned ,因为这样没用,而且也不是最优方案。因为 update 失败后返回错误,你还要检查错误是否是 out of range 。如果只看是否成功,出了其他问题没有 update 成功都算作库存为 0 的话问题就来了。
    realpg
        18
    realpg  
       2016-02-02 02:07:58 +08:00
    @sheldondai
    现在都是依托内存 kv 引擎做这种东西了。性能极强,抗压能力提升三个量级以上。

    我很早就搞过这种东西的算法,各种避免 select 再 update 的模型,最开始的理由极其朴素
    你有没有发现我提到的这种算法可以不依赖支持事务的引擎?

    最开始就是在高并发的前提下, MyISAM 引擎上跑类似这种逻辑,才提出的这种方案……
    neoblackcap
        19
    neoblackcap  
       2016-02-02 02:45:19 +08:00
    @realpg 你这用法感觉跟游戏公司一个用户一个表有异曲同工之妙啊。
    br00k
        20
    br00k  
       2016-02-02 09:04:48 +08:00 via Android
    @realpg 😂😂😂求解决淘宝超售问题。
    ugg
        21
    ugg  
       2016-02-02 11:27:50 +08:00
    redis INCRBY + mysql update - 1 where > 0
    skydiver
        22
    skydiver  
       2016-02-02 13:03:12 +08:00
    @sheldondai 重点不是有没有这种情况,重点是解决问题的思路太奇葩……
    brucefeng
        23
    brucefeng  
       2016-02-02 13:21:03 +08:00
    秒杀系统的高峰 QPS 是多少,期望解决问题的成本有多高(比如开发量多大能接受)。

    如果没有这两个限制条件,讨论下去没有最优解,因为很多 NB 的技术太复杂,实现成本比较高。
    zacard
        24
    zacard  
       2016-02-02 16:37:38 +08:00
    先经过 redis 原子操作下
    xiaoyong
        25
    xiaoyong  
       2016-10-22 17:22:54 +08:00
    这个问题与我正要解决的问题相同,正准备试试 @realpg 的方法。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5532 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 08:32 · PVG 16:32 · LAX 00:32 · JFK 03:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.