基于约束大于规范的想法,封装缓存组件
架构?何谓架构?好像并没有一个准确的概念。以前我觉得架构就是搭出一套完美的框架,可以让其他开发人员减少不必要的代码开发量;可以完美地实现高内聚低耦合的准则;可以尽可能地实现用最少的硬件资源,实现最高的程序效率......事实上,架构也并非只是追求这些。因为,程序是人写出来的,所以,似乎架构更多的需要考虑人这个因素。
我们发现,即便我们在程序设计之初定了诸多规范,到了实际开发过程中,由于种种原因,规范并没有按照我们预想的情况落实。这个时候,我的心里突然有一个声音:约束大于规范冒了出来。但是,约束同样会带来一些问题,比如,牺牲了一些性能,比如,带了一定的学习成本。但是,似乎一旦约束形成,会在后续业务不断发展中带来便利。
架构师似乎总是在不断地做抉择。我想,架构师心里一定有一个声音:世间安得两全法,不负如来不负卿。
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))}
}
这篇文章就写到这里结束了。水平有限,有写的不对的地方,还望广大网友斧正,不胜感激。
相关文章:

基于约束大于规范的想法,封装缓存组件
架构?何谓架构?好像并没有一个准确的概念。以前我觉得架构就是搭出一套完美的框架,可以让其他开发人员减少不必要的代码开发量;可以完美地实现高内聚低耦合的准则;可以尽可能地实现用最少的硬件资源,实现最高的程序效率…...

自动化测试面试真题(附答案)
一、编程语法题 1 、 python 有哪些数据类型 python 数据类型有很多,基本数据类型有整型(数字)、字符串、元组、列表、字典和布尔类型等 2 、怎么将两个字典合并 调用字典的 update 方法,合并 2 个字典。 3 、 json.l python …...

云原生架构概念
云原生架构概念 云原生架构(Cloud Native Architechtrue)作为一种现代软件开发的革新力量,正在逐渐改变企业构建、部署和管理应用程序的方式。它的核心优势在于支持微服务架构,使得应用程序能够分解为独立、松耦合的服务…...
85、 探针
一、pod的进阶 pod的进阶: 1.1、pod的生命周期当中的状态: 1、Running运行中,pod已经分配到节点上且pod内的容器正常运行。正常状态(ready 1/1)。 2、complete:完成之后退出,容器内的返回码…...

2024全国大学省数学建模竞赛A题-原创参考论文(部分+第一问代码)
一问题重述 1.1 问题背景 "板凳龙",又称"盘龙",是浙闽地区的传统地方民俗文化活动。这种独特的表演艺术形式融合了中国传统龙舞的精髓和地方特色,展现了人们对美好生活的向往和对传统文化的传承。 在板凳龙表演中&am…...

在VScode上写网页(html)
一、首先点进VScode,下载3个插件。 VScode安装:VScode 教程 | 菜鸟教程 二、新建 HTML 文件 作者运行的代码来自:http://t.csdnimg.cn/vIAQi 把代码复制粘贴进去,然后点击文件→另存为→选择html格式。 三、运行代码...
C#中LINQ的Cast<T>与OfType<T>
在C#中,Cast() 方法是LINQ(Language Integrated Query)的一部分,它位于 System.Linq 命名空间中。这个方法用于将 IEnumerable 集合(或任何实现了 IEnumerable 接口的集合)的元素转换为指定类型 T 的集合。…...

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

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

ARM----时钟
时钟频率可以是由晶振提供的,我们需要高频率,但是外部接高的晶振会不稳定,所有使用PLL(锁相环)来放大频率。接下来就让我们学习用外部晶振提供的频率来配置时钟频率。 一.时钟源的选择 在这里我们选择外部晶振作为时钟…...
NISP 一级 —— 考证笔记合集
该笔记为导航目录,在接下来一段事件内,我会每天发布我关于考取该证书的相关笔记。 当更新完成后,此条注释会被删除。 第一章 信息安全概述 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安装驱动 右击,为run文件添加可…...

IDEA 安装lombok插件不兼容的问题及解决方法
解决:IDEA 安装lombok插件不兼容问题,plugin xxxx is incompatible 一、去官网下载最新的2024版本 地址传送通道: lombok插件官网地址https://plugins.jetbrains.com/plugin/6317-lombok/versions/stable 二、修改参数的配置 在压缩包路径…...
聊聊说话的习惯
1 在日常生活中,每个人都有固定的说话习惯。心理学研究表明,通过一个人的说话习惯,也可以分析出他的性格特点。对于每一个人来讲,说话习惯已经融为他们生活中的一部分。在社交活动中,一些不良的说话习惯很可能会给他们带来麻烦。…...

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

【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,大家好,我是灰小猿! 在做分布式项目开发进行数据表结构设计时,有时候为了提高查询性能,在进行数据库表设计时,会使用自增ID来代替UUID作为数据的主键ID,但是这样就会有一个问题ÿ…...
小程序多个set-cookie无法处理
1、情景: 项目中遇到一个问题,客户的服务器上了华为云的防火墙,导致小程序请求头中携带了3个set- cookie(有两个是华为云给自动加的),而小程序端不知道用哪个来 处理,结果选了个错误的进行处理…...

Mybatis【分页插件,缓存,一级缓存,二级缓存,常见缓存面试题】
文章目录 MyBatis缓存分页延迟加载和立即加载什么是立即加载?什么是延迟加载?延迟加载/懒加载的配置 缓存什么是缓存?缓存的术语什么是MyBatis 缓存?缓存的适用性缓存的分类一级缓存引入案例一级缓存的配置一级缓存的工作流程一级…...

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...

linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...