C# MySQL连接池的限制和清理连接

6

我有一个简单的数据库管理器类(名字比它的能力更宏大):

class DbManager
{
    private MySqlConnectionStringBuilder _connectionString;

    public DbManager()
    {
        _connectionString = new MySqlConnectionStringBuilder();
        _connectionString.UserID = Properties.Database.Default.Username;
        _connectionString.Password = Properties.Database.Default.Password;
        _connectionString.Server = Properties.Database.Default.Server;
        _connectionString.Database = Properties.Database.Default.Schema;
        _connectionString.MaximumPoolSize = 5;
    }


    public MySqlConnection GetConnection()
    {
        MySqlConnection con = new MySqlConnection(_connectionString.GetConnectionString(true));
        con.Open();
        return con;
    }

}

我还有另一个类别代表数据库中其中一张表的记录,然后我通过如下方式填充这个类别:

class Contact
{
    private void Populate(object contactID)
    {
        using (OleDbConnection con = DbManager.GetConnection())
        {
            string q = "SELECT FirstName, LastName FROM Contacts WHERE ContactID = ?";

            using (OleDbCommand cmd = new OleDbCommand(q, con))
            {
                cmd.Parameters.AddWithValue("?", contactID);

                using (OleDbDataReader reader = cmd.ExecuteReader())
                {
                    if (reader.HasRows)
                    {
                        reader.Read();
                        this.FirstName = reader.GetString(0);
                        this.LastName = reader.GetString(1);
                        this.Address = new Address();
                        this.Address.Populate(ContactID)
                    }
                }
            }
        }
    }
}


class Address
{
    private void Populate(object contactID)
    {
        using (OleDbConnection con = DbManager.GetConnection())
        {
            string q = "SELECT Address1 FROM Addresses WHERE ContactID = ?";

            using (OleDbCommand cmd = new OleDbCommand(q, con))
            {
                cmd.Parameters.AddWithValue("?", contactID);

                using (OleDbDataReader reader = cmd.ExecuteReader())
                {
                    if (reader.HasRows)
                    {
                        reader.Read();
                        this.Address1 = reader.GetString(0);
                    }
                }
            }
        }
    }
}

现在我认为所有的 using 语句都会确保在完成后将连接返回到池中,以便下一个使用,但是我有一个循环,创建了数百个这些 Contacts 并填充它们,似乎连接没有被释放。
连接、命令和读取器都在各自的 using 语句中。

只是为了检查发生了什么,如果你在 using 块结束时显式调用 con.Dispose() 会发生什么? - Harps
嗯,我觉得这让我遇到了一个问题。populate 调用了另一个子对象的 populate。如果我将其注释掉,就没问题了。不过,子对象的 populate 几乎和那个一样。 - Cylindric
@Cylindric:我已经添加了一个答案,解释了我认为正在发生的事情。使用池是一个好主意,但实质上你需要确保池足够大以应对。 :) - Chris
@Cylindric:我刚刚编辑了它,添加了有关递归调用的注释。 - Chris
另一个可能有帮助的重构是向您的dbmanager添加方法,该方法接受您的SQL和参数并返回数据表。这样做的优点是将连接保持打开的时间减少到最小(因为在方法返回时它已关闭),这意味着您的连接池不应轻易耗尽,并且您正在尽快释放数据库资源。此外,它避免了所有与SQL相关的代码的重复。缺点是您正在创建一个DataTable对象,有时比您需要的更加沉重。 - Chris
显示剩余3条评论
2个回答

4
如果应用程序是多线程的,那么你可能会有10个线程同时运行。每个线程都需要自己的连接,但如果你将该池大小限制为5,则第6个线程将无法从池中获取连接。 你可能在某些方面限制了线程数量,但我建议显着增加应用程序池的大小,以确保您拥有比线程更多的可用连接。作为指标,默认大小(通常对大多数人来说已足够)为100。
此外,如果使用块内部有任何递归(例如再次调用populate),则会遇到进一步的问题,如您在注释中所示,而不是上面的代码。
如果在using块内调用populate,则将打开并使用父连接(因此无法重复使用),然后子调用将打开另一个连接。 如果这种情况发生几次,您将用尽连接的分配。
为了防止这种情况,您需要将二级的Populate调用移出using块。最简单的方法是,在记录集中循环调用每个ID来填充之前,将ID添加到列表中,然后在关闭连接后为所有新ID执行填充操作。
或者,您可以只懒惰地评估诸如地址之类的东西。将addressID存储在私有字段中,然后使Address成为检查其支持字段(而不是addressID)是否被填充并且如果没有则使用AddressID查找的属性。这样做的好处是,如果您从未查看地址,则甚至不会执行数据库调用。根据数据的使用情况,这可能会为您节省大量的数据库敲击,但如果您确实使用所有细节,则仅将它们移动到不同位置,可能会有助于性能或可能根本没有任何区别。 :)
通常,对于数据库访问,我尽可能快地获取所有数据并尽早关闭连接,最好在对数据进行任何复杂计算之前。另一个很好的原因是,根据您的数据库查询等情况,您可能会在使用查询访问的表上持有锁定,这可能会导致数据库方面的锁定问题。

我正在限制我的线程来适当控制远程数据库连接,但我认为我可能没有考虑到每个主要的“populate”需要大约三个连接,一个用于父级,一个用于查找子级,一个用于填充每个子级。因此 connections = 3*threads - Cylindric
我们的编辑冲突了。似乎评论不是线程安全的...你似乎是正确的,我认为这是连接数量爆炸的原因。 - Cylindric
如果您已经限制了线程数量,那么我不会太担心过多地限制线程。如果您只需要五个线程,那么在池中有五十个连接也无所谓。它不会一开始就创建所有五十个连接,而是在需要时创建它们并保留一段时间。如果出现意外情况,您还有缓冲区。如果您想对数据库连接非常严格,那么我相信有办法对获取连接进行锁定,以便可以使线程等待直到获得连接。 - Chris
是的,我只将连接最大值设置为5,这样它就会更快地中断。但在生产环境中,需要同步约80k条记录,因此我不希望在我不注意时它会悄悄地增加到max - Cylindric

0

我建议做出一些更改:

1)在 using 语句终止之前显式关闭连接。查看 OleDbConnection(以及 DbConnection 和 DbConnectionInternal)的源代码,我认为连接在 dispose 时不会被显式关闭,而是被遗弃。

2)修改 Address.Populate,接受一个连接参数,这样当你只需要一个连接时就不必创建两个打开的连接并从 contact 传递打开的连接。如果有情况下,在调用 Populate 重载时没有可用的连接,你可以在 Address 中创建一个重载版本的 Populate,在调用带有连接对象的 Populate 重载之前打开连接。更新 Chris 的建议:我假设人们知道在这种情况下如何关闭打开的 reader,但这并不一定是正确的。如果使用此方法,则必须在将打开的连接传递给该方法之前显式关闭任何打开的 reader。

更新

刚刚验证了建议 #1。来自 MSDN 文档

如果DbConnection超出范围,则不会关闭。因此,您必须通过调用Close或Dispose来显式关闭连接,它们在功能上是等效的。如果连接池值Pooling设置为true或yes,则还会释放物理连接。


@Chris:只要清理之前的活动(即关闭打开的读取器),就可以一遍又一遍地使用开放连接。 - competent_tech
@Cylindric:实际上,dbConnection在这里提到了这种行为:http://msdn.microsoft.com/en-us/library/system.data.common.dbconnection.close.aspx - competent_tech
@Chris:说得好;我们所有的数据访问代码都包含在一个从企业库派生的层中,因此任何打开的读取器都会被该代码自动关闭,有时我会忘记这一点。我已经更新了答案以反映这一点(归功于你)。谢谢。 - competent_tech
是的,我认为这是正确的方法,这就是为什么在我的答案中,我建议使用返回数据表并进行少量计算的方法,以便读者打开的时间最短。 - Chris
@comptent_tech:关闭和处理是功能上等效的。这不会验证您的建议#1,而是使其无效。对吗? - hagello
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接