1. 什麼是鎖
在一個進程中,當存在多個線程同時操作某個共享資源時,就需要對共享資源做同步,使其在修改這個共享資源時能夠線性地執行操作。而實現同步的手段就是鎖,當線程準備對共享資源做修改前,先獲取鎖,如果當前共享資源已經被鎖,則進行等待;若沒有被鎖,則成功獲取鎖,並允許執行修改。在修改完畢後,釋放鎖資源,那麼下一個線程也是如此,只有獲取到鎖,才能對共享資源修改。
2. 什麼是分佈式鎖
分佈式鎖是爲了防止分佈式系統中多個進程之間相互干擾,從而需要一種分佈式協調技術來對這些進程進行調度。
3. Redis實現分佈式鎖
實現分佈式鎖的手段有很多,比如基於數據庫的悲觀鎖和樂觀鎖、基於Redis做分佈式鎖、基於Zookeeper做分佈式鎖等等。之前,我們把Redis當做緩存系統使用,今天我們再來看看如何使用Redis實現分佈式鎖。
Redis實現分佈式鎖需要用到其中的兩個方法:setnx()
和expire()
。
- setnx()
setnx
的含義就是SET if Not Exists
,該方法是原子的,如果key不存在,則設置當前key成功,返回1;如果當前key已經存在,則設置當前key失敗,返回0。
- expire()
expire
設置過期時間,要注意的是setnx
命令不能設置key的超時時間,只能通過expire
來對key設置。
3.1 實現步驟
setnx(key, value)
如果返回0,則說明當前資源已經被鎖,需要等待;如果返回1,則說明成功獲取鎖;expire()
命令對key設置超時時間,避免死鎖;- 執行業務代碼後,通過
delete
命令刪除key。
需要注意的是:從解決日常工作中的需求來說,該方案已經夠用。但從技術角度來說,該方案還不夠嚴謹。比如在第一步設置
setnx
執行成功後,expire
命令執行前出現了異常,那麼就還是會出現死鎖問題。但是我們這個項目中暫時不考慮這種異常情況了,在後面可能會有一個專門討論分佈式鎖
的專題博客,可以持續關注下。
3.2 業務背景
以商品微服務下的商品入庫單業務爲背景,商品入庫單存在未生效
和已生效
兩種業務狀態,當錄入一個商品入庫單保存成功之後,該商品入庫單爲未生效
狀態,此時商品還沒有入庫。只有在執行生效這個業務動作時,商品入庫單從未生效
變成已生效
狀態,然後再對商品的庫存進行增加。從業務角度上來說,一個商品入庫單是不允許重複生效的,所以在這裏就需要對生效的商品入庫單進行加鎖,避免分佈式環境下重複生效。
3.3 代碼實現
在com.autumn.mall.commons.utils
包結構中,有一個RedisUtils
工具類,用來對Redis常規操作做了簡單的封裝,其中我們也對獲取分佈式鎖的操作進行了封裝:
public boolean tryLock(String key) {
return tryLock(key, null);
}
public boolean tryLock(String key, String value) {
return tryLock(key, value, 3, TimeUnit.MINUTES);
}
public boolean tryLock(String key, String value, int timeout, TimeUnit timeUnit) {
try {
if (StringUtils.isBlank(value)) {
long currTime = System.currentTimeMillis();
// 加鎖成功
return redisTemplate.opsForValue().setIfAbsent(key, currTime);
}
return redisTemplate.opsForValue().setIfAbsent(key, value);
} finally {
redisTemplate.expire(key, timeout, timeUnit);
}
}
商品入庫單(GoodsInboundServiceImpl
)生效方法:
public void doEffect(String uuid) {
// 獲取分佈式鎖
try {
while (redisUtils.tryLock(getLockKeyPrefix() + uuid) == false) {
TimeUnit.SECONDS.sleep(3);
}
} catch (Exception e) {
MallExceptionCast.cast(CommonsResultCode.TRY_LOCKED_ERROR);
}
Optional<GoodsInbound> optional = goodsInboundRepository.findById(uuid);
if (optional.isPresent() == false) {
MallExceptionCast.cast(CommonsResultCode.ENTITY_IS_NOT_EXIST);
}
if (optional.get().getState().equals(BizState.effect)) {
MallExceptionCast.cast(ProductResultCode.ENTITY_IS_EQUALS_TARGET_STATE);
}
// 生效入庫單
GoodsInbound entity = optional.get();
entity.setState(BizState.effect);
getRepository().save(entity);
// 商品入庫
List<GoodsInboundDetail> details = goodsInboundDetailRepository.findAllByGoodsInboundUuidOrderByLineNumber(entity.getUuid());
List<Stock> stocks = new ArrayList<>();
details.stream().forEach(detail -> {
Stock stock = new Stock();
stock.setEntityKey(MallModuleKeyPrefixes.PRODUCT_KEY_PREFIX_OF_GOODS + detail.getGoodsUuid());
stock.setWarehouse(entity.getWarehouse());
stock.setQuantity(detail.getQuantity());
stocks.add(stock);
});
ResponseResult responseResult = stockClient.inbound(stocks);
// 刪除分佈式鎖
redisUtils.remove(getLockKeyPrefix() + uuid);
}