如何设计高并发下的可靠库存扣减?(Redis+MySQL)

如何实现可靠的高并发扣减(Redis+MySQL)

电商高并发场景库存扣减是至关重要的,要保证响应快、不超卖,不少卖,利用Redis或者MySQL两者都能实现业务需求。但均存在一些缺陷:
一、数据库方案性能较差;
二、极端情况下会存在缓存里的数据无法回滚,导致出现少卖的情况。如果使用异步写库,也可能发生异步写库失败,导致多扣的数据再也无法找回的情况。
因此我们使用Redis缓存+MySQL的方式解决潜在问题。Go!
先建表一个库存表

CREATE TABLE `storage` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `product_id` int(11) unsigned DEFAULT '0',
  `storage_num` int(11) unsigned DEFAULT '0' COMMENT '库存数',
  `sale_price` decimal(10,2) DEFAULT '0.00' COMMENT '销售价',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存表'

在建立一个库存扣减任务表(后面会讲用来做什么)

CREATE TABLE `storage_task` (
  `id` bigint(20) unsigned NOT NULL,
  `task_id` bigint(20) unsigned DEFAULT '0' COMMENT '生成的唯一任务ID,比如使用订单号',
  `sku_id` bigint(20) unsigned DEFAULT '0' COMMENT 'sku_id',
  `storage` int(8) unsigned DEFAULT '0' COMMENT '扣减库存数',
  `state` tinyint(4) unsigned DEFAULT '0' COMMENT '同步状态:0 未同步, 1已同步',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='同步扣减库存任务表'

先上代码

/**
     * 库存扣减Demo
     *
     * @param Request $request
     * @return array
     */
    public function setStorage(Request $request)
    {
        $skuId = $request->input('skuId', 100039);
        $storageNum = $request->input('storageNum', 1);
        try {
            // 开启事务
            DB::beginTransaction();
            // 插入库存扣减任务
            $id = StorageTask::insertGetId([
                'skuId' => $skuId,
                'storageNum' => $storageNum,
            ]);

            // 插入成功后进行缓存扣减
            if ($id > 0) {
                /**
                 * 缓存扣减
                 */
                // 查看是否满足扣减要求
                $getRedisStorage = Redis::get('product:goods:storage:id:' . $skuId);
                if ($getRedisStorage < $storageNum) {
                    //缓存的库存数不够扣减,回滚
                    DB::rollBack();
                }
                // 满足要求,减少缓存中的库存数
                $setRedisStorage = Redis::incrby('product:goods:storage:id:' . $skuId, $storageNum);
                if (!$setRedisStorage) {
                    //扣减失败,回滚
                    DB::rollBack();
                }
                // 扣减成功 , 事务提交
                DB::commit();
                return response()->json(['code' => '200', 'msg' => 'Success', "data" => ''])->setCallback();
            } else {
                DB::rollBack();
                return response()->json(['code' => '501', 'msg' => 'Fail', "data" => ''])->setCallback();
            }
        } catch (\Exception $exception) {
            DB::rollBack();
            return response()->json(['code' => '501', 'msg' => 'Fail', "data" => ''])->setCallback();
        }
    }

扣减业务流程:
1、校验数据
2、开启事务
3、将此次扣减的skuID和对用的扣减数存入扣减任务表(这样异步处理,减少Update数据带来的损耗)
4、插入成功扣减明细到扣减任务表后,开始扣减缓存中的库存
5、扣减缓存中的库存可能会有失败的情况
5.1、扣减数量不够,比如缓存中有20个,本次扣减要21个,此时回滚
5.2、缓存出现故障,导致扣减失败。比如网络不通、调用缓存扣减超时、在扣减到一半时缓存突然宕机了,以及在上述返回的过程中产生异常等。针对上述请求,都有相应的异常抛出,根据异常进行数据库回滚即可,最终保证任务库里的数据都是准确无误的
6、事务提交
7、结束(Worker数据同步到正式库存表中保持一致)
此设计模式具备了:
一、数据库顺序写入要比更新性能快,真正落地到正式库存表可以异步同步
二、利用了数据库的事务特性来保证数据的最终一致性
三、利用上一节对监听的库存操作数据OBServer,对库存数进行同步缓存(不记得的同学可以翻上一章节)


最后

缺点:数据库事务中操作Redis,违反了在事务里操作网络、RPC等原则。
优点:对于类似额度扣减、实物库存扣减等场景,此方案均适用。此外,“顺序追加写要比随机修改的性能好”这个技巧,其实在很多场景里都有应用,是一个值得你深入学习和理解的技能。Elasticsearch 里的 Translog 都是先将数据按非结构化的方式顺序写入日志文件里,再进行正常的变更。当出现宕机后,采用日志进行数据恢复。
补充
目前的设计不满足Redis分布式缓存中的解决方案,无法保证高并发下的原子性,对异常的处理也有可提高的方案,下一篇准备写分布式缓存中依靠分布式锁的秒杀场景并且保证原子性,技术栈PHP+Redis+Lua,感兴趣的朋友加下群(还没有公众号)共同交流技术,一起进步。有什么问题也可以留言交流。谢谢观看。
最后祝大家周末愉快啊~

Elkan的小破站
请先登录后发表评论
  • latest comments
  • 总共0条评论