當線上接口請求量比較大時,若是剛好遇到緩存失效,會形成大量的請求直接打到數據庫,致使數據庫壓力過大、甚至崩潰。若是緩存的數據實時性要求不那麼高,能夠試試 do-once-while-concurrent
https://github.com/abusizhish...mysql
do-once-while-concurrent
中有三個主要方法,git
Req 方法
對具備同一資源標識的請求進行攔截Wait 方法
等待數據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
和穩定性
緩存