Rails是一个庞大的框架,内置了很多方便的工具,用于特定情况。在这个系列中,我们将看看隐藏在Rails庞大代码库中的一些鲜为人知的工具。
在这篇文章中,我们将解释increment 和decrement 中的方法Rails.cache 。
Rails.cache Helper
Rails.cache 是与你的应用程序中的缓存交互的入口。它也是一个抽象,给你一个通用的API来调用,而不考虑引擎盖下实际使用的缓存 "商店"。开箱后,Rails支持以下内容:
- 文件存储(FileStore
- 内存存储
- MemCacheStore
- NullStore
- RedisCacheStore
检查Rails.cache ,将显示你正在运行的是哪一个:
> Rails.cache
=> <#ActiveSupport::Cache::RedisCacheStore options={:namespace=>nil, ...
我们不会对它们进行详细的检查,但作为一个快速的总结:
- NullStore不存储任何东西;从这里读回的数据将总是返回
nil。这是一个新的Rails应用程序的默认设置。 - FileStore将缓存作为文件存储在硬盘上,所以即使你重新启动Rails服务器,它们也会持续存在。
- MemoryStore将缓存保存在RAM中,所以如果你停止你的Rails服务器,你也会抹去缓存。
- MemCacheStore和RedisCacheStore使用外部程序(分别是MemCache和Redis)来维护缓存。
由于各种原因,这里的前三个最常被用于开发/测试。在生产中,你可能会使用Redis或MemCache。
因为Rails.cache 抽离了这些服务之间的差异,所以很容易在不同的环境中运行不同的版本,而不用改变代码;例如,你可以在开发中使用NullStore ,在测试中使用MemoryStore ,在生产中使用RedisCacheStore 。
缓存数据
通过Rails的Rails.cache.read 、Rails.cache.write 和Rails.cache.fetch ,我们有一个简单的方法来存储和检索缓存中的任何任意数据。前面的文章详细介绍了这些方法;本文需要注意的是,这些方法没有内置的线程安全功能。假设我们正在更新来自多个线程的缓存数据,以保持运行计数;我们需要用某种锁来包裹读/写操作,以避免竞赛条件。考虑这个例子,假设我们已经设置好使用Redis缓存存储:
threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0)
4.times do
threads << Thread.new do
100.times do
current_count = Rails.cache.read(:test_counter)
current_count += 1
Rails.cache.write(:test_counter, current_count)
end
end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter)
这里我们有四个线程,每个线程将我们的缓存值递增一百次。结果应该是400,但大多数时候,会少很多--在我的测试运行中是269。这里所发生的是一个竞赛条件。我在上一篇文章中详细介绍了这些情况,但作为一个快速的总结,由于线程都是在相同的 "共享 "数据上操作,它们可能会彼此不同步。例如,一个线程可能会读取数值,然后另一个线程接手,也读取了这个数值,并将其递增和存储。然后,第一个线程继续工作,使用它现在已经过时的值。
解决这个问题的常见方法是用一个互斥锁(或Mutex)来包围代码,这样每次只有一个线程可以在锁内执行代码。不过在我们的案例中,Rails.cache有一些方法来处理这种情况。
Rails 缓存的增量和减量
Rails.cache 对象有increment 和decrement 两种方法,可以直接作用于缓存数据,比如我们的计数器方案:
threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0, raw: true)
4.times do
threads << Thread.new do
100.times do
Rails.cache.increment(:test_counter)
# repeating the increment just to highlight the thread safety
Rails.cache.decrement(:test_counter)
Rails.cache.increment(:test_counter)
end
end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter, raw: true)
要使用increment 和decrement ,我们必须告诉缓存存储是一个 "原始 "值(通过raw: true )。在读回数值时也必须这样做;否则,你会得到一个错误。基本上,我们是在告诉缓存我们想把这个值作为一个原始的整数来存储,这样我们就可以对它调用增量/减量,但是你仍然可以同时使用expires_in 和其他缓存标志。
这里的关键是,increment 和decrement 使用原子操作(至少对Redis和MemCache而言),这意味着它们是线程安全的;在原子操作期间,线程没有办法暂停。
值得注意的是,尽管我没有在这里的例子中使用它,这两个方法也都返回新的值。因此,如果你需要使用新的计数器值,而不仅仅是更新它,你也可以这样做,而无需额外调用read 。
真实世界的应用
从表面上看,这些increment 和decrement 方法似乎是低级别的辅助方法,你可能只在实现或维护像后台作业处理 gem 这样的东西时才关心它们。不过,一旦你了解了它们,你可能会惊讶于它们在哪里能派上用场。
我曾在一个生产应用中使用过这些东西,以避免重复的预定后台作业同时运行。在我们的案例中,我们有各种预定作业来更新搜索索引,标记废弃的购物车,等等。一般来说,这样做很好;问题是有些工作(尤其是搜索指数)会消耗大量的内存--如果两个工作同时运行,就会超过我们Heroku dyno的限制,工人就会被杀死。因为我们有几个这样的作业,所以不是简单地把它们标记为不重复或强制唯一的作业;两个不同的(因此也是唯一的)作业可能会试图同时运行,并使工作者瘫痪。
为了防止这种情况,我们为计划中的作业创建了一个基类,保持一个计数器,记录当前运行的数量。如果计数过高,作业就会重新排队并等待。
另一个例子是在我的一个副业项目中,一个后台作业(或多个作业)在用户等待时做一些处理。这就带来了一个常见的问题,即向后台作业的用户传达当前的进度。虽然有很多方法可以解决这个问题,但作为一个实验,我尝试使用Rails.cache.increment 来更新一个全局可用的计数器。其结构如下:
- 首先,我在
/app/models中添加了一个新的类,以抽象出计数器存在于缓存中这一事实。这就是所有对数值的访问都会流经的地方。其中的一部分是生成与工作相关的唯一的缓存密钥。 - 然后,工作创建这个模型的一个实例,并在项目被处理时更新它。
- 一个简单的JSON端点创建了这个模型的一个实例,以获取当前的值。
- 前端每隔几秒就会轮询这个端点以更新用户界面。当然,你可以用像ActionCable这样的东西使之更高级,并推送更新。
总结
老实说,Rails.cache.increment 并不是我经常使用的工具,因为我并不经常想要更新存储在缓存中的数据(从本质上讲,这些数据是暂时的)。如上所述,我使用它的时候通常与后台工作有关,因为这些工作已经在Redis中存储了他们的数据(至少在我工作过的大多数应用程序中),并且通常是临时的。在这种情况下,将相关的数据(例如完成百分比)存储在同一地方,并具有类似的短期持久性,似乎是很自然的。
就像所有 "不走寻常路 "的东西一样,你应该对在你的代码库中引入这样的东西保持警惕。我建议,至少要添加一些注释,向未来的开发者解释为什么你要使用这种他们可能不熟悉的方法。