秒杀系统 | 交易性能优化 | 库存操作流水 | 最终一致性 | RocketMQ 事务型消息反查接口的依据

数据类型

主业务数据(Master Data)
  • 比如 ItemModel,记录了商品的主数据;
  • 比如 item_stock 记录了商品库存的主数据;
操作型数据(Log Data)
  • 比如库存扣减这样的操作发生了,需要把操作本身的过程记录下来,用于支持这种记录的数据,就是操作型数据;
  • 记录下操作型数据是为了追踪,比如库存流水的操作状态,可以根据这个状态做回滚,或者查询正在处理中的状态,使得很多异步的动作,比如下单之前,先落一条待扣减的库存操作的流水,成功之后,再把流水的状态设置为成功,或者下单失败时候,将对应的库存操作的流水设置为失败,可以根据这个操作流水的状态反查,得到异步动作进行的当前状态;

库存操作流水模型构建

创建库存流水表 stock_log
  • 顺便生成 mybatis-generator 的一套;
CREATE TABLE `stock_log` (
  `stock_log_id` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
  `item_id` int NOT NULL DEFAULT '0',
  `amount` int NOT NULL DEFAULT '0',
  `status` int NOT NULL COMMENT '1 表示初始状态, 2 表示下单减库存成功, 3 表示下单回滚',
  PRIMARY KEY (`stock_log_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
下单前生成库存流水
  • 用户的下单请求到来时,先生成一个库存流水记录,库存流水的 status 设置成 1;
  • 下单的时候,把库存流水号带上;
@RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId,
                                    @RequestParam(name = "amount") Integer amount,
                                    @RequestParam(name = "promoId", required = false) Integer promoId) throws BusinessException {
    String token = httpServletRequest.getParameterMap().get("token")[0];
    if (StringUtils.isEmpty(token)) {
        throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
    }
    UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
    if (userModel == null) {
        throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
    }

    String stockLogId = itemService.initStockLog(itemId, amount);
    if(!mqProducer.transactionAsyncReduceStock(userModel.getId(), promoId, itemId, amount, stockLogId)) {
        throw new BusinessException(EmBusinessError.UNKNOWN_ERROR, "下单失败");
    }
    return CommonReturnType.create(null);
}
下单成功后,修改库存流水的 status 为 2;
  • 整个下单逻辑要经历的步骤有:1. 查询验证,2. 减库存,3. 下单,4. 增销量,5. 设置库存流水状态;
// 5. 设置库存流水状态为成功
StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
if (stockLogDO == null) {
    throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
}
stockLogDO.setStatus(2);
stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
本地事务(下单)失败,要设置库存流水的 status 为 3;
  • 设置完了让 Broker 中的 Prepared 状态的消息回滚;
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object args) {
    // 真正要执行的操作:创建订单
    Integer userId = (Integer)((Map) args).get("userId");
    Integer itemId = (Integer)((Map) args).get("itemId");
    Integer promoId = (Integer)((Map) args).get("promoId");
    Integer amount = (Integer)((Map) args).get("amount");
    String stockLogId = (String)((Map) args).get("stockLogId");
    try {
        OrderModel orderModel = orderService.createOrder(userId, itemId, promoId, amount, stockLogId);
    } catch (BusinessException e) {
        e.printStackTrace();
        // 设置对应的 stockLog 为回滚状态
        StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
        stockLogDO.setStatus(3);
        stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
        return LocalTransactionState.ROLLBACK_MESSAGE;
    }
    return LocalTransactionState.COMMIT_MESSAGE;
}
Broker 长时间没收到 Commit 提交要走反查逻辑
  • 反查的依据就是库存操作流水号;
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    // 根据是否扣减库存成功,来判断要返回 COMMIT, ROLLBACK, 还是继续 UNKNOW
    String jsonString = new String(msg.getBody());
    Map<String, Object> map = JSON.parseObject(jsonString, Map.class);
    String stockLogId = (String) map.get("stockLogId");
    StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
    if (stockLogDO == null) {
        return LocalTransactionState.UNKNOW;
    }
    if (stockLogDO.getStatus().intValue() == 2) {
        return LocalTransactionState.COMMIT_MESSAGE;
    } else if (stockLogDO.getStatus().intValue() == 1) {
        return LocalTransactionState.UNKNOW;
    }
    return LocalTransactionState.ROLLBACK_MESSAGE;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351