Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
· 小村 豪 · SQLite, Windows, .NET, C#, 数据存储, 注册表, Access, 设计, 判断表, 技术咨询
「配置用INI文件就行了吗」「历史数据越来越多,想改用Access保存」「注册表和配置文件该怎么分工」。开发Windows业务应用时,数据存储位置的选择是必经之路。然而这个选择往往在项目初期随意确定之后就不再回顾,数年后才以「JSON文件膨胀到几十MB,启动变慢」「共享文件夹里的Access文件每周坏一次」「直接写入Program Files目录,导致在Windows 11环境下无法运行」等形式暴露出问题。
本文将针对业务用Windows应用的本地数据存储,把「放在哪里」(文件夹的选择)与「用什么保存」(格式・引擎的选择)分开整理。以本博客多次采用的判断表形式,汇总SQLite / JSON / 注册表 / Access各自的优势与陷阱。
1. 先说结论
- 存储位置的选择是「放在哪里」与「用什么保存」这两个独立的判断。前者选错会导致权限、多用户相关的事故,后者选错则会导致损坏、性能、维护方面的事故。
- 存放位置的基本原则是:用户个人的配置与数据放在
%LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData),全用户共享的数据放在%PROGRAMDATA%,而绝不写入与exe相同的文件夹(Program Files目录下)。1 - 格式的首选很简单,只有两种:结构化的小型配置用JSON文件,持续增加的业务数据・历史记录・需要检索的数据用SQLite。这两者就能覆盖业务应用本地存储的大部分需求。2
- 注册表是「存放小型标志或与Windows联动信息的地方」,并不是应用的数据存储区。如果不了解32bit/64bit的注册表重定向机制(
Wow6432Node)就直接使用,会踩到「明明写入了却读不到值」的坑。3 - 在新开发项目中选择Access(.accdb)作为数据存储区的理由几乎已经不存在。即使是为了与现有资产联动而使用,也会伴随「ACE提供程序的位数必须一致」这项部署上的限制。4
- 不论选用哪种格式,机密信息(密码、API密钥)都必须单独处理:不要以纯文本方式放进JSON或注册表,而要用DPAPI保护。详情请参阅另一篇文章「Windows应用的机密信息存储 - 用DPAPI避免纯文本配置」。
2. 把要保存的数据分成4种类型
在决定用什么保存之前,先对打算保存的数据性质做分类。业务应用的本地数据,大致可分为以下4类。
| 分类 | 示例 | 特征 |
|---|---|---|
| 配置 | 连接目标、界面布局、上次打开的文件夹 | 小。启动时全部读取。有时用户想直接编辑 |
| 业务数据・历史记录 | 测量结果、处理历史记录、主数据的本地副本 | 持续增加。需要检索・汇总。损坏时业务影响很大 |
| 缓存 | 缩略图、已下载的资源 | 消失也能重新生成。需要容量管理 |
| 机密信息 | 保存的密码、Token | 少量。不可以纯文本形式存放 |
每种分类适合的存放位置与格式各不相同,这正是本文的核心思路。如果你的应用是「配置和历史记录全部塞进同一个XML」,重新做这项分类就是改进的第一步。
3. 放在哪里 ── 文件夹选择的基本原则
如果使用.NET,就以 Environment.GetFolderPath 能取得的位置为基准。5
| 位置 | 获取方式 | 用途 |
|---|---|---|
%LOCALAPPDATA%\公司名\应用名 |
SpecialFolder.LocalApplicationData |
用户个人数据的默认位置。优先考虑这里 |
%APPDATA%\公司名\应用名(Roaming) |
SpecialFolder.ApplicationData |
仅限在漫游用户配置文件环境中,希望随用户移动的配置 |
%PROGRAMDATA%\公司名\应用名 |
SpecialFolder.CommonApplicationData |
全用户共享的数据。需要设计ACL |
| 文档文件夹下 | SpecialFolder.MyDocuments |
仅限用户会当作自己的文件处理的产出物(如导出的报表) |
写成代码就是这么简单,但如果连「公司名\应用名」这层目录结构,以及首次运行时创建文件夹,都包进公共处理里,就能防止存储位置随意增加的情况。
public static class AppPaths
{
public static string DataDir { get; } = CreateDir(
Environment.SpecialFolder.LocalApplicationData);
private static string CreateDir(Environment.SpecialFolder root)
{
var dir = Path.Combine(
Environment.GetFolderPath(root), "KomuraSoft", "MyApp");
Directory.CreateDirectory(dir); // 若已存在则不做任何事
return dir;
}
}
之所以不用字符串拼接自己组出 %LOCALAPPDATA%,而是使用 Environment.GetFolderPath,是因为即使在服务账户、其他用户身份运行,或配置了文件夹重定向的环境下,它都能返回正确的位置。用任务计划程序以不同账户运行的瞬间路径就变了的事故(这是任务计划程序文章第5章提到的「手动运行才能用」问题的一种),也能借此避免。
下面列举3个陷阱。
- 不要写入exe所在的文件夹。 标准用户无法写入Program Files目录下。旧版32bit应用可能因为UAC兼容性功能而被悄悄重定向到
VirtualStore,这会造成「以管理员身份运行和以标准用户身份运行时,配置文件内容却不一样」这种令人费解的问题。 - ProgramData是「能写,但不安全」。 在默认ACL下,可能形成某用户创建的文件,其他用户无法修改的状态。若要供全用户共享读写,应在安装程序中创建文件夹并明确设置ACL。
- 不要把Roaming当作默认选项。 在域的漫游用户配置文件环境中,Roaming目录下的内容会在登录・注销时同步。若把体积大的数据或机器特有的数据(缓存、硬件配置)放在Roaming,会造成同步延迟,或「污染」到其他机器。犹豫不决时就选Local。
4. 用什么保存 ── 4种选项的真面目
4.1 JSON文件 ── 配置的首选
可以用System.Text.Json直接读写、人类可读、便于用Git管理与比对差异——用于配置用途的优点一应俱全。需要注意的地方有两点。
要做好防止损坏的对策。 若在写入过程中断电,残留下不完整的文件,下次启动时就会读不到。惯用做法是「先写入临时文件再替换」,.NET的话File.Replace就提供了带备份的替换功能。
var json = JsonSerializer.Serialize(settings, options);
var tmp = path + ".tmp";
File.WriteAllText(tmp, json);
if (File.Exists(path))
File.Replace(tmp, path, path + ".bak");
else
File.Move(tmp, path);
读取端也一开始就内置「若损坏就尝试.bak,如果也不行就以默认值启动并发出警告」这种降级处理,配置文件损坏就不会变成支持工单。
不要把它当作数据存储区使用。 JSON的适用范围,是「启动时全部读取、结束时全部写入」这个前提能成立的大小(大约数百KB以内)。如果开始把会持续追加的历史记录,或需要检索的数据放进JSON,那就是该换成SQLite的信号。
4.2 SQLite ── 持续增加的数据・需要检索的数据的首选
SQLite是无需服务器、单一文件、属于公共领域的嵌入式数据库,在.NET中可以通过Microsoft维护的ADO.NET提供程序Microsoft.Data.Sqlite,或EF Core的SQLite提供程序来操作。2 Microsoft本身也将其推荐为Windows应用本地数据存储的手段6,对于「在本地持续增加的结构化数据」,目前的情况是可以优先考虑SQLite。
首先用代码展示它有多方便。只要通过NuGet添加Microsoft.Data.Sqlite,不需要服务器搭建,也不需要连接字符串的管理界面,只要指定文件路径就能开始使用。
using Microsoft.Data.Sqlite;
var dbPath = Path.Combine(AppPaths.DataDir, "app.db");
using var conn = new SqliteConnection($"Data Source={dbPath}");
conn.Open();
// 仅首次运行:启用WAL模式并创建表
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = """
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS measurement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
value REAL NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS ix_measurement_device
ON measurement(device_id, created_at);
""";
cmd.ExecuteNonQuery();
}
// 插入时必须使用参数(不要用字符串拼接组出SQL)
using (var cmd = conn.CreateCommand())
{
cmd.CommandText =
"INSERT INTO measurement (device_id, value) VALUES ($device, $value)";
cmd.Parameters.AddWithValue("$device", "CAM-01");
cmd.Parameters.AddWithValue("$value", 23.5);
cmd.ExecuteNonQuery();
}
可以看到,只需花费和「往JSON文件里追加内容」差不多的力气,就能获得带索引的检索・汇总,以及数量不受限的历史记录。如果想套用ORM,EF Core的SQLite提供程序就是构建在这个库之上的。
在此基础上,列举几个实务要点。
- 启用WAL模式。 就是上面代码中的
PRAGMA journal_mode=WAL;。这能提升读取与写入的并发性,即使是UI线程与后台处理同时操作同一个数据库的架构,也比较不容易卡住。WAL的设置会持久保存在数据库文件本身,所以不需要每次连接都重新发出。 - 把写入集中到单一进程的单一路径。 SQLite的写入是以数据库为单位进行互斥的。若要从多个线程写入,通过队列把写入角色统一为一个,这样的设计比较安全。此外,大量执行细碎的INSERT时,用明确的事务整批处理,速度会比逐条commit快上好几个量级。
- 不要放在网络共享文件夹。 通过SMB的文件锁定有很多与环境相关的问题,SQLite官方也把网络文件系统上的共享列为损坏原因的首位。7 一旦需要多台机器、多用户同时使用,那就已经是客户端・服务器型数据库(如SQL Server Express)的范畴了。
- 要知道类型实际上只有4种。 SQLite的实际类型是INTEGER / REAL / TEXT / BLOB,日期和GUID都是以TEXT形式保存。事先确认一次
Microsoft.Data.Sqlite的类型映射规则,日后在日期比较与排序上就不会犯难。8 上面示例中把created_at设为datetime('now')(UTC),也是因为混入本地时间会在排序与夏令时切换时产生问题。显示时再转换成本地时间的做法比较安全。 - 备份不要用「复制文件」,而要用
VACUUM INTO或Backup API。 单纯复制运行中的数据库文件,有时会拿到WAL与主体不一致的内容(详见第6章)。
4.3 注册表 ── 仅限小型标志与Windows联动信息
注册表适合存放的是「是否已安装」「启动项注册」「文件关联」等与Windows自身联动的信息,以及极小型的用户配置。原则是应用自己使用的配置放在HKCU下,机器通用的信息由安装程序写入HKLM(如果设计成在运行期间写入HKLM,会需要管理员权限,应避免)。
最大的陷阱在于位数。在64bit Windows中,从32bit进程看到的HKLM\Software会被重定向到HKLM\Software\Wow6432Node。3 「用注册表编辑器看明明有值,应用却读不到」「32bit应用写入的值,64bit维护工具却看不到」,这些问题几乎都是这个原因。这类问题常在迁移到AnyCPU或转为64bit时暴露出来,请与COM、ActiveX的32bit/64bit问题(「COM/OCX/ActiveX的陷阱 ── Visual Studio的位数与管理员权限」)放在同样的语境下理解。
如果从.NET非得读取另一种位数视图不可(例如仍维持32bit的应用,需要读取注册在64bit侧的值),可以用RegistryView明确指定要用哪种视图。
using Microsoft.Win32;
// 从32bit进程读取64bit视图的HKLM
using var hklm64 = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine, RegistryView.Registry64);
using var key = hklm64.OpenSubKey(@"SOFTWARE\KomuraSoft\MyApp");
var installDir = key?.GetValue("InstallDir") as string;
反过来说,一旦需要这样指定,也代表「究竟该写在32bit还是64bit才对」这个设计判断被拖延了。让写入端与读取端的位数保持一致,才是正道。
把超过数KB的数据或数组型数据放进注册表,无论在备份、迁移还是诊断方面都很不利。这类用途应该让位给文件(JSON / SQLite)。
4.4 Access (.accdb) ── 新项目几乎不再采用,现有系统对接则要有所取舍
过去业务应用的本地数据库首选是Access(JET/ACE),但现在几乎没有理由在新开发项目中选它。主要原因在于部署。要以代码访问.accdb,需要ACE(Access Database Engine)提供程序,而且应用的位数必须与ACE的位数一致才能连接。4 还存在与Office位数共存的问题,「在开发机上能跑,但客户端出现Microsoft.ACE.OLEDB.12.0 提供程序未在本地计算机上注册」是经典的支持工单。此外还需要安装可再发行组件包(Access Database Engine 2016 Redistributable),这也增加了部署物。9
尽管如此,实务中仍有需要用到Access的场合,比如与现有的Access业务系统做数据对接、读取用Access制作的主数据等。此时建议做出以下取舍:
- 固定读写进程的位数(实务中多半固定为x86较为现实),并在安装程序中确认对应的ACE是否存在
- 设计上要避免多人同时写入放在共享文件夹的.accdb(一旦损坏,恢复成本并不划算)
- 长期而言,要保留迁移到SQLite或服务器型数据库的路径
建议做上述取舍。包含Excel/VBA资产在内的现有资产处理方式,也整理在「VBA是什么 - 局限性、未来性、该替换的场合」一文中。
5. 判断表
| 视角 | JSON文件 | SQLite | 注册表 | Access (.accdb) |
|---|---|---|---|---|
| 擅长的数据 | 小型配置 | 持续增加的结构化数据、检索・汇总 | 小型标志、Windows联动 | 与现有Access资产对接 |
| 数据量参考值 | ~数百KB | ~数十GB | ~数KB | ~2GB(规格上限) |
| 检索・汇总 | ✕(前提是全部读入) | ◎(SQL) | ✕ | ○(SQL) |
| 人类可直接阅读 | ◎ | △(需工具) | △ | △(需Access) |
| 抗损坏能力 | △(需自行处理) | ○(事务) | ○ | △ |
| 多进程同时访问 | ✕ | ○(同一台机器内) | ○ | △ |
| 多机共享 | ✕ | ✕ | ✕ | ✕(事实上) |
| 额外的部署物 | 无 | 无(NuGet随附) | 无 | 必须有ACE提供程序 |
如最后一行所示,任何一种本地存储技术都不适合「多机共享」。放进共享文件夹看起来像是能共享,但JSON没有互斥机制,SQLite在SMB上的锁定不可靠,Access则会伴随损坏风险而遭遇瓶颈。一旦出现多站点、多用户需要操作同一份数据的需求,请把它当作该搭建SQL Server Express之类的服务器型数据库,或搭建Web API的判断分界线。
6. 不损坏・可迁移・可恢复 ── 不分格式的通用设计
不论选择哪种格式,只要运行个几年,就一定会需要以下3项设计。是否在首次发布时就纳入,会大幅影响之后的维护成本。
6.1 为架构・格式添加版本号
应用一旦更新,保存数据的形式也会随之改变。「新版本读取旧版本写入的数据」的瞬间必定会到来,所以要让数据本身带有格式版本信息。
如果是SQLite,PRAGMA user_version正是为这个用途准备的。
int GetVersion(SqliteConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "PRAGMA user_version";
return Convert.ToInt32(cmd.ExecuteScalar());
}
void Migrate(SqliteConnection conn)
{
void Exec(string sql)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
var v = GetVersion(conn);
if (v > 2)
// 用旧版应用打开了新版应用创建的数据库的情况。
// 不去碰触不了解的架构,在此中止才安全
throw new InvalidOperationException(
$"这个数据库(版本 {v})是由更新版本的应用创建的。");
using var tx = conn.BeginTransaction();
if (v < 1) Exec("ALTER TABLE measurement ADD COLUMN unit TEXT");
if (v < 2) Exec("CREATE TABLE operator (id INTEGER PRIMARY KEY, name TEXT)");
Exec("PRAGMA user_version = 2");
tx.Commit();
}
这是启动时检查版本,只应用差异部分,也就是所谓迁移(migration)的最简形式。开头之所以拒绝「比自己更新的版本」,是为了防止把应用回退到旧版本(rollback)时,旧代码写入自己不了解的架构而造成损坏的事故。JSON的思路也一样,在根层放入"version": 2,读取时插入从旧格式转换的步骤,并拒绝读取过新的格式。「不要让没有版本号的数据格式问世」,只要守住这一点,未来的自己就能得救。
6.2 事先确定损坏时的降级行为
第4章提到了各格式的损坏对策(JSON的原子写入、SQLite的事务),但即便如此,仍会遇到「读不到的数据」——磁盘故障、防病毒软件误报而隔离、用户手动编辑。如果没有事先确定应用此时该如何应对,就会变成连启动都做不到的应用。
- 配置读不到 → 以默认值启动,并通知用户此事(若默默套用默认值,会变成「我的配置丢了」的投诉)
- 业务数据读不到 → 以只读模式或错误界面呈现「哪个文件损坏了」。不要自动覆盖修复(会让证据消失)
- 有备份 → 提议恢复。不过自动恢复伴随着「误判为损坏而回退到旧数据」的风险,原则上要经过用户操作确认
6.3 备份的关键不在于「有没有备」,而在于「能不能恢复」
本地数据和服务器上的数据库不同,没有人会帮你备份。若要由应用端负责处理,就要确定以下3点。
- 备份什么:业务数据要备,缓存不用备,机密信息则要考虑到DPAPI的特性——只能在同一用户、同一台机器上解密(迁移到其他机器需要另外的流程)
- 何时・备到哪里:可在启动时或每日,以带世代编号的方式备份到
%LOCALAPPDATA%内的backup文件夹。是否再放到共享文件夹或现有的PC备份对象文件夹,则要与运维方协商 - 怎么备份:SQLite禁止在运行中直接单纯复制文件。用
VACUUM INTO 'backup.db'可以一句话获得具有一致性的快照
// VACUUM INTO 不会自动创建父文件夹,且若输出目标已存在会报错。
// 事先完成文件夹创建与不重复文件名的确定
var backupDir = Path.Combine(AppPaths.DataDir, "backup");
Directory.CreateDirectory(backupDir);
var backupPath = Path.Combine(backupDir, $"app-{DateTime.Now:yyyyMMdd-HHmmss}.db");
using var cmd = conn.CreateCommand();
cmd.CommandText = "VACUUM INTO $path";
cmd.Parameters.AddWithValue("$path", backupPath);
cmd.ExecuteNonQuery();
因为世代会持续累积,备份后最好也一并加入「只保留最近N个世代、删除较旧者」的处理。
而且请务必实际演练一次恢复。有备份文件却没人知道怎么还原、也没试过,这是业务系统的「常见状况」。试着写一份在更换PC时把数据迁移到新设备的操作手册,通常就能发现备份设计的漏洞(比如用DPAPI保护的凭据无法迁移、路径包含用户名导致换用户就出问题等)。关于PC报废时如何清除数据,也请参阅「报废Windows PC前该做的事」。
7. 容易犯难的场景指南
- 「虽然是配置,但将来似乎会增加」 ── 如果预计「启动时全部读取」的用法迟早撑不住,一开始就直接用SQLite。在SQLite里建一个「settings表」并没有什么不妥。
- 「从INI/XML迁移」 ── 如果只是单纯换格式,改用JSON即可;如果这时混杂着历史记录类数据,就分离出来改用SQLite。在读取端保留1~2个版本的旧格式兜底方案,迁移会更安全。
- 「被要求要能用Excel看」 ── 不要把数据存储区直接改成Excel/Access,而是保存在SQLite后再加上导出到CSV/Excel的功能,这样才能同时满足数据可靠性与需求。报表输出的做法请参阅「如何制作Excel报表输出」。
- 「想用多个进程读写同一份文件」 ── 如果在同一台机器内,SQLite(WAL)相当够用,但需要设计写入冲突的处理。如果要以文件为基础进行对接,请使用「文件对接与锁定的最佳实践」中的互斥模式。
- 「想在多台机器间共享」 ── 这就代表要从本地存储毕业了。首选是在相当于文件服务器的机器上搭建SQL Server Express(免费,数据库上限10GB)的客户端・服务器架构。另外,SQL Server的「LocalDB」尽管名称如此,实际上是供开发用途的单用户环境,请不要为了共享目的而选它。如果需要跨站点、也要从公司外部使用,就到了该考虑加入Web API架构的阶段。
8. 总结
存储位置的选择,只要拆成「放在哪里」(LocalAppData/ProgramData,且不写入Program Files)与「用什么保存」(配置用JSON、持续增加的数据用SQLite、注册表尽量少用、Access仅限现有系统对接)来思考,绝大多数情况都能毫不犹豫地决定。
在此基础上,不论选用哪种格式,都要在首次发布时就纳入第6章的3项套餐──格式版本号、损坏时的降级行为、可恢复的备份。唯独机密信息,永远要单独用DPAPI处理。只要掌握本文的判断表与通用设计,就能大致避开「膨胀到几十MB的JSON」「每周坏一次的共享Access」这类日后代价高昂的架构。如果对现有应用的存储方式感到不安,建议先从「盘点什么数据存在哪里」开始入手。
相关文章
- Windows应用的机密信息存储 - 用DPAPI避免纯文本配置
- 文件对接与锁定的最佳实践
- 如何制作Excel报表输出 - COM自动化/Open XML/模板方式的判断表
- 用任务计划程序安全地运行定时任务
相关咨询领域
合同会社小村软件承接业务应用数据存储方式的重新评估(包括从INI/XML/Access迁移的设计),以及数据损坏、性能劣化的原因排查。
参考链接
-
Microsoft Learn,KNOWNFOLDERID。关于LocalAppData、RoamingAppData、ProgramData等Windows已知文件夹的定义。 ↩
-
Microsoft Learn,Microsoft.Data.Sqlite overview。关于Microsoft维护的SQLite用ADO.NET提供程序概述,以及它是EF Core SQLite提供程序的基础。 ↩ ↩2
-
Microsoft Learn,Registry Redirector。关于在64bit Windows中,32bit进程的注册表访问被重定向到Wow6432Node的机制。 ↩ ↩2
-
Microsoft Learn,Can’t establish a connection to Access Database Engine OLE DB。关于ACE OLE DB提供程序必须与访问的进程位数一致。 ↩ ↩2
-
Microsoft Learn,Environment.SpecialFolder Enum。关于用于从.NET获取已知文件夹的枚举类型。 ↩
-
Microsoft Learn,Use a SQLite database in a Windows app。官方教程,推荐在Windows应用的本地数据存储中使用SQLite与Microsoft.Data.Sqlite / EF Core。 ↩
-
SQLite,How To Corrupt An SQLite Database File。关于网络文件系统上的锁定失效是数据库损坏的主要原因。 ↩
-
Microsoft Learn,Data types (Microsoft.Data.Sqlite)。关于SQLite的4种基本类型,以及DateTime、Guid映射到TEXT的规则。 ↩
-
Microsoft,Microsoft Access Database Engine 2016 Redistributable。关于用于访问.accdb / .mdb的ACE可再发行组件包(32bit/64bit)。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
在 C# 业务应用中使用 SQLite ── WAL 模式、排他控制、防损坏对策、与 EF Core 的取舍
本文整理在 Microsoft.Data.Sqlite 中把 SQLite 嵌入业务 Windows 应用的实务知识:连接字符串与连接池、WAL 模式的原理、SQLITE_BUSY 与写入归并、通过事务提速、类型映射的陷阱、用 VACUUM INTO 备份,以及该在什么场合...
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化
常驻处理应该做成 Windows 服务,还是任务计划程序就够用?本文整理判断表,以及用 .NET Worker Service(UseWindowsService)创建服务的方法、运行账户、恢复选项、事件日志、安全停止处理,从实务角度梳理投入运维所需的设计。
防止 Windows 应用程序重复启动 ── 命名 Mutex 与重复执行时的窗口前置
整理业务型 Windows 应用程序的常见需求——「不让同一个应用程序启动两次」——如何用命名 Mutex 来实现。涵盖 Global\ 与 Local\ 命名空间差异在 RDP 环境中的陷阱、拥有线程限制与 AbandonedMutexException、在 SetFor...
常见问题
汇总了咨询这一主题时常见的问题。
- Windows应用的配置文件该保存在哪里?
- 如果是用户个人的配置・数据,基本原则是放在 %LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData)下,以「公司名\应用名」的层级结构存放。若是全用户共享,则放在 %PROGRAMDATA%,但由于默认ACL可能形成其他用户无法修改的状态,需要在安装程序中明确设置ACL。绝对不可以写入与exe相同的文件夹(Program Files下)。标准用户无法写入该处,旧版32bit应用还会因为被悄悄重定向到VirtualStore而产生难以理解的问题。
- 配置该用JSON还是SQLite保存?
- 结构化的小型配置用JSON文件,会持续增加的业务数据・历史记录・需要检索的数据则以SQLite为首选,这两者就能覆盖业务应用本地存储的大部分需求。JSON的适用范围大约是「启动时全部读取、结束时全部写入」这个前提能成立的数百KB以内。如果开始把持续追加的历史记录或需要检索的数据放进JSON,那就是该迁移到SQLite的信号。即使是配置,只要预期未来会增加,一开始就在SQLite里建一个settings表也没有问题。
- 可以把SQLite数据库放在网络共享文件夹里吗?
- 应该避免。通过SMB的文件锁定有很多与环境相关的问题,SQLite官方也把网络文件系统上的共享列为损坏原因的首位。JSON没有互斥机制,Access也会伴随损坏风险而遭遇瓶颈,因此任何一种本地存储技术都不适合多机共享。一旦出现多站点、多用户需要操作同一份数据的需求,就是该考虑搭建SQL Server Express(免费,数据库上限10GB)之类的服务器型数据库,或搭建Web API的判断分界线。
- 可以把应用的数据保存进注册表吗?
- 注册表适合存放的仅限于启动项注册、文件关联等与Windows自身联动的信息,以及极小型的用户配置。超过数KB的数据或数组型数据,无论在备份、迁移还是诊断方面都不利,应该让位给JSON或SQLite。此外,在64bit Windows中,从32bit进程看到的 HKLM\Software 会被重定向到 Wow6432Node,这会造成「用注册表编辑器看明明有值,应用却读不到」的问题。让写入端与读取端的位数保持一致,才是正道。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。