2

几年前我发现了 ASP.NET 配置文件,并发现在我的网站中实现一些强大的功能既快捷又容易。我最终只使用“匿名”配置文件属性,因为我没有使用 ASP.NET 成员资格,并且想要跟踪信息并为我的站点用户提供个性化,即使他们没有登录。

快进 3 年,现在我正在努力将我的 aspnetdb 数据库的大小保持在我的托管服务提供商的 3 GB 限制内。一年前我决定只保留 6 个月的配置文件数据,现在我每个月都必须运行 aspnet_Profile_DeleteInactiveProfiles 过程,然后向我的主机提出支持请求以截断数据库文件。

真正的缺点是现在我已经实现了这个功能,网站依赖于这个数据库,每次我需要截断时都会出现停机时间。今天真的很成功 - 在维护 aspnetdb 时,站点关闭了 12 个多小时,我最终创建了 aspnetdb 数据库的故障转移副本,这样我就可以再次使网站上线(原来的 aspnetdb 数据库仍然关闭在我写这篇文章的时候)。

去年我重写了一些功能,因为它存储了一个小的二进制对象集合,我将它转换为存储逗号分隔的 ID 字符串,使其更适合存储。

我已经考虑过获得更多磁盘空间,但现实情况是,拥有一个每年增长 4 GB 的数据库只是为了做一些简单的事情,比如保留每个用户最近查看的产品的列表、跟踪他们在哪个类别中的“继续购物”按钮将他们带回来,并存储他们购物车中的总金额(仅限货币)。实际上,只有不到 5% 的数据被网站的重复访问者重复使用,但是无法提前知道哪些用户会返回访问他们的个人资料信息。

所以我的问题是,是否有更有效的方法来存储配置文件数据,这样它就不会占用太多空间,或者是否有替代方法可以为每个用户创建配置文件数据库记录以支持此功能?

我查看了table profile provider,但是有人可以保证使用它是否会使用比默认提供程序更少的磁盘空间来存储数据?

更新:

我做了一些研究,似乎 nvarchar(max) 字段是 aspnetdb 的默认实现使用的 ntext 字段的直接替换,但根据这篇文章这篇文章只需要一半的磁盘空间。这意味着我应该能够创建一个新的 aspnetdb 副本,在其架构和代码中修改数据类型,并将数据传输到新数据库中。这应该修复数据的存储方式以及执行收缩操作时发生的任何碎片。

我现在已经完成了测试并将这个解决方案投入生产。起初我以为会有问题,因为 Microsoft 将 NText 数据类型硬编码到 SqlProfileProvider 内部的存储过程调用中。但是,事实证明 SQL Server 会将 NText 参数隐式转换为 nvarchar(MAX)。简而言之,除了aspnet_Profiles 表的PropertyNames 和PropertyValuesString 列上的类型之外,我不需要更改任何内容。

必须将数据重写到磁盘以释放空间,但我最终仅通过更改数据类型就节省了大约 30%。

另一个更新:

我还发现,虽然我每天有大约 700 名独立访问者,但 aspnet_Users 表中的平均每日“用户”数平均约为 4000-5000。我推测这是由于某些用户在浏览时没有使用 cookie。我做了一些测试,发现如果配置文件没有更新,则不会创建用户,但是如果您写入配置文件,如果未启用 cookie,则会在每个请求上创建用户(和配置文件)。

我正在为此制定解决方法。我正在尝试为对 webmethod 的 AJAX 调用编写 javascript。理论上,如果启用了 cookie,第二个请求(AJAX 调用)应该有 .ASPXANONYMOUS cookie,因此我可以安全地写入配置文件。只有当这是会话的开始时(由 Session.IsNewSession 属性确定),javascript 才会被注入页面。用户永远不应该知道 AJAX 调用 - 它只是告诉页面是否启用了 cookie,以便它可以更新配置文件。

当然,每个后续请求都可以简单地检查 cookie 集合以确保 .ASPXANONYMOUS cookie 存在。我很可能会创建一个共享函数,该函数既可以被称为 webmethod 的 AJAX 调用,也可以被 Page_Load 事件直接调用,并使用 IsNewSession 属性来确定允许运行 cookie 检查和后续配置文件更新的请求。

根据微软的说法,使用 session 来跟踪是否启用了 cookie 会破坏目的(因为 session 取决于 cookie)。他们建议将您的 AreCookiesEnabled 值保存到数据库中,但他们没有提及您应该如何跟踪保存值的位置 - 如果未启用 cookie,您如何将请求链接到数据库记录?

4

1 回答 1

1

我实施了我在原始帖子中提出的解决方法,但结果与我最初描述的有所不同。该修复实际上可以分为两部分 - 一个修复禁用 cookie 时更新数据库的问题,第二个用于检测何时禁用 cookie 而无需执行重定向。

我已经将解决​​方案发布到匿名配置文件在禁用 cookie 时创建记录

所以现在我将专注于第二部分 - 从请求的第一页获取信息到配置文件中。仅当您正在执行分析跟踪或类似操作时才需要这样做 - 第一部分将负责保护数据库在 1) 禁用 cookie 和 2) 使用匿名配置文件属性并从第二个请求(或第一个回发)开始。

当我研究检查是否启用 cookie 的问题时,大多数解决方案都使用重定向到同一页面或不同页面并再次返回。有趣的是,MSDN是提出 2-redirect 解决方案的人。

虽然在某些情况下重定向是可以接受的,但我不希望额外的性能影响影响我们的大多数用户。相反,我选择了另一种方法——在第一个请求完成后,使用 AJAX 在服务器上运行代码。虽然这具有不会导致重定向的优点,但它的缺点是在禁用 JavaScript 时无法运行。但是,我选择了这种方法,因为在初始请求时丢失的数据百分比微不足道,并且应用程序本身不依赖于这些数据。

所以,从过程的开始到结束...

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    If Not Me.IsPostBack Then

        If Session.IsNewSession Then
            Me.InjectProfileJavaScript()
        ElseIf AnonymousProfile.IsAnonymousCookieStored Then
            'If cookies are supported, and this isn't the first request, update the
            'profile using the current page data.
            UpdateProfile(Request.RawUrl, Request.UrlReferrer.OriginalString, CurrentProductID.ToString)
        End If

    End If

End Sub

这是放置在我的自定义 PageBase 类中的 Page_Load 方法,项目中的所有页面都从该类继承。我们首先通过检查 Session.IsNewSession 属性来检查这是否是一个新会话。如果禁用 cookie 或这是第一个请求,则此属性始终为 true。在这两种情况下,我们都不想写入数据库。

如果客户端接受了会话 cookie 并且这不是对服务器的第一个请求,则“else if”部分运行。关于此代码片段需要注意的是,这两个部分不能在同一个请求中运行,这意味着每个请求只能更新配置文件 1(或 0)次。

AnonymousProfile 类包含在我的另一篇文章中。

Private Sub InjectProfileJavaScript()

    Dim sb As New StringBuilder

    sb.AppendLine("$(document).ready(function() {")
    sb.AppendLine("  if (areCookiesSupported() == true) {")
    sb.AppendLine("    $.ajax({")
    sb.AppendLine("      type: 'POST',")
    sb.AppendLine("      url: 'HttpHandlers/UpdateProfile.ashx',")
    sb.AppendLine("      contentType: 'application/json; charset=utf-8',")
    sb.AppendFormat("      data: ""{3}'RawUrl':'{0}', 'ReferralUrl':'{1}', 'ProductID':{2}{4}"",", Request.RawUrl, Request.UrlReferrer, CurrentProductID.ToString, "{", "}")
    sb.AppendLine()
    sb.AppendLine("      dataType: 'json'")
    sb.AppendLine("    });")
    sb.AppendLine("  }")
    sb.AppendLine("});")

    Page.ClientScript.RegisterClientScriptBlock(GetType(Page), "UpdateProfile", sb.ToString, True)

End Sub

Public Shared Sub UpdateProfile(ByVal RawUrl As String, ByVal ReferralUrl As String, ByVal ProductID As Integer)
    Dim context As HttpContext = HttpContext.Current
    Dim profile As ProfileCommon = CType(context.Profile, ProfileCommon)

    Dim CurrentUrl As New System.Uri("http://www.test.com" & RawUrl)
    Dim query As NameValueCollection = HttpUtility.ParseQueryString(CurrentUrl.Query)
    Dim source As String = query.Item("source")
    Dim search As String = query.Item("search")
    Dim OVKEY As String = query.Item("OVKEY")

    'Update the profile
    profile.TestValue1 = source
    profile.TestValue2 = search

End Sub

接下来,我们有将 AJAX 调用注入页面的方法。请记住,这仍然是基类,因此无论用户进入哪个页面,此代码都将在第一个页面请求上运行。

在 JavaScript 内部,我们首先测试是否启用了 cookie,如果启用,则使用 AJAX 和 JQuery 在服务器上调用自定义处理程序。我们将来自服务器的参数传递到此代码中(尽管其中 2 个可能刚刚由客户端提供,但额外的字节并不那么重要)。

第二种方法更新配置文件并将包含我的自定义逻辑来执行此操作。我包含了一个关于如何从部分 URL 解析查询字符串值的片段。但这里唯一真正需要知道的是,这是更新配置文件的共享方法。

重要提示:对于函数的 AJAX 调用,必须将以下处理程序添加到 web.config 文件的 system.web 部分:

<httpModules>
    <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</httpModules>

我决定最好在客户端测试 cookie,如果禁用 cookie,则不要进行额外的 AJAX 调用。要测试 cookie,请使用以下代码:

function areCookiesSupported() {
    var c='c';var ret = false;
    document.cookie = 'c=2;';
    if (document.cookie.indexOf(c,0) > -1) {
        ret = true;
    } else {
        ret = false;
    }
    deleteCookie(c);
    return ret
}
function deleteCookie(name) {
    var d = new Date();
    document.cookie = name + '=1;expires=' + d.toGMTString() + ';' + ';';
}

这是 2 个 JavaScript 函数(在自定义 .js 文件中),它们只需编写一个 cookie 并将其读回以确定是否可以读取 cookie。然后它通过设置过去的过期日期来清理 cookie。

<%@ WebHandler Language="VB" Class="Handlers.UpdateProfile" %>

Imports System
Imports System.Web
Imports System.Web.SessionState
Imports Newtonsoft.Json
Imports System.Collections.Generic
Imports System.IO

Namespace Handlers

    Public Class UpdateProfile : Implements IHttpHandler : Implements IRequiresSessionState

        Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest

            If AnonymousProfile.IsAnonymousCookieStored Then

                If context.Session.IsNewSession Then
                    'Writing to session state will reset the IsNewSession flag on the
                    'next request. This will fix a problem if there is no Session_Start
                    'defined in global.asax and no other session variables are written.
                    context.Session("ActivateSession") = ""
                End If

                Dim reader As New StreamReader(context.Request.InputStream)
                Dim params As Dictionary(Of String, String) = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(reader.ReadToEnd())

                Dim RawUrl As String = params("RawUrl")
                Dim ReferralUrl As String = params("ReferralUrl")
                Dim ProductID As Integer = params("ProductID")

                PageBase.UpdateProfile(RawUrl, ReferralUrl, ProductID)
            End If
        End Sub

        Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
            Get
                Return False
            End Get
        End Property

    End Class

End Namespace

这是我们接收 AJAX 请求的自定义 HttpHandler 类。仅当传入 .ASPXANONYMOUS cookie 时才会处理请求(通过使用我另一篇文章中的 AnonymousProfile 类再次检查),这将阻止机器人和其他脚本执行它。

接下来,如果需要,我们运行一些代码来更新会话对象。出于某种奇怪的原因,IsNewSession 值将保持为真,直到会话实际更新,但前提是 Global.asax 中不存在 Session_Start 的处理程序。因此,为了使此代码在有和没有 Global.asax 文件以及没有任何其他更新会话对象的代码的情况下都能正常工作,我们在此处运行更新。

我从这篇文章中获取的下一段代码包含对 JSON.NET 序列化程序的依赖。由于额外的依赖性,我对使用这种方法感到很痛苦,但最终决定 JSON 序列化程序在未来可能会很有价值,因为我会继续向网站添加 AJAX 和 JQuery。

然后我们只需获取参数并将它们传递给我们之前定义的 PageBase 类中的共享 UpdateProfile 方法。

<!-- Required for anonymous profiles -->
<anonymousIdentification enabled="true"/>
<profile defaultProvider="SqlProvider" inherits="AnonymousProfile">
    <providers>
        <clear/>
        <add name="SqlProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="SqlServices" applicationName="MyApp" description="SqlProfileProvider for profile test web site"/>
    </providers>
    <properties>
        <add name="TestValue1" allowAnonymous="true"/>
        <add name="TestValue2" allowAnonymous="true"/>
    </properties>
</profile>

最后,我们有配置文件属性的配置部分,设置为匿名使用(我故意省略了连接字符串部分,但还需要相应的连接字符串和数据库)。这里要注意的主要事情是在配置文件中包含继承属性。这又是针对在我的另一篇文章中定义的 AnonymousProfile 类。

于 2010-10-10T23:09:16.267 回答