thinkphp 的 inc/dec 方法通过生成“set field = field ± n”原生sql实现数据库级原子更新,避免并发丢数据;而save()手动加减因先查后改导致竞态,update()配合exp虽可行但易错且冗长。

ThinkPHP 的 inc 和 dec 方法能直接生成 SQL 的 SET field = field + 1 语句,避免查-改-存三步操作,是真正意义上的原子更新,不是“模拟自增”。
为什么不能用 save() 手动加减?
手动先 select 出值、PHP 里加减、再 save(),在并发场景下必然丢数据。比如两个请求同时读到 score = 100,各自加 1 后都写回 101,实际应为 102。
使用 inc/dec 时,ThinkPHP 会跳过查询,直接拼出原生 SQL:UPDATE table SET score = score + 5 WHERE id = 123,由数据库保证原子性。
无序列表说明常见误用点:
- 把
$model->score += 1; $model->save();当作自增——这是 PHP 层累加,非数据库原子操作 - 用
update(['score' => ['exp', 'score + 1']])手写表达式——可行但冗长,且易错写成'score + 1'(字符串字面量) - 在事务外对高频计数字段(如阅读量)频繁
select再更新——性能差且不安全
inc() 和 dec() 的基本用法与参数细节
这两个方法本质是封装了 exp 表达式,但更语义化、更防错。
最简用法(+1/-1):
$user->where('id', 1)->inc('score')->update(); // score = score + 1
$user->where('id', 1)->dec('login_times')->update(); // login_times = login_times - 1
指定步长(支持小数和负数):
-
inc('score', 5)→score = score + 5 -
inc('price', -0.5)→price = price - 0.5(等价于dec('price', 0.5)) -
dec('stock', 99.9)→stock = stock - 99.9
注意:inc/dec 必须配合 where 条件调用,且最终要执行 update()(或 save(),但推荐 update() 更明确)。
复合操作:多个字段同时自增/自减
单次 SQL 更新多个字段,减少数据库往返。ThinkPHP 不支持链式多次 inc,需用数组传参:
$user->where('id', 1)
->inc(['score' => 10, 'exp' => 5])
->dec(['warning_count' => 1])
->update();
生成的 SQL 类似:
UPDATE user SET score = score + 10, exp = exp + 5, warning_count = warning_count - 1 WHERE id = 1
关键限制:
- 数组键必须是字段名,值是步长(支持整数、浮点数、负数)
- 不能混用
inc数组和链式inc('field'),后者会被覆盖 - 若需条件自增(如“仅当 score > 0 时才扣减”),不能依赖
inc/dec,得用原生 SQL 或update(['field' => ['exp', 'CASE WHEN ...']])
容易被忽略的边界情况
自增自减不是万能的,这几个点线上出过问题:
- 字段类型不匹配:对
VARCHAR字段调用inc('name', 1)不报错,但 MySQL 会隐式转为 0,结果变成0 + 1 = 1,原字符串丢失——务必确认字段是数值类型(INT/DECIMAL等) - 溢出不提示:
INT(11) UNSIGNED字段加到 4294967295 后再inc(1),MySQL 直接报错Out of range value,ThinkPHP 默认不捕获该异常,需手动 try/catch - NULL 值参与运算:若字段允许 NULL 且当前为 NULL,
score = score + 1结果仍是 NULL(MySQL 规则),不是 1。上线前检查默认值或用IFNULL(score, 0) + 1——这时就得退回到exp写法
真正需要原子更新时,别省那两行代码;但字段类型、取值范围、NULL 处理,这些得在模型层或数据库约束里提前卡死,光靠 inc 挡不住脏数据。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











