PHP&MySQL多更新操作竞态条件导致多张卡片设为默认的解决方案咨询
问题根源
现有代码的两次update操作是非原子执行的,并发场景下多个请求会先后完成「将当前用户所有卡片设为非默认」的操作,再各自把目标卡片更新为默认,最终就会出现多默认卡片的异常。
更简单的可行方案
方案1:合并两次更新为单条原子SQL(改动最小,无额外依赖)
这是成本最低的方案,无需额外组件、无需改表结构,仅需调整业务代码的SQL写法即可:
单条DML操作本身是数据库保证原子性的,你可以直接用IF语法将两次更新逻辑合并为一条SQL,从根源上消除并发问题:
use App\Models\Card; use Illuminate\Support\Facades\DB; public function setAsDefault(Request $request, $id) { $userId = $request->user()->id; // 单条SQL原子完成所有更新操作,参数绑定避免SQL注入 Card::where('user_id', $userId) ->update([ 'is_default' => DB::raw('IF(id = ?, 1, 0)', [$id]) ]); return ['status' => true]; }
执行后永远只会有一张卡片是默认状态,并发场景下最后执行的请求结果会作为最终状态,符合业务预期。
方案2:新增数据库部分唯一索引(可靠性最高,兜底防护)
该方案从数据库层面直接杜绝多默认卡片的可能,即使后续业务代码被误改也不会出现异常,几乎不需要改动业务逻辑:
- 如果你使用MySQL 8.0+、PostgreSQL等支持部分索引的数据库,直接创建限定条件的唯一索引即可:
CREATE UNIQUE INDEX udx_user_default_card ON cards(user_id) WHERE is_default = 1;
- 如果你使用低版本MySQL,可将
is_default字段的非默认值存储为NULL(唯一索引会自动忽略NULL值的重复校验),再创建普通联合唯一索引:
CREATE UNIQUE INDEX udx_user_default_card ON cards(user_id, is_default);
加完索引后即使并发请求到达数据库,也只会有一个请求更新成功,另一个会抛出唯一约束冲突异常,你只需要在全局异常处理里捕获该异常,返回操作频繁的友好提示即可。
方案3:用户维度加轻量分布式锁
如果不想改动SQL和表结构,用现有缓存组件(Redis/Memcached)针对用户ID加锁即可,实现非常简单,且锁粒度极细,不会影响其他用户的操作,以Laravel框架为例:
use App\Models\Card; use Illuminate\Support\Facades\Cache; public function setAsDefault(Request $request, $id) { $userId = $request->user()->id; // 给当前用户加5秒过期的操作锁,避免同用户并发执行设置默认逻辑 return Cache::lock("set_default_card:{$userId}", 5)->get(function () use ($userId, $id) { Card::where('user_id', $userId)->update(['is_default' => false]); Card::where([ 'id' => $id, 'user_id' => $userId ])->update(['is_default' => true]); return ['status' => true]; }); }
如果是其他框架,自己基于setnx实现简单的加锁逻辑也只有几行代码,性能优于数据库事务加锁方案。
内容的提问来源于stack exchange,提问作者Jacqueline
相关产品推荐
相关产品推荐

