記一次有意思的業務實現 → 單向關註是關註,雙向關註則成好友

来源:https://www.cnblogs.com/youzhibing/archive/2022/06/06/16273601.html
-Advertisement-
Play Games

開心一刻 有個問題一直困擾著我:許仙選擇了救蛇,為什麼楊過卻選擇救雕(而不救蛇) 後面想想,其實楊過救神雕是有原因的,當年神雕和巨蛇打架的時候 雕對楊過說:殺蛇,殺蛇,殺蛇! 蛇對楊過說:殺雕,殺雕,殺雕! 楊過果斷選擇了殺蛇 業務場景 業務描述 業務上有這樣的需求,張三、李四兩個用戶,如果互相關註 ...


開心一刻

  有個問題一直困擾著我:許仙選擇了救蛇,為什麼楊過卻選擇救雕(而不救蛇)

  後面想想,其實楊過救神雕是有原因的,當年神雕和巨蛇打架的時候

  雕對楊過說:殺蛇,殺蛇,殺蛇!

  蛇對楊過說:殺雕,殺雕,殺雕!

  楊過果斷選擇了殺蛇

業務場景

  業務描述

  業務上有這樣的需求,張三、李四兩個用戶,如果互相關註則成為好友

  設計上有兩張表,關註關係表: tbl_follow 

  朋友關係表: tbl_friend 

  我們以張三關註李四為例,業務實現流程是這樣的

    1、先查詢李四有沒有關註張三

    2、如果李四關註了張三,則成為好友,往 tbl_friend 插入一條記錄;如果李四沒有關註張三,則只是張三單向關註李四,往 tbl_follow 插入一條記錄

  看似沒問題,可如果我們從併發的角度來看,是不是還正常了?

  如果張三、李四同時關註對方,那麼業務實現流程的第 1 步得到的結果可能就是雙方都沒有關註對方(加資料庫的排他鎖也沒用,記錄不存在,行鎖無法生效)

  得到的結果就是張三關註李四、李四關註張三,但張三和李四沒有成為朋友,這就導致了與業務需求不符!

  問題復現

  相關環境如下

   MySQL : 5.7.21-log ,隔離級別 RR

   Spring Boot : 2.1.0.RELEASE 

   MyBatis-Plus : 3.1.0 

  核心代碼如下

  完整代碼見:mybatis-plus-demo

  我們來複現下問題

  正確結果應該是: tbl_follow 、 tbl_friend 中各插入一條記錄

  但目前的結果是只往 tbl_follow 中插了兩條記錄

  該如何處理該問題,歡迎大家評論區留言

JVM 鎖

  既然併發了,那就加鎖唄

  JVM 自帶的 synchronized 和 Lock 都有同步作用,我們以 synchronized 為例,來看看效果

   tbl_follow 和 tbl_friend 中各插入一條記錄,問題得到解決!

  但是完美嗎?如果項目是集群部署,張三、李四關註對方的請求分別落在了集群中不同的節點上,不能成為好友的問題會不會出現?

分散式鎖

  因為 JVM 鎖只能控制同個 JVM 進程的同步,控制不了不同 JVM 進程間的同步,所有如果項目是集群部署,那麼就需要用分散式鎖來控制同步了

  關於分散式鎖,我就不多說了,網上資料太多了,推薦一篇:再有人問你分散式鎖,這篇文章扔給他

  如果用分散式鎖去解決上述案例的問題,樓主就不去實現了,只是強調一個小細節:如何保證 張三關註李四 、 李四關註張三 它們申請同一把鎖

  以 Redis 實現為例, key 的命名是有規範的,比如:業務名:方法名:資源名,具體到如上的案例中, key 的名稱:user:follow:123:456

  如果 張三關註李四 申請的 user:follow:123:456 ,而 李四關註張三 申請的是 user:follow:456:123 ,那麼申請的都不是同一把鎖,自然也就沒法控制同步了

  所以申請鎖之前,需要進行一個小細節處理,將 followId 與 userId 進行排序處理,小的放前面,大的放後面,類似: user:follow:小id:大id 

  那麼就能保證它們申請的是同一把鎖,自然就能控制同步了

唯一索引

  接下來要講的實現方式不常見,但是挺有意思的,大家仔細看

  我們改造一下 tbl_follow ,另取名字 tbl_follow_plus 

  註意欄位看欄位的描述

  tbl_follow 中 user_id 固定為 被關註者 , tbl_follow 中 follower_id 固定為 關註者 

  tbl_follow_plus 中 one_side_id 和 other_side_id 沒有固定誰是 關註者 ,誰是 被關註者 ,而是通過 relation_ship 的值來指明誰關註誰

  業務實現

  當 one_side_id 關註 other_side_id 的時候,比較它倆的大小

  若 one_side_id < other_side_id ,執行如下邏輯

  若 one_side_id > other_side_id ,則執行如下邏輯

  不太容易看懂,我們直接看代碼實現

  執行效果如下

  我們分析下結果

  tbl_follow_plus 只插入了一條記錄

  relation_ship = 3 表示雙向關註

  tbl_friend 插入了一條記錄

  同時關註 這個業務就實現了

  有小伙伴就有疑問了:樓主你只分析了 one_side_id 關註 other_side_id 的情況,沒分析 other_side_id 關註 one_side_id 的情況呀

  大家註意看 tbl_follow_plus 表中各個列名的註釋, one_side_id 和 other_side_id 並不是具體的 關註者 和 被關註者 ,兩者的業務含義是等價的

  至於是誰關註誰,是通過 relation_ship 的值來確定的,所以 one_side_id 關註 other_side_id 和 other_side_id 關註 one_side_id 是一樣的

  至於適不適用單向關註的情況,大家自行去驗證

  原理分析

  雖然業務需求是實現了,但卻難以理解,讓我們一步一步往下分析

  1、為什麼要比較 one_side_id 和 other_side_id 的大小?

     tbl_follow_plus 有個唯一索引 UNIQUE KEY `uk_one_other` (`one_side_id`,`other_side_id`) 

    比較大小的目的就是保證 tbl_follow_plus 的 one_side_id 記錄的是小值,而 other_side_id 記錄的是大值

    例如 123 關註 456 , one_side_id = 123 , other_side_id = 456 , relation_ship = 1 

       456 關註 123 , one_side_id = 123 , other_side_id = 456 ,但 relation_ship = 2 

    那這有什麼用?

    還記得我在上面的 分散式鎖 實現方案中強調的那個細節嗎

    這裡比較大小的作用也是為了保證 123 關註 456 與 456 關註 123 在唯一索引上競爭的是用一把行鎖

  2、insert … on duplicate key update

    其作用簡單點說就是:資料庫表中存在某個記錄時,執行這個語句會更新,而不存在這條記錄時,就會插入

    有個前置條件:只能基於唯一索引或主鍵使用;具體細節可查看:記錄不存在則插入,存在則更新 → MySQL 的實現方式有哪些?

     insert ... on duplicate 確保了在事務內部,執行了這個 SQL 語句後,就占住了這個行鎖(先占鎖,再執行 SQL)

    確保了之後查詢 relation_ship 的邏輯是在行鎖保護下的讀操作

  3、relation_ship=relation_ship | 1(relation_ship=relation_ship | 2)

    這個寫法就有點巧妙了,這裡的 | 指的是 按位或運算 

     relation_ship 的值是在業務代碼中指定的,只能是 1 或者 2

    因為在 MySQL 層面有個唯一索引的 行鎖 ,所以 123 關註 456 和 456 關註 123 的事務之間存在鎖競爭,必定是串列的

    3.1 若先執行 123 關註 456 的事務, relation_ship 傳入的值是 1,事務執行完之後, relation_ship 的值等於 1 | 1 = 1 ;

      再執行 456 關註 123 的事務, relation_ship 傳入的值是 2,事務執行完之後, relation_ship 的值等於 1 | 2 = 3 

    3.2 若先執行 456 關註 123 的事務, relation_ship 傳入的值是 2,事務執行完之後, relation_ship 的值等於 2 | 2 = 2 ;

      再執行 123 關註 456 的事務, relation_ship 傳入的值是 1,事務執行完之後, relation_ship 的值等於 2 | 1 = 3 

    這裡也可以看出 relation_ship 的枚舉值也不是隨意的,當然也可以選擇其他的,但是需要滿足如上的位運算邏輯

  4、insert ignore into friend

    其作用簡單點說就是:資料庫表中存在該記錄時忽略,不存在時插入

    同樣也是基於主鍵或唯一索引使用

  另外,在重覆調用時,按位或(|)和 insert ignore 可以保證冪等性

總結

  1、就文中這個業務而言,唯一索引的實現可讀性太差,不推薦大家使用

  2、 insert into on duplicate key update 和 insert ignore into 還是比較常見的,最好掌握它們

參考

  《MySQL 實戰 45 講》


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

-Advertisement-
Play Games
更多相關文章
  • 一、函數概述 • PL/SQL中的過程和函數(通常稱為子程式)是PL/SQL塊的一種特殊的類型,這種類型的子程式可以以編譯的形式存放在資料庫中,併為後續的程式塊調用。 • 相同點:完成特定功能的程式 • 不同點:是否用return語句返回值 二、函數語法 CREATE [OR REPLACE] FU ...
  • mysql精簡單機版,免登錄,可複製,不啟動服務,與本機mysql無衝突 ...
  • datebase管理 1.創建資料庫-create 語法:create database 資料庫名 character set 編碼 # 註意:預設會存在四個資料庫,其資料庫中存儲的是mysql資料庫伺服器的配置的數據 示例:create database firstDB character set ...
  • 搭建從庫,本質上需要的只是一個一致性備份集及這個備份集對應的位置點信息。之前介紹的幾個備份工具( MySQL中如何選擇合適的備份策略和備份工具 )均可滿足。 這裡,我們重點看看如何基於 XtraBackup 搭建從庫。 整個過程其實比較簡單,無非是備份還原。唯一需要註意的是建立複製時位置點的選擇,包 ...
  • 隨著表中記錄(數據行)的不斷積累,存儲數據逐漸增加,有時我們可能希望計算出這些數據的合計值或者平均值等。 本文介紹如何使用 SQL 語句對錶進行聚合和分組的方法。此外,還介紹在彙總操作時指定條件,以及對彙總結果進行升序、降序的排序方法。 一、對錶進行聚合查詢 本節重點 使用聚合函數對錶中的列進行計算 ...
  • 對實時數據湖的解讀 數據湖的概念是比較寬泛的,不同的人可能有著不同的解讀。這個名詞誕生以來,在不同的階段被賦予了不同的含義。 數據湖的概念最早是在 Hadoop World 大會上提出的。當時的提出者給數據湖賦予了一個非常抽象的含義,他認為它能解決數據集市面臨的一些重要問題。 其中最主要的兩個問題是 ...
  • 來源公眾號:SQL資料庫運維 原文鏈接:https://mp.weixin.qq.com/s?__biz=MzI1NTQyNzg3MQ==&mid=2247485400&idx=1&sn=432b580ed77224bf883db109cb7767b4&chksm=ea3753a8dd40dabed ...
  • 鎖 併發事務可能出現的情況: 讀-讀事務併發:此時是沒有問題的,讀操作不會對記錄又任何影響。 寫-寫事務併發:併發事務相繼對相同的記錄做出改動,因為寫-寫併發可能會產生臟寫的情況,但是沒有一個隔離級別允許臟寫的情況發生。MySQL使用鎖的機制來控制併發情況下讓事務對一條記錄進行排隊修改,只有對記錄修 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...