InnoDB+Python下并发事务互无视锁引发死锁问题求助
嘿,这个问题我之前帮团队从SQL Server迁移到MySQL时踩过一模一样的坑!先给你拆解问题根源,再给你落地的解决方案。
为什么会出现并发读同一行的情况?
首先得明确MySQL和SQL Server在Serializable隔离级+行锁上的核心差异:
- SQL Server的Serializable会自动用键范围锁阻止幻读,且事务内的查询默认是当前读(会直接加锁);
- 但MySQL的InnoDB引擎下,即使开了Serializable隔离级,普通SELECT还是快照读(读事务启动时的快照,不会加锁),只有用
SELECT ... FOR UPDATE/SELECT ... LOCK IN SHARE MODE才会触发当前读并加行锁。
你已经用了SELECT ... FOR UPDATE还出问题,大概率是踩了这几个常见坑:
- 自动提交没关:MySQL默认
autocommit=ON,如果没显式开启事务,SELECT ... FOR UPDATE的锁会在语句执行完立刻释放,另一个事务马上就能读到同一行; - 查询条件没用到主键/唯一索引:如果
SELECT ... FOR UPDATE的WHERE条件是普通无索引列,InnoDB会做全表扫描并锁所有行,反而可能出现锁等待异常或逻辑错误; - 隔离级没真正生效:有可能代码没正确设置Serializable隔离级,比如在事务启动后才设置,或者连接池配置覆盖了隔离级。
解决步骤(附Python代码示例)
1. 显式开启事务+关闭自动提交
不管用原生驱动还是ORM,都要确保在Serializable隔离级下显式启动事务,关闭自动提交:
原生pymysql示例:
import pymysql def safe_update_row(row_id, new_value): # 建立连接时关闭自动提交,后续显式控制事务 conn = pymysql.connect( host="your_host", user="your_user", password="your_pass", db="your_db", autocommit=False ) try: # 启动Serializable隔离级的事务 conn.start_transaction(isolation_level='SERIALIZABLE') cursor = conn.cursor() # 用主键+FOR UPDATE精准锁行(必须用主键/唯一索引!) cursor.execute("SELECT * FROM your_table WHERE id = %s FOR UPDATE", (row_id,)) target_row = cursor.fetchone() if not target_row: print("目标行不存在,回滚事务") conn.rollback() return None # 执行更新操作 cursor.execute("UPDATE your_table SET your_column = %s WHERE id = %s", (new_value, row_id)) conn.commit() print("更新成功") return target_row except Exception as e: print(f"操作出错:{str(e)}") conn.rollback() raise e finally: cursor.close() conn.close()
SQLAlchemy ORM示例:
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from your_models import YourTable # 导入你的数据表模型类 # 连接时直接指定Serializable隔离级 engine = create_engine( "mysql+pymysql://user:pass@host/db", isolation_level="SERIALIZABLE", pool_pre_ping=True ) # 关闭自动提交和自动刷新,手动控制事务 Session = sessionmaker(bind=engine, autocommit=False, autoflush=False) def orm_safe_update(row_id, new_value): session = Session() try: # 用with_for_update()触发行锁,必须基于主键/唯一索引查询 target_row = session.query(YourTable).filter(YourTable.id == row_id).with_for_update().first() if not target_row: session.rollback() return None target_row.your_column = new_value session.commit() return target_row except Exception as e: session.rollback() raise e finally: session.close()
2. 验证锁是否生效的小技巧
你可以开两个终端同时运行测试代码:
- 第一个事务执行到
SELECT ... FOR UPDATE后暂停(比如加个input()),不要提交; - 第二个事务执行同样的
SELECT ... FOR UPDATE,此时会被阻塞,直到第一个事务提交/回滚,这就说明锁已经生效了。
额外注意事项
- 锁行后别做耗时操作:行锁会保持到事务提交/回滚,所以锁行后要尽快完成更新,避免长时间占用锁导致并发性能下降;
- 尽量用主键/唯一索引锁行:如果必须用非索引列做查询条件,InnoDB会锁全表,并发性能会大幅降低,建议给查询条件添加索引。
内容的提问来源于stack exchange,提问作者Marcus Cemes
相关产品推荐
相关产品推荐

