基于spring cloud实现订单服务框架demo(二)

在上一篇中阐述了基于zuul实现网关进行权限校验的技术点,本篇主要阐述下单服务的相关逻辑:

基于seata实现分布式订单服务
下单服务的业务逻辑包括两个部分: 扣减库存、生成订单,其中扣减库存调用库存服务的接口,生成订单调用订单服务的接口,这2个接口的调用需要保证原子性,即库存扣减了订单必须要生成;如果订单生成失败则扣减的库存需要重新加上。另外订单生成之后有一个待付款的时间,一般为30分钟,如果超时未支付,则关闭该订单且将库存重新加上,整体的业务时序图如下:
在这里插入图片描述
这里实现的难点在于如何保证2个接口调用的原子性,即扣减库存和创建订单必须同时成功同时失败,如果库存扣减失败则需要将创建的订单删除;如果创建订单失败则需要将扣减的库存恢复.

seata服务依赖eureka,需要单独部署启动,用户服务模块、库存服务模块、订单服务模块需要配置seata:

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: seata-demo
  service:
    vgroup-mapping:
      seata-demo: seata-server
  config:
    type: file
    file:
      name: file.conf
  registry:
    type: eureka
    eureka:
      application: eureka
      service-url: http://localhost:8001/eureka
      weight: 1

用户服务下单的业务逻辑如下(加上@GlobalTransactional注解开启seata事务):

    @RequestMapping("/placeOrder")
    @Transactional
    @GlobalTransactional(timeoutMills = 60000 * 2)
    @ResponseBody
    public OrderResponse placeOrder(@RequestParam("commodityId") String commodityId,
                                    @RequestParam("count") Integer count) throws BusinessException {
        String orderId = OrderUtil.createOrderId(commodityId);
        DeductWarehouseResponse deduct = warehouseService.deductWareHouse(commodityId, orderId, count);
        if (null != deduct.getErrMsg()) {
            throw new BusinessException(deduct.getErrMsg());
        }
        return orderService.createOrder(commodityId, orderId, count);
    }

库存服务deductWarehouse接口的定义如下:

    @TwoPhaseBusinessAction(name = "deductWarehouse", commitMethod = "commitDeduct", rollbackMethod = "cancelDeduct")
    DeductWarehouseResponse deductWarehouse(
            @BusinessActionContextParameter(paramName = "commodityId") String commodityId,
            @BusinessActionContextParameter(paramName = "orderId") String orderId,
            @BusinessActionContextParameter(paramName = "count") Integer count) throws BusinessException;

    boolean commitDeduct(BusinessActionContext context);

    boolean cancelDeduct(BusinessActionContext context);

在@TwoPhaseBusinessAction注解中定义commit方法和rollback方法,即原方法中做尝试扣减库存的操作:
deductWarehouse完成预扣减库存操作,通过以下脚本实现,retailer-warehouse是商品的库存信息表,扣减库存之后,将待扣减的数量存入到fronzen-warehouse表中暂时冻结

local record_id = KEYS[1]
local record_num = KEYS[2]
local order_id = KEYS[3]

if (redis.call('HEXISTS', 'retailer-warehouse', record_id) == 0) then
    return '\"ERROR_WAREHOUSE_NOTEXIST\"'
end

local total_num = redis.call('HGET', 'retailer-warehouse', record_id)
if (tonumber(total_num) < tonumber(record_num)) then
    return '\"ERROR_NOT_ENOUGH_WAREHOUSE\"'
end

redis.call('HSET', 'retailer-warehouse', record_id, total_num - record_num)
redis.call('HSET', 'fronzen-warehouse', order_id, record_num)
return '\"OK\"'

commitDeduct完成确认操作,清除fronzen-warehouse表中冻结的库存

cancelDeduct完成库存恢复的操作,通过以下lua脚本实现,根据订单id在fronzen-warehouse表中查找冻结的数量,将该数量重新加回到库存表中

local record_id = KEYS[1]
local order_id = KEYS[2]

if (redis.call('HEXISTS', 'retailer-warehouse', record_id) == 0) then
return '\"ERROR_WAREHOUSE_NOTEXIST\"'
end

local total_num = redis.call('HGET', 'retailer-warehouse', record_id)
if (redis.call('HEXISTS', 'fronzen-warehouse', order_id) == 0) then
return '\"ERROR_WAREHOUSE_NOTEXIST\"'
end

local fronzen_num = redis.call('HGET', 'fronzen-warehouse', order_id)
redis.call('HSET', 'retailer-warehouse', record_id, total_num + fronzen_num)
redis.call('HDEL', 'fronzen-warehouse', order_id)
return '\"OK\"'

订单服务createOrder接口定义如下:

    @TwoPhaseBusinessAction(name = "createOrder", commitMethod = "commitOrder", rollbackMethod = "cancelOrder")
    OrderMessage createOrder(@BusinessActionContextParameter(paramName = "orderId") String orderId,
                             @BusinessActionContextParameter(paramName = "userId") Long userId,
                             @BusinessActionContextParameter(paramName = "commodityId") String commodityId,
                             @BusinessActionContextParameter(paramName = "count") Integer count);

    boolean commitOrder(BusinessActionContext context);

    boolean cancelOrder(BusinessActionContext context);

createOrder方法完成订单的预创建,即此时创建的订单对外不可见:
commitOrder方法修改订单状态为待支付
cancelOrder方法删除订单
考虑到下单高并发情况下写数据库的压力,这里将待支付的订单都写入到redis中,等订单支付完成之后再同步写入到mysql中。

下单完成之后,一般会预留一个30分钟的支付窗口期,如果超时未支付则删除该订单同时恢复库存,这里利用redis的过期时间和过期监听机制来实现:
首先订单写入到redis中时,设置一个超时时间;当超时时间到时,基于监听机制来实现库存的恢复。
redis中添加如下配置开启监听机制

notify-keyspace-events Ex

监听机制基于KeyExpirationEventMessageListener类实现:

@Component
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {

    private static final Logger logger = LoggerFactory.getLogger(RedisKeyExpiredListener.class);

    @Autowired
    private WarehouseService warehouseService;

    public RedisKeyExpiredListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        if (!RedisUtil.isRedisKeyForOrderId(expiredKey)) {
            return;
        }

        if (!RedisUtil.isRedisKeyForOrderId(expiredKey)) {
            return;
        }

        String commodityId = RedisUtil.getCommodityIdFromRedisKey(expiredKey);
        if (StringUtils.isNotEmpty(commodityId)) {
            logger.info("订单{}超时未支付, 准备恢复商品{}的库存", expiredKey, commodityId);
            warehouseService.restoreWarehouse(commodityId, expiredKey);
        }
    }
}

由于redis的键超时之后,通过监听器只能获取到键值,无法获取到其中的内容,而恢复商品的库存还需要商品id信息,为了简便处理,订单信息在写入到redis中键按照以下规则生成: 订单id-商品id,这样通过过期的键值便可以获取到商品id。

下面演示一下效果:
假设商品1001的库存为2000件,现在需要购买10件商品
在这里插入图片描述
rest接口返回订单信息,redis中查询到订单信息:
在这里插入图片描述
此时商品1001的库存为1990件
订单超过30分钟未支付,redis中订单信息删除,同时库存恢复为2000件
在这里插入图片描述

模拟异常情况,创建订单之后抛出异常:
在这里插入图片描述
seata回滚事务,库存未发生变化:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值