9、事务处理:BEGIN TRANSACTION、COMMIT、ROLLBACK、事务的ACID特性、C语言中的事务实现
大家好,我是老张。今天咱们聊聊数据库里一个特别重要的概念——事务。说实话,我刚开始做嵌入式数据库开发那会儿,觉得事务就是个花架子,不就是几条SQL语句嘛。直到有一次,我在一个工业控制项目里,因为没处理好事务,导致设备状态数据写了一半就断电了……嗯,从那以后,我再也不敢小看事务了。
说白了,事务就是一组操作,要么全部成功,要么全部失败。就像你转账,从A账户扣钱和往B账户加钱,这两步必须同时完成,不能只扣不加。这就是事务要解决的问题。
9.1 事务的ACID特性
聊事务,绕不开ACID这四个字母。我当年面试的时候,面试官问ACID,我背得滚瓜烂熟,但真正理解是在项目里摔了跟头之后。
| 特性 | 全称 | 大白话解释 |
|---|---|---|
| A | Atomicity(原子性) | 事务里的操作,要么全做,要么全不做 |
| C | Consistency(一致性) | 事务执行前后,数据要符合所有规则 |
| I | Isolation(隔离性) | 多个事务同时跑,互不干扰 |
| D | Durability(持久性) | 事务一旦提交,数据就永久保存 |
核心要点:ACID这四个特性,原子性是基础,一致性是目标,隔离性是手段,持久性是保障。缺一个,事务就不完整。
你想想看,如果原子性做不到,那转账扣了钱但没加上,这账怎么平?如果持久性做不到,提交完数据一断电就没了,那还玩什么?
9.2 BEGIN TRANSACTION、COMMIT、ROLLBACK
这三个命令,是事务操作的三大金刚。我习惯把它们叫做「开始-确认-撤销」三部曲。
- BEGIN TRANSACTION:开启一个新事务。从这一刻起,你做的所有修改都处于「待定」状态。
- COMMIT:提交事务。确认所有操作,数据正式生效。
- ROLLBACK:回滚事务。撤销所有操作,回到事务开始前的状态。
举个例子,你在SQLite里这么写:
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
如果第二个UPDATE执行失败了,你直接ROLLBACK,第一个UPDATE的扣钱操作也会被撤销。这就是原子性的体现。
我的小技巧:在嵌入式环境里,我习惯在每次事务开始前先检查数据库连接状态。曾经有一次,连接已经断了,我还傻乎乎地执行了BEGIN,结果后面所有操作都报错,排查了半天。
9.3 C语言中的事务实现
好了,理论说完了,咱们上点硬货。在C语言里怎么实现事务?我用SQLite的C API来演示,因为它在嵌入式领域用得最多。
核心就三个函数:sqlite3_exec()执行SQL,sqlite3_prepare_v2()准备语句,以及错误处理。
9.3.1 基本的事务流程
#include <sqlite3.h>
#include <stdio.h>
int transfer_funds(sqlite3 *db, int from_id, int to_id, double amount) {
char *err_msg = NULL;
int rc;
// 1. 开启事务
rc = sqlite3_exec(db, "BEGIN TRANSACTION", 0, 0, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "开启事务失败: %s\n", err_msg);
sqlite3_free(err_msg);
return -1;
}
// 2. 执行扣款
char sql[256];
snprintf(sql, sizeof(sql),
"UPDATE accounts SET balance = balance - %.2f WHERE id = %d",
amount, from_id);
rc = sqlite3_exec(db, sql, 0, 0, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "扣款失败: %s\n", err_msg);
sqlite3_free(err_msg);
sqlite3_exec(db, "ROLLBACK", 0, 0, 0); // 回滚
return -1;
}
// 3. 执行加款
snprintf(sql, sizeof(sql),
"UPDATE accounts SET balance = balance + %.2f WHERE id = %d",
amount, to_id);
rc = sqlite3_exec(db, sql, 0, 0, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "加款失败: %s\n", err_msg);
sqlite3_free(err_msg);
sqlite3_exec(db, "ROLLBACK", 0, 0, 0); // 回滚
return -1;
}
// 4. 提交事务
rc = sqlite3_exec(db, "COMMIT", 0, 0, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "提交事务失败: %s\n", err_msg);
sqlite3_free(err_msg);
sqlite3_exec(db, "ROLLBACK", 0, 0, 0);
return -1;
}
return 0;
}
注意:我曾经犯过一个错误——在ROLLBACK之后没有检查返回值。如果ROLLBACK本身也失败了(比如磁盘满了),那数据就处于一个「半提交半回滚」的尴尬状态。所以,关键操作一定要检查返回值。
9.3.2 使用保存点(Savepoint)
有时候,你不想回滚整个事务,只想回滚到某个中间点。这时候就需要保存点。我做过一个批量导入数据的工具,每导入1000条记录设一个保存点,出错了只回滚到上一个保存点,不用全部重来。
// 设置保存点
sqlite3_exec(db, "SAVEPOINT sp1", 0, 0, 0);
// 执行一些操作
// ... 如果出错 ...
sqlite3_exec(db, "ROLLBACK TO sp1", 0, 0, 0); // 回滚到保存点
// 继续执行
// ... 如果成功 ...
sqlite3_exec(db, "RELEASE sp1", 0, 0, 0); // 释放保存点
9.4 事务的隔离级别
说到隔离性,SQLite支持四种隔离级别。不过在实际嵌入式开发中,我大部分时间只用两种:DEFERRED和EXCLUSIVE。
| 隔离级别 | 行为 | 适用场景 |
|---|---|---|
| DEFERRED | 延迟获取锁,直到第一次读写操作 | 单线程、低并发 |
| IMMEDIATE | 立即获取写锁,允许读 | 多线程读多写少 |
| EXCLUSIVE | 独占锁,不允许任何其他操作 | 批量写入、数据迁移 |
经验之谈:在嵌入式设备上,如果多个线程同时写数据库,我建议用EXCLUSIVE模式。虽然性能会下降一点,但能避免很多诡异的死锁问题。我曾经在一个传感器数据采集项目里,因为用了DEFERRED模式,两个线程互相等待,最后整个系统卡死了。
9.5 事务实现的核心逻辑
下面这张图,是我自己总结的事务处理核心流程。每次写事务代码前,我都会在脑子里过一遍这个流程。
9.6 避坑指南
做事务开发这些年,我踩过不少坑。挑几个典型的说说:
- 忘记检查返回值:我刚开始写代码时,总觉得sqlite3_exec肯定能成功。直到有一次磁盘空间不足,事务提交失败,数据丢了。从那以后,每个API调用我都检查返回值。
- 事务嵌套:SQLite默认不支持嵌套事务。如果你在事务里又开一个事务,外层事务会被自动提交。我吃过这个亏,后来改用SAVEPOINT解决。
- 长时间持有事务:嵌入式设备资源有限,长时间持有事务会占用锁,影响其他操作。我建议事务内的操作尽量精简,别在事务里做耗时的计算或I/O。
- 忽略WAL模式:SQLite的WAL(Write-Ahead Logging)模式能大幅提升并发性能。我一般在多线程场景下都会开启WAL模式,用
PRAGMA journal_mode=WAL;。
一个小建议:在嵌入式设备上,如果对数据完整性要求极高(比如医疗设备、工业控制),可以考虑在每次COMMIT之后,再执行一次PRAGMA integrity_check;。虽然会慢一点,但能确保数据文件没有损坏。
9.7 总结
事务处理,说白了就是给数据操作加了一层「后悔药」。BEGIN是开始吃药,COMMIT是确认药效,ROLLBACK是吐出来。ACID四个特性,是这层「后悔药」的质量标准。
在C语言里实现事务,核心就是三步:开启、执行、提交或回滚。每一步都要检查返回值,每一步都要考虑异常情况。别嫌麻烦,这些检查在关键时刻能救你一命。
好了,今天就聊到这儿。记住,写事务代码的时候,多想想「如果这里断电了怎么办?」——想明白了,你的代码就稳了。