项目总结59:Redis分布式锁解决电商订单库存并发问题
在电商分布式项目中,需要考虑提交订单时,因为并发的原因导致库存异常的情况。
其中一个解决方案是:使用redis锁,因为Redis是单线程的,即线程安全的;在提交订单的时候,先通过Redis锁进行库存判断,如果库存校验通过,则正常提交顶顶那,否则返回失败。
具体逻辑如下:
1- 用户请求提交订单接口,接口内先通过Redis锁进行库存校验(如果第一次获取锁失败,则会继续请求锁,但不超过5次);
2- Redis锁进行库存校验,从订单层面具有排他性(即一个订单在进行Redis锁库存校验时),其它提交的订单只能等待。
3- 且Redis锁进行库存校验,做两件事:(1)进行Redis库存校验,如果库存不够,则返回false;否则继续(2);(2)进行Redis减库存操作。
4-Redis锁进行库存校验通过后,订单信息被正常提交。
具体代码如下
@Autowired private CommonRedisHelper commonRedisHelper; public final static String PREFIX_LOCK_ORDER_SUBMIT = "lock_orderSubmit"; //校验并更新库存 public Boolean updateStockToRedis(List<Long> cartIdList){ boolean lock = commonRedisHelper.lock(PREFIX_LOCK_ORDER_SUBMIT); if(lock){ //更新redis中sku的库存 //代码略... reduceMultiSkuStock(cartIdList) //删除锁 commonRedisHelper.delete(PREFIX_LOCK_ORDER_SUBMIT); return true; }else{ // 设置失败次数计数器, 当到达5次时, 返回失败 int failCount = 1; while(failCount <= 5){ // 等待100ms重试 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (commonRedisHelper.lock(PREFIX_LOCK_ORDER_SUBMIT)){ // 执行逻辑操作 //更新redis中sku的库存 //代码略... reduceMultiSkuStock(cartIdList) //删除锁 commonRedisHelper.delete(PREFIX_LOCK_ORDER_SUBMIT); return true; }else{ failCount ++; } } return false; } } //判断并更新某个SKU库存 private boolean reduceMultiSkuStock(List<Long> cartIdList){ //2-判断秒杀商品SKU是否足够 //代码略... //2-更新秒杀商品的库存 //代码略... }
CommonRedisHelper 类
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.Objects; @Component public class CommonRedisHelper { //锁名称 public static final String LOCK_PREFIX = "redis_lock"; //加锁失效时间,毫秒 public static final int LOCK_EXPIRE = 300; // ms @Autowired RedisTemplate redisTemplate; /** * 最终加强分布式锁 * * @param key key值 * @return 是否获取到 */ public boolean lock(String key){ String lock = LOCK_PREFIX + key; // 利用lambda表达式 return (Boolean) redisTemplate.execute((RedisCallback) connection -> { //当前锁的过期时间 long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1; //当锁不存在时,设置锁,key为锁名称,value为过期时间 Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes()); if (acquire) { //如果设置成功,则返回true return true; } else { //如果锁没有设置成功 //获取已经存在的锁的value(即已经存在的锁的过期时间) byte[] value = connection.get(lock.getBytes()); //当已经存在的旧锁的过期时间存在时 if (Objects.nonNull(value) && value.length > 0) { long expireTime = Long.parseLong(new String(value)); // 如果旧锁已经过期,则重新加锁 if (expireTime < System.currentTimeMillis()) { // 重新强制加锁,防止死锁,并返回旧锁的过期时间 byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE + 1).getBytes()); //判断:如果旧锁已经过期,则返回true,否则返回false return Long.parseLong(new String(oldValue)) < System.currentTimeMillis(); } } } return false; }); } /** * 删除锁 * * @param key */ public void delete(String key) { String lock = LOCK_PREFIX + key; redisTemplate.delete(lock); } }