并发条件下,update 语句中直接赋值和计算的区别是什么?

学习 redis 时的一个 demo

表结构#

  • 商品信息表
    CREATE TABLE `ims_hotmallstore_goods` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(50) NOT NULL COMMENT '商品名称',
    `type_id` int(11) NOT NULL COMMENT '商品分类',
    `img` text COMMENT '商品图片',
    `money` decimal(10,2) NOT NULL COMMENT '售价',
    `money2` decimal(10,2) NOT NULL COMMENT '原价',
    `is_show` int(11) NOT NULL DEFAULT '1' COMMENT '1.上架2.下架',
    `uniacid` int(11) DEFAULT NULL COMMENT '小程序id',
    `inventory` int(11) NOT NULL COMMENT '库存',
    `details` text NOT NULL COMMENT '详情',
    `store_id` int(11) NOT NULL COMMENT '商家id',
    `sales` int(11) NOT NULL COMMENT '销量',
    `logo` varchar(100) DEFAULT NULL,
    `num` int(11) NOT NULL,
    `is_gg` int(11) NOT NULL DEFAULT '2' COMMENT '是否开启规格',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  • 订单表
    CREATE TABLE `ims_order` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `order_sn` char(32) NOT NULL,
    `user_id` int(11) NOT NULL,
    `status` int(11) NOT NULL DEFAULT '0',
    `goods_id` int(11) NOT NULL DEFAULT '0',
    `sku_id` int(11) NOT NULL DEFAULT '0',
    `number` int(11) NOT NULL,
    `price` int(10) NOT NULL COMMENT '价格:单位为分',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=7521 DEFAULT CHARSET=utf8 COMMENT='订单表';
  • 订单日志表

    CREATE TABLE `ims_order_log` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `status` int(11) NOT NULL DEFAULT '0',
    `msg` text CHARACTER SET utf8,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `status` (`status`)
    ) ENGINE=InnoDB AUTO_INCREMENT=10466 DEFAULT CHARSET=gb2312 COMMENT='订单日志表';

    demo 源码#

    代码已被折叠,点此展开

使用#

  • 初始化数据
    http://redis.test/demo/test.php?i=1

  • 使用
    http://redis.test/demo/test.php?i=2

结果#

以抢购 100 件商品为例,商品表 num=100
在函数 doPageGoodsOrder 中,如果使用如下 sql

$sql="update ims_hotmallstore_goods set num=$number where num>0 and id=".$goods['id'];

即直接更新数据,不在 sql 中运算,在使用 apache ab 测试时

ab -n 1000 -c 100 http://redis.test/demo/test.php?i=2

发现:

  • 商品表,num 字段不为 0
  • 订单表,生成 456 份订单
  • 日志表,1000 条日志,其中抢购成功的即 status=1 的是 100 条

如果在 sql 中计算,即:

$sql="update ims_hotmallstore_goods set num=num-".$goods_number." where num>0 and id=".$goods['id'];

结果正常,num=0,订单 100 条,日志 1000 条

问题#

  1. 为什会出现这种现象(猜测是锁,但不了解具体的机制,正在学习)
  2. 这个 demo,是否能算一个解决超卖的基本方法

谢谢!

《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 7

并发高的情况下,必须要用 mysql 的更新锁。
问题出在上面判断库存的时候:

  1. 假设 A 取到的是剩余 100 库存,并发时 B 取到的也是 100 库存
  2. A 自行减法算出库存为 99 并更新,这时 C 拿到的库存可能是 100,也可能是 99
  3. C 自行减一后,算出 98 时,这时候可能 B 算出来是 99 并更新到库存
  4. D 获得库存时,可能是 99 还可能是 100
    以上就会出现这种情况。

解决方案:
使用 mysql 事务处理,在获取库存之前开始处理事务并锁定表,然后结束事务,laravel 文档中有这一条代码

DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();
5年前 评论
crhhoho (楼主) 5年前
畅畅 (作者) 5年前

可以了解一下数据库的锁的机制 在对数据发生变更的时候 mysql 是自带锁的

5年前 评论

应该是插入订单成功了;而更新库存时,当库存为 0 以后的库存更新失败。
可以加上事务,当库存更新失败时,订单插入也回滚

5年前 评论
颠倒的玉石

可以用 redis 的原子操作

5年前 评论
// 抢购下单
public function doPageGoodsStore()
{
        $pdo = self::Pdo();
        $goods_id=1;
        /**诺,问题所在
            对于 redis 来说,它属于单线程的原子性操作,但是 mysql 不是。
            所以这里拿商品数据,你是通过查询数据库获取的。
            而又因为你算库存的时候,通过查询数据库获取到的库存来计算,
            在并发情况下自然会引起不正确,这里的库存数,建议也通过 redis 中存储的热数据来计算
            比如 $stock = $redis->llen('num');
        **/
        $sql="select id, num, money from ims_hotmallstore_goods where id=".$goods_id;
}

补充第二个问题,看下来是不会的,但是会出现数据的错误,最直接的就是你能看到的超库存。

当然,这个还有一些隐藏的问题。因为并发,数据库压力比较大,如果实时去 update 和 insert 的话,会出现一些单点故障,比如数据库奔溃。

我一般入库的做法是在热度过后再批量入库。

5年前 评论
crhhoho (楼主) 5年前
萧晔离

mark

5年前 评论