基于Repository设计缓存方案
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.
相比于使用一个中间件来“暴力”缓存接口的响应,提高接口查询速度而言,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
参考
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK