【问题标题】:Oracle 11g - System performace impacted as the data growsOracle 11g - 系统性能随着数据增长而受到影响
【发布时间】:2015-05-14 02:36:52
【问题描述】:

我们的系统具有相当高的并发性,多个 Java 线程一次从给定的 Oracle 11g 表中提取一条记录,该表通常保存大约 200 万条记录。 总是有许多记录可供提取以进行处理。准备处理的记录是基于相对复杂的 SQL 语句选择的,但一旦选择,处理顺序是基于 FIFO 算法(ID 顺序)。 至关重要的是,相同的记录不会被两个不同的线程拾取。因此,我们有一个锁定机制。

从高层次的角度来看,它目前的工作方式是 java 线程调用一个存储过程,该存储过程反过来会打开一个 RECORD_READY_KEYS 游标,然后遍历该游标并尝试获取对记录的锁定用那把钥匙锁起来的桌子。锁定尝试是使用 SELECT FOR UPDATE SKIP LOCKED 完成的。如果锁定成功,则将要处理的记录返回给 java 线程进行处理。

只要准备处理的记录不多,一切都会正常工作。然而,当这个数字增长超过一个限制时(根据我们在超过 15K 时的观察),用于获取 RECORD_READY_KEYS 游标的 SQL 语句的性能开始下降。尽管它已经完全优化,但它开始运行需要接近 0.2 秒,这意味着每个 Java 线程每秒最多只能处理 5 条记录。实际上,考虑到获取锁、通过网络传输、实际执行处理、提交事务等所花费的时间,将导致处理速度更慢。

增加 java 线程的数量是一种选择,但我们不能超过一定的限制,因为它们会开始对数据库/应用程序服务器/系统资源等造成压力。

真正的问题是我们运行一条 SQL 语句从总共两百万个键中获取包含一万五千个键的 RECORD_READY_KEYS,然后我们从顶部拾取第一条可用记录,然后通过关闭游标丢弃其余记录.

我的想法是在包级别定义一个 KEYS_CACHE 嵌套表,并将 RECORD_READY_KEYS 选择的结果存储在该嵌套表中。一旦一个键被锁定,它将从 KEYS_CACHE 中删除它并将其返回给 java 线程。这个过程可以一直这样下去,直到整个 KEYS_CACHE 被消耗掉,当这种情况发生时,它会再次填充它。

现在我的问题是:

第一季度。你能看到这种方法的任何弱点吗? 我可以看到多个线程试图同时锁定同一个记录,这样浪费了一些时间。在 java 方面,我们可以使存储过程调用与给定的扩展同步,因为调用将来自多个 JVM。但是,我看不出这是一个主要问题。 另一个问题是当发生不太可能的回滚时,因为没有简单的方法可以恢复已删除的密钥。下一个 RECORD_READY_KEYS 选择将再次将其取回,并且延迟几分钟并不重要。

第二季度。随着嵌套表获得的记录越来越少,它将变得非常稀疏。你能看到这成为一个问题吗?如果是这样,我应该将初始大小限制为 5000 个键,否则这并不重要。

第三季度。你能看到这么多线程(我们有 25 到 100 个线程)同时访问包级别 KEYS_CACHE 嵌套表的问题吗

第四季度。您能看到不需要重新设计整个系统的替代方法吗?

提前谢谢你

我认为我在解释我的情况时不是很清楚。我们不会在两百万条记录表中锁定要处理的记录,但我们会锁定键,而不是锁定也保存在不同锁定表中的键。

假设我有一个名为 messages 的 200 万条记录表:

并且只有带有 Key-A、Key-B 和 Key-C 的消息准备好被处理,密钥锁定表的可能内容可能是:

请注意,即使没有准备好为该密钥处理的消息,Key-X 也在那里,因为具有此类密钥的消息刚刚完成处理并且清理线程尚未启动。这是可以的,甚至是可取的,以防更多带有 Key-X 的新消息将在短时间内进入系统,同时它会保存新的插入。

所以我们的选择(完全优化)将按此顺序获得包含 Key-A、Key-C 和 Key-B 的列表(Key-C 位于 Key-B 之前,因为有一条 Id = 2 的消息小于 Id=6 的第一个 Key-B 消息 非常简化我们在这里所做的实际上是

SELECT key FROM messages WHERE ready = ‘Y’ GROUP BY key ORDER BY min(id)

一旦我们在游标中获得该选择,我们就会一一获取密钥并尝试将其锁定在 key_lockckings 表中。一旦锁定成功,密钥就会被分配给一个线程(为此有线程表),并将留在该线程处理所有为该密钥做好准备的消息。正如我在第一篇文章中提到的,具有相同密钥的消息由同一线程处理至关重要,因为密钥是我们如何链接必须按顺序处理的相关消息。

The SELECT above is instantly when the number of keys selected is up to a few thousands.当它获得大约 10000 个键时,它仍然表现良好。一旦检索到的密钥数量超过 15000,性能就会开始下降。运行 SELECT 的时间仍然可以(大约 0.2 秒),并且我们确实在此选择所涉及的所有字段上都有索引。只是将 WHERE、GROUP、ORDER BY 应用于从 200 万条记录中选择 15000 个键需要时间。

所以对我们来说问题是每个线程都将运行相同的 SELECT 并且会获得 15000 条记录,只是为了获取其中一条。我正在考虑的想法是,与其像我们目前所做的那样关闭游标并放弃艰苦的工作,而是尝试将这些键存储在包级别的嵌套表中,并在我们将它们分配给线程时从那里删除键。我的前三个问题只是想了解其他人对这种方法的看法,而最后一个问题是关于寻找一些替代想法(例如,有人会说使用高级队列等)

【问题讨论】:

  • 也许你应该发布慢的代码。

标签: java performance oracle11g


【解决方案1】:

我有一个(我认为)非常相似的例子。

您有一个包含很多行的表,并且您有多个消费者进程(Java 线程),它们都希望处理该表的内容。

首先,我建议避免使用SKIP LOCKED。如果您认为自己需要它,请考虑是否将INITRANS 设置在桌面上足够高。考虑SKIP LOCKED 意味着Oracle 将跳过锁定的资源,而不仅仅是锁定的行。如果一个块的 ITL 已满,SKIP LOCKED 将跳过它,即使该块中有未锁定的行!

有关此问题的更详细讨论,请参见此处: https://markjbobak.wordpress.com/2010/04/06/unintended-consequences/

那么,就我的建议而言。对于每个并发 JAVA 线程,定义一个线程号。因此,假设您有 10 个并发线程,为每个线程分配一个线程号或线程 ID,0-9。现在,如果您可以灵活地修改表格,您可以添加一个列 THREAD_ID,然后在从表格中选择时在 select 语句中使用它。每个并发 JAVA 线程将只选择那些与其线程 id 匹配的行。这样,您可以保证避免碰撞。如果您没有向表中添加列的能力,那么您是否希望拥有一个不错的、数字的、序列驱动的主键?如果是这样,你可以通过查询MOD(PRIMARY_KEY_COLUMN, 10) = :client_thread_id得到同样的效果。

此外,您是否有指定某种状态或类似状态的列,您将使用这些列来确定表中的哪些行有资格被 Java 线程处理?如果是这样,特别是如果该标准显着提高了选择性,那么创建一个只为您感兴趣的值填充的虚拟列可能非常有用,如果该列随后添加到索引中。以(THREAD_ID, STATUS) 为例。

最后,您提到了按特定顺序处理。如果THREAD_ID, STATUS 是您的选择标准,那么PRIORITYSTATUS_DATE 列可能是您的订购要求。在这种情况下,继续构建索引,添加指定所需顺序的列,并以表的主键作为顶部,可能会很有用。

通过精心构建的索引,并使用THREAD_ID 的想法,应该可以构建一个索引,让您能够:

  • 避免冲突(在主键上使用 THREAD_ID 或 MOD())
  • 最小化索引的大小(虚拟列)
  • 避免任何ORDER BY 操作(按列添加顺序到索引)
  • 避免任何TABLE ACCESS BY ROWID 操作(将主键列添加到索引末尾)

我做了一些可能适用也可能不适用的假设。

【讨论】:

  • 非常感谢您的投入以及花时间撰写如此详细的评论。我知道 SELECT FOR UPDATE SKIP LOCKED 可以使记录保持解锁状态,即使可以锁定,因为它们与已锁定的数据块位于同一数据块中。不好,但我认为只要数据锁定发生在块级别而不是行级别,我们就无能为力。无论如何,锁定是我知道的唯一一种防止两个单独的线程拾取相同记录进行处理的方法。请阅读我更新的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-12
  • 2019-09-02
  • 2018-12-26
  • 2014-02-15
  • 1970-01-01
  • 2012-05-12
相关资源
最近更新 更多