找回密码
 立即注册
首页 业界区 业界 .NET 本地Db数据库-技术方案选型

.NET 本地Db数据库-技术方案选型

酒跚骼 昨天 21:50
公司现有项目使用了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客户端本地标准成熟的方案了
2.png

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. 1 using Microsoft.EntityFrameworkCore;
  2. 2 using Microsoft.EntityFrameworkCore.Migrations;
  3. 3
  4. 4 var db = new AppDbContext();
  5. 5 db.Database.Migrate(); // 纯代码触发迁移
  6. 6
  7. 7 // 写
  8. 8 db.Users.Add(new User { Name = "Alice", Email = "alice@test.com", Age = 20 });
  9. 9 db.SaveChanges();
  10. 10
  11. 11 // 读
  12. 12 foreach (var u in db.Users.AsNoTracking())
  13. 13 {
  14. 14     Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
  15. 15 }
  16. 16
  17. 17 public class AppDbContext : DbContext
  18. 18 {
  19. 19     public DbSet<User> Users => Set<User>();
  20. 20
  21. 21     protected override void OnConfiguring(DbContextOptionsBuilder options)
  22. 22         => options.UseSqlite("Data Source=efcore_sqlite_demo.db");
  23. 23 }
  24. 24
  25. 25 public class User
  26. 26 {
  27. 27     public int Id { get; set; }
  28. 28     public string Name { get; set; } = "";
  29. 29     public string Email { get; set; } = "";
  30. 30     public int? Age { get; set; }
  31. 31 }
  32. 32
  33. 33 // ====== 迁移1:Init ======
  34. 34 [DbContext(typeof(AppDbContext))]
  35. 35 [Migration("202602260001_Init")]
  36. 36 public class Init : Migration
  37. 37 {
  38. 38     protected override void Up(MigrationBuilder migrationBuilder)
  39. 39     {
  40. 40         migrationBuilder.CreateTable(
  41. 41             name: "Users",
  42. 42             columns: table => new
  43. 43             {
  44. 44                 Id = table.Column<int>(nullable: false)
  45. 45                     .Annotation("Sqlite:Autoincrement", true),
  46. 46                 Name = table.Column<string>(nullable: false),
  47. 47                 Email = table.Column<string>(nullable: false)
  48. 48             },
  49. 49             constraints: table => table.PrimaryKey("PK_Users", x => x.Id));
  50. 50     }
  51. 51
  52. 52     protected override void Down(MigrationBuilder migrationBuilder)
  53. 53         => migrationBuilder.DropTable(name: "Users");
  54. 54 }
  55. 55
  56. 56 // ====== 迁移2:AddAgeAndBackfill ======
  57. 57 [DbContext(typeof(AppDbContext))]
  58. 58 [Migration("202602260002_AddAgeAndBackfill")]
  59. 59 public class AddAgeAndBackfill : Migration
  60. 60 {
  61. 61     protected override void Up(MigrationBuilder migrationBuilder)
  62. 62     {
  63. 63         migrationBuilder.AddColumn<int>(
  64. 64             name: "Age",
  65. 65             table: "Users",
  66. 66             nullable: true);
  67. 67
  68. 68         migrationBuilder.Sql("UPDATE Users SET Age = 18 WHERE Age IS NULL;");
  69. 69     }
  70. 70
  71. 71     protected override void Down(MigrationBuilder migrationBuilder)
  72. 72         => migrationBuilder.DropColumn(name: "Age", table: "Users");
  73. 73 }
复制代码
EF Core + 补充手写sql

如果既想用 EF Core,又希望对数据库变更“强可控”,则可以使用EF Core + Microsoft.Data.Sqlite
  1.   1 using Microsoft.Data.Sqlite;
  2.   2 using Microsoft.EntityFrameworkCore;
  3.   3
  4.   4 var connStr = "Data Source=efcore_manual_demo.db";
  5.   5 await MigrationRunner.MigrateAsync(connStr);
  6.   6
  7.   7 using var db = new AppDbContext(connStr);
  8.   8
  9.   9 // 写
  10. 10 db.Users.Add(new User { Name = "Alice", Email = "alice@test.com", Age = 20 });
  11. 11 db.SaveChanges();
  12. 12
  13. 13 // 读
  14. 14 foreach (var u in db.Users.AsNoTracking())
  15. 15 {
  16. 16     Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
  17. 17 }
  18. 18
  19. 19 public static class MigrationRunner
  20. 20 {
  21. 21     public static async Task MigrateAsync(string connStr)
  22. 22     {
  23. 23         await using var conn = new SqliteConnection(connStr);
  24. 24         await conn.OpenAsync();
  25. 25
  26. 26         // 版本表
  27. 27         var createVersion = conn.CreateCommand();
  28. 28         createVersion.CommandText = """
  29. 29             CREATE TABLE IF NOT EXISTS __schema_migrations (
  30. 30                 version TEXT NOT NULL PRIMARY KEY,
  31. 31                 applied_at TEXT NOT NULL
  32. 32             );
  33. 33             """;
  34. 34         await createVersion.ExecuteNonQueryAsync();
  35. 35
  36. 36         await ApplyIfNotExists(conn, "202602260001_Init", """
  37. 37             CREATE TABLE IF NOT EXISTS Users (
  38. 38                 Id INTEGER PRIMARY KEY AUTOINCREMENT,
  39. 39                 Name TEXT NOT NULL,
  40. 40                 Email TEXT NOT NULL
  41. 41             );
  42. 42             """);
  43. 43
  44. 44         await ApplyIfNotExists(conn, "202602260002_AddAgeAndBackfill", """
  45. 45             ALTER TABLE Users ADD COLUMN Age INTEGER NULL;
  46. 46             UPDATE Users SET Age = 18 WHERE Age IS NULL;
  47. 47             """);
  48. 48     }
  49. 49
  50. 50     private static async Task ApplyIfNotExists(SqliteConnection conn, string version, string sql)
  51. 51     {
  52. 52         var check = conn.CreateCommand();
  53. 53         check.CommandText = "SELECT COUNT(1) FROM __schema_migrations WHERE version = $v";
  54. 54         check.Parameters.AddWithValue("$v", version);
  55. 55         var exists = Convert.ToInt32(await check.ExecuteScalarAsync()) > 0;
  56. 56         if (exists) return;
  57. 57
  58. 58         await using var tx = await conn.BeginTransactionAsync();
  59. 59         try
  60. 60         {
  61. 61             var cmd = conn.CreateCommand();
  62. 62             cmd.Transaction = tx;
  63. 63             cmd.CommandText = sql;
  64. 64             await cmd.ExecuteNonQueryAsync();
  65. 65
  66. 66             var ins = conn.CreateCommand();
  67. 67             ins.Transaction = tx;
  68. 68             ins.CommandText = """
  69. 69                 INSERT INTO __schema_migrations(version, applied_at)
  70. 70                 VALUES($v, $t);
  71. 71                 """;
  72. 72             ins.Parameters.AddWithValue("$v", version);
  73. 73             ins.Parameters.AddWithValue("$t", DateTime.UtcNow.ToString("O"));
  74. 74             await ins.ExecuteNonQueryAsync();
  75. 75
  76. 76             await tx.CommitAsync();
  77. 77         }
  78. 78         catch (SqliteException ex) when (ex.Message.Contains("duplicate column name"))
  79. 79         {
  80. 80             await tx.RollbackAsync();
  81. 81         }
  82. 82     }
  83. 83 }
  84. 84
  85. 85 public class AppDbContext : DbContext
  86. 86 {
  87. 87     private readonly string _connStr;
  88. 88     public AppDbContext(string connStr) => _connStr = connStr;
  89. 89     public DbSet<User> Users => Set<User>();
  90. 90     protected override void OnConfiguring(DbContextOptionsBuilder options)
  91. 91         => options.UseSqlite(_connStr);
  92. 92 }
  93. 93
  94. 94 public class User
  95. 95 {
  96. 96     public int Id { get; set; }
  97. 97     public string Name { get; set; } = "";
  98. 98     public string Email { get; set; } = "";
  99. 99     public int? Age { get; set; }
  100. 100 }
复制代码
Dapper + Microsoft.Data.Sqlite

适合性能优先、SQL 可控优先、追求轻量。这类开销低、速度快、透明 SQL;适合高频读写和明确数据模型。但缺点很明显,sql量太多了
  1. 1 using Dapper;
  2. 2 using Microsoft.Data.Sqlite;
  3. 3
  4. 4 var connStr = "Data Source=dapper_demo.db";
  5. 5 using var conn = new SqliteConnection(connStr);
  6. 6 conn.Open();
  7. 7
  8. 8 Migrate(conn);
  9. 9
  10. 10 // 写
  11. 11 conn.Execute(
  12. 12     "INSERT INTO Users(Name, Email, Age) VALUES (@Name, @Email, @Age);",
  13. 13     new { Name = "Alice", Email = "alice@test.com", Age = 20 });
  14. 14
  15. 15 // 读
  16. 16 var users = conn.Query<User>("SELECT Id, Name, Email, Age FROM Users ORDER BY Id;").ToList();
  17. 17 foreach (var u in users)
  18. 18 {
  19. 19     Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
  20. 20 }
  21. 21
  22. 22 static void Migrate(SqliteConnection conn)
  23. 23 {
  24. 24     conn.Execute("""
  25. 25         CREATE TABLE IF NOT EXISTS __schema_migrations (
  26. 26             version TEXT NOT NULL PRIMARY KEY,
  27. 27             applied_at TEXT NOT NULL
  28. 28         );
  29. 29     """);
  30. 30
  31. 31     Apply(conn, "202602260001_Init", """
  32. 32         CREATE TABLE IF NOT EXISTS Users (
  33. 33             Id INTEGER PRIMARY KEY AUTOINCREMENT,
  34. 34             Name TEXT NOT NULL,
  35. 35             Email TEXT NOT NULL
  36. 36         );
  37. 37     """);
  38. 38
  39. 39     Apply(conn, "202602260002_AddAgeAndBackfill", """
  40. 40         ALTER TABLE Users ADD COLUMN Age INTEGER NULL;
  41. 41         UPDATE Users SET Age = 18 WHERE Age IS NULL;
  42. 42     """);
  43. 43 }
  44. 44
  45. 45 static void Apply(SqliteConnection conn, string version, string sql)
  46. 46 {
  47. 47     var exists = conn.ExecuteScalar<long>(
  48. 48         "SELECT COUNT(1) FROM __schema_migrations WHERE version=@v", new { v = version }) > 0;
  49. 49     if (exists) return;
  50. 50
  51. 51     using var tx = conn.BeginTransaction();
  52. 52     try
  53. 53     {
  54. 54         conn.Execute(sql, transaction: tx);
  55. 55         conn.Execute("""
  56. 56             INSERT INTO __schema_migrations(version, applied_at)
  57. 57             VALUES(@v, @t)
  58. 58         """, new { v = version, t = DateTime.UtcNow.ToString("O") }, tx);
  59. 59
  60. 60         tx.Commit();
  61. 61     }
  62. 62     catch (SqliteException ex) when (ex.Message.Contains("duplicate column name"))
  63. 63     {
  64. 64         tx.Rollback();
  65. 65     }
  66. 66 }
  67. 67
  68. 68 public class User
  69. 69 {
  70. 70     public long Id { get; set; }
  71. 71     public string Name { get; set; } = "";
  72. 72     public string Email { get; set; } = "";
  73. 73     public int? Age { get; set; }
  74. 74 }
复制代码
SqlSugar + Microsoft.Data.Sqlite

上手快,功能集成度高(CodeFirst/DbFirst 等)。如果是数据库表结构经常变动,建议使用这个方案,CodeFrist开发非常便捷
  1. 1 using SqlSugar;
  2. 2
  3. 3 var db = new SqlSugarClient(new ConnectionConfig
  4. 4 {
  5. 5     ConnectionString = "Data Source=sqlsugar_demo.db",
  6. 6     DbType = DbType.Sqlite,
  7. 7     IsAutoCloseConnection = true,
  8. 8     InitKeyType = InitKeyType.Attribute
  9. 9 });
  10. 10
  11. 11 Migrate(db);
  12. 12
  13. 13 // 写
  14. 14 db.Insertable(new User { Name = "Alice", Email = "alice@test.com", Age = 20 }).ExecuteCommand();
  15. 15
  16. 16 // 读
  17. 17 var list = db.Queryable<User>().OrderBy(x => x.Id).ToList();
  18. 18 foreach (var u in list)
  19. 19 {
  20. 20     Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
  21. 21 }
  22. 22
  23. 23 static void Migrate(SqlSugarClient db)
  24. 24 {
  25. 25     db.Ado.ExecuteCommand("""
  26. 26         CREATE TABLE IF NOT EXISTS __schema_migrations (
  27. 27             version TEXT NOT NULL PRIMARY KEY,
  28. 28             applied_at TEXT NOT NULL
  29. 29         );
  30. 30     """);
  31. 31
  32. 32     Apply(db, "202602260001_Init", """
  33. 33         CREATE TABLE IF NOT EXISTS Users (
  34. 34             Id INTEGER PRIMARY KEY AUTOINCREMENT,
  35. 35             Name TEXT NOT NULL,
  36. 36             Email TEXT NOT NULL
  37. 37         );
  38. 38     """);
  39. 39
  40. 40     Apply(db, "202602260002_AddAgeAndBackfill", """
  41. 41         ALTER TABLE Users ADD COLUMN Age INTEGER NULL;
  42. 42         UPDATE Users SET Age = 18 WHERE Age IS NULL;
  43. 43     """);
  44. 44 }
  45. 45
  46. 46 static void Apply(SqlSugarClient db, string version, string sql)
  47. 47 {
  48. 48     var exists = db.Ado.GetInt("""
  49. 49         SELECT COUNT(1) FROM __schema_migrations WHERE version=@v
  50. 50     """, new { v = version }) > 0;
  51. 51
  52. 52     if (exists) return;
  53. 53
  54. 54     db.Ado.BeginTran();
  55. 55     try
  56. 56     {
  57. 57         db.Ado.ExecuteCommand(sql);
  58. 58         db.Ado.ExecuteCommand("""
  59. 59             INSERT INTO __schema_migrations(version, applied_at)
  60. 60             VALUES(@v, @t)
  61. 61         """, new { v = version, t = DateTime.UtcNow.ToString("O") });
  62. 62
  63. 63         db.Ado.CommitTran();
  64. 64     }
  65. 65     catch
  66. 66     {
  67. 67         db.Ado.RollbackTran();
  68. 68     }
  69. 69 }
  70. 70
  71. 71 [SugarTable("Users")]
  72. 72 public class User
  73. 73 {
  74. 74     [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
  75. 75     public int Id { get; set; }
  76. 76     public string Name { get; set; } = "";
  77. 77     public string Email { get; set; } = "";
  78. 78     public int? Age { get; set; }
  79. 79 }
复制代码
如果使用SqlSugar的已封装CodeFrist方案,迁移数据表会更简单:
  1. 1 using SqlSugar;
  2. 2
  3. 3 var db = new SqlSugarClient(new ConnectionConfig
  4. 4 {
  5. 5     ConnectionString = "Data Source=app.db",
  6. 6     DbType = DbType.Sqlite,
  7. 7     IsAutoCloseConnection = true,
  8. 8     InitKeyType = InitKeyType.Attribute,
  9. 9     ConfigureExternalServices = new ConfigureExternalServices
  10. 10     {
  11. 11         EntityService = (prop, col) =>
  12. 12         {
  13. 13             // 可选:统一处理字符串长度等
  14. 14             if (prop.PropertyType == typeof(string) && col.Length == 0)
  15. 15                 col.Length = 200;
  16. 16         }
  17. 17     }
  18. 18 });
  19. 19
  20. 20 // 1) CodeFirst 建表/补字段
  21. 21 db.CodeFirst.InitTables<User>();
  22. 22
  23. 23 // 2) 如需“迁移数据”(例如给新字段Age回填),用.NET代码执行SQL
  24. 24 db.Ado.ExecuteCommand("UPDATE Users SET Age = 18 WHERE Age IS NULL;");
  25. 25
  26. 26 // 3) 写入
  27. 27 db.Insertable(new User
  28. 28 {
  29. 29     Name = "Alice",
  30. 30     Email = "alice@test.com",
  31. 31     Age = 20
  32. 32 }).ExecuteCommand();
  33. 33
  34. 34 // 4) 读取
  35. 35 var users = db.Queryable<User>().OrderBy(x => x.Id).ToList();
  36. 36 foreach (var u in users)
  37. 37 {
  38. 38     Console.WriteLine($"{u.Id} {u.Name} {u.Email} Age={u.Age}");
  39. 39 }
  40. 40
  41. 41 [SugarTable("Users")]
  42. 42 public class User
  43. 43 {
  44. 44     [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
  45. 45     public int Id { get; set; }
  46. 46
  47. 47     [SugarColumn(Length = 100, IsNullable = false)]
  48. 48     public string Name { get; set; } = string.Empty;
  49. 49
  50. 50     [SugarColumn(Length = 200, IsNullable = false)]
  51. 51     public string Email { get; set; } = string.Empty;
  52. 52
  53. 53     // 新增字段:CodeFirst会尝试补列
  54. 54     [SugarColumn(IsNullable = true)]
  55. 55     public int? Age { get; set; }
  56. 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/让学习成为习惯,假设明天就有重大机遇等着你,你准备好了么本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册