电商网站库存模块
库存表包含了商品的sku,商品类型,商品款号,颜色,尺码,库存数,版本号,创建时间,修改时间。
商品类型,可根据商品分为,普通商品,赠品,内卖商品,预售商品等
库存表结构
@Data
public class ProductStock extends OrderEntity<String> {
private static final long serialVersionUID = 6324321924144806460L;
/**
* sku
*/
private String sku;
/**
* good type
*/
private GoodsType goodsType;
/**
* 款号
*/
private String sn;
/**
* 颜色
*/
private String color;
/**
* 尺码
*/
private String size;
/**
* 库存
*/
private Integer stock;
@Version
private Long version;
private Date createdDate;
private Date lastModifiedDate;
}
有库存表就有库存操作表,记录每次出库入库的日志。
库存操作记录表
@Data
public class ProductStockLog {
private static final long serialVersionUID = -5035787394251728152L;
/**
* 类型
*/
public enum Type {
/** 入库 */
stockIn,
/** 出库 */
stockOut
}
/** 类型 */
private ProductStockLog.Type type;
private String sku;
private GoodsType goodsType;
/** 入库数量 */
private Integer inQuantity;
/** 出库数量 */
private Integer outQuantity;
/** 当前库存 */
private Integer stock;
/** 操作员 */
private String operator;
/** 备注 */
private String memo;
/** 商品 */
private String productName;
@Version
private Long version;
private Date createdDate;
private Date lastModifiedDate;
}
基础的实体创建好了,那就需要具体的操作库存了,库存的扣减一般分为下单扣库存,支付扣库存等,可根据项目需要在合适的场景下进行库存操作。
首先下单的时候,肯定需要先检查库存。
通过sku和商品的类型查询库存,返回Integer类型
public BaseResponse<Integer> getStockNumBySku(String sku,GoodsType type) {
try {
String key = stockRequest.getType().name() + "_" + stockRequest.getSku();
log.info("init key:{}", key);
Long stock = redisStockService.getStock(key, 60 * 60, (IStockCallback) productStockService);
if (stock == null || stock.equals(UNINITIALIZED_STOCK)) {
log.info("没有该商品库存,请先维护商品库存数据!");
return BaseResponse.exceptionResponse("500", "没有该商品库存,请先维护商品库存数据!");
} else {
return BaseResponse.successResponse(Integer.parseInt(String.valueOf(stock)));
}
} catch (Exception e) {
log.error("服务器端查询库存失败", e);
return BaseResponse.exceptionResponse("500", "服务器端查询库存失败");
}
}
通过redis + lua 保证库存原子性操作。
检查库存完毕后,就是调整库存了,一般情况下并发都是发生在扣库存的场景下,我们先创建一个库存调整请求类
@Data
@ApiModel(description = "调整库存请求类")
public class AdjustStockRequest {
@ApiModelProperty(value = "sku", name = "sku", example = "sku")
private String sku;
@ApiModelProperty(value = "goodsType")
private GoodsType goodsType;
@ApiModelProperty(value = "stock", name = "增、减的库存量(负数表示减库存)", example = "0")
private Integer stock;
@ApiModelProperty(value = "商品SKU名称", name = "productName", example = "LV")
private String productName;
}
以及库存操作状态的枚举类型
public enum StockOperationStatEnum {
SUCCESS(1, "库存操作成功"),
STOCK_UNLIMITED(-1,"库存不限"),
STOCK_NOT_ENOUGH(-2, "库存不足"),
STOCK_UNINITIALIZED(-3, "库存未初始化"),
FAILED(-4, "库存操作失败");
private long state;
private String stateInfo;
StockOperationStatEnum(long state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public long getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public static StockOperationStatEnum stateOf(long index){
for (StockOperationStatEnum state : values()){
if (state.getState() == index){
return state;
}
}
return null;
}
}
public BaseResponse<Boolean> adjustStock(@RequestBody AdjustStockRequest stockRequest) {
// TODO 省略了参数的非空校验
String key = stockRequest.getGoodsType() + "_" + stockRequest.getSku();
StockOperationStatEnum resultEnum = productStockService.adjustStock(key, stockRequest.getStock());
if (resultEnum == StockOperationStatEnum.SUCCESS) {
ProductStockLog productStockLog = new ProductStockLog();
productStockLog.setSku(stockRequest.getSku());
productStockLog.setGoodsType(stockRequest.getGoodsType());
productStockLog.setType(stockRequest.getStock() > 0 ? ProductStockLog.Type.stockIn : ProductStockLog.Type.stockOut);
productStockLog.setInQuantity(stockRequest.getStock() > 0 ? stockRequest.getStock() : 0);
productStockLog.setOutQuantity(stockRequest.getStock() < 0 ? Math.abs(stockRequest.getStock()) : 0);
Long stock = redisStockService.getStock(key, 60 * 60, (IStockCallback) productStockService);
if (stock == null || stock.equals(UNINITIALIZED_STOCK)) {
stock = Long.valueOf(0);
}
//当前库存
productStockLog.setStock(Integer.parseInt(String.valueOf(stock)));
productStockLog.setOperator("customer");
productStockLog.setProductName(stockRequest.getProductName());
productStockLogService.saveProductStockLog(productStockLog);
// 调整库存后更改商品假库存
ProductStockVo productStockVo = new ProductStockVo();
productStockVo.setStock(Integer.parseInt(String.valueOf(stock)));
productStockVo.setGoodsType(stockRequest.getGoodsType());
productStockVo.setSku(stockRequest.getSku());
stockSendGoods.send(GsonUtil.toJson(productStockVo));
return BaseResponse.successResponse(true);
}
return BaseResponse.exceptionResponse("500",false);
}
整个库存的操作我们都是放到了redis中,通过redis+lua保证库存操作的原子性,最终在完成库存调整后,通过ProductStockVo 对象,将库存调整的信息通过MQ同步到我们库存的WMS系统。
Lua 嵌入 Redis 优势:
@Service
@Slf4j
public class RedisStockService {
/**
* Redis 客户端
*/
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public static final String REDIS_STOCK_KEY_PREFIX = "redis_key:stock:";
public static final String REDIS_INIT_STOCK_KEY_PREFIX = "redis_key:stock_init:";
/**
* 库存未初始化
*/
public static final Long UNINITIALIZED_STOCK = -3L;
/**
* 执行扣库存的脚本
*/
private static final String STOCK_LUA;
static {
/**
*
* @desc 扣减库存Lua脚本
* 库存(stock)-1:表示不限库存
* 库存(stock)0:表示没有库存
* 库存(stock)大于0:表示剩余库存
*
* @params 库存key
* @return
* -3:库存未初始化
* -2:库存不足
* -1:不限库存
* 大于等于0:剩余库存(扣减之后剩余的库存)
* redis缓存的库存(value)是-1表示不限库存,直接返回1
*/
StringBuilder sb = new StringBuilder();
sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));");
sb.append(" local num = tonumber(ARGV[1]);");
sb.append(" if (stock == -1) then");
sb.append(" return -1;");
sb.append(" end;");
sb.append(" if (num < 0) then");
sb.append(" if (stock >= math.abs(num)) then");
sb.append(" return redis.call('incrby', KEYS[1], 0 + num);");
sb.append(" end;");
sb.append(" return -2;");
sb.append(" end;");
sb.append(" return redis.call('incrby', KEYS[1], 0 + num);");
sb.append("end;");
sb.append("return -3;");
STOCK_LUA = sb.toString();
log.info("init stock_lua script:{}", STOCK_LUA);
}
/**
* @param key 库存key
* @param expire 库存有效时间,单位秒
* @param num 扣减数量
* @param stockCallback 初始化库存回调函数
* @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存
*/
public Long adjustStock(String key, long expire, int num, IStockCallback stockCallback) {
if (StringUtils.isEmpty(key)) {
key = "";
}
if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
key = REDIS_STOCK_KEY_PREFIX + key;
}
//redis lua脚本修改库存
long stock = this.adjustStock(key, num);
// 如果没有初始库存
if (stock == UNINITIALIZED_STOCK) {
RedisLock redisLock = new RedisLock(redisTemplate, key);
try {
// 获取锁
if (redisLock.tryLock()) {
// 双重验证,避免并发时重复回源到数据库
stock = this.adjustStock(key, num);
if (stock == UNINITIALIZED_STOCK) {
// 获取初DB库存
final Long initStock = stockCallback.getDataBaseStock(key);
if (initStock != null && initStock >= 0) {
// 将库存设置到redis
redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
// 调整库存的操作
stock = this.adjustStock(key, num);
}
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
redisLock.unlock();
}
}
return stock;
}
/**
* 获取库存
*
* @param key 库存key
* @return -1:不限库存; 大于等于0:剩余库存
*/
public Long getStock(String key, long expire, IStockCallback stockCallback) {
if (StringUtils.isEmpty(key)) {
key = "";
}
if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
key = REDIS_STOCK_KEY_PREFIX + key;
}
Integer stockCache = (Integer) redisTemplate.opsForValue().get(key);
Long stock = null;
if (stockCache == null || stockCache.longValue() == UNINITIALIZED_STOCK) {
RedisLock redisLock = new RedisLock(redisTemplate, key);
try {
// 获取锁
if (redisLock.tryLock()) {
if (stockCache == null || stockCache.longValue() == UNINITIALIZED_STOCK) {
// 获取DB库存
final Long initStock = stockCallback.getDataBaseStock(key);
if (initStock != null && initStock >= 0) {
// 将库存设置到redis
redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
}
stock = initStock;
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
redisLock.unlock();
}
} else {
stock = stockCache.longValue();
}
return stock;
}
public void setRedisKeyValue(String key, Object stock) {
if (StringUtils.isEmpty(key)) {
key = "";
}
if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
key = REDIS_STOCK_KEY_PREFIX + key;
}
redisTemplate.opsForValue().set(key, stock, 60 * 60, TimeUnit.SECONDS);
}
/**
* 扣库存
*
* @param key 库存key
* @param num 扣减库存数量
* @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】
*/
private Long adjustStock(String key, int num) {
if (StringUtils.isEmpty(key)) {
key = "";
}
if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
key = REDIS_STOCK_KEY_PREFIX + key;
}
// 脚本里的KEYS参数
List<String> keys = new ArrayList<>();
keys.add(key);
// 脚本里的ARGV参数
List<String> args = new ArrayList<>();
args.add(Integer.toString(num));
long result = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
}
return UNINITIALIZED_STOCK;
}
});
return result;
}
}
getStock
获取库存的时候,如果是首次获取,库存并没存在缓存中,或者库存尚未初始化时,通过redis锁防止分布式场景中并发操作,DB获取库存放到redis中。
adjustStock
如果调整库存失败,将会通过回调函数stockCallback
,初始化当前库存,并且再将库存设置到redis中。
public interface IStockCallback {
/**
* 获取库存
* @return
*/
Long getDataBaseStock(String sku);
}
分布式环境下,我们需要一个分布式锁来控制只能有一个服务去初始化库存,因此在对库存进行操作的时候,优先判断redis是否获取到了锁tryLock
/**
* Redis分布式锁
* 使用 SET resource-name anystring NX EX max-lock-time 实现
* EX seconds — 以秒为单位设置 key 的过期时间;
* PX milliseconds — 以毫秒为单位设置 key 的过期时间;
* NX — 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
* XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。
*
* 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
*
* 客户端执行以上的命令:
*
* 如果服务器返回 OK ,那么这个客户端获得锁。
* 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
*
*/
@Slf4j
public class RedisLock {
private static Logger logger = LoggerFactory.getLogger(RedisLock.class);
private RedisTemplate<String, Object> redisTemplate;
/**
* 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
*/
public static final String NX = "NX";
/**
* seconds — 以秒为单位设置 key 的过期时间,等效于EXPIRE key seconds
*/
public static final String EX = "EX";
/**
* 调用set后的返回值
*/
public static final String OK = "OK";
/**
* 默认请求锁的超时时间(ms 毫秒)
*/
private static final long TIME_OUT = 100;
/**
* 默认锁的有效时间(s)
*/
public static final int EXPIRE = 60;
/**
* 解锁的lua脚本
*/
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
log.debug("init UNLOCK_LUA:{}", UNLOCK_LUA);
}
/**
* 锁标志对应的key
*/
private String lockKey;
/**
* 记录到日志的锁标志对应的key
*/
private String lockKeyLog = "";
/**
* 锁对应的值
*/
private String lockValue;
/**
* 锁的有效时间(s)
*/
private int expireTime = EXPIRE;
/**
* 请求锁的超时时间(ms)
*/
private long timeOut = TIME_OUT;
/**
* 锁标记
*/
private volatile boolean locked = false;
final Random random = new Random();
/**
* 使用默认的锁过期时间和请求锁的超时时间
*
* @param redisTemplate
* @param lockKey 锁的key(Redis的Key)
*/
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey + "_lock";
}
/**
* 使用默认的请求锁的超时时间,指定锁的过期时间
*
* @param redisTemplate
* @param lockKey 锁的key(Redis的Key)
* @param expireTime 锁的过期时间(单位:秒)
*/
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int expireTime) {
this(redisTemplate, lockKey);
this.expireTime = expireTime;
}
/**
* 使用默认的锁的过期时间,指定请求锁的超时时间
*
* @param redisTemplate
* @param lockKey 锁的key(Redis的Key)
* @param timeOut 请求锁的超时时间(单位:毫秒)
*/
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, long timeOut) {
this(redisTemplate, lockKey);
this.timeOut = timeOut;
}
/**
* 锁的过期时间和请求锁的超时时间都是用指定的值
*
* @param redisTemplate
* @param lockKey 锁的key(Redis的Key)
* @param expireTime 锁的过期时间(单位:秒)
* @param timeOut 请求锁的超时时间(单位:毫秒)
*/
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int expireTime, long timeOut) {
this(redisTemplate, lockKey, expireTime);
this.timeOut = timeOut;
}
/**
* 自旋锁
* 尝试获取锁 超时返回
*
* @return
*/
public boolean tryLock() {
// 生成随机key
lockValue = UUID.randomUUID().toString();
// 请求锁超时时间,纳秒
long timeout = timeOut * 1000000;
// 系统当前时间,纳秒
long nowTime = System.nanoTime();
while ((System.nanoTime() - nowTime) < timeout) {
if (OK.equalsIgnoreCase(this.set(lockKey, lockValue, expireTime))) {
locked = true;
// 上锁成功结束请求
log.debug("获取redis锁:key{},value:{},expireTime:{}", lockKey, lockValue, expireTime);
return locked;
}
// 每次请求等待一段时间
seleep(10, 50000);
}
return locked;
}
/**
* 尝试获取锁 立即返回
*
* @return 是否成功获得锁
*/
public boolean lock() {
lockValue = UUID.randomUUID().toString();
//不存在则添加 且设置过期时间(单位ms)
String result = set(lockKey, lockValue, expireTime);
locked = OK.equalsIgnoreCase(result);
return locked;
}
/**
* 以阻塞方式的获取锁
*
* @return 是否成功获得锁
*/
public boolean lockBlock() {
lockValue = UUID.randomUUID().toString();
while (true) {
//不存在则添加 且设置过期时间(单位ms)
String result = set(lockKey, lockValue, expireTime);
if (OK.equalsIgnoreCase(result)) {
locked = true;
return locked;
}
// 每次请求等待一段时间
seleep(10, 50000);
}
}
/**
* 解锁
*
* 可以通过以下修改,让这个锁实现更健壮:
*
* 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
* 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
* 这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。
*/
public Boolean unlock() {
// 只有加锁成功并且锁还有效才去释放锁
// 只有加锁成功并且锁还有效才去释放锁
if (locked) {
return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
Long result = 0L;
List<String> keys = new ArrayList<>();
keys.add(lockKey);
List<String> values = new ArrayList<>();
values.add(lockValue);
// 集群模式
if (nativeConnection instanceof JedisCluster) {
result = (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values);
}
// 单机模式
if (nativeConnection instanceof Jedis) {
result = (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values);
}
if (result == 0 && !StringUtils.isEmpty(lockKeyLog)) {
logger.info("Redis分布式锁,解锁{}失败!解锁时间:{}", lockKeyLog, System.currentTimeMillis());
}
locked = result == 0;
return result == 1;
}
});
}
return true;
}
/**
* 获取锁状态
*
* @return
* @Title: isLock
*/
public boolean isLock() {
return locked;
}
/**
* 重写redisTemplate的set方法
*
* 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
*
* 客户端执行以上的命令:
*
* 如果服务器返回 OK ,那么这个客户端获得锁。
* 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
*
* @param key 锁的Key
* @param value 锁里面的值
* @param seconds 过去时间(秒)
* @return
*/
private String set(final String key, final String value, final long seconds) {
Assert.isTrue(!StringUtils.isEmpty(key), "key不能为空");
return (String) redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
String result = null;
if (nativeConnection instanceof JedisCommands) {
result = ((JedisCommands) nativeConnection).set(key, value, NX, EX, seconds);
}
if (!StringUtils.isEmpty(lockKeyLog) && !StringUtils.isEmpty(result)) {
logger.info("获取锁{}的时间:{}", lockKeyLog, System.currentTimeMillis());
}
return result;
}
});
}
/**
* @param millis 毫秒
* @param nanos 纳秒
* @Title: seleep
* @Description: 线程等待时间
*/
private void seleep(long millis, int nanos) {
try {
Thread.sleep(millis, random.nextInt(nanos));
} catch (InterruptedException e) {
logger.info("获取分布式锁休眠被中断:", e);
}
}
public String getLockKeyLog() {
return lockKeyLog;
}
public void setLockKeyLog(String lockKeyLog) {
this.lockKeyLog = lockKeyLog;
}
public int getExpireTime() {
return expireTime;
}
public void setExpireTime(int expireTime) {
this.expireTime = expireTime;
}
public long getTimeOut() {
return timeOut;
}
public void setTimeOut(long timeOut) {
this.timeOut = timeOut;
}
}
这样就大功告成了吗?其实细心的同学已经发现了在我们代码中有这么一段Integer stockCache = (Integer) redisTemplate.opsForValue().get(key);
,那是因为我在向redis放库存的时候存放的是Long类型,所以在这里需要强转一下,使用RedisTemplate
的时候默认的序列化反序列方式为JdkSerializationRedisSerializer
,如果我们只是存一下参数倒还无所谓,如果存的是序列化的对象,那么反序列化拿到的key就不再是我们需要的key了。当然在这个项目中,如果我们约定了redis,key的类型是Integer,那么get的时候就不需要转换。但是我们在set的时候是用的Long类型,那么通过Integer强转的时候,肯定会报转换错误。所以需要我们自定义一下redis序列化方式,指定redis 的key和value使用什么类型。
自定义序列化方式
@Configuration
public class RedisConfig {
/**
* 重写Redis序列化方式,使用Json方式:
* 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。
* Spring Data JPA为我们提供了下面的Serializer:
* GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
* 在此我们将自己配置RedisTemplate并定义Serializer。
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 设置值(value)的序列化采用Jackson2JsonRedisSerializer。
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 设置键(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
ok。这样在redis放值取值时,如果类型不一致,进行转换的时候就不会出错了。
当然这仅仅是商城端扣减库存的操作,在上边的代码中我们并没有太多的针对库存系统(类似WMS)同步的问题,只是在调整的时候通过MQ将商城端的库存同步过去了。如果MQ失败了会怎样,岂不是从一开始就错下去了。
其实在实际项目中,这种这种商城端库存推送我们一般称之为“增量库存”,增量库存失败的情况下,系统自动调用同步全量库存。同时为了尽可能保证完全商城商品库存和仓库商品库存同步,一般需要系统定时的去获取仓库的全量库存,来尽可能保证仓库库存与商城库存一致。
之前看过一篇关于中台程序的电商更新文档,里边一些业务实现方案还是可以参考一下的:比如这个增量同步库存扣减失败系统库存同步处理