SQLite3 事务处理
概述
事务是数据库操作的逻辑单元,确保一组操作要么全部成功提交,要么全部失败回滚。SQLite 完整支持 ACID 事务特性,结合 WAL 模式可以提供良好的并发读写性能。
事务核心原则
- 原子性(Atomicity) — 事务中的操作要么全部执行,要么全部不执行
- 一致性(Consistency) — 事务前后数据库状态一致
- 隔离性(Isolation) — 并发事务互不干扰
- 持久性(Durability) — 提交后的数据永久保存
基础概念
SQLite 事务模式
SQLite 支持不同级别的事务锁定:
| 语句 | 锁类型 | 说明 |
|---|---|---|
BEGIN DEFERRED | 不获取锁(默认) | 首次读写时才获取锁 |
BEGIN IMMEDIATE | RESERVED 锁 | 阻止其他写入者,允许其他读取者 |
BEGIN EXCLUSIVE | EXCLUSIVE 锁 | 独占访问,阻止所有其他操作 |
隔离级别
SQLite 使用串行化(Serializable)隔离级别 — 这是最高级别的隔离,确保事务之间完全独立。但 SQLite 的串行化与 MySQL 的有所不同:
- SQLite 通过文件级锁实现隔离
- WAL 模式下读者不会阻塞写者
- 写者仍然需要排他访问
语法与代码
基本事务操作
php
<?php
declare(strict_types=1);
$db = new SQLite3(__DIR__ . '/app.db');
$db->enableExceptions(true);
// 方式1: 使用 exec()
$db->exec('BEGIN IMMEDIATE');
try {
$db->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$db->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$db->exec('COMMIT');
echo "转账成功\n";
} catch (Exception $e) {
$db->exec('ROLLBACK');
echo "转账失败: " . $e->getMessage() . "\n";
}WAL 模式与并发事务
php
<?php
declare(strict_types=1);
$db = new SQLite3(__DIR__ . '/app.db');
$db->enableExceptions(true);
// 检查并开启 WAL
$currentMode = $db->querySingle('PRAGMA journal_mode');
if ($currentMode !== 'wal') {
$db->exec('PRAGMA journal_mode = WAL;');
echo "已切换到 WAL 模式\n";
}
// 设置繁忙超时
$db->busyTimeout(5000);
// 在 WAL 模式下:
// - 多个进程可以同时读取
// - 只有一个进程可以写入
// - 写入不会阻塞读取
// - 读取不会阻塞写入
echo "当前模式: " . $db->querySingle('PRAGMA journal_mode') . "\n";WAL 模式的工作原理
WAL 模式将新数据写入 WAL 文件而非直接修改数据库文件。读取操作从数据库文件读取,写入操作追加到 WAL 文件。定期将 WAL 文件合并回主数据库(称为 checkpoint)。
保存点(Savepoint)
php
<?php
declare(strict_types=1);
$db = new SQLite3(__DIR__ . '/app.db');
$db->enableExceptions(true);
$db->exec('BEGIN IMMEDIATE');
try {
$db->exec("INSERT INTO orders (user_id, total) VALUES (1, 500)");
$db->exec("UPDATE accounts SET balance = balance - 500 WHERE id = 1");
// 创建保存点 — 用于部分回滚
$db->exec('SAVEPOINT order_created');
try {
$db->exec("INSERT INTO shipments (order_id, address) VALUES (1, '北京')");
$db->exec("INSERT INTO notifications (user_id, message) VALUES (1, '已发货')");
} catch (Exception $e) {
// 回滚到保存点,保留之前的操作
$db->exec('ROLLBACK TO order_created');
echo "发货步骤失败,但订单已创建\n";
}
$db->exec('RELEASE order_created');
$db->exec('COMMIT');
echo "事务提交成功\n";
} catch (Exception $e) {
$db->exec('ROLLBACK');
echo "事务完全回滚: " . $e->getMessage() . "\n";
}用 SAVEPOINT 模拟嵌套事务
php
<?php
declare(strict_types=1);
function transferMoney(SQLite3 $db, int $from, int $to, float $amount): void
{
$db->exec('SAVEPOINT transfer');
try {
$balance = $db->querySingle(
"SELECT balance FROM accounts WHERE id = {$from}"
);
if ($balance < $amount) {
throw new RuntimeException('余额不足');
}
$db->exec("UPDATE accounts SET balance = balance - {$amount} WHERE id = {$from}");
$db->exec("UPDATE accounts SET balance = balance + {$amount} WHERE id = {$to}");
$db->exec('RELEASE transfer');
} catch (Exception $e) {
$db->exec('ROLLBACK TO transfer');
$db->exec('RELEASE transfer');
throw $e;
}
}
// 外层事务
$db->exec('BEGIN IMMEDIATE');
try {
transferMoney($db, 1, 2, 100);
transferMoney($db, 2, 3, 50);
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
echo "批量转账失败: " . $e->getMessage() . "\n";
}实战示例
事务管理器类
php
<?php
declare(strict_types=1);
class TransactionManager
{
private SQLite3 $db;
private int $transactionLevel = 0;
public function __construct(SQLite3 $db)
{
$this->db = $db;
}
public function begin(): void
{
if ($this->transactionLevel === 0) {
$this->db->exec('BEGIN IMMEDIATE');
} else {
$this->db->exec('SAVEPOINT sp_' . $this->transactionLevel);
}
$this->transactionLevel++;
}
public function commit(): void
{
$this->transactionLevel--;
if ($this->transactionLevel === 0) {
$this->db->exec('COMMIT');
} else {
$this->db->exec('RELEASE sp_' . $this->transactionLevel);
}
}
public function rollback(): void
{
$this->transactionLevel--;
if ($this->transactionLevel === 0) {
$this->db->exec('ROLLBACK');
} else {
$this->db->exec('ROLLBACK TO sp_' . $this->transactionLevel);
$this->db->exec('RELEASE sp_' . $this->transactionLevel);
}
}
/**
* 在事务中执行闭包
*/
public function transaction(callable $callback): mixed
{
$this->begin();
try {
$result = $callback($this->db);
$this->commit();
return $result;
} catch (Exception $e) {
$this->rollback();
throw $e;
}
}
public function getTransactionLevel(): int
{
return $this->transactionLevel;
}
}电商订单处理示例
php
<?php
declare(strict_types=1);
class OrderService
{
private SQLite3 $db;
private TransactionManager $txManager;
public function __construct(SQLite3 $db)
{
$this->db = $db;
$this->db->enableExceptions(true);
$this->db->busyTimeout(5000);
$this->txManager = new TransactionManager($db);
}
/**
* 创建订单 — 包含库存扣减、余额扣减、记录创建
*/
public function createOrder(int $userId, array $items): int
{
return $this->txManager->transaction(function () use ($userId, $items) {
// 1. 计算总金额
$totalAmount = 0;
foreach ($items as $item) {
$price = $this->db->querySingle(
"SELECT price FROM products WHERE id = {$item['product_id']}"
);
$totalAmount += $price * $item['quantity'];
}
// 2. 检查并扣减库存
foreach ($items as $item) {
$stock = $this->db->querySingle(
"SELECT stock FROM products WHERE id = {$item['product_id']}"
);
if ($stock < $item['quantity']) {
throw new RuntimeException("产品 {$item['product_id']} 库存不足");
}
$this->db->exec(
"UPDATE products SET stock = stock - {$item['quantity']} WHERE id = {$item['product_id']}"
);
}
// 3. 扣减用户余额
$balance = $this->db->querySingle(
"SELECT balance FROM accounts WHERE user_id = {$userId}"
);
if ($balance < $totalAmount) {
throw new RuntimeException("余额不足");
}
$this->db->exec(
"UPDATE accounts SET balance = balance - {$totalAmount} WHERE user_id = {$userId}"
);
// 4. 创建订单
$this->db->exec("
INSERT INTO orders (user_id, total_amount, status)
VALUES ({$userId}, {$totalAmount}, 'pending')
");
$orderId = $this->db->lastInsertRowID();
// 5. 创建订单明细
foreach ($items as $item) {
$this->db->exec("
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES ({$orderId}, {$item['product_id']}, {$item['quantity']}, {$price})
");
}
return $orderId;
});
}
/**
* 取消订单 — 恢复库存和余额
*/
public function cancelOrder(int $orderId): void
{
$this->txManager->transaction(function () use ($orderId) {
$order = $this->db->querySingle(
"SELECT user_id, total_amount, status FROM orders WHERE id = {$orderId}",
true
);
if (!$order || $order['status'] === 'cancelled' || $order['status'] === 'shipped') {
throw new RuntimeException("无法取消该订单");
}
// 恢复余额
$this->db->exec("
UPDATE accounts SET balance = balance + {$order['total_amount']}
WHERE user_id = {$order['user_id']}
");
// 恢复库存
$items = $this->db->query("
SELECT product_id, quantity FROM order_items WHERE order_id = {$orderId}
");
while ($item = $items->fetchArray(SQLITE3_ASSOC)) {
$this->db->exec("
UPDATE products SET stock = stock + {$item['quantity']}
WHERE id = {$item['product_id']}
");
}
$this->db->exec("UPDATE orders SET status = 'cancelled' WHERE id = {$orderId}");
});
}
}批量插入优化
php
<?php
declare(strict_types=1);
class BulkInserter
{
private SQLite3 $db;
public function __construct(SQLite3 $db)
{
$this->db = $db;
$this->db->enableExceptions(true);
}
/**
* 高性能批量插入 — 使用单事务 + 逐条预处理
*/
public function batchInsert(string $table, array $rows): int
{
if (empty($rows)) {
return 0;
}
$columns = array_keys($rows[0]);
$placeholders = implode(', ', array_fill(0, count($columns), '?'));
$sql = sprintf(
'INSERT INTO %s (%s) VALUES (%s)',
$table,
implode(', ', $columns),
$placeholders
);
$this->db->exec('BEGIN IMMEDIATE');
try {
$stmt = $this->db->prepare($sql);
$count = 0;
foreach ($rows as $row) {
foreach (array_values($row) as $i => $value) {
$stmt->bindValue($i + 1, $value);
}
$stmt->execute();
$stmt->reset();
$count++;
}
$this->db->exec('COMMIT');
return $count;
} catch (Exception $e) {
$this->db->exec('ROLLBACK');
throw $e;
}
}
}注意事项
SQLite 事务限制
重要限制
- 写锁粒度 — SQLite 使用数据库文件级锁,一次只能有一个写者
- 嵌套事务 — 不支持真正的嵌套事务,需使用 SAVEPOINT 模拟
- DDL 与事务 — SQLite 的 DDL 语句可以参与事务
- 内存使用 — 大事务会占用大量临时文件空间
死锁预防
php
<?php
// 预防措施:
// 1. 设置合理的 busy_timeout
$db->busyTimeout(5000);
// 2. 保持事务简短
$db->exec('BEGIN IMMEDIATE');
try {
// 只做必要操作
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
}
// 3. 按固定顺序访问资源(如果涉及多表)WAL Checkpoint 管理
php
<?php
// WAL 文件会持续增长,需要定期 checkpoint
// 默认当 WAL 文件达到 1000 页时自动 checkpoint
// 手动触发 checkpoint
$db->exec('PRAGMA wal_checkpoint(TRUNCATE);');
// Checkpoint 模式:
// PASSIVE — 不阻塞,将部分 WAL 数据写入主数据库
// FULL — 等待所有写入者完成后合并
// RESTART — 等待所有写入者完成后重建 WAL 文件
// TRUNCATE — 截断 WAL 文件为零字节最佳实践
1. 事务配置模板
php
<?php
function configureSqliteDatabase(SQLite3 $db): void
{
$db->enableExceptions(true);
$db->busyTimeout(5000);
$db->exec('PRAGMA journal_mode = WAL;');
$db->exec('PRAGMA synchronous = NORMAL;');
$db->exec('PRAGMA foreign_keys = ON;');
$db->exec('PRAGMA cache_size = -8000;');
$db->exec('PRAGMA temp_store = MEMORY;');
$db->exec('PRAGMA wal_autocheckpoint = 1000;');
}2. 事务使用原则
php
<?php
// 原则1: 事务要尽可能短
// 原则2: 始终使用异常处理
// 原则3: WAL 模式下用 BEGIN IMMEDIATE 开始写事务
// 原则4: 设置 busy_timeout 避免立即失败
// 原则5: 高频写入考虑使用消息队列缓冲3. 并发替代方案
php
<?php
// 写入缓冲类 — 减少数据库写入频率
class WriteBuffer
{
private array $buffer = [];
private int $threshold;
public function __construct(int $threshold = 100)
{
$this->threshold = $threshold;
}
public function push(string $sql, array $params = []): void
{
$this->buffer[] = [$sql, $params];
if (count($this->buffer) >= $this->threshold) {
// 触发 flush — 需要在外部调用 flush()
$this->buffer = []; // 简化处理
}
}
public function flush(SQLite3 $db): void
{
if (empty($this->buffer)) {
return;
}
$db->exec('BEGIN IMMEDIATE');
try {
foreach ($this->buffer as [$sql, $params]) {
$stmt = $db->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
}
$db->exec('COMMIT');
$this->buffer = [];
} catch (Exception $e) {
$db->exec('ROLLBACK');
throw $e;
}
}
}