数据库缓存策略详解

数据库缓存策略详解

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、随机过期时间等手段进行防护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值