转载自:C# 下 SQLite 并发操作与锁库问题的 5 种解决方案_51CTO博客_sqlcipher c#
SQLite是轻量级的数据库,可用于嵌入式设备,仅需几百KB的内存即可工作,整个数据库存储在单一文件中,便于管理,迁移,备份。无需繁琐配置。
轻量高性能必然带来一定的局限,这次遇到的就是SQLite数据库的并发操作问题
关键点:在多线程或多进程并发访问的场景下,同一时刻仅允许单个线程进行写入操作。
现象:database is locked 错误
问题描述:在一个线程正在执行写入操作的过程中,另一个线程的写入请求只能被迫等待,等待时间过长超过系统预设的超时时间(通常5s可更改)就会触发此错误。
解决方案
1. 读写锁
2. 事务
3. WAL模式
4. 连接池
5. 多线程模式
详细说明
1. 读写锁
确保资源不被争抢导致崩溃
读写锁
using System.Threading;public class SqliteDataManager
{//ReaderWriterLockSlim类提供读写锁private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim;private static string _connString = "Data Source=mydb.db;Version=3;";// 读操作(允许多线程并发)public void ReadData{_rwLock.EnterReadLock; // 获取读锁try{using (var conn = new SQLiteConnection(_connString)){conn.Open;var cmd = conn.CreateCommand;cmd.CommandText = "SELECT * FROM MyTable";using (var reader = cmd.ExecuteReader){while (reader.Read) { /* 读取数据 */ }}}}finally{_rwLock.ExitReadLock; // 必须在finally中释放}}// 写操作(独占锁)public void WriteData(string value){_rwLock.EnterWriteLock; // 获取写锁try{using (var conn = new SQLiteConnection(_connString)){conn.Open;var cmd = conn.CreateCommand;cmd.CommandText = "INSERT INTO MyTable VALUES (@Value)";cmd.Parameters.AddWithValue("@Value", value);cmd.ExecuteNonQuery;}}finally{_rwLock.ExitWriteLock; // 必须释放}}
}
可升级为写锁的读锁,可减少锁竞争,避免重复加锁
可升级为写锁的读锁
public sealed class SqliteConnectionManager
{private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim;private static SQLiteConnection _conn;public static SQLiteConnection GetConnection{_lock.EnterUpgradeableReadLock; // 可升级为写锁的读锁try{if (_conn == null){_lock.EnterWriteLock; // 升级为写锁try{if (_conn == null){_conn = new SQLiteConnection("Data Source=mydb.db");_conn.Open;}}finally { _lock.ExitWriteLock; }}return _conn;}finally { _lock.ExitUpgradeableReadLock; }}
}
具体使用
查看代码
using System;
using System.Data.SQLite;
using System.Threading;class DatabaseManager
{private static SQLiteConnection _connection;private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();public static SQLiteConnection GetConnection(){if(_connection==null){_lock.EnterWriteLock;try{if(_connection == null){_connection=new SQLiteConnection("Data Source=database.db");}}finally{_lock.ExitWriteLock();}return _connection;}public static void InsertUser(string name) { var connection = GetConnection();_lock.EnterWriteLock(); try { //写锁中使用了事务using (var transaction = connection.BeginTransaction()) { try { using (var command = new SQLiteCommand(connection)) { command.CommandText = "INSERT INTO Users (Name) VALUES (@name)"; command.Parameters.AddWithValue("@name", name); command.ExecuteNonQuery(); } transaction.Commit(); } catch (Exception ex) { transaction.Rollback(); } } } finally { _lock.ExitWriteLock(); }}public static void SelectUsers() { var connection = GetConnection(); _lock.EnterReadLock(); try { using (var command = new SQLiteCommand("SELECT * FROM Users", connection)) { using (var reader = command.ExecuteReader()) { while (reader.Read()) { Console.WriteLine(reader["Name"]); } } } } finally { _lock.ExitReadLock(); }}}}
}
2.事务
保障数据完整性,事务具有原子性,内部的所有操作要么成功要么一起失败并回滚到插入之前的状态。一般只在读的时候使用事务。
在写入时使用事务
public static void InsertUser(string name)
{ var connection = GetConnection(); using (var transaction = connection.BeginTransaction()) { try { using (var command = new SQLiteCommand(connection)) { command.CommandText = "INSERT INTO Users (Name) VALUES (@name)"; command.Parameters.AddWithValue("@name", name); command.ExecuteNonQuery(); } transaction.Commit(); } catch (Exception ex) { transaction.Rollback(); }}
}
3. WAL模式
Write-Ahead Logging
_connection = new SQLiteConnection("Data Source=database.db;Journal Mode=WAL");
在连接字符串中,加入Journal Mode =WAL即可开启WAL模式,此模式可读写并行
原理:将写入的数据暂存到WAL文件中,在这个过程中主数据库文件依然可以对外提供读服务,当文件满足一定条件(如WAL文件大小达到阈值、事务提交等)时,数据库引擎在后台将WAL文件中的数据合并到主数据库文件中。
4. 连接池
_connection = new SQLiteConnection("Data Source=database.db;Max Pool Size=100;Pooling=True");
预先创建一定数量的数据库连接,减少连接创建与销毁开销,但需要合理配置连接池参数,否则可能出现连接泄露或资源浪费