多维度筛选 + 分页优化

一、优化背景:痛点与目标

1. 原始问题

  • 仅支持单一条件筛选(作者/标签/收藏),无法满足复杂查询需求;
  • Offset-Limit分页在大数据量下性能急剧下降(100万条数据查第1000页耗时5秒);
  • 缓存策略不完善,热门查询重复穿透数据库。

2. 优化目标

  • 支持作者+标签+收藏多维度复合筛选;
  • 新增游标分页,保障大数据量下分页性能稳定;
  • 全场景缓存覆盖,缓存命中响应时间<1ms;
  • 完全兼容原有接口,支持渐进式迁移。

二、核心优化实现:代码级拆解

1. 多维度复合筛选:灵活的JOIN查询架构

设计思路

将特殊条件(标签、收藏)与普通条件(作者)分离,通过动态JOIN实现条件组合,同时保持统一查询接口。

关键代码实现

1.1 标签关联查询(article.go:473-482)

go 复制代码
// 关联article_tags表实现标签筛选
db = db.Joins("INNER JOIN article_tags ON article_models.id = article_tags.article_model_id").
    Where("article_tags.tag_model_id = ?", tagModel.ID)

1.2 收藏用户关联查询(article.go:484-493)

go 复制代码
// 关联favorite_models表实现收藏用户筛选
db = db.Joins("INNER JOIN favorite_models ON article_models.id = favorite_models.favorite_id").
    Where("favorite_models.favorite_by_id = ?", userModel.ID)

1.3 作者条件筛选(article.go:469-471)

go 复制代码
// 基础作者条件筛选(无需JOIN)
db = db.Where("article_models.author_id = ?", authorIDVal)

1.4 条件组合逻辑

通过判断条件数量,自动组合JOIN语句,支持:

  • 单条件(兼容原有逻辑);
  • 双条件(作者+标签/作者+收藏/标签+收藏);
  • 三条件(作者+标签+收藏)。

2. 分页优化:从Offset-Limit到游标分页

两种分页机制对比
特性 Offset-Limit 分页 游标分页
性能 O(n),offset越大越慢 O(1),性能稳定
大数据量表现(100万) 5000ms(第1000页) 25ms(第1000页)
数据一致性 有重复/遗漏风险 无一致性问题
适用场景 小数据量、需跳页 大数据量、滚动加载
游标分页核心代码(article.go:519-585)
go 复制代码
// 游标分页核心逻辑:通过ID游标替代Offset
func (s *articleStore) ListWithCursor(ctx context.Context, cursor int64, limit int) ([]*model.ArticleM, int64, bool, error) {
    var ret []*model.ArticleM
    var nextCursor int64
    var hasMore bool

    db := s.db.WithContext(ctx).Model(&model.ArticleM{})

    // 游标过滤:只查ID小于游标值的记录(保证顺序和唯一性)
    if cursor > 0 {
        db = db.Where("id < ?", cursor)
    }

    // 多查1条,用于判断是否有下一页
    queryLimit := limit + 1
    if err := db.Order("id DESC").Limit(queryLimit).Find(&ret).Error; err != nil {
        return nil, 0, false, err
    }

    // 处理hasMore和nextCursor
    hasMore = len(ret) > limit
    if hasMore {
        ret = ret[:limit] // 截断多查的1条
    }
    if len(ret) > 0 {
        nextCursor = ret[len(ret)-1].ID // 下一页游标为当前最后一条ID
    }

    return ret, nextCursor, hasMore, nil
}
复合条件游标分页扩展

在上述基础上,叠加标签/收藏/作者的JOIN查询逻辑,实现ListWithCursorAndCondition方法(article.go:587-701),保证复合条件下仍能享受游标分页的性能优势。

3. 缓存优化:全场景缓存+智能缓存键

设计思路
  • 缓存类型:文章详情缓存 + 文章列表缓存;
  • 缓存键:SHA256哈希保证唯一性;
  • 防穿透:空标记机制;
  • 过期时间:1小时(可根据业务调整)。
关键代码实现

3.1 缓存键生成(article_cache.go:239-245)

go 复制代码
// 生成唯一缓存键:查询类型+参数哈希
func GenerateQueryKey(queryType string, params map[string]interface{}) string {
    data, _ := json.Marshal(params) // 序列化查询参数(作者/标签/游标/limit等)
    hash := sha256.Sum256(data)     // SHA256哈希避免键过长
    return fmt.Sprintf("%s:%x", queryType, hash)
}

3.2 文章列表缓存操作(article_cache.go:148-199)

go 复制代码
// 从缓存获取文章列表
func (c *articleCache) GetArticleList(ctx context.Context, queryKey string) (int64, []*model.ArticleM, error) {
    val, err := c.rdb.Get(ctx, queryKey).Result()
    if err == redis.Nil {
        return 0, nil, nil // 缓存未命中
    }
    if err != nil {
        return 0, nil, err
    }

    // 反序列化缓存数据
    var cacheData struct {
        Total    int64             `json:"total"`
        Articles []*model.ArticleM `json:"articles"`
    }
    if err := json.Unmarshal([]byte(val), &cacheData); err != nil {
        return 0, nil, err
    }
    return cacheData.Total, cacheData.Articles, nil
}

// 设置文章列表到缓存
func (c *articleCache) SetArticleList(ctx context.Context, queryKey string, total int64, articles []*model.ArticleM) error {
    cacheData := struct {
        Total    int64             `json:"total"`
        Articles []*model.ArticleM `json:"articles"`
    }{
        Total:    total,
        Articles: articles,
    }
    data, err := json.Marshal(cacheData)
    if err != nil {
        return err
    }
    // 设置缓存并指定过期时间(1小时)
    return c.rdb.SetEx(ctx, queryKey, string(data), articleListTTL).Err()
}

4. 向后兼容:渐进式迁移策略

为避免修改影响现有功能,在Handler层做条件判断,单条件仍使用原有方法,多条件使用新方法:

go 复制代码
// handler/article.go:327-375
if len(conditions) == 1 {
    // 单条件:兼容原有接口
    switch {
    case author != "":
        return h.article.GetByAuthor(ctx, authorID, offset, limit)
    case tag != "":
        return h.article.GetByTag(ctx, tag, offset, limit)
    case favorited != "":
        return h.article.GetByFavorited(ctx, favorited, offset, limit)
    }
} else {
    // 多条件:使用新的复合查询方法
    return h.article.GetByComplexCondition(ctx, conditions, offset, limit)
}

三、优化效果:性能与功能双提升

优化效果:性能与功能双提升

1. 性能指标:极致的效率提升

基于真实业务场景的全量数据测试,游标分页对比传统Offset-Limit分页实现了数量级的性能突破,核心性能指标优化效果如下:

维度 传统Offset-Limit分页 游标分页 优化效果
1万条数据-首页查询 3ms 1ms 耗时降低66.7%
1万条数据-中间页查询 7ms 0ms 耗时降低100%
1万条数据-末页查询 11ms 0ms 耗时降低100%,性能提升>10倍
10万条数据-首页查询 28ms 0ms 耗时降低100%
10万条数据-中间页查询 86ms 0ms 耗时降低100%
10万条数据-末页查询 140ms 0ms 耗时降低100%,性能提升>140倍
数据库扫描行数 全表扫描(最高10万行) 仅扫描目标行(20行) 扫描行数减少99.98%以上
查询类型 ALL(全表扫描) range(范围查询) 从无索引依赖到基于主键索引高效查询

四、关键技术亮点

  1. 动态JOIN架构:分离特殊条件与普通条件,按需拼接JOIN语句,兼顾灵活性和性能;
  2. 游标分页设计 :通过id < cursor替代Offset,避免全表扫描,性能稳定在O(1);
  3. 智能缓存策略:SHA256哈希缓存键保证唯一性,空标记防穿透,全场景覆盖减少数据库压力;
  4. 渐进式迁移:单/多条件分支处理,原有功能不受影响,新功能可逐步推广。
相关推荐
齐生14 天前
iOS 知识点 - 渲染机制、动画、卡顿小集合
笔记
用户962377954485 天前
VulnHub DC-1 靶机渗透测试笔记
笔记·测试
齐生16 天前
iOS 知识点 - IAP 是怎样的?
笔记
tingshuo29176 天前
D006 【模板】并查集
笔记
tingshuo29177 天前
S001 【模板】从前缀函数到KMP应用 字符串匹配 字符串周期
笔记
西岸行者12 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
starlaky12 天前
Django入门笔记
笔记·django
勇气要爆发12 天前
吴恩达《LangChain LLM 应用开发精读笔记》1-Introduction_介绍
笔记·langchain·吴恩达
悠哉悠哉愿意12 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
勇气要爆发12 天前
吴恩达《LangChain LLM 应用开发精读笔记》2-Models, Prompts and Parsers 模型、提示和解析器
android·笔记·langchain