基于Repository设计缓存方案

徐晃
• 阅读 4028

基于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)工程师非常熟悉的指标,这两个值能比较直观的反映该接口的性能,间接直接影响了前端页面的流畅度。。。


问题来了

接口查询性能如何提高

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

  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
水平有限,如有错误,欢迎勘误指正🙏。

代码

github.com/yeqown/cached-repository

参考

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这