什么是建立数据库连接的最佳方法(静态、抽象、每个请求、...)?

14

我使用了很多模型来连接数据库,在我最后一个工程中,我使用了C#和Entity Framework,并创建了一个静态类用于数据库连接,但我在打开和关闭连接时遇到问题,当同时发出10-15个请求时会出错,现在我改变了连接数据库的方法,每次请求连接一次数据库并删除所有静态方法和类。

现在我想知道:

什么是最佳的连接模型?

  1. 每次查询后是否应该关闭连接再重新打开,还是...?
  2. 在静态类中使用连接是好的模型(我不需要每次创建)吗?
  3. 是否有适合此问题的设计模式?
  4. 所有这些都是针对同一个问题的:什么是建立数据库连接的最佳方法(静态、抽象、按请求、...)?

例如,我正在开发一个短信发送者Web面板,我每秒应该发送100K条短信,这些短信与其他短信收集在一起形成一个包,每个包含1~20条短信,然后我需要每秒发送5K~100K个包,当我发送一个包时,我需要执行以下步骤:

  1. 将单个短信更新为已交付或未交付
  2. 如果交付则将用户余额更新到useraccounts表中
  3. 在用户表中更新发送短信的数量
  4. 在移动号码表中更新发送短信的数量
  5. 在发件人号码表中更新发送短信的数量
  6. 在package table中更新已交付和失败的短信的包
  7. 在package table中更新此线程发送此包的方式
  8. 更新线程表,以便了解此线程发送了多少条短信以及有多少条失败了
  9. 在AccountDocument表中添加此事务的帐户文档

所有步骤和许多其他事项(如日志、用户界面和监控小部件)都需要执行,并且我需要每个单独的事务都需要进行数据库连接。

现在,什么是连接到数据库的最佳模型?是按人工请求、线程请求还是每个单独的事务?


4
查阅 工作单元 和仓储库模式,这是一个不错的起点。请注意保持内容的准确性和原意,并将其表达得通俗易懂。 - Belogix
2
重点在于您想如何使用事务,通常每个事务都需要一个新的连接(除非您可以确定事务不会重叠)。您不希望每个事务有多个连接,因为那样就需要使用分布式事务,这将影响性能。如果您没有以事务方式考虑它,那么您可能应该这样做。 - James Gaunt
3
选项“2”绝对不行,永远不要这样做。 - Marc Gravell
1
@Mehdi - 工作单元是正确的选择。我的观点是不要以请求/静态/实例/线程为思考方式,而是将工作单元视为事务。你的数据库访问模式不应该与任何其他事务绑定。这是获得最佳性能和事务安全性的方法。搜索“工作单元”,有很多不同实现的代码示例。 - James Gaunt
1
@MehdiYeganeh 说实话,如果你每秒处理那么多请求,请雇用最好的DBA,他们曾经解决过亚马逊规模的问题,因为这是一个价值100万至200万美元的问题。 - Yaur
显示剩余5条评论
3个回答

10

你的问题的答案:

  1. 关闭它。.NET会在幕后为您进行连接池。

  2. 创建一个连接。每次使用 using (Connection conn = new ....),这样,您将充分利用.NET池机制。

  3. 您可以使用.NET ThreadPool(或自己的定制线程池),定义ThreadPool仅并行使用10个线程,并逐个排队工作项。这样,最多只使用10个连接+它可能会更快。有关自定义线程池的更多信息:Custom ThreadPool Implementation

  4. 每个实例。


下面是我的架构建议:

  1. 为待发送的SMS创建一个数据库表(队列)。

  2. 每行将包含发送短信所需的所有信息+当前状态。

  3. 创建一个工作进程,例如Windows服务,它将不断地采样此表 - 每5秒钟一次。它将选择状态为'pending to be sent'(应表示为int)的前20个SMS,并将其状态更新为'sending'

  4. 每个短信都将使用Windows服务端上的自定义线程池发送。

  5. 在过程结束时,将使用CTE(常用表达式 - 您可以发送具有刚刚处理的所有短信行ID的cte以执行'批量更新'到“完成”状态)将所有已处理的短信状态更新为“done”。

  6. 您可以使状态更新存储过程与“getpending”相同。这样,您可以进行无锁选择并使数据库更快。

  7. 这样,您可以运行不止一个处理器服务(但然后您将必须失去nolock)。

  1. 记得尽可能避免锁定。
  2. 顺便提一句,这也很好,因为您可以通过向待处理短信表添加一行来从系统中的任何位置发送短信。
  3. 还有一件事,我不建议在此使用实体框架,因为它在后台执行了太多的操作。对于这种类型的任务,您只需要简单地调用三到四个存储过程就可以了。也许可以看看 Dapper-dot-NET - 这是一个非常轻量级的MicroDal框架,在大多数情况下比EF(实体框架)运行速度快10倍以上。

请问您能否为使用自定义线程池的情况建议一个好的设计模式? - Mehdi Yeganeh

7

1. 每个查询之后我应该关闭连接吗?

.Net会自动处理这个问题,这是垃圾回收器的任务。所以不必手动释放对象,Jon Skeet提供了一个很好的答案:https://dev59.com/c3I-5IYBdhLWcg3wJEwK#1998600。但你可以使用using(IDisposable){ }语句来强制垃圾回收器起作用。这里有一篇关于资源重分配的好文章:http://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About

2. 静态类中的连接是否好?

永远不要将数据上下文设置为静态!数据上下文既不是线程安全的,也不是并发安全的。

3. 是否有适用于此问题的良好设计模式?

如Belogix所说,依赖注入和工作单元模式非常好,事实上实体框架本身就是一个工作单元。虽然DI和UoW有点被高估了,如果这是你第一次处理IoC容器,那么它并不容易实现。如果要选择这条道路,我建议使用Ninject。另外,如果您不打算运行测试,您实际上并不需要DI。这些模式的强大之处在于解耦,这样您就可以轻松进行测试和模拟。

简而言之:如果您要对代码进行测试,请使用这些模式。如果没有,我将向您提供一个关于如何在所需的服务之间共享数据上下文的示例。这是您第四个问题的答案。

4. 建立数据库连接的最佳方法是什么(静态的,每个请求的)?

您的上下文服务:

public class FooContextService {
    private readonly FooContext _ctx;

    public FooContext Context { get { return _ctx; } }

    public FooContextService() {
        _ctx = new FooContext();
    }
}

其他服务:

public class UnicornService {
    private readonly FooContext _ctx;

    public UnicornService(FooContextService contextService) {
        if (contextService == null)
            throw new ArgumentNullException("contextService");

        _ctx = contextService.Context;
    }

    public ICollection<Unicorn> GetList() {
        return _ctx.Unicorns.ToList();
    }
}

public class DragonService {
    private readonly FooContext _ctx;

    public DragonService(FooContextService contextService) {
        if (contextService == null)
            throw new ArgumentNullException("contextService");

        _ctx = contextService.Context;
    }

    public ICollection<Dragon> GetList() {
        return _ctx.Dragons.ToList();
    }
}

Controller:

public class FantasyController : Controller {
    private readonly FooContextService _contextService = new FooContextService();

    private readonly UnicornService _unicornService;
    private readonly DragonService _dragonService;

    public FantasyController() {
        _unicornService = new UnicornService(_contextService);
        _dragonService = new DragonService(_contextService);
    }

    // Controller actions
}

重新考虑一下(几乎是编辑): 如果您需要上下文不为实体创建代理,因此也没有延迟加载,您可以按以下方式重载上下文服务:

public class FooContextService {
    private readonly FooContext _ctx;

    public FooContext Context { get { return _ctx; } }

    public FooContextService() : this(true) { }

    public FooContextService(bool proxyCreationEnabled) {
        _ctx = new FooContext();
        _ctx.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
    }
}

注意:

  • 如果将代理创建启用设置为false,您将无法开箱即用地进行延迟加载。
  • 如果您有API控制器,则不希望处理任何完整的对象图。

编辑:

首先阅读一些内容:

完成这个:

(_context as IObjectContextAdapter).ObjectContext.Connection.Open();

这是一篇关于管理连接和事务的优秀文章。

实体框架通过 Connection 属性公开 EntityConnection。阅读为:public sealed class EntityConnection : DbConnection

管理连接时需要考虑以下事项:(来自上述链接)

  • 如果对象上下文在操作之前未打开连接,则会打开该连接。如果对象上下文在操作期间打开了连接,则始终会在操作完成时关闭该连接。
  • 如果您手动打开连接,则对象上下文不会关闭它。调用 CloseDispose 将关闭连接。
  • 如果对象上下文创建连接,则在上下文被处置时始终会处理该连接。
  • 在长时间运行的对象上下文中,必须确保在不再需要时处置上下文。

希望对您有所帮助。


1
什么?“不要费心手动处理对象”是错误的建议。而且,“使用using(IDisposable){ }语句来强制垃圾回收器(GC)完成其工作”也是不准确的 - using语句并不能强制GC做任何事情。 - default.kramer
1
@default.kramer: "不要费心手动处理对象":谁会手动处理每个对象?即使您手动处理每个对象,GC也不会自动执行任何操作,我只是说GC非常聪明,知道何时以及如何释放资源。而"使用using{}语句强制GC执行其工作":您是否意味着在using块中的资源在其范围结束时不调用该资源的Dispose方法?如果是这样,为什么在使用using语句时需要实现IDisposable?我将更改答案以反映这一点。 - Esteban
1
你的前两句话听起来像是“把你的数据库连接随意丢在那里,不管不顾,GC 最终会处理它。”也许这不是你的意思。我认为你误解了 GC 和 IDisposable 之间的关系(或缺乏关系)。一个 using 块调用 Dispose;GC 调用 Finalizer。一个常见的“安全网”是确保 Finalizer 确保已调用 Dispose,但在一个行为良好的应用程序中,这个安全网是不需要的 - 所有的 IDisposables 应该在没有任何 GC 参与的情况下被处理。 - default.kramer
1
我不同意“如果你不打算运行测试,就不需要 DI”的说法。好的面向对象设计通常倾向于支持 DI(无论是容器管理还是非容器管理)。我将主要的好处描述为“可组合性”,这将导致更易维护的应用程序。可测试性确实是一种好处,但它更多的是一种良好设计的副作用。我不建议“如果你要写测试,就这样做,否则就那样做”。 - default.kramer
1
@Esteban 非常感谢您的帮助。我对您的答案很满意,再次感谢。我知道可处理对象和垃圾回收器,但我认为让句柄通过垃圾回收器关闭连接不安全,让我检查一下示例...我有一些linQ查询,我尝试通过线程查找应该从队列短信等待表发送的短信,并且线程应该更新记录状态并加载所有短信包和所有移动号码并计算费用,当我使用异步调用方法时,GC无法处理关闭它。所有连接保持打开状态。 - Mehdi Yeganeh
@Esteban,请您更详细地描述一下在大量线程(约100~500个线程)相互协作并且每个线程调用了许多异步方法时,如何在异步调用方法中打开和关闭连接? - Mehdi Yeganeh

5
我认为按请求扩展性最佳。使用线程安全的连接池,并使连接范围与工作单元相一致。让负责事务行为和工作单元的服务检出连接,使用它,并在工作单元提交或回滚时将其返回到池中。
更新:
花费10-12秒钟来提交状态更新?你做错了其他事情。你的问题并不足以提供一个合适的答案。 每日纳斯达克交易量为13亿笔交易,在8小时的工作日内,每秒处理约45,000笔交易。您的交易量是纳斯达克的两倍。如果您试图使用一台机器完成此操作,那么我会说纳斯达克正在使用多台服务器。

我也想知道是否可以不使用ACID来更新状态。毕竟,星巴克不使用两阶段提交。也许更好的解决方案是使用生产者/消费者模式和阻塞队列,在发送后尽可能更新这些状态。


我在我的项目中遇到了更多问题..我有一个短信发送面板,每秒钟发送100,000条短信,并且我发送的每一条短信都应该在数据库中更新状态,我有100个线程一起工作,并且我使用异步方法发送短信,在另一方面,用户追踪发送短信..现在当我使用每个请求时,我的CPU性能会变高,我无法在1秒钟内发送所有短信,需要10-12秒。如果我知道最好的和安全的方法是连接每个请求,那就没有问题,我可以升级我的硬件! - Mehdi Yeganeh
提交只需10-12秒,没有任何状态更新,对于我想要在一秒钟内发送的100000条发送记录,我需要更多的纳斯达克,我想要每秒发送100K,并且仅发送,我还更新我的移动号码表、发送者号码、用户发送计数和计算单个线程发送短信的线程,我减少用户余额并创建一个帐户文档事务,所有这些都是为了发送1条短信和许多其他业务... - Mehdi Yeganeh
您在这些评论中听起来有点情绪化和语无伦次。我不认为我能为您做更多。投票关闭。 - duffymo
抱歉,我认为这是我的英语问题而不是个性问题。我已经更改了所有连接到数据库的类5次,每次都阅读相关资料,我真的很想找到一种更好的方式,并且我应该在下周六交付项目,我认为我在数据库连接方面遇到了大问题。请向我介绍一种连接到数据库的模式。我已开始阅读有关工作单元和星巴克不使用两阶段提交的内容,我认为这很好,但我想找到更多的模式。 - Mehdi Yeganeh
我为MCI的一个分支机构做这个项目,硬件和其他事情不是我的问题,我已经工作了10个月,现在想在我的PC上解决这些问题(Core i7,8GB RAM,1TB硬盘,Windows 7),如果我赢了,更好的硬件就更好了,并且我设置了所需的较低要求为xxx,所以我想知道我连接到数据库的方式是否正确?对于赚钱,这家公司每天赚大约100K美元。 - Mehdi Yeganeh
显示剩余2条评论

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