## 如何保證萬無一失

假如我們是一家銀行,收到第三方發來的一筆轉賬請求(比如支付寶,微信支付里面的提現操作),收到這筆交易請求后,會進行下面三個動作(有簡化,但是能體現核心部分):
1. 創建一條交易記錄
2. 將轉賬操作扔到隊列
3. 消費者消費隊列,最終完成轉賬操作
>題外話:這就是為什么我們提現有延時的原因(2小時內到賬),因為最先放到隊列了,而不是直接處理,為什么不直接處理呢,因為交易量很大,計算能力有限,一時處理不過來。
這關鍵的三步,怎么保證安全性呢?
什么是安全性呢,在這里,正確性就是安全性。
再來看會出現哪些問題呢?下面我們提出一些問題:
1. 轉賬失敗了。
2. 重復轉賬了。
3. 轉賬成功了,但狀態還未顯示。
……
不管有哪些情況,我們只知道,正確的結果只有一種,那就是轉賬成功,沒有多轉也沒有少轉,并且轉賬記錄顯示為成功。
**如果三步都能成功,和預期的一樣執行,那么就不會有任何問題了,但關鍵是沒有人能夠擔保一定不會出問題。**
所以我們就要用一些方法來解決這個不確定性問題了。
先來一步一步的看,假設執行到哪一步失敗的話會怎樣,以及如何解決。
1. 第一步失敗的話,交易記錄不能創建,也就沒有任何交易被創建。直接響應請求失敗。這不會造成什么問題,畢竟這是第一步,第一步失敗就不會有后面的事情了,失敗了又能有什么問題呢。
2. 第二步失敗的話,扔到隊列失敗了,可是第一步交易已經成功創建了,這可怎么辦。
好,先來解決這個問題。
**Q:扔隊列操作失敗了怎么辦?**
**A:** 沒人能保證任何操作都一定能成功,隊列系統剛好不可用了,只能怪老天了,那此時怎么保證業務不出問題呢,總不能丟失這筆轉賬吧。這里有幾種方式:1. 重試,放隊列失敗,就不斷重試,如果有多條消息,后面消費時要檢查交易狀態(上鎖,不能出現并發問題),要保證冪等性,不能重復消費消息。2. 補償:在放隊列之前(交易創建之后),做一個補償標記,10小時候或者每隔10小時檢查該筆交易的狀態(檢查交易狀態,檢查確認,檢查隊列),如果有問題就重試,或者做好失敗記錄,記好日志。(這個時間是根據系統處理情況來定的,直到交易狀態為完成才刪除這個補償標記)
**解決了這個問題,那么其實下面的n個問題就都解決了,比如隊列消息丟失啊,都是同樣的思路。不管你哪一步失敗,會出現什么問題,我們只知道有且只有一種正確結果,只要保持這種正確結果的最終正確性,系統就是可靠的,萬無一失的。**
(如果系統確實出問題了,比如由于前期設計考慮不周全、疏忽而導致的問題,那我們也要最大限度的減少由此帶來的影響,最小化損失,并且設法挽回。)
>[danger] 關鍵詞:**重試、補償、事后校正、確認、ACK確認、容錯、柔性事務、兩階段提交、最終一致性、鎖、防止并發問題、冪等性、防止重復消費、日志記錄**。
* * * * *
### 沒有絕對的健壯,但是有相對的
保持悲觀的同時也不要忘記樂觀。
由于每段代碼執行邏輯不同,所處環境也不同,所以出錯的幾率也不同,**一般主進程存在較小的崩潰概率,因為它邏輯直觀,不會摻雜任何的業務邏輯代碼,所以幾乎不會出錯中斷(甚至設計中可以認為此部分不會出錯,負載均衡部分也同理)**,但是worker進程就不同了,它是業務邏輯的具體執行部分,這里出錯是不可預料的,所以對于這部分代碼,可以理解為一定會出錯,主進程應做好維護工作。
這世界上并沒有萬無一失,就像兩座城堡的通信,信使總是可能不可靠的,你無法確定他一定不會叛變或者旅途中遇到突發情況,不論是什么情況發生,只要城堡是堅固的,我們就是安全的。任何時候我們都不能將安全的賭注壓在信使身上。如果你理解這個道理,你構建的系統就是堅固可靠的。
*****
### 一致性保證
例:轉賬操作
1. **轉賬表** remittance: (id, from, to, amount, status, create_time, complete_time) : 增加一條轉賬記錄
2. **轉賬隊列消息記錄表** remittance_queue: (id, remittance_id status[待消費, 正在執行, 處理完成, 已關閉]) : 增加一條消息記錄
> 消費失敗,則轉入失敗記錄表進行相應的業務邏輯(如轉賬日志),而不是標記為處理失敗
(有人可能認為 `remittance_queue` 表是不必要的,的確只有 `remittance` 表也做得到行鎖并發控制,但是有這張表可以記錄某條轉賬的隊列處理記錄,并且將鎖開銷轉移到用戶不回訪問的表上了,所以我認為這張表有存在的必要。同時對于系統內多種這樣的操作,可以抽象出一張專門記錄隊列操作的表,如 隊列操作記錄表: [操作標識, 對應資源ID, 狀態] 這樣整個系統只需要這一張表保證就可以。)
3. **插入一條隊列消息** `Resque::enqueue()`: task, 載荷(包含 remittance_queue_id)
這樣應該被處理的業務邏輯就被 **“裝”** 到了隊列中,即Broker中。而隊列消息是不可靠的(存/取):丟失消息、消息重復(同樣的消息存在多條)、重復消費……,即便如此,我們也要在這樣的情況下保證整個轉賬的業務邏輯正確性。
具體要面對和解決的問題就是:冪等性
具體做法:
1. **防止并發問題:** task 消息消費時,必須使用 事務行鎖 檢查 `remittance_queue` 隊列消息的狀態是否為 `待消費`,只在 待消費 的狀態下執行任務處理。是否已經消費過了,(防止重復消費,或者隊列消息不穩定)
2. **補償機制:** 當一段時間過了還沒有到賬,說明隊列消息可能丟失了,或者其他原因,這時需要重發消息,相當于 **“再次執行轉賬操作”** (不過只是進行重發隊列消息),也就是 **啟動補償機制**。
此時的 **補償操作** 為:
1. 先 事務行鎖 檢查這條 轉賬記錄 對應的 隊列消息表,根據 `remittance_id` 查到 `remittance_queue` 中所在行,`status` 是否還為 `待消費`,是則將 `status` 標記 `關閉`,相當于拋棄對應的隊列消息了,不管你那條隊列消息此時到底是跑哪里了。
2. 然后,再次增加一條 隊列消息記錄
3. 再次插入一條隊列
> 上面說補償時的重發消息相當于 再次執行轉賬,但這個 “再次執行轉賬操作” 是加引號的,這個操作與轉賬操作不同的是,沒有其他的業務邏輯(如轉賬前的相關邏輯),只有安全的隊列操作。(操作對象是remittance_queue和隊列)
#### 補償機制如何實現
如:發起一筆轉賬時,就建立一個補償,采用延時隊列實現,延時時間根據預估,比如轉賬發起后的12小時。這樣當轉賬開始后的12小時后,補償機制就會執行。補償發現轉賬不成功,會有相應的機制,如重發消息,日志記錄,報警等等。如成功了,那么補償時就什么都不做。
除了延時隊列實現補償,還可以手動觸發,比如后臺的“重試”按鈕,就相當于是手動的補償機制了。
并且每次操作/補償,都伴隨著補償(補償重發時也要再次加個延時補償),直至最終操作成功。
補償就像一個護花使者,它不直接與你同行,而是用另一種方式伴隨著你。
**另外沒有絕對的安全,系統應該定時執行財務對賬,這樣才能及時發現和規避風險。**
~~~
### 最終一致性 補償
砍價服務 ? 砍價應用
砍價服務成功調用 砍價應用成功了,但自身的后續處理失敗了,那就沒辦法了,所以調用方需要自己確保自己也能落盤成功
分布式事務很難,只能做到最終一致性,如果失敗只能補償處理了
補償方案:
1. 被調用方 執行成功后 寫入一條 巡視確認記錄 后再返回結果
2. 調用方 執行成功后 處理 其巡視記錄(刪除或標記)
3. 監控經常 定期掃描 超時待確認的巡視記錄 就能發現 數據不一致的記錄
4. 程序或人工手工修復數據不一致的記錄,分析問題原因,這樣就能實現 最終一致性
~~~
#### 如果補償也失敗了怎么辦?
還有人打破砂鍋問到底,說如果補償、確認等機制也失敗了怎么辦,好吧,抱著務實的態度,也來說一下:
一般來說,越簡單的系統越不容易出錯,出錯的系統一般是復雜性較大的系統,但凡事總有萬一,萬一最簡單的補償機制也失效了怎么辦?還有報警啊,還有日志啊,那如果報警和日志也都失效了怎么辦?好吧。你是不打算放過我了是吧。
其實你不用這么軸,這種情況即使出現我們也不怕,認真的告訴你,我們真的有考慮過這種情況的。別忘了我們還有最后一道防線,人工。人工審查/糾錯(成熟的系統都有財務、對賬、審查的,即使系統沒有這樣做,公司也會要求審查對賬的)。試想轉賬超過10天還沒有到賬的,就算沒有審查出來,用戶也會打電話來投訴的。
雖然你很煩,不過我還是喜歡這么軸這么認真的你,好樣的。
#### 沒有絕對的完美
還有人說,我代碼很完美了,怎么會有那么多的失敗呢,有可能嗎?
首先世界上沒有最完美的代碼,沒有永遠沒有BUG的系統,最完美的代碼就是不斷進化、升級、更新的代碼,最可靠的系統就是得到長期支持和維護的系統。
就算你的代碼不出問題,你能控制內因,你也無法控制外因啊。你能預料到地震,海嘯,臺風等自然災害嗎?硬盤爆炸呢,內存燒焦呢,CPU冒煙呢,停電呢,臨時工挖斷網線呢,這些你能控制嗎,**所以啊,任何時候,任何指令都可能執行失敗或者沒被執行**,如果你能在腦內模擬出硬件,那么你就能很簡單的看出程序是怎么運行的。就能在大腦里面想象程序運行的原理,你就知道這是怎么一回事了。
* * * * *
### 代碼的失敗率
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.connect() // failure rate: 1/千
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.beginTransaction() // failure rate: 1/萬
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.ping() // failure rate: 1/十萬
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.query() // failure rate: 1/八萬
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.query() // failure rate: 1/七萬
</p>
sleep(2)
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.query() // failure rate: 1/五萬
</p>
<p style="background: linear-gradient(to right,#36ff670,#ffffff 5%);">db.commit() // failure rate: 1/三萬
</p>
1. 每次調用都是與 db 服務端進行交互
2. 假設 sql 都是正確的,每行代碼也都會有執行失敗的可能,只是失敗概率不同而已
3. 我們無法改變總是會有可能失敗的現實,只能盡量讓我們的業務代碼處在一個較低的失敗率之中,并做好容錯處理,考慮異常的情況
4. 即代碼并不是完全可靠的,但我們要有容錯,確保異常是可控的,這樣業務才是安全的,正確的
----
### 擴展
我們發現關系型數據庫還是很重要的,我們的數據都交給了它,可靠性也交給了它。
[傳統事務與柔性事務](https://www.jianshu.com/p/ab1a1c6b08a1)
> 日志,冪等性,業務彈性,最終一致,重試,補償
[支付寶運營架構中柔性事務指的是什么? - 知乎](https://www.zhihu.com/question/31813039)
> 業務層2PC(兩階段提交),事后校正
[創始人快去跟公司技術人員落實這件事](https://mp.weixin.qq.com/s/YQbxfI389FLVuwIpYhr9sw)
> 有可能出 bug 的代碼最終都會出 bug。
>
> 一定。
[RabbitMQ從入門到精通---ACK機制 | 菜鳥IT路](https://www.dev-heaven.com/posts/36563.html)
[RabbitMQ ACK 機制的意義是什么? - 知乎](https://www.zhihu.com/question/41976893)
[TCP報文到達確認(ACK)機制 - CSDN博客](https://blog.csdn.net/wjtxt/article/details/6606022)
[php手冊經常見到,什么是“二進制安全”? - zhuocr的博客 - CSDN博客](https://blog.csdn.net/zhuocr/article/details/70591310)(充分了解你使用的系統)
[事務已提交,數據卻丟了,趕緊檢查下這個配置!!! | 數據庫系列](https://mp.weixin.qq.com/s/-Hx2KKYMEQCcTC-ADEuwVA)
* * * * *
last update:2018-10-26 16:49:22
- 開始
- 公益
- 更好的使用看云
- 推薦書單
- 優秀資源整理
- 技術文章寫作規范
- SublimeText - 編碼利器
- PSR-0/PSR-4命名標準
- php的多進程實驗分析
- 高級PHP
- 進程
- 信號
- 事件
- IO模型
- 同步、異步
- socket
- Swoole
- PHP擴展
- Composer
- easyswoole
- php多線程
- 守護程序
- 文件鎖
- s-socket
- aphp
- 隊列&并發
- 隊列
- 講個故事
- 如何最大效率的問題
- 訪問式的web服務(一)
- 訪問式的web服務(二)
- 請求
- 瀏覽器訪問阻塞問題
- Swoole
- 你必須理解的計算機核心概念 - 碼農翻身
- CPU阿甘 - 碼農翻身
- 異步通知,那我要怎么通知你啊?
- 實時操作系統
- 深入實時 Linux
- Redis 實現隊列
- redis與隊列
- 定時-時鐘-阻塞
- 計算機的生命
- 多進程/多線程
- 進程通信
- 拜占庭將軍問題深入探討
- JAVA CAS原理深度分析
- 隊列的思考
- 走進并發的世界
- 鎖
- 事務筆記
- 并發問題帶來的后果
- 為什么說樂觀鎖是安全的
- 內存鎖與內存事務 - 劉小兵2014
- 加鎖還是不加鎖,這是一個問題 - 碼農翻身
- 編程世界的那把鎖 - 碼農翻身
- 如何保證萬無一失
- 傳統事務與柔性事務
- 大白話搞懂什么是同步/異步/阻塞/非阻塞
- redis實現鎖
- 淺談mysql事務
- PHP異常
- php錯誤
- 文件加載
- 路由與偽靜態
- URL模式之分析
- 字符串處理
- 正則表達式
- 數組合并與+
- 文件上傳
- 常用驗證與過濾
- 記錄
- 趣圖
- foreach需要注意的問題
- Discuz!筆記
- 程序設計思維
- 抽象與具體
- 配置
- 關于如何學習的思考
- 編程思維
- 談編程
- 如何安全的修改對象
- 臨時
- 臨時筆記
- 透過問題看本質
- 程序后門
- 邊界檢查
- session
- 安全
- 王垠
- 第三方數據接口
- 驗證碼問題
- 還是少不了虛擬機
- 程序員如何談戀愛
- 程序員為什么要一直改BUG,為什么不能一次性把代碼寫好?
- 碎碎念
- 算法
- 實用代碼
- 相對私密與絕對私密
- 學習目標
- 隨記
- 編程小知識
- foo
- 落盤
- URL編碼的思考
- 字符編碼
- Elasticsearch
- TCP-IP協議
- 碎碎念2
- Grafana
- EFK、ELK
- RPC
- 依賴注入
- 開發筆記
- 經緯度格式轉換
- php時區問題
- 解決本地開發時調用遠程AIP跨域問題
- 后期靜態綁定
- 談tp的跳轉提示頁面
- 無限分類問題
- 生成微縮圖
- MVC名詞
- MVC架構
- 也許模塊不是唯一的答案
- 哈希算法
- 開發后臺
- 軟件設計架構
- mysql表字段設計
- 上傳表如何設計
- 二開心得
- awesomes-tables
- 安全的代碼部署
- 微信開發筆記
- 賬戶授權相關
- 小程序獲取是否關注其公眾號
- 支付相關
- 提交訂單
- 微信支付筆記
- 支付接口筆記
- 支付中心開發
- 下單與支付
- 支付流程設計
- 訂單與支付設計
- 敏感操作驗證
- 排序設計
- 代碼的運行環境
- 搜索關鍵字的顯示處理
- 接口異步更新ip信息
- 圖片處理
- 項目搭建
- 閱讀文檔的新方式
- mysql_insert_id并發問題思考
- 行鎖注意事項
- 細節注意
- 如何處理用戶的輸入
- 不可見的字符
- 抽獎
- 時間處理
- 應用開發實戰
- python 學習記錄
- Scrapy 教程
- Playwright 教程
- stealth.min.js
- Selenium 教程
- requests 教程
- pyautogui 教程
- Flask 教程
- PyInstaller 教程
- 蜘蛛
- python 文檔相似度驗證
- thinkphp5.0數據庫與模型的研究
- workerman進程管理
- workerman網絡分析
- java學習記錄
- docker
- 筆記
- kubernetes
- Kubernetes
- PaddlePaddle
- composer
- oneinstack
- 人工智能 AI
- 京東
- pc_detailpage_wareBusiness
- doc
- 電商網站設計
- iwebshop
- 商品規格分析
- 商品屬性分析
- tpshop
- 商品規格分析
- 商品屬性分析
- 電商表設計
- 設計記錄
- 優惠券
- 生成唯一訂單號
- 購物車技術
- 分類與類型
- 微信登錄與綁定
- 京東到家庫存系統架構設計
- crmeb
- 命名規范
- Nginx https配置
- 關于人工智能
- 從人的思考方式到二叉樹
- 架構
- 今日有感
- 文章保存
- 安全背后: 瀏覽器是如何校驗證書的
- 避不開的分布式事務
- devops自動化運維、部署、測試的最后一公里 —— ApiFox 云時代的接口管理工具
- 找到自己今生要做的事
- 自動化生活
- 開源與漿果
- Apifox: API 接口自動化測試指南