Design cache scheme based on Repository

Keywords: Go Database Programming git github

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:

  1. Look up N multiple tables. The table has no index orz
  2. Useless data, increase the Size of transmission
  3. Repeatedly query some hot data, but directly hit the database every time
  4. Slow response of upstream services
  5. 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

Reference resources

Posted by snapbackz on Thu, 14 Nov 2019 22:10:25 -0800