NestJS中MySQL SELECT...FOR UPDATE行锁未生效问题排查
问题根因
行锁不生效的核心原因是:你手动创建queryRunner启动的事务,和Repository层执行FOR UPDATE查询的数据库连接不是同一个。
TypeORM中调用getConnection().createQueryRunner()会从连接池申请一个独立的数据库连接,在这个连接上手动启动事务后,所有需要加入事务的SQL必须显式通过该queryRunner实例执行。而你在Repository中直接调用this.query()时,会从连接池申请另一个独立连接执行SQL,这个连接默认开启自动提交:SELECT ... FOR UPDATE执行完成后连接会自动提交,行锁被立刻释放,根本不会等到你调用queryRunner.commitTransaction()才释放,自然无法实现跨服务的互斥锁效果。
修复方案
核心原则:事务内所有SQL操作必须使用启动事务的同一个queryRunner连接执行,具体修改如下:
- Service层启动事务后,将
queryRunner实例传递给Repository层的所有操作方法 - Repository层所有SQL(包括加锁查询、状态更新)都通过传入的
queryRunner执行 - 补充异常回滚、连接释放逻辑,避免事务悬挂、连接泄漏
修改后的代码示例:
// update-crawler.service.ts async updateState() { const queryRunner = getConnection().createQueryRunner(); await queryRunner.startTransaction(); try { // 传入queryRunner,确保加锁查询走事务连接 const updateStateJobs = await this.bridgeTransactionStateRepository.getUpdateCrawlingJob(queryRunner); for (const updateStateJob of updateStateJobs) { // 更新操作同样走同一个事务连接 await this.bridgeTransactionStateRepository.updateState(txId, nextState, queryRunner); } await queryRunner.commitTransaction(); } catch (err) { // 异常时回滚事务 await queryRunner.rollbackTransaction(); throw err; } finally { // 释放连接回连接池 await queryRunner.release(); } }
// bridge-transaction-state.repository.ts import { QueryRunner } from 'typeorm'; async getUpdateCrawlingJob(queryRunner: QueryRunner): Promise<UpdateStateJobVo[]> { // 通过传入的queryRunner执行加锁查询,SQL在事务连接上运行 const updateStateJobList: UpdateStateJobVo[] = await queryRunner.query( `SELECT * FROM bridge_transaction_state as bridge WHERE bridge.state IN ('TRANSFER_SUCCESS') FOR UPDATE;`, ); return updateStateJobList; } async updateState(txId: string, nextState: string, queryRunner: QueryRunner) { // 更新操作使用同一个queryRunner执行 await queryRunner.query( `UPDATE bridge_transaction_state SET state = ? WHERE tx_id = ?`, [nextState, txId] ); }
额外注意事项
- 必须给
bridge_transaction_state表的state字段创建索引,否则WHERE state IN ('TRANSFER_SUCCESS')会触发全表扫描,FOR UPDATE会锁住整张表的所有记录,而非只锁定符合条件的行,会严重影响并发性能 - 表引擎必须使用InnoDB,MyISAM引擎不支持事务和行级锁,
FOR UPDATE不会生效 - 事务隔离级别使用InnoDB默认的
REPEATABLE READ即可,无需额外调整 - 不要在持有锁的事务内执行RPC调用、耗时计算等慢操作,会拉长锁持有时间,降低系统吞吐量
内容的提问来源于stack exchange,提问作者김도영 Dara
相关产品推荐
相关产品推荐

