冪等校驗
- 一、作業真實場景 + 常出現場景
- 二、作業解決方案 + 百度解決方案
一、作業真實場景 + 常出現場景
- 真實場景:在一次作業中進行成品出庫創建成品出庫單時,手抖了一下,重復點擊了兩次確定(提交表單),結果很神奇的發現居然產生了兩筆一模一樣的資料(流水號都一樣),當時就很懵逼,稍作思考,想想應該是在同一時刻創建了兩個出庫單,感覺很有意思(因為之前沒有遇到過,寫代碼的時候也沒有考慮到這個問題的發生),后面換了一種方式復現場景:提交表單時多次點擊Enter按鈕,還是會產生多筆重復資料,
- 常見場景:
訂單介面, 不能多次創建訂單
支付介面, 重復支付同一筆訂單只能扣一次錢
支付寶回呼介面, 可能會多次回呼, 必須處理重復回呼
普通表單提交介面, 因為網路超時等原因多次點擊提交, 只能成功一次
等等
二、作業解決方案 + 百度解決方案
- 作業:
(1) 在對應業務介面上加上@Transactional,后面發現不能要(只是為了在遇到錯誤時之前對資料庫操作做一個回滾),因為在做相同資料校驗的時候去資料庫是查不到同比資料的,此時所有代碼沒走完,事務未提交,所以只能拿掉事務,
(2)在對應的業務代碼塊加上synchronized (this) {}對業務代碼進行代碼塊的同步鎖,鎖里面做唯一性校驗(查詢資料庫是否有相同的流水號[流水號是按照英文前綴加上時間戳實作],有的話直接拋例外),后面發現如果在極端條件下這個判斷還是有一定缺陷(時間戳只是精確到秒),
@Override
public JsonData saveOrUpdate(OutboundProductReq outboundProductReq) {
// 出庫單不能為空
if (StringUtils.isBlank(outboundProductReq.getOutboundName())) {
return JsonData.buildResult(BizCodeEnum.STORE_PRODUCT_OUTBOUND_NAME_EMPTY);
}
OutboundProductDO outboundProductDO;
if (outboundProductReq.getId() != null && outboundProductReq.getId() > 0) {
outboundProductDO = outboundProductMapper.selectById(outboundProductReq.getId());
Integer outboundCount = outboundProductDO.getOutboundCount();
// 校驗成品庫存
JsonData data = checkProductStock(outboundProductReq.getProductId(), outboundProductReq.getOutboundCount(), outboundCount);
if (data.getCode() == 0) {
// 修改
if (outboundProductDO.getOutboundState() == 0) {
BeanUtils.copyProperties(outboundProductReq, outboundProductDO);
outboundProductMapper.updateById(outboundProductDO);
// 修改庫存詳情表
updateTODetailProduct(outboundProductDO);
// 修改成品凍結數
StockProductDO stockProductDO = stockProductMapper.selectOne(new QueryWrapper<StockProductDO>().eq("product_id", outboundProductDO.getProductId()));
stockProductDO.setFreezeCount(stockProductDO.getFreezeCount() - outboundCount + outboundProductReq.getOutboundCount());
stockProductMapper.updateById(stockProductDO);
} else {
return JsonData.buildResult(BizCodeEnum.STORE_PRODUCT_NOT_MODIFY);
}
} else {
return data;
}
} else {
// 校驗成品庫存
JsonData data = checkProductStock(outboundProductReq.getProductId(), outboundProductReq.getOutboundCount());
if (data.getCode() == 0) {
// 新增
synchronized (this) {
// 問題:連續點擊速度很快時,會出現兩條重復記錄 解決方案:將以下操作抽取出來做方法,在方法上加事務和鎖,
if (outboundProductMapper.selectOne(new QueryWrapper<OutboundProductDO>()
.eq("serial_code", CommonUtil.getCurrentSerialNumber("SN_OUT"))) != null) {
return JsonData.buildResult(BizCodeEnum.STORE_PRODUCT_OUTBOUND_SAME);
}
outboundProductDO = new OutboundProductDO();
BeanUtils.copyProperties(outboundProductReq, outboundProductDO);
outboundProductDO.setOutboundId(UUID.randomUUID().toString());
outboundProductDO.setOutboundState(0);
outboundProductDO.setOutboundType(0);
outboundProductDO.setSerialCode(CommonUtil.getCurrentSerialNumber("SN_OUT"));
outboundProductMapper.insert(outboundProductDO);
// 凍結成品庫存
StockProductDO stockProductDO = stockProductMapper.selectOne(new QueryWrapper<StockProductDO>()
.eq("product_id", outboundProductDO.getProductId()));
stockProductDO.setFreezeCount(outboundProductReq.getOutboundCount() + stockProductDO.getFreezeCount());
stockProductMapper.updateById(stockProductDO);
// 保存到庫存詳情表
saveTODetailProduct(outboundProductDO, OpTypeEnum.OP_LOCK);
}
} else {
return data;
}
}
return JsonData.buildSuccess();
}
- 唯一索引 – 防止新增臟資料
token機制 – 防止頁面重復提交
悲觀鎖 – 獲取資料的時候加鎖(鎖表或鎖行)
樂觀鎖 – 基于版本號version實作, 在更新資料那一刻校驗資料
分布式鎖 – redis(jedis、redisson)或zookeeper實作
狀態機 – 狀態變更, 更新資料時判斷狀態
分享優秀博客
冪等校驗
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/353484.html
標籤:其他
