賬戶余額更新優(yōu)化

業(yè)務(wù)場景
用戶預(yù)存一定余額,可以用余額在平臺購買套餐商品,支付扣除余額需控制并發(fā),當(dāng)前采用的是樂觀鎖方式。即每個用戶的余額記錄都有一個版本號,更新記錄時,需要帶上版本號。版本號采用整數(shù)遞增。
問題
當(dāng)有兩個扣減余額的操作同時發(fā)生時,其中一個有幾率失敗。失敗結(jié)果直接返回給用戶,此時用戶操作重試即可,但會影響用戶體驗(yàn)。如果一直處于高并發(fā)狀態(tài),用戶可能會連續(xù)操作失敗多次。主要針對此扣款失敗場景進(jìn)行優(yōu)化。
方案演進(jìn)
增加失敗重試

int i = 0, max = 3;//最多嘗試3次
while (i < max && !success) {
    //獲取余額記錄
    AgentRechargeEntity arEntity = agentRechargeService.findByAgentId(context.getAdminUserEntity().getAgentId());
    //版本記錄值,用于控制并發(fā)操作
    Integer exceptTxVersion = arEntity.getTxVersion();
    //修改金額計(jì)算
    //更新余額
    success = agentRechargeService.updateMoneyByExpectTxVersion(id, exceptTxVersion, money);
    i++;
}

進(jìn)行上面重試修改之后,仍然存在失敗日志


image.png

通過分析日志可知,失敗時確實(shí)有三次重試,說明我們修改的代碼是生效的。問題在于,失敗后重新獲取的記錄值仍然是老的數(shù)據(jù),版本號expectTxVersion沒有變化。實(shí)際獲取上次更新記錄值如下。


image.png

懷疑是可能存在緩存,該方法使用的是mybatis框架,由于我們沒有人為增加緩存,會不會是mybatis的緩存。經(jīng)研究,mybatis默認(rèn)是開啟二級緩存的,于是通過在select方法上增加flushCache="true" useCache="false"配置去除緩存。


image.png

然后更新上線了,本以為就此結(jié)束,然而。。。還是一樣的失敗日志。
重新分析:更新失敗說明版本號已經(jīng)變更了,意味著其他修改已經(jīng)提交入庫了。
為什么沒有讀到其他事務(wù)的最新數(shù)據(jù)呢,研究一下事務(wù)的隔離級別。
查看mysql默認(rèn)的隔離級別:

select @@transaction_isolation;
image.png

默認(rèn)為:可重復(fù)讀,看下該級別的定義。

一個事務(wù)啟動的時候,能夠看到所有已經(jīng)提交的事務(wù)結(jié)果。但是之后,這個事務(wù)執(zhí)行期間,其他事務(wù)的更新對它不可見。

因?yàn)楂@取記錄操作是在事務(wù)中,所以重復(fù)獲取不能得到最新數(shù)據(jù)。
因此,可以將數(shù)據(jù)獲取排除到事務(wù)之外,主要用spring的事務(wù)傳遞管理,設(shè)置為Propagation.NOT_SUPPORTED:以非事務(wù)方式執(zhí)行操作,如果當(dāng)前存在事務(wù),就把當(dāng)前事務(wù)掛起。

image.png

再看日志,雖然也有失敗,但基本重試一次之后就成功。


image.png

至此,問題解決。
總結(jié)
本以為是一個簡單的重試優(yōu)化,逐漸引出mybatis二級緩存和數(shù)據(jù)庫的事務(wù)管理。任何一個點(diǎn)的遺漏都達(dá)不到想要的效果。平時的知識儲備是必要的,否則遇到問題時將花費(fèi)成倍的時間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容