前言
- 當我們去使用 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 協議》,轉載必須註明作者和本文連結