很多开发者学习MySQL事务的时候只停留在理论层面,不知道事务真正生效是什么样子。本文提供两套对照可运行代码:无事务产生脏数据、开启事务异常自动回滚。直接运行代码,观察数据库数据变化,直观验证事务效果。
环境准备
安装依赖
pipinstallpymysql测试表SQL
必须使用
InnoDB引擎,MyISAM不支持事务。
CREATETABLE`account`(`id`INTPRIMARYKEYAUTO_INCREMENT,`username`VARCHAR(32)NOTNULL,`balance`DECIMAL(12,2)NOTNULLDEFAULT0)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;-- 初始化测试数据:张三、李四各1000元INSERTINTOaccount(username,balance)VALUES('张三',1000.00),('李四',1000.00);业务场景:张三转账200元给李四。
流程:
- 张三余额扣减200
- 模拟程序抛出异常
- 李四余额增加200
案例一:不使用事务(自动提交,产生脏数据)
pymysql默认autocommit=True,每执行一条SQL就直接写入数据库。
当中间代码抛出异常,后面SQL无法执行,就会出现一部分成功、一部分失败的数据错乱。
importpymysqldefdemo_without_transaction():# 默认 autocommit=Trueconn=pymysql.connect(host="127.0.0.1",port=3306,user="root",password="你的密码",database="test_db",charset="utf8mb4")cursor=conn.cursor()# 1.张三扣200,这条SQL立刻生效落库cursor.execute("UPDATE account SET balance = balance - %s WHERE username=%s",(200,"张三"))# 模拟业务异常、接口报错、程序崩溃print("模拟程序发生异常")1/0# 2.李四加钱,这行代码永远不会执行cursor.execute("UPDATE account SET balance = balance + %s WHERE username=%s",(200,"李四"))cursor.close()conn.close()if__name__=="__main__":demo_without_transaction()运行现象
- 程序抛出
ZeroDivisionError异常终止 - 查询数据库:张三余额变成800,李四仍然1000
- 出现脏数据,钱凭空消失,业务逻辑被破坏
⚠️复现完成后记得重置表数据,方便测试下一个案例。
UPDATEaccountSETbalance=1000.00;案例二:开启事务,异常自动回滚
关键点:
- 设置
conn.autocommit = False关闭自动提交 - 所有修改操作先在事务缓冲区,不会真正写入数据库
- 正常执行完毕调用
conn.commit()持久化数据 - 捕获异常调用
conn.rollback()撤销全部修改
importpymysqldefdemo_with_transaction():conn=pymysql.connect(host="127.0.0.1",port=3306,user="root",password="你的密码",database="test_db",charset="utf8mb4")# 关闭自动提交,开启事务模式conn.autocommit=Falsecursor=conn.cursor()try:# 第一步:张三扣款cursor.execute("UPDATE account SET balance = balance - %s WHERE username=%s",(200,"张三"))# 模拟异常,触发回滚逻辑print("模拟程序发生异常")1/0# 第二步:李四收款cursor.execute("UPDATE account SET balance = balance + %s WHERE username=%s",(200,"李四"))# 全部执行成功,提交事务,数据真正写入数据库conn.commit()print("✅事务提交成功,转账完成")exceptExceptionase:# 出现任何异常,回滚本次事务所有变更conn.rollback()print(f"❌捕获异常,事务已全部回滚,异常信息:{e}")finally:cursor.close()conn.close()if__name__=="__main__":demo_with_transaction()运行现象(异常分支)
- 程序抛出除零异常,进入except代码块执行
rollback() - 查询数据库,张三、李四余额依旧都是1000,数据完全没有变化
- 所有update操作全部撤销,不会产生脏数据
测试正常提交场景
把代码中的1 / 0注释掉,再次运行。
- 不会触发异常,执行
commit() - 数据库:张三800,李四1200,转账业务正常完成。
案例三:演示部分回滚(SAVEPOINT保存点)
MySQL支持保存点,可以实现事务内部局部回滚,不需要全部回滚。
importpymysqldefdemo_savepoint():conn=pymysql.connect(host="127.0.0.1",port=3306,user="root",password="你的密码",database="test_db",autocommit=False,charset="utf8mb4")cur=conn.cursor()try:cur.execute("UPDATE account SET balance=balance-100 WHERE username='张三'")# 设置保存点sp1cur.execute("SAVEPOINT sp1")cur.execute("UPDATE account SET balance=balance+100 WHERE username='李四'")# 回滚到保存点,撤销上面李四加钱操作,张三扣钱保留cur.execute("ROLLBACK TO SAVEPOINT sp1")conn.commit()print("保存点演示完成")exceptExceptionase:conn.rollback()print(f"异常回滚{e}")finally:cur.close()conn.close()对比总结表
| 场景 | 无事务 autocommit=True | 开启事务 autocommit=False |
|---|---|---|
| 程序中途报错 | 部分SQL生效,生成脏数据 | 全部操作回滚,数据不变 |
| 程序正常结束 | 逐条SQL立即生效 | commit之后统一生效 |
开发避坑要点
- autocommit=False是开启事务的前提,忘记关闭自动提交,commit/rollback完全无效。
- 同一个事务内所有SQL必须使用同一个connection对象,更换连接代表全新事务。
- 异常分支必须手动执行
rollback(),否则未提交事务会挂起,占用数据库锁资源。 - 表引擎必须是InnoDB,MyISAM不支持事务。
- 尽量缩小事务粒度,避免长事务,长事务会造成锁等待、数据库性能下降。
SELECT查询语句不会修改数据,不会产生事务变更;SELECT ... FOR UPDATE会增加行锁。
业务模板
订单、库存、转账等多写业务直接套用这套模板:
conn.autocommit=Falsetry:# 多条写sqlconn.commit()exceptException:conn.rollback()finally:conn.close()通过以上样例代码,就可以直观验证MySQL事务原子性效果:一组操作要么全部成功,要么全部撤销。