最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Mysql數(shù)據(jù)庫樂觀鎖與悲觀鎖示例詳解

 更新時間:2026年04月02日 09:55:41   作者:zhoupenghui168  
樂觀鎖和悲觀鎖是并發(fā)控制的一種機制,用于多線程或多進程環(huán)境下對共享資源的訪問管理,以防止數(shù)據(jù)不一致或競態(tài)條件,這篇文章主要介紹了Mysql數(shù)據(jù)庫樂觀鎖與悲觀鎖的相關(guān)資料,需要的朋友可以參考下

樂觀鎖悲觀鎖是兩種常見的并發(fā)控制機制,用于解決多用戶同時操作同一數(shù)據(jù)時的一致性問題

一、悲觀鎖(Pessimistic Locking)

1. 原理

  • 假設:并發(fā)沖突很可能發(fā)生,因此在讀取數(shù)據(jù)時就加鎖,防止其他事務修改。
  • 適用于寫操作頻繁、沖突概率高的場景。

2. MySQL 中的實現(xiàn)

通過 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE(8.0 后推薦用 FOR SHARE)實現(xiàn)行級鎖(InnoDB 引擎)。

-- 排他鎖(寫鎖):其他事務不能讀(除非快照讀)、不能寫
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 共享鎖(讀鎖):允許多個事務讀,但阻止寫
SELECT * FROM accounts WHERE id = 1 FOR SHARE;

?? 必須在事務中使用,否則鎖會立即釋放。

3. Gin + GORM 示例(悲觀鎖)

func TransferHandler(c *gin.Context) {
    tx := db.Begin()
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()
    var fromAccount Account
    // 悲觀鎖:鎖定 from 賬戶
    if err := tx.Set("gorm:query_option", "FOR UPDATE").
        Where("id = ?", 1).First(&fromAccount).Error; err != nil {
        tx.Rollback()
        c.JSON(400, gin.H{"error": "賬戶不存在"})
        return
    }
    var toAccount Account
    if err := tx.Set("gorm:query_option", "FOR UPDATE").
        Where("id = ?", 2).First(&toAccount).Error; err != nil {
        tx.Rollback()
        c.JSON(400, gin.H{"error": "目標賬戶不存在"})
        return
    }
    if fromAccount.Balance < 100 {
        tx.Rollback()
        c.JSON(400, gin.H{"error": "余額不足"})
        return
    }
    fromAccount.Balance -= 100
    toAccount.Balance += 100
    tx.Save(&fromAccount)
    tx.Save(&toAccount)
    tx.Commit()
    c.JSON(200, gin.H{"msg": "轉(zhuǎn)賬成功"})
}

? 優(yōu)點:強一致性,避免臟讀/丟失更新
? 缺點:性能差(鎖等待)、易死鎖、降低并發(fā)

二、樂觀鎖(Optimistic Locking)

1. 原理

  • 假設:并發(fā)沖突很少發(fā)生,因此不加鎖,只在更新時檢查數(shù)據(jù)是否被他人修改。
  • 通常通過 版本號(version)字段 或 時間戳 實現(xiàn)。

2. MySQL 中的實現(xiàn)

表結(jié)構(gòu)需包含 version 字段(整型):

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    stock INT,
    version INT DEFAULT 0
);

更新時帶上版本號條件:

UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 5;  -- 只有 version 未變才更新

如果返回 affected_rows == 0,說明數(shù)據(jù)已被他人修改,需重試或報錯。

3. Gin + GORM 示例(樂觀鎖)

GORM 內(nèi)置支持樂觀鎖(需使用 gorm.DeletedAt 同包下的 Version 字段):

type Product struct {
    ID      uint `gorm:"primarykey"`
    Name    string
    Stock   int
    Version uint32 // GORM 自動識別為樂觀鎖字段
}
func ReduceStock(c *gin.Context) {
    var product Product
    id := c.Param("id")
    // 第一次讀取
    if err := db.First(&product, id).Error != nil {
        c.JSON(404, gin.H{"error": "商品不存在"})
        return
    }
    // 業(yè)務邏輯:扣減庫存
    if product.Stock <= 0 {
        c.JSON(400, gin.H{"error": "庫存不足"})
        return
    }
    // 嘗試更新(GORM 自動在 WHERE 中加入 version 條件)
    product.Stock--
    result := db.Save(&product)
    if result.Error != nil {
        c.JSON(500, gin.H{"error": "數(shù)據(jù)庫錯誤"})
        return
    }
    if result.RowsAffected == 0 {
        // 樂觀鎖失?。喊姹静黄ヅ?
        c.JSON(409, gin.H{"error": "庫存已被其他請求修改,請重試"})
        return
    }
    c.JSON(200, gin.H{"msg": "扣減成功", "stock": product.Stock})
}

? 優(yōu)點:高并發(fā)、無鎖、性能好
? 缺點:沖突時需重試、不適合高頻寫沖突場景

三、樂觀鎖 vs 悲觀鎖 對比

特性悲觀鎖樂觀鎖
并發(fā)性能低(串行化)高(無鎖)
一致性保障強(事務隔離)最終一致(需處理沖突)
適用場景寫多讀少、沖突頻繁讀多寫少、沖突較少
實現(xiàn)復雜度簡單(SQL 加鎖)需版本字段 + 重試邏輯
死鎖風險
典型應用銀行轉(zhuǎn)賬、訂單支付商品庫存、點贊、評論計數(shù)

四、在 Gin 項目中的選型建議

場景推薦鎖類型說明
轉(zhuǎn)賬、資金結(jié)算悲觀鎖強一致性要求高,不能出錯
秒殺、搶購庫存扣減樂觀鎖 + 重試 或 Redis 預減庫存高并發(fā)下悲觀鎖性能差
用戶資料編輯樂觀鎖沖突少,體驗好
訂單狀態(tài)變更(如支付)悲觀鎖 或 狀態(tài)機校驗防止重復支付/狀態(tài)錯亂

?? 高并發(fā)場景(如秒殺)通常不直接依賴數(shù)據(jù)庫鎖,而是:

  • 使用 Redis 預減庫存 + 隊列異步落庫
  • 結(jié)合 Lua 腳本保證原子性
  • 數(shù)據(jù)庫僅做最終一致性校驗

五、GORM 樂觀鎖注意事項

  • 字段名必須為 Version(類型 uint32 或 int
  • GORM 在 Save() 或 Update() 時自動添加 WHERE version = ? 并遞增
  • 若使用 Updates(map),需手動包含 version 字段

六.總結(jié)

  • 悲觀鎖:適合強一致性、低并發(fā)寫場景,用 FOR UPDATE + 事務。
  • 樂觀鎖:適合高并發(fā)、沖突少場景,用 version 字段 + 重試機制。
  • 在 Gin + GORM 項目中,根據(jù)業(yè)務特性選擇合適方案,必要時結(jié)合緩存(Redis)提升性能。

實際項目中,混合使用也很常見:核心資金用悲觀鎖,普通業(yè)務用樂觀鎖。

七.Redis 預減庫存 + 消息隊列異步落庫

在高并發(fā)場景(如秒殺、搶購)中,直接操作數(shù)據(jù)庫扣減庫存極易導致性能瓶頸、超賣甚至系統(tǒng)崩潰。因此,業(yè)界普遍采用 “Redis 預減庫存 + 消息隊列異步落庫” 的架構(gòu)來兼顧 高性能、一致性與可靠性

1、整體架構(gòu)圖

用戶請求
    │
    ▼
[ Gin Web 服務 ] ←─┐
    │              │
    ▼              │
[ Redis 預減庫存 ] │ ←─ 庫存校驗 & 原子扣減(Lua 腳本)
    │              │
    ▼              │
[ 發(fā)送消息到 MQ ] ─┘ → [ Kafka / RabbitMQ / RocketMQ ]
    │
    ▼
[ 異步消費服務 ]
    │
    ▼
[ MySQL 落庫 ] ←─ 訂單創(chuàng)建、庫存最終扣減、記錄日志
    │
    ▼
[ 返回結(jié)果給用戶(可延遲)]

核心思想

  • 快速響應:Redis 操作毫秒級,用戶幾乎無等待
  • 削峰填谷:MQ 緩沖瞬時高并發(fā)
  • 最終一致:異步確保數(shù)據(jù)持久化

2、核心步驟詳解

步驟 1:初始化庫存到 Redis

  • 系統(tǒng)啟動或活動開始前,將商品庫存同步到 Redis。
  • 使用 String 類型 或 Hash 存儲,如 stock:product:1001 = 100
// 初始化庫存(管理后臺或定時任務調(diào)用)
redisClient.Set(ctx, "stock:product:1001", 100, 0)

步驟 2:用戶請求秒殺接口(Gin Handler)

  1. 參數(shù)校驗(用戶 ID、商品 ID)
  2. 防重放:檢查是否已下單(可用 Redis Set user:1001:product:1001
  3. Lua 腳本原子扣減庫存
    • 若庫存 > 0,則 DECR 并返回成功
    • 否則返回“庫存不足”
  4. 發(fā)送消息到 MQ(僅當 Redis 扣減成功)

?? 關(guān)鍵:Redis 扣減必須是原子操作,防止超賣!

步驟 3:Lua 腳本實現(xiàn)原子預減庫存

-- stock_decrease.lua
local key = KEYS[1]
local userId = ARGV[1]
-- 1. 檢查是否已搶購(防重)
if redis.call("EXISTS", "seckill:user:" .. userId .. ":product:" .. string.match(key, ":(%d+)$")) == 1 then
    return -2  -- 已參與
end
-- 2. 獲取當前庫存
local stock = tonumber(redis.call("GET", key))
if not stock or stock <= 0 then
    return -1  -- 庫存不足
end
-- 3. 扣減庫存
redis.call("DECR", key)
-- 4. 記錄用戶已參與(防重,TTL 可選)
redis.call("SET", "seckill:user:" .. userId .. ":product:" .. string.match(key, ":(%d+)$"), "1", "EX", 3600)
return stock - 1

返回值含義:

  • -2:已搶過
  • -1:庫存不足
  • >=0:剩余庫存,表示成功

步驟 4:Gin 處理秒殺請求(Go 代碼)

// main.go 或 handler/seckill.go
func SeckillHandler(c *gin.Context) {
    userID := c.GetString("user_id") // 假設已鑒權(quán)
    productID := c.Param("product_id")
    // 構(gòu)造 Redis Key
    stockKey := fmt.Sprintf("stock:product:%s", productID)
    userProductKey := fmt.Sprintf("seckill:user:%s:product:%s", userID, productID)
    // 執(zhí)行 Lua 腳本
    result, err := redisClient.Eval(
        ctx,
        luaScript,           // 上述 Lua 腳本內(nèi)容
        []string{stockKey},
        userID,
    ).Result()
    if err != nil {
        c.JSON(500, gin.H{"error": "系統(tǒng)繁忙"})
        return
    }
    switch ret := result.(type) {
    case int64:
        if ret == -1 {
            c.JSON(400, gin.H{"error": "庫存不足"})
            return
        }
        if ret == -2 {
            c.JSON(400, gin.H{"error": "您已參與過本次秒殺"})
            return
        }
    default:
        c.JSON(500, gin.H{"error": "未知錯誤"})
        return
    }
    // 成功!發(fā)送消息到 MQ(異步落庫)
    msg := SeckillMessage{
        UserID:    userID,
        ProductID: productID,
        Timestamp: time.Now(),
    }
    // 序列化并發(fā)送到 Kafka / RabbitMQ
    if err := mqProducer.Send("seckill_queue", msg); err != nil {
        // 注意:此處即使 MQ 發(fā)送失敗,Redis 已扣減,需有補償機制!
        log.Printf("MQ send failed: %v", err)
        // 可考慮回滾 Redis(復雜),或依賴后續(xù)對賬
    }
    // 立即返回用戶“搶購成功,請等待訂單生成”
    c.JSON(200, gin.H{
        "msg": "搶購成功!正在生成訂單...",
        "queue_status": "processing",
    })
}

步驟 5:異步消費服務(Worker)

// worker/seckill_worker.go
func StartSeckillWorker() {
    for msg := range mqConsumer.Subscribe("seckill_queue") {
        var seckillMsg SeckillMessage
        if err := json.Unmarshal(msg, &seckillMsg); err != nil {
            continue
        }
        // 開啟事務,落庫
        tx := db.Begin()
        defer tx.Rollback()
        // 1. 再次校驗(兜底):MySQL 中庫存是否足夠?
        var product Product
        if err := tx.Where("id = ? AND stock > 0", seckillMsg.ProductID).First(&product).Error; err != nil {
            log.Printf("MySQL 庫存不足或商品不存在: %v", seckillMsg)
            continue // 丟棄消息 or DLQ
        }
        // 2. 創(chuàng)建訂單
        order := Order{
            UserID:    seckillMsg.UserID,
            ProductID: seckillMsg.ProductID,
            Status:    "created",
        }
        if err := tx.Create(&order).Error != nil {
            continue
        }
        // 3. 扣減 MySQL 庫存
        if err := tx.Model(&Product{}).
            Where("id = ? AND stock = ?", seckillMsg.ProductID, product.Stock).
            Update("stock", gorm.Expr("stock - 1")).Error; err != nil {
            continue
        }
        tx.Commit()
        log.Printf("訂單創(chuàng)建成功: %v", order.ID)
    }
}

?? 兜底校驗很重要!防止 Redis 與 MySQL 數(shù)據(jù)不一致(如 Redis 重啟未同步)。

3、關(guān)鍵設計點與注意事項

問題解決方案
Redis 與 MySQL 數(shù)據(jù)不一致異步消費時做 MySQL 庫存二次校驗;定期對賬補償
MQ 消息丟失使用可靠消息(Kafka 副本、RabbitMQ 持久化 + ACK)
重復消費消費端冪等(如訂單表加唯一索引 (user_id, product_id)
Redis 宕機高可用部署(Redis Cluster / Sentinel)
超賣Lua 腳本保證原子性 + MySQL 兜底校驗
用戶重復提交Redis 記錄 user:product 防重鍵(帶 TTL)

4、擴展:失敗補償與對賬

  • 定時對賬任務:每天對比 Redis 初始庫存、Redis 當前庫存、MySQL 已售數(shù)量,發(fā)現(xiàn)差異則告警或自動修復。
  • 死信隊列(DLQ):處理多次失敗的消息,人工介入。
  • 前端輪詢/WebSocket:告知用戶“訂單已生成”,提升體驗。

5、總結(jié)

優(yōu)勢

  • 高并發(fā):Redis 承載 10w+ QPS
  • 防超賣:Lua 原子操作
  • 系統(tǒng)解耦:MQ 異步削峰
  • 最終一致:異步落庫 + 兜底校驗

復雜度

  • 需維護 Redis + MQ + 對賬系統(tǒng)
  • 調(diào)試和監(jiān)控難度增加

?? 適用場景:秒殺、搶購、限量發(fā)放等高并發(fā)、低轉(zhuǎn)化率業(yè)務

到此這篇關(guān)于Mysql數(shù)據(jù)庫樂觀鎖與悲觀鎖示例詳解的文章就介紹到這了,更多相關(guān)Mysql樂觀鎖與悲觀鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • mysql?復制行數(shù)據(jù)命令經(jīng)驗分享(Mysql?復制一條數(shù)據(jù))

    mysql?復制行數(shù)據(jù)命令經(jīng)驗分享(Mysql?復制一條數(shù)據(jù))

    有時候表里有一行已經(jīng)添加好的數(shù)據(jù),想要多復制幾百條用來測試比如要分頁測試等,需要多條數(shù)據(jù),但是有時候數(shù)據(jù)表字段太多了,有幾十個,一個一個手寫那是不可能的
    2023-09-09
  • MySQL學習之DDL數(shù)據(jù)庫定義與操作

    MySQL學習之DDL數(shù)據(jù)庫定義與操作

    本文詳細介紹SQL中DDL的數(shù)據(jù)庫操作,包括查詢、創(chuàng)建、刪除數(shù)據(jù)庫和表的操作,以及修改表結(jié)構(gòu)等功能,通過這些操作,讀者可以深入了解如何使用SQL進行數(shù)據(jù)庫管理和維護,需要的朋友可以參考下
    2024-11-11
  • xampp修改mysql默認密碼的方法

    xampp修改mysql默認密碼的方法

    在這里介紹xampp修改mysql默認密碼的大概過程是先利用xampp的phpmyadmin進入修改mysql密碼,修改之后我們再修改xampp中phpmyadmin的密碼,這樣就完整的修改mysql默認密碼了,感興趣的朋友一起通過本文學習吧
    2016-10-10
  • MySQL?where和having的異同

    MySQL?where和having的異同

    我們在進行查詢的時候,經(jīng)常需要按照條件對查詢結(jié)果進行篩選,這就要用到條件語句where和having了,本文主要介紹了MySQL?where和having的異同,具有一定的參考價值,感興趣的可以了解一下
    2024-02-02
  • 使用mysql語句查看數(shù)據(jù)庫表所占容量空間大小

    使用mysql語句查看數(shù)據(jù)庫表所占容量空間大小

    這篇文章主要給大家介紹了關(guān)于如何使用mysql語句查看數(shù)據(jù)庫表所占容量空間大小的相關(guān)資料,如何在MySQL數(shù)據(jù)庫管理中查詢數(shù)據(jù)庫、表、索引的容量大小是經(jīng)常遇到的需求,需要的朋友可以參考下
    2023-08-08
  • 查詢數(shù)據(jù)庫空間(mysql和oracle)

    查詢數(shù)據(jù)庫空間(mysql和oracle)

    本文通過代碼示例詳細介紹了如何查詢MySQL數(shù)據(jù)空間和Oracle數(shù)據(jù)空間,具有一定的參考價值,感興趣的小伙伴可以參考閱讀
    2023-04-04
  • MySQL limit性能分析與優(yōu)化

    MySQL limit性能分析與優(yōu)化

    今天小編就為大家分享一篇關(guān)于MySQL limit性能分析與優(yōu)化,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-02-02
  • MySQL中常見關(guān)鍵字的用法總結(jié)

    MySQL中常見關(guān)鍵字的用法總結(jié)

    這篇文章主要為大家詳細介紹了MySQL中常見關(guān)鍵字的用法,例如GROUP BY、ORDER BY和LIMIT,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下
    2023-09-09
  • MySQL事務&用戶與權(quán)限管理方式

    MySQL事務&用戶與權(quán)限管理方式

    本文介紹了事務的概念以及ACID特性,事務能在并發(fā)環(huán)境中保證數(shù)據(jù)正確性和一致性,重點介紹了MySQL事務的實現(xiàn),包括開啟和關(guān)閉事務、保存點、自動提交/手動提交等,此外,還詳細解釋了事務的隔離級別和常見問題,并介紹了用戶權(quán)限管理的重要性及基本操作
    2026-05-05
  • MySQL之union和union all的使用及區(qū)別說明

    MySQL之union和union all的使用及區(qū)別說明

    這篇文章主要介紹了MySQL之union和union all的使用及區(qū)別說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-04-04

最新評論

麻阳| 汝城县| 融水| 定安县| 富民县| 文化| 新河县| 敦煌市| 仁寿县| 辽宁省| 鹤壁市| 会同县| 阳泉市| 宣威市| 两当县| 嵊州市| 富阳市| 辉南县| 纳雍县| 卓尼县| 达孜县| 蓬莱市| 珲春市| 介休市| 吉木萨尔县| 永顺县| 徐汇区| 密山市| 新闻| 威信县| 宝丰县| 通道| 清苑县| 南通市| 抚远县| 金塔县| 上高县| 林芝县| 社会| 开平市| 富锦市|