redis實現分散式鎖---實操---問題解決

Ziwei Xu發表於2020-12-08

專案有一個留痕的操作,考慮到併發以及生產環境是多伺服器的情況,決定使用分散式鎖來保證併發下的正確性,由於是第一次做,除了一些問題,來記錄一下:

【本人經驗不足,如有錯誤,希望得到大神的指教】

專案的競態問題是:
同一使用者/同一股票資訊,都只記錄一次,並且要拿到使用者和股票的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());
    }
}

相關文章