Pfz.Caching - 使用 ViewId 而非 ViewState






4.85/5 (12投票s)
用于缓存数据的框架,包括将 ViewStates 存储在文件中的可能性,重用相同的文件以避免过多的硬盘使用。
引言
这是一个用于通用缓存可序列化对象的框架。它有一个通用的 Cache
类,可以在内存/磁盘中缓存任何对象。如果对象不再内存中,它将从磁盘读取。
它还有一个 CacheDictionary
,功能相同,但对小数据有一些优化,避免生成过多的“缓冲区文件”。
最后,最棒的部分来了:在项目中添加 App_Browsers
,您可以让所有 ViewStates 使用这项技术,这样只有很小的 ViewId
会被发送到客户端,而不是完整的 ViewState。此外,它会在一段时间后删除未使用的文件,并重用相同的文件,避免浪费硬盘空间。
背景
我在 Web 开发中并没有遇到太多问题。我开始编程时,电脑只有 1MB 或 2MB 内存。但是,我看到人们普遍对何时存储数据感到困惑。关于 ViewState 和 Session 的文档也很令人困惑,它们看起来可以互换,但实际上并非如此。
我首先创建了一个用于分页记录集的缓存技术。
然后,我创建了用于任何数据的缓存技术。
最后,是用于 ViewStates 的模块。它非常棒,并且已经稳定运行一年多了。
工作原理(基础)
实现可能非常复杂,因为代码是线程安全的,并且您需要正确管理垃圾回收,但原理非常简单。
- 每个“缓冲区”对象都会被序列化,并为其缓冲区创建一个哈希码(校验和)。如果该缓冲区的目录存在,我将检查是否有具有相同长度的缓冲区,如果有,并且是相同的,则避免创建新的缓冲区,或者我创建一个新的唯一 ID,保存文件并返回该缓冲区的 ID 和哈希码。这在所有 Session 中共享,但由于数据是只读的,所以没有问题。
- 项 1 是
BufferId
,而不是ViewId
。现在,有了bufferId
,我将尝试查找当前 Session 中是否存在包含该bufferId
的内存中的ViewState
信息。如果没有,将生成一个新的ViewId
。如果存在,将返回旧的 ID。显然,每次重用文件时,我都会执行一次 keep-alive(在文件上更新文件的日期/时间,在内存中调用GCUtils.KeepAlive
)。 - 每个缓冲区也使用弱引用保存在内存中,但为了防止最近使用的缓冲区被回收,我调用
GCUtils.KeepAlive
。GCUtils.KeepAlive
(而不是GC.KeepAlive
)保证对象将在下一次垃圾回收中存活。 ViewId
是为sessionId
创建的,所以不会出现一个用户获取另一个用户viewstate
的问题,即使内部缓冲区相同(在这种情况下,每个用户将有不同的ViewId
,但它们将指向同一个缓冲区)。此外,每个页面都会生成一个新的viewstate
,所以我将尝试获取现有viewstate
的 ID(如果可能),或者创建一个新文件。- 默认情况下,
FileCachePersister
每 30 分钟运行一次清理进程,删除超过 4 小时的文件。这与 Session 过期时间无关。
工作原理(高级)
此时,我不会详细解释 CacheDictionary
(它实际上是 Cache
对象的字典,但有一些优化)的细节,也不会解释 WeakDictionary
,因为其本身的解释会比整篇文章还长。但重要的是要知道,它是一个允许垃圾回收器收集其项目的字典,但会保持最近使用的值存活。
我将开始解释 CacheManager
。CacheManager
类负责加载和保存缓冲区(字节),因为它不了解对象的实际类型。最重要的东西是一个 WeakDictionary
,其中键是缓冲区的哈希码,值是 ID 和序列化字节的字典。其内部函数尝试使用哈希码和缓冲区 ID 在内存中查找值,如果没有找到,则请求持久化器加载它,然后存储加载的值(如果有),并对其进行 KeepAlive
。Save
函数执行类似的过程,尝试在内存中查找兼容的缓冲区,以重用 ID,或者如果没有找到,则调用持久化器保存并返回生成的 ID。
通用的 Cache
类具有序列化和反序列化缓冲区的能力。它使用 CacheManager
来读取或写入这些缓冲区,但 Cache
本身有自己的 WeakReference
指向实际对象。这样做是因为,在反序列化缓存对象时,只需要对象的 ID,而不是实际对象。如果将太多“相同”的对象放入缓存,所有缓存对象的实际对象都可能被回收,但生成它们的缓冲区可能仍在内存中。这看起来有些冗余,但在我的经验中,事实并非如此。
那么,你创建一个对象的 Cache
。cache
将对象序列化并调用 CacheManager
,CacheManager
将尝试重用相同缓冲区的 ID 或请求保存它…但它会被保存在哪里?
这就是持久化器的作用。在框架中,唯一存在的且已经可用的持久化器是 FileCachePersister
。它简单地接收参数并尝试加载文件,如果存在,则返回 null
。它接收文件名并尝试更新其日期/时间以使其保持存活,或者保存文件。这样做是为了方便你创建持久化器来将数据存储到数据库,或使用远程服务器,它也可以有自己的缓存,避免并发进程访问相同文件。这很重要,因为 FileCachePersister
在多线程环境下工作得非常好,但只有一个进程必须使用该目录。
好的,在 FileCachePersister
中有一个线程用于删除旧文件,但这并不是什么复杂的事情。
ViewState
ViewState
解决方案与 Cache
解决方案非常相似,但它还包含额外的安全信息。负责加载和保存 ViewState
s 的类是 PfzPageStatePersister
。与 CacheManager
类类似,它有一个由 SessionId
s 组成的字典,所以 ViewId
s 对当前 Session 是独占的,然后值是 ViewId
s 的字典,该字典的值是生成 ViewState
的页面类型(因此将 ViewId
复制到另一个页面将不起作用),以及 ViewState
的实际信息。
或者,更好的是,一个指向该值的缓存。为什么?因为如果你从一页转到另一页,生成相同的 viewstates,只会生成一个新的缓冲区“引用”,但该缓冲区(可能非常大)是相同的。它看起来有点复杂,因为它有一个 Pair,但这是因为 PageStatesPersisters
的通用工作方式,它们只生成两个对象,其唯一目的是被序列化。说实话,不太友好。
但,这里的想法是一样的:
查看 ViewState
是否在内存中。如果不在,则请求 Persister
加载它。
保存时,在内存中搜索一个相同的 ViewState
,或者创建一个新的,调用 persister
来保存它。
Using the Code
CacheManager
类是这一切的起点。如果你只使用框架来将 viewstate
s 保存到文件中,那么它也在这里结束。在 Global.asax 文件中,添加以下内容(或类似内容):
CacheManager.Persister = new FileCachePersister
("c:\\temp\\PfzCachingWebApplication_ViewStates\\");
CacheManager.Start();
其他有趣的事情
GCUtils
- 我已经在另一篇文章中介绍过这个类,但现在它已被修改。通过注册到 Collected
事件,您可以了解最近发生的垃圾回收,如果您有任何额外的内存可以释放(例如调用 TrimExcess
)。GCUtils.KeepAlive
也非常有用,可以告知 GarbageCollector
避免回收最近使用的对象,即使它们只有弱引用。
Cache
类用法 - 您可以在 Windows Forms 中使用 Cache
类,或者当您确实需要将一些大信息放入 Session
,而不是放入 ViewState
。
例如,当您将对象放入 Session 时,您会创建一个 cache
对象并将 cache
对象放入 Session 中。
Session["MyVeryLargeItem"] = new Cache<byte[]>(new byte[5000000]);
读取时,您这样做:
byte[] data = ((Cache<byte[]>)Session["MyVeryLargeItem"]).Target;
在普通代码中,您仍然需要进行类型转换,但您通常不会创建缓存,也不会将其转换回缓存来读取目标。但是,这个额外的步骤会让您的 5MB 对象在 Session 中变成大约 32 字节,而 5MB 则保存在文件中。
后续相关主题文章
在未来的文章中,我计划用好的例子来解释 Cache
类、CacheDictionary
、WeakDictionary
以及特别是 StatedPage
的用法。但这些是我个人框架的一部分,并非真正属于 ViewState
解决方案,因为它们独立于 ViewState
,甚至可以在非 Web 应用程序中使用。
在这篇文章中,我只想展示 ViewState
解决方案,它确实有效(并且已经长期投入生产环境),并且不会改变编程方式,除了现在您可以分页记录并将整个数据集存储在 ViewState
中,因为它不会被发送到客户端。
历史
- 2009 年 10 月 7 日:初始发布