UPDATE点赞计数行卡表主因是锁竞争:若id非主键/唯一索引,可能触发间隙锁;即使走主键,高并发下单行锁仍成热点;分片计数+Redis缓存才是高效解法。

为什么 UPDATE 一条点赞计数行会卡住整个表?
很多人以为 UPDATE t SET likes = likes + 1 WHERE id = 123 只锁那行,但实际是否真能避免排队,取决于索引结构和事务隔离级别。InnoDB 行级锁生效的前提是:WHERE 条件必须命中**唯一索引或主键**。如果 id 是主键,那确实只锁该行;但如果用的是普通索引(比如 post_id),又没加 FOR UPDATE 或事务未提交,可能触发间隙锁或锁升级,导致其他并发更新被阻塞。
更隐蔽的问题是:高并发下大量短事务争抢同一行,即使锁粒度正确,也会因锁队列排队、CPU 上下文切换、日志刷盘 I/O 等造成明显延迟——这不是锁错了,是锁太“热”了。
- 确认
SELECT ... FOR UPDATE或UPDATE是否走主键/唯一索引(用EXPLAIN看key字段) - 避免在长事务中更新计数器,否则锁持有时间被拉长
- 检查
innodb_lock_wait_timeout是否过小(默认 50 秒),导致业务误报超时而非真卡死
把单行计数拆成多行,真能扛住每秒几千点赞?
是的,但不是随便拆。核心思路是用“分片计数器”把热点分散:比如按 post_id % 16 将一个帖子的点赞数存到 16 行,每次随机选一行更新,查总数时 SUM() 聚合。这能显著降低单行锁冲突概率,实测 QPS 从几百提升到三四千没问题。
关键不在“拆”,而在“怎么拆不引入新问题”:
- 分片数别太少(如 2 或 4),否则仍热点;也别太多(如 1024),增加聚合开销和存储碎片
- 必须用
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO保证幂等,防止重复插入脏数据 - 读总数不能只靠
SUM(likes)实时查——要加缓存(如 Redis),否则每次读都扫多行,反而拖垮性能 - 分片字段(如
post_id % N)必须参与联合唯一索引,否则ON DUPLICATE KEY失效
INSERT ... ON DUPLICATE KEY UPDATE 比 UPDATE 更适合点赞场景?
对,尤其在分片计数器模型下。UPDATE 要求行必须存在,而点赞是高频写入、低频初始化的场景——第一次点赞时得先 INSERT,再 UPDATE,中间有竞态风险;INSERT ... ON DUPLICATE KEY UPDATE 原子完成“有则增、无则建”,省去 SELECT 判断,减少一次锁竞争。
解析 MySQL 8.0 的 DDL 脚本,并将表结构导出为 Excel 格式。适用于用户需将 MySQL 的 `CREATE TABLE` 语句转换为结构化的 Excel 文件场景,导出内容包含字段名、数据类型、长度及精度等信息。支持多张表的批量处理,每张表对应 Excel 中的一个独立工作表。适用于数据库文档编写、数据字典生成及模式分析等任务。
但要注意它的限制:
- 必须有
UNIQUE KEY或PRIMARY KEY触发“duplicate”逻辑,否则退化为普通INSERT - 执行时会对匹配到的唯一索引值加
INSERT intention lock,若并发插入相同 key,仍会排队——所以分片设计必须让 key 尽量分散 - 不要在
ON DUPLICATE KEY UPDATE里调用函数(如NOW()),MySQL 8.0+ 对部分函数有严格限制,容易报错ERROR 1690 (HY000): DOUBLE value is out of range
为什么最终还是得上 Redis,而不是硬刚 MySQL?
因为 InnoDB 再怎么优化锁和分片,本质仍是磁盘持久化系统,写入路径长(Buffer Pool → Redo Log → Doublewrite → Data File)。点赞这种纯累加、允许短暂不一致的场景,直接压给 MySQL,等于让数据库承担缓存职责。
合理分工是:Redis 做实时计数(INCR / INCRBY),MySQL 做落地校准(定时或按批次同步)。这样既保性能,又不丢数据:
- Redis 单节点轻松扛 10w+ QPS,集群可线性扩展
- 用
INCR不用事务,天然无锁;配合EXPIRE防雪崩,用PERSIST防误删 - MySQL 同步时机很关键:别每点一次就写一次,改用定时任务(如每 5 秒)批量
INSERT ... ON DUPLICATE KEY UPDATE合并写入 - 必须加校验机制(比如 Redis 和 MySQL 的差值超过阈值就告警),否则网络分区时可能漏同步
最易被忽略的一点:Redis 的 INCR 是原子的,但“先读再判断再写”的三步操作不是——所有业务侧的 if-else 逻辑都要挪到 Lua 脚本里执行,否则并发下仍可能超限。

















