不该直接UPDATE表字段。高频点赞时会导致行锁争用、事务排队和超时;应采用写分离,记录明细事件后异步聚合,或在允许弱一致性时用乐观锁+重试机制。

点赞计数该不该直接 UPDATE 表字段?
不该。高频点赞场景下,对 likes_count 字段做 UPDATE post SET likes_count = likes_count + 1 会引发行锁争用,尤其在 InnoDB 中,同一行被大量并发请求更新时,事务排队、锁等待、甚至超时(Lock wait timeout exceeded)很常见。
真正可行的路径是「写分离」:把点赞动作记为明细事件,异步聚合更新计数。但若业务允许弱一致性(比如点赞数延迟几秒再刷新),可先用乐观锁兜底:
- 加
version字段或用WHERE likes_count = ?做条件更新,失败则重试 - 用
INSERT ... ON DUPLICATE KEY UPDATE记录用户-帖子点赞关系(唯一索引:user_id + post_id),再通过COUNT(*)实时查——适合低频或数据量小的情况 - 绝不直接在高并发接口里执行
UPDATE ... SET count = count + 1,这是最典型的性能雷区
如何设计点赞记录表避免重复和查询慢?
核心是两个约束:用户不能重复点同一个内容;查某条内容被谁点过要快;查某个用户点过哪些内容也要快。
表结构必须包含:user_id、post_id、created_at,并建立复合唯一索引:
CREATE TABLE post_likes ( user_id BIGINT NOT NULL, post_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, post_id), INDEX idx_post_id (post_id) );
注意点:
-
PRIMARY KEY (user_id, post_id)同时保证唯一性和按用户查的效率 - 单独建
INDEX idx_post_id (post_id)支持按帖子查点赞用户列表(如“显示已点赞的 3 个好友”) - 别用
AUTO_INCREMENT主键+额外唯一索引,多一次索引维护开销 - 如果需要取消点赞,直接
DELETE FROM post_likes WHERE user_id = ? AND post_id = ?,别用软删(is_deleted),否则索引失效、COUNT 变慢
实时计数用 COUNT(*) 还是缓存字段?
两者不是二选一,而是分层使用:缓存字段用于展示,COUNT(*) 用于校验和兜底。
具体做法:
- 每次点赞/取消,异步触发一个任务(如通过 MQ 或定时 Job),执行
SELECT COUNT(*) FROM post_likes WHERE post_id = ?,结果写回posts.likes_count - 前端读取时优先查
posts.likes_count,但接口可加X-Likes-Dirty: true响应头提示“此数值可能滞后”,供客户端决定是否触发拉取明细 - 缓存字段本身要带更新时间戳(
likes_count_updated_at),超过 30 秒未更新就自动 fallback 到COUNT(*)查询(防缓存长期不一致) - 别依赖 Redis 计数后定期回写 MySQL——Redis 挂了或网络分区会导致数据永久丢失,MySQL 必须是唯一事实源
INSERT IGNORE 和 REPLACE INTO 在点赞场景怎么选?
只用 INSERT IGNORE,永远不要用 REPLACE INTO。
原因很实在:
-
INSERT IGNORE遇到唯一冲突静默跳过,符合“点赞一次生效,重复点无效”的语义 -
REPLACE INTO是先DELETE再INSERT,会触发自增 ID 变化、外键级联、触发器重复执行,还可能误删历史记录(比如你加了updated_at字段想记录最后操作时间,它却给你重置成当前时间) - 如果需要更新时间戳(比如记录“最后点赞时间”),改用
INSERT ... ON DUPLICATE KEY UPDATE created_at = VALUES(created_at),明确控制哪些字段覆盖 - MySQL 8.0+ 可考虑
INSERT ... ON CONFLICT DO NOTHING(不支持),所以还是老实用INSERT IGNORE最稳
高频更新字段的本质不是“怎么快”,而是“怎么不拖垮其他读写”。所有优化都得从索引有效性、事务粒度、主从延迟容忍度出发,而不是幻想一条 SQL 解决一切。










