23

基于Repository设计缓存方案

 4 years ago
source link: https://studygolang.com/articles/24689
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

mEFBJfv.jpg!web

相比于使用一个中间件来“暴力”缓存接口的响应,提高接口查询速度而言,Repository缓存能更好的控制缓存粒度和更新时机 —— 鲁迅。

文章同步更新于我的 知乎专栏博客

场景

Tester—A:这个 getInfo 接口咋这么慢呢?查一下要5+s?QPS竟然只有10!!!!
RD-B    :这是因为getInfo要查库。。。N多库
Tester-B:那优化一下呗?
RD-B    :好的,容我操作一波(给接口加上一个响应缓存),好了你再测试一下
Tester-B:(测试中。。。),速度果然快了不少。诶不对,这个接口里拿到的用户信息不对,我明明已经balaba了,这里没有更新!!!
RD-B    :哦哦哦,我晓得咯,再容我操作一波(缓存加有效时间,个人信息更新的时候再强删缓存),O了

至此开始了针对于QPS+缓存更新的一些列测试。。。剧终。

QPS和响应时间是后(jie)端(kou)工程师非常熟悉的指标,这两个值能比较直观的反映该接口的性能,

间接

直接影响了前端页面的流畅度。。。

问题来了

接口 查询性能如何提高

除去机器和编程语言的因素之后,肯定要从业务场景出发,分析接口响应缓慢的原因。譬如,最常见的:

  1. 查N多表,表还没有索引orz
  2. 无用数据,增加传输的Size
  3. 反复查询某些 热点 数据,但每次都直接打到数据库
  4. 上游服务响应缓慢
  5. 其他

好了,这里只讨论热点数据的缓存方案,毕竟要具体场景具体分析,而缓存方案是比较通用的。

缓存方案如何选择

序号 缓存方案 优势 劣势 1 Response缓存 简单暴力 缓存更新时机不好把控,如果面面俱到可能心态崩坏;缓存粒度太大,无法局部更新;针对查询接口有帮助,其他业务下查询数据则毫无帮助 2 Repository缓存 粒度由Repo自行掌握,可控性强;Repo复用场景下会提高应用整体的速度 需要针对各个Repo做缓存的处理;改动较多;其他orz

总的来说,Repository的缓存方案,在上述背景上较简单暴力的中间件缓存法要更加优雅可控~。

缓存算法

提到缓存就一定会提到缓存替换策略,有最常见的: LRU LFU FIFO MRU(最近频繁使用算法) LRU的多个变种算法 LIRS 等。

这里选用了LRU-K(K=2)并基于 golang 来实现 cached-repository ,更多算法的详细信息参见参考文档中的 LRU和LRU-K :

这里分成了两个 interface :

CacheAlgor 重点在于与 Repo 交互,所以只提供了简单的增删改查,底层还是基于 Cache 来实现的。本意是想实现多种缓存替换算法来丰富 cached-repository ,orz

// cache.go
// CacheAlgor is an interface implements different alg.
type CacheAlgor interface {
    Put(key, value interface{})
    Get(key interface{}) (value interface{}, ok bool)
    Update(key, value interface{})
    Delete(key interface{})
}

lru.Cache 在于提供 基于 LRU-like 算法缓存和替换能力,所以接口会更丰富一些,

// lru/types.go
// Cache is the interface for simple LRU cache.
type Cache interface {
    // Puts a value to the cache, returns true if an eviction occurred and
    // updates the "recently used"-ness of the key.
    Put(key, value interface{}) bool

    // Returns key's value from the cache and
    // updates the "recently used"-ness of the key. #value, isFound
    Get(key interface{}) (value interface{}, ok bool)

    // Removes a key from the cache.
    Remove(key interface{}) bool

    // Peeks a key
    // Returns key's value without updating the "recently used"-ness of the key.
    Peek(key interface{}) (value interface{}, ok bool)

    // Returns the oldest entry from the cache. #key, value, isFound
    Oldest() (interface{}, interface{}, bool)

    // Returns a slice of the keys in the cache, from oldest to newest.
    Keys() []interface{}

    // Returns the number of items in the cache.
    Len() int

    // iter all key and items in cache
    Iter(f IterFunc)

    // Clears all cache entries.
    Purge()
}

关于如何实现 LRU 或者 LRU-K ,网上已经有很多文章了,原理也不复杂,这里就不过多赘述了,直接上测试结果

简单测试

完整代码参见 code

// MysqlRepo .
type MysqlRepo struct {
    db   *gorm.DB
    calg cp.CacheAlgor
    // *cp.EmbedRepo
}

// NewMysqlRepo .
func NewMysqlRepo(db *gorm.DB) (*MysqlRepo, error) {
    // func NewLRUK(k, size, hSize uint, onEvict EvictCallback) (*K, error)
    c, err := lru.NewLRUK(2, 10, 20, func(k, v interface{}) {
        fmt.Printf("key: %v, value: %v\n", k, v)
    })
    if err != nil {
        return nil, err
    }

    return &MysqlRepo{
        db:   db,
        // func New(c lru.Cache) CacheAlgor
        calg: cp.New(c),
    }, nil
}

// GetByID .
func (repo MysqlRepo) GetByID(id uint) (*userModel, error) {
    start := time.Now()
    defer func() {
        fmt.Printf("this queryid=%d cost: %d ns\n",id, time.Now().Sub(start).Nanoseconds())
    }()

    v, ok := repo.calg.Get(id)
    if ok {
        return v.(*userModel), nil
    }

    // actual find in DB
    m := new(userModel)
    if err := repo.db.Where("id = ?", id).First(m).Error; err != nil {
        return nil, err
    }

    repo.calg.Put(id, m)
    return m, nil
}

// Update .
func (repo MysqlRepo) Update(id uint, m *userModel) error {
    if err := repo.db.Where("id = ?", id).Update(m).Error; err != nil {
        return err
    }

    fmt.Printf("before: %v\n", m)
    m.ID = id
    if err := repo.db.First(m); err != nil {

    }
    fmt.Printf("after: %v\n", m)

    // update cache, ifcache hit id
    repo.calg.Put(id, m)

    return nil
}

// Delete .
func (repo MysqlRepo) Delete(id uint) error {
    if err := repo.db.Delete(nil, "id = ?", id).Error; err != nil {
        return err
    }

    repo.calg.Delete(id)
    return nil
}

func main() {
    // ... prepare data

    rand.Seed(time.Now().UnixNano())
    for i := 0; i < 1000; i++ {
        go func() {
            wg.Add(1)
            id := uint(rand.Intn(10))
            if id == 0 {
                continue
            }
    
            v, err := repo.GetByID(id)
            if err != nil {
                fmt.Printf("err: %d , %v\n", id, err)
                continue
            }
    
            if v.ID != id ||
                v.Name != fmt.Sprintf("name-%d", id) ||
                v.Province != fmt.Sprintf("province-%d", id) ||
                v.City != fmt.Sprintf("city-%d", id) {
                fmt.Printf("err: not matched target with id[%d]: %v\n", v.ID, v)
            }
            wg.Done()
        }()
    }
    wg.Wait()
}
➜  custom-cache-manage git:(master) ✗ go run main.go 
this queryid=9 cost: 245505 ns
this queryid=1 cost: 131838 ns
this queryid=3 cost: 128272 ns
this queryid=2 cost: 112281 ns
this queryid=7 cost: 123942 ns
this queryid=4 cost: 140267 ns
this queryid=7 cost: 148814 ns
this queryid=9 cost: 126904 ns
this queryid=6 cost: 129676 ns
this queryid=2 cost: 174202 ns
this queryid=1 cost: 151673 ns
this queryid=4 cost: 156370 ns
this queryid=3 cost: 159285 ns
this queryid=6 cost: 142215 ns
this queryid=3 cost: 691 ns
this queryid=1 cost: 450 ns
this queryid=8 cost: 160263 ns
this queryid=5 cost: 149655 ns
this queryid=4 cost: 756 ns
this queryid=8 cost: 143363 ns
this queryid=3 cost: 740 ns
this queryid=9 cost: 558 ns
this queryid=2 cost: 476 ns
this queryid=5 cost: 184098 ns
this queryid=1 cost: 824 ns
this queryid=8 cost: 556 ns
this queryid=9 cost: 632 ns
this queryid=7 cost: 480 ns
this queryid=5 cost: 439 ns
this queryid=5 cost: 409 ns
this queryid=7 cost: 431 ns
this queryid=6 cost: 479 ns
this queryid=4 cost: 423 ns
this queryid=8 cost: 423 ns
this queryid=1 cost: 411 ns
this queryid=6 cost: 423 ns
this queryid=8 cost: 394 ns
this queryid=7 cost: 410 ns
this queryid=9 cost: 424 ns
this queryid=4 cost: 428 ns
this queryid=2 cost: 433 ns
this queryid=4 cost: 420 ns
this queryid=9 cost: 424 ns
this queryid=6 cost: 406 ns
this queryid=6 cost: 399 ns
this queryid=5 cost: 405 ns
this queryid=2 cost: 428 ns
this queryid=9 cost: 383 ns
this queryid=4 cost: 399 ns
this queryid=7 cost: 413 ns
this queryid=4 cost: 381 ns
this queryid=1 cost: 427 ns
this queryid=2 cost: 430 ns
this queryid=1 cost: 468 ns
this queryid=1 cost: 406 ns
this queryid=4 cost: 380 ns
this queryid=2 cost: 360 ns
this queryid=3 cost: 660 ns
this queryid=6 cost: 393 ns
this queryid=5 cost: 419 ns
this queryid=7 cost: 1254 ns
this queryid=6 cost: 723 ns
this queryid=4 cost: 503 ns
this queryid=8 cost: 448 ns
this queryid=3 cost: 510 ns
this queryid=1 cost: 432 ns
this queryid=2 cost: 999 ns
this queryid=1 cost: 419 ns
this queryid=8 cost: 658 ns
this queryid=9 cost: 1322 ns
this queryid=9 cost: 543 ns
this queryid=4 cost: 1311 ns
this queryid=5 cost: 348 ns
this queryid=4 cost: 309 ns
this queryid=5 cost: 350 ns
this queryid=9 cost: 311 ns
this queryid=5 cost: 336 ns
this queryid=3 cost: 567 ns
this queryid=9 cost: 293 ns
this queryid=7 cost: 338 ns
this queryid=4 cost: 499 ns
this queryid=7 cost: 318 ns
this queryid=3 cost: 330 ns
this queryid=7 cost: 322 ns
this queryid=6 cost: 339 ns
this queryid=7 cost: 1273 ns
this queryid=4 cost: 1175 ns
this queryid=6 cost: 306 ns
this queryid=2 cost: 316 ns
this queryid=5 cost: 330 ns
this queryid=5 cost: 322 ns
this queryid=6 cost: 324 ns
this queryid=8 cost: 291 ns
this queryid=2 cost: 310 ns
this queryid=3 cost: 321 ns
this queryid=3 cost: 294 ns
this queryid=6 cost: 293 ns
this queryid=8 cost: 3566 ns
...more ignored

水平有限,如有错误,欢迎勘误指正:pray:。

代码

github.com/yeqown/cached-repository

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK