【发布时间】: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