go防止緩存穿透

當線上接口請求量比較大時,若是剛好遇到緩存失效,會形成大量的請求直接打到數據庫,致使數據庫壓力過大、甚至崩潰。若是緩存的數據實時性要求不那麼高,能夠試試 do-once-while-concurrent
https://github.com/abusizhish...mysql

do-once-while-concurrent中有三個主要方法,git

  1. Req 方法 對具備同一資源標識的請求進行攔截
  2. Wait 方法 等待數據
  3. Release 方法 廣播信號,數據已就位

下面是一個簡單的示例
咱們的實際項目中有 兩級緩存 ,一級 本地緩存 ,一級 redis ,若是都查詢不到纔會 讀取mysql調用中臺接口 ,本次只模擬 本地緩存失效 時, do-once-while-concurrent 對防止 緩存穿透 的處理(實際叫 重複資源過濾 更合理)github

1.緩存失效時, 全部請求該緩存的請求會先調用 Req方法 對具備相同標籤的重複請求進行攔截
2.只有第一個請求會 獲取鎖 ,執行讀取redis操做
3.全部其餘的線程 獲取鎖 失敗,調用 Wait 方法,等待第一個線程 執行結束
4.第一個線程讀取到用戶信息,寫入本地緩存,經過 close(chan) 事件來 廣播消息
5.其餘線程收到消息,結束 等待 ,讀取本地緩存,返回用戶信息redis

package main  
  
import (  
   "errors"  
   "fmt" 
   "github.com/abusizhishen/do-once-while-concurrent/src" 
   "log" 
   "sync" 
   "time"
)  
  
func main() {  
   //併發do something  
   for i := 0; i < 5; i++ {  
      go doSomeThing()  
   }  
  
   //避免程序直接退出
   time.Sleep(time.Second * 5)  
}  
  
var once src.DoOnce  
  
//模擬獲取用戶信息  
func doSomeThing() {  
   var userId = 12345  
   var user, err = getUserInfo(userId)  
   fmt.Println(user, err)  
}  
  
//example for usage  
// 演示獲取用戶詳情的過程,先從本地緩存讀取用戶,若是本地緩存不存在,就從redis讀取  
var keyUser = "user_%d"  
  
func getUserInfo(userId int) (user UserInfo, err error) {  
   user, err = userCache.GetUser(userId)  
   if err == nil {  
      return  
  }  
  
   log.Println(err)  
   var requestTag = fmt.Sprintf(keyUser, userId)  
   if !once.Req(requestTag) {  
      log.Println("沒搶到鎖,等待搶到鎖的線程執行結束。。。")  
      once.Wait(requestTag)  
      log.Println("等待結束:", requestTag)  
      return userCache.GetUser(userId)  
   }  
  
   //獲得資源後釋放鎖  
   defer once.Release(requestTag)  
   log.Println(requestTag, "得到鎖,let's Go")  
  
   //爲演示效果,sleep  
  time.Sleep(time.Second * 3)  
  
   //redis讀取用戶信息  
  log.Println("redis讀取用戶信息:", userId)  
  user, err = getUserInfoFromRedis(userId)  
  if err != nil {  
     return  
  }  
  
   //用戶寫入緩存  
  log.Println("用戶寫入緩存:", userId)  
  userCache.setUser(user)  
  return  
}  
  
//用戶信息緩存  
type UserCache struct {  
   Users map[int]UserInfo  
   sync.RWMutex  
}  
  
type UserInfo struct {  
  Id   int  
  Name string  
  Age  int  
}  
  
var userCache UserCache  
var errUserNotFound = errors.New("user not found in cache")  
  
func (c *UserCache) GetUser(id int) (user UserInfo, err error) {  
   c.RLock()  
   defer c.RUnlock()  
   var ok bool  
   user, ok = userCache.Users[id]  
   if ok {  
      return  
   }  
  
   return user, errUserNotFound  
}  
  
func (c *UserCache) setUser(user UserInfo) {  
   c.Lock()  
   defer c.Unlock()  
   if c.Users == nil {  
      c.Users = make(map[int]UserInfo)  
   }  
  
   c.Users[user.Id] = user  
   return  
}  
  
func getUserInfoFromRedis(id int) (user UserInfo, err error) {  
   user = UserInfo{  
      Id:   12345,  
      Name: "abusizhishen",  
      Age:  18,  
  }  
   return  
}

輸出sql

2020/03/09 20:11:39 user not found in cache
2020/03/09 20:11:39 user_12345 得到鎖,let's Go
2020/03/09 20:11:39 user not found in cache
2020/03/09 20:11:39 沒搶到鎖,等待搶到鎖的線程執行結束。。。
2020/03/09 20:11:39 user not found in cache
2020/03/09 20:11:39 沒搶到鎖,等待搶到鎖的線程執行結束。。。
2020/03/09 20:11:39 user not found in cache
2020/03/09 20:11:39 user not found in cache
2020/03/09 20:11:39 沒搶到鎖,等待搶到鎖的線程執行結束。。。
2020/03/09 20:11:39 沒搶到鎖,等待搶到鎖的線程執行結束。。。
2020/03/09 20:11:42 redis讀取用戶信息: 12345
2020/03/09 20:11:42 用戶寫入緩存: 12345
2020/03/09 20:11:42 等待結束: user_12345
2020/03/09 20:11:42 等待結束: user_12345
{12345 abusizhishen 18} <nil>
{12345 abusizhishen 18} <nil>
{12345 abusizhishen 18} <nil>
2020/03/09 20:11:42 等待結束: user_12345
{12345 abusizhishen 18} <nil>
2020/03/09 20:11:42 等待結束: user_12345
{12345 abusizhishen 18} <nil>

能夠看到,當第一個線程 獲取鎖 後,其餘線程所有處於 等待狀態,直到第一個線程 執行結果釋放鎖 ,其餘線程 獲取到數據 ,返回結果數據庫

事實上不止於防止 緩存穿透, do-once-while-concurrent 更準確的定位是 重複資源過濾 ,,在某講座業務中,使用 do-once-while-concurrent 來避免同一時刻同一用戶id 重複解析 、列表頁 重複檢索排序 等,減小了資源競爭,提升了總體的qps穩定性緩存

相關文章
相關標籤/搜索