go-zero(十五)缓存实践:分页列表

go zero 缓存实践:分页列表

在实际开发中,分页列表 是一个非常常见的需求,尤其在面对大量数据时,通过分页可以有效减轻服务器和数据库的压力,提高用户体验。本篇文章将通过go zero 和 Redis 的结合,提供一个高效、灵活的分页列表实现方案,涵盖 基本分页逻辑Redis 缓存结合常见优化方法

一、需求分析和实现方案

1.需求

在一个社交媒体平台中,每个用户可以发布多篇文章,当用户浏览文章时,需要分页加载他们的内容。考虑以下场景:

  1. 发布时间点赞数 排序。
  2. 数据需要 支持分页,并在高并发情况下保持高性能。
  3. 结合 Redis 缓存 提升效率,减少数据库查询压力。
  4. 防止重复数据 或分页游标不一致问题。

2. 分页实现方案

分页通常分为两种实现方式

  • 基于偏移量(Offset-based Pagination): 使用 SQL 的 LIMITOFFSET 实现,适合小型数据集。
  • 基于游标(Cursor-based Pagination): 通过某个字段(如 idpublish_time)来标记分页起点,更适合大型数据集和高并发场景。

在本文中,我们主要讨论 游标分页 的实现。

完整的分页步骤总结:

  • 参数校验:确保用户输入的参数有效,并设置合理的默认值。
  • **排序字段设置 **:根据排序方式选择排序字段,确定游标的意义。
  • **缓存查询 **:尝试从缓存中获取数据,优先使用缓存提升性能。
  • **数据库查询 **:当缓存未命中时,从数据库查询数据,确保数据一致性。
  • **数据排序 **:根据排序字段对数据进行排序,确保结果符合业务逻辑。
  • **边界处理 **:防止分页数据重复,同时正确处理最后一页标记。
  • **缓存更新 **:异步更新缓存,提升后续查询效率。
  • **结果返回 **:封装分页数据、游标以及是否为最后一页的信息。

二、 项目设计

1.数据表设计

article 表:

CREATE TABLE `article` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',`title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',`content` TEXT NOT NULL COMMENT '内容',`author_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '作者ID',`like_num` INT NOT NULL DEFAULT '0' COMMENT '点赞数',`publish_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间',PRIMARY KEY (`id`),INDEX `idx_author_publish_time` (`author_id`, `publish_time`)
);

2.分页接口需求

  1. 请求参数:

    • userId:用户 ID。
    • cursor:上一页的最后一个游标值(如 publish_time)。
    • pageSize:每页的记录数量。
    • sortType:排序方式(0 按发布时间排序,1 按点赞数排序)。
  2. 返回结果:

    • isEnd:是否为最后一页。
    • cursor:下一页的游标值。
    • articles:当前页的文章列表。
    • articleId : 最后一个文章ID

article.proto 文件:

syntax = "proto3";package pb;
option go_package="./pb";service Article {rpc Articles(ArticlesRequest) returns (ArticlesResponse);
}message ArticlesRequest {int64 userId = 1;int64 cursor = 2;int64 pageSize = 3;int64 sortType = 4;}message ArticleItem {int64 Id = 1;string title = 2;string content = 3;string description = 4;string cover = 5;int64 commentCount = 6;int64 likeCount = 7;int64 publishTime = 8;int64 authorId = 9;
}message ArticlesResponse {repeated ArticleItem articles = 1;bool isEnd = 2;int64 cursor = 3;int64 articleId = 4;
}

三、项目实现

为了进一步提高性能,可以使用 Redis 存储文章列表的分页缓存。这里使用 Redis 的有序集合(ZSET),根据 publish_timelike_num 排序。

1.自定义常量

const (SortPublishTime = iotaSortLikeCount
)
const (articlesExpire = 3600 * 24 * 2
)
const (DefaultPageSize       = 20DefaultLimit          = 200DefaultSortLikeCursor = 1 << 30
)

2.通过用户ID查询文章

func (m *customArticleModel) ArticlesByUserId(ctx context.Context, userId, likeNum int64, pubTime, sortField string, limit int) ([]*Article, error) {//var anyField anyvar sql stringif sortField == "like_num" {//anyField = likeNum//sql = fmt.Sprintf("select "+articleRows+" from "+m.table+" where user_id=? and like_num < ? order by %s desc limit ?", sortField)sql = fmt.Sprintf("select %s from %s  where `author_id`=? and like_num < %d order by %s desc limit ?", articleRows, m.table, likeNum, sortField)} else {//anyField = pubTimesql = fmt.Sprintf("select %s from %s  where `author_id`=? and publish_time < '%s' order by %s desc limit ?", articleRows, m.table, pubTime, sortField)}var articles []*Articleerr := m.QueryRowsNoCacheCtx(ctx, &articles, sql, userId, limit)if err != nil {return nil, err}return articles, nil
}

3.从缓存查询数据

// 查数据  先查缓存, 如果存在则续期
func (l *ArticlesLogic) cacheArticles(ctx context.Context, userId, cursor, pageSize, sortType int64) ([]int64, error) {key := fmt.Sprintf("biz#articles#%d#%d", userId, sortType)err := l.extendCacheExpiration(ctx, key)if err != nil {return nil, err}return l.fetchArticlesFromCache(ctx, key, cursor, pageSize)}// 缓存续期函数
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))
}// 从缓存中获取文章 ID
func (l *ArticlesLogic) fetchArticlesFromCache(ctx context.Context, key string, cursor int64, pageSize int64) ([]int64, error) {paris, err := l.svcCtx.Rds.ZrevrangebyscoreWithScoresAndLimitCtx(ctx, key, 0, cursor, 0, int(pageSize))if err != nil {return nil, err}var ids []int64for _, pair := range paris {id, err := strconv.ParseInt(pair.Key, 10, 64)if err != nil {return nil, err}ids = append(ids, id)}return ids, nil
}

4.从数据库中查询文章信息

如果缓存未命中,使用MapReduce从数据库中查询,go zero会自动写入缓存


// 缓存没有去数据库
func (l *ArticlesLogic) articleByIds(ctx context.Context, articleIds []int64) ([]*model.Article, error) {articles, err := mr.MapReduce[int64, *model.Article, []*model.Article](func(source chan<- int64) {for _, aid := range articleIds {source <- aid}}, func(id int64, writer mr.Writer[*model.Article], cancel func(error)) {p, err := l.svcCtx.ArticleModel.FindOne(ctx, id)if err != nil {cancel(err)return}writer.Write(p)}, func(pipe <-chan *model.Article, writer mr.Writer[[]*model.Article], cancel func(error)) {var articles []*model.Articlefor article := range pipe {articles = append(articles, article)}writer.Write(articles)})if err != nil {return nil, err}return articles, nil
}

5.数据添加到有序集合

如果从数据库中查询到信息,把它加入到redis的有序集合中


func (l *ArticlesLogic) addCacheArticles(ctx context.Context, articles []*model.Article, userId int64, sortType int32) error {if len(articles) == 0 {return nil}key := fmt.Sprintf("biz#articles#%d#%d", userId, sortType)for _, article := range articles {var score int64if sortType == SortLikeCount {score = article.LikeNum} else if sortType == SortPublishTime && article.Id != 0 {score = article.PublishTime.Local().Unix()}if score < 0 {score = 0}_, err := l.svcCtx.Rds.ZaddCtx(ctx, key, score, strconv.Itoa(int(article.Id)))if err != nil {return err}}return l.svcCtx.Rds.ExpireCtx(ctx, key, articlesExpire)
}

6.游标分页主逻辑

func (l *ArticlesLogic) Articles(in *pb.ArticlesRequest) (*pb.ArticlesResponse, error) {// todo: add your logic here and delete this line//输入校验  检查 SortType/UserId  是否有效if in.SortType != SortPublishTime && in.SortType != SortLikeCount {in.SortType = SortPublishTime}if in.UserId < 0 {return nil, errors.New("用户ID不合法")}//设置默认的 PageSize 和 Cursor。if in.PageSize == 0 {in.PageSize = DefaultPageSize}if in.Cursor == 0 {if in.SortType == SortPublishTime {in.Cursor = time.Now().Unix()} else {in.Cursor = DefaultSortLikeCursor}}var sortField stringvar sortLikeNum int64var sortPublishTime string//根据排序类型确定排序字段if in.SortType == SortLikeCount {sortField = "like_num"sortLikeNum = in.Cursor} else {sortField = "publish_time"//2023-12-01 12:00:00//sortPublishTime = "CURRENT_TIMESTAMP"sortPublishTime = time.Unix(in.Cursor, 0).Format("2006-01-02 15:04:05")}var isCache = falsevar isEnd boolvar curPage []*pb.ArticleItemvar articles []*model.Articlevar err errorvar lastId, cursor int64// 先查缓存 ,缓存不要做错误处理, 不影响正常流程//尝试通过缓存获取文章ID集合articleIds, _ := l.cacheArticles(l.ctx, in.UserId, in.Cursor, in.PageSize, int64(in.SortType))if len(articleIds) > 0 {fmt.Println("缓存命中")//如果缓存中有数据,标记 isCache 为 trueisCache = true//若缓存返回的最后一个 ID 为 -1,表示数据已经到达末尾,设置 isEnd = trueif articleIds[len(articleIds)-1] == -1 {isEnd = true}fmt.Println("articleIds:", articleIds)//根据缓存的文章 ID 查询具体的文章内容。articles, err = l.articleByIds(l.ctx, articleIds)if err != nil {return nil, err}// 通过sortFiled对articles进行排序var cmpFunc func(a, b *model.Article) intif sortField == "like_num" {cmpFunc = func(a, b *model.Article) int {return cmp.Compare(b.LikeNum, a.LikeNum)}} else {cmpFunc = func(a, b *model.Article) int {return cmp.Compare(b.PublishTime.Unix(), a.PublishTime.Unix())}}slices.SortFunc(articles, cmpFunc)// 数据封装与分页//遍历排序后的文章数据,将其封装为 pb.ArticleItem 并追加到 curPagefor _, article := range articles {curPage = append(curPage, &pb.ArticleItem{Id:           int64(article.Id),Title:        article.Title,Content:      article.Content,LikeCount:    article.LikeNum,AuthorId:     int64(article.AuthorId),CommentCount: article.CommentNum,PublishTime:  article.PublishTime.Unix(),})}} else {//使用 SingleFlight 防止并发查询,确保同一用户的多次查询只会执行一次数据库操作。//如果缓存未命中,则查询数据库获取文章列表。articlesT, _ := l.svcCtx.SingleFlightGroup.Do(fmt.Sprintf("ArticlesByUserId:%d:%d", in.UserId, in.SortType),func() (interface{}, error) {//最大查询200条return l.svcCtx.ArticleModel.ArticlesByUserId(l.ctx, in.UserId, sortLikeNum, sortPublishTime, sortField, 200)})if articlesT == nil {return &pb.ArticlesResponse{}, nil}//将查询结果转换为 []*model.Article 类型//从数据库查询结果中获取文章数据articles = articlesT.([]*model.Article)//第一页var firstPageArticles []*model.Article//如果文章数量超过了 PageSize,只取前 PageSize 个文章。if len(articles) > int(in.PageSize) {//设置第一页的文章数据firstPageArticles = articles[:int(in.PageSize)]} else {firstPageArticles = articlesisEnd = true}//把第一页的数据,存储到当前页数据for _, article := range firstPageArticles {curPage = append(curPage, &pb.ArticleItem{Id:           int64(article.Id),Title:        article.Title,Content:      article.Content,LikeCount:    article.LikeNum,AuthorId:     int64(article.AuthorId),CommentCount: article.CommentNum,PublishTime:  article.PublishTime.Unix(),})}}if len(curPage) > 0 {//获取当前页的最后一个数据的 ID 和 CursorpageLast := curPage[len(curPage)-1]lastId = pageLast.Id//根据上一页最后一个数据的cursor设置下一页的Cursorif in.SortType == SortLikeCount {cursor = pageLast.LikeCount} else {cursor = pageLast.PublishTime}// 确保 Cursor 不为负数if cursor < 0 {cursor = 0}//判断是否有重复的文章for k, article := range curPage {if in.SortType == SortPublishTime {if article.PublishTime == in.Cursor && article.Id == in.ArticleId {curPage = curPage[k:] // 从下一个开始break}} else {if article.LikeCount == in.Cursor && article.Id == in.ArticleId {curPage = curPage[k:] // 从下一个开始break}}}}//fmt.Println("isCache:", isCache)if !isCache {fmt.Println("补偿数据")threading.GoSafe(func() {if len(articles) < DefaultLimit && len(articles) > 0 {articles = append(articles, &model.Article{Id: -1})}err = l.addCacheArticles(context.Background(), articles, in.UserId, in.SortType)if err != nil {logx.Errorf("addCacheArticles error: %v", err)}})}return &pb.ArticlesResponse{IsEnd:     isEnd,Cursor:    cursor,ArticleId: lastId,Articles:  curPage,}, nil}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/493305.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Word使用分隔符实现页面部分分栏

文章目录 Word使用分隔符实现页面部分分栏分隔符使用页面设置 Word使用分隔符实现页面部分分栏 分隔符使用 word中的分隔符&#xff1a; 前面不分栏&#xff0c;后面分栏(或前面分栏&#xff0c;后面不分栏)&#xff0c;只需要在分隔位置处插入分隔符&#xff1a;“连续”即…

多协议视频监控汇聚/视频安防系统Liveweb搭建智慧园区视频管理平台

智慧园区作为现代化城市发展的重要组成部分&#xff0c;不仅承载着产业升级的使命&#xff0c;更是智慧城市建设的重要体现。随着产业园区竞争的逐渐白热化&#xff0c;将项目打造成完善的智慧园区是越来越多用户关注的内容。 然而我们往往在规划前期就开始面临众多难题&#…

PHP接入美团联盟推广

美团给的文档没有PHP的示例代码&#xff0c;下面是以Javascript示例更改的PHP代码&#xff0c;并且已经跑通 一、计算签名 签名类&#xff0c;因为接口不多&#xff0c;所以这里只写了获取请求头 class Meituan {private $APP_KEY 你的APP_KEY;private $APP_SECRET 你的APP…

ChatGPT重大更新:新增实时搜索和高级语音

12月17日消息&#xff0c;据报道&#xff0c;OpenAI开启了第八天技术分享直播&#xff0c;对ChatGPT搜索功能进行了大量更新。 此次ChatGPT新增的功能亮点纷呈。其中&#xff0c;实时搜索功能尤为引人注目。OpenAI对搜索算法进行了深度优化&#xff0c;使得用户提出问题后&…

Vue3组件封装技巧与心得

摘要&#xff1a; 日常开发中&#xff0c;用Vue组件进行业务拆分&#xff0c;代码解耦是一个很好的选择&#xff1b; 今天就来分享一下我在使用Vue3进行组件封装的一些技巧和心得&#xff0c;希望能够帮助到大家&#xff1b; 1. 组件特性&#xff1a; 在Vue中组件是一个独立的…

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

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…

【自用】通信内网部署rzgxxt项目_01,后端pipeDemo部署(使用nssm.exe仿照nohup)

做完这些工作之后&#xff0c;不要忘记打开 Windows Server 的防火墙端口&#xff0c;8181、8081、8080、22、443、1521 做完这些工作之后&#xff0c;不要忘记打开 Windows Server 的防火墙端口&#xff0c;8181、8081、8080、22、443、1521 做完这些工作之后&#xff0c;不要…

【Apache Doris】周FAQ集锦:第 26 期

SQL问题 Q1 doris 3.0存算分离模式下&#xff0c;建表的时是否需要指定表的副本数 不需要&#xff0c;指定了也会忽略&#xff1b;存算分离模式下&#xff0c;数据副本由远端存储去管控。 Q2 doris 通过dbeaver查询时报错&#xff1a;[SXXXX]… doris的错误码通常都是EXXXX&…

OpenSSL 心脏滴血漏洞(CVE-2014-0160)

OpenSSL 心脏滴血漏洞(CVE-2014-0160) Openssl简介: 该漏洞在国内被译为"OpenSSL心脏出血漏洞”&#xff0c;因其破坏性之大和影响的范围之广&#xff0c;堪称网络安全里程碑事件。 OpenSSL心脏滴血漏洞的大概原理是OpenSSL在2年前引入了心跳(hearbea0机制来维特TS链接的…

Git实用指南(精简版)

目录 读者须知 Git是什么 Git的原理 文件在Git中的几种状态 快速上手 结尾 读者须知 本文章适合从未接触过git,或者需要深度学习Git的用户进行阅读. 文末有详细的文档,读者可以前往Github下载阅读!!三克油 Git是什么 简单来说,Git是一个代码备份工具,你可以使用指令对…

YOLOv8目标检测(七)_AB压力测试

YOLOv8目标检测(一)_检测流程梳理&#xff1a;YOLOv8目标检测(一)_检测流程梳理_yolo检测流程-CSDN博客 YOLOv8目标检测(二)_准备数据集&#xff1a;YOLOv8目标检测(二)_准备数据集_yolov8 数据集准备-CSDN博客 YOLOv8目标检测(三)_训练模型&#xff1a;YOLOv8目标检测(三)_训…

在 Spring Boot 3 中实现基于角色的访问控制

基于角色的访问控制 (RBAC) 是一种有价值的访问控制模型,可增强安全性、简化访问管理并提高效率。它在管理资源访问对安全和运营至关重要的复杂环境中尤其有益。 我们将做什么 我们有一个包含公共路由和受限路由的 Web API。受限路由需要数据库中用户的有效 JWT。 现在用户…

线程知识总结(一)

1、概述 1.1 进程与线程 进程是程序运行时&#xff0c;操作系统进行资源分配的最小单位&#xff0c;包括 CPU、内存空间、磁盘 IO 等。从另一个角度讲&#xff0c;进程是程序在设备&#xff08;计算机、手机等&#xff09;上的一次执行活动&#xff0c;或者说是正在运行中的程…

OpenCV圆形标定板检测算法findGrid原理详解

OpenCV的findGrid函数检测圆形标定板的流程如下: class CirclesGridClusterFinder {CirclesGridClusterFinder(const CirclesGridClusterFinder&); public:CirclesGridClusterFinder

基于SpringBoot+Vue实现的个人备忘录系统

&#x1f384; 写在前面 最近学习vue&#xff0c;所以抽时间就用SpringBootVue做了一个个人备忘录&#xff0c;本意是想打造一个轻量级的、自托管的备忘录中心&#xff0c;可能是老了&#xff08;haha&#xff09;,很多时候都觉得好记性不如烂笔头&#xff0c;所以就有了这个小…

docker简单命令

docker images 查看镜像文件 docker ps -a 查看容器文件 docker rm 0b2 删除容器文件&#xff0c;id取前三位即可 docker rmi e64 删除镜像文件&#xff08;先删容器才能删镜像&#xff09;&#xff0c;id取前三位即可 在包含Dockerfile文件的目录…

【前端】vue数组去重的3种方法

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、数组去重说明二、Vue数组去重的3种方法 前言 随着开发语言及人工智能工具的普及&#xff0c;使得越来越多的人会主动学习使用一些开发工具&#xff0c;本…

BPMN与一般的流程图区别在那里?

1. 语义和标准性 BPMN&#xff08;业务流程建模符号&#xff09; 基于标准语义&#xff1a;BPMN是一种标准化的业务流程建模语言&#xff0c;拥有一套严谨的语义规范。它由国际对象管理组织&#xff08;OMG&#xff09;维护&#xff0c;定义了事件、活动、网关和流向等元素的确…

《薄世宁医学通识50讲》以医学通识为主题,涵盖了医学的多个方面,包括医学哲学、疾病认知、治疗过程、医患关系、公共卫生等

《薄世宁医学通识50讲》是一门由薄世宁医生主讲的医学通识课程&#xff0c;该课程旨在通过深入浅出的方式&#xff0c;向广大听众普及医学知识&#xff0c;提升公众对医学的认知和理解。 晓北斗推荐-薄世宁医学通识 以下是对该课程的详细介绍&#xff1a; 一、课程概述 《薄世…

二八(vue2-04)、scoped、data函数、父子通信、props校验、非父子通信(EventBus、provideinject)、v-model进阶

1. 组件的三大组成部分(结构/样式/逻辑) 1.1 scoped 样式冲突 App.vue <template><!-- template 只能有一个根元素 --><div id"app"><BaseOne></BaseOne><BaseTwo></BaseTwo></div> </template><script…