成都網(wǎng)站開(kāi)發(fā)排名廈門(mén)關(guān)鍵詞排名優(yōu)化
啥是冪等?
客戶端對(duì)同一API的多次調(diào)用應(yīng)該產(chǎn)生相同的服務(wù)器狀態(tài)。
通俗的說(shuō):某個(gè)行為重復(fù)的執(zhí)行,最終獲取的結(jié)果是相同的,不會(huì)因?yàn)橹貜?fù)執(zhí)行對(duì)系統(tǒng)造成變化。
比如說(shuō)同步數(shù)據(jù)的功能,重復(fù)可以重復(fù)同步,如果可以就表示是冪等的。
常見(jiàn)場(chǎng)景
??HTTP方法??:
冪等方法:GET、PUT、DELETE
非冪等方法:POST、PATCH
??支付系統(tǒng)??:重復(fù)支付請(qǐng)求不應(yīng)導(dǎo)致多次扣款
??訂單創(chuàng)建??:重復(fù)提交訂單不應(yīng)創(chuàng)建多個(gè)相同訂單
實(shí)現(xiàn)冪等性的方法
??唯一標(biāo)識(shí)符??:為每個(gè)操作分配唯一ID,服務(wù)器記錄已處理ID
??版本號(hào)/時(shí)間戳??:資源更新時(shí)攜帶版本信息
??狀態(tài)機(jī)??:確保操作只在特定狀態(tài)下執(zhí)行
??數(shù)據(jù)庫(kù)約束??:利用唯一索引防止重復(fù)數(shù)據(jù)
具體
1 update + where,利用數(shù)據(jù)庫(kù)的行級(jí)鎖保證查詢和更新是原子性的。偽代碼如下。
String rechargeId = "充值訂單id";// 根據(jù)rechargeId去找充值記錄,如果已處理過(guò),則直接返回成功
RechargePO rechargePo = select * from t_recharge where id = #{rechargeId};// 充值記錄已處理過(guò),直接返回成功
if(rechargePo.status==1){return "SUCCESS";
}開(kāi)啟Spring事務(wù)// 下面這個(gè)sql是重點(diǎn),重點(diǎn)在where后面要加 status = 0 這個(gè)條件;count表示影響行數(shù)
int count = (update t_recharge set status = 1 where id = #{rechargeId} and status = 0);// count = 1,表示上面sql執(zhí)行成功
if(count!=1){// 走到這里,說(shuō)明有并發(fā),直接拋出異常throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)重試")
}else{//給賬戶加錢update t_account set balance = balance + #{rechargePo.price} where id = #{rechargePo.accountId}
}提交Spring事務(wù)
2 基于版本號(hào)的樂(lè)觀鎖
String rechargeId = "充值訂單id";// 根據(jù)rechargeId去找充值記錄,如果已處理過(guò),則直接返回成功
RechargePO rechargePo = select * from t_recharge where id = #{rechargeId};// 充值記錄已處理過(guò),直接返回成功
if(rechargePo.status==1){return "SUCCESS";
}開(kāi)啟Spring事務(wù)// 期望的版本號(hào)
Long exceptVersion = rechargePo.version;// 下面這個(gè)sql是重點(diǎn),重點(diǎn)在set后面要有version = version + 1,where后面要加 status = 0 這個(gè)條件;count表示影響行數(shù)
int count = (update t_recharge set status = 1,version = version + 1 where id = #{rechargeId} and version = #{exceptVersion});// count = 1,表示上面sql執(zhí)行成功
if(count!=1){// 走到這里,說(shuō)明有并發(fā),直接拋出異常throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)重試")
}else{//給賬戶加錢update t_account set balance = balance + #{rechargePo.price} where id = #{rechargePo.accountId}
}提交spring事務(wù)
3 關(guān)聯(lián)一個(gè)唯一約束輔助表
-- 冪等輔助表
create table if not exists t_idempotent
(id varchar(50) primary key comment 'id,主鍵',idempotent_key varchar(200) not null comment '需要確保冪等的key',unique key uq_idempotent_key (idempotent_key)
) comment '冪等輔助表';
重點(diǎn)關(guān)注第二個(gè)字段idempotent_key,這個(gè)字段添加了唯一約束,說(shuō)明同時(shí)向這個(gè)表中插入同樣值的idempotent_key,則只有一條記錄會(huì)執(zhí)行成功,其他的請(qǐng)求會(huì)報(bào)異常,而失敗,讓事務(wù)回滾。
String rechargeId = "充值訂單id";// 根據(jù)rechargeId去找充值記錄,如果已處理過(guò),則直接返回成功
RechargePO rechargePo = select * from t_recharge where id = #{rechargeId};// 充值記錄已處理過(guò),直接返回成功
if(rechargePo.status==1){return "SUCCESS";
}// 生成idempotentKey,這里可以使用,業(yè)務(wù)id:業(yè)務(wù)類型,那么我們這里可以使用rechargeId+":"+"RECHARGE_CALLBACK"
String idempotentKey = rechargeId+":"+"RECHARGE_CALLBACK";// 冪等表是否存在記錄,如果存在說(shuō)明處理過(guò),直接返回成功
IdempotentPO idempotentPO = select * from t_idempotent where idempotent_key = #{idempotentKey};
if(idempotentPO!=null){return "SUCCESS";
}開(kāi)啟Spring事務(wù)(這里千萬(wàn)不要漏掉,一定要有事務(wù))// count表示影響行數(shù),這個(gè)sql比較特別,看起來(lái)并發(fā)會(huì)出現(xiàn)問(wèn)題,實(shí)際上配合唯一約束輔助表,就不會(huì)有問(wèn)題了
int count = update t_recharge set status = 1 where id = #{rechargeId};// count != 1,表示未成功
if(count!=1){// 走到這里,直接拋出異常,讓事務(wù)回滾throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)重試")
}else{//給賬戶加錢update t_account set balance = balance + #{rechargePo.price} where id = #{rechargePo.accountId}
}String idempotentId = "";
// 這里是關(guān)鍵一步,向 t_recharge 插入記錄,如果有并發(fā)過(guò)來(lái),只會(huì)有一個(gè)成功,其他的會(huì)報(bào)異常導(dǎo)致事務(wù)回滾,上面的
insert into t_recharge (id, idempotent_key) values (#{idempotentId}, #{idempotentKey});提交spring事務(wù)