它的本质是:一条看似简单的“自增”语句,在底层被转化为UPDATE posts SET likes = likes + ? WHERE id = ?。虽然它在 SQL 层面是原子的(不会读到脏数据),但在高并发场景下,它会导致严重的行锁等待 (Row Lock Wait)和索引页争用 (Index Page Contention)。对于热门帖子,这行代码就是导致数据库 CPU 飙升、响应延迟激增的元凶**。
如果把数据库比作银行柜台:
increment():是排队改账本。- 流程:顾客 A 走到柜台 -> 柜员锁定账本第 10 页(加行锁)-> 读取当前余额 100 -> 计算 100+1=101 -> 写入 101 -> 解锁。
- 并发问题:顾客 B、C、D… 同时来改第 10 页。他们必须串行排队。A 没办完,B 只能干等(Lock Wait)。
- 后果:队伍越来越长,柜台处理速度越来越慢,最后大堂经理(连接池)崩溃。
- Redis
INCR:是电子计数器。- 流程:顾客按一下按钮,数字自动+1。无需排队,微秒级完成。
- 优势:完全异步,无锁竞争。
- 核心逻辑:别让所有人都去抢同一本账本。把记账工作交给高速缓存,账本只在最后对一次总账。
一、SQL 本质:它到底做了什么?
1. 生成的 SQL
Laravel 的increment方法最终生成:
UPDATEpostsSETlikes=likes+1WHEREid=123;-- 如果 $count > 1UPDATEpostsSETlikes=likes+5WHEREid=123;2. 原子性保证 (Atomicity)
- 正确性:在 InnoDB 引擎中,这条语句是原子的。
- 它不是
SELECT likes->PHP计算->UPDATE。 - 它是直接在存储引擎层完成
读取+计算+写入。 - 结论:数据不会错(不会少加),这是它唯一的优点。
- 它不是
3. 锁机制 (Locking)
- 行锁 (Row Lock):InnoDB 会对
id=123这一行加X锁 (Exclusive Lock)。 - 持续时间:直到事务提交。
- 影响:其他任何试图修改或锁定该行的事务(包括另一个
increment)都必须等待。
💡 核心洞察:
increment保证了数据的“正确性”,但牺牲了系统的“并发性”。在低并发下没问题,在高并发下是灾难。
二、并发危害:为什么它是热点杀手?
1. 行锁等待 (Lock Wait)
- 场景:爆款文章,每秒 1000 人点赞。
- 现象:
- Thread 1 获得锁,执行 UPDATE (耗时 1ms)。
- Thread 2-1000 进入Lock Wait Queue。
- Thread 2 等待 1ms,Thread 1000 等待 1000ms (1秒)。
- 后果:接口响应时间线性增长,用户感觉“卡死”。
2. 上下文切换开销 (Context Switch Overhead)
- 机制:MySQL 线程不断在“运行”和“等待锁”之间切换。
- 后果:CPU 大量时间花在调度线程上,而非执行 SQL 上。
sys态 CPU 使用率飙升。
3. 索引页争用 (Index Page Latch Contention)
- 机制:
id是主键,聚簇索引。频繁更新同一行,会导致该索引页在 Buffer Pool 中被频繁读写。 - 后果:即使没有行锁等待,内存层面的Latch (闩锁)竞争也会限制吞吐量。
4. Binlog 压力
- 机制:每次
UPDATE都会生成 Binlog 日志。 - 后果:高频小事务导致 Binlog 文件迅速膨胀,主从同步延迟增加。
三、性能优化:如果必须用 DB,怎么救?
如果你不能引入 Redis,必须在 MySQL 层面优化:
1. 批量合并 (Batching)
- 策略:不要在每个请求中都调用
increment。 - 实现:
- 在 PHP 内存中累计计数。
- 每隔 1 秒或每满 100 次,执行一次
DB::...->increment('likes', 100)。
- 效果:将 100 次行锁竞争合并为 1 次。
- 风险:服务重启会丢失未刷新的计数。
2. 减少事务范围
- 策略:确保
increment在一个极短的事务中执行,尽快提交。 - 代码:
DB::transaction(function()use($postId){DB::table('posts')->where('id',$postId)->increment('likes');// 不要在这里做其他耗时操作!});
3. 乐观锁重试 (Optimistic Locking Retry) -不推荐用于计数
- 说明:乐观锁适合状态变更,不适合高频计数,因为冲突率太高,重试会导致更严重的 CPU 浪费。
四、替代方案:架构级解法
方案 A:Redis INCR + 异步落库 (最佳实践)
- 流程:
- 写:
Redis::incr("post:{$id}:likes")。原子操作,无锁,微秒级。 - 读:直接读 Redis 获取点赞数。
- 同步:
- 定时任务:每分钟将 Redis 计数同步到 MySQL。
- 消息队列:每次
INCR发送 MQ,消费者批量更新 MySQL。
- 写:
- 优势:彻底解除数据库行锁瓶颈,支撑万级 QPS。
- 一致性:最终一致性。用户看到的可能比实际多/少几秒,但可接受。
方案 B:MySQL 延迟更新 (Write-Behind)
- 流程:
- PHP 接收请求,将
(post_id, user_id)放入本地内存数组或 APCu。 - 当数组达到阈值(如 50 个),一次性执行
UPDATE ... SET likes = likes + 50。
- PHP 接收请求,将
- 优势:减少 DB 交互次数。
- 劣势:单机部署有效,集群部署复杂。
方案 C:分表/分库 (Sharding)
- 流程:将点赞记录分散到多个表中。
- 劣势:架构复杂度极高,对于单纯的计数场景,杀鸡用牛刀。
🚀 总结:原子化“DB Increment”全景图
| 维度 | 关键点 |
|---|---|
| 本质 | 基于行锁的原子更新,高并发下的性能瓶颈 |
| SQL 行为 | UPDATE table SET col = col + 1 WHERE id = ? |
| 主要危害 | 行锁等待、上下文切换、Binlog 膨胀 |
| 适用场景 | 低频更新、非热点数据、强一致性要求极高 |
| 禁忌场景 | 爆款文章点赞、秒杀库存扣减、高频计数器 |
| 最佳替代 | Redis INCR + 异步持久化 |
| PHP 隐喻 | Mutex Lock on Database Row |
| 公式 | Throughput = 1 / (Lock_Wait_Time + Execution_Time) |
终极心法:
DB Increment 的本质,是“用串行化换取正确性”。
在低并发时,它是安全的捷径;在高并发时,它是致命的堵塞点。
别让数据库承担它不该承担的计数压力。
于原子中见安全,于锁中见瓶颈;以架构为尺,解单点之牛,于高并发工程中,求吞吐之真。
行动指令:
- 审查代码:找出项目中所有的
increment调用。 - 评估频率:哪些是热点数据(如文章点赞、视频播放量)?
- 重构热点:将热点计数迁移到 Redis
INCR。 - 保留冷点:低频数据(如文章评论数、后台统计)可以保留 DB
increment,简化架构。 - 思维升级:记住,数据库擅长存数据和复杂查询,但不擅长高频简单计数。把计数交给 Redis,把存储交给 MySQL。