后端开发中,转账、订单创建、库存扣减这类业务,必须保证多条数据库操作全部成功或者全部回滚,这就是事务的价值。本文基于 pymysql 讲解 Python 下 MySQL 事务完整用法,包含原理、代码示例、异常处理、常见坑与最佳实践。
环境准备
安装 pymysql
bash
pip install pymysql
注意:MySQL 只有 InnoDB 引擎支持事务,MyISAM 不支持事务。
测试表准备
sql
CREATE TABLE `account` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(32) NOT NULL,
`balance` DECIMAL(12,2) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO account(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元给李四,模拟业务异常回滚。
python
import pymysql
def transfer():
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. 张三扣200
cursor.execute("UPDATE account SET balance = balance - %s WHERE username = %s", (200, "zhangsan"))
# 2. 模拟异常,触发回滚
# 1 / 0
# 3. 李四加200
cursor.execute("UPDATE account SET balance = balance + %s WHERE username = %s", (200, "lisi"))
# 全部成功,提交事务
conn.commit()
print("事务提交成功")
except Exception as e:
# 出现任何异常,回滚所有变更
conn.rollback()
print(f"事务回滚,异常:{e}")
finally:
cursor.close()
conn.close()
if __name__ == "__main__":
transfer()
打开代码中
1/0模拟报错,你会发现两条update全部失效,不会出现张三扣钱、李四没加钱的数据错乱。
示例2:with上下文管理器用法
pymysql 的 connection 支持 with,退出上下文如果没有commit会自动回滚。
python
import pymysql
def transfer_with():
conn = pymysql.connect(
host="127.0.0.1",
user="root",
password="xxx",
database="test_db",
autocommit=False,
charset="utf8mb4"
)
try:
with conn.cursor(pymysql.cursors.DictCursor) as cur:
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("提交成功")
except Exception as e:
conn.rollback()
print(f"回滚:{e}")
finally:
conn.close()
⚠️重要提醒:
with cursor()只是管理游标,不会自动commit/rollback,事务的提交回滚仍然由connection控制。
示例3:嵌套事务?MySQL没有真正嵌套事务
MySQL InnoDB 不支持真正嵌套事务,可以使用保存点 savepoint实现局部回滚。
python
def savepoint_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()
except Exception as e:
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(并发扣库存场景)
悲观锁示例,防止并发超卖
python
def deduct_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()
if row[0] >= 100:
cur.execute("UPDATE account SET balance=balance-100 WHERE username='zhangsan'")
conn.commit()
except Exception as e:
conn.rollback()
print(e)
finally:
cur.close()
conn.close()
最佳实践总结
- 业务涉及多写操作,必须使用事务;设置
autocommit=False。 - 使用
try‑except‑finally,异常分支必须执行rollback(),最后关闭连接。 - 事务粒度尽量小,避免长事务,执行完尽快commit或rollback释放锁。
- 并发场景需要锁时,合理使用
SELECT ... FOR UPDATE悲观锁,或业务层乐观锁。 - 确认表引擎为InnoDB。
- 一个事务全程复用同一个数据库连接对象。
如果你使用 SQLAlchemy ORM,框架会封装事务逻辑,但底层依然是MySQL事务机制。