SQL Transaction的一些总结
大家好,今天我们来聊聊数据库里一个非常重要但又经常被忽略的概念------SQL Transaction(事务) 。无论你是后端开发、数据分析师,还是刚入门数据库的新手,理解事务都是必须的。简单来说,事务就是一组"要么全做,要么全不做"的操作,它保证了数据的完整性和一致性。在这篇文章里,我会用最直白的话,配合可运行的代码示例,把事务的四个特性(ACID)、隔离级别、以及在实际开发中怎么用事务讲清楚。咱们直接开始。---## 1. 为什么需要事务?先看一个"转账"的痛点假设你正在开发一个银行系统,用户小明要给小红转账 100 元。这个操作在数据库里至少分成两步:1. 从小明的账户扣掉 100 元2. 给小红的账户增加 100 元如果这两步之间,数据库突然崩溃了,或者程序报错了,那会出现什么情况?------小明钱扣了,小红没收到,钱凭空消失了。这绝对不行。这时候,事务就派上用场了。我们把这两步操作包在一个事务里,要么都成功,要么都失败回滚,绝不允许"只做一半"的情况发生。---## 2. 事务的四大特性(ACID)事务有四个核心特性,记住这个缩写 ACID 就够了:- A(Atomicity,原子性) :事务里的所有操作,要么全部成功,要么全部失败回滚。就像上面转账的例子,两步操作绑定在一起。- C(Consistency,一致性) :事务执行前后,数据库的完整性约束不被破坏。比如转账前后,所有账户余额的总和必须不变。- I(Isolation,隔离性) :多个事务并发执行时,彼此之间不能互相干扰。每个事务都感觉自己独占数据库。- D(Durability,持久性) :事务一旦提交成功,对数据库的修改就是永久的,即使系统崩溃也不会丢失。这四个特性是事务的基石。但实际中,完全满足"隔离性"会严重影响性能,所以数据库提供了不同的隔离级别,我们后面详细讲。---## 3. 事务的基本语法:用 Python + SQLite 示例我们先看一个最简单的例子。使用 Python 的 sqlite3 模块,它天然支持事务。pythonimport sqlite3# 连接数据库(内存中演示)conn = sqlite3.connect(":memory:")cursor = conn.cursor()# 创建账户表cursor.execute("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT, balance REAL)")# 插入两个初始账户cursor.execute("INSERT INTO accounts (name, balance) VALUES ('小明', 1000)")cursor.execute("INSERT INTO accounts (name, balance) VALUES ('小红', 500)")conn.commit() # 提交初始数据def transfer(from_id, to_id, amount): try: # 开启事务(Python sqlite3 默认自动开启,但我们可以显式控制) conn.execute("BEGIN") # 第一步:扣款 cursor.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (amount, from_id)) # 第二步:加款 cursor.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (amount, to_id)) # 检查是否扣成负数(业务规则) cursor.execute("SELECT balance FROM accounts WHERE id = ?", (from_id,)) if cursor.fetchone()[0] < 0: raise Exception("余额不足,回滚事务!") # 全部成功,提交事务 conn.commit() print("转账成功!") except Exception as e: conn.rollback() # 发生错误,回滚所有操作 print(f"转账失败:{e}")# 测试:从 id=1(小明)转 100 元给 id=2(小红)transfer(1, 2, 100)# 查看结果cursor.execute("SELECT * FROM accounts")for row in cursor.fetchall(): print(row)# 输出应该看到:小明 900,小红 600代码解释 :- conn.execute("BEGIN") 显式开启事务。- 如果中间任何一步出错(比如余额不足),我们调用 conn.rollback() 回滚,数据库就回到事务开始前的状态。- 只有所有操作都成功,才 commit() 提交。---## 4. 事务的隔离级别:并发下的博弈现实系统中,多个事务是并发执行的。如果完全不隔离,就会出现各种问题。数据库定义了四种标准隔离级别,从低到高是:| 隔离级别 | 脏读 | 不可重复读 | 幻读 ||---------|------|------------|------|| READ UNCOMMITTED | 可能 | 可能 | 可能 || READ COMMITTED | 不可能 | 可能 | 可能 || REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB 中可避免) || SERIALIZABLE | 不可能 | 不可能 | 不可能 |- 脏读 :读到另一个事务未提交的数据(很危险)。- 不可重复读 :同一个事务内,两次读取同一行数据,结果不一样(因为有其他事务修改并提交了)。- 幻读 :同一个事务内,两次执行相同的查询,返回的行数不一样(有其他事务插入了新行)。不同的数据库默认隔离级别不同。比如 MySQL InnoDB 默认是 REPEATABLE READ,PostgreSQL 默认是 READ COMMITTED。---## 5. 实战示例:演示"不可重复读"和隔离级别的作用我们用 Python 模拟两个并发事务,来看看不同隔离级别下的区别。这里用 SQLite 可能不支持所有级别,所以我们改用 MySQL 的思维来写伪代码,但逻辑通用。python# 伪代码示例,演示隔离级别的作用# 假设我们使用 MySQL,且事务1和事务2并发执行# 事务1START TRANSACTION;SELECT balance FROM accounts WHERE id = 1; -- 第一次读取,balance = 1000-- 此时事务2正在执行并提交了修改...SELECT balance FROM accounts WHERE id = 1; -- 第二次读取,balance = 900(如果隔离级别低)COMMIT;# 事务2START TRANSACTION;UPDATE accounts SET balance = 900 WHERE id = 1;COMMIT; -- 事务2提交- 如果隔离级别是 READ UNCOMMITTED ,事务1第一次读取就可能读到事务2还没提交的脏数据。- 如果是 READ COMMITTED ,事务1两次读取结果可能不同(900),这就是不可重复读。- 如果是 REPEATABLE READ ,事务1两次读取结果都是 1000,即使事务2提交了,也不会影响事务1的读取。实际代码示例(使用 MySQL 的 Python 连接器) :pythonimport mysql.connectorfrom mysql.connector import IsolationLevelconn = mysql.connector.connect( host="localhost", user="root", password="password", database="test")# 设置隔离级别为 REPEATABLE READ(默认)conn.isolation_level = IsolationLevel.REPEATABLE_READcursor = conn.cursor()# 开启事务conn.start_transaction()cursor.execute("SELECT balance FROM accounts WHERE id = 1")first_read = cursor.fetchone()[0]print(f"第一次读取:{first_read}")# 模拟另一个事务修改并提交(实际并发请用多线程)# 这里我们用另一个连接直接修改并提交conn2 = mysql.connector.connect(...) # 省略连接参数conn2.cursor().execute("UPDATE accounts SET balance = 900 WHERE id = 1")conn2.commit()# 再次读取当前事务的数据cursor.execute("SELECT balance FROM accounts WHERE id = 1")second_read = cursor.fetchone()[0]print(f"第二次读取:{second_read}")# 在 REPEATABLE READ 下,first_read == second_read == 1000conn.commit()这个例子告诉我们:选择合适的隔离级别,就是要在数据一致性 和系统性能 之间做权衡。隔离级别越高,并发性能越差,但数据越安全。---## 6. 事务的常见坑和最佳实践1. 事务内不要做耗时操作 (比如调用外部 API、发邮件),否则会长时间占用数据库连接,影响并发。2. 尽量让事务短小精悍 ,只包含必须的数据库操作。3. 学会使用 savepoint(保存点) :允许在事务内部做部分回滚,而不是整体回滚。4. 注意死锁 :多个事务互相等待对方的资源,数据库会自动检测并回滚其中一个事务。你需要重试。5. 不要忽略异常处理 :一定要有 try...except...rollback() 的兜底。---## 7. 代码示例:带 Savepoint 的事务pythonimport sqlite3conn = sqlite3.connect(":memory:")cursor = conn.cursor()cursor.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, val INTEGER)")cursor.execute("INSERT INTO t (val) VALUES (1)")conn.commit()conn.execute("BEGIN")try: cursor.execute("INSERT INTO t (val) VALUES (2)") conn.execute("SAVEPOINT sp1") # 设置保存点 cursor.execute("INSERT INTO t (val) VALUES (3)") # 假设这里出错了,但我们只想回滚到保存点,而不影响之前的插入 raise Exception("模拟错误") except Exception as e: print(f"出错:{e}") conn.execute("ROLLBACK TO SAVEPOINT sp1") # 回滚到保存点 # 注意:此时 val=2 的数据还在,val=3 的插入被回滚conn.commit()cursor.execute("SELECT * FROM t")print(cursor.fetchall()) # 输出 [(1,), (2,)],val=3 没有被插入---## 8. 总结事务是数据库保证数据一致性的核心机制。通过 ACID 四个特性,我们能确保转账、订单、支付等关键业务不会出现"半成品"数据。在实际开发中:- 默认使用数据库的隔离级别,除非有明确需求再调整。- 事务代码一定要写 try/except/rollback,不能漏。- 保持事务短小,避免死锁和性能瓶颈。- 用 Savepoint 精细化控制回滚范围。掌握事务,你就掌握了数据库可靠性的钥匙。希望这篇文章能帮你理清思路,少踩一些坑。如果你有更多问题,欢迎在评论区讨论。