数据库缓存策略详解
1. 缓存架构概述
缓存是提升数据库性能的重要手段,通过将热点数据存储在内存中,减少数据库访问压力。
2. 多级缓存
2.1 本地缓存
import "github.com/patrickmn/go-cache"
var localCache = cache.New(5*time.Minute, 10*time.Minute)
// 设置缓存
localCache.Set("user:1", userData, cache.DefaultExpiration)
// 获取缓存
if value, found := localCache.Get("user:1"); found {
user := value.(*User)
return user, nil
}
2.2 分布式缓存
import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置缓存,带过期时间
err := rdb.Set(ctx, "user:1", data, 30*time.Minute).Err()
// 获取缓存
data, err := rdb.Get(ctx, "user:1").Bytes()
3. Cache-Aside模式
3.1 读操作
func GetUser(ctx context.Context, userID int) (*User, error) {
cacheKey := fmt.Sprintf("user:%d", userID)
// 1. 先查本地缓存
if localData, found := localCache.Get(cacheKey); found {
return localData.(*User), nil
}
// 2. 再查Redis
if redisData, err := rdb.Get(ctx, cacheKey).Bytes(); err == nil {
var user User
json.Unmarshal(redisData, &user)
localCache.Set(cacheKey, &user, 5*time.Minute)
return &user, nil
}
// 3. 最后查数据库
var user User
if err := db.First(&user, userID).Error; err != nil {
return nil, err
}
// 4. 回填缓存
data, _ := json.Marshal(&user)
rdb.Set(ctx, cacheKey, data, 30*time.Minute)
localCache.Set(cacheKey, &user, 5*time.Minute)
return &user, nil
}
3.2 写操作
func UpdateUser(ctx context.Context, user *User) error {
// 1. 更新数据库
if err := db.Save(user).Error; err != nil {
return err
}
cacheKey := fmt.Sprintf("user:%d", user.ID)
// 2. 删除缓存(不是更新)
localCache.Delete(cacheKey)
rdb.Del(ctx, cacheKey)
return nil
}
4. 缓存过期策略
4.1 TTL策略
// 设置不同过期时间
type CacheConfig struct {
UserCacheTTL time.Duration // 30分钟
ProductCacheTTL time.Duration // 5分钟
ConfigCacheTTL time.Duration // 24小时
}
// 热点数据设置较长的TTL
rdb.Set(ctx, "hot:product:1", data, 5*time.Minute)
// 冷门数据设置较短的TTL
rdb.Set(ctx, "cold:product:99999", data, 30*time.Second)
4.2 LRU策略
import "github.com/hashicorp/golang-lru"
lruCache, _ := lru.NewARC(10000) // 最多缓存10000条
// 使用LRU缓存
if value, ok := lruCache.Get("key"); ok {
return value.([]byte)
}
lruCache.Add("key", value)
5. 缓存问题
5.1 缓存穿透
func GetUserWithProtection(ctx context.Context, userID int) (*User, error) {
cacheKey := fmt.Sprintf("user:%d", userID)
//布隆过滤器检查
if !bloomFilter.Exists(cacheKey) {
return nil, ErrUserNotFound
}
// 正常的缓存查询
return getUserFromCache(ctx, userID)
}
// 布隆过滤器实现
type BloomFilter struct {
bitmap []byte
size uint
}
func (bf *BloomFilter) Add(key string) {
for i := uint(0); i < 3; i++ {
pos := hash(key, i) % bf.size
bf.bitmap[pos/8] |= 1 << (pos % 8)
}
}
func (bf *BloomFilter) Exists(key string) bool {
for i := uint(0); i < 3; i++ {
pos := hash(key, i) % bf.size
if bf.bitmap[pos/8]&(1<<(pos%8)) == 0 {
return false
}
}
return true
}
5.2 缓存击穿
import "sync"
type SingleFlight struct {
mu sync.Mutex
m map[string]*call
}
type call struct {
wg sync.WaitGroup
val interface{}
err error
}
func (sf *SingleFlight) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
sf.mu.Lock()
if sf.m == nil {
sf.m = make(map[string]*call)
}
if c, ok := sf.m[key]; ok {
sf.mu.Unlock()
c.wg.Wait()
return c.val, c.err
}
c := new(call)
c.wg.Add(1)
sf.m[key] = c
sf.mu.Unlock()
c.val, c.err = fn()
c.wg.Done()
sf.mu.Lock()
delete(sf.m, key)
sf.mu.Unlock()
return c.val, c.err
}
// 使用SingleFlight
user, err := sf.Do(fmt.Sprintf("user:%d", userID), func() (interface{}, error) {
return getUserFromDB(userID)
})
5.3 缓存雪崩
// 方案1:随机过期时间
func GetUser(ctx context.Context, userID int) (*User, error) {
cacheKey := fmt.Sprintf("user:%d", userID)
// 添加随机过期时间,避免同时过期
jitter := time.Duration(rand.Int63n(300)) * time.Second
data, err := rdb.Get(ctx, cacheKey).Bytes()
if err == nil {
var user User
json.Unmarshal(data, &user)
return &user, nil
}
user, err := getUserFromDB(userID)
if err != nil {
return nil, err
}
data, _ = json.Marshal(user)
rdb.Set(ctx, cacheKey, data, 30*time.Minute+jitter)
return user, nil
}
// 方案2:二级缓存
type TwoLevelCache struct {
l1 *cache.Cache
l2 *redis.Client
}
func (c *TwoLevelCache) Get(ctx context.Context, key string) ([]byte, error) {
// L1缓存
if val, found := c.l1.Get(key); found {
return val.([]byte), nil
}
// L2缓存
data, err := c.l2.Get(ctx, key).Bytes()
if err == nil {
c.l1.Set(key, data, 5*time.Minute)
return data, nil
}
return nil, err
}
6. 缓存一致性
6.1 双删策略
func UpdateUser(ctx context.Context, user *User) error {
cacheKey := fmt.Sprintf("user:%d", user.ID)
// 1. 先删除缓存
rdb.Del(ctx, cacheKey)
// 2. 更新数据库
if err := db.Save(user).Error; err != nil {
return err
}
// 3. 延迟再删除缓存
time.Sleep(100 * time.Millisecond)
rdb.Del(ctx, cacheKey)
return nil
}
6.2 Binlog同步
// 使用Canal监听数据库变更
type BinlogSync struct {
canal *canal.Client
rdb *redis.Client
}
func (s *BinlogSync) Start() error {
ddl, rows := s.canal.GetRows(100)
for _, row := range rows {
if row.EventType == canal.Update {
table := row.Table.Name
data := row.NewRow.Map
if table == "users" {
userID := data["id"]
cacheKey := fmt.Sprintf("user:%v", userID)
s.rdb.Del(ctx, cacheKey)
}
}
}
return nil
}
7. 总结
缓存是提升系统性能的重要手段,需要根据业务场景选择合适的缓存策略。Cache-Aside模式是最常用的缓存读写模式,同时需要注意缓存穿透、击穿、雪崩等问题,通过布隆过滤器、SingleFlight、随机过期时间等手段进行防护。
270

被折叠的 条评论
为什么被折叠?



