高性能`锁库存`/`释放库存`重构实战

记录一次高性能锁库存/释放库存重构实战

编写本文的主要目的有两个,一是输出文档方便回顾,二是从网上很难找到高性能锁库存实战,本文可供大家参考。

本次重构有几大难点:

1、随着业务发展存在诸多历史遗留问题,技术债积重难返

2、重新组建重构团队,人永远是最重要的

3、团队在业务和系统实现细节方面存在诸多盲点

4、跨团队协作问题,言外之意人才是最大的不可控因素

重构之路举步维艰艰,所以一定要保持良好的心态,去积极应对各种各样的问题和难题。尤其涉及到跨团队协作时一定要慎重,何时该妥协,何时该硬刚,一切以重构上线为目标。

1、重构实施流程

1、收集和梳理现有系统的痛点

2、确定重构边界

3、确定重构目标

4、制定重构计划

5、实施

下面将围绕库存相关部分的重构进行展开,具体见下文。

2、现状

下面将列举目前存在的主要问题,如下:

1、直接通过SQL修改库存的场景有10多处,而且SQL还不一样

2、锁库存操作的所有逻辑直接耦合下单主逻辑中

3、锁库存操作采用粗粒度的分布式锁

4、DB和Redis中的库存不一致

5、存在超卖的情况

2.1、分析

1、各种业务场景各自直接操作库存,库存的准确性不可控,极易导致出现超卖或少卖的问题。

2、迭代新需求时,只要涉及到库存往往都会遍地开花,开发难度高并且风险极大,同时无法满足快速迭代的要求。

3、库存操作逻辑与下单逻辑完全耦合,复用性和扩展性差。

4、锁库存操作采用不合理的分布式锁,影响下单接口的性能。

5、只要涉及到库存开发人员往往不敢动、不想动。

6、代码质量差。

2.3、优化手段

  • 性能优化的关键手段: 无锁化、异步化、空间换时间

1、统一锁库存接口: 提供统一的修改库存接口,解决库存修改混乱的问题。

2、锁库存无锁化: 原来加分布式锁,现在直接通过Redis递增/递减 的原子操作来增减库存。

3、空间换时间: 锁库存幂等控制,原来基于库存变更流水表来保证幂等,DB压力大有性能瓶颈,现在通过在Redis中存储商品维度的订单号_操作类型key来保证幂等(注:有效期与订单流程的关单时限有关)。

4、锁库存入库异步化: 原来直接更新DB,现在通过MQ异步更新DB。

5、回滚锁库存异步化: 原来同步回滚库存,现在通过线程池异步回滚库存(异常时提供补偿机制)。

小结:锁库存/释放库存,要提升性能,关键在于将增删改DB的操作,异步化或者用其他方式替代

注意:通过以上手段,达到提升性能,解决超卖和少卖问题的目的。

3、方案

一图胜千言,直接上图。

锁库存

在这里插入图片描述

释放库存

在这里插入图片描述

核心:

1、通过Redis的递增(increment) / 递减(decrement)操作 来保证锁库存释放库存的原子性。

2、批量锁库存操作,要么全部成功,要么全部失败。

3、批量锁库存操作,全部成功,才 异步 发送**库存操作MQ** 。

其实一开始的方案是,一个商品锁库存成功就发一个MQ,如果有10个商品则有10个发送MQ的开销,若其中一个失败,那么回滚时还需要几次回滚MQ的开销,所以后面优化为全部成功,才异步发送库存操作MQ,此方案一方面无需回滚DB中的库存,另一方面通过简化流程使实现更加简洁还提升了性能。

异步:通过线程池异步发送MQ,减少锁库存操作的RT,提升性能。

库存操作MQ:异步将库存同步到DB,保证DB和Redis中库存的一致性。

注:异步操作出现异常时,需提供补偿机制,保证最终一致性。

4、批量锁库存操作,存在失败,则异步回滚锁库存成功的商品的Redis库存。

异步回滚Redis库存,减少锁库存操作的RT,提升性能。

因为全部成功,才会发送MQ同步DB中的库存,所以失败时无需回滚DB中的库存。

注:异步操作出现异常时,需提供补偿机制,保证最终一致性。

5、锁库存操作的幂等控制。

锁库存操作的幂等性控制方案有两种:

1、基于DB,通过库存变更流水表 的唯一索引来保证幂等。

直接操作DB,性能存在瓶颈;DB的压力较大。

2、基于Redis,通过存储商品维度的订单号_操作类型key来保证幂等。

以空间换时间,来达到高性能的目的。定时清除已下架一段时间的商品的订单号缓存。

通过权衡和结合压测结果,最终选择方案2。

6、锁库存和释放库存接口仅操作Redis,保证高性能。

库存保证最终一致性。

4、核心代码

直接上核心代码。

StockListDTO

/**
 * 库存操作 DTO
 */
@Data
@ApiModel("库存操作")
public class StockListDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "库存操作列表")
    private List<StockDTO> stockDTOList;

    /**
     * 添加库存操作参数
     */
    public void addStockDTO(StockDTO stock) {
        if (null == stockDTOList) {
            stockDTOList = new ArrayList<>();
        }
        stockDTOList.add(stock);
    }

    /**
     * 库存操作参数
     */
    @Data
    public static class StockDTO implements Serializable {

        private static final long serialVersionUID = 1L;

        public StockDTO() {

        }

        public StockDTO(Integer goodsSpecId, String orderId, Long quantity) {
            this(null, goodsSpecId, orderId, quantity);
        }

        public StockDTO(Integer stockOperate, Integer goodsSpecId, String orderId, Long quantity) {
            this.stockOperate = stockOperate;
            this.goodsSpecId = goodsSpecId;
            this.orderId = orderId;
            this.quantity = quantity;
        }

        @ApiModelProperty(value = "库存操作,1-锁库存,2-释放库存", required = true, hidden = true)
        private Integer stockOperate;

        @ApiModelProperty(value = "商品规格ID", required = true)
        @NotNull(message = "商品规格ID不能为空")
        private Integer goodsSpecId;

        @ApiModelProperty(value = "订单ID", required = true)
        @NotBlank(message = "订单ID不能为空")
        private String orderId;

        @ApiModelProperty(value = "购买数量", required = true)
        @NotNull(message = "购买数量不能为空")
        @Min(value = 1, message = "购买数量不能小于1")
        private Long quantity;
    }
}

StockServiceImpl

/**
 * 库存 servcie
 */
@Slf4j
@Service
public class StockServiceImpl implements IStockService {

    @Autowired
    StockCacheService stockCacheService;

    @Autowired
    OrderIdCacheService orderIdCacheService;

    @Autowired
    StockBizService stockBiz;

    @Override
    public JsonResult lockOrUnlockStock(StockListDTO input) {
        if (CollectionUtils.isEmpty(input.getStockDTOList())) {
            return JsonResult.build(CodeEnums.FAIL.getCode(), "库存操作参数不能为空");
        }
        JsonResult result = JsonResult.ok();
		// 订单id占位成功的库存参数
        List<StockListDTO.StockDTO> succOrderIdList = new ArrayList<>();
        // 库存操作成功的库存参数
        List<StockListDTO.StockDTO> succStockList = new ArrayList<>();
        for (StockListDTO.StockDTO stockDTO : input.getStockDTOList()) {
            try {
                // 从redis获取商品可用库存
                Long availableStock = stockCacheService.getOrLoad(stockDTO.getGoodsSpecId());

                if (StockOperateType.LOCK.equals(stockDTO.getStockOperate())) {
                    if (availableStock <= 0 || availableStock < stockDTO.getQuantity()) {
                        result.setResultCode(CodeEnums.FAIL.getCode());
                        result.setResultMsg("库存不足");
                        result.setResultData(stockDTO.getGoodsSpecId());
                        log.error("[lockStock]stockOperate={},goodsSpecId={},orderId={},availableStock={},{}", stockDTO.getStockOperate(),
                                stockDTO.getGoodsSpecId(), stockDTO.getOrderId(), availableStock, result.getResultMsg());
                        break;
                    }

                    // 新增订单ID到redis,做幂等控制
                    this.putIfAbsentOrderId(result, stockDTO, succOrderIdList);
                    if (!result.isSuccess()) {
                        break;
                    }

                    // 锁库存,实质为减少可用库存,可用库存-1
                    this.addStock(result, stockDTO, 0 - stockDTO.getQuantity());
                    if (!result.isSuccess()) {
                        break;
                    }
                } else if (StockOperateType.UNLOCK.equals(stockDTO.getStockOperate())) {
                    // 释放库存时,检查锁库存操作的orderId缓存是否存在,不存在,则不能释放库存
                    // 为了兼容上线后,在旧系统下单直接操作DB的情况,所以默认为OFF
                    if (stockProperties.checkValidLockFlowOnoff()) {
                        log.info("[lockStock]stockOperate={},goodsSpecId={},orderId={},检查锁库存流水是否存在", stockDTO.getStockOperate(), stockDTO.getGoodsSpecId(), stockDTO.getOrderId());
                        this.checkLockStockFlowExist(result, stockDTO);
                        if (!result.isSuccess()) {
                            break;
                        }
                    }
       
                    // 新增订单ID到redis,做幂等控制
                    this.putIfAbsentOrderId(result, stockDTO, succOrderIdList);
                    if (!result.isSuccess()) {
                        // 释放库存存在重复发起的情况,所以需要做到:对于重复释放库存的请求,若已成功释放库存,则直接返回成功,而不是失败,方便调用方发起重复请求时进行后续逻辑处理
                        Integer value = orderIdCacheService.get(stockDTO);
                        if (Consts.STOCK_OPT_SUCC.equals(value)) {
                            result.setResultCode(CodeEnums.SUCCESS.getCode());
                            result.setResultMsg("重复的释放库存请求");
                            log.warn("[lockStock]stockOperate={},goodsSpecId={},orderId={},对于已成功释放库存的重复请求,直接返回成功", stockDTO.getStockOperate(), stockDTO.getGoodsSpecId(), stockDTO.getOrderId());
                            continue;
                        } else {
                            break;
                        }
                    }

                    // 释放库存,实质为增加可用库存,可用库存+1
                    this.addStock(result, stockDTO, stockDTO.getQuantity());
                    if (!result.isSuccess()) {
                        break;
                    }
                }
                succStockList.add(stockDTO);
            } catch (Exception e) {
                result.setResultCode(CodeEnums.FAIL.getCode());
                result.setResultMsg("库存操作异常," + e.getMessage());
                result.setResultData(stockDTO.getGoodsSpecId());
                log.error("[lockStock]stockOperate=" + stockDTO.getStockOperate() + ",goodsSpecId=" + stockDTO.getGoodsSpecId() + "orderId=" + stockDTO.getOrderId() + ",库存操作异常", e);
                break;
            }
        }
        if (result.isSuccess()) {
            // 库存操作都成功才异步发送MQ
            // 优点:1、简化流程,无需回滚MQ;2、异步发送MQ
            succStockList.forEach(stockDTO -> {
                stockBiz.asyncDealLockOrUnlockStock(stockDTO);
            });
            return result;
        }

        // 异步回滚锁库存或释放库存
        stockBiz.asyncRollbackLockOrUnlockStock(succOrderIdList, succStockList);
        return result;
    }

    /**
     * 释放库存时,检查锁库存操作的orderId缓存是否存在,不存在,则不能释放库存
     * 注:要保证锁库存和释放库存的业务连贯性,避免释放库存接口被刷导致出现超卖的情况
     */
    private void checkLockStockFlowExist(JsonResult result, StockListDTO.StockDTO stockDTO) {
        StockListDTO.StockDTO lockStockDTO = new StockListDTO.StockDTO();
        lockStockDTO.setStockOperate(StockOperateType.LOCK);
        lockStockDTO.setIsDeliverGoods(false);
        lockStockDTO.setGoodsSpecId(stockDTO.getGoodsSpecId());
        lockStockDTO.setOrderId(stockDTO.getOrderId());
        Integer value = orderIdCacheService.get(lockStockDTO);
        if (null == value) {
            result.setResultCode(CodeEnums.FAIL.getCode());
            result.setResultMsg("释放库存时,锁库存流水必须存在");
            result.setResultData(stockDTO.getOrderId());
            log.error("[lockStock]stockOperate={},goodsSpecId={},orderId={},{}", stockDTO.getStockOperate(), stockDTO.getGoodsSpecId(), stockDTO.getOrderId(), result.getResultMsg());
            return;
        }
        // 此处通过巧妙的设计,在释放库存时,对释放库存的梳理进行校验,而无需从数据库获取库存数量,可极大的提高性能
        if (!stockDTO.getQuantity().equals(Long.valueOf(value))) {
            result.setResultCode(CodeEnums.FAIL.getCode());
            result.setResultMsg("释放库存时,释放数量与锁库存操作中锁定数量必须一致,unlock_quantity=" + stockDTO.getQuantity() + ",lock_quantity=" + value);
            result.setResultData(stockDTO.getOrderId());
            log.error("[lockStock]stockOperate={},goodsSpecId={},orderId={},{}", stockDTO.getStockOperate(), stockDTO.getGoodsSpecId(), stockDTO.getOrderId(), result.getResultMsg());
            return;
        }
    }
    
    /**
     * 新增订单ID到redis(做幂等控制,若设置过期时间,则只能在一定程度上防止重复请求出现超卖)
     * 注:重复请求返回10014,供调用方进行处理。
     */
    private void putIfAbsentOrderId(JsonResult result, StockListDTO.StockDTO stockDTO, List<StockListDTO.StockDTO> succOrderIdList) {
        if (!orderIdCacheService.putIfAbsent(stockDTO, Consts.STOCK_OPT_INIT)) {
            result.setResultCode(CodeEnums.SYS_RECORD_EXIST.getCode());
            result.setResultMsg("重复请求");
            result.setResultData(stockDTO.getOrderId());
            log.error("[lockStock]stockOperate={},goodsSpecId={},orderId={},{}", stockDTO.getStockOperate(),
                    stockDTO.getGoodsSpecId(), stockDTO.getOrderId(), result.getResultMsg());
            return;
        }
        succOrderIdList.add(stockDTO);
    }

    /**
     * 添加库存,实际为锁库存/释放库存
     *
     * @param quantity 库存数量,正数表示释放库存(增),负数表示锁库存(减)
     */
    private void addStock(JsonResult result, StockListDTO.StockDTO stockDTO, Long quantity) {
        JsonResult<Long> tempResult = stockBiz.addStockCacheOrRollback(stockDTO.getGoodsSpecId(), quantity);
        if (!tempResult.isSuccess()) {
            result.setResultCode(tempResult.getResultCode());
            result.setResultMsg(tempResult.getResultMsg());
            result.setResultData(stockDTO.getGoodsSpecId());
            log.error("[lockStock]stockOperate={},goodsSpecId={},availableStock={},库存操作失败,{}", stockDTO.getStockOperate(),
                    stockDTO.getGoodsSpecId(), tempResult.getResultData(), result.getResultMsg());
            return;
        }
    }

}

StockBizService

/**
 * 库存相关
 *
 */
@Slf4j
@Component
public class StockBizService {

    @Autowired
    RedissonClient redissonClient;

    @Autowired
    StockCacheService stockCacheService;

    @Autowired
    OrderIdCacheService orderIdCacheService;

    @Autowired
    StockOptProducer stockOptProducer;

    /**
     * 添加并获取可用库存,当可用库存小于0时,还原可用库存
     * <p>
     * 注:锁库存/释放库存 操作时调用。
     *
     * @param goodsSpecId 商品规格id
     * @param quantity    库存数量,正数表示释放库存(增),负数表示锁库存(减)
     */
    public JsonResult<Long> addStockCacheOrRollback(Integer goodsSpecId, Long quantity) {
        // 从redis获取商品可用库存
        RAtomicLong atomicLong = redissonClient.getAtomicLong(stockCacheService.buildCacheKey(goodsSpecId));

        Long availableStock = atomicLong.addAndGet(quantity);
        if (availableStock >= 0) {
            return JsonResult.ok(availableStock);
        }

        log.warn("[addStockCacheOrRollback]goodsSpecId={},quantity={},可用库存小于0,回滚可用库存", goodsSpecId, quantity);
        availableStock = atomicLong.addAndGet(0 - quantity);
        return JsonResult.build(CodeEnums.FAIL.getCode(), "库存不足!", availableStock);
    }

    /**
     * 增量添加并获取可用库存
     * <p>
     * 注:回滚锁库存/回滚释放库存时调用。
     *
     * @param goodsSpecId 商品规格id
     * @param quantity    库存数量,正数表示释放库存(增),负数表示锁库存(减)
     */
    public JsonResult<Long> addStockCache(Integer goodsSpecId, Long quantity) {
        // 从redis获取商品可用库存
        RAtomicLong atomicLong = redissonClient.getAtomicLong(stockCacheService.buildCacheKey(goodsSpecId));

        Long availableStock = atomicLong.addAndGet(quantity);
        return JsonResult.ok(availableStock);
    }

    /**
     * 异步处理成功的锁库存或释放库存
     */
    @Async
    public void asyncDealLockOrUnlockStock(StockListDTO.StockDTO stockDTO) {
        // 发送库存操作MQ
        stockOptProducer.sendStockOptMq(stockDTO);

        try {
            orderIdCacheService.put(stockDTO, Consts.STOCK_OPT_SUCC);
        } catch (Exception e) {
            log.error("[lockStock]订单ID缓存状态修改为成功异常,stockOperate=" + stockDTO.getStockOperate() + ",goodsSpecId=" + stockDTO.getGoodsSpecId() + "orderId=" + stockDTO.getOrderId(), e);
        }
    }

    /**
     * 异步回滚锁库存或释放库存
     *
     * @param succOrderIdList 订单id占位成功的库存参数
     * @param succStockList   库存操作成功的库存参数
     * @param stockFlowIdList 库存变更流水列表
     */
    @Async
    public void asyncRollbackLockOrUnlockStock(List<StockListDTO.StockDTO> succOrderIdList, List<StockListDTO.StockDTO> succStockList) {
        if (!CollectionUtils.isEmpty(succOrderIdList)) {
            succOrderIdList.forEach(stockDTO -> {
                try {
                    orderIdCacheService.evict(stockDTO);
                    log.info("[lockStock]回滚订单ID缓存,stockOperate={},goodsSpecId={},orderId={},quantity={}", stockDTO.getStockOperate(),
                            stockDTO.getGoodsSpecId(), stockDTO.getOrderId(), stockDTO.getQuantity());
                } catch (Exception e) {
                    log.error("[lockStock]回滚订单ID缓存异常,stockOperate=" + stockDTO.getStockOperate() + ",goodsSpecId=" + stockDTO.getGoodsSpecId() + "orderId=" + stockDTO.getOrderId(), e);
                    // TODO 异常的情况下,提供补偿机制来进行回滚,重试 or MQ
                }
            });
        }

        // 回滚锁库存/释放库存
        if (!CollectionUtils.isEmpty(succStockList)) {
            for (StockListDTO.StockDTO stockDTO : succStockList) {
                try {
                    if (StockOperateType.LOCK.equals(stockDTO.getStockOperate())) {
                        // 回滚锁库存(释放库存,实质为增加可用库存),可用库存+1
                        JsonResult<Long> tempResult = this.addStockCache(stockDTO.getGoodsSpecId(), stockDTO.getQuantity());
                        log.info("[lockStock]回滚锁库存,stockOperate={},goodsSpecId={},orderId={},quantity={},{}", stockDTO.getStockOperate(),
                                stockDTO.getGoodsSpecId(), stockDTO.getOrderId(), stockDTO.getQuantity(), tempResult.getResultMsg());
                    } else if (StockOperateType.UNLOCK.equals(stockDTO.getStockOperate())) {
                        if (!stockDTO.getIsDeliverGoods()) {
                            // 回滚释放库存(锁库存,实质为减少可用库存),可用库存-1
                            JsonResult<Long> tempResult = this.addStockCache(stockDTO.getGoodsSpecId(), 0 - stockDTO.getQuantity());
                            log.info("[lockStock]回滚释放库存,stockOperate={},goodsSpecId={},orderId={},quantity={},{}", stockDTO.getStockOperate(),
                                    stockDTO.getGoodsSpecId(), stockDTO.getOrderId(), stockDTO.getQuantity(), tempResult.getResultMsg());
                        }
                    }
                } catch (Exception e) {
                    log.error("[lockStock]回滚库存操作异常,stockOperate=" + stockDTO.getStockOperate() + ",goodsSpecId=" + stockDTO.getGoodsSpecId() + "orderId=" + stockDTO.getOrderId(), e);
                    // TODO 异常的情况下,提供补偿机制来进行回滚,重试 or MQ
                }
            }
        }
    }
}

CacheService

/**
 * 缓存 接口
 * 基于业务维度标准化缓存操作
 *
 * @param <K> 表示相关的业务要素,可以是单个字段,也可以是一个对象。由开发人员在实现类中自己定义。
 * @param <R> 表示返回的缓存数据
 */
public interface CacheService<K, R> {

    /**
     * 构建缓存key
     */
    default String buildCacheKey(K key) {
        return null;
    }

    /**
     * 获取缓存
     */
    R get(K key);

    /**
     * 获取或加载缓存,若缓存不存在,则从加载并设置到缓存,并返回
     */
    R getOrLoad(K key);

    /**
     * 设置指定key的缓存项
     */
    R put(K key, R value);

    /**
     * 仅当之前没有存储指定key的value时,才存储由指定key映射的指定value
     */
    default boolean putIfAbsent(K key, R value) {
        return false;
    }

    /**
     * 重新加载缓存(存在则替换,不存在则设置)
     */
    R reload(K key);

    /**
     * 淘汰缓存
     */
    void evict(K key);

    /**
     * 判断key是否存在
     */
    boolean isExists(K key);
}

StockCacheService

/**
 * 库存缓存业务类
 */
@Slf4j
@Component
public class StockCacheService implements CacheService<Integer, Long> {

    private static final String STOCK_CACHE_PREFIX = "goods:spec:stock:";

    @Autowired
    RedissonClient redissonClient;

    @Resource
    GoodsSpecFeignClient goodsSpecFeignClient;

    @Override
    public String buildCacheKey(Integer goodsSpecId) {
        return STOCK_CACHE_PREFIX + goodsSpecId;
    }

    /**
     * 获取商品规格可用库存
     *
     * @param goodsSpecId 商品规格id
     */
    @Override
    public Long get(Integer goodsSpecId) {
        // 从redis获取商品可用库存
        RAtomicLong atomicLong = redissonClient.getAtomicLong(this.buildCacheKey(goodsSpecId));

        // 若redis中不存在该key,则默认为0
        return atomicLong.get();
    }

    /**
     * 获取或设置商品规格可用库存,若缓存不存在,则从DB加载并设置到缓存
     * <p>
     * 注:可用于预热等场景
     *
     * @param goodsSpecId 商品规格id
     */
    @Override
    public Long getOrLoad(Integer goodsSpecId) {
        // 从redis获取商品可用库存
        RAtomicLong atomicLong = redissonClient.getAtomicLong(this.buildCacheKey(goodsSpecId));

        Long availableStock = atomicLong.get();
        if (availableStock > 0) {
            return availableStock;// 可用库存大于0,则直接返回
        }

        // 判断可用库存是否存在,应对可用库存为0的情况,避免每次去db加载,出现缓存穿透
        if (atomicLong.isExists()) {
            log.info("[getOrLoad][exist]goodsSpecId={},availableStock={}", goodsSpecId, availableStock);
            return availableStock;
        }

        log.info("[getOrLoad][stock]goodsSpecId={},load from db", goodsSpecId);
        RLock lock = redissonClient.getLock(this.buildLoadStockLockCacheKey(goodsSpecId));
        // 高并发场景下,拦截一部分请求将其快速失败,保证性能
        if (!lock.tryLock()) {
            log.warn("[getOrLoad][stock]goodsSpecId={},tryLock fail, 存在正在加载库存的请求,请稍后重试!", goodsSpecId);
            return availableStock;
        }
        try {
            // 双重检查
            availableStock = atomicLong.get();
            if (availableStock > 0) {
                return availableStock;
            }

            JsonResult<Long> result = getGoodsSpecAvailableStock(goodsSpecId);
            if (!result.isSuccess()) {
                throw new BusinessException(CodeEnums.FAIL.getCode(), result.getResultMsg());
            }
            atomicLong.set(result.getResultData());// 设置可用库存到redis
            log.info("[getOrLoad][stock]goodsSpecId={},availableStock={},set succ", goodsSpecId, result.getResultData());
            return result.getResultData();
        } finally {
            lock.unlock();// 释放锁
        }
    }

    @Override
    public Long put(Integer goodsSpecId, Long availableStock) {
        if (null == availableStock) {
            throw new BusinessException(CodeEnums.FAIL.getCode(), "可用库存不能为空");
        }
        if (availableStock < 0) {
            throw new BusinessException(CodeEnums.FAIL.getCode(), "可用库存不能小于0");
        }
        RAtomicLong atomicLong = redissonClient.getAtomicLong(this.buildCacheKey(goodsSpecId));
        atomicLong.set(availableStock);// 设置可用库存到redis
        log.info("[put][stock]goodsSpecId={},availableStock={},reload succ", goodsSpecId, availableStock);
        return availableStock;
    }

    @Override
    public Long reload(Integer goodsSpecId) {
        log.info("[reload][stock]goodsSpecId={},reload from db", goodsSpecId);
        RLock lock = redissonClient.getLock(this.buildReloadStockLockCacheKey(goodsSpecId));
        if (!lock.tryLock()) {
            throw new BusinessException(CodeEnums.FAIL.getCode(), "重复的reload请求!");
        }
        try {
            JsonResult<Long> result = getGoodsSpecAvailableStock(goodsSpecId);
            if (!result.isSuccess()) {
                throw new BusinessException(CodeEnums.FAIL.getCode(), result.getResultMsg());
            }
            RAtomicLong atomicLong = redissonClient.getAtomicLong(this.buildCacheKey(goodsSpecId));
            Long oldStock = atomicLong.get();
            atomicLong.set(result.getResultData());// 设置可用库存到redis
            log.info("[reload][stock]goodsSpecId={},availableStock={},oldStock={},reload succ", goodsSpecId, result.getResultData(), oldStock);
            return result.getResultData();
        } finally {
            lock.unlock();// 释放锁
        }
    }

    @Override
    public void evict(Integer goodsSpecId) {
        RAtomicLong atomicLong = redissonClient.getAtomicLong(this.buildCacheKey(goodsSpecId));
        boolean result = atomicLong.delete();
        log.info("[evict][stock]淘汰库存,goodsSpecId={}, result={}", goodsSpecId, result);
    }

    @Override
    public boolean isExists(Integer key) {
        throw new BusinessException("stock缓存暂不支持该方法");
    }

    // ----------------- 自定义方法

    /**
     * 构建加载库存lock的缓存key
     */
    private String buildLoadStockLockCacheKey(Integer goodsSpecId) {
        return "lock:stock:load:" + goodsSpecId;
    }

    private String buildReloadStockLockCacheKey(Integer goodsGroupId) {
        return "lock:stock:reload:" + goodsGroupId;
    }

    /**
     * 获取商品规格的可用库存
     *
     * @param goodsSpecId 商品规格id
     */
    private JsonResult<Long> getGoodsSpecAvailableStock(Integer goodsSpecId) {
        JsonResult<GoodsSpecDTO> result = goodsSpecFeignClient.queryGoodsSpecById(goodsSpecId);
        log.info("[load][stock]goodsSpecId={},queryGoodsSpecById,result={}", goodsSpecId, JSON.toJSONString(result));
        if (!result.isSuccess()) {
            return JsonResult.build(CodeEnums.FAIL.getCode(), result.getResultMsg());
        }
        GoodsSpecDTO goodsSpecDTO = result.getResultData();
        if (null == goodsSpecDTO) {
            return JsonResult.build(CodeEnums.FAIL.getCode(), "商品规格不存在!");
        }
        // 可用库存 = 剩余库存 - 锁定库存
        Long availableStock = Long.valueOf(goodsSpecDTO.getStock() - goodsSpecDTO.getLockingStock());
        if (availableStock < 0) {
            log.info("[load][stock]goodsSpecId={},queryGoodsSpecById,result={}", goodsSpecId, JSON.toJSONString(result));
            return JsonResult.build(CodeEnums.FAIL.getCode(), "库存不足");
        }
        return JsonResult.ok(availableStock);
    }

}

OrderIdCacheService

采用String数据结构存储商品的orderId缓存,因为hash数据结构,在秒杀场景下可能存在热点数据问题。

/**
 * 设置订单ID到redis,主要为了提升锁库存操作的性能,同时保证幂等
 * 方案:不设置过期时间,后续通过定时任务扫描商品,当商品下架7天后清理掉缓存。若商品再次上架,则将该商品对应的订单ID再次预热到缓存。
 * 优势:将技术维度和业务维度结合来保证幂等,比直接通过DB唯一约束来保证幂等效率更高,保证高性能
 *
 * 注:采用String数据结构,因为hash数据结构存在热点数据问题。
 */
@Slf4j
@Component
public class OrderIdCacheService implements CacheService<StockListDTO.StockDTO, Integer> {

    private static final String CACHE_PREFIX = "goods:orderId:";
    private static final String SPLIT = ":";

    /**
     * 下单成功的订单id缓存有效时间,默认15天
     */
    @Value("${orderId.live.time.lock.hours:360}")
    private Long orderIdLiveTimeLockHours;

    /**
     * 下单成功的订单id缓存有效时间,默认15天
     */
    @Value("${orderId.live.time.unlock.hours:360}")
    private Long orderIdLiveTimeUnlockHours;

    @Autowired
    RedissonClient redissonClient;

    @Override
    public String buildCacheKey(StockListDTO.StockDTO stockDTO) {
        StringBuffer key = new StringBuffer(CACHE_PREFIX);
        key.append(stockDTO.getGoodsSpecId()).append(SPLIT);
        key.append(stockDTO.getOrderId()).append(SPLIT);
        key.append(stockDTO.getStockOperate()).append(SPLIT);
        if (stockDTO.getIsDeliverGoods()) {
            key.append("2");
        } else {
            key.append("1");
        }
        return key.toString();
    }

    @Override
    public Integer get(StockListDTO.StockDTO stockDTO) {
        RBucket<Integer> bucket = redissonClient.getBucket(buildCacheKey(stockDTO));
        return bucket.get();
    }

    @Override
    public Integer getOrLoad(StockListDTO.StockDTO stockDTO) {
        throw new BusinessException("暂不支持订单ID的getOrLoad操作");
    }

    /**
     * 设置值orderId缓存的value值
     * <p>
     * value值有两个维度的含义,
     * 1、等于0,表示库存操作的初始状态
     * 2、大于0,表示库存操作成功,并且大于0的值表示库存操作对应的库存数量
     * 大于0的值,可用于在释放库存时,判断 释放的库存数量 和 orderId对应的锁库存操作的锁定库存数量 是否相等。
     * 注:通过对该value值的巧妙设计,可避免在释放库存时操作数据库
     *
     * @param stockDTO 库存操作参数
     * @param value    值
     */
    @Override
    public Integer put(StockListDTO.StockDTO stockDTO, Integer value) {
        RBucket<Integer> bucket = redissonClient.getBucket(buildCacheKey(stockDTO));
        long liveTimeHours = getLiveTimeHours(stockDTO.getStockOperate());
        bucket.set(value, liveTimeHours, TimeUnit.HOURS);
        log.debug("[put][orderId] key={}, value={}, liveTimeHours={}", this.buildCacheKey(stockDTO), value, liveTimeHours);
        return value;
    }

    /**
     * 设置订单ID到redis,主要为了提升锁库存操作的性能,同时保证幂等
     * 方案:不设置过期时间,后续通过定时任务扫描商品,当商品下架7天后清理掉缓存。若商品再次上架,则将该商品对应的订单ID再次预热到缓存。
     * 优势:将技术维度和业务维度结合来保证幂等,比直接通过DB唯一约束来保证幂等效率更高,保证高性能
     */
    @Override
    public boolean putIfAbsent(StockListDTO.StockDTO stockDTO, Integer value) {
        RBucket<Integer> bucket = redissonClient.getBucket(buildCacheKey(stockDTO));
        long liveTimeHours = getLiveTimeHours(stockDTO.getStockOperate());
        boolean rslt = bucket.trySet(value, liveTimeHours, TimeUnit.HOURS);
        log.info("[putIfAbsent][orderId] key={}, value={}, liveTimeHours={}, rslt={}", this.buildCacheKey(stockDTO), value, liveTimeHours, rslt);
        return rslt;
    }

    @Override
    public Integer reload(StockListDTO.StockDTO stockDTO) {
        throw new BusinessException("暂不支持订单ID的reload操作");
    }

    @Override
    public void evict(StockListDTO.StockDTO stockDTO) {
        RBucket<Integer> bucket = redissonClient.getBucket(buildCacheKey(stockDTO));
        boolean rslt = bucket.delete();
        log.info("[evict][orderId] key={}, result={}", this.buildCacheKey(stockDTO), rslt);
    }

    @Override
    public boolean isExists(StockListDTO.StockDTO stockDTO) {
        RBucket<Integer> bucket = redissonClient.getBucket(buildCacheKey(stockDTO));
        return bucket.isExists();
    }

    private Long getLiveTimeHours(Integer stockOperate) {
        if (StockOperateType.LOCK.equals(stockOperate)) {
            return orderIdLiveTimeLockHours;
        }
        return orderIdLiveTimeUnlockHours;
    }

}

StockOptProducer

基于Rocketmq实现。

/**
 * 锁库存/释放库存MQ Producer
 */
@Slf4j
@Service
public class StockOptProducer {

    @Resource
    RocketmqConfig rocketmqConfig;

    @Resource
    ProducerBean producerBean;

    /**
     * 发送库存操作MQ
     *
     * @param stockDTO          库存操作参数
     * @param isRollbackOperate 是否是回滚操作,true表示回滚
     */
    public void sendStockOptMq(StockListDTO.StockDTO stockDTO) {
        try {
            StockOptMessageDTO stockOptMessageDTO = new StockOptMessageDTO();
            BeanUtils.copyProperties(stockDTO, stockOptMessageDTO);
            stockOptMessageDTO.setTraceId(MDCLogTracerUtil.getTraceId());// 方便查找日志

            log.info("send topic {} message={}", rocketmqConfig.getTopicName(), stockOptMessageDTO);
            Message message = new Message(rocketmqConfig.getTopicName(), RocketmqTagConsts.TAG_STOCK_OPT, JSON.toJSONString(stockOptMessageDTO).getBytes());
            SendResult sendResult = producerBean.send(message);
            log.info("send topic {} sendResult={}", rocketmqConfig.getTopicName(), sendResult);
        } catch (Exception e) {
            log.error("error send topic " + rocketmqConfig.getTopicName(), e);
        }
    }
}

StockOptMessageListener

基于Rocketmq实现。

/**
 * 库存操作MQ监听器
 */
@Slf4j
@Component
public class StockOptMessageListener implements MessageListener {

    @Resource
    StockDao stockDao;

    @Override
    public Action consume(final Message message, final ConsumeContext context) {
        log.info("consumer {}, tag={}, message={}", message.getTopic(), message.getTag(), message);
        try {
            String body = new String(message.getBody());
            StockOptMessageDTO stockOptMessageDTO = JSON.parseObject(body, StockOptMessageDTO.class);
            log.info("consumer tag={}, message={}", message.getTag(), stockOptMessageDTO);
            
            StockOptDTO input = new StockOptDTO
            input.setGoodsId(stockOptMessageDTO.getGoodsId());
            input.setGoodsSpecId(stockOptMessageDTO.getGoodsSpecId());
            input.setOrderId(stockOptMessageDTO.getOrderId());
            input.setQuantity(stockOptMessageDTO.getQuantity());
            input.setGoodsSpecId(stockOptMessageDTO.getGoodsSpecId());
			// 锁库存or释放库存
            stockDao.lockOrUnlockStock(input);
            return Action.CommitMessage;
        } catch (Exception e) {
            log.error("consumer error " + message.getTopic(), e);
            //消费失败,挂起当前队列
            return Action.ReconsumeLater;
        }
    }
}

StockDao

高并发场景下存在热点更新的问题。

可以通过分而治之的方式来降低单行的锁竞争,将商品规格的库存,由单行变为多行来实现。

public class StockDao {
    /**
      * 锁库存MQ,锁定库存+1
      * 释放库存MQ,锁定库存-1
      */
    @Transactional
    public void lockOrUnlockStock(StockOptDTO input) {
        // 通过库存变更流水控制幂等
        StockChangeFlow stockChangeFlow = new StockChangeFlow();
        stockChangeFlow.setGoodsSpecId(input.getGoodsSpecId());
        stockChangeFlow.setOrderId(input.getOrderId());
        stockChangeFlow.setOptType(optType);
        stockChangeFlow.setQuantity(input.getQuantity());
        stockChangeFlow.setStatus(2);// 状态,1-初始,2-已同步
        int rslt = stockChangeFlowMapper.insert(stockChangeFlow);
        log.info("[addStockFlow]新增库存变更流水,goodsSpecId={},orderId={},optType={}", input.getGoodsSpecId(), input.getOrderId(), optType);
        if (rslt <= 0) {
            throw new BusinessException("[addStockFlow]新增库存变更流水失败,affect rows " + rslt);
        }

        if (StockOperateType.LOCK.equals(input.getStockOperate())) {
            rslt = goodsSpecMapper.updateLockingStock(input.getGoodsSpecId(), input.getQuantity());
        } else if (StockOperateType.UNLOCK.equals(input.getStockOperate())) {
            rslt = goodsSpecMapper.updateLockingStock(input.getGoodsSpecId(), 0 - input.getQuantity());
        }
        log.info("更新锁定库存,goodsSpecId={},orderId={},quantity={},affect rows={}", input.getGoodsSpecId(), input.getOrderId(), input.getQuantity(), rslt);
        if (rslt <= 0) {
            throw new BusinessException("更新锁定库存失败,affect rows " + rslt);
        }
    }
}

Consts

public class Consts {
   /**
     * 库存操作初始状态
     */
    public static final Integer STOCK_OPT_INIT = 1;
    /**
     * 库存操作成功
     */
    public static final Integer STOCK_OPT_SUCC = 2;
}

GoodsSpecMapper.xml

<!-- 更新锁定库存 range 级别 -->
<update id="updateLockingStock">
	UPDATE `goods_spec` SET locking_stock = locking_stock + #{quantity}
	WHERE goods_spec_id = #{goodsSpecId} and stock >= (locking_stock + #{quantity}) and (locking_stock + #{quantity}) >= 0
</update>

5、核心问题

高并发update锁定库存性能低的问题

  • 描述

在秒杀活动时,有大量的锁库存操作。换句话说就是高并发场景下,对于单个数据行的update操作,锁竞争非常激烈,导致update性能低下。这是一个典型的高并发热点数据的update场景。

商品规格表数据量2000w+,数据量也是影响update性能的一个重要因素。因为mysql的锁是在索引上的,数据量越大锁定查找的耗时就越大。

ERROR HY000: Lock wait timeout exceeded; try restarting transaction

锁等待超时,是当前事务在等待其它事务释放锁资源造成的。可以找出锁资源竞争的表和语句,优化你的SQL,创建索引等,如果还是不行,可以适当减少并发线程数。

  • 分析

要想db操作的性能足够高,巧妙的设计很重要,事务的操作范围要尽量的小。

从上面可得知,本质是大量并发请求对单个数据行加锁,导致的性能低下,那么我们能不能将单个数据行的热点update分散开来,降低锁竞争呢?

  • 方案

基于分而治之的理念,我们引入slot概念,将原来一个row通过多个row来表示,然后通过sum来汇总。同时为了不让slot成为瓶颈,我们 rand slot,然后将update转换为instert,通过on duplicate key update 子句来解决冲突问题。

该方案降低了锁的粒度,提升了并发update的性能。

  • 首先,创建库存表
CREATE TABLE `t_sku_stock` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `sku_id` bigint(20) NOT NULL,
  `sku_stock` int(11) DEFAULT '0',
  `slot` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_sku_slot` (`sku_id`,`slot`),
  KEY `idx_sku_id` (`sku_id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4;

表中唯一性索引 idx_sku_slot 用来约束同一个 sku_id 不同 slot 。

  • 库存增加操作
insert into t_sku_stock (sku_id,sku_stock,slot)
values(10001, 10, round(rand()*15)+1) 
on duplicate key update sku_stock=sku_stock+values(sku_stock)

我们给 sku_id=101010101 增加10个库存,通过 round(rand()*9)+1 将slot控制在16个以内(可以根据情况放宽或缩小),当 unique key 不冲突的话就一直是insert,一旦发生 duplicate 就会执行 update。update也是分散的。

  • 库存减少操作

减少库存比增加库存复杂一些,最大的问题是要做前置检查,不能超扣。

检查总库存数

select sku_id, sum(sku_stock) as ss
from t_sku_stock
where sku_id= 10001
group by sku_id having ss>= 10 for update

mysql的查询是使用mvcc来实现无锁并发,所以为了实时一致性我们需要加上for update来做实时检查。

如果库存是够扣减的话我们就执行 insert into select 插入操作。

insert into t_sku_stock (sku_id, sku_stock, slot)
select sku_id,-10 as sku_stock,round(rand()*15+1)
from(
    select sku_id, sum(sku_stock) as ss
    from t_sku_stock
    where sku_id= 10001
    group by sku_id having ss>= 10 for update) as tmp
on duplicate key update sku_stock= sku_stock+values(sku_stock)

整个操作都是在一次db交互中执行完成,如果控制好单表的数据量加上 unique key 配合性能是非常高的。

消除 select…for update

参见文章 Mysql大并发热点行更新的两个骚操作

11-04 2044
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白云coy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值