ThinkPHP6 乐观锁需手动实现,核心是读取version、UPDATE带id和version双条件、检查影响行数为1;不可用save()或setDec()等绕过校验的方式,推荐封装带重试的safeDecreaseStock方法。

ThinkPHP6 本身不提供开箱即用的乐观锁插件(如 MyBatis-Plus 的 OptimisticLockerInnerInterceptor),它的乐观锁必须由开发者在 SQL 层面**显式构造、严格校验、主动重试**。这不是一个配置开关,而是一套需闭环落地的原子操作逻辑。
核心三要素缺一不可
任何一次乐观更新要真正生效,必须同时满足:
-
读取时拿到当前 version 值:查询必须包含
version字段,不能用find()忽略它,也不能用SELECT *后未映射该字段 -
UPDATE 语句带双校验条件:WHERE 中必须有
id = ? AND version = ?,SET 中必须含version = version + 1 -
执行后立刻判断影响行数:
$res为0表示冲突失败,不是“没改”,而是“被别人抢先改了”——这是重试的唯一可靠信号
手动实现带重试的扣减示例(推荐原生 Db::raw)
以商品库存扣减为例,要求:扣 1 件,且仅当库存 ≥ 1 且版本未变时才成功:
// 最大重试次数
$maxRetries = 3;
for ($i = 0; $i where('id', $goodsId)->find();
if (!$goods) {
throw new Exception('商品不存在');
}
if ($goods['stock'] // 2. 构造带版本校验的原子更新
$res = Db::name('goods')
->where('id', $goodsId)
->where('version', $goods['version']) // 关键:校验旧版本
->update([
'stock' => Db::raw('stock - 1'),
'version' => Db::raw('version + 1'), // 关键:自增版本
'updated_at'=> time()
]);
// 3. 检查是否成功
if ($res === 1) {
return ['success' => true, 'new_stock' => $goods['stock'] - 1];
}
// 4. 失败:说明 version 已被其他请求更新,等待后重试
if ($i <p>}
throw new Exception('并发更新冲突,重试 ' . $maxRetries . ' 次后仍失败');
</p>
为什么不能用模型 save() 或 where+save?
以下写法看似简洁,但存在严重并发漏洞:
-
$model->where('id', $id)->save(['stock' => $newStock]):没校验 version,纯覆盖写入 -
$model->where('id', $id)->setDec('stock', 1):不支持 WHERE 中嵌入 version 条件,无法防止覆盖 - 先
find()再计算再save():中间窗口期可能被其他请求修改,属于“伪乐观锁”
这些方式都绕过了 version 校验本质,等于没加锁。
进阶建议:封装成可复用方法
可在模型中添加一个安全扣减方法,把重试逻辑内聚:
public function safeDecreaseStock($id, $num = 1, $maxRetries = 3)
{
for ($i = 0; $i field('id, stock, version')->find($id);
if (!$row || $row['stock'] $res = $this->where('id', $id)
->where('version', $row['version'])
->update([
'stock' => Db::raw("stock - {$num}"),
'version' => Db::raw('version + 1')
]);
if ($res === 1) {
return true;
}
if ($i === $maxRetries - 1) break;
usleep(10000);
}
return false;
}
调用时只需 $goodsModel->safeDecreaseStock(123),业务层无需感知重试细节。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











