Python 如何使用 MySQL 的事务
后端开发中,转账、订单创建、库存扣减这类业务,必须保证多条数据库操作全部成功或者全部回滚,这就是事务的价值。本文基于 pymysql 讲解 Python 下 MySQL 事务完整用法,包含原理、代码示例、异常处理、常见坑与最佳实践。
环境准备
安装 pymysql
pipinstallpymysql注意:MySQL 只有 InnoDB 引擎支持事务,MyISAM 不支持事务。
测试表准备
CREATETABLE`account`(`id`INTPRIMARYKEYAUTO_INCREMENT,`username`VARCHAR(32)NOTNULL,`balance`DECIMAL(12,2)NOTNULLDEFAULT0)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;INSERTINTOaccount(username,balance)VALUES('zhangsan',1000.00),('lisi',1000.00);什么是事务
事务是一组SQL操作的逻辑单元:
- 全部执行成功,执行
commit()提交,修改永久生效; - 任意步骤失败,执行
rollback()回滚,撤销这一组所有修改。
ACID四大特性
- 原子性(Atomicity):事务内操作不可分割,全部成功或全部失败。
- 一致性(Consistency):事务执行前后业务数据保持合法状态。
- 隔离性(Isolation):多个事务之间互相隔离,受事务隔离级别控制。
- 持久性(Durability):commit提交之后,修改永久保存,数据库宕机也不会丢失。
pymysql事务核心要点
- pymysql 默认
autocommit=True,每条SQL执行后立即提交,此时没有事务效果。 - 使用事务必须关闭自动提交:
conn.autocommit(False)。 - 两个关键方法:
conn.commit():提交事务conn.rollback():回滚事务
- 同一个事务必须使用同一个connection连接对象,不能换连接。
示例1:转账业务(try‑except标准写法)
张三转账200元给李四,模拟业务异常回滚。
importpymysqldeftransfer():conn=pymysql.connect(host="127.0.0.1",port=3306,user="root",password="xxx",database="test_db",charset="utf8mb4")# 关闭自动提交,开启事务模式conn.autocommit(False)cursor=conn.cursor(pymysql.cursors.DictCursor)try:# 1. 张三扣200cursor.execute("UPDATE account SET balance = balance - %s WHERE username = %s",(200,"zhangsan"))# 2. 模拟异常,触发回滚# 1 / 0# 3. 李四加200cursor.execute("UPDATE account SET balance = balance + %s WHERE username = %s",(200,"lisi"))# 全部成功,提交事务conn.commit()print("事务提交成功")exceptExceptionase:# 出现任何异常,回滚所有变更conn.rollback()print(f"事务回滚,异常:{e}")finally:cursor.close()conn.close()if__name__=="__main__":transfer()打开代码中
1/0模拟报错,你会发现两条update全部失效,不会出现张三扣钱、李四没加钱的数据错乱。
示例2:with上下文管理器用法
pymysql 的 connection 支持 with,退出上下文如果没有commit会自动回滚。
importpymysqldeftransfer_with():conn=pymysql.connect(host="127.0.0.1",user="root",password="xxx",database="test_db",autocommit=False,charset="utf8mb4")try:withconn.cursor(pymysql.cursors.DictCursor)ascur:cur.execute("UPDATE account SET balance=balance-%s WHERE username=%s",(100,"zhangsan"))cur.execute("UPDATE account SET balance=balance+%s WHERE username=%s",(100,"lisi"))# with游标结束不会自动commit,需要手动提交conn.commit()print("提交成功")exceptExceptionase:conn.rollback()print(f"回滚:{e}")finally:conn.close()⚠️重要提醒:
with cursor()只是管理游标,不会自动commit/rollback,事务的提交回滚仍然由connection控制。
示例3:嵌套事务?MySQL没有真正嵌套事务
MySQL InnoDB 不支持真正嵌套事务,可以使用保存点 savepoint实现局部回滚。
defsavepoint_demo():conn=pymysql.connect(host="127.0.0.1",user="root",password="xxx",database="test_db",autocommit=False)cur=conn.cursor()try:cur.execute("UPDATE account SET balance=balance-50 WHERE username='zhangsan'")# 设置保存点cur.execute("SAVEPOINT sp1")cur.execute("UPDATE account SET balance=balance+50 WHERE username='lisi'")# 回滚到保存点sp1,只撤销后面的操作,前面的保留cur.execute("ROLLBACK TO SAVEPOINT sp1")conn.commit()exceptExceptionase:conn.rollback()print(e)finally:cur.close()conn.close()常见踩坑清单
autocommit忘记关闭
autocommit=True时每一条SQL直接生效,commit/rollback完全无效,事务失效。事务中途更换connection对象
同一个事务的多条SQL必须在同一个连接,换连接等于新开另一个事务。查询不会加锁,update/delete才会产生事务变更
普通select不会修改数据;SELECT ... FOR UPDATE会开启行锁,用于并发扣库存场景。异常捕获后忘记rollback
如果发生异常不执行rollback,未提交的事务会一直挂起,占用数据库锁资源,产生锁等待。MyISAM引擎使用事务
MyISAM不支持事务,commit/rollback调用无效果,建表必须指定ENGINE=InnoDB。长事务
事务不要长时间不commit/rollback,长事务会大量占用回滚段、锁资源,严重影响数据库性能。
结合SELECT FOR UPDATE(并发扣库存场景)
悲观锁示例,防止并发超卖
defdeduct_stock():conn=pymysql.connect(host="127.0.0.1",user="root",password="xxx",database="test_db",autocommit=False)cur=conn.cursor()try:# for update 行锁,其他事务会阻塞此处cur.execute("SELECT balance FROM account WHERE username='zhangsan' FOR UPDATE")row=cur.fetchone()ifrow[0]>=100:cur.execute("UPDATE account SET balance=balance-100 WHERE username='zhangsan'")conn.commit()exceptExceptionase:conn.rollback()print(e)finally:cur.close()conn.close()最佳实践总结
- 业务涉及多写操作,必须使用事务;设置
autocommit=False。 - 使用
try‑except‑finally,异常分支必须执行rollback(),最后关闭连接。 - 事务粒度尽量小,避免长事务,执行完尽快commit或rollback释放锁。
- 并发场景需要锁时,合理使用
SELECT ... FOR UPDATE悲观锁,或业务层乐观锁。 - 确认表引擎为InnoDB。
- 一个事务全程复用同一个数据库连接对象。
如果你使用 SQLAlchemy ORM,框架会封装事务逻辑,但底层依然是MySQL事务机制。
