項目總結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); } }
