ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

数据库专题08:点赞唯一约束——把“幂等”落到一条 SQL

数据库专题08:点赞唯一约束——把“幂等”落到一条 SQL 数据库专题08点赞唯一约束——把“幂等”落到一条 SQL用户快速双击点赞、网络重试或多个设备同时操作服务可能收到多次相同请求。若只在 Python 中先查再插两个请求都可能认为“还没点过”最后产生重复记录。本篇用likes复合主键和ON CONFLICT实现幂等点赞/取消并说明计数、事务和 Redis 热点数据如何分工。上一篇练习讲解评论练习实现了软删除和 30 天匿名化。删除是更新状态而不是立刻物理删除公开列表显示占位文本报表始终过滤deleted_at is null。管理员角色必须来自服务器会话软删除也要写审计。点赞同样要求“谁操作”来自认证上下文。1. 点赞表和复合主键createtablelikes(user_idbigintnotnullreferencesusers(id)ondeletecascade,article_idbigintnotnullreferencesarticles(id)ondeletecascade,created_at timestamptznotnulldefaultnow(),primarykey(user_id,article_id));createindexidx_likes_article_createdonlikes(article_id,created_atdesc);复合主键同时表达“唯一关系”和“查询入口”。不需要额外id因为业务从来不会按点赞 id 操作。删除用户或文章时点赞级联清理统计计数随之减少如果要保留历史可改为软删除并增加deleted_at但唯一约束设计会不同。2. 幂等点赞fromsqlalchemyimporttextdeflike_article(conn,user_id:int,article_id:int)-bool:返回本次是否新增加点赞重复请求不报错、不重复计数。resultconn.execute(text( insert into likes(user_id, article_id) select :user_id, id from articles where id:article_id and statuspublished on conflict (user_id, article_id) do nothing ),{user_id:user_id,article_id:article_id})returnresult.rowcount1defunlike_article(conn,user_id:int,article_id:int)-bool:resultconn.execute(text(delete from likes where user_id:user_id and article_id:article_id),{user_id:user_id,article_id:article_id})returnresult.rowcount1调用方在事务中根据返回值决定是否给 RedisINCR/DECR。数据库提交成功而 Redis 更新失败时计数可由定时任务重建因此 Redis 不能反过来决定点赞是否成功。3. 点赞总数和事务边界selectcount(*)aslike_countfromlikes ljoinarticles aona.idl.article_idwherel.article_id:article_idanda.statuspublished;在高并发详情页直接 count 可能变慢可以维护article_stats.like_count但更新统计必须和 likes 写入同一事务或记录可重放事件。初学项目先使用 count 验证正确性再在第19篇引入 Redis 热点计数。4. 并发实验fromconcurrent.futuresimportThreadPoolExecutordefworker(engine):withengine.begin()asconn:returnlike_article(conn,user_id7,article_id1)withThreadPoolExecutor(max_workers20)aspool:resultlist(pool.map(lambda_:worker(engine),range(20)))print(sum(result))预期输出1数据库中(7,1)只有一行。若把on conflict删除测试会暴露唯一约束异常不要在应用层捕获后再插入第二次因为这会掩盖真正的并发设计问题。5. 点赞接口的身份和防刷HTTP 接口从 token 得到user_id请求体只需要{}。短时间大量不同用户点赞属于业务风控问题可使用 Redis 限流但不能为了防刷删除数据库唯一约束。取消点赞同样幂等不存在时返回 204 或明确already_unliked由产品统一约定。验收与排错单用户重复点赞 3 次 - 第一次 created后两次 already_exists 20 并发点赞 - 新增 1 行like_count1 取消两次 - 第一次删除 1 行第二次删除 0 行 对草稿点赞 - 0 行不产生 likes如果rowcount在某个驱动中总是 -1改用returning查询或统计前后数量不要盲猜成功如果计数比关系行多检查 Redis 是否存在未落库增量。把“点赞结果”与 HTTP 响应分开数据库的rowcount是内部事实接口可以把它映射成更容易使用的响应deflike_endpoint(engine,user_id:int,article_id:int)-dict:withengine.begin()asconn:createdlike_article(conn,user_id,article_id)countconn.execute(text(select count(*) from likes where article_id:id),{id:article_id}).scalar_one()return{liked:True,created:created,like_count:count}createdfalse不是错误客户端重试仍然可以得到一致的likedtrue。计数在同一事务提交后再更新缓存若 Redis 失败只影响展示速度不影响这条点赞事实。取消点赞也应返回当前状态避免移动网络重试时前端出现“按钮已取消但数据库没删”的错觉。唯一约束的面试回答面试被问“为什么不先查再插入”时要指出时间线T1 和 T2 同时 SELECT 都得到 0随后都 INSERT只有数据库唯一索引能在并发下裁决。ON CONFLICT DO NOTHING把预期冲突转成可处理的幂等结果但仍需监控真正的约束异常、死锁和连接超时。约束是数据层最后防线应用校验是用户体验层两者不能互相替代。课后练习为点赞增加唯一的event_id并写 outbox实现“我的点赞列表”按时间倒序分页用 pytest 验证 Redis 更新失败时 PostgreSQL 点赞仍成功且可以重建计数。下一篇学习索引和EXPLAIN把本篇查询跑出执行计划。
返回列表