研究背景:
這幾天被支付寶充值后通知所產生的重復處理問題搞得焦頭爛額, 一周連續發生兩次重復充錢的杯具, 發事故郵件發到想吐。。為了挽回程序員的尊嚴, 我用了Redis的鎖機制。
事故場景:
支付寶下單 -> 客戶支付 -> 回調我方接口通知支付結果
服務器節點: 2個
事故發生原因: 回調我方接口后, 第一次通知還未處理完, 第二次通知又來了(間隔幾秒),未對通知進行判定重復,導致兩個節點均處理了通知, 給客戶加了兩次錢。。
解決方案:
1. 基於數據庫的原子性操作原理控制數據只能被處理一次。
方法: 由於數據能被處理的條件是 pay_record.status = 'paying', 即支付狀態為待支付中, 才能更新數據為支付成功。
修改前的更新SQL: update pay_record set status = 'succ' where id = #{id};
修改后的更新SQL: update pay_record set status = 'succ' where id = #{id} and status = 'paying';
通過對返回值是否 > 0判斷是否有數據被更新成功, 如果有,則執行后續的給錢包加錢操作,否則不處理。
2. 基於Redis的分布式鎖setnx方法控制同時只能有一個線程處理加錢邏輯
// 第一次通知,設置緩存 if(jedis.setnx(DONKEY_ALICALLBACK_NOTICE + outTradeNo, outTradeNo) > 0) { // 設置生效時長(因為setnx沒有生效時間的入參) jedis.expire(DONKEY_ALICALLBACK_NOTICE + outTradeNo, 3600 * 3); LOGGER.warn("key = {} 新增緩存成功, 進入處理..", DONKEY_ALICALLBACK_NOTICE + outTradeNo);
// 錢包加錢操作
addMoney(outTradeNo); } else { // 新增緩存失敗, 不處理 LOGGER.warn("key = {} 新增緩存失敗, 不處理", DONKEY_ALICALLBACK_NOTICE + outTradeNo); return; }
setnx: 當key不存在時設置成功,返回1, 否則返回0, 這個操作是線程安全的, 可以查看到它的源代碼如下:
public Long setnx(String key, String value) { this.checkIsInMultiOrPipeline(); this.client.setnx(key, value); return this.client.getIntegerReply(); }
this.checkIsInMultiOrPipeline()方法源碼:
protected void checkIsInMultiOrPipeline() { if(this.client.isInMulti()) { throw new JedisDataException("Cannot use Jedis when in Multi. Please use Transation or reset jedis state."); } else if(this.pipeline != null && this.pipeline.hasPipelinedResponse()) { throw new JedisDataException("Cannot use Jedis when in Pipeline. Please use Pipeline or reset jedis state ."); } }
總結:
通過jedis.class源碼分析可知, redis的大部分的方法均實現了線程安全,都是單線程操作, 故使用redis作為分布式鎖效果很好, 也很輕量級。
完畢~~