千家信息网

go-zero如何自动管理缓存

发表于:2025-02-22 作者:千家信息网编辑
千家信息网最后更新 2025年02月22日,本篇内容主要讲解"go-zero如何自动管理缓存",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"go-zero如何自动管理缓存"吧!go-zero 概览go
千家信息网最后更新 2025年02月22日go-zero如何自动管理缓存

本篇内容主要讲解"go-zero如何自动管理缓存",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"go-zero如何自动管理缓存"吧!

go-zero 概览

go-zero 虽然是20年8月7号才开源,但是已经经过线上大规模检验了,也是我近20年工程经验的积累,开源后得到社区的积极反馈,在5个多月的时间里,获得了6k stars。多次登顶github Go语言日榜、周榜、月榜榜首,并获得了gitee最有价值项目(GVP),开源中国年度最佳人气项目。同时微信社区极为活跃,3000+人的社区群,go-zero爱好者们一起交流go-zero使用心得和讨论使用过程中的问题。

go-zero 如何自动管理缓存?

缓存设计原理

我们对缓存是只删除,不做更新,一旦DB里数据出现修改,我们就会直接删除对应的缓存,而不是去更新。

我们看看删除缓存的顺序怎样才是正确的。

  • 先删除缓存,再更新DB

我们看两个并发请求的情况,A请求需要更新数据,先删除了缓存,然后B请求来读取数据,此时缓存没有数据,就会从DB加载数据并写回缓存,然后A更新了DB,那么此时缓存内的数据就会一直是脏数据,直到缓存过期或者有新的更新数据的请求。如图

  • 先更新DB,再删除缓存

A请求先更新DB,然后B请求来读取数据,此时返回的是老数据,此时可以认为是A请求还没更新完,最终一致性,可以接受,然后A删除了缓存,后续请求都会拿到最新数据,如图

让我们再来看一下正常的请求流程:

  1. 第一个请求更新DB,并删除了缓存

  2. 第二个请求读取缓存,没有数据,就从DB读取数据,并回写到缓存里

  3. 后续读请求都可以直接从缓存读取

我们再看一下DB查询有哪些情况,假设行记录里有ABCDEFG七列数据:

  1. 只查询部分列数据的请求,比如请求其中的ABC,CDE或者EFG等,如图

  1. 查询单条完整行记录,如图

  1. 查询多条行记录的部分或全部列,如图

对于上面三种情况,首先,我们不用部分查询,因为部分查询没法缓存,一旦缓存了,数据有更新,没法定位到有哪些数据需要删除;其次,对于多行的查询,根据实际场景和需要,我们会在业务层建立对应的从查询条件到主键的映射;而对于单行完整记录的查询,go-zero 内置了完整的缓存管理方式。所以核心原则是:go-zero 缓存的一定是完整的行记录

下面我们来详细介绍 go-zero 内置的三种场景的缓存处理方式:

  1. 基于主键的缓存

    PRIMARY KEY (`id`)


    这种相对来讲是最容易处理的缓存,只需要在 redis 里用 primary key 作为 key 来缓存行记录即可。

  2. 基于唯一索引的缓存

    在做基于索引的缓存设计的时候我借鉴了 database 索引的设计方法,在 database 设计里,如果通过索引去查数据时,引擎会先在 索引->主键tree 里面查找到主键,然后再通过主键去查询行记录,就是引入了一个间接层去解决索引到行记录的对应问题。在 go-zero 的缓存设计里也是同样的原理。

    基于索引的缓存又分为单列唯一索引和多列唯一索引:

    但是对于 go-zero 来说,单列和多列只是生成缓存 key 的方式不同而已,背后的控制逻辑是一样的。然后 go-zero 内置的缓存管理就比较好的控制了数据一致性问题,同时也内置防止了缓存的击穿、穿透、雪崩问题(这些在 gopherchina 大会上分享的时候仔细讲过,见后续 gopherchina 分享视频)。

    另外,go-zero 内置了缓存访问量、访问命中率统计,如下所示:

    dbcache(sqlc) - qpm: 5057, hit_ratio: 99.7%, hit: 5044, miss: 13, db_fails: 0


    可以看到比较详细的统计信息,便于我们来分析缓存的使用情况,对于缓存命中率极低或者请求量极小的情况,我们就可以去掉缓存了,这样也可以降低成本。

    • 单列唯一索引如下:

      UNIQUE KEY `product_idx` (`product`)


    • 多列唯一索引如下:

      UNIQUE KEY `vendor_product_idx` (`vendor`, `product`)


缓存代码解读

1. 基于主键的缓存逻辑

具体实现代码如下:

func (cc CachedConn) QueryRow(v interface{}, key string, query QueryFn) error {  return cc.cache.Take(v, key, func(v interface{}) error {    return query(cc.db, v)  })}

这里的 Take 方法是先从缓存里去通过 key 拿数据,如果拿到就直接返回,如果拿不到,那么就通过 query 方法去 DB 读取完整行记录并写回缓存,然后再返回数据。整个逻辑还是比较简单易懂的。

我们详细看看 Take 的实现:

func (c cacheNode) Take(v interface{}, key string, query func(v interface{}) error) error {  return c.doTake(v, key, query, func(v interface{}) error {    return c.SetCache(key, v)  })}

Take 的逻辑如下:

  • key 从缓存里查找数据

  • 如果找到,则返回数据

  • 如果找不到,用 query 方法去读取数据

  • 读到后调用 c.SetCache(key, v) 设置缓存

其中的 doTake 代码和解释如下:

// v - 需要读取的数据对象// key - 缓存key// query - 用来从DB读取完整数据的方法// cacheVal - 用来写缓存的方法func (c cacheNode) doTake(v interface{}, key string, query func(v interface{}) error,  cacheVal func(v interface{}) error) error {  // 用barrier来防止缓存击穿,确保一个进程内只有一个请求去加载key对应的数据  val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {    // 从cache里读取数据    if err := c.doGetCache(key, v); err != nil {      // 如果是预先放进来的placeholder(用来防止缓存穿透)的,那么就返回预设的errNotFound      // 如果是未知错误,那么就直接返回,因为我们不能放弃缓存出错而直接把所有请求去请求DB,      // 这样在高并发的场景下会把DB打挂掉的      if err == errPlaceholder {        return nil, c.errNotFound      } else if err != c.errNotFound {        // why we just return the error instead of query from db,        // because we don't allow the disaster pass to the DBs.        // fail fast, in case we bring down the dbs.        return nil, err      }      // 请求DB      // 如果返回的error是errNotFound,那么我们就需要在缓存里设置placeholder,防止缓存穿透      if err = query(v); err == c.errNotFound {        if err = c.setCacheWithNotFound(key); err != nil {          logx.Error(err)        }        return nil, c.errNotFound      } else if err != nil {        // 统计DB失败        c.stat.IncrementDbFails()        return nil, err      }      // 把数据写入缓存      if err = cacheVal(v); err != nil {        logx.Error(err)      }    }        // 返回json序列化的数据    return jsonx.Marshal(v)  })  if err != nil {    return err  }  if fresh {    return nil  }  // got the result from previous ongoing query  c.stat.IncrementTotal()  c.stat.IncrementHit()  // 把数据写入到传入的v对象里  return jsonx.Unmarshal(val.([]byte), v)}
2. 基于唯一索引的缓存逻辑

因为这块比较复杂,所以我用不同颜色标识出来了响应的代码块和逻辑,block 2 其实跟基于主键的缓存是一样的,这里主要讲 block 1 的逻辑。

代码块的 block 1 部分分为两种情况:

  1. 通过索引能够从缓存里找到主键

    此时就直接用主键走 block 2 的逻辑了,后续同上面基于主键的缓存逻辑

  2. 通过索引无法从缓存里找到主键

    • 通过索引从DB里查询完整行记录,如有 error,返回

    • 查到完整行记录后,会把主键到完整行记录的缓存和索引到主键的缓存同时写到 redis

    • 返回所需的行记录数据

// v - 需要读取的数据对象// key - 通过索引生成的缓存key// keyer - 用主键生成基于主键缓存的key的方法// indexQuery - 用索引从DB读取完整数据的方法,需要返回主键// primaryQuery - 用主键从DB获取完整数据的方法func (cc CachedConn) QueryRowIndex(v interface{}, key string, keyer func(primary interface{}) string,  indexQuery IndexQueryFn, primaryQuery PrimaryQueryFn) error {  var primaryKey interface{}  var found bool  // 先通过索引查询缓存,看是否有索引到主键的缓存  if err := cc.cache.TakeWithExpire(&primaryKey, key, func(val interface{}, expire time.Duration) (err error) {    // 如果没有索引到主键的缓存,那么就通过索引查询完整数据    primaryKey, err = indexQuery(cc.db, v)    if err != nil {      return    }    // 通过索引查询到了完整数据,设置found,后面直接使用,不需要再从缓存读取数据了    found = true    // 将主键到完整数据的映射保存到缓存里,TakeWithExpire方法已经将索引到主键的映射保存到缓存了    return cc.cache.SetCacheWithExpire(keyer(primaryKey), v, expire+cacheSafeGapBetweenIndexAndPrimary)  }); err != nil {    return err  }  // 已经通过索引找到了数据,直接返回即可  if found {    return nil  }  // 通过主键从缓存读取数据,如果缓存没有,通过primaryQuery方法从DB读取并回写缓存再返回数据  return cc.cache.Take(v, keyer(primaryKey), func(v interface{}) error {    return primaryQuery(cc.db, v, primaryKey)  })}

我们来看一个实际的例子

func (m *defaultUserModel) FindOneByUser(user string) (*User, error) {  var resp User  // 生成基于索引的key  indexKey := fmt.Sprintf("%s%v", cacheUserPrefix, user)    err := m.QueryRowIndex(&resp, indexKey,    // 基于主键生成完整数据缓存的key    func(primary interface{}) string {      return fmt.Sprintf("user#%v", primary)    },    // 基于索引的DB查询方法    func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) {      query := fmt.Sprintf("select %s from %s where user = ? limit 1", userRows, m.table)      if err := conn.QueryRow(&resp, query, user); err != nil {        return nil, err      }      return resp.Id, nil    },    // 基于主键的DB查询方法    func(conn sqlx.SqlConn, v, primary interface{}) error {      query := fmt.Sprintf("select %s from %s where id = ?", userRows, m.table)      return conn.QueryRow(&resp, query, primary)    })    // 错误处理,需要判断是否返回的是sqlc.ErrNotFound,如果是,我们用本package定义的ErrNotFound返回  // 避免使用者感知到有没有使用缓存,同时也是对底层依赖的隔离  switch err {    case nil:      return &resp, nil    case sqlc.ErrNotFound:      return nil, ErrNotFound    default:      return nil, err  }}

到此,相信大家对"go-zero如何自动管理缓存"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

缓存 数据 索引 查询 方法 更新 逻辑 管理 情况 代码 如图 生成 设计 同时 部分 问题 场景 实际 对象 方式 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 长沙移动软件开发报价 读取数据库源出现未知错误 陕西综合农业信息系统数据库 下载数据库需要哪些条件 农产品网络技术的应用 惠州IOS软件开发招聘 江苏正规服务器生产商云空间 服务器清单 国税局做好网络安全工作 整个表导入数据库语句 数据安全法网络安全风险 韦根门禁系统如何导入数据库 28岁了还可以学软件开发吗 俄罗斯国立大学网络安全专业 哔哩漫游请求解析服务器失败 互联网时代的网络技术 中小企业服务器搭建开题报告 数据库只有bak 衡水饮品店移动点餐软件开发 网络安全就业好吗 国外高匿代理服务器 江苏师范大学网络安全部 软件开发包括 哪些方向 桂电数据库综合实验 嘉兴网络技术支持报价 杭州悦天云网络技术有限公司 手机服务器出了点问题怎么办 云南知名软件开发价格 湖北省农业银行网络安全宣传 网络安全有没有靠谱的公司
0