0

我有一个 Rails 应用程序,它将其配置存储在 34 个 MySQL 表中,这些表由各种对象和关联组成,总共大约 900 条记录。直到最近,业务逻辑都建立在 ActiveRecord 上,但性能不稳定,因为我没有足够的控制来控制多少查询被触发。最近我将业务逻辑“移植”到Dry::Struct,将所有涉及的 ActiveRecord 类复制到Dry::Struct值模型并预加载Configuration实例中的所有配置对象:这大大减少了查询数量到固定的小数量,并显着提高了性能因为所有的联想“行走”都是在记忆中完成的,而且还有很多。

到目前为止一切顺利,但加载 34 个表(我在每次请求时都需要其中的大部分)仍然需要 34 个查询和大约 160 毫秒。该策略在 ActiveRecord 上处于低级别,将所有表中的所有记录加载为普通哈希,然后用这些初始化结构并将所有内容存储在Configuration对象中。

我想进一步提高性能,所以我想通过UNION在所有表中的所有字段中创建一个查询来获取所有数据。这构成了一个强大的 30 KB 的 SQL 查询,令人惊讶的是,它仅在 20 毫秒内执行。出色的!现在我只需要在大约 10 毫秒内展开这个大结构,我就赢了!

嗯,不。事实证明,仅扫描结果数组需要 48 毫秒(其中 20 是 SQL 查询),当在这里和那里解析一些 JSON 字段并准备哈希以初始化结构时,时间会增加到 78 毫秒......然后初始化它们本身需要额外的89 毫秒。如果我没有通过重复算法 100 次来测量每一步(当然是在预热记忆值之后),我不会相信,但确实如此。总而言之,与之前单独加载每个表的简单得多的算法相比,尽管单个查询很有效,但没有任何性能提升。

下面是 SQL 的样子:

SELECT a1, a2, NULL, NULL, NULL, NULL FROM table_a
UNION ALL
SELECT NULL, NULL, b1, b2, NULL, NULL FROM table_b
UNION ALL
SELECT NULL, NULL, NULL, NULL, c1, c2 FROM table_c

产生这样的“对角线”结构

"string", 3, NULL, NULL, NULL, NULL -- from table_a
"other string", 5, NULL, NULL, NULL, NULL -- from table_a
NULL, NULL, 1, "{\"json\":true}", NULL, NULL -- from table_b
NULL, NULL, 2, NULL, NULL, NULL -- from table_b
NULL, NULL, NULL, NULL, 7, 10 -- from table_c
NULL, NULL, NULL, NULL, 9, 51 -- from table_c

然后以下算法将其展开为原始记录:

  def preload_all!
    ranges = self.class.preload_field_ranges.invert
    logger.measure_debug("Preloaded configuration") do
      ApplicationRecord.connection.execute(self.class.preload_query).each do |data|
        # finding where the first significant column is
        pos = data.index { |i| !i.nil? }
        # resolving the table name based on where the significant value was found, exiting early
        range, table_name = ranges.select { |k, v| break [ k, v ] if pos.in?(k) }
        v_class = self.class.tables_to_value_classes[table_name]
        values = data[range].map do |i|
          case i
          when String
            # horrible kludge to parse JSON fields, because I wasn't able to inspect AR
            # classes to ask them which fields are serialized, any help is appreciated
            case
            when i[0].in?([ "{", "[" ]) then JSON.parse(i)
            else i
            end
          else i
          end
        end
        # assignments are for clarity, doing these operations inline shaves about 10 ms
        ivar = "@#{table_name}"
        hash = self.class.ar_attribute_names[table_name].zip(values).to_h
        v_model = v_class.new(hash.merge(configuration: self))
        vhash = instance_variable_get(ivar) || {}
        instance_variable_set(ivar, vhash.tap { |h| h[v_model.id] = v_model })
      end
    end
    self
  end

我试图尽可能地收紧代码,但只是删除v_class.new部分将时间缩短了一半。有没有改进的余地?

附带说明一下,从 Redis 加载Marshaled 完成的Configuration对象只需要 10 毫秒,但我想避免使用 Redis 来防止错位。

4

1 回答 1

0

正如solnic本人所建议的那样:

更改.new.load它只会设置不带应用类型的 ivars,这将为您提供巨大的速度提升。仅当您信任数据时才执行此操作。

于 2021-04-22T14:51:13.263 回答