【问题标题】:Using SQL LocalDB in a Windows Service在 Windows 服务中使用 SQL LocalDB
【发布时间】:2014-12-23 05:14:08
【问题描述】:

我有一个非常小的测试应用程序,我试图在其中安装 Windows 服务并在安装过程中创建一个 LocalDB 数据库,然后在 Windows 服务运行时连接到该 LocalDB 数据库。

我在从我的 Windows 服务连接到 LocalDB 实例时遇到了很大的问题。

我的安装过程是这样的:

  1. 执行安装程序 .msi 文件,该文件以 NT AUTHORITY\SYSTEM 帐户运行 msiexec 进程。

  2. 使用以下命令运行自定义操作以执行 SqlLocalDB.exe:

    • sqllocaldb.exe 创建 MYINSTANCE
    • sqllocaldb.exe 共享 MYINSTANCE MYINSTANCESHARE
    • sqllocaldb.exe 启动 MYINSTANCE
  3. 使用 ADO.NET (System.Data.SqlConnection) 运行自定义 C# 操作以执行以下操作:

    • 连接到以下连接字符串Data Source=(localdb)\MYINSTANCE; Integrated Security=true
    • 创建数据库测试数据库
    • 使用测试数据库
    • 创建表 ...
  4. 在安装程序完成之前启动 Windows 服务。

  5. Windows 服务安装到 LocalSystem 帐户,因此也作为 NT AUTHORITY\SYSTEM 用户帐户运行。

  6. 服务尝试使用上面使用的相同连接字符串进行连接。

尝试从 Windows 服务中打开与上述连接字符串的连接时,我一直收到以下错误:

System.Data.SqlClient.SqlException (0x80131904):网络相关或 建立连接时发生特定于实例的错误 SQL 服务器。服务器未找到或无法访问。核实 实例名称正确且 SQL Server 配置为 允许远程连接。 (提供者:SQL 网络接口,错误:50 - 发生本地数据库运行时错误。指定的 LocalDB 实例不存在。

这令人沮丧,因为 msi 安装程序自定义操作和 Windows 服务都在同一个 Windows 用户帐户下运行(我检查过,它们都是 NT AUTHORITY\System)。那么为什么第一个有效而第二个无效是我无法理解的。

我尝试将自定义操作和 Windows 服务中使用的连接字符串更改为使用共享名称 (localdb)\.\MYINSTANCESHARE,但我从 Windows 服务中得到了完全相同的错误。

我已尝试将 Windows 服务登录的用户帐户更改为我的 Windows 用户帐户,只要我首先运行命令将其添加到该实例的 SQL 服务器登录名,它就可以工作。

我也尝试过运行控制台应用程序并连接到共享名称连接字符串,效果也很好。

我还尝试从 SQL Server Management Studio 连接到共享名称,并且效果也很好。

但是,这些方法都没有真正解决我的问题。我需要一个 Windows 服务,因为它会在计算机启动后立即启动(即使没有用户登录)并且无论登录哪个用户帐户都会启动。

Windows 服务如何连接到 LocalDB 私有实例?

我正在使用 SQL Server 2014 Express LocalDB。

【问题讨论】:

  • 我怀疑 Windows 现在正在为作为系统运行的所有服务创建“虚拟帐户”,因此它们无法相互通信。我可能是错的,我隐约记得在某处读过一些类似的东西,不久前。但是现在查不出来。希望对您有所帮助。
  • 出于测试目的,我尝试让服务作为非系统帐户运行,而不是在创建 LocalDB 实例期间使用的帐户。我得到了同样的错误。所以我不确定问题是我正在使用的系统帐户,问题要么是 SqlLocalDB.exe 实用程序作为系统帐户运行以创建实例,要么是帐户之间的共享只是根本不工作。
  • 安装程序完成后,实例在哪里物理创建?它应该位于类似于: C:\Users\{some login}\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances 的位置。请记住,LocalDB 是“用户模式”,因此它必须在用户的上下文中,我猜虚拟帐户不符合拥有实例/数据库的条件,但仍然可以连接。跨度>
  • 另外,您为 Windows 服务使用的 .Net 版本是什么?本地系统帐户是否被视为管理帐户?您是否在连接字符串中使用双反斜杠,或者如果没有,则在字符串前面加上“@”?
  • @srutzky 该实例应该在C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances 中创建,这是 SYSTEM 帐户的正确 AppData 文件夹。但是,我看到在 C:\Windows\System32\config\systemprofile 中创建 .mdf 和 .ldf 文件时,正在为处于活动状态的用户在 C:\Users\{current user}\... 文件夹中创建实际的实例文件夹。

标签: sql sql-server localdb


【解决方案1】:

从 cmets 的问题中挑选出来,这里有一些值得关注的地方。其中一些问题已经在这些 cmets 中得到了解答,但我在此记录以供其他人参考,以防这些信息可能对您有所帮助。


  • 正在使用什么版本的 .Net?这里是 4.5.1(好),但早期版本无法处理首选连接字符串(即@"(localdb)\InstanceName")。以下引用摘自上述链接:

    如果您的应用程序使用 4.0.2 之前的 .NET 版本,您必须直接连接到 LocalDB 的命名管道。

    根据SqlConnection.ConnectionString的MSDN页面:

    从 .NET Framework 4.5 开始,您还可以按如下方式连接到 LocalDB 数据库:

    server=(localdb)\\myInstance

  • 路径:

    • 实例:C:\Users{Windows Login}\AppData\Local\Microsoft\Microsoft SQL Server 本地 DB\Instances

    • 数据库:

      • 通过 SSMS 或直接连接创建:C:\Users{Windows Login}\DocumentsC:\Users{Windows Login}
      • 通过 Visual Studio 创建:C:\Users{Windows Login}\AppData\Local\Microsoft\VisualStudio\SSDT


  • 最初的问题
    • 症状:
      • 在预期位置创建的数据库文件(.mdf.ldf):
        C:\Windows\System32\config\systemprofile
      • 在意外位置创建的实例文件:
        C:\Users\{当前用户}\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
    • 原因(来自上面链接的“SqlLocalDB Utility”MSDN 页面的注释;强调我的):

      启动以外的操作只能在属于当前登录用户的实例上执行。


  • 要尝试的事情:

    • 指定数据库的连接字符串(如果错误与无法连接到实例有关,则可能是一个长镜头):
    • "Server=(LocalDB)\MYINSTANCE; Integrated Security=true ;AttachDbFileName=C:\Windows\System32\config\systemprofile\TestDB.mdf"
    • "Server=(LocalDB)\.\MYINSTANCESHARE; Integrated Security=true ;AttachDbFileName=C:\Windows\System32\config\systemprofile\TestDB.mdf"

    • 服务是否正在运行?从命令提示符运行以下命令:
      TASKLIST /FI "IMAGENAME eq sqlservr.exe"
      它可能应该列在“会话名称”列的“控制台”下

    • 从命令提示符运行以下命令:
      sqllocaldb.exe info MYINSTANCE
      并验证“所有者”的值是否正确。 “共享名称”的值应该是什么?如果没有,文档说明:

      只有计算机上的管理员才能创建 LocalDB 的共享实例

    • 作为设置的一部分,添加NT AUTHORITY\System 帐户作为登录到系统,如果此帐户未显示为实例的“所有者”,这是必需的:
      CREATE LOGIN [NT AUTHORITY\System] FROM WINDOWS; ALTER SERVER ROLE [sysadmin] ADD MEMBER [NT AUTHORITY\System];

    • 检查以下文件以获取线索/详细信息:
      C:\Users{Windows Login}\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MYINSTANCE\error.log

    • 最后,您可能需要创建一个实际帐户来创建和拥有实例和数据库,以及运行您的服务。 LocalDB 真的是用户模式,让你的服务有自己的登录有什么缺点吗?那时您可能不需要共享该实例。

      事实上,正如微软在 SQL Server YYYY Express LocalDB MSDN 页面上所指出的那样:

      内置帐户(例如 NT AUTHORITY\SYSTEM)拥有的 LocalDB 实例可能由于 Windows 文件系统重定向而存在可管理性问题;而是使用普通的 Windows 帐户作为所有者。

更新 (2015-08-21)

根据 OP 的反馈,在某些环境中使用常规用户帐户可能会出现问题,并且请记住在 %LOCALAPPDATA% 文件夹中为运行安装程序的用户创建的 LocalDB 实例的原始问题(以及 not NT AUTHORITY\System%LOCALAPPDATA% 文件夹),我找到了一个似乎符合易于安装的意图的解决方案(无需用户创建)并且不需要需要额外的代码来加载 SYSTEM 配置文件。

尝试使用两个内置帐户之一,不是LocalSystem 帐户(它不维护自己的注册表信息。使用其中一个:

两者的配置文件文件夹都位于:C:\Windows\ServiceProfiles

虽然我无法通过安装程序进行测试,但我确实通过将我的 SQL Server Express 2014 实例设置为此帐户登录来测试以 NT AUTHORITY\NetworkService 身份登录的服务,并且重新启动 SQL Server 服务。然后我运行了以下内容:

EXEC xp_cmdshell 'sqllocaldb c MyTestInstance -s';

它在以下位置创建了实例:C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances

然后我运行了以下内容:

EXEC xp_cmdshell N'SQLCMD -S (localdb)\MyTestInstance -E -Q "CREATE DATABASE [MyTestDB];"';

它已在以下位置创建数据库:C:\Windows\ServiceProfiles\NetworkService

【讨论】:

  • 抱歉回复晚了。我不得不开始另一个赏金,并在允许时奖励它。最后,您的最终建议对我有用。我必须修改我的 MSI 安装程序以创建一个用户帐户并将其分配给管理员组,然后安装该服务并让它作为我创建的用户帐户运行,然后我必须以这种方式运行我的所有 MSI 自定义操作他们将作为我创建的用户帐户执行 SqlCmd 和 SqlLocalDb(使用 Process.Start 并提供用户名和密码)。
  • @TrevorElliott:感谢您的跟进 :)。只是出于好奇,你有没有尝试过 Rolo 的建议?无论哪种方式,我认为微软明确区分(在一两个博客中)LocalDB 是用户模式,如果您需要后端服务,那么您应该使用完整的 SQL Server Express Edition。关于赏金:a)感谢您这样做,b)我注意到 CJBS 还建议创建一个新帐户,因此如果您能够这样做,我愿意分配奖励。
  • Rolo 的回答没有帮助。使用 Impersonation 属性允许您以本地系统帐户或当前用户身份执行命令。我们已经确定 LocalDB 不能与系统帐户一起正常工作。当前用户帐户的问题是该应用程序应该为所有用户安装,我也不知道当前用户帐户的密码以确保服务以它启动。
  • 这绝对正确 Trevor,我的回答理论上应该可行,所以我决定自己实现这个,我可以看到在本地系统帐户下所有内容都是 100% 正确创建的,但是当 Windows 服务尝试连接时您收到“与网络相关或特定于实例的错误...”。然后我同意,系统帐户不能很好地与 LocalDB 配合使用。正确的方法是,正如这里提出和接受的那样,为该服务创建一个特定的用户帐户。
  • @TrevorElliott :我刚刚在答案顶部的“信息”部分添加了第四个项目符号。这是来自 SQL Server Express 团队的博客文章。它有一些很好的信息以及一个问答部分,其中一个问题是特定于从服务运行的。另外,不确定您是否在上面看到了 Rolo 的评论,因为您没有在其中被标记。在我的补充或他的评论中没有什么新的东西,但它确实有助于更清楚地描绘出你选择的路径可能是唯一可行的路径(而不是想知道是否还有其他方式)。
【解决方案2】:

我最近在我们的 WiX 安装程序中解决了类似的问题。我们有一个在 SYSTEM 帐户下运行的 Windows 服务和一个安装程序,其中基于 LocalDB 的存储是数据库配置的选项之一。一段时间(实际上是几年)产品升级和服务运行良好,没有与 LocalDB 相关的问题。我们正在使用默认的 v11.0 实例,该实例在 C:\Windows\System32\config 树的 SYSTEM 配置文件中创建,以及通过 AttachDbFileName 指定的数据库,在 ALLUSERSPROFILE 树中创建。数据库提供程序配置为使用 Windows 身份验证。我们还在安装程序中有一个自定义操作,计划为延迟/非模拟,它运行数据库架构更新。

直到最近,这一切都运作良好。在又一批数据库更新之后,我们的新版本在升级前一个版本后开始失败 - 服务无法启动,报告臭名昭著的“建立与 SQL Server 的连接时发生网络相关或特定于实例的错误”(错误 50 ) 错误。

在调查此问题时,很明显问题在于 WiX 运行自定义操作的方式。尽管非模拟 CA-s 在 SYSTEM 帐户下运行,但注册表配置文件和环境仍然是当前用户的(我怀疑 WiX 在附加到用户会话时会自动加载这些)。这会导致从 LOCALAPPDATA 变量扩展的路径不正确 - 服务接收 SYSTEM 配置文件一,但架构更新 CA 与用户的配置文件一起使用。

所以这里有两种可能的解决方案。第一个很简单,但对用户系统的侵入性太大 - 通过 psexec 启动 cmd.exe,在 SYSTEM 帐户下重新创建损坏的实例。这对我们来说不是一个选项,因为用户可能在 v11.0 实例中创建了其他数据库,这是公共的。第二种选择假设进行了大量重构,但不会造成任何损害。以下是在 WiX CA 中使用 LocalDB 正确运行数据库架构更新的方法:

  1. 将您的 CA 配置为延迟/非模拟(应在 SYSTEM 帐户下运行);
  2. 修复环境以指向 SYSTEM 配置文件路径:

    var systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
    Environment.SetEnvironmentVariable("USERPROFILE", String.Format(@"{0}\System32\config\systemprofile", systemRoot));
    Environment.SetEnvironmentVariable("APPDATA", String.Format(@"{0}\System32\config\systemprofile\AppData\Roaming", systemRoot));
    Environment.SetEnvironmentVariable("LOCALAPPDATA", String.Format(@"{0}\System32\config\systemprofile\AppData\Local", systemRoot));
    Environment.SetEnvironmentVariable("HOMEPATH", String.Empty);
    Environment.SetEnvironmentVariable("USERNAME", Environment.UserName);
    
  3. 加载 SYSTEM 帐户配置文件。我使用了LogonUser/LoadUserProfile原生API方法,如下:

    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool LogonUser(
        string lpszUserName,
        string lpszDomain,
        string lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        ref IntPtr phToken);
    
    [StructLayout(LayoutKind.Sequential)]
    struct PROFILEINFO
    {
        public int dwSize; 
        public int dwFlags;
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpUserName; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpProfilePath; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpDefaultPath; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpServerName; 
        [MarshalAs(UnmanagedType.LPWStr)] 
        public String lpPolicyPath; 
        public IntPtr hProfile; 
    }
    
    [DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool LoadUserProfile(IntPtr hToken, ref PROFILEINFO lpProfileInfo);
    
    var hToken = IntPtr.Zero;
    var hProfile = IntPtr.Zero;
    bool result = LogonUser("SYSTEM", "NT AUTHORITY", String.Empty, 3 /* LOGON32_LOGON_SERVICE */, 0 /* LOGON32_PROVIDER_DEFAULT */, ref token);
    if (result)
    {
        var profileInfo = new PROFILEINFO();
        profileInfo.dwSize = Marshal.SizeOf(profileInfo);
        profileInfo.lpUserName = @"NT AUTHORITY\SYSTEM";
        if (LoadUserProfile(token, ref profileInfo))
            hProfile = profileInfo.hProfile;
    }
    

    将其包装在 IDisposable 类中,并与 using 语句一起使用来构建上下文。

  4. 最重要的 - 重构代码以在子进程中执行必要的数据库更新。这可以是安装程序 DLL 上的简单 exe 包装器,也可以是独立实用程序(如果您已经有的话)。

附:所有这些困难都可以避免,只要微软让用户通过命令行选项选择在哪里创建 LocalDB 实例。例如,就像 Postgres 的 initdb/pg_ctl 实用程序一样。

【讨论】:

  • 非常有见地的答案。我们最终创建了一个用户帐户,以运行我们的 Windows 服务以使用 LocalDB。这有其自身的相关问题。
  • 对您的回答进行澄清:步骤 2 和 3 中的代码,是否应该在自定义操作中运行?这段代码应该在调用子进程之前执行吗?或者它可以在与 LocalDb 接口的同一进程中运行吗?在我到目前为止的测试中,使用系统帐户的 LogonUser 总是返回 1326(无效的用户名或密码)。
  • 是的,所有步骤都应该在自定义操作中运行。您应该在调用子进程之前设置环境并加载 SYSTEM 用户配置文件。我已经修复了在调用 LogonUser 时传递“null”作为密码的代码,它应该是空字符串。然后,在一个子进程中,连接到 LocalDB 并运行您的更新。
  • 我搞定了。 :) 我不需要子进程,设置环境变量后,我可以直接从自定义操作连接到 LocalDB。 LogonUser 部分不起作用,但不是必需的。
  • 我遇到的一个问题是系统配置文件在 32 位和 64 位进程中的路径不同,因此我必须确保将 WiX 自定义操作 DLL 编译为 x86 或 x64 而不是“任何 CPU”取决于我的应用程序是安装为 32 位还是 64 位。32 位系统配置文件路径被虚拟化为 C:\WINDOWS\SysWOW64\... 所以看起来您在 32 位进程中创建的任何 LocalDB 实例只能在 32 位进程中使用,与 64 位进程相同。
【解决方案3】:

我建议使用不同的用户帐户,而不是使用系统帐户,方法是执行以下操作:-

  • 在机器上创建一个新账户,并将其设置为该账户 Windows 服务在其下运行。使用它不是一个好习惯 系统帐户只是为了运行应用程序,无论如何,作为 权限过多。
  • 确保将 LocalDB 文件的权限设置为允许所述用户帐户访问数据库(从而继续 使用集成安全性)
  • 通过运行sqlcmd 或 Management Studio 尝试连接到同一用户帐户下的数据库(安装后)以确保其正常工作 在所述用户的上下文中,然后与 Integrated 确保其正常运行的安全性。

其他一些可以尝试/考虑的事情:

  • 您是否检查过 Windows 事件日志中是否有任何可能对诊断有用的事件?
  • 确保如果您有用于命令行工具的任何其他版本的 SQL Server(尤其是 2012 之前的版本),则 %PATH% 未设置为首先查找较旧的工具版本。旧工具不支持 LocalDB。
  • 也可以(作为替代)设置 LocalDB 以与其他用户共享。这涉及共享实例,然后授予其他用户访问权限。请参阅本文中的“共享问题”部分:Troubleshoot SQL Server 2012 Express LocalDB

还有another SO article,其中的链接可能包含一些更有用的信息(通过将 pl-pl 更改为 en-us 将 URL 中的语言从波兰语更改为英语)。他的解决方法是使用 SQL Server 帐户,在您的情况下这可能不合适。

这也可能很有用,因为它与安全权限被拒绝以及可能的解决方案有关:https://dba.stackexchange.com/questions/30383/cannot-start-sqllocaldb-instance-with-my-windows-account

【讨论】:

    【解决方案4】:

    Trevor,您遇到的问题在于 MSI 自定义操作。您必须使用“Impersonate=false”对其进行配置,否则自定义操作将在当前用户上下文下执行。

    顺便说一句,您使用什么工具来创建安装程序? 根据您使用的工具,您能否提供自定义操作配置的屏幕截图或代码 sn-ps?

    这篇文章中接受的答案将为您提供一些有关不同自定义操作执行替代方案的额外信息: Run ExeCommand in customAction as Administrator mode in Wix Installer

    您可以在此处找到有关模拟的更多信息: http://blogs.msdn.com/b/rflaming/archive/2006/09/23/768248.aspx

    【讨论】:

      【解决方案5】:

      我不会在系统的 localdb 实例下创建数据库。我会在安装产品的当前用户下创建它。如果您需要删除或管理数据库,这将使生活变得更加轻松。他们可以通过 sql management studio 做到这一点。否则,您将不得不使用 psexc 或其他工具在 SYSTEM 帐户下启动一个进程来管理它。

      创建数据库后,请使用您提到的共享选项。然后 SYSTEM 帐户可以通过共享名称访问数据库。

      sqllocaldb 共享 MSSqlLocalDb LOCAL_DB

      共享时,我注意到您必须重新启动本地数据库实例才能通过共享名称实际访问数据库:

      sqllocaldb 停止 MSSQLLocalDB

      sqllocaldb 启动 MSSQLLocalDB

      另外,您可能需要将 SYSTEM 帐户作为数据库读取器和写入器添加到数据库...

      EXEC sp_addrolemember db_datareader, 'NT AUTHORITY\SYSTEM'

      【讨论】:

      • 这对于某些情况可能就足够了。但是,我发现它在真正的多用户场景中不可行,因为实例在所有者(即创建它的用户)注销时停止。而且它只能由所有者重新启动。即使实例是共享的也是如此。
      • 关于我之前的评论:我想可以配置在所有者/创建者的凭据下运行的 Windows 服务,这样即使在他们注销时实例也会运行,但我觉得这种解决方案令人反感,因为所有者必须记住在更改网络密码时更改服务登录密码。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-03-16
      • 2018-02-04
      • 1970-01-01
      • 1970-01-01
      • 2016-01-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多