程式碼解決快取穿透和快取雪崩問題

奇蹟師發表於2022-02-11

前言

  • 當我們去使用 redis 組合 golang 時,看到網上有關很多快取穿透和快取雪崩的問題,但是真正組合在一起的程式碼比較少,其實可以去多參考他人的程式碼編寫出自己的模板
  • 不過像這種已經成套體系,能用現成的才是最好的(笑)

什麼是快取穿透,快取雪崩,快取擊穿

1.快取雪崩

大量快取在同一時間失效

  • 解決方法: 設定快取時間為一定範圍的隨機數

2.快取穿透

快取和資料庫中都不存在(請求資料沒有被快取攔截,一直都在找資料庫,但是資料庫沒有,所以一直找)

  • 解決方法:當第一次命中時設定 該快取 value 為 DISABLE ,之後每次都只會打到該快取上

3.快取擊穿

快取失效後,有某些 key 被超高併發地訪問

  • 解決方法:使用互斥鎖,有鎖時,等待獲取

golang 快取穿透 + 快取雪崩 + 快取擊穿解決方法

1.第三方包

  • 資料庫對映框架 orm : gorm
  • 快取連線工具: go-redis
  • 錯誤封裝工具: pkg/errors

2.配置結構體 + 編寫快取 key + 快取設定時間

// 首先選好一個資料庫中的表
// 快取選用的結構是 序列化後的 string   ---
// (hash 不選是因為hash 無法同時設定過期時間,防止快取成為一個永久快取 -- 哈哈我才不說是因為懶)
type CatUser struct {
    ID        uint      `gorm:"column:id" json:"id"`
    CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
    UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
    UserId    int       `gorm:"column:user_id" json:"user_id"`
    Name      string    `gorm:"column:name" json:"name"`
    Password  string    `gorm:"cloumn:password" json:"password"`
}

// RedisKey 快取key
func (o *CatUser) RedisKey() string {
    // 這裡推薦: 1.用:分隔 1.如果有能夠識別唯一標識的 id ,用它 -- (用id也行)
    // user_id 能唯一標識該資料 -- 同 id 類似
    return fmt.Sprintf("cat_user:%d", o.UserId)
}

func (o *CatUser) ArrayRedisKey() string {
    return fmt.Sprintf("cat_user")
}

// 快取時間
func (o *CatUser) RedisDuration() time.Duration {
    // 這個時候可以用隨機時間 解決快取雪崩問題
    // 設定 30 ~ 60 分鐘  -- 這裡記得不要設定  0 ~ n 時間,因為萬一是 0 相當於沒有設定
    return time.Duration((rand.Intn(60-30) + 30)) * time.Minute
}

3.編寫快取操作

1.同步快取

// SyncToRedis 新增快取
// 使用 序列化後的 string 型別儲存 快取
func (o *CatUser) SyncToRedis(conn *redis.Conn) error {
    if o.RedisKey() == "" {
        return errors.New("not set redis key")
    }
    buf, err := json.Marshal(o)
    if err != nil {
        return errors.WithStack(err)
    }
    if err = conn.SetEX(context.Background(), o.RedisKey(), string(buf), o.RedisDuration()).Err(); err != nil {
        return errors.WithStack(err)
    }
    return nil
}

2.獲取快取

// 獲取快取
// 1.判斷是否存在 key
// 2.獲取是否為空
// 3.判斷是否快取穿透
// 3.獲取後反序列化

// GetFromRedis 獲取快取
func (o *CatUser) GetFromRedis(conn *redis.Conn) error {
    if o.RedisKey() == "" {
        return errors.New("not set redis key")
    }
    buf, err := conn.Get(context.Background(), o.RedisKey()).Bytes()
    if err != nil {
        if err == redis.Nil {
            return redis.Nil
        }
        return errors.WithStack(err)
    }
  // 是否出現過快取穿透
    if string(buf) == "DISABLE" {
        return errors.New("not found data in redis nor db")
    }

    if err = json.Unmarshal(buf, o); err != nil {
        return errors.WithStack(err)
    }
    return nil
}

3.刪除快取

func (o *CatUser) DeleteFromRedis(conn *redis.Conn) error {
    if o.RedisKey() != "" {
        if err := conn.Del(context.Background(), o.RedisKey()).Err(); err != nil {
            return errors.WithStack(err)
        }
    }
    // 同時刪除陣列快取
    if o.ArrayRedisKey() != "" {
        if err := conn.Del(context.Background(), o.ArrayRedisKey()).Err(); err != nil {
            return errors.WithStack(err)
        }
    }
    return nil
}

4.重點 – 獲取資料

// MustGet 獲取資料
// 1.先從快取中獲取
// 2.如果沒找到 --找資料庫 (也沒找到--設定DISABLE 防止快取穿透)
func (o *CatUser) MustGet(engine *gorm.DB, conn *redis.Conn) error {
    err := o.GetFromRedis(conn)
  // 如果為空證明找到了,提前返回不考慮後續操作
    if err == nil {
        return nil
    }

    if err != nil && err != redis.Nil {
        return errors.WithStack(err)
    }
    // 在快取中沒有找到這條資料,則從資料庫中找
    var count int64
    if err = engine.Count(&count).Error; err != nil {
        return errors.WithStack(err)
    }
    // 如果 為 count =0  設定 DISABLE 防止快取穿透
    if count == 0 {
        if err = conn.SetNX(context.Background(), o.RedisKey(), "DISABLE", o.RedisDuration()).Err(); err != nil {
            return errors.WithStack(err)
        }
        return errors.New("not found data in redis nor db")
    }

    // 這個時候找到了 -- 並且資料庫中存在資料 --加鎖防止快取擊穿
    // 設定 5 秒的互斥鎖鎖時間
    var mutex = o.RedisKey() + "_MUTEX"
    if err = conn.Get(context.Background(), mutex).Err(); err != nil {
    // 非 快取為空 異常錯誤,提前報錯
        if err != redis.Nil {
            return errors.WithStack(err)
        }
        // err == redis.Nil
    // 設定 5 s 的互斥鎖時間
        if err = conn.SetNX(context.Background(), mutex, 1, 3*time.Second).Err(); err != nil {
            return errors.WithStack(err)
        }
        // 從資料庫中查詢
        if err = engine.First(&o).Error; err != nil {
            return errors.WithStack(err)
        }
        // 同步快取
        if err = o.SyncToRedis(conn); err != nil {
            return errors.WithStack(err)
        }
        // 刪除鎖
        if err = conn.Del(context.Background(), mutex).Err(); err != nil {
            return errors.WithStack(err)
        }
    } else {
        // 這個時候不為空,加了鎖 -- 進行迴圈等等待
        var index int
        for {
            if index > 10{
                return errors.New(mutex + " lock error")
            }
            if err2 := conn.Get(context.Background(), mutex).Err(); err2 != nil {
                break
            } else {
                time.Sleep(30 * time.Millisecond)
                index++
                continue
            }
        }
        if err = o.MustGet(engine, conn); err != nil {
            return errors.WithStack(err)
        }
    }
    return nil
}

5. 陣列操作相同

func (o *CatUser) ArraySyncToRedis(list []CatUser, conn *redis.Conn) error {
    if o.ArrayRedisKey() == "" {
        return errors.New("not set redis key")
    }
    buf, err := json.Marshal(list)
    if err != nil {
        return errors.WithStack(err)
    }
    if err = conn.SetEX(context.Background(), o.ArrayRedisKey(), string(buf), o.RedisDuration()).Err(); err != nil {
        return errors.WithStack(err)
    }
    return nil
}

func (o *CatUser) ArrayGetFromRedis(conn *redis.Conn) ([]CatUser, error) {
    if o.RedisKey() == "" {
        return nil, errors.New("not set redis key")
    }
    buf, err := conn.Get(context.Background(), o.ArrayRedisKey()).Bytes()
    if err != nil {
        if err == redis.Nil {
            return nil, redis.Nil
        }
        return nil, errors.WithStack(err)
    }
    if string(buf) == "DISABLE" {
        return nil, errors.New("not found data in redis nor db")
    }

    var list []CatUser
    if err = json.Unmarshal(buf, &list); err != nil {
        return nil, errors.WithStack(err)
    }
    return list, nil
}

func (o *CatUser) ArrayDeleteFromRedis(conn *redis.Conn) error {
    return o.DeleteFromRedis(conn)
}

// ArrayMustGet
func (o *CatUser) ArrayMustGet(engine *gorm.DB, conn *redis.Conn) ([]CatUser, error) {
    list, err := o.ArrayGetFromRedis(conn)
    if err == nil {
        return list, nil
    }
    if err != nil && err != redis.Nil {
        return nil, errors.WithStack(err)
    }

    // not found in redis
    var count int64
    if err = engine.Count(&count).Error; err != nil {
        return nil, errors.WithStack(err)
    }
    if count == 0 {
        if err = conn.SetNX(context.Background(), o.ArrayRedisKey(), "DISABLE", o.RedisDuration()).Err(); err != nil {
            return nil, errors.WithStack(err)
        }
        return nil, errors.New("not found data in redis nor db")
    }

    var mutex = o.ArrayRedisKey() + "_MUTEX"
    if err = conn.Get(context.Background(), mutex).Err(); err != nil {
        if err != redis.Nil {
            return nil, errors.WithStack(err)
        }
        // err = redis.Nil
        if err = conn.SetNX(context.Background(), mutex, 1, 3*time.Second).Err(); err != nil {
            return nil, errors.WithStack(err)
        }
        if err = engine.Find(&list).Error; err != nil {
            return nil, errors.WithStack(err)
        }
        if err = o.ArraySyncToRedis(list, conn); err != nil {
            return nil, errors.WithStack(err)
        }
        if err = conn.Del(context.Background(), mutex).Err(); err != nil {
            return nil, errors.WithStack(err)
        }
    } else {
        var index int
        for {
            if index > 10 {
                return nil, errors.New(mutex + " lock error")
            }
            if err2 := conn.Get(context.Background(), mutex).Err(); err2 != nil {
                break
            } else {
                time.Sleep(50 * time.Millisecond)
                index++
                continue
            }
        }
        list, err = o.ArrayMustGet(engine, conn)
        if err != nil {
            return nil, errors.WithStack(err)
        }
    }
    return list, nil
}

6.單元測試

func TestCatUser(t *testing.T) {
    db := InitDb()
    conn := InitRedis()

    t.Run("single", func(t *testing.T) {
        var cu CatUser
        engine := db.Model(&CatUser{}).Where("user_id=?", 1)
        cu.UserId = 1
        cu.DeleteFromRedis(conn)
        if err := cu.MustGet(engine, conn); err != nil {
            fmt.Printf("%+v", err)
            panic(err)
        }
        fmt.Println(cu)
    })

    t.Run("list", func(t *testing.T) {
        var cu CatUser
        engine := db.Model(&CatUser{})
        list, err := cu.ArrayMustGet(engine, conn)
        if err != nil {
            panic(err)
        }
        fmt.Println(list)
    })
}

7.程式碼總結

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/catbugdemo/errors"
    "github.com/go-redis/redis/v8"
    "gorm.io/gorm"
    "math/rand"
    "time"
)

// 首先選好一個資料庫中的表
// 快取選用的結構是 序列化後的 string   ---
// (hash 不選是因為hash 無法同時設定過期時間,防止快取成為一個永久快取 -- 哈哈我才不說是因為懶)

// 確定一個結構體
type CatUser struct {
    ID        uint      `gorm:"column:id" json:"id"`
    CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
    UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
    UserId    int       `gorm:"column:user_id" json:"user_id"`
    Name      string    `gorm:"column:name" json:"name"`
    Password  string    `gorm:"cloumn:password" json:"password"`
}

// 然後寫設定快取 -- (因為這是開始 -- 同時也最簡單)

// RedisKey 快取key
func (o *CatUser) RedisKey() string {
    // 這裡推薦: 1.用:分隔 1.如果有能夠識別唯一標識的 id ,用它 -- (用id也行)
    // user_id 能唯一標識該資料 -- 同 id 類似
    return fmt.Sprintf("cat_user:%d", o.UserId)
}

func (o *CatUser) ArrayRedisKey() string {
    return fmt.Sprintf("cat_user")
}

// 時間
func (o *CatUser) RedisDuration() time.Duration {
    // 根據秒來識別
    // 這個時候可以用隨機時間 解決快取雪崩問題
    // 設定 30 ~ 60 分鐘  -- 這裡記得不要設定  0 ~ n 時間,因為萬一是 0 相當於沒有設定
    // 如果是 -1 則設定為永久
    return time.Duration((rand.Intn(60-30) + 30)) * time.Minute
}

// SyncToRedis 新增快取
// 使用 序列化後的 string 型別儲存 快取
func (o *CatUser) SyncToRedis(conn *redis.Conn) error {
    if o.RedisKey() == "" {
        return errors.New("not set redis key")
    }
    buf, err := json.Marshal(o)
    if err != nil {
        return errors.WithStack(err)
    }
    if err = conn.SetEX(context.Background(), o.RedisKey(), string(buf), o.RedisDuration()).Err(); err != nil {
        return errors.WithStack(err)
    }
    return nil
}

// 獲取快取
// 1.判斷是否存在 key
// 2.獲取是否為空
// 3.判斷是否快取穿透
// 3.獲取後反序列化

// GetFromRedis 獲取快取
func (o *CatUser) GetFromRedis(conn *redis.Conn) error {
    if o.RedisKey() == "" {
        return errors.New("not set redis key")
    }
    buf, err := conn.Get(context.Background(), o.RedisKey()).Bytes()
    if err != nil {
        if err == redis.Nil {
            return redis.Nil
        }
        return errors.WithStack(err)
    }
    if string(buf) == "DISABLE" {
        return errors.New("not found data in redis nor db")
    }

    if err = json.Unmarshal(buf, o); err != nil {
        return errors.WithStack(err)
    }
    return nil
}

func (o *CatUser) DeleteFromRedis(conn *redis.Conn) error {
    if o.RedisKey() != "" {
        if err := conn.Del(context.Background(), o.RedisKey()).Err(); err != nil {
            return errors.WithStack(err)
        }
    }
    // 設定
    if o.ArrayRedisKey() != "" {
        if err := conn.Del(context.Background(), o.ArrayRedisKey()).Err(); err != nil {
            return errors.WithStack(err)
        }
    }
    return nil
}

// MustGet 獲取資料
// 1.先從快取中獲取
// 2.如果沒找到 --找資料庫 (也沒找到--設定DISABLE 防止快取穿透)
func (o *CatUser) MustGet(engine *gorm.DB, conn *redis.Conn) error {
    err := o.GetFromRedis(conn)
    if err == nil {
        return nil
    }

    if err != nil && err != redis.Nil {
        return errors.WithStack(err)
    }
    // 在快取中沒有找到這條資料,則從資料庫中找
    var count int64
    if err = engine.Count(&count).Error; err != nil {
        return errors.WithStack(err)
    }
    // 如果 為 count =0  設定 DISABLE 防止快取穿透
    if count == 0 {
        if err = conn.SetNX(context.Background(), o.RedisKey(), "DISABLE", o.RedisDuration()).Err(); err != nil {
            return errors.WithStack(err)
        }
        return errors.New("not found data in redis nor db")
    }

    // 這個時候找到了 -- 並且資料庫中存在資料 --加鎖防止快取擊穿
    // 設定 5 秒的互斥鎖鎖時間
    var mutex = o.RedisKey() + "_MUTEX"
    if err = conn.Get(context.Background(), mutex).Err(); err != nil {
        if err != redis.Nil {
            return errors.WithStack(err)
        }
        // err == redis.Nil
        if err = conn.SetNX(context.Background(), mutex, 1, 5*time.Second).Err(); err != nil {
            return errors.WithStack(err)
        }
        // 從資料庫中查詢
        if err = engine.First(&o).Error; err != nil {
            return errors.WithStack(err)
        }
        // 同步快取
        if err = o.SyncToRedis(conn); err != nil {
            return errors.WithStack(err)
        }
        // 刪除鎖
        if err = conn.Del(context.Background(), mutex).Err(); err != nil {
            return errors.WithStack(err)
        }
    } else {
        // 這個時候不為空,加了鎖 -- 進行迴圈等等待
        var index int
        for {
            if index > 10 {
                return errors.New(mutex + " lock error")
            }
            if err2 := conn.Get(context.Background(), mutex).Err(); err2 != nil {
                break
            } else {
                time.Sleep(30 * time.Millisecond)
                index++
                continue
            }
        }
        if err = o.MustGet(engine, conn); err != nil {
            return errors.WithStack(err)
        }
    }
    return nil
}

// 同樣對陣列進行操作

// ArraySyncToRedis 新增快取
// 使用 序列化後的 string 型別儲存 快取
func (o *CatUser) ArraySyncToRedis(list []CatUser, conn *redis.Conn) error {
    if o.ArrayRedisKey() == "" {
        return errors.New("not set redis key")
    }
    buf, err := json.Marshal(list)
    if err != nil {
        return errors.WithStack(err)
    }
    if err = conn.SetEX(context.Background(), o.ArrayRedisKey(), string(buf), o.RedisDuration()).Err(); err != nil {
        return errors.WithStack(err)
    }
    return nil
}

// 獲取快取
// 1.判斷是否存在 key
// 2.獲取是否為空
// 3.判斷是否快取穿透
// 3.獲取後反序列化

// ArrayGetFromRedis 獲取快取
func (o *CatUser) ArrayGetFromRedis(conn *redis.Conn) ([]CatUser, error) {
    if o.RedisKey() == "" {
        return nil, errors.New("not set redis key")
    }
    buf, err := conn.Get(context.Background(), o.ArrayRedisKey()).Bytes()
    if err != nil {
        if err == redis.Nil {
            return nil, redis.Nil
        }
        return nil, errors.WithStack(err)
    }
    if string(buf) == "DISABLE" {
        return nil, errors.New("not found data in redis nor db")
    }

    var list []CatUser
    if err = json.Unmarshal(buf, &list); err != nil {
        return nil, errors.WithStack(err)
    }
    return list, nil
}

// ArrayDeleteFromRedis 刪除快取
func (o *CatUser) ArrayDeleteFromRedis(conn *redis.Conn) error {
    return o.DeleteFromRedis(conn)
}

// ArrayMustGet
func (o *CatUser) ArrayMustGet(engine *gorm.DB, conn *redis.Conn) ([]CatUser, error) {
    list, err := o.ArrayGetFromRedis(conn)
    if err == nil {
        return list, nil
    }
    if err != nil && err != redis.Nil {
        return nil, errors.WithStack(err)
    }

    // not found in redis
    var count int64
    if err = engine.Count(&count).Error; err != nil {
        return nil, errors.WithStack(err)
    }
    if count == 0 {
        if err = conn.SetNX(context.Background(), o.ArrayRedisKey(), "DISABLE", o.RedisDuration()).Err(); err != nil {
            return nil, errors.WithStack(err)
        }
        return nil, errors.New("not found data in redis nor db")
    }

    var mutex = o.ArrayRedisKey() + "_MUTEX"
    if err = conn.Get(context.Background(), mutex).Err(); err != nil {
        if err != redis.Nil {
            return nil, errors.WithStack(err)
        }
        // err = redis.Nil
        if err = conn.SetNX(context.Background(), mutex, 1, 5*time.Second).Err(); err != nil {
            return nil, errors.WithStack(err)
        }
        if err = engine.Find(&list).Error; err != nil {
            return nil, errors.WithStack(err)
        }
        if err = o.ArraySyncToRedis(list, conn); err != nil {
            return nil, errors.WithStack(err)
        }
        if err = conn.Del(context.Background(), mutex).Err(); err != nil {
            return nil, errors.WithStack(err)
        }
    } else {
        var index int
        for {
            if index > 10 {
                return nil, errors.New(mutex + " lock error")
            }
            if err2 := conn.Get(context.Background(), mutex).Err(); err2 != nil {
                break
            } else {
                time.Sleep(30 * time.Millisecond)
                index++
                continue
            }
        }
        list, err = o.ArrayMustGet(engine, conn)
        if err != nil {
            return nil, errors.WithStack(err)
        }
    }
    return list, nil
}

總結

  • 按照上面的方法,就能夠集合 包括 快取穿透 + 快取雪崩 + 快取擊穿的解決方法
  • 但是每次都要寫這麼多,單獨一個還行,多了就吃不消了
  • 沒關係,我寫了一個 自動生成模板,直接用就行了

自動生成模板地址

結語

  • 感謝各位讀者大大的閱讀

參考

github.com/fwhezfwhez/model_conver...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章