锁定老帖子 主题:当心跨页面状态传递设计中潜在的内存溢出
精华帖 (0) :: 良好帖 (0) :: 新手帖 (3) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2008-12-31
最后修改:2009-12-21
前两周做的一个 Web 应用系统项目中,遇到了一个由于跨页面状态传递机制设计不合理,造成内存泄露的小问题 。有这里做以记录,欢迎大家一同探讨,同时在本文的后面探讨了解决方案,并详细探讨了一个自定义 Session 实现并提供了完整代码 。
闭话少絮,描述问题请先看图。
上面的序列图中描述的一个这样特点的业务:
相信明眼人已经看出来了,这个设计由于一些原因将一项业务分解到二个页中完成,就必然涉及到一次状态传递,因此一旦用户在完成页面1但又不提交页面2(即二步操作被切断,业务终止),则由页面1保存的状态数据就不会被释放(即方法3.4不会“如期”执行)。由于这里的方案是采用了 Session 作为状态数据容器,所以这些无用的对象最终会在 Session 过期后由后台守护线程所清除。但是,这里又有了另外的一个问题,也是我真正所要说的,Session 中对象过期是有时间的,一般都在几十分钟,往往默认都在20、30分钟,有的可能更长。那么结合到上述 Web 应用的结果的,只要在这几十分钟的 Session 有效期内、只访问到该业务页面1的并发用户压力足够大、同时保存到 Session 中的状态数据(由方法1.3存入)占用的内存足够大(往往都不小),就会使内存溢出。结果就是性能逐步下降,最终导致 core dump 的发生。
接下来讨论一下可行的解决方案。实际上替代的方案真的不少,可以大致罗列一下:
下面的代码所描述的就是这样一个容器对象,Java 平台的兄弟们看个意思吧。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace com.lzy.javaeye { public interface ISessionId<T> { T Value { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace com.lzy.javaeye { public interface ISessionEntry<T> { ISessionId<T> SessionId { get; set; } DateTime LastAccessTime { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace com.lzy.javaeye { public interface ISession<T, U> where T : ISessionEntry<U> { IList<T> Entries(); void SetData(string key, object val, ISessionId<U> sessionId); object GetData(string key, ISessionId<U> sessionId); T this[ISessionId<U> sessionId] { get; } void Register(ref T newEntry); bool Unregister(ISessionId<U> sessionId); bool IsOnline(ISessionId<U> sessionId); void PrepareForDispose(); bool UpdateLastAccessTime(ISessionId<U> sessionId); SessionIdExpiresPolicy SessionIdExpiredPolicy { get; set; } event SessionEntryTimeoutDelegate<T, U> EntryTimeout; } public delegate void SessionEntryTimeoutDelegate<T, U>(T sessionEntry) where T : ISessionEntry<U>; }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace com.lzy.javaeye { [Serializable] public class SessionLongId : ISessionId<long> { public SessionLongId() { this.Value = default(long); } #region ISessionId<long> Members public long Value { get; set; } #endregion public override bool Equals(object obj) { if (obj.GetType().Equals(this.GetType())) return this.Value == ((SessionLongId)obj).Value; else return obj.Equals(this); } public override int GetHashCode() { int i = this.Value.GetHashCode(); return i; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace com.lzy.javaeye { public class SessionEntry<T> : ISessionEntry<T> { public SessionEntry(ISessionId<T> sessionId) { this.SessionId = sessionId; } #region ISessionEntry<T> Members public ISessionId<T> SessionId { get; set; } public DateTime LastAccessTime { get; set; } #endregion } }
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace com.lzy.javaeye { public class Session<T> : ISession<T, long> where T : ISessionEntry<long> { // Timeout event. public event SessionEntryTimeoutDelegate<T, long> EntryTimeout = null; // Storage collections. private IList<T> activeEntries = new List<T>(); private IList<T> expiredEntries = new List<T>(); private Dictionary<ISessionId<long>, Hashtable> data = new Dictionary<ISessionId<long>, Hashtable>(); // 'FollowSessionEntry' policy is safe, but 'Never' is simple. SessionIdExpiresPolicy sessionIdExpiredPolicy = SessionIdExpiresPolicy.Never; // Threading private Thread cleaner = null; private ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); private volatile bool running = true; private T GetEntryById(ISessionId<long> sessionId, out int posInActiveEntries) { T outputEntry = default(T); posInActiveEntries = -1; for (int idx = 0; idx < this.activeEntries.Count; idx++) { if (this.activeEntries[idx].SessionId.Equals(sessionId)) { outputEntry = this.activeEntries[idx]; posInActiveEntries = idx; break; } } return outputEntry; } public Session(int sessionTimeoutInMinutes) { this.cleaner = new Thread(new ParameterizedThreadStart(this.ClearExpired)); this.cleaner.IsBackground = true; this.cleaner.Start(sessionTimeoutInMinutes); } public T this[ISessionId<long> sessionId] { get { this.slimLock.EnterReadLock(); T outputEntry = default(T); int posInActiveEntries = -1; try { outputEntry = this.GetEntryById(sessionId, out posInActiveEntries); } finally { slimLock.ExitReadLock(); } if (posInActiveEntries == -1) throw new SessionIdExpiresException<ISessionId<long>, long>(sessionId); return outputEntry; } } public void Register(ref T newEntry) { this.slimLock.EnterWriteLock(); try { newEntry.LastAccessTime = DateTime.Now; // Support 'Never' session Id expires policy. if (newEntry.SessionId.Value == default(long)) newEntry.SessionId.Value = newEntry.LastAccessTime.ToBinary(); this.activeEntries.Add(newEntry); } finally { this.slimLock.ExitWriteLock(); } } public bool Unregister(ISessionId<long> sessionId) { this.slimLock.EnterWriteLock(); bool result = false; try { int posInActiveEntries = -1; this.GetEntryById(sessionId, out posInActiveEntries); if (posInActiveEntries != -1) { this.data.Remove(sessionId); this.activeEntries.RemoveAt(posInActiveEntries); result = true; } } finally { this.slimLock.ExitWriteLock(); } return result; } public bool UpdateLastAccessTime(ISessionId<long> sessionId) { this.slimLock.EnterWriteLock(); bool result = false; try { int posInActiveEntries = -1; T entry = this.GetEntryById(sessionId, out posInActiveEntries); if (posInActiveEntries != -1) { entry.LastAccessTime = DateTime.Now; result = true; } } finally { this.slimLock.ExitWriteLock(); } return result; } public bool IsOnline(ISessionId<long> sessionId) { this.slimLock.EnterReadLock(); bool result = false; try { int posInActiveEntries = -1; this.GetEntryById(sessionId, out posInActiveEntries); if (posInActiveEntries != -1) result = true; } finally { this.slimLock.ExitReadLock(); } return result; } public IList<T> Entries() { this.slimLock.EnterReadLock(); try { return this.activeEntries; } finally { this.slimLock.ExitReadLock(); } } public void SetData(string key, object val, ISessionId<long> sessionId) { if (!this.IsOnline(sessionId)) { if (sessionIdExpiredPolicy == SessionIdExpiresPolicy.FollowSessionEntry) { throw new SessionIdExpiresException<ISessionId<long>, long>(sessionId); } else if (sessionIdExpiredPolicy == SessionIdExpiresPolicy.Never) { T entry = (T)(ISessionEntry<long>)(new SessionEntry<long>(sessionId)); this.Register(ref entry); } else { throw new NotSupportedException(); } } this.slimLock.EnterWriteLock(); try { Hashtable ht = null; if (this.data.ContainsKey(sessionId)) { ht = data[sessionId]; // Overwrite value if key exists. Actions like the ASP.NET session. if (ht.ContainsKey(key)) ht[key] = val; else ht.Add(key, val); this.data[sessionId] = ht; } else { ht = new Hashtable(); ht.Add(key, val); this.data.Add(sessionId, ht); } } finally { this.slimLock.ExitWriteLock(); } } public object GetData(string key, ISessionId<long> sessionId) { this.slimLock.EnterReadLock(); object result = null; try { if (this.data.ContainsKey(sessionId)) result = data[sessionId][key]; } finally { this.slimLock.ExitReadLock(); } return result; } public SessionIdExpiresPolicy SessionIdExpiredPolicy { get; set; } public void PrepareForDispose() { // Setup flag. this.running = false; // Wake up cleaner. if (this.cleaner.ThreadState == ThreadState.WaitSleepJoin) this.cleaner.Interrupt(); // Wait for the thread to stop for (int i = 0; i < 100; i++) { if ((this.cleaner == null) || (this.cleaner.ThreadState == ThreadState.Stopped)) { System.Diagnostics.Debug.WriteLine( "Cleaner has stopped after " + i * 100 + " milliseconds"); break; } Thread.Sleep(100); } // Prepare objects for GC. this.activeEntries.Clear(); this.activeEntries = null; this.expiredEntries.Clear(); this.expiredEntries = null; this.data.Clear(); this.data = null; } void ClearExpired(object sessionTimeout) { while (this.running) { this.slimLock.EnterUpgradeableReadLock(); try { // Process all active entries. for (int i = 0; i < this.activeEntries.Count; i++) { TimeSpan span = DateTime.Now - this.activeEntries[i].LastAccessTime; if (span.TotalMinutes >= Convert.ToDouble(sessionTimeout)) this.expiredEntries.Add(this.activeEntries[i]); } // Remove timeout entries. if (this.expiredEntries.Count > 0) { this.slimLock.EnterWriteLock(); try { foreach (T entry in this.expiredEntries) { System.Diagnostics.Debug.WriteLine(string.Format("Session {0} expired.", entry.SessionId.Value)); // Will slow down the thread. if (this.EntryTimeout != null) this.EntryTimeout(entry); this.data.Remove(entry.SessionId); this.activeEntries.Remove(entry); } this.expiredEntries.Clear(); } finally { this.slimLock.ExitWriteLock(); } } } finally { this.slimLock.ExitUpgradeableReadLock(); } // Sleep for 1 minute. (larger values will speed up the session) Thread.Sleep((int)TimeSpan.FromMinutes(1).TotalMilliseconds); } } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace com.lzy.javaeye { public enum SessionIdExpiresPolicy { FollowSessionEntry, Never // Unsafe option. } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Security.Permissions; using System.Runtime.Serialization; using System.Runtime.Remoting; namespace com.lzy.javaeye { [Serializable] public class SessionIdExpiresException<T, U> : RemotingException where T : ISessionId<U> { protected SessionIdExpiresException(SerializationInfo info, StreamingContext context) : base(info, context) { this.SessionId = (T) info.GetValue("_SessionId", typeof(T)); } public SessionIdExpiresException(T sessionId) : base(string.Format("Session {0} expired.", sessionId.Value)) { if (sessionId.Value.Equals(default(U))) throw new ArgumentException(string.Format("Session Id value '{0}' is invalid.", sessionId.Value)); this.SessionId = sessionId; } public T SessionId { get; private set; } [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("_SessionId", this.SessionId, typeof(T)); } } }
上面的代码实现了以下5个方面,分别包括功能契约接口和实现:
代码本身已经很简单了,相信仔细看看理解起来是没问题的,需要注意的是需要把放入 Session 中的对象继承自 SessionEntry (或自定义实现的 ISessionEntry 类),就这点来说有些侵入的稍深了些,就看怎么看待了。
写到这里必须要感谢 stefanprodan,Session provider for .NET Remoting and WCF ,原型代码和思路原自他。
先到这里吧,准备休息迎接2009年了,也正好以此贴纪念不平凡的2008年(感觉实际还是平凡度过的,呵呵)。预祝大家元旦快乐,在2009年都能心想事成,一切顺利。
// 2009.03.07 13:30 添加 ////
作者:lzy.je
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2009-01-04
subwayline13 写道 引用需要注意的是需要吧放入 Session 中的对象继承自 SessionEntry (或自定义实现的 ISessionEntry 类),不过这点来说有些侵入的稍深了些,就看怎么看待了。 不喜欢继承方式,烂用继承是代码的坏味道,造成强耦合。 同意你的看法,这里有个度的把握问题,存在的就是合理的。 个人觉得,像 Java、C# 等这些只支持单继承体系的语言,更应小心这种把继承作为支撑顶层结构的设计风格。 |
|
返回顶楼 | |
发表时间:2009-01-05
session传递业务数据
这个设计本身就是有问题的 |
|
返回顶楼 | |
发表时间:2009-01-05
dotaking 写道 session传递业务数据 这个设计本身就是有问题的 请您详细讲讲。 |
|
返回顶楼 | |
发表时间:2009-01-05
subwayline13 写道 这样设计不就不是强耦合了吗? C#代码 public class SessionData<T> { public SessionData() { this.CreateTime = DateTime.Now; } public long SessionID { get; set; } public string Name { get; set; } public T Data { get; set; } public TimeSpan CacheTime { get; set; } internal DateTime CreateTime { get; set; } } public interface IDataContainer { SessionData<T> GetData<T>(long sessionId,string name); void SetData<T>(SessionData<T> data); } public class SessionData<T> { public SessionData() { this.CreateTime = DateTime.Now; } public long SessionID { get; set; } public string Name { get; set; } public T Data { get; set; } public TimeSpan CacheTime { get; set; } internal DateTime CreateTime { get; set; } } public interface IDataContainer { SessionData<T> GetData<T>(long sessionId,string name); void SetData<T>(SessionData<T> data); } 具体怎么实现这个容器还是不用Session的的好,自己实现一个呗,按销毁时间排序,过期的销毁。 我觉得上边的代码和你说的这些是一回事吧? 呵呵,怎么有点乱呢。 |
|
返回顶楼 | |
发表时间:2009-01-08
跨页面session传递业务数据
对任何web系统都有这样的情况 设计的人的问题。 |
|
返回顶楼 | |
浏览 2915 次