公司现有项目使用了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生成如下:
维度SQLiteLiteDBLocalDB (SQL Server Express LocalDB)数据模型关系型(SQL)文档型(BSON)关系型(SQL Server 子集)语言/协议SQL类 NoSQL API / LINQT‑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 这种"卡死"问题。 原因如下:
1. SQLite 有内置的 busy_timeout 机制,写锁冲突时会自动等待+重试,超时后返回错误,不会无限阻塞
2. WAL 模式下读写不互相阻塞,只有写-写冲突
3. 多个连接实例访问同一文件是 SQLite 的正常用法,而 LiteDB 在这种模式下就容易死锁
4. 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.SqliteMicrosoft.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.SqliteDapper
Microsoft.Data.Sqlite手写SQL + 轻量映射高(最轻薄)SqlSugar + Microsoft.Data.SqliteSqlSugarCore
Microsoft.Data.SqliteORM + 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() 主要用于建表/补字段,复杂变更(改列类型、重命名列、删列、数据搬迁)通常仍需你手动 SQL 或版本脚本。
Sugar的几种操作方式,
CodeFirst:db.CodeFirst.InitTables()
ORM 的 CRUD/表达式 API:Insertable / Updateable / Queryable
原生 SQL 执行:db.Ado.ExecuteCommand("UPDATE ...")
所以,个人建议使用SqlSugar方案,CodeFirst数据表字段补全真的非常适合表结构变动,ORM链式操作提供了便捷的读写操作。当然如果需要提升读写性能,也可以通过纯sql语句来替换Insertable、Updateable、Queryable操作
出处:http://www.cnblogs.com/kybs0/让学习成为习惯,假设明天就有重大机遇等着你,你准备好了么本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |