事务中统计请求量易丢数据,因count操作常为“先查后增”,高并发下发生读-改-写竞争;应改用increment()原子操作或显式加锁(lock(true)),并确保表结构与事务使用一致。

事务中统计请求量为什么容易丢数据
因为 count 类操作常被写成「先查后增」,比如 select count(*) from request_log where date = '2024-06-01' 再 insert 或 update。在高并发下,两个请求几乎同时执行,查到的值一样,最终只加 1 次——本质是读-改-写(RMW)没加锁或没原子化。
ThinkPHP 默认的 Db::transaction() 不会自动给 count() 加行锁,除非你显式用 lock(true) 或改用 increment() 这类原子操作。
常见错误现象:RequestLog::where('date', '2024-06-01')->count() 在事务里反复调用,结果统计总数比实际请求少;或者用 update(['hits' => $hits + 1]) 但没带 where id = ? 条件,导致覆盖写。
用 Db::raw() + increment() 实现原子计数
最稳的方式是绕过「查+算+写」流程,直接让数据库完成自增。ThinkPHP 的 increment() 底层生成 UPDATE ... SET hits = hits + 1,天然具备原子性,且能和事务共存。
- 必须确保目标记录已存在,否则
increment()不生效(返回 0),需配合exists()或先insert()再increment() - 如果按小时/分钟聚合,建议用
date_format(create_time, '%Y-%m-%d %H:00:00')作分组键,避免日期字符串拼接出错 - 不要在事务里混合使用
Db::table()->count()和->increment(),前者不触发行锁,后者才真正锁定对应行
示例:
Db::transaction(function () {
$key = date('Y-m-d H:00:00');
$affected = Db::name('request_stat')
->where('stat_key', $key)
->increment('count', 1);
if (!$affected) {
Db::name('request_stat')->insert([
'stat_key' => $key,
'count' => 1,
'create_time' => date('Y-m-d H:i:s'),
]);
}
});
需要行锁时务必用 lock(true) + forUpdate
某些场景必须先读原始值再决定逻辑(比如限流判断、配额扣减),这时不能跳过查询,但得确保查到的行被锁定,防止其他事务并发修改。ThinkPHP 中 lock(true) 等价于 SELECT ... FOR UPDATE。
-
lock(true)必须跟在where()后面,且where字段要有索引,否则会锁表 - 不能在事务外调用
lock(true),否则抛出异常或静默失效 - 避免在锁住的记录上做耗时操作(如 HTTP 请求、文件读写),否则拖长锁持有时间,引发阻塞
示例(带锁读取并更新):
Db::transaction(function () {
$record = Db::name('request_quota')
->where('user_id', 1001)
->lock(true) // 关键:加上这行
->find();
if ($record && $record['remaining'] > 0) {
Db::name('request_quota')
->where('id', $record['id'])
->update(['remaining' => $record['remaining'] - 1]);
}
});
日志表设计要适配事务与查询性能
请求统计不是纯分析型场景,而是写多读少、强一致性要求高的业务数据。别一上来就建宽表或 JSON 字段,容易拖慢事务提交速度。
- 主键建议用
stat_key(如'2024-06-01_14')而非自增 ID,方便唯一约束和快速定位 - 高频更新字段(如
count)不要放在大文本字段旁边,避免行迁移影响性能 - 如果用 MySQL,
engine=InnoDB是必须的;如果用 PostgreSQL,注意SELECT FOR UPDATE在 READ COMMITTED 隔离级下仍可能幻读,需配合WHERE范围锁
一个最小可用结构:
CREATE TABLE `request_stat` ( `stat_key` char(13) NOT NULL COMMENT '格式:YYYY-MM-DD_HH', `count` int unsigned NOT NULL DEFAULT '0', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`stat_key`) ) ENGINE=InnoDB;
真正容易被忽略的是:事务里混用模型(RequestStat::)和原生 Db 类,可能导致事务上下文丢失;还有就是把 Db::transaction() 套在中间件里却忘了捕获异常,导致事务不回滚。这些细节比语法更重要。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











