需求背景
在開發的收入結轉平台界面
上有一個歸集
按鈕,可以實現抓取結轉表里面的多條數據進行歸集操作。為了防止多人多電腦同時操作一條數據,我們自己開發了一個簡單的基於Redis實現的分布式鎖。
代碼實現
邏輯代碼中的使用案例
參數說明:
scIds
:結轉數據的ID主鍵集合。
timeOutToDeleteRedisKey
:最大鎖超時時間(用於自動解鎖)
organizationId
:租戶ID(這個參數根據情況選擇是否需要)
ReturnLock returnLock = RedisLock.applyByIds(scIds, redisLockTime, organizationId, () -> {
// dothing 具體的代碼業務邏輯
return 123;
});
if (!returnLock.getFlag()) {
if(returnLock.getLock()){
throw new CommonException("歸集數據有程序正在運行,請稍候刷新頁面重試");
}else {
throw new CommonException(returnLock.getErrorMsg());
}
}
// 返回對象
System.out.println(returnLock.getResObj()) // 123
Redis加鎖方法封裝
public static ReturnLock applyByIds(List<Long> scIds, Long timeOutToDeleteRedisKey, Long tenantId,
Supplier<Object> supplier) {
// 獲得鎖
Map<String, String> keyMap = getLockByIds(scIds, timeOutToDeleteRedisKey, tenantId);
ReturnLock returnLock = new ReturnLock();
returnLock.setFlag(true);
returnLock.setLock(false);
try {
// 判斷主鍵ID數量和加鎖的數量是否一致,不一致說明有加鎖失敗的數據,返回失敗鎖信息
if(scIds.size() > keyMap.size()){
returnLock.setFlag(false);
returnLock.setLock(true);
returnLock.setKeyMap(keyMap);
return returnLock;
}
// 應用代碼執行后的返回結果 supplier:java8四大內置函數的供給型接口
// Supplier<T>(供給型接口)無參數,返回類型為T的對象:T get()
returnLock.setResObj(supplier.get());
}catch (Exception e) {
returnLock.setFlag(false);
returnLock.setErrorMsg(e.getMessage());
e.printStackTrace();
}finally {
// 應用代碼執行報錯,解鎖
unLockByIds(keyMap);
}
return returnLock;
}
getLockByIds
批量獲取每一條數據的Redis鎖
private static Map<String, String> getLockByIds(List<Long> scIds, Long timeOutToDeleteRedisKey, Long tenantId) {
Map<String, String> keyMap = new HashMap<>();
// 從spring上下文中獲取Redis的操作對象,因為這個代碼是寫在util中,所以通過上下文方式獲取bean對象
// RedisHelper:Redis的操作對象,我們自己公司基於redisTemplate封裝的
RedisHelper redisHelper = SpringUtil.getBean(RedisHelper.class);
try {
if (CollectionUtil.isNotEmpty(scIds)) {
for (int i = 0; i < scIds.size(); i++) {
String item = "L_" + scIds.get(i);
String UUID = UUIDUtils.generateTenantUUID(tenantId);
// 判斷key值是否被鎖,如果沒有鎖則加鎖並設置過期時間, 如果有鎖, 則報錯並立即釋放已加的鎖
// strSetIfAbsent會返回true/false,底層是封裝了java的setIfAbsent方法和Redis的setnx方法
// 如果設置成功返回true否則false
if (redisHelper.strSetIfAbsent(item, UUID)) {
// 設置過期時間
redisHelper.setExpire(item, timeOutToDeleteRedisKey, TimeUnit.SECONDS);
// 保存設置的鎖信息
keyMap.put(item, UUID);
} else {
// 鎖設置異常,表示有數據被鎖住了,解鎖之間加鎖的數據
if (MapUtil.isNotEmpty(keyMap)) {
if (unLockByIds(keyMap)) {
// 解鎖失敗再次解鎖
unLockByIds(keyMap);
}
}
// 返回鎖信息
return keyMap;
}
}
}
return keyMap;
}catch (Exception e) {
e.printStackTrace();
return keyMap;
}
}
解鎖
private static Boolean unLockByIds(Map<String, String> keyMap) {
RedisHelper redisHelper = SpringUtil.getBean(RedisHelper.class);
try {
if (MapUtil.isEmpty(keyMap)) {
return false;
}
// 判斷是否是自己的鎖
for (String key : keyMap.keySet()) {
String uuid = keyMap.get(key);
if (StringUtils.equals(uuid, redisHelper.strGet(key))) {
// 封裝了redisTemplate.delete(key);
redisHelper.delKey(key);
}
}
return true;
}catch (Exception e) {
e.printStackTrace();
return false;
}
}
實現效果
當對數據進行批量加鎖的時候,若在加鎖的過程中出現加鎖失敗,則回滾直接加的鎖,並提示"歸集數據有程序正在運行,請稍候刷新頁面重試"。
若業務代碼邏輯執行成功或者執行報錯都會自動的解鎖當前加鎖的數據。