不使用自增列时MySQL并发插入唯一ID重复解决方案
你写的代码里\DB::raw('LOCK TABLES leads WRITE')和\DB::raw('UNLOCK TABLES')根本没有实际执行。Laravel的DB::raw()方法仅用于生成原生SQL表达式片段,不会向数据库发送请求执行SQL,等于加锁、解锁逻辑完全没生效。
默认数据库事务隔离级别下,多个并发请求会同时查到同一个max(id)值,最终插入重复id。另外MySQL的LOCK TABLES语句本身会隐式提交当前活跃事务,你代码里先开事务再执行锁表的顺序本身也是错的,就算真的执行了锁表SQL也会破坏事务逻辑。
方案1:修正表锁逻辑(改动最小)
调整锁表语句的执行方式和顺序,不要用DB::raw()跑锁表SQL,改用DB::statement()实际执行SQL,同时调整锁和事务的顺序:
<?php for($i=1; $i<1000; $i++){ // 先执行写锁,阻塞其他会话对leads表的读写 \DB::statement('LOCK TABLES leads WRITE'); $max = (int)\DB::table('leads')->max('id'); $newId = str_pad(++$max, 10, '0', STR_PAD_RIGHT); \DB::table('leads')->insert(['id' => $newId]); // 操作完成后解锁,自动释放锁给其他排队请求 \DB::statement('UNLOCK TABLES'); }
这个方案改动最小,但是写锁会锁整张leads表,并发高的时候吞吐量会比较低。
方案2:用序列表+行级悲观锁(性能更好)
单独建一张只存id序列值的小表,用行级锁代替表锁,并发性能高很多:
- 先建序列表,初始化一条记录存当前最大id:
CREATE TABLE leads_seq ( current_max_id INT UNSIGNED NOT NULL DEFAULT 0 ); INSERT INTO leads_seq (current_max_id) SELECT IFNULL(MAX(id),0) FROM leads; - 业务代码里用
lockForUpdate()加行级排他锁,保证同一时间只有一个请求能读取、修改序列值:<?php for($i=1; $i<1000; $i++){ \DB::beginTransaction(); // 排他锁锁定序列记录,其他请求会在这里阻塞等待锁释放 $seq = \DB::table('leads_seq')->lockForUpdate()->first(); $newIdValue = $seq->current_max_id + 1; // 更新序列值 \DB::table('leads_seq')->update(['current_max_id' => $newIdValue]); // 插入业务表 \DB::table('leads')->insert([ 'id' => str_pad($newIdValue, 10, '0', STR_PAD_RIGHT) ]); \DB::commit(); }
这个方案锁的粒度是序列表的单行记录,不会阻塞leads表的其他查询操作,并发表现远好于锁全表。
兜底必做项
不管用上面哪种方案,都要给leads表的id字段加唯一索引,从数据库层面彻底挡住重复id写入,就算代码逻辑出极端异常,也不会出现重复id的脏数据。如果插入时触发唯一索引冲突,捕获异常后重试1-2次即可。
注意:Laravel中所有需要实际执行的原生SQL,都不能直接靠
DB::raw()触发,必须调用statement()/select()/insert()/update()这类会实际发起数据库连接请求的方法,DB::raw()本身只负责拼接SQL片段,不会执行任何数据库操作。
内容的提问来源于stack exchange,提问作者slsvmvnrdtekomojny

