当一条更新操作执行到一半时,如果系统掉电或者程序异常退出,数据库会不会留下不完整的数据?这个问题的答案取决于你是否使用了事务。SQLite中的事务可以把多条SQL语句组织成一个原子单元,要么全部生效,要么全部撤销,从而保证数据状态始终一致。事务控制的核心只有三个命令:BEGIN、COMMIT和ROLLBACK,但围绕它们展开的细节却不少。

SQLite默认运行在自动提交模式下,每一条单独的INSERT、UPDATE或DELETE语句都会在一个自动开启并提交的隐式事务中执行。只有显式使用BEGIN命令,后续的SQL语句才会被归入同一个事务,直到遇到COMMIT或ROLLBACK。理解这一点,是掌握SQLite事务的第一步。
SQLite事务的基本概念与ACID特性
事务是数据库管理系统中的一个逻辑工作单元,由一组SQL语句组成。SQLite支持标准的事务语义,并通过ACID四个特性来提供可靠性。原子性(Atomicity)确保事务中的操作要么全部完成,要么全部不发生;一致性(Consistency)保证事务完成后数据库从一个有效状态转变为另一个有效状态;隔离性(Isolation)让并发事务之间互不干扰;持久性(Durability)则意味着已提交的修改在掉电或崩溃后仍然存在。
在SQLite内部,原子性和持久性主要通过回滚日志(rollback journal)或预写式日志(WAL)来实现。当你在事务中执行写入操作时,SQLite会先把原始数据记录到日志中,只有收到COMMIT命令后才会把修改同步到主数据库文件。如果中途执行ROLLBACK或进程崩溃,SQLite可以根据日志内容恢复原状。这种机制让SQLite在嵌入式场景下也能具备接近大型数据库的事务能力。
值得注意的是,自动提交模式下的每一条语句也是一个事务,只不过事务边界由SQLite自动管理。对于多步骤业务操作,必须手动开启事务才能把多个SQL语句组合成一个不可分割的单元,否则一旦中途失败,之前已经执行的部分修改就会被永久保留下来。
BEGIN、COMMIT、ROLLBACK命令实战
三个命令的语法非常简单:BEGIN用于开启新事务,COMMIT用于提交事务,ROLLBACK用于回滚事务。在SQLite命令行工具sqlite3中,可以直接输入这些命令。下面用一个转账场景来演示它们的实际效果。
假设账户表中有两个用户,初始余额各为100。先执行一次转账:从A转50给B。这个操作包含两条UPDATE语句。如果第一条执行成功、第二条执行失败,没有事务保护时A的余额会减少而B的余额不变,造成数据不一致。把这两条UPDATE放在同一个事务里,就可以通过回滚避免这种半更新状态。
-- 创建账户表并插入初始数据
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
balance REAL NOT NULL
);
INSERT INTO accounts (name, balance) VALUES ('A', 100);
INSERT INTO accounts (name, balance) VALUES ('B', 100);
-- 开启事务
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE name = 'A';
UPDATE accounts SET balance = balance + 50 WHERE name = 'B';
-- 先回滚,观察数据是否恢复
ROLLBACK;
SELECT * FROM accounts;
执行上面的脚本后,查询结果会发现A和B的余额仍然是100,说明两条UPDATE都被撤销了。如果把ROLLBACK替换为COMMIT,则修改会被永久写入数据库。这种可控性正是事务存在的价值。
还要注意,在交互式命令行中,如果某条语句因为语法错误或约束失败而报错,SQLite并不会自动回滚整个事务。需要在应用层捕获错误并决定是继续执行还是回滚,否则事务可能一直处于未提交状态,占用锁资源。
在Python中使用SQLite事务
实际开发中很少直接手敲BEGIN命令,更多是通过编程语言驱动来控制事务。Python内置的sqlite3模块提供了事务管理接口。连接对象默认启用事务,只有在调用commit()后修改才会提交。如果在执行过程中出现异常,可以调用rollback()撤销未提交的修改。
来看一个Python版本的转账示例。代码先连接数据库,然后执行两条UPDATE,最后提交。如果任一UPDATE失败,except块会调用rollback()恢复原状。
import sqlite3
def transfer_money(conn, from_user, to_user, amount):
try:
cur = conn.cursor()
cur.execute('UPDATE accounts SET balance = balance - ? WHERE name = ?', (amount, from_user))
# 模拟一个可能因余额不足或约束导致的异常
if amount > 100:
raise ValueError('金额超出限制')
cur.execute('UPDATE accounts SET balance = balance + ? WHERE name = ?', (amount, to_user))
conn.commit()
print('转账成功')
except Exception as e:
conn.rollback()
print(f'转账失败,已回滚:{e}')
conn = sqlite3.connect('test.db')
# 需要在连接后确保表存在,此处省略建表步骤
transfer_money(conn, 'A', 'B', 50)
conn.close()
Python的sqlite3模块还提供了上下文管理器。使用with conn:语句时,如果代码块正常结束会自动提交,如果抛出异常则会自动回滚。这种方式更简洁,也能避免遗漏提交或回滚调用。
import sqlite3
conn = sqlite3.connect('test.db')
try:
with conn:
cur = conn.cursor()
cur.execute('UPDATE accounts SET balance = balance - 50 WHERE name = \'A\'')
cur.execute('UPDATE accounts SET balance = balance + 50 WHERE name = \'B\'')
except Exception as e:
print(f'操作失败:{e}')
其他编程语言也遵循类似模式。例如Java的JDBC中Connection.setAutoCommit(false)会关闭自动提交,之后需要手动调用commit()或rollback()。Go的database/sql包则通过Tx类型来封装事务操作。无论哪种语言,核心思想都是一致的:把多条SQL语句放入同一个事务,由代码控制提交或回滚。
SAVEPOINT与部分回滚
除了整体回滚,SQLite还支持SAVEPOINT命令,允许在事务内部设置命名保存点。当执行ROLLBACK TO savepoint_name时,只有保存点之后的修改被撤销,事务本身仍然保持活动状态。这对于复杂业务流程非常有用,比如在一个大事务中执行多步操作,某一步失败后不必放弃全部工作,而只需回退到上一个稳定状态。
保存点的语法同样简单:SAVEPOINT sp1; 设置保存点sp1,然后执行若干SQL。如果需要回退,执行ROLLBACK TO sp1;,最后仍然需要COMMIT或RELEASE sp1;来结束保存点。保存点可以多个并存,也可以嵌套,但会占用一定内存和日志空间。
BEGIN; -- 设置保存点 SAVEPOINT before_update; UPDATE accounts SET balance = balance - 200 WHERE name = 'A'; UPDATE accounts SET balance = balance + 200 WHERE name = 'B'; -- 假设第二条更新失败,回退到保存点 ROLLBACK TO before_update; -- 事务仍然可用,可以继续其他操作 UPDATE accounts SET balance = balance - 10 WHERE name = 'A'; COMMIT;
这段代码中,ROLLBACK TO before_update不会关闭事务,只是把数据库状态恢复到保存点创建时的样子。之后的UPDATE仍然被包含在事务中,最终由COMMIT提交。理解保存点和事务的关系,可以让你更精细地控制回滚范围。
事务与并发锁的关系
SQLite使用文件锁来协调多个连接对数据库的并发访问。事务的开启方式会影响锁的获取时机。默认的BEGIN等同于BEGIN DEFERRED,它不会在事务开始时获取任何锁,直到第一次实际执行读或写操作时才获取相应的共享锁或保留锁。这种延迟加锁有利于提高并发性,但也可能导致死锁。
另外两种模式是BEGIN IMMEDIATE和BEGIN EXCLUSIVE。BEGIN IMMEDIATE在事务开始时就获取保留锁,可以防止其他连接写入,但允许读取。BEGIN EXCLUSIVE则直接获取排他锁,阻止其他连接进行读写。在需要避免写冲突的批量更新场景中,选择合适的开启模式能减少database is locked错误。
写事务在提交时需要将保留锁升级为排他锁,如果此时有其他连接持有共享锁,SQLite会等待或者返回SQLITE_BUSY错误。可以通过busy_timeout参数设置等待时间。理解锁机制后,你就可以合理控制事务的粒度,避免长时间持有写锁影响其他读写操作。
SQLite事务BEGIN COMMIT ROLLBACK事务回滚修改时间:2026-09-23 06:44:07