Skip to content

SQLite 并发控制

并发控制概述

并发控制是数据库管理系统的核心功能之一,用于协调多个并发事务对共享数据的访问。SQLite 采用多种机制实现并发控制,包括锁机制、预写日志(WAL)模式和事务隔离级别。

SQLite 锁机制

锁类型

SQLite 使用五种不同级别的锁,从低到高依次为:

  1. SHARED(共享锁):用于读取操作,允许多个事务同时读取数据库
  2. RESERVED(保留锁):表示事务准备写入数据库,此时仍允许其他事务读取
  3. PENDING(挂起锁):表示事务即将写入数据库,不允许新的读取事务开始,但已有的读取事务可以继续
  4. EXCLUSIVE(排他锁):用于写入操作,完全锁定数据库,不允许其他任何事务访问
  5. UNLOCKED(未锁定):表示事务未持有任何锁

锁升级过程

事务执行过程中,锁会按照以下顺序自动升级:

  1. 读取操作开始时,获取 SHARED 锁
  2. 写入操作开始前,升级到 RESERVED 锁
  3. 实际写入数据前,升级到 PENDING 锁
  4. 等待所有现有读取事务完成后,升级到 EXCLUSIVE 锁
  5. 事务提交或回滚后,释放所有锁

锁状态查看

可以使用 PRAGMA locking_mode 查看和设置数据库的锁定模式:

sql
-- 查看当前锁定模式
PRAGMA locking_mode;

-- 设置为 NORMAL 锁定模式(默认)
PRAGMA locking_mode = NORMAL;

-- 设置为 EXCLUSIVE 锁定模式
PRAGMA locking_mode = EXCLUSIVE;

WAL 模式

WAL 模式概述

预写日志(Write-Ahead Logging,WAL)是 SQLite 3.7.0 引入的一种新的日志模式,显著提高了并发性能。在 WAL 模式下,写入操作不会直接修改数据库文件,而是先写入一个单独的 WAL 文件。

WAL 模式的优势

  • 读-写并发:读取操作可以在写入操作进行时继续,不需要等待写入完成
  • 写-写序列化:多个写入操作仍然是串行的,但它们对读取操作的影响最小化
  • 更快的写入速度:写入操作只需要追加到 WAL 文件,不需要修改数据库文件的多个页面
  • 更好的崩溃恢复:WAL 文件包含完整的事务日志,恢复过程更可靠

启用 WAL 模式

sql
-- 启用 WAL 模式
PRAGMA journal_mode = WAL;

-- 查看当前日志模式
PRAGMA journal_mode;

WAL 模式相关参数

参数描述默认值
wal_autocheckpoint自动检查点的 WAL 文件大小阈值(页)1000
wal_checkpoint手动执行检查点-
wal_synchronousWAL 模式下的同步级别FULL

检查点操作

检查点是将 WAL 文件中的修改合并到数据库文件的过程:

sql
-- 执行检查点
PRAGMA wal_checkpoint;

-- 执行完全检查点(等待所有读取者完成)
PRAGMA wal_checkpoint(FULL);

-- 执行截断检查点(合并并截断 WAL 文件)
PRAGMA wal_checkpoint(TRUNCATE);

-- 执行 PASSIVE 检查点(不等待任何操作)
PRAGMA wal_checkpoint(PASSIVE);

事务隔离级别

SQLite 支持四种事务隔离级别,用于控制并发事务之间的可见性:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不可能可能可能
REPEATABLE READ不可能不可能可能
SERIALIZABLE不可能不可能不可能

设置隔离级别

sql
-- 设置为 READ COMMITTED 隔离级别
PRAGMA read_uncommitted = 0;
PRAGMA journal_mode = WAL;

-- 设置为 READ UNCOMMITTED 隔离级别(不推荐)
PRAGMA read_uncommitted = 1;

并发控制策略

1. 选择合适的日志模式

  • DELETE 模式:传统日志模式,写入时锁定数据库,适合单用户或低并发场景
  • WAL 模式:现代日志模式,支持高并发读-写操作,适合多用户场景

2. 优化事务设计

  • 保持事务简短:减少锁持有时间
  • 避免长时间读事务:特别是在 DELETE 日志模式下
  • 使用批量操作:减少事务次数和锁升级频率

3. 合理设置隔离级别

  • 对于需要最高数据一致性的场景,使用默认的 SERIALIZABLE 隔离级别
  • 对于高并发场景,如果业务允许,可以考虑使用 READ COMMITTED 隔离级别

4. 优化查询性能

  • 创建合适的索引,减少查询执行时间
  • 避免全表扫描,减少锁持有时间
  • 使用 ANALYZE 命令更新统计信息,帮助查询优化器生成更好的执行计划

版本差异

SQLite 3.7.0 及以上

  • 引入 WAL 模式,支持读-写并发
  • 改进了锁机制,减少锁争用

SQLite 3.8.0 及以上

  • 优化了 WAL 模式的性能
  • 支持 wal_autocheckpoint 参数,自动管理检查点

SQLite 3.11.0 及以上

  • 引入 wal_checkpoint 命令,支持更灵活的检查点操作
  • 改进了锁升级算法,减少死锁可能性

SQLite 3.22.0 及以上

  • 支持 PRAGMA wal_synchronous 参数,允许调整 WAL 模式下的同步级别
  • 优化了 WAL 文件的管理,减少磁盘空间使用

并发问题与解决方案

1. 锁争用

症状:事务执行缓慢,出现大量锁等待

解决方案

  • 切换到 WAL 模式
  • 保持事务简短
  • 优化查询,减少锁持有时间
  • 使用批量操作
  • 考虑分片或分区(如果数据量很大)

2. 死锁

症状:事务陷入无限等待,无法完成

解决方案

  • 保持事务简短
  • 按照固定顺序访问资源
  • 避免嵌套事务
  • 设置合理的超时机制

3. 写入饥饿

症状:写入事务长时间等待,无法获得排他锁

解决方案

  • 使用 WAL 模式,减少读-写冲突
  • 合理设置 wal_autocheckpoint 参数
  • 定期手动执行检查点
  • 考虑限制同时进行的读取事务数量

4. WAL 文件过大

症状:WAL 文件大小持续增长,占用过多磁盘空间

解决方案

  • 减小 wal_autocheckpoint 参数值
  • 定期手动执行检查点
  • 使用 PRAGMA wal_checkpoint(TRUNCATE) 截断 WAL 文件
  • 考虑使用 VACUUM 命令重组数据库

常见问题(FAQ)

Q: WAL 模式下,读取操作会锁定数据库吗?

A: 不会。在 WAL 模式下,读取操作直接从数据库文件读取,不需要锁定 WAL 文件,因此可以与写入操作并发执行。

Q: 如何确定是否应该使用 WAL 模式?

A: 以下情况适合使用 WAL 模式:

  • 有大量并发读-写操作
  • 读取操作远多于写入操作
  • 对写入性能要求较高
  • 需要支持多个连接同时访问数据库

Q: WAL 模式会影响数据安全性吗?

A: 不会。WAL 模式同样保证 ACID 特性,只是日志的实现方式不同。可以通过调整 wal_synchronous 参数来平衡性能和安全性。

Q: 多个连接可以同时写入 SQLite 数据库吗?

A: 在任何模式下,SQLite 都只允许一个连接同时写入数据库。但在 WAL 模式下,写入操作对读取操作的影响最小化,多个读取连接可以在写入操作进行时继续工作。

Q: 如何监控锁争用情况?

A:

  • 使用 PRAGMA busy_timeout 设置超时时间,超时后会返回错误
  • 使用 SQLite 的内置性能监控功能
  • 检查应用程序日志中的锁等待相关错误
  • 使用第三方监控工具,如 SQLite Expert 或 DB Browser for SQLite

生产运维建议

  1. 默认使用 WAL 模式:对于大多数应用场景,WAL 模式提供更好的并发性能
  2. 合理设置 WAL 参数:根据业务需求调整 wal_autocheckpointwal_synchronous 参数
  3. 定期执行检查点:特别是在写入频繁的场景下
  4. 监控 WAL 文件大小:设置告警,防止 WAL 文件过大
  5. 测试并发性能:在生产环境部署前,进行充分的并发性能测试
  6. 考虑使用连接池:对于高并发场景,使用连接池管理数据库连接
  7. 备份 WAL 文件:确保 WAL 文件与数据库文件一起备份,以便在恢复时使用

通过合理配置和优化,可以充分发挥 SQLite 在并发场景下的性能优势,满足大多数应用的需求。