實戰:工作中對併發問題的處理

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/08/15/17630613.html
-Advertisement-
Play Games

本文是一次工作中對併發問題的處理案例,問題發生在快遞分揀的流程中,我儘可能將業務背景簡化,讓大家只關註併發問題本身。 ...


1. 問題背景

問題發生在快遞分揀的流程中,我儘可能將業務背景簡化,讓大家只關註併發問題本身。

分揀業務針對每個快遞包裹都會生成一個任務,我們稱它為 task。task 中有兩個欄位需要關註,一個是分揀中發生的異常(exp_type),另一個是分揀任務的狀態(status)。另外,需要關註分揀狀態上報介面,通過它來記錄分揀過程中的異常和狀態變更。

Task概覽.png

一般情況下,分揀機在分揀異常發生時會及時調用介面上報,在分揀完成時調用介面來標記為完成狀態,兩次介面調用的時間間隔較長,不會發生併發問題。

但是有一種特殊的分揀機,它不會在異常發生時及時上報,而是在分揀完成時將分揀過程中發生的異常和分揀結果一起上報,那麼此時分揀狀態上報介面在同一時間內就會有兩次調用,這時便發生了預期外的併發問題。

我們先看下分揀狀態上報介面的執行流程:

  1. 先查詢到該分揀任務 task,預設情況下 exp_type 和 status 均為預設值0
  2. 分揀異常修改 task 中的 exp_type,分揀完成修改 status 欄位信息
  3. 修改完成將 task 寫入

併發問題發生的圖示如下:

併發問題流程圖.png

資料庫初始值為1, 0, 0,分揀異常和分揀完成幾乎同時上報,它們都讀取到該值。分揀異常動作將 exp_type 修改為9,寫入資料庫,此時資料庫值為1, 9, 0;分揀完成動作將 status 修改為1,寫入資料庫,使得資料庫最終值為1, 0, 1,它將異常欄位的值覆蓋掉了。正常情況下,最終值應該為1, 9, 1,分揀完成動作應該讀取到分揀異常完成後的值1, 9, 0後再進行修改才對。

2. 解決方案

發生這個問題的原因很容易就能發現:兩個事務同時執行讀取-修改-寫入序列,其中一個寫操作在沒有合併另一個寫操作變更的情況下,直接覆蓋了另一個寫操作的結果,所以導致了數據的丟失。

這種問題是比較典型的丟失更新問題,可以通過對資料庫讀操作加鎖或者改變資料庫的隔離級別為可串列化使事務串列執行的方式進行避免。下麵我會將大家在討論避免丟失更新問題時提出的方案進行介紹,並儘可能的用代碼來表現它們。

2.1 資料庫讀操作加鎖和可串列化隔離級別

我們可以考慮:如果對每條Task數據修改的事務都是在當前事務完成之後才允許後續事務進行修改,使事務串列執行,那麼我們就能夠避免這種情況。比較直接的實現是通過顯式加鎖來實現,如下

select exp_type, status
from task
where id = 1
for update;

先查詢該行數據的事務會獲取到該行數據的排他鎖,後續針對該數據的所有讀寫請求都會被阻塞,直到先前事務執行完將鎖釋放。

這樣通過加鎖的方式實現了事務的串列執行。但是,在為SQL添加加鎖語句時,需要確定是不是為該行數據加鎖而不是鎖住了整個表,如果是後者,那麼可能會造成系統性能嚴重下降,而且還需要關註有哪些業務場景使用到了該SQL,是否存在長時間執行的只讀事務使用,如果存在的話可能會出現因加鎖導致延遲和系統性能下降,所以需要謹慎的評估。

此外,可串列化的資料庫隔離級別也能保證事務的串列執行,不過它針對的是所有事務。一般情況下為了保證性能,我們不會採用這種方案(預設使用MySQL可重覆讀隔離級別)。

MySQL的InnoDB引擎實現可串列化隔離級別採用的是2PL機制:在第一階段事務執行時獲取鎖,第二階段事務執行完成釋放鎖。

2.2 針對業務只修改必要欄位

如果異常狀態請求僅修改 exp_type 欄位,分揀完成僅修改 status 欄位的話,那麼我們可以梳理一下業務邏輯,僅將必要修改的欄位寫入資料庫,這樣就不會發生丟失更新的異常,如下代碼所示:

// 處理異常狀態請求,封裝修改數據的對象
Task task = new Task();
tast.setId(id);
task.setExpType(expType);

// 更改數據
taskService.updateById(task);

在執行修改數據前,創建一個新的修改對象,並只為其必要修改欄位賦值。但是還需要考慮的是:如果這個業務流程處理已經很複雜了,很可能不清楚該為哪些欄位賦值而導致再發生新的異常,所以採用這種方法需要對業務足夠熟悉,並且在修改完後進行充分的測試。

2.3 分散式鎖

分散式鎖的方法與方法一類似,都是通過加鎖的方式來保證同時只有一個事務執行,區別是方法一的鎖加在了資料庫層,而分散式鎖是藉助Redis來實現。

這種實現方式的好處是鎖的粒度小,發生鎖爭搶僅限於單個包裹,無需像資料庫加鎖一樣去考慮鎖的粒度和對相關業務的影響。偽代碼如下所示:

// 分散式鎖KEY
String distributedKey = String.format(DISTRIBUTED_KEY_PREFIX, packageNo);
try {
    // 分散式鎖阻塞同一包裹號的修改
    lock(distributedKey);
    // 處理業務邏輯
    handler();
} finally {
    // 執行完解鎖
    redissonDistributedLocker.unlock(distributedKey);
}

需要註意,lock()加鎖方法要保證加鎖失敗或發生其他異常情況不影響業務邏輯的執行,並設定好鎖持有時間和等待鎖的阻塞時間,此外解鎖方法務必添加到finally代碼塊中保證鎖的釋放。

2.4 CAS

CAS是樂觀的解決方案,它一般通過在資料庫中增加時間戳列來記錄上次數據更改的時間,當新的事務執行時,需要比對讀取時該行數據的時間戳和資料庫中保存的時間戳是否一致,以此來判斷事務執行期間是否有其他事務修改過該行數據,只有在沒有發生改變的情況下才允許更新,否則需要重試這個事務。樣例SQL如下所示:

update task
set exp_type = #{expType}, status = #{status}, ts = #{currentTs}
where id = #{id} and ts = #{readTs}

它的原理不難理解,但是實現起來可能會存在困難,因為需要考慮在執行失敗後該如何重試,重試的方式和重試的次數需要根據業務去判斷。

巨人的肩膀

  • 《數據密集型應用系統設計》第七章 事務

作者:京東物流 王奕龍

來源:京東雲開發者社區 自猿其說Tech 轉載請註明出處


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 提要:本系列文章主要參考`MIT 6.828課程`以及兩本書籍`《深入理解Linux內核》` `《深入Linux內核架構》`對Linux內核內容進行總結。 記憶體管理的實現覆蓋了多個領域: 1. 記憶體中的物理記憶體頁的管理 2. 分配大塊記憶體的伙伴系統 3. 分配較小記憶體的slab、slub、slob分 ...
  • 本文將詳細介紹如何發佈.NET Core項目到IIS伺服器。首先,第一步需要安裝IIS,介紹了在本地電腦和伺服器中進行安裝。然後需要安裝SDK和運行時才能發佈.NETCore項目。其次介紹瞭如何發佈.NETCore項目和Vue項目,並配置IIS。最後介紹瞭如何將項目部署到Service服務中。 ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202308/3076680-20230815092340479-67825278.png) # 1. 事務日誌 ## 1.1. 事務日誌有助於提高事務的效率 ### 1.1.1. 存儲引擎只需要更改記憶體中的數 ...
  • # mysql複製技術/mysql集群 ![](https://img2023.cnblogs.com/blog/3165277/202308/3165277-20230815183932805-1063237772.png) ## 準備 1.四台虛擬機都關閉防火牆 ``` systemctl st ...
  • 最近系統有個需求,希望工作流的審批人被催辦後就要置頂在最前面, 工作流列表我是用es的,一開始想用pinned實現,但用pinned的話,每頁都會置頂在前面,我的需求只是想讓他優先排在前面,翻頁後正常顯示 後面找到這個,通過把匹配到數據的分數提高,然後用sort進行排序,就能實現我的需求了 GET ...
  • 簡介 Timescale Documentation | Getting started with Timescale Timescale是一個用於時間序列,事件和分析的PostgreSQL數據平臺。 提供了PostgreSQL的可靠性,這是時間序列的超能力 TimescaleDB,。它提供 自動備 ...
  • GaussDB(for Influx)推出了單機版方案,可用於開發、測試等場景,既能享受到服務化帶來的便利,也可以明顯地降低使用成本。 ...
  • 說到分散式事務,大家並不陌生。在實際工作中,用得比較多的還是柔性分散式事務,今天主要把在工作中運用到的幾種柔性分散式事務的場景及實現方式做一個簡單介紹,也可以看做是柔性分散式事務的一個演進過程。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...