当前位置: 首页 > news >正文

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

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

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

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

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))}
}

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

相关文章:

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

架构&#xff1f;何谓架构&#xff1f;好像并没有一个准确的概念。以前我觉得架构就是搭出一套完美的框架&#xff0c;可以让其他开发人员减少不必要的代码开发量&#xff1b;可以完美地实现高内聚低耦合的准则;可以尽可能地实现用最少的硬件资源&#xff0c;实现最高的程序效率…...

自动化测试面试真题(附答案)

一、编程语法题 1 、 python 有哪些数据类型 python 数据类型有很多&#xff0c;基本数据类型有整型&#xff08;数字&#xff09;、字符串、元组、列表、字典和布尔类型等 2 、怎么将两个字典合并 调用字典的 update 方法&#xff0c;合并 2 个字典。 3 、 json.l python …...

云原生架构概念

云原生架构概念 云原生架构&#xff08;Cloud Native Architechtrue&#xff09;作为一种现代软件开发的革新力量&#xff0c;正在逐渐改变企业构建、部署和管理应用程序的方式。它的核心优势在于支持微服务架构&#xff0c;使得应用程序能够分解为独立、松耦合的服务&#xf…...

85、 探针

一、pod的进阶 pod的进阶&#xff1a; 1.1、pod的生命周期当中的状态&#xff1a; 1、Running运行中&#xff0c;pod已经分配到节点上且pod内的容器正常运行。正常状态&#xff08;ready 1/1&#xff09;。 2、complete&#xff1a;完成之后退出&#xff0c;容器内的返回码…...

2024全国大学省数学建模竞赛A题-原创参考论文(部分+第一问代码)

一问题重述 1.1 问题背景 "板凳龙"&#xff0c;又称"盘龙"&#xff0c;是浙闽地区的传统地方民俗文化活动。这种独特的表演艺术形式融合了中国传统龙舞的精髓和地方特色&#xff0c;展现了人们对美好生活的向往和对传统文化的传承。 在板凳龙表演中&am…...

在VScode上写网页(html)

一、首先点进VScode&#xff0c;下载3个插件。 VScode安装&#xff1a;VScode 教程 | 菜鸟教程 二、新建 HTML 文件 作者运行的代码来自&#xff1a;http://t.csdnimg.cn/vIAQi 把代码复制粘贴进去&#xff0c;然后点击文件→另存为→选择html格式。 三、运行代码...

C#中LINQ的Cast<T>与OfType<T>

在C#中&#xff0c;Cast() 方法是LINQ&#xff08;Language Integrated Query&#xff09;的一部分&#xff0c;它位于 System.Linq 命名空间中。这个方法用于将 IEnumerable 集合&#xff08;或任何实现了 IEnumerable 接口的集合&#xff09;的元素转换为指定类型 T 的集合。…...

小阿轩yx-Kubernertes日志收集

小阿轩yx-Kubernertes日志收集 前言 在 Kubernetes 集群中如何通过不同的技术栈收集容器的日志&#xff0c;包括程序直接输出到控制台日志、自定义文件日志等 有哪些日志需要收集 日志收集与分析很重要&#xff0c;为了更加方便的处理异常 简单总结一些比较重要的需要收集…...

0to1使用Redis实现“登录验证”次数限制

1 引言 系统为了避免密码遭到暴力破解&#xff0c;通常情况下需要在登录时&#xff0c;限制用户验证账号密码的次数&#xff0c;当达到一定的验证次数后&#xff0c;在一段时间内锁定该账号&#xff0c;不再验证。本章将用几行代码实现该功能&#xff0c;完整代码链接在文章最…...

ARM----时钟

时钟频率可以是由晶振提供的&#xff0c;我们需要高频率&#xff0c;但是外部接高的晶振会不稳定&#xff0c;所有使用PLL&#xff08;锁相环&#xff09;来放大频率。接下来就让我们学习用外部晶振提供的频率来配置时钟频率。 一.时钟源的选择 在这里我们选择外部晶振作为时钟…...

NISP 一级 —— 考证笔记合集

该笔记为导航目录&#xff0c;在接下来一段事件内&#xff0c;我会每天发布我关于考取该证书的相关笔记。 当更新完成后&#xff0c;此条注释会被删除。 第一章 信息安全概述 1.1 信息与信息安全1.2 信息安全威胁1.3 信息安全发展阶段与形式1.4 信息安全保障1.5 信息系统安全保…...

C++三位状态比较排序

数组相同元素个数及按序 void 交换3个数升(int& A, int& B, int& C, bool& k) {int J 0;if (B > A&&A > C)J C, C B, B A, A J, k true;//231else if (C > A&&A > B)J A, A B, B J, k true;//213else if (A > B&a…...

麒麟系统安装GPU驱动

1.nvidia 1.1显卡驱动 本机显卡型号:nvidia rtx 3090 1.1.1下载驱动 打开 https://www.nvidia.cn/geforce/drivers/ 也可以直接使用下面这个地址下载 https://www.nvidia.com/download/driverResults.aspx/205464/en-us/ 1.1.3安装驱动 右击&#xff0c;为run文件添加可…...

IDEA 安装lombok插件不兼容的问题及解决方法

解决&#xff1a;IDEA 安装lombok插件不兼容问题&#xff0c;plugin xxxx is incompatible 一、去官网下载最新的2024版本 地址传送通道&#xff1a; lombok插件官网地址https://plugins.jetbrains.com/plugin/6317-lombok/versions/stable 二、修改参数的配置 在压缩包路径…...

聊聊说话的习惯

1 在日常生活中&#xff0c;每个人都有固定的说话习惯。心理学研究表明&#xff0c;通过一个人的说话习惯,也可以分析出他的性格特点。对于每一个人来讲&#xff0c;说话习惯已经融为他们生活中的一部分。在社交活动中&#xff0c;一些不良的说话习惯很可能会给他们带来麻烦。…...

当水泵遇上物联网:智能水务新时代的浪漫交响

在当代科技的宏伟乐章中&#xff0c;物联网&#xff08;IoT&#xff09;技术宛如一位技艺高超的指挥家&#xff0c;引领着各行各业迈向智能化的新纪元。当这股创新浪潮涌向古老的水务行业时&#xff0c;一场前所未有的“智能水务”革命便悄然上演&#xff0c;而水泵——这一传统…...

【Canvas与钟表】干支表盘

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>387.干支表盘</title><style type"text/css">…...

分布式项目中使用雪花算法提前获取对象主键ID

hello&#xff0c;大家好&#xff0c;我是灰小猿&#xff01; 在做分布式项目开发进行数据表结构设计时&#xff0c;有时候为了提高查询性能&#xff0c;在进行数据库表设计时&#xff0c;会使用自增ID来代替UUID作为数据的主键ID&#xff0c;但是这样就会有一个问题&#xff…...

小程序多个set-cookie无法处理

1、情景&#xff1a; 项目中遇到一个问题&#xff0c;客户的服务器上了华为云的防火墙&#xff0c;导致小程序请求头中携带了3个set- cookie&#xff08;有两个是华为云给自动加的&#xff09;&#xff0c;而小程序端不知道用哪个来 处理&#xff0c;结果选了个错误的进行处理…...

Mybatis【分页插件,缓存,一级缓存,二级缓存,常见缓存面试题】

文章目录 MyBatis缓存分页延迟加载和立即加载什么是立即加载&#xff1f;什么是延迟加载&#xff1f;延迟加载/懒加载的配置 缓存什么是缓存&#xff1f;缓存的术语什么是MyBatis 缓存&#xff1f;缓存的适用性缓存的分类一级缓存引入案例一级缓存的配置一级缓存的工作流程一级…...

【Qt开发】QT6.5.3安装方法(使用国内源)亲测可行!!!

目录 &#x1f315;下载在线安装包&#x1f315; 把安装包放到系统盘&#x1f315;开始安装&#x1f315;参考文章 &#x1f315;下载在线安装包 https://mirrors.nju.edu.cn/qt/official_releases/online_installers/ &#x1f315; 把安装包放到系统盘 我的系统盘是G盘&…...

springblade-JWT认证缺陷漏洞CVE-2021-44910

漏洞成因 SpringBlade前端通过webpack打包发布的&#xff0c;可以从其中找到app.js获取大量接口 然后直接访问接口&#xff1a;api/blade-log/api/list 直接搜索“请求未授权”&#xff0c;定位到认证文件&#xff1a;springblade/gateway/filter/AuthFilter.java 后面的代码…...

Chapter 12 Vue CLI脚手架组件化开发

欢迎大家订阅【Vue2Vue3】入门到实践 专栏&#xff0c;开启你的 Vue 学习之旅&#xff01; 文章目录 前言一、项目目录结构二、组件化开发1. 组件化2. Vue 组件的基本结构3. 依赖包less & less-loader 前言 组件化开发是Vue.js的核心理念之一&#xff0c;Vue CLI为开发者提…...

Ubuntu: 配置OpenCV环境

从从Ubuntu系统安装opencv_ubuntu安装opencv-CSDN博客文章浏览阅读2.3k次&#xff0c;点赞4次&#xff0c;收藏14次。开源计算机视觉(OpenCV)是一个主要针对实时计算机视觉的编程函数库。OpenCV的应用领域包括:2D和3D功能工具包、运动估计、面部识别系统、手势识别、人机交互、…...

芯片解决方案--SL8541e-OpenHarmony适配方案

摘要 本文描述8541E芯片适配OpenHarmony的整体方案。 本文描述的整体方案&#xff0c;不止适用于8541e&#xff0c;也适用于该芯片厂家的其他芯片&#xff0c;如7863、7885&#xff0c;少部分子系统会略有差异。 整体方案架构 整体方案架构如下图&#xff0c;遵循OpenHarmo…...

Spring Boot之数据访问集成入门

Spring Boot中的数据访问和集成支持功能是其核心功能之一&#xff0c;通过提供大量的自动配置和依赖管理&#xff0c;极大地简化了数据访问层的开发。Spring Boot支持多种数据库&#xff0c;包括关系型数据库&#xff08;如MySQL、Oracle等&#xff09;和非关系型数据库&#x…...

Learn ComputeShader 09 Night version lenses

这次将要制作一个类似夜视仪的效果 第一步就是要降低图像的分辨率&#xff0c; 这只需要将id.xy除上一个数字然后再乘上这个数字 可以根据下图理解&#xff0c;很明显通过这个操作在多个像素显示了相同的颜色&#xff0c;并且很多像素颜色被丢失了&#xff0c;自然就会有降低分…...

Java学习第七天

成员方法分类&#xff1a; 静态成员方法&#xff08;有static修饰 属于类&#xff09;建议用类名访问&#xff0c;也可以用对象访问 实例成员方法&#xff08;无static修饰 属于对象&#xff09;只能用对象出发访问 使用static来定义一些工具类 工具类直接使用类名.方法调用即…...

深入剖析 Redis 基础及其在 Java 应用中的实战演练

引言 在现代分布式系统和高并发应用中&#xff0c;缓存系统是不可或缺的一环&#xff0c;而 Redis 作为一种高性能的内存数据存储以其丰富的数据结构和快速的读写性能&#xff0c;成为了众多开发者的首选。本篇博客将详细介绍 Redis 的基础知识&#xff0c;并通过 Java 代码演…...

Why I‘m getting 404 Resource Not Found to my newly Azure OpenAI deployment?

题意&#xff1a;为什么我新部署的Azure OpenAI服务会出现404资源未找到的错误&#xff1f; 问题背景&#xff1a; Ive gone through this quickstart and I created my Azure OpenAI resource created a model deployment which is in state succeedded. I also playaround …...