记录一次高性能锁库存/释放库存重构实战
编写本文的主要目的有两个,一是输出文档方便回顾,二是从网上很难找到高性能锁库存实战,本文可供大家参考。
本次重构有几大难点:
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大并发热点行更新的两个骚操作

3703

被折叠的 条评论
为什么被折叠?



