redis實現分散式鎖---實操---問題解決
專案有一個留痕的操作,考慮到併發以及生產環境是多伺服器的情況,決定使用分散式鎖來保證併發下的正確性,由於是第一次做,除了一些問題,來記錄一下:
【本人經驗不足,如有錯誤,希望得到大神的指教】
專案的競態問題是:
同一使用者/同一股票資訊,都只記錄一次,並且要拿到使用者和股票的id來記錄流水錶
就涉及到先判斷後插入這種競態操作
第一版的思路是:
使用者進來後先去查一次資料庫,
-
如果為空,就去獲取鎖,
- 如果獲取到鎖,就執行插入操作,插入完成後就釋放鎖;(獲取鎖就是插入一個代表當前使用者的value值,釋放就是刪除掉這個值)
- 如果沒有獲取到鎖就去get,如果get不到了對應的value,就說明插入操作已經完成,就再查一次資料庫,獲取id
-
如果不為空,說明資料庫中有這條資料,就直接能夠拿到id
第一版的問題:記錄流水會有遺漏
發現是duplicate-key的原因(插入資料庫的時候,唯一索引),這說明鎖是不成功的
我的猜想是在這個位置:
tempUser = daxinUserMapper.selectOne(user);
if (tempUser == null) {
if (lock(accountLockKey)) {
插入操作
釋放鎖
}
如果同一個使用者,同一時間進來了兩次,兩個tempUser都是null,然後A執行緒很快,馬上獲取到鎖了,插入然後就釋放了,然後B執行緒這時獲取鎖是能夠獲取到的,然後B執行緒又進行了一次插入操作,就報了這個錯(檢視日誌也是這個樣子)
第二版的思路是:
使用者進來後,直接去redis(hash的結構)中,嘗試獲取鎖,也就是進行一個putIfAbsent,給一個“lock”的標誌位的操作
- 如果put成功,說明使用者是第一次來,那麼就進行插入操作,並且插入後將id寫到對應key的value中,並且不釋放
- 如果put不成功,嘗試去get,獲取對應key的value的值,
- 如果值是lock,說明有人在操作,並且還沒操作完,就過一會再來get,直到get到id的值
- 如果值不是lock,說明獲得的是id,那就直接拿到了(redis是單執行緒,所以不會出問題)
第一版的程式碼是:
@Service
@Slf4j
public class StockFlowService {
@Resource
private DaxinUserMapper daxinUserMapper;
@Resource
private DaxinStockMapper daxinStockMapper;
@Resource
private DaxinStockFlowMapper daxinStockFlowMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource(name = StackFlowStream.FLOW_SAVE_SEND)
private MessageChannel flowSaveQueue;
private static final String ACCOUNT_LOCK_KEY_TEMPLATE = "editorial:newstock:fundaccount:%s:value";
private static final String STOCK_LOCK_KEY_TEMPLATE = "editorial:newstock:stock:%s:value";
/**
* 驗證手機號後 啟用
* <p>
* 消費者
*
* @see StockFlowService#saveFlow(UserFlowVo)
*/
public void store(UserFlowVo userFlowVo) {
flowSaveQueue.send(MessageBuilder.withPayload(userFlowVo).build());
log.info("send to save userFlow :{}", userFlowVo);
}
private Boolean lock(String key) {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(
key, "lock", 20, TimeUnit.SECONDS);
return lock;
}
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
private void retryLockExist(int count, String lockKey) {
try {
while (count-- > 0) {
Thread.sleep(1000);
log.info("retry redis lock {}({})", lockKey, count + 1);
if (!stringRedisTemplate.hasKey(lockKey)) {
break;
}
}
} catch (InterruptedException e) {
}
}
/**
* 啟用非同步處理
* <p>
* 生產者
*
* @see com.glsc.editorial.stockflow.controller.StockFlowController#store(UserFlowVo)
*/
public void saveFlow(UserFlowVo userFlowVo) {
log.info("enter saveFlow:{}", userFlowVo);
// 1. 存入使用者資訊
log.info("處理使用者資訊...");
StopWatch stopWatch = new StopWatch("store");
stopWatch.start("user..");
User user = User.builder().fundAccount(userFlowVo.getFundAccount())
.assetProp(userFlowVo.getAssetProp())
.build();
Integer uid;
User tempUser = null;
String accountLockKey
= String.format(StockFlowService.ACCOUNT_LOCK_KEY_TEMPLATE, userFlowVo.getFundAccount() + "|" + userFlowVo.getAssetProp());
tempUser = daxinUserMapper.selectOne(user);
if (tempUser == null) {
if (lock(accountLockKey)) {
log.info("獲取到鎖:{}", user.getFundAccount());
try {
log.info("運算元據庫,存入使用者資訊:{}", user);
daxinUserMapper.insert(user);
uid = user.getId();
} finally {
log.info("釋放鎖");
unLock(accountLockKey);
}
} else {
log.info("未獲取到鎖,開始等待:{}", user.getFundAccount());
retryLockExist(3, accountLockKey);
tempUser = daxinUserMapper.selectOne(user);
uid = tempUser.getId();
}
} else {
log.info("已有使用者資訊,獲取使用者id:{}", tempUser.getId());
uid = tempUser.getId();
}
stopWatch.stop();
// 2. 存入股票資訊
log.info("處理股票資訊...");
LocalDateTime now = LocalDateTime.now();
String sign = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
.toString() + "_" + userFlowVo.getFundAccount();
stopWatch.start("stock..");
List<Stock> stocks = userFlowVo.getStockFlowvos().stream().map(e -> {
Stock stock = CopyMapping.INSTANCE.stockFlow2Stock(e);
String stockLockKey =
String.format(StockFlowService.STOCK_LOCK_KEY_TEMPLATE, e.getStockCode() + "|" + e.getStockType());
Stock modelStock = Stock.builder().stockCode(e.getStockCode())
.stockType(e.getStockType())
.build();
Stock tempStock = null;
tempStock = daxinStockMapper.selectOne(modelStock);
if (tempStock == null) {
if (lock(stockLockKey)) {
log.info("獲取到鎖:{}", e.getStockCode() + "|" + e.getStockType());
try {
// 同一股票資訊只存一次
log.info("存入股票資訊:{}", stock);
daxinStockMapper.insert(stock);
} finally {
log.info("釋放鎖");
unLock(stockLockKey);
}
} else {
log.info("未獲取到鎖,開始等待:{}", stock.getStockCode());
retryLockExist(3, stockLockKey);
tempStock = daxinStockMapper.selectOne(modelStock);
stock.setId(tempStock.getId());
}
} else {
log.info("已有股票資訊,獲取股票id:{}", tempStock.getId());
stock.setId(tempStock.getId());
}
return stock;
}).collect(Collectors.toList());
stopWatch.stop();
// 存入流水資訊
log.info("處理流水資訊...");
stopWatch.start("insert flow");
Map<String, StockFlowVo> stockFlowVo = userFlowVo.getStockFlowvos().stream().collect(Collectors.toMap(e -> e.getStockCode() + "|" + e.getStockType(), Function.identity()));
List<StockFlow> collect = stocks.stream().map(stock -> {
StockFlowVo vo = stockFlowVo.get(stock.getStockCode() + "|" + stock.getStockType());
return StockFlow.builder().stockId(stock.getId())
.userId(uid)
.gmtCreate(now)
.isSuccess(vo.getIsSuccess())
.actualCount(vo.getActualCount())
.sign(sign)
.remark(vo.getRemark())
.hasRight(vo.getHasRight())
.build();
}).collect(Collectors.toList());
log.info("存入流水資訊{}", JSON.toJSONString(collect));
daxinStockFlowMapper.insertList(collect);
stopWatch.stop();
log.info(stopWatch.prettyPrint());
}
}
第二版的程式碼是:
service
@Service
@Slf4j
public class StockFlowService {
@Resource
private DaxinUserMapper daxinUserMapper;
@Resource
private DaxinStockMapper daxinStockMapper;
@Resource
private DaxinStockFlowMapper daxinStockFlowMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource(name = SFSource.FLOW_SAVE_SEND)
private MessageChannel flowSaveQueue;
private static final String ACCOUNT_LOCK_KEY_TEMPLATE = "editorial:newstock:fundaccount:hash";
private static final String STOCK_LOCK_KEY_TEMPLATE = "editorial:newstock:stock:hash";
/**
* 驗證手機號後 啟用
* <p>
* 消費者
*
* @see StockFlowService#saveFlow(UserFlowVo)
*/
public void store(UserFlowVo userFlowVo) {
flowSaveQueue.send(MessageBuilder.withPayload(userFlowVo).build());
log.info("send to save userFlow :{}", userFlowVo);
}
private Boolean lock(String key, String hashkey) {
Boolean lock = stringRedisTemplate.opsForHash().putIfAbsent(key, hashkey, "lock");
return lock;
}
private Integer getId(String key, String hashKey) {
while ("lock".equals(stringRedisTemplate.opsForHash().get(key, hashKey))) {
try {
Thread.sleep(1000);
log.info("retry get uid {}", hashKey);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return Integer.parseInt(String.valueOf(stringRedisTemplate.opsForHash().get(key, hashKey)));
}
/**
* 啟用非同步處理
* <p>
* 生產者
*
* @see com.glsc.editorial.stockflow.controller.StockFlowController#store(UserFlowVo)
*/
public void saveFlow(UserFlowVo userFlowVo) {
log.info("enter saveFlow:{}", userFlowVo);
// 1. 存入使用者資訊
log.info("處理使用者資訊...");
StopWatch stopWatch = new StopWatch("store");
stopWatch.start("user..");
User user = User.builder().fundAccount(userFlowVo.getFundAccount())
.assetProp(userFlowVo.getAssetProp())
.build();
Integer uid;
String accountLockKey = userFlowVo.getFundAccount() + "|" + userFlowVo.getAssetProp();
if (lock(ACCOUNT_LOCK_KEY_TEMPLATE,accountLockKey)){
log.info("獲取到鎖:{}", accountLockKey);
log.info("運算元據庫,存入使用者資訊:{}", user);
daxinUserMapper.insert(user);
uid = user.getId();
log.info("更新redis:{}", uid);
stringRedisTemplate.opsForHash().put(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey, uid.toString());
}else {
if ("lock".equals(stringRedisTemplate.opsForHash().get(ACCOUNT_LOCK_KEY_TEMPLATE,accountLockKey))){
log.info("未獲取到鎖:{}", accountLockKey);
log.info("等待,直到獲取...");
uid = getId(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey);
}else {
log.info("已有使用者資訊,獲取使用者id:{}", accountLockKey);
Object o = stringRedisTemplate.opsForHash().get(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey);
uid = Integer.parseInt(String.valueOf(o));
}
}
stopWatch.stop();
// 2. 存入股票資訊
log.info("處理股票資訊...");
LocalDateTime now = LocalDateTime.now();
String sign = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
.toString() + "_" + userFlowVo.getFundAccount();
stopWatch.start("stock..");
List<Stock> stocks = userFlowVo.getStockFlowvos().stream().map(e -> {
Stock stock = CopyMapping.INSTANCE.stockFlow2Stock(e);
String stockLockKey = e.getStockCode() + "|" + e.getStockType();
Stock modelStock = Stock.builder().stockCode(e.getStockCode())
.stockType(e.getStockType())
.build();
if (lock(STOCK_LOCK_KEY_TEMPLATE, stockLockKey)){
log.info("獲取到鎖:{}", stockLockKey);
log.info("運算元據庫,存入股票資訊:{}", modelStock);
daxinStockMapper.insert(stock);
log.info("更新redis:{}", stock.getId());
stringRedisTemplate.opsForHash().put(STOCK_LOCK_KEY_TEMPLATE, stockLockKey, stock.getId().toString());
}else {
if ("lock".equals(stringRedisTemplate.opsForHash().get(STOCK_LOCK_KEY_TEMPLATE,accountLockKey))){
log.info("未獲取到鎖:{}", stockLockKey);
log.info("等待,直到獲取id...");
stock.setId(getId(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey));
}else {
log.info("已有使用者資訊,獲取使用者id:{}", accountLockKey);
Object o = stringRedisTemplate.opsForHash().get(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey);
stock.setId(Integer.parseInt(String.valueOf(o)));
}
}
return stock;
}).collect(Collectors.toList());
stopWatch.stop();
// 存入流水資訊
log.info("處理流水資訊...");
stopWatch.start("insert flow");
Map<String, StockFlowVo> stockFlowVo = userFlowVo.getStockFlowvos().stream().collect(Collectors.toMap(e -> e.getStockCode() + "|" + e.getStockType(), Function.identity()));
List<StockFlow> collect = stocks.stream().map(stock -> {
StockFlowVo vo = stockFlowVo.get(stock.getStockCode() + "|" + stock.getStockType());
return StockFlow.builder().stockId(stock.getId())
.userId(uid)
.gmtCreate(now)
.isSuccess(vo.getIsSuccess())
.actualCount(vo.getActualCount())
.sign(sign)
.remark(vo.getRemark())
.hasRight(vo.getHasRight())
.build();
}).collect(Collectors.toList());
log.info("存入流水資訊{}", JSON.toJSONString(collect));
daxinStockFlowMapper.insertList(collect);
stopWatch.stop();
log.info(stopWatch.prettyPrint());
}
}
相關文章
- Redis分散式鎖實現Redisson 15問Redis分散式
- 分散式鎖----Redis實現分散式Redis
- Redis實現分散式鎖Redis分散式
- 【Redis】利用 Redis 實現分散式鎖Redis分散式
- 面試題詳解:如何用Redis實現分散式鎖?面試題Redis分散式
- redis分散式鎖的實現Redis分散式
- redis分散式鎖-java實現Redis分散式Java
- Redis之分散式鎖實現Redis分散式
- Redis如何實現分散式鎖Redis分散式
- 分散式鎖之Redis實現分散式Redis
- 利用Redis實現分散式鎖Redis分散式
- redis分散式鎖-SETNX實現Redis分散式
- 分散式鎖實現(一):Redis分散式Redis
- redis分散式鎖的問題和解決Redis分散式
- 如何使用Redis實現分散式鎖Redis分散式
- 如何用 Redis 實現分散式鎖Redis分散式
- redis分散式鎖實現(golang版)Redis分散式Golang
- Redis優雅實現分散式鎖Redis分散式
- 實現一個 Redis 分散式鎖Redis分散式
- 用 Go + Redis 實現分散式鎖GoRedis分散式
- 基於redis實現分散式鎖Redis分散式
- redis分散式鎖-spring boot aop+自定義註解實現分散式鎖Redis分散式Spring Boot
- Java使用Redis實現分散式鎖來防止重複提交問題JavaRedis分散式
- Redis面試系列:Redis實現分散式鎖Redis面試分散式
- Redis專題(3):鎖的基本概念到Redis分散式鎖實現Redis分散式
- 深入理解PHP+Redis實現分散式鎖的相關問題PHPRedis分散式
- 分散式鎖與實現(一)基於Redis實現!分散式Redis
- Redis實現可重入的分散式鎖Redis分散式
- 使用Redis分散式鎖實現主備Redis分散式
- Spring Boot Redis 實現分散式鎖,真香!!Spring BootRedis分散式
- 實現一個redis的分散式鎖Redis分散式
- redis實現分散式鎖天然的缺陷Redis分散式
- Redis分散式鎖的原理和實現Redis分散式
- Redis實現分散式鎖那件事Redis分散式
- 利用Python+Redis實現分散式鎖PythonRedis分散式
- Redis分散式鎖的正解實現方式Redis分散式
- Redis 分散式鎖解決方案Redis分散式
- Redis分散式鎖解決方案Redis分散式