go-zero(十四)实践:缓存一致性保证、缓存击穿、缓存穿透与缓存雪崩解决方案

embedded/2024/12/22 13:22:51/

go zero 实践:缓存一致性保证、缓存击穿、缓存穿透与缓存雪崩解决方案

缓存 作为一种重要的技术手段,可以有效提高系统的响应速度,降低对数据库的压力。但是缓存的使用伴随一些常见问题,如缓存一致性缓存击穿缓存穿透缓存雪崩。下面我们将结合 go zero 框架,深入剖析这些问题的概念以及对应的解决方案。

一、项目构建

本文项目都基于下面的文件构建,通过文章的增删改查,来演示缓存实践

1.SQL

CREATE TABLE `article` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',`title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题' COLLATE 'utf8mb4_bin',`content` TEXT NOT NULL COMMENT '内容' COLLATE 'utf8_unicode_ci',`cover` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '封面' COLLATE 'utf8mb4_bin',`description` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '描述' COLLATE 'utf8mb4_bin',`author_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '作者ID',`status` TINYINT NOT NULL DEFAULT '0' COMMENT '状态 0:待审核 1:审核不通过 2:可见 3:用户删除',`comment_num` INT NOT NULL DEFAULT '0' COMMENT '评论数',`like_num` INT NOT NULL DEFAULT '0' COMMENT '点赞数',`collect_num` INT NOT NULL DEFAULT '0' COMMENT '收藏数',`view_num` INT NOT NULL DEFAULT '0' COMMENT '浏览数',`share_num` INT NOT NULL DEFAULT '0' COMMENT '分享数',`tag_ids` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标签ID' COLLATE 'utf8mb4_bin',`publish_time` TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '发布时间',`create_time` TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP) COMMENT '创建时间',`update_time` TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP) ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',PRIMARY KEY (`id`) USING BTREE,INDEX `ix_author_id` (`author_id`) USING BTREE,INDEX `ix_update_time` (`update_time`) USING BTREE
)
COMMENT='文章表'
COLLATE='utf8mb4_bin'
ENGINE=InnoDB
AUTO_INCREMENT=103
;

2.API文件

syntax = "v1"type (Token {AccessToken  string `json:"access_token"`AccessExpire int64  `json:"access_expire"`}VerificationRequest {Mobile string `json:"mobile"`}VerificationResponse  {}RegisterRequest {Name             string `json:"name"`Mobile           string `json:"mobile"`Password         string `json:"password"`VerificationCode string `json:"verification_code"`}RegisterResponse {UserId int64 `json:"user_id"`Token  Token `json:"token"`}LoginRequest {Mobile           string `json:"mobile"`Password         string `json:"password"`VerificationCode string `json:"verification_code"`}LoginResponse {UserId int64 `json:"userId"`Token  Token `json:"token"`}UserInfoResponse {UserId   int64  `json:"user_id"`Username string `json:"username"`Avatar   string `json:"avatar"`}
)@server (prefix: /v1
)
service user-api {@handler RegisterHandlerpost /register (RegisterRequest) returns (RegisterResponse)@handler VerificationHandlerpost /verification (VerificationRequest) returns (VerificationResponse)@handler LoginHandlerpost /login (LoginRequest) returns (LoginResponse)
}@server (prefix:    /v1/usersignature: truejwt:       Auth
)
service user-api {@handler UserInfoHandlerget /info returns (UserInfoResponse)
}

3.PROTO文件

syntax = "proto3";package userpb;
option go_package="./userpb";service User {rpc Register(RegisterRequest) returns (RegisterResponse);rpc FindById(FindByIdRequest) returns (FindByIdResponse);rpc FindByMobile(FindByMobileRequest) returns (FindByMobileResponse);rpc SendSms(SendSmsRequest) returns (SendSmsResponse);
}message RegisterRequest {string username = 1;string mobile = 2;string avatar = 3;string password = 4;
}message RegisterResponse {int64 userId = 1;
}message FindByIdRequest {int64 userId = 1;
}message FindByIdResponse {int64 userId = 1;string username = 2;string password =3;string mobile = 4;string avatar = 5;
}message FindByMobileRequest {string mobile = 1;
}message FindByMobileResponse {int64 userId = 1;string username = 2;string password =3;string mobile = 4;string avatar = 5;
}message SendSmsRequest {int64 userId = 1;string mobile = 2;
}message SendSmsResponse {string code =1;
}

二、缓存一致性

1.概念

缓存一致性缓存数据与数据库中的数据保持一致性。如果缓存数据过时或被修改后未及时更新,可能导致业务逻辑错误。

2.现象

  • 缓存中存储了过期数据,而数据库已经更新,导致查询结果不一致。
  • 多服务环境下,缓存与数据库之间的数据同步问题尤为显著。

3.解决方案

  1. 读操作

    • 先从缓存读取数据。
    • 如果缓存中存在,直接返回。
    • 如果缓存中不存在(缓存未命中),则从数据库查询,返回结果后将数据写入缓存
  2. 写操作(更新、删除):

    • 先更新数据库中的数据。
    • 再删除或更新缓存中的数据,保证缓存中的数据是最新的。

4.代码演示

go-zero 除了提供 sqlx.SqlConn, 我们也提供了一个 sqlc.CachedConn 的封装,用于sql 数据库缓存的支持。

当我们使用goctl model -c生成model的代码,model的方法都带有缓存管理,所以我们不需要对单独的数据做缓存处理,我们使用Redis有序集合,来做文章列表的缓存

发布文章
下面通过文章的发布,向有序集合写入缓存(即向文章列表缓存添加文章信息),来演示缓存的一致性:

func (l *PublishLogic) Publish(in *pb.PublishRequest) (*pb.PublishResponse, error) {// todo: add your logic here and delete this lineif in.UserId <= 0 {return nil, errors.New("用户ID不合法")}if len(in.Title) == 0 || len(in.Content) == 0 {return nil, errors.New("标题或者文章内容不能为空")}//文章数据插入数据库//调用Insert方法,自动写入行缓存result, err := l.svcCtx.ArticleModel.Insert(l.ctx, &model.Article{Title:       in.Title,Content:     in.Content,AuthorId:    uint64(in.UserId),Cover:       "",TagIds:      "",Status:      1, //  0 未发布 1 代表已发布 2 待审核  3 仅自己可见PublishTime: time.Now(),CreateTime:  time.Now(),UpdateTime:  time.Now(),})if err != nil {return nil, err}//获取插入后的文章 IDarticleId, err := result.LastInsertId()if err != nil {return nil, errors.New("返回ID失败")}//缓存键生成// 0为发布时间排序  1为点赞数排序  -默认按发布时间排序//"biz#articles#ID#SortType"publishTimeKey := fmt.Sprintf("biz#articles#%d#0", in.UserId)likeNumKey := fmt.Sprintf("biz#articles#%d#1", in.UserId)articleIdStr := strconv.FormatInt(articleId, 10)//缓存存在性检查与更新//如果对应的缓存键存在,使用 Redis 的 ZADD 命令将文章 ID 和分数添加到有序集合中。isExits, _ := l.svcCtx.Rds.ExistsCtx(l.ctx, publishTimeKey)if isExits {//Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。_, err := l.svcCtx.Rds.ZaddCtx(l.ctx, publishTimeKey, time.Now().Unix(), articleIdStr)if err != nil {return nil, err}}isExits, _ = l.svcCtx.Rds.ExistsCtx(l.ctx, likeNumKey)if isExits {//Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。_, err := l.svcCtx.Rds.ZaddCtx(l.ctx, likeNumKey, time.Now().Unix(), articleIdStr)if err != nil {return nil, err}}return &pb.PublishResponse{ArticleId: articleId}, nil
}

删除文章

func (l *ArticleDeleteLogic) ArticleDelete(in *pb.ArticleDeleteRequest) (*pb.ArticleDeleteResponse, error) {// todo: add your logic here and delete this lineif in.UserId <= 0 {return nil, errors.New("用户ID不合法")}if in.ArticleId <= 0 {return nil, errors.New("文章ID不合法")}//判断文章ID是否存在article, err := l.svcCtx.ArticleModel.FindOne(l.ctx, uint64(in.ArticleId))if err != nil {return nil, err}//检查是否是自己的文章if article.AuthorId != uint64(in.UserId) {return nil, errors.New("您没有权限删除该文章")}//删除采用软删除,修改文章状态为4 不可见状态,article.Status = 4err = l.svcCtx.ArticleModel.Update(l.ctx, article)if err != nil {return nil, err}//从有序集合中删除该条文章缓存publishTimeKey := fmt.Sprintf("biz#articles#%d#0", in.UserId)likeNumKey := fmt.Sprintf("biz#articles#%d#1", in.UserId)//Redis Zrem 命令用于移除有序集中的一个或多个成员,不存在的成员将被忽略。//删除不需要检查是否存在,因为不存在也不会报错l.svcCtx.Rds.ZremCtx(l.ctx, publishTimeKey, in.ArticleId)l.svcCtx.Rds.ZremCtx(l.ctx, likeNumKey, in.ArticleId)return &pb.ArticleDeleteResponse{}, nil
}

三、缓存击穿

1.概念

缓存击穿 是指当热点数据的缓存过期时,大量并发请求同时查询该数据,导致数据库瞬间负载过高。

2.现象

  • 热点数据过期导致大量请求打到数据库,瞬时压力增大。
  • 容易导致数据库响应变慢甚至崩溃。

3.解决方案

  1. 设置热点数据的合理过期时间,每次查询缓存的时候使用Exists来判断key是否存在,如果存在就使用Expire给缓存续期,既然是热点数据通过不断地续期也就不会过期了
  2. 利用互斥锁:只允许一个请求更新缓存,其他请求等待。

4.代码演示

方法一:延迟缓存过期时间


// 缓存续期函数
func (l *ArticlesLogic) extendCacheExpiration(ctx context.Context, key string) error {exists, err := l.svcCtx.Rds.ExistsCtx(ctx, key)if err != nil || !exists {return err}return l.svcCtx.Rds.ExpireCtx(ctx, key, articlesExpire+rand.Intn(60))
}

方法二:加锁

在singleflight 包提供了重复函数调用抑制机制。github.com/golang/groupcache/singleflight

在svc引用singleflight:

type ServiceContext struct {SingleFlightGroup singleflight.Group
}

如果缓存没有命中,只允许一个请求更新缓存

	/*......*/articlesT, _ := l.svcCtx.SingleFlightGroup.Do(fmt.Sprintf("ArticlesByUserId:%d:%d", in.UserId, in.SortType),func() (interface{}, error) {//最大查询200条//ArticlesByUserId 为自定义方法return l.svcCtx.ArticleModel.ArticlesByUserId(l.ctx, in.UserId, sortLikeNum, sortPublishTime, sortField, 200)})if articlesT != nil {//将查询结果转换为 []*model.Article 类型articles = articlesT.([]*model.Article)}/*......*/

四、缓存穿透

1.概念

缓存穿透 是指查询的数据在缓存和数据库中都不存在,导致每次查询都需要访问数据库。

2.现象

  • 恶意用户频繁查询不存在的数据,导致缓存被绕过,数据库压力过大。 恶意用户频繁请求 article🆔99999(数据库和缓存均不存在)
  • 这种情况容易造成数据库崩溃。

3.解决方案

  1. 缓存空值:当数据库中查询结果为空时,将空值缓存起来,避免重复查询数据库。
  2. 布隆过滤器:使用布隆过滤器拦截无效请求,过滤掉不存在的数据。

4.代码演示

方法一:缓存空值
这部分功能,go zero以及帮我们实现,当我们访问不存在的数据的时候,go-zero框架会帮我们自动加上空缓存,go zero会把不存在的数据的值设置为"*"

方法二:布隆过滤器
布隆过滤器的核心思想是用一个空间高效的位数组快速判断一个元素是否可能存在。如果布隆过滤器认为某个键不存在,则可以直接返回,不再查询缓存或数据库

go zero也为我们提供了 布隆过滤器 ,github.com/zeromicro/go-zero/core/bloom

import ("fmt""github.com/zeromicro/go-zero/core/bloom""github.com/zeromicro/go-zero/core/stores/redis"
)func main() {redisStore := redis.MustNewRedis(redis.RedisConf{Host: "redis-16976.c340.ap-northeast-2-1.ec2.redns.redis-cloud.com:16976",Pass: "lb8ZWuQwJENyzRiHUFjNnGJG0fgnKx5y",Type: "node",})filter := bloom.New(redisStore, "articleId", 10000)//模拟从数据库中添加数据到布隆过滤器for i := 1; i <= 50; i++ {key := fmt.Sprintf("article:id:%d", i)err := filter.Add([]byte(key))if err != nil {return}}//从布隆过滤器中查询KEY是否存在for i := 40; i <= 60; i++ {key := fmt.Sprintf("article:id:%d", i)b, _ := filter.Exists([]byte(key))fmt.Printf("%s  %v\n", key, b)}
}

五、缓存雪崩

1.概念

缓存雪崩 是指大量的请求,无法在Redis中进行处理,然后所有的请求同时访问数据库,导致数据库被打挂。

2.现象

  • 短时间内大量缓存失效,导致数据库瞬间压力过大。
  • 系统性能明显下降,甚至导致宕机。

3.解决方案

  • 设置随机过期时间:为缓存的过期时间增加随机值,避免大量缓存同时过期。
  • 分批加载缓存:将缓存重建的任务分批进行,减少瞬时压力。
  • 预热缓存:系统上线或重启时,提前加载热点数据到缓存
  • 熔断处理 :让数据库压力比较大的时候就触发熔断,忽略部分请求,尽可能的维持服务。但是这个方法是有损的

4.代码演示

方法一:设置随机过期时间

	key := fmt.Sprintf("biz#articles#%d#%d, userId, sortType)//检查缓存是否存在isExists, err := l.svcCtx.Rds.ExistsCtx(ctx, key)if err != nil {return nil, err}//给缓存设置随机过期时间if isExists {err := l.svcCtx.Rds.ExpireCtx(ctx, key, 3600 * 24 * 2+rand.Intn(60))if err != nil {return nil, err}}

方法二:熔断处理

go zero 自带熔断处理中间件


// BreakerHandler returns a break circuit middleware.
func BreakerHandler(method, path string, metrics *stat.Metrics) func(http.Handler) http.Handler {brk := breaker.NewBreaker(breaker.WithName(strings.Join([]string{method, path}, breakerSeparator)))return func(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {promise, err := brk.Allow()if err != nil {metrics.AddDrop()logx.Errorf("[http] dropped, %s - %s - %s",r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent())w.WriteHeader(http.StatusServiceUnavailable)return}cw := response.NewWithCodeResponseWriter(w)defer func() {if cw.Code < http.StatusInternalServerError {promise.Accept()} else {promise.Reject(fmt.Sprintf("%d %s", cw.Code, http.StatusText(cw.Code)))}}()next.ServeHTTP(cw, r)})}
}

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

相关文章

【hackmyvm】Diophante 靶场

1. 基本信息^toc 这里写目录标题 1. 基本信息^toc2. 信息收集2.1. 端口扫描2.2. 目录扫描2.3. knock 3. WordPress利用3.1. wpscan扫描3.2. smtp上传后门 4. 提权4.1. 提权leonard用户4.2. LD劫持提权root 靶机链接 https://hackmyvm.eu/machines/machine.php?vmDiophante 作者…

数据分析实战—鸢尾花数据分类

1.实战内容 (1) 加载鸢尾花数据集(iris.txt)并存到iris_df中,使用seaborn.lmplot寻找class&#xff08;种类&#xff09;项中的异常值&#xff0c;其他异常值也同时处理 。 import pandas as pd from sklearn.datasets import load_iris pd.set_option(display.max_columns, N…

UDP系统控制器_音量控制、电脑关机、文件打开、PPT演示、任务栏自动隐藏

UDP系统控制器(ShuiYX) 帮助文档 概述 本程序设计用于通过UDP协议接收指令来远程控制计算机的音量、执行特定命令和其他功能。为了确保程序正常工作&#xff0c;请确认防火墙和网络设置允许UDP通信&#xff0c;并且程序启动后会最小化到托盘图标。 命令格式及说明 音量控制…

Apache POI

2.1 介绍 Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下,POI 都是用于操作 Excel 文件。 Apache POI 的应用场景: 银行网银系统导出交易明细 各种业…

无人机航测VS传统测绘

无人机航测系统的优点 机动灵活&#xff0c;作业周期短&#xff1a; 无人机航测系统能够迅速响应测绘需求&#xff0c;不受地形和交通限制&#xff0c;可以灵活调整航线&#xff0c;作业周期短。 无人机体积小&#xff0c;噪音小&#xff0c;可以垂直起降、悬停、侧飞、倒飞…

Neo4j【环境部署 02】图形数据库Neo4j在Linux系统ARM架构下的安装使用

图形数据库Neo4j在Linux系统ARM架构下的安装使用 1.说明2.下载安装并配置3.其他配置4.创建一个实例5.最后 Neo4J 无论是在官网或者其他镜像网站上都是只有两个版本 Linux和 Windows不区分 X86 和 ARM&#xff0c;原因是 Neo4j 运行在 JVM 上&#xff0c;只要 JVM 能够正常使…

scala图书馆系统

class LibrarayPresentation {private val BookService new BookService()private val UserService new UserService()//显示游客的菜单def showVisitorMenu(): Unit {var running truewhile (running) {println("欢迎来到我的图书管理系统&#xff0c;请选择")p…

孔夫子根剧关键字获取已售商品 API接口详解

引言 孔夫子旧书网作为国内知名的二手书交易平台&#xff0c;提供了丰富的API接口&#xff0c;允许开发者通过编程方式获取在售商品及已售商品的信息。本文将详细介绍如何使用孔夫子旧书网的API接口&#xff0c;通过关键字获取已售商品的详细信息。 API接口概述 孔夫子旧书网…