公司现有项目使用了LiteDB作为本地数据存储,但有较高的概率读取阻塞。
因为死锁或者损坏导致的阻塞问题,目前只能设置超时。在db读取超时后,部分情况可以删除文件、重建db解决,也有无法删除db文件的情况。
导致的技术债务造成了非常多的冗余维护工作量,需要基于常用的数据库及使用方式,重新做个技术选型确认
LiteDB,是一类NoSql的文档数据库,引用Nuget包LiteDB对接开发,社区litedb-org/LiteDB: LiteDB - A .NET NoSQL Document Store in a single data file
在Windows本地数据存储场景中主要有Sqlite、LiteDB、LocalDB几个主要选项
Windows本地数据库选型
.NET Windows 本地数据库中 SQLite、LiteDB、LocalDB 的对比,CodeX生成如下:
| 维度 | SQLite | LiteDB | LocalDB (SQL Server Express LocalDB) |
|---|---|---|---|
| 数据模型 | 关系型(SQL) | 文档型(BSON) | 关系型(SQL Server 子集) |
| 语言/协议 | SQL | 类 NoSQL API / LINQ | T‑SQL(完整 SQL Server 语法) |
| 部署 | 单文件,零安装 | 单文件,零安装 | 需安装 LocalDB runtime |
| 依赖 | SQLite 引擎 | 纯 .NET(无需 native) | SQL Server LocalDB 组件 |
| 体积/性能 | 极小、快 | 极小、快(适合小规模) | 较大、重 |
| 并发能力 | 多读单写 | 多读单写 | 多用户/多连接更强 |
| 事务 | 支持 | 支持 | 支持(完整) |
| ORM 支持 | 很成熟(EF Core) | 限制(非 EF) | 极好(EF Core) |
| 跨平台 | 完全跨平台 | 完全跨平台 | 仅 Windows |
| 典型使用场景 | 轻量关系型本地库 | 轻量文档型嵌入库 | 需要 SQL Server 兼容性的本地库 |
1. SQLite
特点:
- 单文件存储(关系型数据库),零安装
- SQL 语法,支持事务、索引、视图(有限)
- EF Core 支持成熟
- 高度跨平台(Windows、Linux、Mac、Mobile)
适合:
- 轻量关系型数据
- 需要 SQL / ORM 的桌面应用
- 高兼容+小体积优先
劣势:
- 并发写能力有限(多读单写)
- 缺少部分高级 SQL Server 特性
2. LiteDB
特点:
- 纯 .NET 嵌入式文档数据库(BSON)
- 不依赖 native DLL
- 类 MongoDB 的使用体验
- 单文件存储
适合:
- 非结构化/半结构化数据
- 简单应用配置、缓存、日志、轻量数据持久化
- 不想写 SQL
劣势:
- 不支持 EF Core
- 社区生态小于 SQLite
- 并发/事务能力相对弱一些
3. LocalDB(SQL Server LocalDB)
**特点: SQL Server Express 的轻量模式
- 完整 T‑SQL 语法
- 与 SQL Server 高度一致,便于迁移
- 支持丰富特性(存储过程、视图、触发器等)
适合:
- 开发/测试环境需要模拟 SQL Server
- 需要复杂 SQL、视图、存储过程
- 将来要迁移到 SQL Server 的桌面应用
劣势:
- 仅 Windows
- 需要安装 LocalDB 组件
- 体积大、启动相对慢
数据库选型建议
1. 死锁损坏问题
按上面收集的情况,litedb存在频繁的db死锁损坏问题
SQLite 是否也会卡死?对比分析
SQLite 不会出现 LiteDB 这种"卡死"问题。 原因如下:
-
SQLite 有内置的 busy_timeout 机制,写锁冲突时会自动等待+重试,超时后返回错误,不会无限阻塞
-
WAL 模式下读写不互相阻塞,只有写-写冲突
-
多个连接实例访问同一文件是 SQLite 的正常用法,而 LiteDB 在这种模式下就容易死锁
-
SQLite 的锁机制经过 20+ 年生产环境验证
根据已知的社区反馈,liteDb在并发读写这块有较多问题。LiteDB 的锁机制在高并发场景下天然脆弱,而 SQLite 的 WAL 模式能更好地支持并发读写,且生态更成熟、调试工具更丰富。
2.社区成熟度
考虑到社区成熟度的情况。LiteDb Github仓库已知大量死锁问题,Nuget引用量37.8M不算高;而Sqlite是windows客户端本地标准成熟的方案了

3.性能对比
拆成 5 个指标看:
- 冷启动延迟:SQLite/LiteDB 常更快;LocalDB 首次唤醒可能慢。
- 单条写入:SQLite/LiteDB 都可以很快;是否开事务影响巨大。
- 批量写入:SQLite 在"单事务 + 预编译语句"下通常非常强。
- 复杂查询:SQLite/LocalDB 通常明显优于 LiteDB。
- 并发读写:LocalDB 多并发能力更完整;SQLite 读并发强、写锁模型需设计;LiteDB 在高并发场更容易到瓶颈。
纯读写吞吐(尤其批量写):通常 SQLite ≥ LocalDB > LiteDB(具体取决于索引、事务、同步模式、数据模型)
所以大部分情况选用Sqlite。如果是其它小场景的需求,对象存储可以选文档型数据库LiteDB, 要兼容 SQL Server可以选LocalDB
Sqlite使用方式选型
.NET sqlte数据库支持包:
- Microsoft.EntityFrameworkCore.Sqlite
- Microsoft.Data.Sqlite
转换数据类有以下几种方式:
- Microsoft.EntityFrameworkCore
- Dapper
- SqlSugar
所以.NET读写数据库有几下方案:
| 方案 | 必需依赖(NuGet) | 使用方式概述 | 性能/开销 |
|---|---|---|---|
| EF Core + EFCore.Sqlite | Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.Sqlite | DbContext + LINQ + Migrations | 中(有跟踪/映射开销) |
| EF Core + Microsoft.Data.Sqlite(手写迁移SQL) | Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.Sqlite Microsoft.Data.Sqlite | DbContext + 手写SQL迁移/修表 | 中(可控性更高) |
| Dapper + Microsoft.Data.Sqlite | Dapper<br>Microsoft.Data.Sqlite | 手写SQL + 轻量映射 | 高(最轻薄) |
| SqlSugar + Microsoft.Data.Sqlite | SqlSugarCore<br>Microsoft.Data.Sqlite | ORM + CodeFirst/DbFirst | 中~高(配置得当) |
以下分别给出4种方案,完成.NET的数据库读写以及表迁移
数据库表迁移目标(V1 -> V2)
- V1 表:
Users(Id, Name, Email) - V2 表:
Users(Id, Name, Email, Age) - 迁移数据规则:给历史数据
Age设为18
EF Core + EFCore.Sqlite
EF Core,适合快速开发、团队熟悉 .NET 官方生态。但映射存在一定的性能开销
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Migrations;
3
4 var db = new AppDbContext();
5 db.Database.Migrate(); // 纯代码触发迁移
6
7 // 写
8 db.Users.Add(new User { Name = "Alice", Email = "alice@test.com", Age = 20 });
9 db.SaveChanges();
10
11 // 读
12 foreach (var u in db.Users.AsNoTracking())
13 {
14 Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
15 }
16
17 public class AppDbContext : DbContext
18 {
19 public DbSet<User> Users => Set<User>();
20
21 protected override void OnConfiguring(DbContextOptionsBuilder options)
22 => options.UseSqlite("Data Source=efcore_sqlite_demo.db");
23 }
24
25 public class User
26 {
27 public int Id { get; set; }
28 public string Name { get; set; } = "";
29 public string Email { get; set; } = "";
30 public int? Age { get; set; }
31 }
32
33 // ====== 迁移1:Init ======
34 [DbContext(typeof(AppDbContext))]
35 [Migration("202602260001_Init")]
36 public class Init : Migration
37 {
38 protected override void Up(MigrationBuilder migrationBuilder)
39 {
40 migrationBuilder.CreateTable(
41 name: "Users",
42 columns: table => new
43 {
44 Id = table.Column<int>(nullable: false)
45 .Annotation("Sqlite:Autoincrement", true),
46 Name = table.Column<string>(nullable: false),
47 Email = table.Column<string>(nullable: false)
48 },
49 constraints: table => table.PrimaryKey("PK_Users", x => x.Id));
50 }
51
52 protected override void Down(MigrationBuilder migrationBuilder)
53 => migrationBuilder.DropTable(name: "Users");
54 }
55
56 // ====== 迁移2:AddAgeAndBackfill ======
57 [DbContext(typeof(AppDbContext))]
58 [Migration("202602260002_AddAgeAndBackfill")]
59 public class AddAgeAndBackfill : Migration
60 {
61 protected override void Up(MigrationBuilder migrationBuilder)
62 {
63 migrationBuilder.AddColumn<int>(
64 name: "Age",
65 table: "Users",
66 nullable: true);
67
68 migrationBuilder.Sql("UPDATE Users SET Age = 18 WHERE Age IS NULL;");
69 }
70
71 protected override void Down(MigrationBuilder migrationBuilder)
72 => migrationBuilder.DropColumn(name: "Age", table: "Users");
73 }
EF Core + 补充手写sql
如果既想用 EF Core,又希望对数据库变更"强可控",则可以使用EF Core + Microsoft.Data.Sqlite
1 using Microsoft.Data.Sqlite;
2 using Microsoft.EntityFrameworkCore;
3
4 var connStr = "Data Source=efcore_manual_demo.db";
5 await MigrationRunner.MigrateAsync(connStr);
6
7 using var db = new AppDbContext(connStr);
8
9 // 写
10 db.Users.Add(new User { Name = "Alice", Email = "alice@test.com", Age = 20 });
11 db.SaveChanges();
12
13 // 读
14 foreach (var u in db.Users.AsNoTracking())
15 {
16 Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
17 }
18
19 public static class MigrationRunner
20 {
21 public static async Task MigrateAsync(string connStr)
22 {
23 await using var conn = new SqliteConnection(connStr);
24 await conn.OpenAsync();
25
26 // 版本表
27 var createVersion = conn.CreateCommand();
28 createVersion.CommandText = """
29 CREATE TABLE IF NOT EXISTS __schema_migrations (
30 version TEXT NOT NULL PRIMARY KEY,
31 applied_at TEXT NOT NULL
32 );
33 """;
34 await createVersion.ExecuteNonQueryAsync();
35
36 await ApplyIfNotExists(conn, "202602260001_Init", """
37 CREATE TABLE IF NOT EXISTS Users (
38 Id INTEGER PRIMARY KEY AUTOINCREMENT,
39 Name TEXT NOT NULL,
40 Email TEXT NOT NULL
41 );
42 """);
43
44 await ApplyIfNotExists(conn, "202602260002_AddAgeAndBackfill", """
45 ALTER TABLE Users ADD COLUMN Age INTEGER NULL;
46 UPDATE Users SET Age = 18 WHERE Age IS NULL;
47 """);
48 }
49
50 private static async Task ApplyIfNotExists(SqliteConnection conn, string version, string sql)
51 {
52 var check = conn.CreateCommand();
53 check.CommandText = "SELECT COUNT(1) FROM __schema_migrations WHERE version = $v";
54 check.Parameters.AddWithValue("$v", version);
55 var exists = Convert.ToInt32(await check.ExecuteScalarAsync()) > 0;
56 if (exists) return;
57
58 await using var tx = await conn.BeginTransactionAsync();
59 try
60 {
61 var cmd = conn.CreateCommand();
62 cmd.Transaction = tx;
63 cmd.CommandText = sql;
64 await cmd.ExecuteNonQueryAsync();
65
66 var ins = conn.CreateCommand();
67 ins.Transaction = tx;
68 ins.CommandText = """
69 INSERT INTO __schema_migrations(version, applied_at)
70 VALUES($v, $t);
71 """;
72 ins.Parameters.AddWithValue("$v", version);
73 ins.Parameters.AddWithValue("$t", DateTime.UtcNow.ToString("O"));
74 await ins.ExecuteNonQueryAsync();
75
76 await tx.CommitAsync();
77 }
78 catch (SqliteException ex) when (ex.Message.Contains("duplicate column name"))
79 {
80 await tx.RollbackAsync();
81 }
82 }
83 }
84
85 public class AppDbContext : DbContext
86 {
87 private readonly string _connStr;
88 public AppDbContext(string connStr) => _connStr = connStr;
89 public DbSet<User> Users => Set<User>();
90 protected override void OnConfiguring(DbContextOptionsBuilder options)
91 => options.UseSqlite(_connStr);
92 }
93
94 public class User
95 {
96 public int Id { get; set; }
97 public string Name { get; set; } = "";
98 public string Email { get; set; } = "";
99 public int? Age { get; set; }
100 }
Dapper + Microsoft.Data.Sqlite
适合性能优先、SQL 可控优先、追求轻量。这类开销低、速度快、透明 SQL;适合高频读写和明确数据模型。但缺点很明显,sql量太多了
1 using Dapper;
2 using Microsoft.Data.Sqlite;
3
4 var connStr = "Data Source=dapper_demo.db";
5 using var conn = new SqliteConnection(connStr);
6 conn.Open();
7
8 Migrate(conn);
9
10 // 写
11 conn.Execute(
12 "INSERT INTO Users(Name, Email, Age) VALUES (@Name, @Email, @Age);",
13 new { Name = "Alice", Email = "alice@test.com", Age = 20 });
14
15 // 读
16 var users = conn.Query<User>("SELECT Id, Name, Email, Age FROM Users ORDER BY Id;").ToList();
17 foreach (var u in users)
18 {
19 Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
20 }
21
22 static void Migrate(SqliteConnection conn)
23 {
24 conn.Execute("""
25 CREATE TABLE IF NOT EXISTS __schema_migrations (
26 version TEXT NOT NULL PRIMARY KEY,
27 applied_at TEXT NOT NULL
28 );
29 """);
30
31 Apply(conn, "202602260001_Init", """
32 CREATE TABLE IF NOT EXISTS Users (
33 Id INTEGER PRIMARY KEY AUTOINCREMENT,
34 Name TEXT NOT NULL,
35 Email TEXT NOT NULL
36 );
37 """);
38
39 Apply(conn, "202602260002_AddAgeAndBackfill", """
40 ALTER TABLE Users ADD COLUMN Age INTEGER NULL;
41 UPDATE Users SET Age = 18 WHERE Age IS NULL;
42 """);
43 }
44
45 static void Apply(SqliteConnection conn, string version, string sql)
46 {
47 var exists = conn.ExecuteScalar<long>(
48 "SELECT COUNT(1) FROM __schema_migrations WHERE version=@v", new { v = version }) > 0;
49 if (exists) return;
50
51 using var tx = conn.BeginTransaction();
52 try
53 {
54 conn.Execute(sql, transaction: tx);
55 conn.Execute("""
56 INSERT INTO __schema_migrations(version, applied_at)
57 VALUES(@v, @t)
58 """, new { v = version, t = DateTime.UtcNow.ToString("O") }, tx);
59
60 tx.Commit();
61 }
62 catch (SqliteException ex) when (ex.Message.Contains("duplicate column name"))
63 {
64 tx.Rollback();
65 }
66 }
67
68 public class User
69 {
70 public long Id { get; set; }
71 public string Name { get; set; } = "";
72 public string Email { get; set; } = "";
73 public int? Age { get; set; }
74 }
SqlSugar + Microsoft.Data.Sqlite
上手快,功能集成度高(CodeFirst/DbFirst 等)。如果是数据库表结构经常变动,建议使用这个方案,CodeFrist开发非常便捷
1 using SqlSugar;
2
3 var db = new SqlSugarClient(new ConnectionConfig
4 {
5 ConnectionString = "Data Source=sqlsugar_demo.db",
6 DbType = DbType.Sqlite,
7 IsAutoCloseConnection = true,
8 InitKeyType = InitKeyType.Attribute
9 });
10
11 Migrate(db);
12
13 // 写
14 db.Insertable(new User { Name = "Alice", Email = "alice@test.com", Age = 20 }).ExecuteCommand();
15
16 // 读
17 var list = db.Queryable<User>().OrderBy(x => x.Id).ToList();
18 foreach (var u in list)
19 {
20 Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
21 }
22
23 static void Migrate(SqlSugarClient db)
24 {
25 db.Ado.ExecuteCommand("""
26 CREATE TABLE IF NOT EXISTS __schema_migrations (
27 version TEXT NOT NULL PRIMARY KEY,
28 applied_at TEXT NOT NULL
29 );
30 """);
31
32 Apply(db, "202602260001_Init", """
33 CREATE TABLE IF NOT EXISTS Users (
34 Id INTEGER PRIMARY KEY AUTOINCREMENT,
35 Name TEXT NOT NULL,
36 Email TEXT NOT NULL
37 );
38 """);
39
40 Apply(db, "202602260002_AddAgeAndBackfill", """
41 ALTER TABLE Users ADD COLUMN Age INTEGER NULL;
42 UPDATE Users SET Age = 18 WHERE Age IS NULL;
43 """);
44 }
45
46 static void Apply(SqlSugarClient db, string version, string sql)
47 {
48 var exists = db.Ado.GetInt("""
49 SELECT COUNT(1) FROM __schema_migrations WHERE version=@v
50 """, new { v = version }) > 0;
51
52 if (exists) return;
53
54 db.Ado.BeginTran();
55 try
56 {
57 db.Ado.ExecuteCommand(sql);
58 db.Ado.ExecuteCommand("""
59 INSERT INTO __schema_migrations(version, applied_at)
60 VALUES(@v, @t)
61 """, new { v = version, t = DateTime.UtcNow.ToString("O") });
62
63 db.Ado.CommitTran();
64 }
65 catch
66 {
67 db.Ado.RollbackTran();
68 }
69 }
70
71 [SugarTable("Users")]
72 public class User
73 {
74 [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
75 public int Id { get; set; }
76 public string Name { get; set; } = "";
77 public string Email { get; set; } = "";
78 public int? Age { get; set; }
79 }
如果使用SqlSugar的已封装CodeFrist方案,迁移数据表会更简单:
1 using SqlSugar;
2
3 var db = new SqlSugarClient(new ConnectionConfig
4 {
5 ConnectionString = "Data Source=app.db",
6 DbType = DbType.Sqlite,
7 IsAutoCloseConnection = true,
8 InitKeyType = InitKeyType.Attribute,
9 ConfigureExternalServices = new ConfigureExternalServices
10 {
11 EntityService = (prop, col) =>
12 {
13 // 可选:统一处理字符串长度等
14 if (prop.PropertyType == typeof(string) && col.Length == 0)
15 col.Length = 200;
16 }
17 }
18 });
19
20 // 1) CodeFirst 建表/补字段
21 db.CodeFirst.InitTables<User>();
22
23 // 2) 如需"迁移数据"(例如给新字段Age回填),用.NET代码执行SQL
24 db.Ado.ExecuteCommand("UPDATE Users SET Age = 18 WHERE Age IS NULL;");
25
26 // 3) 写入
27 db.Insertable(new User
28 {
29 Name = "Alice",
30 Email = "alice@test.com",
31 Age = 20
32 }).ExecuteCommand();
33
34 // 4) 读取
35 var users = db.Queryable<User>().OrderBy(x => x.Id).ToList();
36 foreach (var u in users)
37 {
38 Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
39 }
40
41 [SugarTable("Users")]
42 public class User
43 {
44 [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
45 public int Id { get; set; }
46
47 [SugarColumn(Length = 100, IsNullable = false)]
48 public string Name { get; set; } = string.Empty;
49
50 [SugarColumn(Length = 200, IsNullable = false)]
51 public string Email { get; set; } = string.Empty;
52
53 // 新增字段:CodeFirst会尝试补列
54 [SugarColumn(IsNullable = true)]
55 public int? Age { get; set; }
56 }
但要注意:
InitTables<T>() 主要用于建表/补字段,复杂变更(改列类型、重命名列、删列、数据搬迁)通常仍需你手动 SQL 或版本脚本。
Sugar的几种操作方式,
CodeFirst:db.CodeFirst.InitTables<User>()
ORM 的 CRUD/表达式 API:Insertable / Updateable / Queryable
原生 SQL 执行:db.Ado.ExecuteCommand("UPDATE ...")
所以,个人建议使用SqlSugar方案,CodeFirst数据表字段补全真的非常适合表结构变动,ORM链式操作提供了便捷的读写操作。当然如果需要提升读写性能,也可以通过纯sql语句来替换Insertable、Updateable、Queryable操作