基于约束大于规范的想法,封装缓存组件

embedded/2024/9/22 23:52:51/

架构?何谓架构?好像并没有一个准确的概念。以前我觉得架构就是搭出一套完美的框架,可以让其他开发人员减少不必要的代码开发量;可以完美地实现高内聚低耦合的准则;可以尽可能地实现用最少的硬件资源,实现最高的程序效率......事实上,架构也并非只是追求这些。因为,程序是人写出来的,所以,似乎架构更多的需要考虑人这个因素。

我们发现,即便我们在程序设计之初定了诸多规范,到了实际开发过程中,由于种种原因,规范并没有按照我们预想的情况落实。这个时候,我的心里突然有一个声音:约束大于规范冒了出来。但是,约束同样会带来一些问题,比如,牺牲了一些性能,比如,带了一定的学习成本。但是,似乎一旦约束形成,会在后续业务不断发展中带来便利。

架构师似乎总是在不断地做抉择。我想,架构师心里一定有一个声音:世间安得两全法,不负如来不负卿。

Cache接口设计的想法

基于约束大于规范的想法,我们有了如下一些约束:

第一、把业务中常用到的缓存的方法集合通过接口的方式进行约束。

第二、基于缓存采用cache aside模式。

  • 读数据时,先读缓存,如果有就返回。没有再读数据源,将数据放到缓存

  • 写数据时,先写数据源,然后让缓存失效

我们把这个规范进行封装,以达到约束的目的。

基于上述的约束,我们进行了如下的封装:

package cacheimport ("context""time"
)type Cache interface {// 删除缓存// 先删除数据库数据,再删除缓存数据DelCtx(ctx context.Context, query func() error, keys ...string) error// 根据key获取缓存,如果缓存不存在,// 通过query方法从数据库获取缓存并设置缓存,使用默认的失效时间TakeCtx(ctx context.Context, key string, query func() (interface{}, error)) ([]byte, error)// 根据key获取缓存,如果缓存不存在,// 通过query方法从数据库获取缓存并设置缓存TakeWithExpireCtx(ctx context.Context, key string, expire time.Duration, query func() (interface{}, error)) ([]byte, error)
}

细心的朋友可能已经发现,这个接口中的方法集合中都包含了一个函数传参。为什么要有这样一个传参呢?首先,在go中函数是一等公民,其地位和其他数据类型一样,都可以做为函数的参数。这个特点使我们的封装更方便。因为,我需要把数据库的操作封装到我的方法中,以达到约束的目的。关于函数式编程,我在另一篇文章中《golang函数式编程》有写过,不过,我尚有部分原理还没有搞清楚,还需要找时间继续探究。

函数一等公民这个特点,似乎很好理解,但是,进一步思考,我们可能会想到,数据库操作,入参不是固定的啊,这个要怎么处理呢?很好的问题。事实上,我们可以利用闭包的特点,把这些不是固定的入参传到函数内部。

基于redis实现缓存的想法

主要就是考虑缓存雪崩,缓存穿透等问题,其中,缓存雪崩和缓存穿透的设计参考了go-zero项目中的设计,我在go-zero设计思想的基础上进行了封装。

package cacheimport ("context""encoding/json""errors""fmt""time""github.com/redis/go-redis/v9""github.com/zeromicro/go-zero/core/mathx""github.com/zeromicro/go-zero/core/syncx""gorm.io/gorm/logger"
)const (notFoundPlaceholder = "*" //数据库没有查询到记录时,缓存值设置为*,避免缓存穿透// make the expiry unstable to avoid lots of cached items expire at the same time// make the unstable expiry to be [0.95, 1.05] * secondsexpiryDeviation = 0.05
)// indicates there is no such value associate with the key
var errPlaceholder = errors.New("placeholder")
var ErrNotFound = errors.New("not found")// ErrRecordNotFound record not found error
var ErrRecordNotFound = errors.New("record not found") //数据库没有查询到记录时,返回该错误type RedisCache struct {rds            *redis.Clientexpiry         time.Duration //缓存失效时间notFoundExpiry time.Duration //数据库没有查询到记录时,缓存失效时间logger         logger.Interfacebarrier        syncx.SingleFlight //允许具有相同键的并发调用共享调用结果unstableExpiry mathx.Unstable     //避免缓存雪崩,失效时间随机值
}func NewRedisCache(rds *redis.Client, log logger.Interface, barrier syncx.SingleFlight, opts ...Option) *RedisCache {if log == nil {log = logger.Default.LogMode(logger.Info)}o := newOptions(opts...)return &RedisCache{rds:            rds,expiry:         o.Expiry,notFoundExpiry: o.NotFoundExpiry,logger:         log,barrier:        barrier,unstableExpiry: mathx.NewUnstable(expiryDeviation),}
}func (r *RedisCache) DelCtx(ctx context.Context, query func() error, keys ...string) error {if err := query(); err != nil {r.logger.Error(ctx, fmt.Sprintf("Failed to query: %v", err))return err}for _, key := range keys {if err := r.rds.Del(ctx, key).Err(); err != nil {r.logger.Error(ctx, fmt.Sprintf("Failed to delete key %s: %v", key, err))//TODO 起个定时任务异步重试}}return nil
}func (r *RedisCache) TakeCtx(ctx context.Context, key string, query func() (interface{}, error)) ([]byte, error) {return r.TakeWithExpireCtx(ctx, key, r.expiry, query)
}func (r *RedisCache) TakeWithExpireCtx(ctx context.Context, key string, expire time.Duration, query func() (interface{}, error)) ([]byte, error) {// 在过期时间的基础上,增加一个随机值,避免缓存雪崩expire = r.aroundDuration(expire)// 并发控制,同一个key的请求,只有一个请求执行,其他请求等待共享结果res, err := r.barrier.Do(key, func() (interface{}, error) {cacheVal, err := r.doGetCache(ctx, key)if err != nil {// 如果缓存中查到的是notfound的占位符,直接返回if errors.Is(err, errPlaceholder) {return nil, ErrNotFound} else if !errors.Is(err, ErrNotFound) {return nil, err}}// 缓存中存在值,直接返回if len(cacheVal) > 0 {return cacheVal, nil}data, err := query()if errors.Is(err, ErrRecordNotFound) {//数据库中不存在该值,则将占位符缓存redisif err := r.setCacheWithNotFound(ctx, key); err != nil {r.logger.Error(ctx, fmt.Sprintf("Failed to set not found key %s: %v", key, err))}return nil, ErrNotFound} else if err != nil {return nil, err}cacheVal, err = json.Marshal(data)if err != nil {return nil, err}if err := r.rds.Set(ctx, key, cacheVal, expire).Err(); err != nil {r.logger.Error(ctx, fmt.Sprintf("Failed to set key %s: %v", key, err))return nil, err}return cacheVal, nil})if err != nil {return []byte{}, err}//断言为[]byteval, ok := res.([]byte)if !ok {return []byte{}, fmt.Errorf("failed to convert value to bytes")}return val, nil
}func (r *RedisCache) aroundDuration(duration time.Duration) time.Duration {return r.unstableExpiry.AroundDuration(duration)
}// 获取缓存
func (r *RedisCache) doGetCache(ctx context.Context, key string) ([]byte, error) {val, err := r.rds.Get(ctx, key).Bytes()if err != nil {if err == redis.Nil {return nil, ErrNotFound}return nil, err}if len(val) == 0 {return nil, ErrNotFound}// 如果缓存的值为notfound的占位符,则表示数据库中不存在该值,避免再次查询数据库,避免缓存穿透if string(val) == notFoundPlaceholder {return nil, errPlaceholder}return val, nil
}// 数据库没有查询到值,则设置占位符,避免缓存穿透
func (r *RedisCache) setCacheWithNotFound(ctx context.Context, key string) error {notFoundExpiry := r.aroundDuration(r.notFoundExpiry)if err := r.rds.Set(ctx, key, notFoundPlaceholder, notFoundExpiry).Err(); err != nil {r.logger.Error(ctx, fmt.Sprintf("Failed to set not found key %s: %v", key, err))return err}return nil
}
package cacheimport "time"const (defaultExpiry         = time.Hour * 24 * 7defaultNotFoundExpiry = time.Minute
)type (// Options is used to store the cache options.Options struct {Expiry         time.DurationNotFoundExpiry time.Duration}// Option defines the method to customize an Options.Option func(o *Options)
)func newOptions(opts ...Option) Options {var o Optionsfor _, opt := range opts {opt(&o)}if o.Expiry <= 0 {o.Expiry = defaultExpiry}if o.NotFoundExpiry <= 0 {o.NotFoundExpiry = defaultNotFoundExpiry}return o
}// WithExpiry returns a func to customize an Options with given expiry.
func WithExpiry(expiry time.Duration) Option {return func(o *Options) {o.Expiry = expiry}
}// WithNotFoundExpiry returns a func to customize an Options with given not found expiry.
func WithNotFoundExpiry(expiry time.Duration) Option {return func(o *Options) {o.NotFoundExpiry = expiry}
}

最后,附上部分测试用例,数据库操作的逻辑,我没有写,通过模拟的方式实现。

package cacheimport ("context""testing""github.com/redis/go-redis/v9""github.com/zeromicro/go-zero/core/syncx""gorm.io/gorm/logger"
)func TestRedisCache(t *testing.T) {rdb := redis.NewClient(&redis.Options{Addr:     "", // Redis地址Password: "",       // 密码(无密码则为空)DB:       11,                  // 使用默认DB})ctx := context.Background()rc := NewRedisCache(rdb, logger.Default.LogMode(logger.Info), syncx.NewSingleFlight())// 测试 TakeCtx 方法key := "testKey"queryVal := "hello, world"// 通过闭包的方式,模拟查询数据库的操作query := func() (interface{}, error) {return queryVal, nil}val, err := rc.TakeCtx(ctx, key, query)if err != nil {t.Fatalf("unexpected error: %v", err)}t.Log("return query func val:", string(val))// 再次调用 TakeCtx 方法,应该返回缓存的值queryVal = "this should not be returned"val, err = rc.TakeCtx(ctx, key, query)if err != nil {t.Fatalf("unexpected error: %v", err)}t.Log("cache val:", string(val))// 测试 DelCtx 方法if err := rc.DelCtx(ctx, func() error {t.Log("mock query before delete")return nil}, key); err != nil {t.Fatalf("unexpected error: %v", err)}queryVal = "this should be cached"// 验证键是否已被删除val, err = rc.TakeCtx(ctx, key, query)if err != nil {t.Fatalf("unexpected error: %v", err)}if string(val) != "this should be cached" {t.Fatalf("unexpected value: %s", string(val))}
}

 这篇文章就写到这里结束了。水平有限,有写的不对的地方,还望广大网友斧正,不胜感激。


http://www.ppmy.cn/embedded/105567.html

相关文章

贪心算法---划分字母区间

题目&#xff1a; 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段&#xff0c;同一字母最多出现在一个片段中。 注意&#xff0c;划分结果需要满足&#xff1a;将所有划分结果按顺序连接&#xff0c;得到的字符串仍然是 s 。 返回一个表示每个字符串片段的长度…

基础学习之——git 的使用方式

git 是一种分布式版本控制系统&#xff08;Distributed Version Control System, DVCS&#xff09;&#xff0c;用于有效地管理代码和文件的变更历史。它最初由林纳斯托瓦兹&#xff08;Linus Torvalds&#xff09;于2005年为管理Linux内核开发而设计&#xff0c;并很快因其效率…

Web Worker

目录 一、概述&#xff08;1&#xff09;同源限制&#xff08;2&#xff09;DOM 限制&#xff08;3&#xff09;通信联系&#xff08;4&#xff09;脚本限制&#xff08;5&#xff09;文件限制 二、基本用法2.1 主线程2.2 Worker 线程2.3 Worker 加载脚本2.4 错误处理2.5 关闭 …

JsonCPP源码分析——Value

1、Json::Value类中有一个自定义的字符串结构体&#xff0c;这个结构体的概念是非常重要的&#xff0c;设计的非常巧妙&#xff0c;首先介绍下这个结构体&#xff0c;个人理解以注释的方式添加到下面的代码中&#xff1a; class CZString {public:// 枚举&#xff0c;标识字符串…

关于edge浏览器登陆CSDN安全验证不跳出验证码

前言 也就是最近这几天才出现这个问题&#xff0c;以前用edge浏览器登陆csdn时即使需要安全验证也能正常弹出验证码&#xff0c;现在根本没反应。 正文 我用edge浏览器登陆时&#xff0c;显示如下界面&#xff0c;就卡住不动了。 起初我以为是我浏览器可能设置了拦截的问题…

深度学习dataset等概念

DATASET 自定义 Dataset 类必须实现三个函数&#xff1a;init、len 和 getitem。 __init__ init 函数在实例化 Dataset 对象时运行一次 __len__ len 函数返回数据集中样本的数量。 __getitem__ getitem 函数加载并返回给定索引 idx 处的数据集样本。 DATALOADER 是在data…

数据结构--排序实现--C语言

文章目录 常见的排序算法一、插入排序1、直接插入排序&#xff1a;2、希尔排序 (缩小增量排序) 二、选择排序1、直接选择排序:2、堆排序 三、交换排序1、冒泡排序 2、快速排序快速排序优化 1、hoare版本2、挖坑法3、前后指针法 四、归并排序1、归并排序 五、非比较排序1、计数排…

【论文阅读】QUEEN: Query Unlearning against Model Extraction(2024)

摘要 Model extraction attacks(模型提取攻击) currently pose a non-negligible threat(不可忽视的威胁) to the security(安全性) and privacy(隐私性) of deep learning models. By querying the model with a small dataset(通过小数据集查询模型) and using th…