SQLite.NET Async+FileSystemWatcher批量插入失效问题求助
解决大量文件同步时SQLiteAsync写入记录丢失的问题
我之前也碰到过类似的FileSystemWatcher结合SQLiteAsync批量操作的坑,咱们一步步来排查和解决:
先聊聊可能的核心原因
- FileSystemWatcher缓冲区溢出:默认8KB的缓冲区在大量文件操作时根本不够,直接导致部分事件丢失,自然不会写入数据库。
- 并发异步写入冲突:SQLite是文件级锁,大量单个异步插入请求会互相竞争锁资源,很多操作会悄悄失败(如果没捕获异常的话)。
- 异步操作未正确等待:如果你的代码是直接触发
InsertAsync()却不await,这些操作会被当作“火并忘记”的任务,很容易被系统回收或失败。
针对性解决方案
1. 先给FileSystemWatcher扩容缓冲区
这是最容易忽略但见效最快的一步,把缓冲区调到64KB(32位进程上限)或1MB(64位),避免事件丢失:
var syncWatcher = new FileSystemWatcher(@"D:\SyncDir"); syncWatcher.BufferSize = 1024 * 64; // 设置为64KB // 别忘了订阅必要的事件:Created、Renamed、Deleted等 syncWatcher.Created += OnFileOrFolderCreated; syncWatcher.Renamed += OnFileOrFolderRenamed; syncWatcher.Deleted += OnFileOrFolderDeleted; syncWatcher.EnableRaisingEvents = true;
同时一定要订阅Error事件,排查缓冲区溢出问题:
syncWatcher.Error += (sender, e) => { var ex = e.GetException(); if (ex is InternalBufferOverflowException) { Console.WriteLine("警告:Watcher缓冲区溢出!请增大BufferSize值"); } else { Console.WriteLine($"Watcher错误:{ex.Message}"); } };
2. 批量收集+事务写入,避免并发冲突
不要每个文件事件都单独插数据库,而是先把路径存到线程安全队列,再定期批量用事务写入:
第一步:定义线程安全队列和后台处理任务
// 线程安全队列,用来暂存待写入的路径 private readonly ConcurrentQueue<SyncOperation> _pendingOperations = new ConcurrentQueue<SyncOperation>(); // 取消令牌,用于程序关闭时停止后台任务 private CancellationTokenSource _cts; // 自定义类存储操作类型(创建/删除/重命名)和路径 public class SyncOperation { public string FilePath { get; set; } public OperationType Type { get; set; } } public enum OperationType { Create, Delete, Rename, Copy }
第二步:事件回调只做入队操作
private void OnFileOrFolderCreated(object sender, FileSystemEventArgs e) { _pendingOperations.Enqueue(new SyncOperation { FilePath = e.FullPath, Type = OperationType.Create }); } // 其他事件(Renamed/Deleted)同理,对应不同的OperationType
第三步:启动后台批量处理任务
在程序初始化时启动这个任务,比如Form的Load事件里:
_cts = new CancellationTokenSource(); _ = ProcessPendingOperationsAsync(_cts.Token); // 后台运行,用_避免编译器警告
然后实现批量处理逻辑,用事务一次性写入:
private async Task ProcessPendingOperationsAsync(CancellationToken token) { while (!token.IsCancellationRequested) { // 每次批量取50条,可根据实际调整 var batch = new List<SyncOperation>(); while (_pendingOperations.TryDequeue(out var op) && batch.Count < 50) { batch.Add(op); } if (batch.Any()) { try { using (var conn = new SQLiteAsyncConnection("SyncRecords.db")) { // 用事务批量写入,提升效率并避免锁冲突 await conn.RunInTransactionAsync(async tx => { foreach (var op in batch) { // 根据操作类型执行不同SQL,这里以插入创建记录为例 await tx.ExecuteAsync( "INSERT INTO SyncLogs (FilePath, OperationType, SyncStatus) VALUES (@path, @type, 0)", new { path = op.FilePath, type = op.Type.ToString() } ); } }); } Console.WriteLine($"成功批量写入{batch.Count}条记录"); } catch (Exception ex) { Console.WriteLine($"批量写入失败:{ex.Message}"); // 失败的记录重新入队,避免丢失(可限制重试次数防止死循环) foreach (var op in batch) { _pendingOperations.Enqueue(op); } } } // 空转时等待100ms,减少CPU占用 await Task.Delay(100, token); } }
第四步:程序关闭时清理任务
在Form的Closing事件里:
private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { _cts?.Cancel(); // 等待后台任务完成,确保所有待处理记录都写入 // 这里可以加个等待逻辑,比如用Task.Wait()或者显示加载提示 }
3. 确保SQLiteAsync版本和配置正确
- 升级到最新的
sqlite-net-pcl包(SQLiteAsync常用的NuGet包),旧版本可能存在异步操作的bug。 - 开启WAL模式提升并发性能,连接字符串里加上
Journal Mode=WAL:
var conn = new SQLiteAsyncConnection("SyncRecords.db", SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache); await conn.ExecuteAsync("PRAGMA journal_mode=WAL;");
额外测试建议
打开SQLite的日志功能,查看具体的数据库操作情况,方便排查隐藏问题:
// 在初始化连接后执行 await conn.ExecuteAsync("PRAGMA logging=full;");
然后可以通过SQLite的工具(比如DB Browser for SQLite)查看日志,看看有没有插入失败的语句。
内容的提问来源于stack exchange,提问作者Ashish Tripathi
相关产品推荐
相关产品推荐

