使用反规范化、观察者设计模式和异步技术优化性能






4.67/5 (6投票s)
利用反规范化、观察者设计模式和异步技术可以提高任何拥有海量数据库的系统的性能。
引言
大多数开发人员都听说过反规范化、观察者设计模式和异步技术。但许多开发人员未能更深入地研究这些技术的潜力。本文的目的是让您能够深入理解这些技术在优化性能方面的组合应用。
通过反规范化优化性能
让我们来看看反规范化的定义
反规范化是通过添加冗余数据或对数据进行分组来优化数据库性能的过程。
如果数据量不大,反规范化就不会有太大帮助,因为性能提升不明显。因此,我们只在处理海量数据时才充分利用反规范化。通常,海量数据会影响需要进行大量复杂计算的模块的性能(例如,报表模块)。简而言之,在报表功能中,汇总数据(如 YTD 报表(年度至今)、QTD 报表(季度至今))可能不会频繁更改……尤其是对于历史数据,最终用户很少更改它。因此,为什么我们需要一直计算数据呢?
报表功能通常依赖于大量的计算,例如求和、求平均值……这些公式会产生处理开销,从而降低性能。反规范化可以解决这个问题。通过这项技术,计算结果将被存储在数据库中。
具体来说,我们首先看以下数据库设计

需求非常明确。每个 Order
都关联一个 Customer
和一个 SalesPerson
。每个 Order
包含各种 OrderDetail
交易(一对多关系)。
最终用户希望查看包含每个产品的总销售额、每个销售人员的总销售额以及每个客户的总付款额的报表或仪表板。显然,求和可能需要很长时间才能完成,导致性能低下。相反,我们只需求和一次,然后将其存储在数据库中。因此,我们找到了解决方案:添加总计字段。
Customer
:添加字段“TotalPaymentAmount
”Product
:添加字段“TotalSalesAmount
”SalesPerson
:添加字段“TotalSoldAmount
”
这些总计字段是冗余的,但它们的存在极大地提高了性能。每当订单处理生命周期发生变化时,都必须通知总计字段更新其数据。
关于反规范化就到这里。现在,让我们更深入地探讨如何更新总计字段。
问题 – 更新/同步总计数据
问题是如何在数据更改时更新总计字段。这些字段何时更新?基本上有两种可能的方法。一种解决方案是在打开报表/仪表板时更新总计字段,但我们必须添加一个标志字段来通知需要重新计算总计字段。这种方法的缺点是每次执行重新计算时性能都很低。另一种解决方案是在数据更改后立即更新总计字段,无需等到报表/仪表板打开。
在本文中,我将重点介绍第二种解决方案。具体来说,当 SalesPerson
更新 Order
数据时,系统也会更新 3 个总计字段。这给了我关于发布者-订阅者模型的提示。有了这个模型,观察者模式似乎很适合这种设计。
什么是观察者设计模式?
根据 www.dofactory.com 的定义,观察者模式定义了对象之间的一对多依赖关系,以便当一个对象的状态发生变化时,所有依赖它的对象都会自动收到通知并更新。

观察者模式简化了问题,促进了低耦合和可伸缩性。观察者模式的优点在于,您可以在运行时完全附加/分离任何观察者对象。
上面的 UML 图可能比较抽象,有点难理解。下面是一个真实场景的清晰图示

在此图中,我添加了两个不同的对象,称为“主题状态”和“观察者状态”。主题状态会让观察者知道主题对象如何运行。此外,观察者状态会告知其他对象观察者对象如何运行(例如,更新成功或失败)。这对于某些业务场景将非常有帮助,例如处理事务;我们需要知道哪个观察者更新数据失败,以便采取适当的措施来处理。
现在,让我们回到上面应用了反规范化的数据库设计。类图如下所示

最好为主题和观察者定义一个状态对象。我实现了两个状态对象,称为“SubjectState
”和“ObserverState
”
public enum SubjectState
{
UPDATE_ORDER, //Indicate that only general information is updated such as
//changing customer information, changing sales person information...
UPDATE_ORDER_DETAIL //Indicate that detail information is updated including
//product price, quantity...
}
public enum ObserverState
{
NO_UPDATE,
UPDATE_SUCCESSFUL,
UPDATE_FAILED
}
每个观察者将根据主题对象传递过来的状态值来决定是否更新观察者对象。如下所示
/// <summary>
/// The 'ConcreteObserver' class
/// </summary>
class ProductObserver : Observer
{
…
public override void Update()
{
//Subject object notifies this observer with a state as "UPDATE_ORDER_DETAIL".
//Only with this state, ProductObserver object decides to update its internal works
if (_subject.State == SubjectState.UPDATE_ORDER_DETAIL)
{
//YOUR CODE GOES HERE: Send a request to database to update
//total field for Product table
bool updateResult = true;
if (updateResult) //Updated successfully
{
this.State = ObserverState.UPDATE_SUCCESSFUL; //Update Observer State
Console.WriteLine("'Total Sales Amount' for Products which are
chosen in Order {0} has been updated successfully too.",
_subject.OrderID);
Console.WriteLine();
}
else
this.State = ObserverState.UPDATE_FAILED;
}
else
this.State = ObserverState.NO_UPDATE;
Console.WriteLine("Observer '{0}'s state is '{1}'", _name, this.State);
Console.WriteLine();
}
…
}
您可以下载本文包含的完整示例。下表显示了运行此示例后的输出
主题状态 | Actions | 观察者状态 | ||
客户观察者 | 产品观察者 | 销售人员观察者 | ||
UPDATE_ORDER |
NO_UPDATE |
UPDATE_ |
UPDATE_ |
|
UPDATE_ORDER |
UPDATE_ |
UPDATE_ |
UPDATE_ |
|
UPDATE_ORDER |
分离销售人员观察者 |
UPDATE_ |
UPDATE_ |
异步更新观察者对象
上述解决方案有一个小缺点。它会在主题对象更新后产生处理开销,从而导致观察者也得到更新。在这种情况下,我们如何解决性能问题?请注意,更新观察者与渲染 UI 完全无关。我们不需要强迫最终用户等待观察者对象完成更新,否则 UI 页面会显得不够响应。常常被忽视的技术之一是异步更新对象的能力。因此,观察者对象可以设计为异步运行,独立于 UI 线程。在 ASP.NET 页面生命周期中,我们可以通过挂接 Unload 事件处理程序轻松实现这一点。

在事务中安全地更新观察者对象
观察者模式在反规范化背后运行,以确保数据是最新的。如果某些观察者更新数据失败怎么办?为了保护数据,我们需要确保没有任何观察者失败。本质上,我们可以将所有这些更新放入一个事务中,以确保安全,并且如果事务失败,我们可以重试。这种方法的缺点是事务设计可能需要编写复杂的代码。另一种解决方案是,您可以在数据库中的其他地方设置一个标志字段,以通知数据已安全更新。报表功能将基于此标志来确定是否需要重新计算总计字段。总的来说,这两种解决方案都有其优缺点。您可以根据自己的业务需求决定最适合的解决方案。
结论
观察者模式非常棒。您可以定义一系列观察者,让它们按预定义顺序执行……通过熟练地将这种设计模式与其他技术结合起来,可以取得丰硕的成果。希望本文能为您提供一些关于如何提高性能的好想法。
历史
- 版本 1.0 (2009 年 8 月 24 日) - 初始发布