【问题标题】:Sharing SQL Server Session State across Web Applications跨 Web 应用程序共享 SQL Server 会话状态
【发布时间】:2012-10-29 21:56:00
【问题描述】:

我正在设置一个非常基本的 SQL Server 会话状态演示,但在使其正常工作时遇到了一些问题。我正在尝试使用 IIS 7.5 和 SQL Server 2008 R2 在本地运行 Windows 7 进行测试。

最终,我需要一种方法来跟踪登录到在几个不同 Web 服务器之间进行负载平衡的系统的用户数量。因此,每次用户登录或注销时,我都需要更新一个会话变量(存储在 SQL 中)。所以会话 ID 可能总是不同的。这可能吗?

这是我到目前为止所做的:

  1. 在 IIS 中创建了两个新站点(Site1 和 Site2)
  2. 在 VS 2010 中创建了两个新的 Web 应用程序项目(Site1 和 Site2)
  3. 在这两个站点的 Default.aspx 页面上,我创建了一个按钮,单击该按钮时会将“Site1”或“Site2”(取决于站点)保存到名为“LastSiteUsed”的会话变量中。还有另一个按钮,当单击该按钮时,将从“LastSiteUsed”会话变量中读取值并将其显示在标签中。
  4. 在 SQL 中,我创建了一个名为 dbSessionTest 的新数据库。然后我按照MSDN to install the Session State Database using the aspnet_regsql tool 上的说明进行操作。
  5. 在我的两个 Web 应用程序的 Web.config 文件中,我在 <System.Web> 下添加了以下内容:

<sessionState mode="SQLServer" sqlConnectionString="server=.\sqlexpress;database=dbSessionTest;uid=myUsername;pwd=myPassword" cookieless="false" timeout="20" allowCustomSqlDatabase="true"/>

两个站点都正常运行,但它们似乎没有共享会话。例如,当我单击按钮从 Site1 保存会话变量时,我希望能够从 Site2 读取该值,但它不起作用。 Site2 只会返回“Site2”,Site1 只会返回“Site1”。

有没有人知道我可能做错了什么?我是否认为我应该能够从 Site2 读取 Site1 设置的值?

更新:

我可以看到会话数据存储在 SQL Management Studio 的 ASPStateTempSessions 表中,但每个站点仍然只能看到它写入的值。两个站点都像这样设置会话变量:

Session("LastSiteUsed") = "Site2"

两个网站都在检索如下值:

lblValue.Text = "Value from Session: " & Session("LastSiteUsed").ToString

访问存储在 SQL Server 中的会话变量是否需要以不同方式执行此操作?

更新 2:

我尝试使用默认数据库 ASPState,它是通过运行以下命令创建的:

aspnet_regsql.exe -S MyServerName -E -ssadd -sstype p

然后简化我的每个 Web.config 文件,例如:

<sessionState mode="SQLServer" sqlConnectionString="Data Source=.\sqlexpress;User ID=myUsername;Password=myPassword" cookieless="false" timeout="20"/>

但同样,没有运气。我希望能够从 Site1 设置会话变量,然后从 Site2 读取值,但它不起作用。同样,我可以看到 ASPStateTempSessions 表中显示的条目,因此我知道它们输入正确。我注意到的一件事是他们有不同的会话 ID?

在设置/读取相同的会话变量时,我是否需要做一些不同的事情来确保在我的站点之间使用相同的会话 ID?

更新 3:

我已按照 this article 的说明修改了 SP 并将用于分组的列添加到 ASPStateTempApplications 表中。这种有效,但前提是我的两个网站都在同一个浏览器中打开(使用标签)。我需要能够在 IE 中打开 Site1,将值保存到我的会话变量,关闭浏览器,在 Chrome 中打开 Site2,然后读取值。从我用 SQL Server 会话状态阅读的所有内容来看......这应该是可能的。

更新 4 - 解决方案:

我关注了@SilverNinja 提供的this article 上的答案。我通过命令提示符重新创建了ASPState 数据库(以撤消更新#3 中我的SP 更改),然后根据链接中的答案修改了TempGetAppID SP。我还更新了我的两个 Web.config 文件以匹配该文章中的答案:

<sessionState mode="SQLServer"
              sqlConnectionString="Data Source=.\sqlexpress;User ID=myUsername;Password=myPassword;Application Name=CacheTest"/>

我还在两个 Web.config 文件中包含相同的机器密钥:

<machineKey validationKey="59C42C5AB0988049AB0555E3F2128061AE9B75E3A522F25B34A21A46B51F188A5C0C1C74F918DFB44C33406B6E874A54167DFA06AC3BE1C3FEE8506E710015DB" decryptionKey="3C98C7E33D6BC8A8C4B7D966F42F2818D33AAB84A81C594AF8F4A533ADB23B97" validation="SHA1" decryption="AES" />

现在我可以(使用 IE)打开我的两个站点,从 Site1 设置一个值,然后从 Site2 读取它(反之亦然)。当我检查表格时,只有一个 SessionID 存在(两个站点都正确使用相同的会话)。在打开一个新浏览器(例如,使用 Chrome)之前,我的想法是错误的,它会使用相同的会话 - 一个新的浏览器将启动它自己的会话。不过,在负载平衡的情况下,这不会导致任何问题。

【问题讨论】:

  • Application Name=CacheTest 对于所有 Web 应用程序(网站)都一样吗?或不同的值

标签: asp.net sql-server iis-7.5 session-variables session-state


【解决方案1】:

您遇到的问题是在 ASP.NET 应用程序之间共享会话状态。 SQL Server 会话提供程序不支持开箱即用。看到这个SO post regarding what changes to make to SQL Server session provider to support cross-application sessions

我不会使用会话来管理这个共享状态(即LastSiteUsed)。此信息应与用户配置文件存储(MembershipProfile provider、自定义数据库等)一起保存。由于会话可能过期 - 跨应用程序使用共享会话不是跟踪持久用户特定状态的可靠机制。如果持久性不重要,则可以使用 AppFabric Cache 来实现内存中跨应用程序共享状态。

【讨论】:

  • 感谢您的回复!我认为如果会话过期(如果我理解正确的话)应该没问题,因为我最终只是跟踪登录的并发用户数。话虽如此,您的第一个链接是否足以完成我需要的事情(特别是该链接的答案)?
  • FWIW 我继续尝试了该解决方案,但没有成功。再想一想,情况就不一样了。他们想在同一台服务器上共享会话。我需要在多个负载平衡服务器之间共享一个会话变量,存储在 SQL 中。好像应该可以吧?
  • 如果服务器是负载平衡的 - 您只需要确保它们的web.configmachine.config 中的所有应用程序都具有相同的机器密钥,以便访问共享会话。无论是同一台服务器还是不同的服务器——只要密钥相同就没有关系。
  • 嗯。我在我的两个 web.config 文件中为 Site1 和 Site2 设置了相同的机器密钥(它们是相同的)。机器密钥是否规定了使用的 SessionID?我是否还需要在每个 web.config 中设置一个应用程序名称(我认为也需要相同)?
  • 如果键相同 - 您只需在每个示例的连接字符串中指定一个 Application Name
【解决方案2】:

对于任何想要或需要在不修改数据库存储过程的情况下解决此问题的人,请考虑这种方法(不适合胆小的人)。

基本思想是在System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo 中存储了一个SqlSessionStateStore.SqlPartitionInfo 的静态实例,我通过调用InitSqlInfo 对其进行初始化。初始化此实例后,我设置内部 _appSuffix 字段的值。在设置_appSuffix字段之前需要初始化实例,否则我的自定义值将在初始化期间被覆盖。我使用与 ASP.NET 状态数据库中相同的哈希函数根据提供的应用程序名称计算哈希码。

请注意,这很糟糕,并且完全依赖于可能随时更改的内部实现细节。但是,我不知道任何实用的替代方案。自行决定使用并承担风险。

现在我已经给出了我的免责声明,如果 .NET BCL 中的这种实现确实发生了变化,我会感到惊讶,因为这种身份验证方法正在被替换,我认为微软没有任何理由在其中进行修补。

/*
 * This code is a workaround for the fact that session state in ASP.NET SqlServer mode is isolated by application. The AppDomain's appId is used to
 * segregate applications, and there is no official way exposed to modify this behaviour.
 * 
 * Some workarounds tackle the problem by modifying the ASP.NET state database. This workaround approaches the problem from the application code
 * and will be appropriate for those who do not want to alter the database. We are using it during a migration process from old to new technology stacks, 
 * where we want the transition between the two sites to be seamless.
 * 
 * As always, when relying on implementation details, the reflection based approach used here may break in future / past versions of the .NET framework.
 * Test thoroughly.
 * 
 * Usage: add this to your Global.asax:
 *       protected void Application_BeginRequest()
 *       {
 *           SessionStateCrossApplicationHacker.SetSessionStateApplicationName("an application");
 *       }
 */

using System;
using System.Data.SqlClient;
using System.Globalization;
using System.Reflection;
using System.Web.SessionState;

public static class SessionStateCrossApplicationHacker
{
    static string _appName;
    static readonly object _appNameLock = new object();

    public static void SetSessionStateApplicationName(string appName)
    {
        if (_appName != appName)
        {
            lock (_appNameLock)
            {
                if (_appName != appName)
                {
                    SetSessionStateApplicationNameOnceOnly(appName);

                    _appName = appName;
                }
            }
        }
    }

    static void SetSessionStateApplicationNameOnceOnly(string appName)
    {
        //get the static instance of SqlSessionStateStore.SqlPartitionInfo from System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo
        var sqlSessionStateStoreType = typeof (SessionStateMode).Assembly.GetType("System.Web.SessionState.SqlSessionStateStore");
        var sqlSessionStatePartitionInfoInstance = GetStaticFieldValue(sqlSessionStateStoreType, "s_singlePartitionInfo");
        if (sqlSessionStatePartitionInfoInstance == null)
            throw new InvalidOperationException("You'll need to call this method later in the pipeline - the session state mechanism (SessionStateModule, which is an IHttpModule) has not been initialised yet. Try calling this method in Global.asax's Application_BeginRequest. Also make sure that you do not specify a partitionResolverType in your web.config.");

        //ensure that the session has not been used prior to this with an incorrect app ID
        var isStaticSqlPartitionInfoInitialised = GetFieldValue<bool>(sqlSessionStatePartitionInfoInstance, "_sqlInfoInited");
        if (isStaticSqlPartitionInfoInitialised)
            throw new InvalidOperationException("You'll need to call this method earlier in the pipeline - before any sessions have been loaded.");

        //force initialisation of the static SqlSessionStateStore.SqlPartitionInfo instance - otherwise this will happen later and overwrite our change
        var connectionString = GetFieldValue<string>(sqlSessionStatePartitionInfoInstance, "_sqlConnectionString");
        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();
            CallInstanceMethod(sqlSessionStatePartitionInfoInstance, "InitSqlInfo", connection);
        }

        //calculate and set the application hash code
        string applicationNameHashCode = GetHashCode(appName).ToString("x8", CultureInfo.InvariantCulture);
        GetField(sqlSessionStatePartitionInfoInstance, "_appSuffix").SetValue(sqlSessionStatePartitionInfoInstance, applicationNameHashCode);
    }

    static int GetHashCode(string appName)
    {
        string s = appName.ToLower();
        int hash = 5381;
        int len = s.Length;

        for (int i = 0; i < len; i++)
        {
            int c = Convert.ToInt32(s[i]);
            hash = ((hash << 5) + hash) ^ c;
        }

        return hash;
    }

    static void CallInstanceMethod(object instance, string methodName, params object[] parameters)
    {
        var methodInfo = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
        methodInfo.Invoke(instance, parameters);
    }

    static object GetStaticFieldValue(Type typeWithStaticField, string staticFieldName)
    {
        return typeWithStaticField.GetField(staticFieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
    }

    static FieldInfo GetField(object instance, string name)
    {
        return instance.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
    }

    static T GetFieldValue<T>(object instance, string name)
    {
        return (T)GetField(instance, name).GetValue(instance);
    }
}

【讨论】:

  • 每个web应用程序都有自己的appName ?
猜你喜欢
  • 2011-02-21
  • 2011-01-25
  • 2011-03-07
  • 1970-01-01
  • 2013-04-10
  • 1970-01-01
  • 2012-04-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多