.NET(C#)32位进程内存受限,求混合磁盘内存嵌入式数据库/数据结构
这个场景我太熟悉了——32位进程的4GB内存天花板真的是个让人头疼的硬限制,尤其是还要处理大量需要快速访问的对象。下面给你几个在.NET(C#)生态里可以直接落地的方案,从简单到复杂都有,你可以根据自己的需求选:
.NET自带的MemoryCache本身就支持*LRU(最近最少使用)*等缓存淘汰策略,我们可以给它加个“磁盘兜底”的逻辑:当内存不足导致缓存项被淘汰时,自动把对象序列化到磁盘;读取时先查内存,没有再从磁盘加载回内存。
示例代码:
// 初始化内存缓存 var cache = MemoryCache.Default; // 配置缓存项策略:非关键项优先淘汰,淘汰时写入磁盘 var cachePolicy = new CacheItemPolicy { Priority = CacheItemPriority.Default, RemovedCallback = args => { // 只处理内存不足导致的淘汰(排除手动移除、过期等情况) if (args.RemovedReason == CacheEntryRemovedReason.Evicted) { // 用System.Text.Json序列化对象(也可以用MessagePack提升性能) var serializedObj = JsonSerializer.Serialize(args.Value); var cacheDir = Path.Combine(AppContext.BaseDirectory, "disk_cache"); Directory.CreateDirectory(cacheDir); File.WriteAllText(Path.Combine(cacheDir, $"{args.Key}.json"), serializedObj); } } }; // 添加对象到缓存 cache.Add("user_1001", new User { Id = 1001, Name = "Alice" }, cachePolicy); // 获取对象的逻辑 User GetUser(string key) { var user = cache.Get(key) as User; if (user != null) return user; // 内存没有,从磁盘加载 var filePath = Path.Combine(AppContext.BaseDirectory, "disk_cache", $"{key}.json"); if (File.Exists(filePath)) { var serializedObj = File.ReadAllText(filePath); user = JsonSerializer.Deserialize<User>(serializedObj); // 加载后重新加入内存缓存 cache.Add(key, user, cachePolicy); } return user; }
这个方案的优势是轻量无依赖,完全可控,但需要自己处理磁盘IO并发、过期文件清理等细节。
如果你不想自己造轮子,CacheManager是个成熟的选择——它支持多级缓存组合(内存+磁盘+其他),内置LRU淘汰策略,自动帮你完成内存和磁盘的对象交换。
首先安装NuGet包:CacheManager.Core、CacheManager.SystemRuntimeCaching(内存缓存)、CacheManager.FileSystem(磁盘缓存)
示例代码:
// 构建多级缓存:内存缓存优先,磁盘缓存兜底 var cache = CacheFactory.Build<User>("my_multi_level_cache", settings => { settings // 第一级:内存缓存,滑动过期30分钟 .WithSystemRuntimeCacheHandle("memory") .WithExpiration(ExpirationMode.Sliding, TimeSpan.FromMinutes(30)) .And() // 第二级:磁盘缓存,绝对过期7天 .WithFileSystemCacheHandle("disk") .WithBasePath(@"C:\app_cache") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromDays(7)); }); // 操作缓存和普通缓存完全一致 cache.Add("user_1001", new User { Id = 1001, Name = "Alice" }); var user = cache.Get("user_1001"); // 自动先查内存,无则查磁盘,加载后自动回写内存
这个方案省心省力,不用自己处理淘汰和持久化逻辑,还支持扩展到其他缓存层(比如Redis),缺点是需要引入第三方库。
如果你的对象需要复杂查询(比如按属性过滤、排序),LiteDB是个绝佳选择——它是纯C#写的嵌入式NoSQL数据库,支持文档模型,内置内存缓存,会自动把不常用的文档刷到磁盘,常用的留在内存。
首先安装NuGet包:LiteDB
示例代码:
// 连接数据库,配置内存缓存大小(比如100MB) using var db = new LiteDatabase(@"C:\app_data\my_db.db", new LiteDatabaseSettings { CacheSize = 100 * 1024 * 1024, // 内存缓存上限100MB CheckpointSize = 10 * 1024 * 1024 // 每写入10MB数据自动刷到磁盘 }); // 获取对象集合 var users = db.GetCollection<User>("users"); // 插入/更新对象 users.Upsert("user_1001", new User { Id = 1001, Name = "Alice" }); // 查询对象:LiteDB会自动缓存常用对象 var user = users.FindById("user_1001"); // 复杂查询示例:找名字包含"li"的用户 var filteredUsers = users.Find(u => u.Name.Contains("li")).ToList();
LiteDB不仅帮你处理内存磁盘交换,还支持索引、事务、LINQ查询等功能,适合对象数量多、需要灵活查询的场景。
如果你的对象结构规整,需要关系型查询,可以用SQLite配合自定义LRU缓存。你可以自己维护一个内存LRU缓存,满了就把最久未访问的对象写入SQLite,读取时先查内存再查数据库。
示例代码(配合Dapper简化SQL操作):
// 简化版LRU缓存(实际可以用第三方成熟实现,比如LRUCache.NET) var lruCache = new LruCache<string, User>(capacity: 1000); lruCache.EvictionCallback = (key, user) => { // 淘汰时写入SQLite using var conn = new SQLiteConnection("Data Source=C:\\app_data\\my_sqlite.db"); conn.Execute(@" INSERT OR REPLACE INTO Users (Id, Name) VALUES (@Id, @Name)", user); }; // 获取对象逻辑 User GetUser(string key) { if (lruCache.TryGetValue(key, out var user)) return user; // 内存没有,从SQLite加载 using var conn = new SQLiteConnection("Data Source=C:\\app_data\\my_sqlite.db"); user = conn.QueryFirstOrDefault<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = key }); if (user != null) lruCache.Add(key, user); return user; }
这个方案适合需要关系型数据模型的场景,但需要自己处理缓存淘汰和数据库交互的细节。
- 序列化选型:优先用MessagePack或Protobuf这类高效序列化库,比Json快很多,尤其适合大对象;避免使用BinaryFormatter(不安全且性能差)。
- 内存碎片优化:32位进程对内存碎片更敏感,尽量用结构体代替小类、避免频繁分配大对象、定期触发轻量GC(
GC.Collect(2, GCCollectionMode.Optimized))。 - 并发安全:磁盘IO和缓存操作要注意线程安全,第三方库一般已处理,自定义逻辑建议用
lock或ConcurrentDictionary。
内容的提问来源于stack exchange,提问作者Coder14

