
1. 项目概述在Web开发中分页查询是最常见的需求之一。传统关系型数据库通过LIMIT OFFSET语法实现分页但在高并发场景下这种查询方式会给数据库带来巨大压力。最近我在一个日活百万级的社区项目中就遇到了分页查询的性能瓶颈问题。通过Redis的List数据结构我们可以完美模拟SQL的LIMIT OFFSET分页机制将热点数据缓存在内存中。实测下来原本需要200ms的数据库分页查询改用Redis List后响应时间降低到5ms以内且对数据库的QPS下降了80%。下面我就详细分享这个方案的实现细节。2. 核心设计思路2.1 为什么选择Redis ListRedis的List是一个双向链表结构具有以下特性非常适合分页场景O(1)时间复杂度获取头尾元素按索引随机访问时间复杂度为O(N)但在分页场景下N通常很小每页20-50条天然支持LRANGE命令相当于内置分页功能内存存储没有磁盘IO瓶颈2.2 与传统方案的对比方案优点缺点MySQL LIMIT实现简单深度分页性能差游标分页性能稳定实现复杂不支持随机跳页Redis List性能极高需要维护缓存一致性3. 具体实现方案3.1 数据结构设计我们以社区帖子列表为例每个帖子用Hash存储帖子ID列表用List存储post:list - [1001, 1002, 1003, ...] (帖子ID列表) post:detail:1001 - {title: ..., content: ..., author: ...}3.2 写入流程def add_post(post): # 1. 先写入数据库 post_id db.insert(post) # 2. 写入Redis缓存 redis.rpush(post:list, post_id) redis.hmset(fpost:detail:{post_id}, post.to_dict()) # 3. 控制列表长度避免内存占用过大 if redis.llen(post:list) 1000: redis.ltrim(post:list, 0, 999)3.3 分页查询实现def get_posts(page1, size10): start (page - 1) * size end start size - 1 # 1. 获取当前页的帖子ID列表 post_ids redis.lrange(post:list, start, end) # 2. 批量获取帖子详情 pipeline redis.pipeline() for post_id in post_ids: pipeline.hgetall(fpost:detail:{post_id}) posts pipeline.execute() return posts4. 关键优化点4.1 缓存预热策略系统启动时自动加载最近1000条热门帖子def warm_up_cache(): hot_posts db.query(SELECT * FROM posts ORDER BY create_time DESC LIMIT 1000) for post in hot_posts: redis.rpush(post:list, post.id) redis.hmset(fpost:detail:{post.id}, post.to_dict())4.2 双写一致性保障采用先更新数据库再删除缓存的策略def update_post(post): # 1. 更新数据库 db.update(post) # 2. 删除缓存 redis.delete(fpost:detail:{post.id}) # 3. 异步重建缓存 async_rebuild_cache(post.id)5. 踩坑实录5.1 大Key问题初期我们缓存了完整的帖子内容导致单个List元素过大。优化方案只缓存ID和基础字段超过10KB的内容单独存储5.2 分页漂移问题在频繁更新的场景下可能出现分页结果重复或丢失。解决方案对实时性要求高的场景改用游标分页或者使用ZSET按时间戳排序5.3 缓存穿透防护对不存在的页码参数要做校验def validate_page(page, size): total redis.llen(post:list) max_page math.ceil(total / size) return min(page, max_page)6. 性能测试数据在4核8G的Redis实例上测试数据量平均响应时间QPS1万条2.3ms450010万条3.1ms4200100万条5.8ms3800相比直接查询MySQL性能提升了40-100倍。这个方案特别适合读多写少的场景比如新闻列表、商品列表、评论列表等。实际项目中我们还将这个方案扩展到了用户消息通知等更多场景。