数据库设计——谷歌应用引擎
Posted
技术标签:
【中文标题】数据库设计——谷歌应用引擎【英文标题】:Database design - google app engine 【发布时间】:2011-03-08 09:43:07 【问题描述】:我正在使用谷歌应用引擎并使用低级 java api 来访问大表。我正在构建一个有 4 层的 SAAS 应用程序:
客户端网络浏览器 RESTful 资源层 业务层 数据访问层我正在构建一个应用程序来帮助管理我的移动汽车美容公司(以及其他类似的公司)。我必须代表这四个独立的概念,但不确定我目前的计划是否是一个好的计划:
约会 订单项 发票 付款约会:“约会”是指员工为了提供服务而需要在的地点和时间。
订单项:“订单项”是一项服务、费用或折扣及其相关信息。可能进入约会的订单项示例:
名称:价格:佣金:时间估算 完整细节,常规尺寸:160 75 3.5 小时 10 美元的全细节优惠券:-10 0 0 小时 高级细节:220 110 4.5 小时 派生总计(非行项目):370 美元 185 美元 8.0 小时发票:“发票”是客户承诺支付的一个或多个订单项的记录。
付款:“付款”是对已收到的付款的记录。
在此应用程序的先前实现中,生活更简单,我将所有这四个概念视为 SQL 数据库中的一个表:“约会”。一个“约会”可以有多个行项目、多个付款和一张发票。发票只是根据行项目和客户记录生成的电子邮件或打印件。
10 次中有 9 次,效果很好。当一位客户预约一辆车或几辆车并自己付款时,一切都很美好。但是这个系统在很多条件下都不起作用。例如:
当一位客户预约了一次,但预约中途下雨导致细节师不得不第二天回来,我需要两次预约,但只有一个订单项、一张发票和一张付款。 当办公室的一群客户都决定在同一天完成他们的汽车以获得折扣时,我需要一个预约,但需要多张发票和多笔付款。 当一位客户用一张支票支付两次预约时,我需要两次预约,但只需要一张发票和一次付款。我能够通过稍微捏造一些东西来处理所有这些异常值。例如,如果一个细部必须在第二天回来,我只需在第二天再预约一个项目,上面写着“完成”,费用将为 0 美元。或者,如果我让一位客户用一张支票支付两次约会,我会在每个约会中放入拆分付款记录。这样做的问题是它为数据不一致创造了巨大的机会。数据不一致可能是一个严重的问题,尤其是对于涉及财务信息的情况,例如第三个示例,其中客户用一张支票支付了两次约会费用。付款必须与提供的商品和服务直接匹配,以便正确跟踪应收账款。
建议结构:
下面是用于组织和存储这些数据的规范化结构。也许是因为我缺乏经验,我非常重视数据规范化,因为这似乎是避免数据不一致错误的好方法。使用这种结构,可以通过一个操作完成对数据的更改,而不必担心更新其他表。但是,读取可能需要多次读取以及内存中的数据组织。稍后我想,如果存在性能问题,我可以在“约会”中添加一些非规范化字段,以便更快地查询,同时保持“安全”规范化结构完好无损。非规范化可能会减慢写入速度,但我在想我可能能够对其他资源进行异步调用或添加到任务队列中,这样客户端就不必等待更新数据的非规范化部分的额外写入.
表格:
Appointment
start_time
etc...
Invoice
due_date
etc...
Payment
invoice_Key_List
amount_paid
etc...
Line_Item
appointment_Key_List
invoice_Key
name
price
etc...
以下是将给定约会列表的所有四个实体(表)联系在一起所需的一系列查询和操作。这将包括有关为每次预约安排哪些服务、每次预约的总费用和天气或每次预约未收到付款的信息。这将是加载日历以进行约会安排或经理获取运营整体视图时的常见查询。
查询“约会”列表,其“开始时间”字段位于给定范围之间。 将返回的约会中的每个键添加到列表中。 QUERY 查询所有“Line_Items”的约会键列表字段包括任何退货约会 将所有行项目中的每个 invoice_key 添加到 Set 集合中。 查询发票集合中的所有“发票”(这可以使用应用引擎在一个异步操作中完成) 将返回的发票中的每个键添加到列表中 对所有“付款”的查询,他们的 invoice_key_list 字段包含与任何退回的发票匹配的键 在内存中重新组织,以便每个约会都反映为其安排的 line_items、总价格、总估计时间以及天气是否已支付。...如您所见,此操作需要 4 个数据存储查询以及一些内存中的组织(希望内存中会很快)
谁能评论这个设计?这是我能想到的最好的,但我怀疑可能有更好的选择或完全不同的设计,我没有想到这可能会更好地工作,或者特别是在 GAE(谷歌应用引擎)的优势、劣势和能力下.
谢谢!
使用说明
大多数应用程序的读取密集度更高,有些应用程序的写入密集度更高。下面,我描述了一个典型的用例并分解了用户想要执行的操作:
经理接到客户的电话:
读取 - 经理加载日历并查找可用时间 Write - 经理向客户询问他们的信息,我认为这是一系列异步读取,因为经理输入了电话号码、姓名、电子邮件、地址等每条信息。 . 或者,如果有必要,也许在客户端应用程序收集所有信息之后最后写一次,然后提交。 写入 - 经理记下客户的信用卡信息并将其作为单独的操作添加到他们的记录中 写入 - 经理从信用卡中扣款并验证付款是否成功经理拨出电话:
读取经理加载日历 读取经理为他要呼叫的客户加载约会 写入 经理点击“呼叫”按钮,发起呼叫并写入新的 CallReacord 实体 读取呼叫服务器响应呼叫请求并读取 CallRecord 以了解如何处理呼叫 写入呼叫服务器将更新信息写入呼叫记录 写入当呼叫关闭时,呼叫服务器向服务器发出另一个请求以更新 CallRecord 资源(注意:此请求不是时间关键的)接受的答案:: 前两个答案都非常周到和赞赏。我接受了票数很少的那个,以便尽可能不完美地平衡他们的曝光率。
【问题讨论】:
与您的问题没有直接关系,但您使用低级 API 是否有原因?它在文档中说它不打算直接使用,只是为了可以在它上面编写其他库。一个这样的库,Objectify (code.google.com/p/objectify-appengine),看起来非常棒,并且可能比使用裸机 API 更适合您的需求。 是的,这绝对值得商榷。我的逻辑是我不想抽象出任何数据存储的功能。它也比看起来容易得多。 我还应该提到 Objectify 是用于 Java App Engine SDK 的;您使用哪种语言?它可以帮助人们包含代码示例。 我正在使用 Java。我编写了一个小的 ORM 类型的代码库来帮助将对象移入和移出数据库。虽然有很多代码,但我不知道它是否适合放在帖子中。 【参考方案1】:以下是我认为您必须应对的一些应用引擎特定因素:
使用不等式查询时,只能对one property 使用不等式。例如,如果您要过滤的应用日期介于 7 月 1 日和 7 月 4 日之间,则不能同时按 price > 200
与您可能习惯的 SQL 数据库相比,应用引擎上的事务有点棘手。您只能对同一“entity group”中的实体进行交易。
【讨论】:
'使用跨组交易' - 此链接中的部分表示我们也可以为多个实体使用交易。【参考方案2】:您指定了您的网站需要提供的两个特定“视图”:
安排约会。您当前的方案应该可以正常工作 - 您只需要执行您提到的第一个查询。
操作的整体视图。我不太确定这意味着什么,但是如果您需要执行上面提到的四个查询字符串来获得它,那么您的设计可以使用一些改进。详情如下。
四个数据存储区查询本身并不一定是多余的。您的问题是其中两个查询很昂贵,甚至可能不可能。我将遍历每个查询:
获取约会列表 - 没问题。此查询将能够扫描索引以有效检索您指定日期范围内的约会。
从 #1 获取每个约会的所有行项目 - 这是一个问题。此查询要求您执行IN
查询。 IN
查询被转换为N
sub-queries behind the scenes - 所以你最终会从#1 中得到每个约会键的一个查询!这些将并行执行,因此还不错。主要问题是IN
查询仅限于一小部分值(最多只有 30 个值)。如果 #1 返回的约会密钥超过 30 个,则此查询将无法执行!
获取行项目引用的所有发票 - 没问题。你是正确的,这个查询很便宜,因为你可以直接通过键简单地获取所有相关的发票。 (注意:这个查询仍然是同步的——我不认为异步是你要找的词)。
获取#3 返回的所有发票的所有付款 - 这是一个问题。与 #2 一样,此查询将是一个 IN
查询,如果 #3 返回您需要为其提取付款的中等数量的发票,则该查询将失败。
如果 #1 和 #3 返回的项目数量足够少,那么 GAE 几乎可以肯定能够在允许的范围内执行此操作。这应该足以满足您的个人需求 - 听起来您主要需要它来工作,并且不需要它来扩展到大量用户(它不会)。
改进建议:
非规范化! 尝试将与给定约会相关的Line_Item
、Invoice
和Payment
实体的密钥存储在约会本身的列表中。然后您可以消除您的IN
查询。确保这些新的ListProperty
未编入索引以避免exploding indices 出现问题
其他不太具体的改进想法:
根据您的“操作的整体视图”将要显示的内容,您可能能够拆分所有这些信息的检索。例如,也许您从显示约会列表开始,然后当经理想要有关特定约会的更多信息时,您继续获取与该约会相关的信息。如果这种交互发生在单个页面上,您甚至可以通过 AJAX 来实现。 Memcache 是您的朋友 - 使用它来缓存数据存储查询的结果(甚至更高级别的结果),这样您就不必在每次访问时从头开始重新计算它。【讨论】:
感谢您的回复。我不知道 IN 查询的 30 值限制。我想我可以对查询进行分片,但这会很讨厌。看起来我可能只是将非规范化字段放入“约会”实体中。我没有任何维护非规范化数据的经验,您有什么建议吗? 非规范化并不像听起来那么可怕。每当您创建或删除Line_Item
、Invoice
或 Payment
时,也只需更新相应的 Appointment
。我也不会太担心以事务方式执行此操作 - 只需创建您的 Line_Item
(等),然后更新您的 Appointment
(如果您在单个请求中创建多个行项目,则只需更新相关的 Appointment
实体一次)。在删除Line_Item
时执行相反的操作。如果第二个查询暂时失败,只需将其推送到任务队列中,它最终会被应用。
不是将LineItem
存储在Invoice
的列表中,而是将LineItem
键的列表存储在Invoice
上。您可以按键检索实体,无需 N 次查询或 30 项限制。
我同意 - 将相关实体的密钥存储在列表中(这就是我在帖子中想说的)。【参考方案3】:
如您所见,此设计无法扩展。它需要 4 (!!!) DB 查询来呈现页面。这3个太多了:)
使用 App Engine 数据存储区的流行概念是,您希望在编写某些内容时做尽可能多的工作,以便在检索和呈现某些内容时几乎不需要做任何事情。与渲染的次数相比,您可能只写入数据的次数很少。
规范化同样是您似乎正在努力实现的目标。数据存储在规范化中没有任何价值——这可能意味着更少的数据不一致,但这也意味着读取数据的速度要慢得多(4 次读取?!!)。由于您的数据的读取频率远高于写入频率,因此请针对读取进行优化,即使这意味着您的数据偶尔会在短时间内重复或不同步。
与其考虑数据在存储时的外观,不如考虑您希望数据在向用户显示时的外观。尽可能接近该格式存储,即使这意味着将预渲染的 html 存储在数据存储中。读取速度快如闪电,这是一件好事。
因此,由于您应该针对读取进行优化,因此您的写入通常会增长到巨大的比例。如此庞大,以至于您无法在 30 秒的请求时间限制内适应它。嗯,这就是task queue 的用途。将您认为模型的“基本必需品”存储在数据存储中,然后启动任务队列以将其拉出,生成要呈现的 HTML,并将其放在后台。这可能意味着您的模型立即准备好显示,直到任务完成,因此在这种情况下您需要优雅地降级,即使这意味着在数据完全填充之前“缓慢地”渲染它。任何进一步的阅读都将是闪电般的快速。
总之,我没有任何与您的数据库直接相关的具体建议——这取决于您希望数据在用户看到时的样子。
我可以为您提供一些关于数据存储的超级有用视频的链接:
Brett Slatkin 的 2008 和 2009 讨论了在 App Engine 上构建可扩展、复杂的应用程序,以及来自 this year 的关于数据管道的精彩文章(我认为这并不直接适用,但总体上非常有用) App Engine Under the Covers:App Engine 是如何在幕后完成它的工作的 AppStats:查看您正在执行的数据存储读取次数以及减少该次数的一些技巧的好方法【讨论】:
哇,这个答案很匆忙 :) 很抱歉,希望它不是完全没用! 嗯,一个长问题的长答案 - 此外,您还有很多有用的东西要指出。 :) 感谢您的回复,非常有帮助。它不是太长,特别是考虑到问题的长度。我添加了一些关于用例的更多信息,以便更好地了解读写比率。 由于数据存储在一定比例的时间内失败并且偶尔进入只读模式,您如何确保非规范化数据的数据完整性? 您必须根据自己的需要来解决这个问题。如果数据存储处于只读模式,您可以检测到这一点,并且在此期间不允许写入。我不知道事务是否可以被只读模式中断,但如果不能,那么这可能是在数据存储降级时保持一致性的一种方式。以上是关于数据库设计——谷歌应用引擎的主要内容,如果未能解决你的问题,请参考以下文章