Compared with using a middleware to "brutally" cache interface response and improve interface query speed, Repository cache can better control cache granularity and update time Lu Xun.
Articles are updated in my Know about columns and Blog
scene
Tester-A: why is the getInfo interface so slow? 5+s? QPS is only 10!!!! RD-B: This is because getInfo needs to check the database... N multi Library Tester-B: how about optimizing it? RD-B: OK, let me operate (add a response buffer to the interface), OK, you can test it again Tester-B: (under test...) It's a lot faster. AI no, the user information in this interface is wrong. I have balaba clearly. There is no update here!!! RD-B: Oh, oh, I know. Let me operate it again (CACHE plus effective time, delete cache when personal information is updated), O So far, we have started some column tests for QPS + cache update... The end of the drama.
QPS and response time are very familiar indicators for engineers at the post (jie) end. These two values can directly reflect the performance of the interface and indirectly affect the smoothness of the front-end page...
The problem is coming.
How to improve interface query performance
After removing the factors of machine and programming language, we must start from the business scenario and analyze the reasons for the slow response of the interface. For example, the most common:
- Look up N multiple tables. The table has no index orz
- Useless data, increase the Size of transmission
- Repeatedly query some hot data, but directly hit the database every time
- Slow response of upstream services
- Other
Well, we only discuss the caching scheme of hot data here. After all, we need to analyze the specific scenarios, and the caching scheme is more general.
How to choose caching scheme
Serial number | Cache scheme | advantage | Inferiority |
---|---|---|---|
1 | Response cache | Simple violence | The timing of cache update is not well controlled. If everything is done, the mentality may collapse. The cache granularity is too large to update locally. It is helpful for query interface, but it is not helpful for other businesses to query data |
2 | Repository cache | The granularity is controlled by Repo itself, which can improve the overall application speed in Repo reuse scenarios | Need to cache for each Repo; more changes; other orz |
In general, the caching scheme of Repository is more elegant and controllable than the simple and violent middleware caching method in the above context.
Cache algorithm
When it comes to cache, it will definitely refer to cache replacement strategy. There are the most common ones: LRU LFU FIFO MRU (recently frequently used algorithm), multiple variants of LRU algorithm, LIRS, etc.
Here, LRU-K (K=2) is selected and the cached repository is implemented based on golang. For more details of the algorithm, see the LRU and LRU-K:
There are two interface s:
Cachelgor focuses on the interaction with Repo, so it only provides a simple add, delete, change and query, and the underlying is implemented based on Cache. The intention is to implement a variety of Cache replacement algorithms to enrich the 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 is to provide LRU like algorithm based cache and replacement capabilities, so the interface will be richer,
// 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() }
There are many articles on how to realize LRU or LRU-K on the Internet, and the principle is not complicated. Here, I will just go over the details and test the results directly
Simple test
Full code see 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
The level is limited. If there is any mistake, please correct it.
Code
github.com/yeqown/cached-repository