Tidb + 分布式鎖實現(xiàn)冪等--golang實現(xiàn)獎品發(fā)放系統(tǒng)

問題背景

最近業(yè)務(wù)上遇到這樣的場景,覺得很有代表性,所以拿來說一說。我們有一個獎品發(fā)放系統(tǒng),當(dāng)用戶申請獎品的時候,首先需要判斷用戶有沒有申請過獎品,如果沒有申請過,則去獎品總量扣除一個,然后再把用戶申請記錄寫回數(shù)據(jù)庫。

流程如下:

時序 事件
t1 檢測用戶申請過獎品
t2 獎品扣除
t3 插入用戶申請記錄
t4 返回申請成功

如果是正常的但線程執(zhí)行完全沒有問題,但是我們是并發(fā)的。所以很有可能會有好多事件都到t2導(dǎo)致多次申請,或者獎品多次扣除等情況。

解決方案

方案1:事務(wù)

最開始想到的就是數(shù)據(jù)庫的事務(wù)了,在t1開始前啟動事務(wù),t1查詢使用for update加鎖,其他事務(wù)繼續(xù)執(zhí)行相同條件的for update的時候則會block,直到上個事務(wù)執(zhí)行完成。這種方案很完美,但是只限于InnoDB,在tidb上行不通。tidb的事務(wù)是樂觀鎖,所以在t1的時候不會阻塞,那么可以在t3提交的時候conflict再回滾也可以啊。抱歉,tidb并不會產(chǎn)生插入沖突,因為tidb的鎖不支持gap lock和next-key lock,所以如果我們的獎品申請表沒有唯一索引沖突的話,完全可以插入(因為我某些設(shè)計,所以這里我們不能使用唯一索引)。

這個方案是行不通了,那么換種思路,既然tidb不支持for update block,那么我們是不是可以使用分布式鎖來解決。

方案2:分布式鎖

在檢測用戶有沒有申請過獎品之前,我們可以以用戶id為key申請分布式鎖(可以使用redis實現(xiàn)),申請成功則進行下一步,其他用戶阻塞等待鎖釋放。
具體實現(xiàn)如下:

        package logic

import (
    "errors"
    "fmt"
    "github.com/LucasGao67/blog001/dao"
    "github.com/LucasGao67/blog001/util"
    "sync"
)

var lock sync.Locker

func AppleAward(userId int64, remark string) error {

    // 1. 去申請鎖
    key := fmt.Sprintf("test:{%d}", userId)
    //lock.()
    util.LockBlock(key)
    defer util.UnLodk(key)

    // 2. 去查詢

    info, err := dao.Award.FindOne(userId)
    if err != nil {
        msg := "查詢獎品申請信息失敗"
        fmt.Printf("%s :%s\n", msg, err.Error())
        return errors.New("查詢獎品申請信息失敗")
    }

    if info != nil {
        msg := "已經(jīng)申請不能重復(fù)申請"
        fmt.Println(msg)
        return errors.New(msg)
    }
    // 3. 插入
    if _, err := dao.Award.InsertOne(userId, remark); err != nil {
        fmt.Println(err.Error())
        return err
    }

    // 結(jié)束
    return nil

}

測試

package logic

import (
    "fmt"
    "github.com/LucasGao67/blog001/util"
    "sync"
    "testing"
    "time"
)

func init() {
    util.SqlInit()
    util.RedisInit()

}

func TestAppleAward(t *testing.T) {
    // 插入1000條數(shù)據(jù)測試
    st := time.Now()
    numSucc := 0
    numFail := 0
    wg := sync.WaitGroup{}
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(i int) {
            err := AppleAward(1000+int64(i), "test 001")
            if err != nil {
                numFail++
            } else {
                numSucc++
            }
            wg.Done()
        }(i)
    }
    wg.Wait()
    et := time.Now()
    fmt.Printf("耗時: %ds\n", et.Unix()-st.Unix())
    fmt.Printf("成功: %d\n", numSucc)
    fmt.Printf("失?。?%d\n", numFail)
}

這樣如果userid都一樣是沒有問題的,但是一旦userid重復(fù)申請,都會阻塞到檢測userid狀態(tài)上,也就算上述的t1。所以這邊加鎖需要優(yōu)化下

方案三 分布式鎖優(yōu)化

采用最經(jīng)典的二次校驗,先查詢,滿足條件,加鎖,再校驗


func lockUtil(key string, exec func() error) (needUnlock bool, err error) {
    if err := exec(); err != nil {
        return false, err
    }
    if err := util.LockBlock(key); err != nil {
        // 申請鎖失敗
        return false, err
    }
    // 需要二次校驗
    if err := exec(); err != nil {
        return true, err
    }
}

func AppleAwardV2(userId int64, remark string) error {
    // 1. 去申請鎖
    key := fmt.Sprintf("test:{%d}", userId)
    needUnlock, err := lockUtil(key, func() error {
        info, err := dao.Award.FindOne(userId)
        if err != nil {
            msg := "查詢獎品申請信息失敗"
            fmt.Printf("%s :%s\n", msg, err.Error())
            return errors.New("查詢獎品申請信息失敗")
        }

        if info != nil {
            msg := "已經(jīng)申請不能重復(fù)申請"
            fmt.Println(msg)
            return errors.New(msg)
        }
        return nil
    })
    if needUnlock {
        util.UnLodk(key)
    }
    if err != nil {
        return err
    }
    // 3. 插入
    if _, err := dao.Award.InsertOne(userId, remark); err != nil {
        fmt.Println(err.Error())
        return err
    }
    return nil
}

github地址

https://github.com/LucasGao67/blog001

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容