学到这一章,数据库才真正开始接近真实业务的核心问题。
前面你已经会建表、插入、查询、更新、删除,也知道了 commit() 很重要。 但如果你只把数据库理解成“把 SQL 一条一条执行掉”,那离真正稳定的数据系统还差一层非常关键的东西:
如果一组操作只做了一半,怎么办
这个问题,表面上看像技术细节。 其实它在真实项目里非常致命。
比如转账时,A 的钱已经扣了,B 的钱却没加上。 比如下单时,订单已经创建了,但库存没扣成功。 比如删除用户时,用户表删掉了,关联资料表却没删。 比如批量导入时,前 300 条插进去了,第 301 条出错,结果整批数据半新半旧。
这些问题的共同点都在于:
业务本来是一整件事。 可数据库却可能只做成了一半。
而事务,正是专门用来解决这种问题的。
一、先别急着记定义,先理解事务到底在防什么
很多教程一上来会告诉你:
事务是一组操作的逻辑单元。
这句话没错,但对初学者来说有点抽象。
你先记更直白的一层:
事务是把多条数据库操作绑成一件事。 这件事要么整体成功,要么整体失败。
这就叫“要么都成功,要么都失败”。
为什么这件事这么重要。
因为真实业务里,很多操作从来不是孤立的一条 SQL。 而是一串动作连在一起,才算真正完成一个业务过程。
比如“转账”不是一条 SQL。 它至少有两步:
先给甲扣钱 再给乙加钱
这两步如果只成功一步,整个业务就是错的。
所以事务不是为了让数据库显得专业。 它是在保护业务完整性。
二、最经典的例子:转账为什么必须有事务
先看一个特别好理解的场景。
假设有两个人:
张三账户里有 1000 元 李四账户里有 500 元
现在要转账 200 元,从张三转给李四。
这件事拆成数据库动作,通常至少有两条更新语句:
第一条,把张三余额减 200 第二条,把李四余额加 200
如果程序正常跑完,两个人余额就会变成:
张三 800 李四 700
这当然没问题。
但如果第一条执行成功了,第二条执行之前程序崩了,或者数据库报错了呢。
结果就会变成:
张三已经扣钱 李四却没收到钱
这就是最典型的“只成功一半”。
从数据库角度看,好像也没什么神秘的,就是一条 SQL 成了,一条 SQL 没成。 可从业务角度看,这就是严重错误。
所以转账这种场景特别适合让你理解事务:
这不是两条独立 SQL。 它们必须被视为一整件事。
如果不能一起成功,那前面成功的也得撤回。
这就是事务存在的核心理由。
三、如果没有事务,数据库最容易出什么问题
先把最现实的风险说透。
没有事务时,数据库操作通常更像:
做一条算一条。 成功一条就留下来。 失败一条再说失败那一条。
这种方式在很简单的单步操作里没什么问题。 比如你只插入一条记录,它要么成功,要么失败,影响还比较可控。
但只要业务跨多步,风险就上来了。
最常见的问题就是:
数据处于“半完成状态”。
比如:
订单表已经插入了一条新订单 库存表却没扣减
用户资料表更新了 日志表却没记上
主表删了 关联表没删
这种半完成状态特别危险。 因为它看起来不像程序彻底崩坏,但数据库里的数据已经开始“逻辑不一致”。
很多线上系统最怕的,不是直接报错。 而是静悄悄地产生这种错账、脏数据、半残状态。
而事务,就是用来尽量避免这种情况的。
四、事务最直白的理解方式:给一组操作加一个共同命运
你可以把事务理解成:
这几条数据库语句,从今天开始命运绑定。
要成功,大家一起成功。 要失败,大家一起撤回。
这是一种特别重要的“共同命运”机制。
在没有事务时,每条 SQL 更像各过各的。 而有了事务后,这一组 SQL 就会被当成一个整体。
这种整体性非常重要。
因为真实业务不是按 SQL 条数来算的。 而是按业务动作来算的。
用户转一次账,是一个动作。 用户下一次单,是一个动作。 管理员删除一个账号及其相关信息,也是一个动作。
事务的意义,就是让数据库更接近真实业务动作,而不是只会机械执行单条语句。
五、先看最小事务示例:成功时 commit,失败时 rollback
这一章最核心的实战骨架,就是这段代码思路:
import sqlite3conn = sqlite3.connect("demo.db")cursor = conn.cursor()try: cursor.execute("第一条SQL") cursor.execute("第二条SQL") cursor.execute("第三条SQL") conn.commit()except Exception as e: conn.rollback() print("事务失败,已回滚:", e)finally: conn.close()
这段代码你现在一定要认真看懂。
try 里放的是一整组想一起完成的数据库操作。 如果这些操作都没问题,就执行 commit()。 如果中间任何一步出错,就执行 rollback()。 最后再关闭连接。
这里的关键不是语法本身。 而是这套节奏:
正常结束,提交。 中途出错,回滚。
你后面做任何带事务的数据库操作,本质上都绕不开这个结构。
六、commit 到底意味着什么
前面几章你已经接触过 commit(),但现在要把它理解得更深一点。
你可以把 commit() 理解成:
正式确认这一批修改,允许它们永久生效。
也就是说,在事务语境下,commit() 不是普通保存那么简单。 它更像是在说:
这一整件事,我确认做成了。
在 commit() 之前,这些修改虽然已经执行过,但还处在“当前事务上下文”里。 一旦你决定提交,它们才真正作为一个整体落下来。
所以以后你看到一串数据库修改语句,最后跟着一个 commit(),你脑子里就要自然翻译成:
前面这一整组动作,现在正式生效了。
七、rollback 又到底意味着什么
rollback() 的意义恰恰相反。
你可以把它理解成:
撤销当前事务中还没正式提交的修改,让数据库回到事务开始之前的状态。
也就是说,如果你已经执行了前两条 SQL,第三条报错了,那么 rollback() 的意思不是“只撤销第三条”。 而是:
把这一组里前面已经做过、但还没提交的改动,都一起撤回去。
这就是“要么都成功,要么都失败”的技术体现。
所以以后你看到:
except Exception: conn.rollback()
你要马上理解成:
这整件业务不算完成,前面那部分成功也不能要,全部退回去。
这才是事务的灵魂。
八、用一个真正可运行的转账例子,把事务讲透
下面我们来写一个更像真实业务的小例子。
先假设有一张账户表:
import sqlite3conn = sqlite3.connect("bank.db")cursor = conn.cursor()cursor.execute("""CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, balance INTEGER NOT NULL)""")conn.commit()conn.close()
再插入两条初始数据:
import sqlite3conn = sqlite3.connect("bank.db")cursor = conn.cursor()cursor.execute("DELETE FROM accounts")cursor.execute("INSERT INTO accounts (name, balance) VALUES (?, ?)", ("张三", 1000))cursor.execute("INSERT INTO accounts (name, balance) VALUES (?, ?)", ("李四", 500))conn.commit()conn.close()
现在开始写转账逻辑:
import sqlite3conn = sqlite3.connect("bank.db")cursor = conn.cursor()try: amount = 200 from_id = 1 to_id = 2 cursor.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (amount, from_id) ) cursor.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (amount, to_id) ) conn.commit() print("转账成功")except Exception as e: conn.rollback() print("转账失败,已回滚:", e)finally: conn.close()
这段代码最重要的不是两条 UPDATE 多高级。 而是它们被放进了同一个事务语境里。
只要其中任何一步失败,就会 rollback()。 这样数据库不会出现“张三扣了钱,李四没加上”的半完成状态。
这才是事务真正保护你的地方。
九、为了真正看懂 rollback,我们故意制造一次失败
如果你只是看成功案例,事务的重要性还不够直观。 所以最好故意制造一个失败。
比如我们改成这样:
import sqlite3conn = sqlite3.connect("bank.db")cursor = conn.cursor()try: amount = 200 from_id = 1 to_id = 2 cursor.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (amount, from_id) )raise ValueError("模拟转账中途出错") cursor.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (amount, to_id) ) conn.commit()except Exception as e: conn.rollback() print("转账失败,已回滚:", e)finally: conn.close()
这里第一条扣款已经执行了。 但中间故意抛了一个异常,后面的加款不会执行。
如果没有事务,你就会留下错误余额。 但现在因为有 rollback(),前面那条扣款也会被撤回。
也就是说,最终数据库会恢复到出错之前的状态。
这就是事务最值得你真正体验的一点:
它不是让错误消失。 而是让错误不至于把数据库留在半残状态。
十、事务最常见的真实业务场景,不只是转账
很多人学事务时,只记住了银行转账。 其实它在业务里比你想的广得多。
比如下单流程。
创建订单 扣减库存 记录支付流水 写操作日志
这些往往都不是一条 SQL,而是一组操作。
再比如用户注销。
删除用户主表 删除资料表 删除关联设置 写审计日志
这也是一整件事。
再比如批量导入。
插入用户表 插入成绩表 插入关系表
一旦导入过程某处出错,你往往希望整批撤回,而不是留一半。
所以事务不是金融系统专属知识。 只要你的业务是一组强关联操作,它都可能需要事务。
十一、事务不是“想用才用”的高级装饰,而是数据一致性的底线
这一点特别重要。
有些人会觉得,事务是不是等我项目大了再考虑。
其实不太对。
因为事务解决的不是“高并发才会有的问题”。 它解决的是“多步操作天然就可能不完整”的问题。
只要你的业务里存在下面这种结构:
先做 A 再做 B 最好 A 和 B 一起算成功
那事务就已经有存在价值了。
所以事务不是某种锦上添花。 很多时候,它其实是业务正确性的底线。
特别是当你从“写练习代码”往“写项目代码”过渡时,这种意识必须早点建立。
十二、SQLite 里事务是不是自动开始的
这是一个非常实用的干货点。
在 Python 的 sqlite3 里,默认情况下,当你执行修改数据库内容的语句时,通常会进入事务上下文,直到你 commit() 或 rollback()。Python 官方文档说明,SQLite 默认会在需要时隐式打开事务;而 commit() 和 rollback() 分别用于提交和回滚当前事务。
你现在先不用把底层规则背得很细。 先抓住最重要的一层:
只要你在做插入、更新、删除这类修改操作, 就应该明确地思考提交和回滚。
也就是说,不要把事务当成“很远的概念”。 在你现在写 SQLite 时,它其实已经就在你手边了。
十三、为什么很多人会误以为“执行成功一条”就等于事务安全
因为他们只盯着 SQL 层面,不看业务层面。
比如下面这种情况:
cursor.execute("UPDATE accounts SET balance = balance - 200 WHERE id = 1")cursor.execute("UPDATE accounts SET balance = balance + 200 WHERE id = 2")conn.commit()
表面上看,好像很自然。 但如果中间第二条语句抛错,而且你又没有合理处理异常,那业务就已经出问题了。
所以“单条 SQL 执行成功”从来不等于“整件事安全”。
事务的核心视角不是:
某一条有没有成功。
而是:
这一组操作对应的业务动作,是否完整成功。
这才是你现在最该建立的判断标准。
十四、事务和 try/except,为什么经常一起出现
因为事务最怕的,就是中途异常。
而异常恰恰是 Python 里最自然的失败信号。
所以数据库事务和 try/except 特别适合配合。
你可以把它理解成:
Python 的异常负责告诉你“中间出事了”。 数据库的 rollback 负责告诉你“前面那部分别算了,撤回”。
这两者组合起来,就形成了最经典的事务处理模板:
try: 做一组数据库修改 conn.commit()except: conn.rollback()
这几乎是数据库项目代码里最常见的基本形态之一。
所以你现在不只是要学会事务这个数据库概念。 还要慢慢体会:
Python 的错误处理机制,和数据库的事务控制,是怎么配合起来保护业务的。
十五、批量插入时,事务的价值会更明显
这一点很实战。
假设你要导入 1000 条学生成绩。
如果你一条条插,一条条提交,那当然也不是绝对不行。 但如果插到第 587 条时出错,前面 586 条已经永久写进去了,后面没写。
这时候你就会面临一个很麻烦的问题:
这批导入到底算成功,还是不算成功
如果你的业务要求是: 这次导入要么整批都成功,要么整批都别进。
那事务就特别重要。
例如:
import sqlite3students = [ ("张三", 90), ("李四", 85), ("王五", 92)]conn = sqlite3.connect("school.db")cursor = conn.cursor()cursor.execute("""CREATE TABLE IF NOT EXISTS scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score INTEGER NOT NULL)""")conn.commit()try:for name, score in students: cursor.execute("INSERT INTO scores (name, score) VALUES (?, ?)", (name, score) ) conn.commit() print("整批导入成功")except Exception as e: conn.rollback() print("导入失败,整批回滚:", e)finally: conn.close()
这里如果中间某条插入失败,前面那部分也不会留下来。 这就保证了导入结果的一致性。
十六、那是不是所有数据库操作都要手写事务模板
不是所有场景都要你写得很复杂,但事务意识必须有。
比如单条简单查询,通常不需要你专门想着事务。 因为查本来就不改数据。
比如只插入一条无关紧要的日志,事务感受也没那么强。
但只要你的业务满足这类特点:
多步修改 步骤之间有强依赖 只成功一半会出错 失败后希望整体撤回
那你就应该自动想到事务。
也就是说,事务不是看代码条数,而是看业务完整性。
这点非常关键。
因为很多人会误以为: 只要 SQL 不多,就不用事务。
其实不对。 两条 SQL 也可能构成一个必须用事务保护的业务。 比如转账就是最典型的例子。
十七、事务和 commit 的关系,千万别混
很多初学者学到这里会开始混:
我不是早就知道 commit() 了吗,那事务不就是 commit 吗
不是。
更准确地说:
事务是一整组操作作为一个整体的概念。commit() 是这组操作最终确认生效的动作。rollback() 是这组操作最终撤回的动作。
也就是说:
事务是一段过程。commit() 和 rollback() 是这段过程的两个可能结局。
这个关系一定要分清。
不然后面你会把“提交”误当成“事务本身”。
十八、SQLite 里还能用 with 让事务写得更稳一点
这是一个很有用的实战小技巧。
在 Python 的 sqlite3 里,连接对象本身可以配合上下文管理使用。官方文档说明,Connection 可以作为上下文管理器使用:正常退出 with 块时会提交事务,发生异常时会回滚。
例如:
import sqlite3with sqlite3.connect("bank.db") as conn: cursor = conn.cursor() cursor.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (200, 1) ) cursor.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (200, 2) )
这种写法的好处是:
如果 with 块正常结束,通常会提交。 如果中间抛异常,通常会回滚。
这会让代码更简洁一点。
不过对于初学阶段来说,我仍然建议你先把最经典的:
try / commit / rollback
那套骨架理解透。 因为这样你更容易真正看清事务的逻辑,而不是只会背简化写法。
十九、初学者在这一章最容易踩的坑
第一个坑,是把事务理解成“只有银行系统才用的东西”。
其实只要业务涉及多步强关联操作,事务就可能很重要。
第二个坑,是只知道 commit(),不知道 rollback() 的价值。
这样一旦出错,前面那部分修改就容易留在数据库里,形成半完成状态。
第三个坑,是业务上需要事务,但代码里没有任何异常处理。
结果一旦中途报错,就只能听天由命。
第四个坑,是把“单条 SQL 成功”误当成“整件业务安全”。
这其实是数据库思维里特别典型的误区。
第五个坑,是觉得事务很抽象,不知道什么时候该用。
你现在可以用一个特别实用的判断法: 只要这组操作“最好一起成,一起败”,事务就该进入你的脑子。
二十、这一章真正要带走的,不是某段代码,而是一种业务完整性思维
到这里,你最该真正建立起来的,不只是:
commit() 怎么写rollback() 怎么写
而是更深一层的东西:
数据库不只是执行 SQL。 它还要尽量保证业务动作的完整性。
一旦你开始用“完整业务动作”的视角去看数据库,你就会越来越自然地意识到:
哪些操作必须绑在一起 哪些失败不能留下半成品 哪些修改必须要么一起生效,要么一起撤回
这其实就是事务思维。
它的价值不仅仅在数据库里。 它也会反过来提升你对“业务系统到底怎么才算正确”的理解。
本章小结
事务的核心作用,是把多条数据库操作绑定成一个整体,让它们在业务上“要么都成功,要么都失败”。
这对于转账、下单、批量导入、关联删除、多步更新这类场景尤其重要。
你这一章最应该真正掌握的几个点是:
事务保护的不是单条 SQL,而是完整业务动作。commit() 表示这一整组修改正式生效。rollback() 表示这一整组未提交修改全部撤回。 最经典的事务代码骨架,是 try + commit + rollback。 判断是否需要事务,不看 SQL 条数,而看业务是不是“只能整体成功,不能成功一半”。
当你真正建立这种思维后,你写数据库代码时,才会开始更像“在保护业务正确性”,而不只是“在执行几条 SQL”。