尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python操作MySQL事务处理实战:从ACID到隔离级别与并发控制
1. 事务到底在解决什么问题先聊点实际的。我刚开始用 Python 连 MySQL 写业务代码的时候一直觉得事务是“高大上”的东西好像只有金融系统、订单系统那种高并发场景才需要。直到有次做一个模拟库存系统遇到一个特别头疼的线上问题用户下单后库存表的数据扣掉了订单表却因为 SQL 语句报错没写进去。钱收了货发不出去后台数据还对不上。那一刻我才真正意识到事务不是一个可有可无的选项而是保证数据不出错的底线机制。后来我把事务处理的关键步骤梳理清楚之后再也没出过这种数据不一致的幺蛾子。这篇文章就把我实际用过的方法、踩过的坑、以及每个操作背后的原理从头到尾摊开讲清楚。1.1 事务的本质与 ACID 通俗解读事务简单来说就是一组 SQL 操作打包成一个整体要么全部成功要么全部失败不允许出现“做了一半”的中间状态。用生活化的类比来说你在超市买了东西扫了付款码收银台那边的系统要从你的账户扣钱同时要把这笔钱记到超市的账上。扣钱成功、记账失败或者反过来这都是一场灾难。事务就是那个协调员保证这两件事同时成功或者同时不发生线下刷 POS 机、银行转账都是这个逻辑。专业一点讲事务必须满足四个特性也就是常说的 ACID原子性Atomicity事务里的所有 SQL 要么都执行成功要么都执行失败回滚。就像掰一根筷子整根掰断不存在掰一半的可能。一致性Consistency事务执行前和执行后数据库的完整性约束不被破坏。比如说账户余额不能为负数如果转账会导致余额变成负数整个事务就必须终止。隔离性Isolation多个事务并发执行时彼此之间不能互相干扰。两个用户同时扣同一件商品的库存数据库要保证他们看到的数据和最终扣减的结果是有序、正确的。持久性Durability事务一旦提交成功数据变更就永久保存下来即使系统突然断电、崩溃数据也不会丢。很多人刚接触事务时容易陷入一个误区以为只有当业务“非常重要”才需要事务。实际上只要一次操作涉及两条及以上的 SQL 更新语句就应该考虑事务否则数据一定会找机会给你挖坑。1.2 没有事务会导致什么严重后果我拿一个非常典型的电商下单场景举例子这个例子我后面还会反复用到。假设某个用户提交了一个订单后台代码要做三件事往订单表插入一条订单记录。扣减商品库存表中的库存数量。锁定用户的优惠券或扣减账户余额。这三步操作要是不放在事务里而是顺序执行、执行完直接提交会发生什么第一步插入订单成功第二步扣库存时发现库存不足报错了。代码抛异常跳出第三步没执行订单却已经留在数据库里了。看起来好像问题不大但用户端会显示“下单成功”后台一看库存没扣超卖了。更可怕的是如果第二步卡在网络超时上代码重试或者用户重复点击订单可能被创建多条数据直接烂掉。还有一种情况大家很容易忽略即使代码里写了事务但执行过程中某个 SQL 报错了程序没有正确回滚就退出数据库连接一释放这个未提交的事务会被数据库自动回滚掉。但如果在长连接里复用了这个连接上一个事务残留的数据可能串到下一个业务请求里造成脏数据。所以我后来养成了一个习惯每次讲 Python MySQL 事务处理一定会强调“代码里的异常处理和事务控制必须成对出现”因为这是新手最容易栽跟头的地方。2. Python 操作 MySQL 的准备工作Python 连接 MySQL 的方式有很多常见的主要是pymysql和mysql-connector-python另外还有一些基于 ORM 的框架比如 SQLAlchemy、Django ORM 也提供了事务接口。我这里以pymysql为主线讲因为它轻量、直白、容易看清楚底层的执行流程非常适合用来理解事务的每个步骤。2.1 驱动选型pymysql 还是 mysql-connector-python我自己的经验是追求“开箱即用”可以用mysql-connector-python它是官方驱动对 MySQL 8.0 的新特性支持最及时追求“轻量、部署简单、社区资料多”就用pymysql它是纯 Python 实现装起来不用编译出问题网上随便一搜就有大把解决方案。两者在事务处理方面的 API 设计几乎是一致的功能点pymysqlmysql-connector-python连接数据库pymysql.connect()mysql.connector.connect()获取游标connection.cursor()connection.cursor()提交事务connection.commit()connection.commit()回滚事务connection.rollback()connection.rollback()自动提交开关connection.autocommitconnection.autocommit不管用哪个驱动事务处理的核心调用逻辑没有任何区别因为你面对的都是同一个 MySQL 服务器底层都是通过 MySQL 协议告诉数据库“我要开启事务”“我要提交”或者“我要回滚”。有一点要提醒很多人会觉得“我用的是 ORM事务怎么处理”但 ORM 底层一样要调用这些 commit 和 rollback只是帮你封装了一层。比如 SQLAlchemy 的session.commit()本质就是触发底层连接的 commit 操作。所以我建议想真正理解事务的人先用裸 SQL pymysql把流程走一遍再去看 ORM 封装这样整个链路会非常通透。2.2 建立数据库连接的关键参数连接数据库看起来就一行代码但里面有几个参数和事务行为直接相关比如下面这段基础连接代码import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyourpassword, charsetutf8mb4, databaseshop_demo, autocommitFalse )这里有个极其重要的参数autocommit。MySQL 默认情况下每次执行一条 SQL 语句如果语句成功了它会立刻把改动持久化到磁盘这个行为就叫“自动提交”。如果autocommitTrue默认就是 True那你在 Python 里写的多条 SQL 其实是各自独立的完全不具备事务的原子性。只有把autocommit设为 FalseMySQL 才会在你执行第一条 SQL 时自动开启一个事务之后所有的 SQL 都在这个事务里暂存直到你调用commit()才算真正落盘或者调用rollback()把这一批操作全部撤销。很多人事务不生效第一反应是怀疑代码有问题其实 80% 的情况是忘了把autocommit关掉。除了autocommit连接时还要注意charset参数。如果表结构是 utf8mb4Python 连接也最好显式声明charsetutf8mb4不然插入表情符号、生僻字可能出现编码错误。这类错误虽然不影响事务机制但会中途打断 SQL 的执行引发异常间接影响事务流程。2.3 游标类型选择与受影响行数执行 SQL 需要游标cursor。pymysql 默认返回的查询结果是一个元祖嵌套的列表每一行是一个元组没有字段名。如果你希望结果带上字段名建议使用字典游标cursor conn.cursor(pymysql.cursors.DictCursor)这样查询出来的每一行都是一个字典键是字段名值是对应的内容。在事务处理中游标还有一个常见作用获取受影响的行数。比如执行UPDATE后可以用cursor.rowcount来判断到底更新了多少行如果期望更新 1 行结果 rowcount 是 0那大概率 WHERE 条件写错了或者数据本身就不存在。需要注意的是影响行数不一定是更新条数取决于 MySQL 是否启用了 CLIENT_FOUND_ROWS 标志。默认情况下如果更新时字段值没有发生变化MySQL 可能返回 0 行受影响这和实际“匹配到”的行数可能不一致。在做事务中的乐观锁判断时这个细节容易导致误判。新手可以先忽略这个 0 行问题但要知道有这回事。3. 事务处理完整实操流程现在进入正题。以下是我在实际项目里反复使用的一套事务处理模板每一步背后都有明确的逻辑不是平白无故写出来的。3.1 标准事务五步法开启、执行、提交、回滚、收尾事务最基础的写法就像下面这样import pymysql conn pymysql.connect( host127.0.0.1, userroot, passwordyourpassword, databaseshop_demo, autocommitFalse, charsetutf8mb4 ) try: cursor conn.cursor() # 1. 插入订单 cursor.execute( INSERT INTO orders (user_id, product_id, amount) VALUES (%s, %s, %s), (1001, 5002, 199.90) ) # 2. 扣减库存 cursor.execute( UPDATE products SET stock stock - 1 WHERE id %s AND stock 0, (5002,) ) # 3. 检查有没有真的扣到库存 if cursor.rowcount 0: raise Exception(库存不足事务需要回滚) # 4. 全部成功提交 conn.commit() except Exception as e: print(事务执行出错正在回滚, e) # 5. 出错回滚 conn.rollback() finally: cursor.close() conn.close()这套流程其实就是事务的完整生命周期拆开看是五步建立连接并关闭自动提交。这决定了后续所有 SQL 会被 MySQL 当作一个事务来处理。执行业务 SQL。多个 SQL 按业务逻辑顺序执行同时需要仔细设计 WHERE 条件和行锁策略确保并发情况下数据依然正确。业务校验。很多开发者忽略这一步。SQL 执行成功了不代表业务逻辑上正确。比如上面例子中扣库存的 SQL 需要确认影响行数是 1如果是 0 就说明库存不够必须主动抛异常触发回滚。提交或回滚。所有 SQL 都成功、业务校验也全部通过才调用commit()。任何一步出错直接rollback()把事务撤销。释放资源。游标和连接都要关闭避免连接泄漏。这套模板我已经用了很多年几乎所有 Python 操作 MySQL 的场景都是在这个骨架上做延伸比如加日志、加重试、加分布式锁等。3.2 为什么要手动 commit而不是靠 MySQL 自动提交有些初学者会问既然 MySQL 默认是自动提交的为什么不直接依赖默认行为反而要手动autocommitFalse再调用commit原因在于自动提交意味着每条 SQL 都是独立事务你无法把两条 SQL 捆绑成一个整体。没有捆绑第一条 SQL 成功、第二条 SQL 失败时你根本没有机会回滚第一条已经执行成功的操作。手动管理事务核心价值就是掌握“整体成功或整体失败”的控制权。这里要补充一个细节conn.commit()和conn.rollback()不只是提交和回滚还会清空这个连接上的事务上下文。换句话说每次调用 commit 或 rollback 之后当前事务就结束了。如果你接下来还要执行新的 SQLMySQL 会自动开启一个新的事务前提是autocommit依然是 False。这就引出一个常见的操作困惑如果要在一个连接里连续处理多个业务事务可以在每次业务操作结束后调用 commit再执行下一轮 SQL。千万不要把不相关的多组操作混在一个事务里否则一旦后面出错回滚会把前面已经成功的无关操作也一并撤销掉。3.3 用好 try-except-finally让异常处理无死角事务代码里的异常处理要分成三层来看第一层SQL 本身可能出错。比如语法错误、字段不存在、唯一键冲突这类异常会从cursor.execute()抛出来。第二层业务逻辑校验不通过。SQL 能正常执行但结果不满足预期需要手动抛异常。我用得最多的手段就是raise Exception(...)或者自己定义一个业务异常类。第三层提交/回滚过程中可能出现异常。比如网络中断conn.commit()本身也可能抛异常需要捕获。一个容易忽略的问题异常发生之后事务并不一定需要回滚。万一错误发生在rollback()之后比如finally里关闭游标的代码报错这时候事务已经结束了不需要再 rollback。所以我更推荐下面这种写法conn None cursor None try: conn pymysql.connect(...) cursor conn.cursor() ... conn.commit() except Exception: if conn: conn.rollback() raise finally: if cursor: cursor.close() if conn: conn.close()这个写法的重点在于except里先判断conn是否存在再调用 rollback同时把异常重新抛出raise让外层调用者知道这里发生了失败。finally里再安全关闭游标和连接。这套结构我在生产环境里用了很久从来没有因为资源释放顺序的问题导致连接泄露。4. 使用上下文管理器简化事务代码每次手写 try-except-finally 其实有点繁琐而且很容易在代码里漏掉某条分支。Python 的with语句配合上下文管理器可以把事务的提交、回滚、资源释放全部封装起来让业务代码只关注 SQL 本身。4.1 自封装一个事务上下文管理器先看一段可以直接拿去用的代码import pymysql from contextlib import contextmanager contextmanager def transaction(conn): 事务上下文管理器正常则提交异常则回滚 try: yield conn conn.commit() except Exception: conn.rollback() raise使用时是这样conn pymysql.connect( host127.0.0.1, userroot, passwordyourpassword, databaseshop_demo, autocommitFalse, charsetutf8mb4 ) with transaction(conn) as conn: cursor conn.cursor() cursor.execute(INSERT INTO orders (user_id, product_id, amount) VALUES (%s, %s, %s), (1001, 5002, 199.90)) cursor.execute(UPDATE products SET stock stock - 1 WHERE id %s AND stock 0, (5002,)) if cursor.rowcount 0: raise Exception(库存不足)这段代码的好处是显而易见的。正常执行完with块上下文管理器自动调用conn.commit()一旦块内抛异常不管异常来自 SQL、业务判断还是别的地方都会自动执行conn.rollback()并且异常继续往上抛。你再也不用在每个业务方法里重复写 try-except-finally 了。有人可能会担心这个上下文管理器好像没有关闭连接是的连接关闭这个动作我没有放进来因为连接通常是整个应用共用的资源不应该在一个事务结束后就被关闭。更合理的做法是把连接交给上层的连接池来管理事务结束后由连接池决定是否回收。4.2 上下文管理器内部的执行细节分析这个封装看起来很简单但里面有几个值得深挖的细节。第一contextmanager装饰器把一个生成器函数包装成了上下文管理器。关键字yield conn之前的部分是进入with块时执行的yield conn之后的部分是离开with块时执行的。在离开时如果块内没有异常就执行conn.commit()否则执行conn.rollback()。第二raise这个动作不能省。如果在上下文管理器里捕获异常后不回抛调用者就感知不到事务失败可能继续往下执行无关逻辑导致整个业务流程数据不一致。第三这个上下文管理器里的conn参数是外部传入的而不是在内部创建。这样做的好处是一个连接可以在一个请求里被多个with transaction(conn)块复用每个块都是独立事务互不干扰。4.3 嵌套事务与保存点实现部分回滚有时候业务非常复杂比如一个大的事务流程里面前面几步操作已经做了后面某一步出错时希望只回滚到中间某个位置而不是全部推翻重来。这时候就要用保存点savepoint。保存点的使用方式是在事务中给某个位置打一个标记之后一旦出错只回滚到这个标记保留这个标记之前已经执行成功的操作。MySQL 中使用保存点很简单SAVEPOINT sp1; -- 执行一些操作 ROLLBACK TO SAVEPOINT sp1;但注意ROLLBACK TO SAVEPOINT之后事务还没有结束你仍然可以继续执行新的 SQL最后统一调用 COMMIT。在 Python 中这样操作try: cursor.execute(UPDATE account SET balance balance - 100 WHERE user_id 1) cursor.execute(SAVEPOINT after_deduct) cursor.execute(UPDATE account SET balance balance 100 WHERE user_id 2) # 这里模拟一个业务异常 if some_condition: cursor.execute(ROLLBACK TO SAVEPOINT after_deduct) # 此时第一个更新生效第二个更新被撤销 conn.commit() except Exception: conn.rollback()这里有个细节值得强调ROLLBACK TO SAVEPOINT不会释放行锁。比如第一个 UPDATE 已经锁住了某些行即使回滚到保存点这部分锁也要等到事务结束commit 或 rollback才会释放。这也是很多并发问题产生的土壤不要以为回滚到保存点就万事大吉锁仍然握在手里。保存点适合用在批处理、长流程、可部分失败的场景中。如果你的业务没有那么复杂完全不需要用保存点一个事务直接全部成功或全部失败反而更简单、更好维护。5. 事务的隔离级别与并发控制事务处理做到上面那一步单机单用户环境下已经可以稳定运行了。但一旦上生产、上并发光会提交和回滚还远远不够。你还得知道 MySQL 的隔离级别是怎么回事否则数据明明没写错读出来却是错的。5.1 MySQL 四种隔离级别详解SQL 标准把事务隔离级别分成四档MySQL 默认的隔离级别是REPEATABLE READ可重复读这一点和很多其他数据库不同。四种隔离级别依次如下隔离级别脏读不可重复读幻读说明READ UNCOMMITTED可能可能可能啥都不防基本没人用READ COMMITTED避免可能可能多数数据库默认级别REPEATABLE READ避免避免可能InnoDB 实际解决了MySQL 默认级别SERIALIZABLE避免避免避免完全串行性能最低先解释几个关键概念。脏读事务 A 修改了一条数据但还没提交事务 B 这时候去读读到了 A 的“临时修改值”。后来 A 回滚了B 读到的就是一个不存在的假数据。不可重复读事务 A 先读了一次数据事务 B 在这期间修改并提交了这条数据事务 A 再读第二次发现值变了。重点在于同一行数据内容前后不一致。幻读事务 A 按某个条件查出来一批记录事务 B 插入了一条新记录并提交事务 A 再按同样条件查发现多了一行“像幻觉一样”的记录。重点在于记录数量前后不一致。MySQL 的 InnoDB 存储引擎通过多版本并发控制MVCC和间隙锁在 REPEATABLE READ 级别下实际上把幻读也解决了但是这个概念比较复杂新手可以暂时先记住结论MySQL InnoDB 下的 REPEATABLE READ 已经能够避免脏读、不可重复读和大多数幻读问题。5.2 Python 中如何设置隔离级别pymysql 连接成功之后可以通过执行 SQL 来设置隔离级别cursor.execute(SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED)或者在建立连接时直接通过 init_command 参数来指定conn pymysql.connect( host127.0.0.1, userroot, passwordyourpassword, databaseshop_demo, autocommitFalse, charsetutf8mb4, init_commandSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED )我个人的经验是如果业务对数据一致性要求高比如账务、订单、库存优先考虑 READ COMMITTED因为它能避免脏读同时并发性能比 REPEATABLE READ 好。MySQL 默认 REPEATABLE READ 主要是为了兼容主从复制和 binlog 的某些场景但现代业务里很多东西在 READ COMMITTED 下反而更简单。5.3 行锁与乐观锁如何正确扣减库存事务并发时最经典的竞态问题是“超卖”。两个用户同时下单买同一件商品库存只有 1 件两个事务都先查库存都发现大于 0然后各自扣减最终库存变成 -1。解决方案有两个方向。第一个方案是用数据库行锁。在事务中执行SELECT ... FOR UPDATE把这一行锁住其他事务必须等这个事务提交后才能继续操作cursor.execute(SELECT stock FROM products WHERE id %s FOR UPDATE, (5002,)) row cursor.fetchone() if row[stock] 0: raise Exception(库存不足) cursor.execute(UPDATE products SET stock stock - 1 WHERE id %s, (5002,))FOR UPDATE是悲观锁思路它认为冲突一定会发生所以提前把所有可能冲突的行锁住简单粗暴但并发量大的时候容易造成锁等待和死锁。第二个方案是乐观锁用版本号或者条件更新的方式认为冲突不常发生在更新时通过 WHERE 条件来校验数据是否被修改过cursor.execute( UPDATE products SET stock stock - 1 WHERE id %s AND stock 0, (5002,) ) if cursor.rowcount 0: raise Exception(库存不足)这就是我前面代码里反复出现的写法。它是利用 MySQL 更新的原子性stock 0这个条件能保证只有库存大于 0 时才会更新成功。受影响行数为 0 则说明库存不足或者已经被其他事务改掉了。两种方案没有绝对的优劣取决于业务规模。库存扣减这种高频操作我倾向于乐观锁 条件更新代码简单不需要考虑事务外层的锁等待如果是金融转账这种对一致性极其敏感的场景悲观锁反而更稳妥。6. 实际项目中的常见问题与排查记录这一部分是我最想写的因为事务的坑几乎都是在线上才暴露出来的。下面这些问题是 Python MySQL 事务处理中最高发的问题每一个都是我自己或者身边同事真实踩过的。6.1 事务没生效的 5 个检查点很多人写完事务代码测试下来发现数据不对回滚没生效。遇到这种情况我一般按下面的顺序排查检查autocommit是否关闭。如果autocommitTrue每次 SQL 执行完立刻提交事务根本不存在。这是最高频的问题。检查 commit 或 rollback 的位置。如果你把 commit 放在异常捕获之前异常发生时事务已经被提交了回滚就没有意义。检查连接是否被连接池复用。有些连接池默认会把 autocommit 重置为 True你在代码里设置的 autocommitFalse 可能被覆盖。检查是否跨了多个连接。事务是针对单个数据库连接的如果第一条 SQL 用连接 A第二条 SQL 用连接 B它们根本不在同一个事务里。检查 SQL 是不是涉及了不支持事务的存储引擎。MySQL 中 MyISAM 引擎不支持事务表建错了引擎commit 和 rollback 都不会生效。检查过一遍之后90% 的问题都能解决。6.2 死锁与 Lock wait timeout exceeded死锁是事务并发绕不开的问题。两个事务各自持有一个锁又同时去申请对方持有的锁谁都不让谁数据库只能检测到死锁后强制回滚其中一个事务。死锁在 Python 里的典型报错是Deadlock found when trying to get lock; try restarting transaction还有一个很常见的报错是Lock wait timeout exceeded; try restarting transaction这是单个事务等待锁的时间超过了innodb_lock_wait_timeout默认 50 秒MySQL 主动放弃等待。遇到这种问题不要只盯着一行代码要检查整个业务链路的加锁顺序。比如事务 A 先更新订单表再更新库存表事务 B 先更新库存表再更新订单表两个事务交叉执行时就很可能死锁。解决办法很简单所有事务里操作多张表时都按同一个顺序加锁比如先订单后库存统一保持一致。另外要注意死锁发生后被回滚的是 MySQL 选中的那个事务Python 代码这边会收到异常。如果业务允许可以做一次重试机制捕获死锁异常后重新执行整个事务。但不要无限重试一般重试 3 次以内比较合理。6.3 事务里执行了 SELECT但查不到自己的修改这种情况也叫“当前读”和“快照读”的差异。在 REPEATABLE READ 隔离级别下普通 SELECT 属于一致性快照读读的是事务开始时的快照即使自己在这个事务里已经 UPDATE 了数据并还没提交再次 SELECT 时可能仍然是旧值。解决方案是用“加锁读”cursor.execute(SELECT stock FROM products WHERE id %s FOR UPDATE, (5002,))加了FOR UPDATE的查询属于当前读读取的是最新已提交的数据并会对读出记录加锁。这样就能保证事务内读到的是最新、真实的数据。同理SELECT ... LOCK IN SHARE MODE是加共享锁的当前读可以允许其他事务继续读但不能修改。这个问题在 REPEATABLE READ 下特别容易碰到比如你刚插入一条订单转手去查询自己插入的订单列表结果发现查不到容易怀疑人生。通常这时候用普通查询即可因为快照读对已提交的数据也很少产生问题真正需要加锁读的场景是在“读完之后要更新同一批数据”的时候。6.4 事务时间过长导致 binlog 堆积还有一个很多人不会注意到的坑事务如果长时间不提交会一直持有事务产生的 undo log 和行锁导致两个连锁反应。第一个连锁反应是修改的数据行会积累大量历史版本MySQL 的 undo log 膨胀占用大量磁盘空间。第二个连锁反应是主从复制时binlog 要等事务提交才写入大事务会直接拖慢复制进度从库数据延迟严重。所以在 Python 代码里要避免在一个事务里做大量循环操作。比如一次性往事务里插入一万条数据猛一看是一条事务实际上远超合理范围。更合理的做法是分批提交每 500 条或 1000 条commit()一次既能保证单批操作的原子性又不会拖垮数据库。6.5 连接泄漏与 MySQL has gone away事务处理代码里如果某条路径上提前 return 了没有执行finally里的连接关闭连接就一直挂在数据库上。时间长了数据库连接数被占满新的请求连接时就会报“Too many connections”执行 SQL 时可能报“MySQL server has gone away”。建议所有连接都加到finally或者上下文管理器里统一关闭不要裸写conn pymysql.connect(...)然后忘记关闭。生产环境建议直接用连接池比如DBUtils或者SQLAlchemy内置的池统一管理连接的生命周期从根上避免连接泄漏。7. 一套完整的事务处理最佳实践代码把上面所有知识点汇总起来我最后给出一个可以直接改造使用的核心工具模块。这个模块封装了连接管理、事务上下文、重试机制和日志输出基本能覆盖绝大多数 Python MySQL 事务处理场景。import time import logging import pymysql from contextlib import contextmanager logger logging.getLogger(mysql_transaction) class MySQLClient: def __init__(self, host, port, user, password, database, **kwargs): self.config { host: host, port: port, user: user, password: password, database: database, autocommit: False, charset: utf8mb4, cursorclass: pymysql.cursors.DictCursor, } self.config.update(kwargs) def connect(self): return pymysql.connect(**self.config) contextmanager def transaction(self, retry_times3): 事务上下文支持死锁重试 conn self.connect() try: for attempt in range(retry_times): try: yield conn conn.commit() return except pymysql.err.OperationalError as e: if Deadlock in str(e) and attempt retry_times - 1: logger.warning(发生死锁第 %s 次重试, attempt 1) conn.rollback() time.sleep(0.2 * (attempt 1)) continue conn.rollback() raise except Exception: conn.rollback() raise finally: conn.close()使用的时候client MySQLClient( host127.0.0.1, port3306, userroot, passwordyourpassword, databaseshop_demo ) with client.transaction() as conn: cursor conn.cursor() cursor.execute( UPDATE products SET stock stock - 1 WHERE id %s AND stock 0, (5002,) ) if cursor.rowcount 0: raise Exception(库存不足) cursor.execute( INSERT INTO orders (user_id, product_id, amount) VALUES (%s, %s, %s), (1001, 5002, 199.90) )这套代码有几个地方是专门为事务设计的autocommitFalse写死在配置里防止调用方误改。事务上下文统一管理 commit 和 rollback。死锁时有 3 次重试机会重试间隔递增避免雪崩式重试。连接固定在用完即关由上下文管理器负责杜绝连接泄漏。所有异常向外抛出由更上层决定是返回错误提示还是记录日志。有人会问连接每次事务都新建一个开销会不会很大这确实存在所以生产环境应当引入连接池。连接池可以把连接复用起来但事务的提交和回滚逻辑保持一模一样。8. 站在实际项目中看事务处理的取舍日常开发中不是所有数据库操作都需要套事务模板。如果一个操作只涉及一条 SQL 的写入比如只是更新一个字段、插入一条日志MySQL 自动提交已经完全够用强行包事务反而增加无谓的开销。什么时候必须手动管理事务我的判断标准很简单只要一个业务流程包含两步或以上写操作并且这些写操作之间存在逻辑上的“全有或全无”关系就必须手动管理事务。反之如果操作之间互不依赖、互相独立即使其中一步失败也不影响其他步骤那么各自独立提交反而更合理。这个取舍的背后本质是在一致性和可用性之间做权衡。有些人一听“一定要用事务”就觉得万事大吉其实事务也是有代价的锁竞争、性能下降、死锁风险、undo 日志膨胀。无脑用事务和完全不用事务都是两个极端。从我个人的实操体会来讲事务处理最核心的不是学会用commit()和rollback()这两个方法而是能够判断“什么该放事务、什么不该放事务”。加锁解决并发冲突、新增索引避免锁范围过大、缩短事务时间减少锁持有周期这些配套手段往往比事务本身更能提升系统的稳定性。最后再分享一个我实际用过的排查技巧遇到事务相关的问题先在 MySQL 命令行执行SHOW ENGINE INNODB STATUS看其中的 LATEST DETECTED DEADLOCK 部分能直接看到死锁产生的 SQL 语句、锁等待的关系定位效率远高于在 Python 代码里瞎猜。把这一步当作压箱底的绝招用能把排查时间缩短一半以上。
RELATED

相关推荐

SSMS使用指南:从连接实例到执行计划,避开掉五大常见坑

SSMS使用指南:从连接实例到执行计划,避开掉五大常见坑

1. 一个“数据库管理全家桶”,为什么值得花几天学1.1 它是什么,能解决什么问题SSMS全称是SQL Server Management Studio,你可以把它理解为SQL Server环境的“可视化控制台”。连接数据库、写T-SQL、调性能、做备份恢复、管理登录权限、看实例…

📅 2026/10/22 22:00:42
PARTITION BY实战:用窗口函数搞定考场编排

PARTITION BY实战:用窗口函数搞定考场编排

每年一到考试季,总有教务老师对着Excel表格生无可恋。几千个考生、几十门科目、几十间教室,要把每个人按科目塞进合适的考场,还得保证每间不超过30人、同班同学尽量分散——这套流程听起来只是“排序加分组”,但真正用MS SQL Serv…

📅 2026/10/22 22:00:42
华为eNSP三层交换实战:SVI配置与跨VLAN通信排障

华为eNSP三层交换实战:SVI配置与跨VLAN通信排障

1. 项目概述:为什么三层交换是网络工程师绕不开的硬核基本功“华为eNSP—三层交换实验”这个标题看起来平平无奇,但背后藏着网络工程实操能力的分水岭。我带过不少刚考完HCIA、甚至手握HCIP证书的学员,一让他们在eNSP里配通一个跨VLAN的三层交…

📅 2026/10/22 22:00:42
MORE NEWS

更多资讯

📰

基于ZigBee的智能家居系统设计与实现:从CC2530到Android全链路解析

简介:面向高校毕业设计及物联网开发者的一份完整论文资料,以ZigBee技术为核心,解决传统智能家居系统部署成本高、可扩展性弱的问题。论文围绕CC2530芯片展开硬件设计,结合WiFi与ZigBee双网通信桥接、MQTT协议消息传输和Android控制…

📰

智慧医联体平台建设:FHIR+API网关+联邦学习落地实践

简介:本资源是一份面向医疗信息化建设者、区域卫生管理者及HIS系统实施人员的《智慧医联体系统平台建设方案》专业PPT课件,聚焦解决医联体内资源协同难、转诊流程不畅、基层能力薄弱等现实问题。方案系统梳理了国家医联体政策导向(如2017年制…

📰

排课管理系统课程设计:贪心+回溯算法实现与避坑指南

简介:这份课程设计报告面向网络工程、计算机相关专业的在校学生,围绕「某中学排课管理系统」这一典型教务信息化课题,提供从需求分析到程序实现的完整设计思路,适合作为数据库与软件工程课程设计的参考模板。报告以数据字典、数据…

📰

Python项目实战:从PDF筛选到机器学习与区块链落地指南

简介:一份名为《Python(机器学习 人工智能 区块链)项目列表 2023-2024》的PDF文档,是2023至2024年间Python在机器学习、人工智能与区块链领域落地项目的高密度清单,共收录20余个具体项目。文档整理了垃圾邮件检测、空气…

📰

告别AI编程失控:规则+提示+审查构建可控工作流

如果你最近在认真使用 AI 写代码,大概率会陷入一种微妙的焦虑:工具确实能快速产出大量代码,但你已经说不清自己是在编程,还是在给 AI 当打字员。网上流传的各种 AI 编程演示视频看起来很爽——用人话描述需求,代码就像…

📰

前端转 AI 100 天 Day 28:CI/CD——GitHub Actions 自动化部署

前端转 AI 第 28 天:CI/CD - GitHub Actions 自动化部署 昨日回顾 Day 27 把项目 Docker 化了,docker compose up -d 就能跑。 但还有个问题——每次改代码都要手动做这些事: # 每次提交后手动跑 pytest # 跑测试 d…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬