Laravel并发提交表单触发自增列数据重复,是否需采用悲观锁?
先理清楚你遇到的问题本质
你碰到的双浏览器同时提交出现重复值、代码层唯一校验失效,本质是读-改-写流程的非原子竞态问题,和你代码写得严谨与否没关系:
你之前写的唯一校验逻辑是「先查当前x的存储值→计算新值→判断新值是否重复→不重复就写入」,两个并发请求几乎同时进来时,都会查到x的当前值为0,都会计算出新值为1,都会判断“当前库中没有1、校验通过”,最后先后把1写进库,自然就出现重复。
这种问题靠应用层校验永远防不住,因为校验和写入两个动作之间不是原子完成的。
悲观锁是不是必选项?—— 不是,按业务场景选最低成本的方案就好
很多人碰到并发问题第一反应是上悲观锁,其实多数场景下根本不用上这么重的方案,按优先级选实现即可:
最高优先级:用数据库原生原子自增,从根源消除竞态
如果你的逻辑就是单纯给x列加1,完全没必要搞“先读值、算新值、再写入”的拆分流程,直接用Laravel封装的increment方法,底层生成的是update 表 set x = x +1 where id = 指定ID的原子SQL,数据库自己会处理并发写入,不管多少请求同时进来,每个请求拿到的自增值都是顺序递增的,绝对不会重复,连锁都不用加。
核心代码逻辑非常简单:
// 替代你原来的“读值→算值→存值”三步操作 YourModel::where('id', 目标行ID)->increment('x');
这个方案性能最好,没有并发等待,没有锁开销,是90%这类场景的最优解。
次选方案:必须基于当前x值做自定义计算时,再考虑锁
如果你的业务不是纯自增——比如x是带规则的流水号,需要读到当前值后补前缀、算校验位、做长度格式化,不能直接用数据库原生自增,这时候再按并发量选锁方案:
- 低并发场景:用Laravel封装的行级悲观排他锁
只要把整个读值、计算、写入的流程放在同一个数据库事务里,配合lockForUpdate()方法给目标行加排他锁,就能保证同一时间只有一个请求能拿到这行数据的修改权限,其他并发请求会在锁外排队,等前一个请求提交事务后再依次执行。
参考实现:
注意两个容易踩的坑:DB::transaction(function () { // 必须加上lockForUpdate(),否则锁完全不生效 $targetRow = YourModel::where('id', 目标行ID)->lockForUpdate()->first(); // 这里写你原来的自定义计算、业务校验逻辑 $newX = 你的自定义计算逻辑($targetRow->x); $targetRow->update(['x' => $newX]); });- 锁必须和事务绑定,单独写
lockForUpdate()不套事务是无效的 - 数据库引擎必须是InnoDB,MyISAM不支持行级锁
- 锁必须和事务绑定,单独写
- 高并发但实际写冲突少的场景:用乐观锁
如果并发量不小,但实际同时修改同一行数据的概率很低,用乐观锁的性能比悲观锁好很多,不会让请求排队。实现方式就是给表加一个version整数列,更新的时候带上之前读到的版本号,只有数据库里的版本号和之前读的一致,才允许更新,否则就返回提示让用户重试。
参考实现:$targetRow = YourModel::find(目标行ID); $newX = 你的自定义计算逻辑($targetRow->x); // 更新时带上原版本号作为校验条件 $affectedRows = DB::table('your_table') ->where('id', 目标行ID) ->where('version', $targetRow->version) ->update([ 'x' => $newX, 'version' => $targetRow->version + 1 ]); // 如果影响行数为0,说明有其他请求抢先修改了数据 if ($affectedRows === 0) { return back()->with('error', '操作频繁,请稍后重试'); }
关于你之前写的唯一校验的优化
你之前在代码里写的unique规则永远防不住并发,只要给x列加上数据库层面的唯一索引,根本不用写应用层的校验逻辑——两个并发请求写入重复值的时候,数据库会直接抛出唯一键冲突异常,你只要在捕获异常的逻辑里给用户返回友好提示就行。
很多人写校验的时候习惯在应用层查一遍,其实只要数据库有唯一索引,哪怕你应用层啥校验都不写,也绝对不会出现重复值入库的情况,应用层的校验只是为了提前拦截、提升用户体验,不能当安全网用。
几个容易踩的坑
- 不要把读值和写值拆成两个独立的流程,100%会出竞态问题
- 能靠数据库原子操作解决的问题,就不要靠应用层加锁,数据库的并发处理能力比应用层高一个数量级
- 如果你对实时性要求不高,可以把自增、写入的逻辑移到队列里串行执行,也能解决并发问题,但要额外维护队列的稳定性
内容的提问来源于stack exchange,提问作者Ilhamzac

