库存设计:设置锁定库存和总库存,当用户下单未支付时锁定库存,支付成功时释放锁定库存并扣减总库存,当30分钟用户还未支付,此时释放锁定库存不扣减总库存。
一、库存超发原因:
下单流程:
当库存为1时,两个用户同时下单,查询库存时都有库存,都通过了库存数量校验,下单成功库存为-1。
二、解决方案:
(1)悲观锁
实现方式:查询时添加更新锁。
实现原理:使当前线程持有数据库记录行更新锁,其它线程被挂起,直到当前线程执行完释放锁,其他线程才能获取行更新锁,这样就防止了超发现象。
select stock from t_stock where id=1 for update
(2)乐观锁
实现方式:添加版本号。
实现原理:先获取当前版本号,执行完业务流程进行库存更新时,判断当前线程持有的版本号是否与数据库中的版本号相同,如一致则更新库存并增加版本号。乐观锁是一种不会阻塞其它线程并发的机制,它不会使用数据库的锁进行实现
缺陷:库存充足,并发的用户无法下单
解决方法:
乐观锁重入机制:
1)使用时间戳执行重入,比如在100毫秒内重复执行。当因为版本号更行失败后,会尝试重新下单,但是会进行时间戳判断,如果在100毫秒内,就继续,否则就判定失败。
2)限制重复次数执行重入
(3)redis使用lua脚本实现
实现原理:lua语言的原子性
-- 获取当前库存
local stock = tonumber(redis.call('hget', productId, 'stock'))
-- 如果购买量小于库存,返回0
if stock < quantity then return 0 end
-- 减少库存
stock = stock - 1
-- 保存当前库存
redis.call('hset', productId, 'stock', tostring(stock))
-- 返回成功
return 1
使用redis实现代码:
@Autowired
private RedisTemplate redisTemplate;
//lua脚本
String script = "local stock = tonumber(redis.call('hget', 'product', KEYS[1]) \n"
+"if stock < ARGV[1] then return 0 end \n"
+"stock = stock - 1 \n"
+"redis.call('hset', 'product', KEYS[1], tostring(stock)) \n"
+"return 1 \n";
//在缓存Lua脚本后,使用该变量保存Redis返回的32位的SHAl编码,使用它去执行缓存的Lua脚本
String shal = null ;
@Override
public Long deductionStockByRedis(Integer productId, Integer qunatity ) {
Long result = null ;
//获取底层Redis操作对象
Jedis jedis = (Jedis)redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
Integer result = null;
try {
//如果脚本没有加载过,那么进行加载,这样就会返回一个shal编码
if (shal == null )
shal =jedis.scriptLoad(script) ;
//执行脚本,返回结果
List<String> key= new ArrayList<String>();
key.add(productId.toString());
List<String> avge = new ArrayList<String>();
avge.add(qunatity.toString());
result = (Integer)jedis.evalsha(shal, key, avge );
}finally{
//确保jedisl顺利关闭
if (jedis != null && jedis . isConnected (}} {
jedis . close(} ;
}
return result;
}
(4)根据数据库更新时会自动加上行级锁来实现防止超发
<update id="lockStock" parameterType="com.macro.mall.request.CartPromotionItem">
UPDATE pms_sku_stock
SET
lock_stock = CASE id
<foreach collection="cartPromotionItemList" item="promotionItem">
WHEN #{promotionItem.productSkuId} THEN lock_stock + #{promotionItem.quantity} + #{promotionItem.restrictQuantity}
</foreach>
END
WHERE
( (stock -lock_stock) >0 )
AND<![CDATA[(stock-lock_stock) >=]]> CASE id
<foreach collection="cartPromotionItemList" item="promotionItem">
WHEN #{promotionItem.productSkuId} THEN (#{promotionItem.quantity} + #{promotionItem.restrictQuantity})
</foreach>
END
AND id IN
<foreach collection="cartPromotionItemList" item="promotionItem" separator="," open="(" close=")">
#{promotionItem.productSkuId}
</foreach>
</update>