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

news2024/11/13 16:07:10

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

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

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

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.coloradmin.cn/o/2103096.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux文件【系统调用接口及进程中对打开文件的管理操作】详细讲解

目录 一、open函数 1.介绍 2.open函数返回值 二、重定向 1.文件描述符的分配规则 2.重定向的本质 3.dup2系统调用 三、C语言库函数中的缓冲区及不同刷新模式 前言&#xff1a; 我们先来简单回顾一下C语言中的文件相关知识 ● 打开文件的方式 r …

数图亮相第三届中国区域零售创新峰会:共绘零售新蓝图,携手迈向新征程

8月31日&#xff0c;备受瞩目的第三届中国区域零售创新峰会在历史悠久的湖北襄阳圆满落下帷幕。在这场零售行业的盛会上&#xff0c;数图信息科技作为重要参会企业&#xff0c;积极参与其中&#xff0c;与众多行业精英共聚一堂&#xff0c;共同擘画零售业的宏伟蓝图。以下是本次…

C程序设计——指针杂谈0

变量和常量讲的差不多了&#xff0c;这里先把指针再深入理解一下&#xff0c;如果你是C语言初学者&#xff0c;本节可能看不太懂&#xff0c;没关系可以以后再看。 变量 当定义变量的时候&#xff0c;本质是在内存中分配了一段空间&#xff0c;这段空间的大小与变量的类型相关…

GD - EmbeddedBuilder - 给已有工程换MCU

文章目录 GD - EmbeddedBuilder - 给已有工程换MCU概述不行的重现 笔记工程的.gdc文件内容中有MCU型号可以改 给已有工程换MCU的使用场景END GD - EmbeddedBuilder - 给已有工程换MCU 概述 一个现存的EmbeddedBuilder的工程&#xff0c;想换个MCU配置做实验&#xff0c;又不想…

极盾故事|某金融租赁机构应用数据保护新策略:“动态脱敏”“二次授权”

数据的流通使用是创新的动力&#xff0c;但安全和合规是不可逾越的底线。企业如何在这三者之间找到平衡点&#xff1f; 极盾科技&#xff0c;助力某金融租赁机构&#xff0c;基于极盾觅踪构建应用数据动态脱敏系统&#xff0c;实现10&#xff0b;核心应用系统的统一管理&#x…

库(Library)

库的定义 在Linux操作系统中&#xff0c;库&#xff08;Library&#xff09;是一段编译好的、可重用的代码&#xff0c;它能够被其他程序或应用程序在运行时调用。库可以提高代码的模块化&#xff0c;使得开发者可以共享和重用代码&#xff0c;从而提高开发效率&#xff0c;减少…

如何在 Ubuntu 24.04 上安装 MariaDB ?

MariaDB 是一个流行的开源关系数据库管理系统&#xff0c;它是 MySQL 的一个分支&#xff0c;它被广泛用于存储和管理数据。本指南将引导您完成在 Ubuntu 24.04 上安装 MariaDB 的步骤。 Step 1: Update Your System 首先更新系统&#xff0c;确保所有的软件都是最新的。 su…

PMP–一、二、三模、冲刺、必刷–分类–14.敏捷–技巧–刺探

文章目录 技巧一模反例不选“刺探”14.敏捷--流程&#xff1a;&#xff08;2&#xff09;每日站会&#xff08;15分钟、轮流开、提出问题、不解决问题&#xff09;&#xff1a;输入任务板/看板 → 输出任务板更新、燃尽图更新、障碍日志、产品增量&#xff1b;14.敏捷--方法--每…

树莓派扩展RGB点阵屏的使用

本篇来介绍一个树莓派的RGB 8x8点阵屏扩展板的使用。 1 RGB点阵屏 这里使用SunFounder的一个RGB 8x8树莓派扩展板&#xff0c;将其插接到树莓派中即可使用。 2 树莓派IIC配置 树莓派系统的安装&#xff0c;可参考之前的文章&#xff1a; 这个RGB点阵屏与树莓派直接使用IIC通…

Opencv中的直方图(2)计算图像的直方图函数calcHist()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算一组数组的直方图。 函数 cv::calcHist 计算一个或多个数组的直方图。用于递增直方图bin的元组的元素是从相同位置的相应输入数组中获取的。…

C++设计模式——Template Method模板方法模式

一&#xff0c;模板方法模式的定义 模板方法模式是一种行为型设计模式&#xff0c;它先定义了一个算法的大致框架&#xff0c;然后将算法的具体实现步骤分解到多个子类中。 模板方法模式为算法设计了一个抽象的模板&#xff0c;算法的具体代码细节由子类来实现&#xff0c;从…

Spring Boot中如何禁用Actuator端点安全性?

Spring Boot中如何禁用Actuator端点安全性&#xff1f; 1、为什么考虑禁用&#xff1f;2、如何禁用&#xff1f;方法一&#xff1a;自定义Security配置&#xff08;推荐&#xff09;方法二&#xff1a;绕过安全性&#xff08;不推荐&#xff09; 3、注意事项4、总结 &#x1f4…

Golang | Leetcode Golang题解之第393题UTF-8编码验证

题目&#xff1a; 题解&#xff1a; const mask1, mask2 1 << 7, 1<<7 | 1<<6func getBytes(num int) int {if num&mask1 0 {return 1}n : 0for mask : mask1; num&mask ! 0; mask >> 1 {nif n > 4 {return -1}}if n > 2 {return n}r…

AI机械键盘,罗技推出首款AI机械键盘K98M

在这个智能化日益普及的时代&#xff0c;我们的生活中充斥着各种智能设备。 从智能手机到智能家居&#xff0c;现在连键盘也加入了智能化的行列。罗技&#xff0c;作为知名的电脑配件制造商&#xff0c;最近推出了他们的首款AI机械键盘K98M。这款键盘集成了百度文心一言提供的…

C++设计模式——Observer观察者模式

一&#xff0c;观察者模式的定义 观察者模式是一种行为型设计模式&#xff0c;又被称为"发布-订阅"模式&#xff0c;它定义了对象之间的一对多的依赖关系&#xff0c;当一个对象的状态发生变化时&#xff0c;所有依赖于它的对象都会收到通知并自动更新。 观察者模式…

13、Django Admin创建两个独立的管理站点

admin文件 from .models import Epic, Event, EventHero, EventVillain from django.contrib.admin import AdminSiteclass EventAdminSite(AdminSite):site_header "Events管理"site_title "欢迎您&#xff01;"index_title "管理员"even…

AI自动生成PPT哪个软件好?如何自动生成专业级PPT?

新学期伊始&#xff0c;准备开学演讲稿的你是否还在为制作PPT而烦恼&#xff1f;别担心&#xff0c;现在有了AI的帮助&#xff0c;生成专业且吸引人的PPT变得轻而易举。 本文将为你揭秘4种高效的AI自动生成PPT的方法&#xff0c;让你在新学期的演讲中脱颖而出。无论是简洁明了…

畅游5G高速网络:联发科集成Wi-Fi6E与蓝牙5.2的系统级单芯片MT7922

这周末,除非外面下钞票,否则谁也拦不住我玩《黑神话悟空》(附:两款可以玩转悟空的显卡推荐) IPBrain平台君 集成电路大数据平台 2024年09月03日 17:28 北京 联发科一直以创新技术追赶市场需求…… “不努力向前游就会被海浪拍回岸边…” 芯片设计公司产品层出不穷,想要站…

Redis集群搭建以及用idea连接集群

一、redis的集群搭建&#xff1a; 判断一个是集群中的节点是否可用,是集群中的所用主节点选举过程,如果半数以上的节点认为当前节点挂掉,那么当前节点就是挂掉了,所以搭建redis集群时建议节点数最好为奇数&#xff0c;搭建集群至少需要三个主节点,三个从节点,至少需要6个节点。…

Datawhle X 李宏毅苹果书AI夏令营深度学习笔记之——卷积神经网络

卷积神经网络简介 卷积神经网络&#xff08;Convolutional Neural Network, CNN&#xff09;是一种深度学习模型&#xff0c;尤其擅长处理图像和视频等高维度的数据。CNN 通过模仿人类视觉系统的工作方式&#xff0c;自动学习数据中的空间层次结构&#xff0c;使得它在计算机视…