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

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

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

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

Cache接口设计的想法

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

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

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

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

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

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

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

package cache

import (
	"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 cache

import (
	"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] * seconds
	expiryDeviation = 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.Client
	expiry         time.Duration //缓存失效时间
	notFoundExpiry time.Duration //数据库没有查询到记录时,缓存失效时间
	logger         logger.Interface
	barrier        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) {
			//数据库中不存在该值,则将占位符缓存到redis
			if 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
	}
	//断言为[]byte
	val, 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 cache

import "time"

const (
	defaultExpiry         = time.Hour * 24 * 7
	defaultNotFoundExpiry = time.Minute
)

type (
	// Options is used to store the cache options.
	Options struct {
		Expiry         time.Duration
		NotFoundExpiry time.Duration
	}

	// Option defines the method to customize an Options.
	Option func(o *Options)
)

func newOptions(opts ...Option) Options {
	var o Options
	for _, 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 cache

import (
	"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.mfbz.cn/a/872927.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

jmeter执行python脚本,python脚本的Faker库

jmeter安装 jython的插件jar包 通过如下地址下载jython-standalone-XXX.jar包并放到jmeter的XXX\lib\ext目录下面 Downloads | JythonThe Python runtime on the JVMhttps://www.jython.org/download.html 重启jmeter在JSR223中找到jython可以编写python代码执行 python造数据…

Minimax-秋招正式批-面经(SQL相关)

1. 谈谈对聚簇索引的理解 聚簇索引 InnoDB通过主键聚集数据&#xff0c;如果没有定义主键&#xff0c;InnoDB会选择非空的唯一索引代替。如果没有这样的索引&#xff0c;InnoDB会隐式定义一个主键来作为聚簇索引聚簇索引就是按照每张表的主键构造一颗B树&#xff0c;同时叶子…

redis之缓存淘汰策略

1.查看redis的最大占用内存 使用redis-cli命令连接redis服务端&#xff0c;输入命令&#xff1a;config get maxmemory 输出的值为0&#xff0c;0代表redis的最大占用内存等同于服务器的最大内存。 2.设置redis的最大占用内存 编辑redis的配置文件&#xff0c;并重启redis服务…

【软考】设计模式之代理模式

目录 1. 说明2. 应用场景3. 结构图4. 构成5. 适用性6. 优点7. 缺点8. java示例 1. 说明 1.代理模式&#xff08;Proxy Pattern&#xff09;。2.意图&#xff1a;为其他对象提供一种代理以控制对这个对象的访问。3.通过提供与对象相同的接口来控制对这个对象的访问。4.是设计模…

WordPress独立资源下载页面插件美化版

插件介绍&#xff1a; xydown是一款wordpress的独立下载页面插件&#xff0c;主要适用于wp建站用户使用&#xff0c;有些用户在发布文章的时候想要添加一些下载资源&#xff0c;使用这款插件可以把下载的内容独立出来&#xff0c;支持添加本地下载或者百度网盘蓝奏网盘的网址&…

FreeRTOS学习笔记—④RTOS通信管理篇/同步互斥与通信(正在更新中)

二、RTOS的核心功能 RTOS的核心功能块主要分为任务管理、内核管理、时间管理以及通信管理4部分&#xff0c;框架图如下所示&#xff1a;   &#xff08;1&#xff09;任务管理&#xff1a;负责管理和调度任务的执行&#xff0c;确保系统中的任务能够按照预期运行。   &…

uni-appH5项目实现导航区域与内容区域联动效果

一、需求描述 将导航区域与内容区域实现联动&#xff0c;即点击导航区域&#xff0c;内容区滚动到对应位置&#xff0c;内容区滚动过程中根据内容定位到相对应的导航栏。 效果如下&#xff1a; 侧边导航与内容联动效果 二、功能实现思路分析汇总&#xff1a; 三、具体代码 1…

流媒体技术革新,EasyCVR视频汇聚平台赋能视频监控全面升级

随着科技的飞速发展&#xff0c;流媒体技术和视频监控正经历着前所未有的变革与融合。本文将从流媒体技术的新兴趋势出发&#xff0c;探讨其与视频监控领域的深度结合&#xff0c;以及这一融合所带来的创新与发展。 一、流媒体技术的新兴趋势 1、5G网络的广泛应用 5G网络以其…

鸿蒙开发入门day16-拖拽事件和手势事件

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;还请三连支持一波哇ヾ(&#xff20;^∇^&#xff20;)ノ&#xff09; 目录 拖拽事件 概述 拖拽流程 ​手势拖拽 ​鼠标拖拽 拖拽背板图 …

企业架构的概念及发展历程简述(附TOGAF架构理论学习资料下载链接)

企业架构在数字化转型中发挥着至关重要的作用。它不仅确保了战略一致性、提高了运营效率、强化了信息安全&#xff0c;还指导了数字化转型路径、推动了技术与业务的深度融合以及促进了生态系统的连接。因此&#xff0c;在数字化转型过程中&#xff0c;企业应高度重视企业架构的…

《OpenCV计算机视觉》—— 图像边缘检测

文章目录 一、图像边缘检测概述二、常见的图像边缘检测算法&#xff08;简单介绍&#xff09;1.sobel算子2.Scharr算子3.Laplacian算子4.Canny算子 三、代码实现 一、图像边缘检测概述 图像边缘检测是一种重要的图像处理技术&#xff0c;用于定位二维或三维图像中对象的边缘。…

计算氨基酸残基之间的键角和二面角

在蛋白质结构中,不同的角度由特定的原子位置决定。常见的原子类型包括氨基酸主链中的 Cα(α 碳)、C(羰基碳)、N(氮原子)和 O(氧原子)。为了更加清晰,下面给出几种常见角度的定义及其对应的原子类型: 使用具体原子的坐标计算键角和二面角 1. 计算 N−Cα−C 的键角…

初次使用住宅代理有哪些常见误区?

随着网络技术的发展&#xff0c;住宅代理因其高匿名性和稳定性成为许多用户进行网络活动的首选工具。然而&#xff0c;对于新手而言&#xff0c;使用住宅代理时往往容易陷入一些误区&#xff0c;这不仅可能影响使用效果&#xff0c;还可能带来安全风险。本文将探讨新手在使用住…

前缀列表(ip-prefix)配置

一. 实验简介 本来前缀列表是要和访问控制列表放在一起讲的&#xff0c;但是这里单拎出来是为了更详细的讲解两者的区别 1.前缀列表针对IP比访问控制更加灵活。 2.前缀列表在后面被引用时是无法对数据包进行过滤的 实验拓扑 二. 实验目的 R4路由器中只引入子网LoopBack的…

oracle数据库安装和配置

​ 大家好&#xff0c;我是程序员小羊&#xff01; 前言&#xff1a; Oracle 数据库的安装和配置是一个较为复杂的过程&#xff0c;涉及多个步骤和配置项。以下将详细介绍如何在 Linux 和 Windows 系统中安装 Oracle 数据库并进行基础配置。 一、Oracle 数据库安装前的准备 …

提升效率!ArcGIS中创建脚本工具

在我们日常使用的ArcGIS中已经自带了很多功能强大的工具&#xff0c;但有时候遇到个人的特殊情况还是无法满足&#xff0c;这时就可以试着创建自定义脚本工具。 一、编写代码 此处的代码就是一个很简单的给图层更改别名的代码。 1. import arcpy 2. input_fc arcpy.GetParam…

Oracle同义词

默认只能访问自己用户下面的对象&#xff0c;所以可以创建同义词。 同义词(Synonym) 是数据库对象的一个别名&#xff0c;Oracle 可以为表、视图、序列、过程、函数、程序包等指定一个别名 https://blog.csdn.net/ChineseSoftware/article/details/121750937

【springboot】使用缓存

目录 1. 添加依赖 2. 配置缓存 3. 使用EnableCaching注解开启缓存 4. 使用注解 1. 配置缓存名称 2. 配置缓存的键 3. 移除缓存 5. 运行结果 1. 添加依赖 <!-- springboot缓存--><dependency><groupId>org.springframework.boot</groupId>…

ngrok | 内网穿透,支持 HTTPS、国内访问、静态域名

前言 当我们需要把本地开发的应用展示给外部用户时&#xff0c;常常会因为无法直接访问而陷入困境。 就为了展示一下&#xff0c;买服务、域名&#xff0c;搭环境&#xff0c;费钱又费事。 那有没有办法&#xff0c;让客户直接访问自己本机开发的应用呢&#xff1f; 这种需…

分享MSSQL、MySql、Oracle的大数据批量导入方法及编程手法细节

1&#xff1a;MSSQL SQL语法篇&#xff1a; BULK INSERT [ database_name . [ schema_name ] . | schema_name . ] [ table_name | view_name ] FROM data_file [ WITH ( [ [ , ] BATCHSIZE batch_size ] [ [ , ] CHECK_CONSTRAINTS …