基于Repository设计缓存方案

相比于使用一个中间件来“暴力”缓存接口的响应,提高接口查询速度而言,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)工程师非常熟悉的指标,这两个值能比较直观的反映该接口的性能,
间接
直接影响了前端页面的流畅度。。。
问题来了
查询性能如何提高
接口
除去机器和编程语言的因素之后,肯定要从业务场景出发,分析接口响应缓慢的原因。譬如,最常见的:
- 查N多表,表还没有索引orz
- 无用数据,增加传输的Size
- 反复查询某些
数据,但每次都直接打到数据库
热点
- 上游服务响应缓慢
- 其他
好了,这里只讨论热点数据的缓存方案,毕竟要具体场景具体分析,而缓存方案是比较通用的。
缓存方案如何选择
序号 | 缓存方案 | 优势 | 劣势 |
---|---|---|---|
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