防止无用的数据库访问
阅读完这篇文章的标题后,很多人可能会想……
嗯,我已经知道怎么做了。不过,我们先等等,因为它可能没你想象的那么明显。比如,你们中有多少人写过这样的代码?我知道我写过!
User.where(:id => user_ids).each do |user|
# Lots of user processing
end
这段代码看起来不错,对吧?如果没有 user_ids,这个代码块就会跳过所有用户处理。这看起来很不错,所以应该没问题。可惜的是,这个假设并不完全正确。让我来解释一下原因。
当您执行该 where 子句时,即使使用空数组,它实际上也会命中 MySQL。
(pry)> User.where(:id => [])
User Load (1.0ms) SELECT `users`.* FROM `users` WHERE 1=0
=> []
注意where 1=0
SQL 末尾的语句。ActiveRecord 正是通过这种方式确保不返回任何记录。当然,这是一个快速1ms
查询,但如果执行这段代码数百万次,那么这个快速查询很容易导致数据库不堪重负,从而降低速度。那么,如何更新这段代码以提高性能呢?
您有两个选择。第一种是,除非万不得已,否则不运行 MySQL 查找。您可以在执行代码块之前使用 Ruby 进行简单的数组检查。
return unless user_ids.any?
User.where(:id => user_ids).each do |user|
# Lots of user processing
end
这样做可以避免无谓的数据库访问,并确保数据库不会被无用的调用压垮。此外,这还能加快代码执行速度。假设你运行这段代码 1 万次,运行 MySQL 查找 1 万次将花费超过半秒钟的时间。
(pry)> Benchmark.realtime do
> 10_000.times { User.where(:id => []) }
> end
=> 0.5508159045130014
相反,如果您通过先检查是否存在任何 user_id 来跳过该 MySQL 查询,那么运行类似的代码块 10k 次只需不到百分之一秒!
(pry)> Benchmark.realtime do
> 10_000.times do
> next unless ids.any?
> User.where(:id => [])
> end
> end
=> 0.0006368421018123627
正如你所见,不必要的 10000 次 MySQL 调用和 10000 次普通的 Ruby 执行之间存在显著的时间差异。这种差异会对应用程序的性能产生重大影响。很多人会查看这段代码
User.where(:id => user_ids).each do |user|
# Lots of user processing
end
他们首先会说“Ruby 很慢”。但这与事实相去甚远,因为我们刚刚看到纯 Ruby 代码的速度要快几百倍!在这种情况下,Ruby 并不慢,访问数据库才慢!在你的代码中,一定要留意类似的情况,你可能会进行一些意想不到的数据库调用。
现在有些人可能会看到这段代码,觉得我写的代码不太对劲。实际上,我把一堆作用域链接到了我的 where 子句中。
users = User.where(:id => user_ids).active.short.single
所以我需要传递那个空的 user_id 数组,否则作用域链就会断裂。值得庆幸的是,虽然 ActiveRecord 不能很好地处理空数组,但它确实提供了一个处理空作用域的选项,那就是 none 作用域。
none 是一个 ActiveRecord 查询方法,它允许你返回一个包含零条记录的可链接关系,而无需查询数据库。让我们来看一下实际操作。之前我们已经知道,如果 where 子句的 ID 为空,则查询数据库。
(pry)> User.where(:id => []).active.tall.single
User Load (0.7ms) SELECT `users`.* FROM `users` WHERE 1=0 AND `users`.`active` = 1 AND `users`.`short` = 0 AND `users`.`single` = 1
=> []
但是,如果我们用 none 范围替换该 where 子句,您会发现没有进行数据库调用,并且我们仍然可以将我们的范围链接在一起。
(pry)> User.none.active.tall.single
=> []
在你的框架中寻找类似的工具,它们能让你更智能地处理空数据集。更重要的是,永远不要假设你的框架或 gem 在被要求处理空数据集时没有进行数据库调用。想要了解更多关于查找此类调用的技巧,请查看我关于日志记录的博客文章。
实际生活应用
使用 Ruby 来防止数据库访问的理念并不仅限于 MySQL;它可以应用于任何类型的数据库!在 Kenna,我们发现它在构建所谓的报告时非常有用。我们每天晚上都会为客户创建这些色彩鲜艳的 PDF 报告。
这些报告始于一个报告对象,该对象包含构建该报告所需的所有信息。然后,为了构建这个漂亮的报告页面,我们每晚都要向 Elasticsearch 发出 20 多个请求,以及向 Redis 和 MySQL 发出多个请求。
我们做了大量工作,确保所有这些请求都能快速处理,但构建报告仍然需要花费数小时。最终,保存的报告数量激增,我们无法在一夜之间完成所有工作。当我和我的团队开始尝试解决这个问题时,我们做的第一件事就是打开控制台,仔细查看现有报告包含的数据。经过一番挖掘,我们发现,在我们系统中的 2.5 万份报告中,超过三分之一是空白的!
(pry)> Report.blank_reports.count
=> 10805
这意味着它们不包含任何数据,如果报告不包含任何数据,那么当我们知道它们不会返回任何内容时,发出所有这些 Elasticsearch、MySQL 和 Redis 请求的意义何在?

灯泡!如果报告为空,就不要访问数据库!通过跳过没有数据的报告,我们将处理时间从10 多个小时缩短到了 3 小时。只需添加一行简单的 Ruby 代码
def build(report)
return if report.blank?
# Processing
end
我们成功避免了大量无用的数据库访问,从而大大加快了处理速度。这种使用 Ruby 保护数据库免受请求影响的策略,我喜欢称之为“数据库防护”。实践起来很简单,但我认为它是编写代码时最容易忽略的事情之一。每次数据库访问都会消耗资源,所以要充分利用它们!
如果您对使用 Ruby 防止数据库命中的其他方法感兴趣,请查看我在RubyConf 上发表的“Cache Is King”演讲,该演讲启发了本文的灵感。
文章来源:https://dev.to/molly/preventing-useless-database-hits-2f50