如何找出我使用 NHibernate 的遗留应用程序在哪里泄漏内存
How to find out where my legacy application using NHibernate is leaking memory
我有一个正在维护的遗留应用程序正在泄漏内存。
我有理由相信源是会话 management/dependency 注入代码。它使用简单注入器和 NHibernate。
首先,这里有一些帮助程序 类 和我们使用的接口:
public class SessionFactory : Dictionary<string, Func<ISession>>,Helpers.ISessionFactory, IDisposable
{
public ISession CreateNew(string name)
{
return this[name]();
}
public void Dispose()
{
foreach (var key in Keys)
{
this[key]().Close();
this[key]().SessionFactory.Close();
}
}
}
public interface ISessionFactory
{
ISession CreateNew(string name);
}
这是容器初始化的样子:
private static void InitializeContainer(Container container)
{
var connectionStrings = System.Configuration.
ConfigurationManager.ConnectionStrings;
var sf1 = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db1"].ConnectionString
).BuildSessionFactory();
var sf2 = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db2"].ConnectionString
).BuildSessionFactory();
var sf3 = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db3"].ConnectionString
).BuildSessionFactory();
container.Register<ISessionFactory>(() =>
new SessionFactory
{
{"db1", sf1.OpenSession},
{"db2", sf2.OpenSession},
{"db3", sf3.OpenSession}
}, Lifestyle.Scoped);
}
然后,在我们的基础控制器(其他控制器继承自它)中,会发生这种情况:
protected BaseController(ISessionFactory factory)
{
this.factory = factory;
db1Session = factory.CreateNew("db1");
db2Session = factory.CreateNew("db2");
db3Session = factory.CreateNew("db3");
}
从那里开始,我们所有的方法都可以使用来自任何数据库的会话。一些请求方法使用多个数据库会话来完成它们的任务。该项目此时 未 使用存储库模式——重写它将是一项昂贵的操作。这段代码中是否遗漏了任何明显的内存泄漏?
看来您已经发明了自己的接口 ISessionFactory。鉴于您使用的 NHibernate 也以该名称提供了一个接口,我认为在您自己的代码中使用相同的名称是非常不幸的。您应该为自己的界面选择一个不同的名称,class 以避免混淆。
至于问题本身,NHibernate 的 ISessionFactory.OpenSession()
正是这样做的。它将打开 return 一个会话。没有任何依据可以假设它会在重用或范围界定方面发挥神奇作用。
要让 NHibernate 协助上下文会话,您需要配置适当的 "context provider" 并使用 ISessionFactory.GetCurrentSession()
。参见 Contextual Sessions in the NHibernate reference。
或者,您可以使用任何您喜欢的方式来管理会话,但是您必须使用该机制来检索当前会话并且不要期望 NHibernate 知道它。
我觉得你的设计很可疑。首先,你的工厂正在泄漏连接,因为尽管你试图处理它,但你唯一能做的就是处理你在处理过程中刚刚打开的东西;这不是很有用,意味着已经创建的连接不会关闭。其次,您的应用程序使用基于字符串的方法请求正确连接的设计很容易出错。您的应用程序可能正在处理多个数据库模式,其中每个连接都与特定模式相关。这意味着连接不可互换,这保证了每个模式使用唯一的抽象。因此,与其使用一个通用的 ISessionFactory
抽象来尝试为所有消费者服务(目前失败),不如通过为每个独特的模式提供自己的抽象来使事情变得明确。例如:
public interface IDb1SessionProvider
{
ISession Session { get; }
}
public interface IDb2SessionProvider
{
ISession Session { get; }
}
public interface IDb3SessionProvider
{
ISession Session { get; }
}
由于缺乏上下文,我将接口命名为 IDbXSessionProvider
,但我敢打赌你能想出一个更好的名字。
这可能看起来很奇怪,因为所有接口都有相同的方法签名,但请记住它们各自有一个非常不同的契约。 Liskov 替换原则描述了它们不应共享相同的接口。
可以按如下方式实现此类提供程序:
public class FuncDb1SessionProvider : IDb1SessionProvider
{
Func<ISession> provider;
public FuncDb1SessionProvider(Func<ISession> sessionProvider) {
this.sessionProvier = provider;
}
public ISession Session => provider();
}
您可以在简单注入器中注册这样的实现,如下所示:
var factory = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db1"].ConnectionString)
.BuildSessionFactory();
var session1Producer = Lifestyle.Scoped.CreateProducer<ISession>(
factory.OpenSession, container);
container.RegisterSingleton<IDb1SessionProvider>(
new FuncDb1SessionProvider(session1Producer.GetInstance));
这段代码的作用是为 db1 会话创建一个作用域 InstanceProducer
。范围 InstanceProducer
将确保在特定范围(通常是 Web 请求)期间仅创建该会话的一个实例,并将确保释放 ISession
实现(如果它实现 IDisposable
).对 InstanceProducer.GetInstance()
的调用包含在 FuncDb1SessionProvider
中。此会话提供程序会将会话的创建转发给包装的委托。
通过这种设计,您可以让您的应用程序代码依赖于 IDb1SessionProvider
,并且该代码可以使用它而无需处置它。在同一个会话中对 IDb1SessionProvider.Session
的每次调用都将确保您获得相同的会话,并且 Simple Injector 保证在请求结束时进行处理。
我有一个正在维护的遗留应用程序正在泄漏内存。
我有理由相信源是会话 management/dependency 注入代码。它使用简单注入器和 NHibernate。
首先,这里有一些帮助程序 类 和我们使用的接口:
public class SessionFactory : Dictionary<string, Func<ISession>>,Helpers.ISessionFactory, IDisposable
{
public ISession CreateNew(string name)
{
return this[name]();
}
public void Dispose()
{
foreach (var key in Keys)
{
this[key]().Close();
this[key]().SessionFactory.Close();
}
}
}
public interface ISessionFactory
{
ISession CreateNew(string name);
}
这是容器初始化的样子:
private static void InitializeContainer(Container container)
{
var connectionStrings = System.Configuration.
ConfigurationManager.ConnectionStrings;
var sf1 = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db1"].ConnectionString
).BuildSessionFactory();
var sf2 = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db2"].ConnectionString
).BuildSessionFactory();
var sf3 = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db3"].ConnectionString
).BuildSessionFactory();
container.Register<ISessionFactory>(() =>
new SessionFactory
{
{"db1", sf1.OpenSession},
{"db2", sf2.OpenSession},
{"db3", sf3.OpenSession}
}, Lifestyle.Scoped);
}
然后,在我们的基础控制器(其他控制器继承自它)中,会发生这种情况:
protected BaseController(ISessionFactory factory)
{
this.factory = factory;
db1Session = factory.CreateNew("db1");
db2Session = factory.CreateNew("db2");
db3Session = factory.CreateNew("db3");
}
从那里开始,我们所有的方法都可以使用来自任何数据库的会话。一些请求方法使用多个数据库会话来完成它们的任务。该项目此时 未 使用存储库模式——重写它将是一项昂贵的操作。这段代码中是否遗漏了任何明显的内存泄漏?
看来您已经发明了自己的接口 ISessionFactory。鉴于您使用的 NHibernate 也以该名称提供了一个接口,我认为在您自己的代码中使用相同的名称是非常不幸的。您应该为自己的界面选择一个不同的名称,class 以避免混淆。
至于问题本身,NHibernate 的 ISessionFactory.OpenSession()
正是这样做的。它将打开 return 一个会话。没有任何依据可以假设它会在重用或范围界定方面发挥神奇作用。
要让 NHibernate 协助上下文会话,您需要配置适当的 "context provider" 并使用 ISessionFactory.GetCurrentSession()
。参见 Contextual Sessions in the NHibernate reference。
或者,您可以使用任何您喜欢的方式来管理会话,但是您必须使用该机制来检索当前会话并且不要期望 NHibernate 知道它。
我觉得你的设计很可疑。首先,你的工厂正在泄漏连接,因为尽管你试图处理它,但你唯一能做的就是处理你在处理过程中刚刚打开的东西;这不是很有用,意味着已经创建的连接不会关闭。其次,您的应用程序使用基于字符串的方法请求正确连接的设计很容易出错。您的应用程序可能正在处理多个数据库模式,其中每个连接都与特定模式相关。这意味着连接不可互换,这保证了每个模式使用唯一的抽象。因此,与其使用一个通用的 ISessionFactory
抽象来尝试为所有消费者服务(目前失败),不如通过为每个独特的模式提供自己的抽象来使事情变得明确。例如:
public interface IDb1SessionProvider
{
ISession Session { get; }
}
public interface IDb2SessionProvider
{
ISession Session { get; }
}
public interface IDb3SessionProvider
{
ISession Session { get; }
}
由于缺乏上下文,我将接口命名为 IDbXSessionProvider
,但我敢打赌你能想出一个更好的名字。
这可能看起来很奇怪,因为所有接口都有相同的方法签名,但请记住它们各自有一个非常不同的契约。 Liskov 替换原则描述了它们不应共享相同的接口。
可以按如下方式实现此类提供程序:
public class FuncDb1SessionProvider : IDb1SessionProvider
{
Func<ISession> provider;
public FuncDb1SessionProvider(Func<ISession> sessionProvider) {
this.sessionProvier = provider;
}
public ISession Session => provider();
}
您可以在简单注入器中注册这样的实现,如下所示:
var factory = new Configuration().Configure().SetProperty(
"connection.connection_string",
connectionStrings["db1"].ConnectionString)
.BuildSessionFactory();
var session1Producer = Lifestyle.Scoped.CreateProducer<ISession>(
factory.OpenSession, container);
container.RegisterSingleton<IDb1SessionProvider>(
new FuncDb1SessionProvider(session1Producer.GetInstance));
这段代码的作用是为 db1 会话创建一个作用域 InstanceProducer
。范围 InstanceProducer
将确保在特定范围(通常是 Web 请求)期间仅创建该会话的一个实例,并将确保释放 ISession
实现(如果它实现 IDisposable
).对 InstanceProducer.GetInstance()
的调用包含在 FuncDb1SessionProvider
中。此会话提供程序会将会话的创建转发给包装的委托。
通过这种设计,您可以让您的应用程序代码依赖于 IDb1SessionProvider
,并且该代码可以使用它而无需处置它。在同一个会话中对 IDb1SessionProvider.Session
的每次调用都将确保您获得相同的会话,并且 Simple Injector 保证在请求结束时进行处理。