文章目錄
- 一、在高并發下,如何避免大量請求直接訪問數據庫?
- 二、避免緩存擊穿
- 二、避免緩存穿透
- 三、避免緩存雪崩
- 四、延遲雙刪保證數據一致性
- 五、在使用 Go 的 time.AfterFunc 函數時,如果刪除緩存操作失敗怎么辦?
MySQL
和
Redis
是工作中最常見的兩個組件,那么在使用過程中也有一些常見的問題和解決辦法。本文通過
Go
實現介紹它們。
一、在高并發下,如何避免大量請求直接訪問數據庫?
在高并發下,如果讓大量請求直接訪問數據庫,可能會造成數據庫壓力過大,響應延遲上升,嚴重時甚至可能導致數據庫崩潰。所以我們需要采取一些措施來保護數據庫,避免大量請求直接訪問數據庫。以下是一些常見的策略:
-
使用緩存:緩存是最常見、最直接的方法,可以大大減少數據庫的請求壓力。對于讀多寫少的場景尤其有效。不僅如此,合理使用緩存,還可以提高系統的響應速度。但是在使用緩存時要考慮
BigKey
、HotKey
、緩存擊穿、緩存穿透、緩存雪崩以及數據一致性等眾多問題。HotKey
和BigKey
可以看本人之前的兩篇文章:
HotKey
:29.Go處理Redis HotKey
BigKey
: 30.Go處理Redis BigKey -
限流:使用限流算法(如漏桶算法、令牌桶算法)來對訪問進行限制,保證系統在可接受的壓力范圍內運行,防止數據庫被過多的請求沖垮。
限流:13. Go中常見限流算法示例代碼 -
熔斷機制:引入熔斷器,當偵測到某個服務的錯誤次數過多(如因數據庫連接問題導致的錯誤)時,熔斷該服務的所有請求,直到服務恢復。
限流與熔斷:48.Go簡要實現令牌桶限流與熔斷器并集成到Gin框架中 -
負載均衡:如果有足夠的資源,可以采用負載均衡策略,如數據庫讀寫分離,主從復制等,將讀寫壓力分散到多個數據庫節點上。
負載均衡:15. Go實現負載均衡算法 -
使用隊列:對于非實時性的數據請求,可以采用異步處理的方式,將請求先放入隊列中,然后通過隊列對數據庫請求進行削峰填谷,避免大量請求同時涌向數據庫。
MQ:28.windows安裝kafka,Go操作kafka示例 -
數據庫優化:適當地對數據庫進行優化,如合理的索引、合理的表結構設計、
SQL
函數優化等,都可以提高數據庫處理請求的能力。
以上就是一些避免數據庫在高并發下被大量請求直接訪問的策略,可以根據具體的場景和需求選擇適合的策略。
二、避免緩存擊穿
緩存擊穿問題也叫熱點Key
問題,就是一個被高并發訪問并且緩存重建業務較復雜的Key
突然失效了,無數的請求訪問會在瞬間給數據庫帶來巨大的沖擊,即瞬時的高并發擊穿了緩存去請求DB
。
緩存雪崩和緩存擊穿的區別在于緩存擊穿針對某一Key緩存,緩存雪崩則是很多Key。
由于緩存擊穿是某個熱點Key
突然過期導致的,那么我們繼續把它加載進緩存就可以了,但是不能讓所有請求都去加載,只需要放行一個請求去加載,之后其他請求直接從緩存獲取,避免高并發流量打掛DB
,通過互斥鎖即可實現。
互斥鎖:在讀取緩存的過程中,如果緩存未命中,則添加鎖并從數據庫中查詢。這樣可以避免在高并發下,大量的請求直接訪問數據庫。
注意:使用double check機制
,此外,從DB
查詢后,如果DB
也不存在,應該緩存一個空對象,否則這些高并發請求會繼續請求DB
。在設置空對象時,也可以設置一個較短的過期時間,避免長時間緩存空對象。
v, found := cache.Get(key)
if !found {lock.Lock()defer lock.Unlock()// double checkv, found = cache.Get(key)if !found {v, err := db.Get(key)if err != nil {// handle errorreturn nil, err}if v == nil {v = EmptyObject}cache.Set(key, v, cache.DefaultExpiration)}
}
return v, nil
二、避免緩存穿透
緩存穿透是指用戶不斷對一個緩存和數據庫都不存在的數據進行訪問,對于這種情況,由于既在緩存中查不到,也在數據庫中查不到,于是每次都會對數據庫進行一次查詢,造成數據庫壓力增大,這種請求有可能是一個惡意攻擊。
Go 語言中我們可以使用以下策略來避免緩存穿透:
緩存空對象:把空結果也進行緩存,當后續請求再次查詢時,即使查不到數據,也會在緩存中得到一個空結果,而不會再對數據庫進行查詢。同時,為了避免未來的查詢都返回空結果,需要對空結果設置一個較短的過期時間。
偽代碼如下:
v, found := cache.Get(key)
if found {return v
}v, err := db.Get(key)
if err != nil {// handle errorreturn nil, err
}if v == nil {v = EmptyObjectcache.Set(key, v, cache.DefaultExpiration)
}
return v, nil
使用布隆過濾器:布隆過濾器(Bloom filter
)是一種用于測試一個元素是否在一個集合中的數據結構。由于它的存儲效率高且可以非常快速的查詢,所以常常被用來過濾掉一部分肯定不存在的數據,避免了對數據庫的無謂請求,在大數據量查找中有很高效率。
偽代碼如下:
if !bloomFilter.Exists(key) {return nil
}v, found := cache.Get(key)
if found {return v
}v, err := db.Get(key)
if err != nil {// handle errorreturn nil, err
}if v == nil {v = EmptyObject
}cache.Set(key, v, cache.DefaultExpiration)
return v, nil
在上面的示例中,如果布隆過濾器中不存在請求中的 key
,則直接返回,不對數據庫做查詢。
上述兩種方法可以有效應對緩存穿透,能夠減輕數據庫的壓力,提升程序的響應速度。
三、避免緩存雪崩
緩存雪崩是指在緩存系統中,大量數據同時過期,在訪問頻率高的情況下可能會引起數據庫的過載。
在Go語言中,我們可以采用以下措施來避免緩存雪崩:
1、設置緩存失效時間的隨機性
使得每一個key
的失效時間都是隨機的,防止所有緩存在同一時刻全部失效。例如,我們可以在原有的失效時間基礎上,增加一個隨機的延長時間。
expires := baseExpires + time.Duration(rand.Intn(randExpires)) * time.Second
cache.Set(key, value, expires)
2、使用數據版本控制
通過在每次緩存數據時將數據打上版本(可以是時間戳、或是遞增版本號等),每次訪問時,先訪問緩存,如果緩存不存在或者版本低于當前的版本,就更新緩存數據。這樣,即使緩存失效,也可以由單一線程去做更新,其它請求只需要等待即可。
func Get(key string, currentVersion int) (string, error) {v, version, found := cache.GetWithVersion(key)if found && version >= currentVersion {return v, nil}// Single flight to load from the database and put to the cache.// Usually done with a lock or using sync.Once type of logic.// ...return v, nil
}
3、熔斷機制和降級
在系統壓力過大或者服務不可用的情況下,可以進行熔斷降級,比如返回一些默認值,或者從備份緩存中讀數據。
整體來說,防止緩存雪崩主要是預防工作以及在系統異常時的快速應對。每一個解決方案都有其應用場景,需要根據具體業務情況進行選擇。
四、延遲雙刪保證數據一致性
使用到緩存,一般就需要考慮緩存與數據庫的一致性。如更新時,是先更新緩存還是先更新DB
,或者是先刪除緩存還是先刪除DB
、或者是否要通過監聽Binlog
同步緩存,或者做一些其他旁路校驗等。方案很多,需要針對當前業務是否能夠容忍緩存與DB
的不一致,以及容忍的程度如何來做具體涉設。但是最常用的還是延遲雙刪方案,成本低,容易實現,且能基本保障一致性。
延遲雙刪是解決緩存更新一致性問題的一個策略,具體策略如下:
- 先刪除緩存
- 再更新數據庫
- 最后延時刪除緩存
這樣做的目的是為了應對并發情況下緩存與數據庫數據不一致的問題。
Go語言實現延遲雙刪的一個簡單示例如下:
// 先刪除緩存
cache.Delete(key)// 更新數據庫
err := db.Update(key, value)
if err != nil {fmt.Printf("DB update error: %v", err)return
}// 延遲刪除緩存
time.AfterFunc(time.Duration(delayMillisecond)*time.Millisecond, func() {cache.Delete(key)
})
上述代碼的邏輯:
- 首先,通過
cache.Delete(key)
刪除舊的緩存數據。 - 然后,通過
db.Update(key, value)
更新數據庫數據。 - 最后,使用
Go
的time.AfterFunc
函數,實現一段時間后再次刪除緩存。時間可以依據實際業務情況設定。
這種方式可以在大部分場景下確保緩存和數據庫的數據一致,但還是存在極端情況下的問題,比如在第二次刪除緩存之前,有其他請求把舊的數據加載到了緩存。這種情況的出現幾率較小,如果業務對此有較高的要求,可能需要使用更嚴格的方案,如監聽Binlog
同步緩存。
五、在使用 Go 的 time.AfterFunc 函數時,如果刪除緩存操作失敗怎么辦?
在使用time.AfterFunc
時,如果刪除緩存操作失敗,最常見的處理辦法是進行重試操作。不過,在設置重試次數和重試延遲時,應謹慎考慮以防止無效操作導致系統資源的浪費。
func deleteCacheWithRetry(key string, retryTimes int, delay time.Duration) {for i := 0; i < retryTimes; i++ {// 嘗試刪除緩存err := cache.Delete(key)if err == nil {// 刪除成功return}// 如果刪除失敗,則等待一段時間再重試time.Sleep(delay)}// 在此處處理連續失敗的情況,例如記錄日志、發送告警等fmt.Printf("Failed to delete cache for key %s after %d attempts\n", key, retryTimes)
}// 在 time.AfterFunc 中使用
time.AfterFunc(time.Duration(delayMillisecond)*time.Millisecond, func() {deleteCacheWithRetry(key, 3, 1*time.Second)
})
在以上代碼中,我們定義了一個名為 deleteCacheWithRetry
的函數,它接受一個緩存鍵、重試次數和每次重試延遲的時間。它將嘗試刪除緩存,如果失敗,將等待一段時間后重試,直到達到最大重試次數。如果超過重試次數仍未成功,將通過日志記錄這個異常情況。
當然,具體的處理方式要根據項目具體需求和場景去判斷,以上只是一個參考示例。