高并发点赞场景下数据库自增字段会卡死,因update触发行锁导致排队超时;应改用redis原子计数(incrby+setnx防重)+异步回刷mysql,并通过频控、jwt校验、降级策略保障一致性。

为什么直接用数据库自增字段会卡死
高并发点赞场景下,UPDATE likes SET count = count + 1 WHERE post_id = ? 这类语句在 MySQL 中会触发行锁,大量请求排队等锁,QPS 上不去还容易超时。更糟的是,如果没加唯一约束,重复点击可能产生脏数据。
实操建议:
- 避免在事务中对同一
post_id频繁执行该 SQL;哪怕用了SELECT FOR UPDATE,锁持有时间一长,瓶颈立刻暴露 - 不要依赖数据库的原子性来扛峰值——它不是为每秒几千次计数更新设计的
- 真实业务中,点赞结果允许短暂不一致(比如前端显示“+1”后延时落库),这是可接受的妥协点
用 Redis 做原子计数器的正确姿势
INCR 看似简单,但直接裸用会丢数据:Redis 崩溃、网络分区、客户端重试逻辑缺失都会导致计数不准。必须结合写后回刷或幂等去重。
实操建议:
- 用
INCRBY而非INCR,方便批量合并(比如一次收 10 个点赞,INCRBY post:123 10) - 为每个用户-帖子组合加
SETNX user_like_post:u123:p456 1,过期设为 24h,防止重复提交 - 后台起 goroutine 定期扫描 Redis 中的点赞增量(比如每 5 秒读一次
GET post:123),写入 MySQL 并清零,用GETSET保证原子性
Gin + GORM 实现带防刷的点赞 Handler
框架层要挡住明显异常请求,不能全丢给下游。Gin 的中间件和 GORM 的事务控制是关键防线。
实操建议:
- 在 handler 开头用
c.ClientIP()+c.GetHeader("User-Agent")做基础频控,单 IP 每分钟最多 30 次/api/v1/like - 检查
user_id是否从 JWT token 解析而来,绝不信任前端传的user_id参数 - 调用
db.Transaction()包裹 MySQL 写操作,但只用于记录日志或最终一致性校验,不用于实时计数更新 - 返回值里不要同步返回最新总数,而是返回
{"status": "ok", "cached_count": 12345},这个值来自 RedisGET,快且准
如何验证并发安全性和数据一致性
本地压测容易误判:Docker 里的 Redis 和 MySQL 共享宿主机资源,网络延迟几乎为 0,跟生产环境差距极大。
实操建议:
- 用
hey -n 10000 -c 200 http://localhost:8080/api/v1/like?post_id=123测吞吐,观察 Redis CPU 和 MySQL 的SHOW PROCESSLIST中锁等待数 - 故意 kill Redis,看服务是否降级为只写 MySQL(需提前实现 fallback 逻辑),并确认恢复后 Redis 计数能追平
- 上线前跑一次「一致性校验脚本」:遍历热门帖子,比对
SELECT count(*) FROM likes WHERE post_id = ?和GET post:?,差值超过阈值就告警
真正难的不是写出来,是让 Redis 的缓存、MySQL 的持久化、前端的展示三者在故障时仍保持边界清晰——比如 Redis 失效期间,MySQL 计数不准是预期之内的,但绝不能出现负数或重复记录。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











