news 2026/6/3 7:00:14

点赞状态和点赞排行榜功能在 Redis下的实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
点赞状态和点赞排行榜功能在 Redis下的实现

一、 点赞状态管理实现

在探店笔记的互动场景中,点赞功能承载着高频的状态切换与实时反馈。传统关系型架构通常依赖一张独立的tb_blog_like关联表记录user_idblog_id的映射关系。然而,在万级并发下,频繁的INSERTDELETE将引发严重的行锁竞争与索引碎片,拖垮数据库写入吞吐。

1.1 数据结构选型与职责划分

引入 Redis 并非为了替代数据库的持久化能力,而是通过读写分离语义映射优化交互路径。我们采用以下职责划分策略:

  • MySQL 侧:仅维护tb_blog表中的liked(点赞总数)整型字段。摒弃关联表,以单字段聚合替代多表 Join,大幅降低存储开销与写入锁竞争。
  • Redis 侧:采用Set结构维护blog:liked:{blog_id}集合,存储已点赞的用户 ID 列表。利用 Set 天然的元素唯一性与 O(1) 时间复杂度,承载“当前用户是否已点赞”的状态校验。

1.2 点赞切换流程与时序图

点赞操作并非单纯的缓存写入,而是 Redis 状态切换与数据库总数同步的协同过程。当用户触发点赞时,系统首先通过SISMEMBER校验当前用户在 Set 中的存在性。若未存在,则执行SADD将用户 ID 加入集合,并同步执行数据库UPDATE tb_blog SET liked = liked + 1;若已存在,则执行SREM移除用户 ID,并同步执行数据库UPDATE tb_blog SET liked = liked - 1。该设计将高频的状态判断下沉至内存,将低频的总数聚合保留在关系型存储中,形成清晰的读写边界。

MySQL 数据库Redis 服务端FastAPI 路由层客户端MySQL 数据库Redis 服务端FastAPI 路由层客户端alt[未点赞 (0)][已点赞 (1)]1. 提交点赞/取消请求 (blog_id, user_id)12. SISMEMBER blog:liked:{id} user_id23. 返回 1 (已点赞) 或 0 (未点赞)34. SADD blog:liked:{id} user_id45. UPDATE blog SET liked = liked + 156. 返回 {"action":"like","is_like":true}64. SREM blog:liked:{id} user_id75. UPDATE blog SET liked = liked - 186. 返回 {"action":"unlike","is_like":false}9

1.3 生产级代码实现

importloggingfromfastapiimportFastAPI,HTTPException,Depends,Requestfromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlalchemyimportupdateimportredis.asyncioasaioredisfromcommon.databaseimportget_dbfromcommon.authimportget_current_user_idfrommodule_blog.modelimportBlog logger=logging.getLogger(__name__)app=FastAPI()redis_client=aioredis.Redis(host="127.0.0.1",port=6379,db=0,decode_responses=True)defget_blog_liked_key(blog_id:int)->str:returnf"blog:liked:{blog_id}"@app.post("/blog/like/{blog_id}")asyncdeftoggle_blog_like(blog_id:int,request:Request,db:AsyncSession=Depends(get_db)):user_id=request.state.user_id blog=awaitdb.get(Blog,blog_id)ifnotblog:raiseHTTPException(status_code=404,detail="Blog not found")liked_key=get_blog_liked_key(blog_id)# 1. 原子判断当前用户是否已点赞is_liked=awaitredis_client.sismember(liked_key,str(user_id))ifis_liked:# 已点赞 -> 取消:移除集合元素 + 数据库总数递减awaitredis_client.srem(liked_key,str(user_id))awaitdb.execute(update(Blog).where(Blog.id==blog_id).values(liked=Blog.liked-1))action="unlike"new_is_like=Falseelse:# 未点赞 -> 点赞:添加集合元素 + 数据库总数递增awaitredis_client.sadd(liked_key,str(user_id))awaitdb.execute(update(Blog).where(Blog.id==blog_id).values(liked=Blog.liked+1))action="like"new_is_like=Trueawaitdb.commit()return{"blog_id":blog_id,"action":action,"is_like":new_is_like,"status":"success"}

该方案彻底消除了传统关联表带来的写入放大与锁竞争问题。Redis Set 仅承载高频的 O(1) 状态校验,MySQL 仅负责低频的总数聚合。在详情查询时,SISMEMBERSCARD的组合可将状态注入延迟压缩至微秒级。若 Redis 发生抖动,系统可直接降级读取 MySQLliked字段与分页查询用户点赞记录,保障核心链路可用性,而不会引发数据丢失。

二、 点赞排行榜功能的数据类型选择

笔记详情页需展示“最早点赞的 TopN 用户”,其核心语义包含两个维度:按时间先后严格排序用户身份唯一性。在 Redis 提供的数据结构中,List、Set 与 SortedSet 均可存储用户 ID,但在排序方式、唯一性保障与查找效率上存在本质差异。需结合业务约束进行逐层推演。

2.1 数据结构多维度对比推演

List 结构的局限性
List 底层基于双向链表实现,其排序方式严格依赖元素的添加顺序。若用户先点赞、取消后再重新点赞,LPUSH会将该用户 ID 重新插入链表头部,原始时间序被彻底打乱。其次,List 不具备唯一性约束,同一用户误触多次将导致集合中出现重复 ID。在查找方式上,List 仅支持按索引或首尾检索,无法直接通过用户 ID 定位元素位置,取消点赞需依赖LREM遍历匹配,时间复杂度退化至 O(N)。因此,List 无法满足“按时间精确排序”与“高效去重”的双重诉求。

Set 结构的局限性
Set 底层基于哈希表实现,具备严格的唯一性约束,重复添加同一用户 ID 会被自动忽略。其查找方式同样基于元素哈希,支持 O(1) 的快速存在性校验。然而,Set 的核心缺陷在于完全无法排序。哈希表的随机分布特性使得元素存储顺序与插入时间毫无关联,执行SMEMBERS仅能获取无序集合,无法直接截取 TopN 或按时间倒序排列。若强行在应用层拉取全量 Set 后排序,将引发严重的网络传输开销与内存计算压力。

SortedSet 的精准契合
SortedSet(ZSet)在 Set 的去重基础上引入 Score(分值)维度,底层采用压缩列表或跳表(SkipList)实现。其排序方式严格依据 Score 值单调递增或递减排列,完美契合“点赞时间戳”的映射需求。唯一性方面,同一用户 ID 再次ZADD仅会覆盖原有 Score,不会破坏集合基数。查找方式支持按元素直接定位,且结合ZRANKZRANGE可实现 O(log N + M) 的排名查询与区间截取。三者对比之下,SortedSet 是唯一能够同时承载“时间序、唯一性、高效截取”语义的结构。

对比维度List 结构Set 结构SortedSet 结构业务匹配度
排序方式按添加顺序排序,重复操作打乱序列底层哈希表无序,无法排序根据 Score 值严格单调排序SortedSet 唯一支持时间序
唯一性不唯一,需额外逻辑防重唯一,自动去重唯一,覆盖旧 Score三者均能满足(List 需补偿)
查找方式按索引或首尾查找,O(N) 遍历根据元素哈希查找,O(1)根据元素查找,O(1) 定位SortedSet 兼顾排序与高效查询

结论:SortedSet 以微小的内存开销(每个元素额外存储 8 字节 Score 与跳表指针),换取了时间复杂度从 O(N) 到 O(log N) 的跨越,是构建时间序排行榜的唯一合理选型。

三、 排行榜功能具体实现

3.1 键值对设计与时间映射

基于上述推演,排行榜的键值映射遵循“业务域:功能域:主键”规范,Key 设计为blog:rank:{blog_id}。Value 中,Member 存储user_id,Score 存储点赞发生的毫秒级时间戳(time.time())。点赞时执行ZADD插入或覆盖,取消点赞执行ZREM原子移除,查询 TopN 执行ZRANGE按 Score 降序截取。

3.2 完整实现时序图

MySQL 数据库Redis 服务端FastAPI 路由层客户端MySQL 数据库Redis 服务端FastAPI 路由层客户端场景:查询点赞排行榜 Top5场景:用户执行点赞/取消1. GET /blog/likes/{id}?limit=512. ZRANGE blog:rank:{id} 0 4 DESC WITHSCORES23. 返回 [(uid1, ts1), (uid2, ts2)...]34. 批量 SELECT 用户基础信息 (WHERE id IN (...))45. 返回用户详情映射表56. 按原始顺序组装 DTO,格式化时间戳67. 返回 Top5 用户列表及点赞时间71. 触发点赞切换82. ZADD/ZREM + 时间戳/移除93. 同步更新 blog.liked 总数104. 返回状态同步结果11

3.3 生产级代码实现

importtimefromtypingimportList,DictfromfastapiimportAPIRouter,DependsfrompydanticimportBaseModelfromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlalchemyimportselectimportredis.asyncioasaioredisfromcommon.databaseimportget_dbfrommodule_user.modelimportUserfrommodule_blog.modelimportBlog router=APIRouter()redis_client=aioredis.Redis(host="127.0.0.1",port=6379,db=0,decode_responses=True)classRankUserDTO(BaseModel):id:intnickname:stravatar:strliked_time:str@router.get("/blog/likes/{blog_id}",response_model=List[RankUserDTO])asyncdefget_blog_likes_ranking(blog_id:int,limit:int=5,db:AsyncSession=Depends(get_db)):rank_key=f"blog:rank:{blog_id}"# 1. 按 Score(时间戳)降序截取 TopNmembers_with_scores=awaitredis_client.zrange(rank_key,0,limit-1,desc=True,withscores=True)ifnotmembers_with_scores:return[]user_ids=[int(uid)foruid,_inmembers_with_scores]# 2. 批量查询用户基础信息,避免 N+1 查询问题result=awaitdb.execute(select(User).where(User.id.in_(user_ids)))user_map={u.id:uforuinresult.scalars().all()}# 3. 保持 ZRANGE 原始顺序组装响应ranking_list=[]foruid,scoreinmembers_with_scores:user=user_map.get(int(uid))ifuser:ranking_list.append(RankUserDTO(id=user.id,nickname=user.nickname,avatar=user.avatar,liked_time=time.strftime("%Y-%m-%d %H:%M",time.localtime(score))))returnranking_list

SortedSet 虽能高效承载排行榜,但需注意内存膨胀风险。单篇爆款笔记的点赞数可能突破十万级,若全量保留历史时间戳将占用可观内存。生产环境通常采用“冷热分离”策略:对ZCARD超过阈值(如 10000)的 Key,通过定时任务将尾部冷数据归档至 MySQL 历史表,并执行ZREMRANGEBYRANK截断,仅保留头部热数据。点赞总数的权威值始终以 MySQLliked字段为准,Redis 排行榜仅作为高频查询的加速层,通过异步任务或 Binlog 监听保障双端最终一致。该设计在保障 TopN 毫秒级响应的同时,严格遵循了存储边界与内存治理规范。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/3 6:54:06

构建个人知识复利系统:从信息处理到可复用资产的技术实践

1. 项目概述:从“银质红利”看个人研究的长期价值最近在整理过往的研究笔记和项目资料时,一个老话题——“研究的长期价值”——又浮现在我的脑海里。这让我想起了多年前读过的一篇关于“Fitzgibbon’s Research Reaps Silver Dividend”的案例&#xff…

作者头像 李华