跳至内容 跳至搜索

Active Support 缓存存储

一个抽象的缓存存储类。有多种缓存存储实现,每种实现都有自己的额外功能。请查看 ActiveSupport::Cache 模块下的类,例如 ActiveSupport::Cache::MemCacheStoreMemCacheStore 目前是大型生产网站最流行的缓存存储。

某些实现可能不支持除 fetchwritereadexist?delete 之外的基本缓存方法之外的所有方法。

ActiveSupport::Cache::Store 可以存储其 coderdumpload 方法支持的任何 Ruby 对象。

cache = ActiveSupport::Cache::MemoryStore.new

cache.read('city')   # => nil
cache.write('city', "Duckburgh") # => true
cache.read('city')   # => "Duckburgh"

cache.write('not serializable', Proc.new {}) # => TypeError

键始终转换为字符串,并且区分大小写。当对象被指定为键并且定义了 cache_key 方法时,将调用此方法来定义键。否则,将调用 to_param 方法。哈希和数组也可以用作键。元素将用斜杠分隔,并且 Hash 中的元素将按键排序,以确保一致性。

cache.read('city') == cache.read(:city)   # => true

可以缓存空值。

如果您的缓存位于共享基础设施上,则可以为缓存条目定义一个命名空间。如果定义了命名空间,它将作为前缀附加到每个键上。命名空间可以是静态值或 Proc。如果它是一个 Proc,它将在评估每个键时被调用,以便您可以使用应用程序逻辑来使键失效。

cache.namespace = -> { @last_mod_time }  # Set the namespace to a variable
@last_mod_time = Time.now  # Invalidate the entire cache by changing namespace
方法
C
D
E
F
I
K
M
N
R
S
W

属性

[R] options
[R] silence
[R] silence?

类公共方法

new(options = nil)

创建一个新的缓存。

选项

:namespace

设置缓存的命名空间。此选项在您的应用程序与其他应用程序共享缓存时特别有用。

:serializer

缓存值的序列化器。必须响应 dumpload

默认序列化器取决于缓存格式版本(在使用 Rails 时通过 config.active_support.cache_format_version 设置)。每个格式版本的默认序列化器都包含一个回退机制,用于反序列化来自任何格式版本的的值。此行为使在格式版本之间迁移变得容易,而不会使整个缓存失效。

您还可以指定 serializer: :message_pack 以使用基于 ActiveSupport::MessagePack 的预配置序列化器。:message_pack 序列化器包含相同的反序列化回退机制,允许从(或到)默认序列化器轻松迁移。:message_pack 序列化器可能会提高性能,但它需要 msgpack gem。

:compressor

序列化缓存值的压缩器。必须响应 deflateinflate

默认压缩器是 Zlib。要定义一个新的自定义压缩器,该压缩器还可以解压缩旧的缓存条目,您可以检查压缩值是否有 Zlib 的 "\x78" 签名

module MyCompressor
  def self.deflate(dumped)
    # compression logic... (make sure result does not start with "\x78"!)
  end

  def self.inflate(compressed)
    if compressed.start_with?("\x78")
      Zlib.inflate(compressed)
    else
      # decompression logic...
    end
  end
end

ActiveSupport::Cache.lookup_store(:redis_cache_store, compressor: MyCompressor)
:coder

用于序列化和(可选)压缩缓存条目的编码器。必须响应 dumpload

默认编码器组合了序列化器和压缩器,并包含一些性能优化。如果您只需要覆盖序列化器或压缩器,则应指定 :serializer:compressor 选项。

如果存储可以直接处理缓存条目,您也可以指定 coder: nil 以省略序列化器、压缩器和编码器。例如,如果您使用的是 ActiveSupport::Cache::MemoryStore 并且可以保证缓存值不会被修改,则可以指定 coder: nil 以避免保护免受修改的开销。

:coder 选项与 :serializer:compressor 选项互斥。一起指定它们将引发 ArgumentError

任何其他指定的选项都被视为相关缓存操作的默认选项,例如 readwritefetch

# File activesupport/lib/active_support/cache.rb, line 295
def initialize(options = nil)
  @options = options ? validate_options(normalize_options(options)) : {}

  @options[:compress] = true unless @options.key?(:compress)
  @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT

  @coder = @options.delete(:coder) do
    legacy_serializer = Cache.format_version < 7.1 && !@options[:serializer]
    serializer = @options.delete(:serializer) || default_serializer
    serializer = Cache::SerializerWithFallback[serializer] if serializer.is_a?(Symbol)
    compressor = @options.delete(:compressor) { Zlib }

    Cache::Coder.new(serializer, compressor, legacy_serializer: legacy_serializer)
  end

  @coder ||= Cache::SerializerWithFallback[:passthrough]

  @coder_supports_compression = @coder.respond_to?(:dump_compressed)
end

实例公共方法

cleanup(options = nil)

通过删除过期条目来清理缓存。

选项被传递给底层的缓存实现。

某些实现可能不支持此方法。

# File activesupport/lib/active_support/cache.rb, line 749
def cleanup(options = nil)
  raise NotImplementedError.new("#{self.class.name} does not support cleanup")
end

clear(options = nil)

清除整个缓存。使用此方法时要小心,因为它可能会影响其他进程(如果使用共享缓存)。

选项哈希被传递给底层的缓存实现。

某些实现可能不支持此方法。

# File activesupport/lib/active_support/cache.rb, line 759
def clear(options = nil)
  raise NotImplementedError.new("#{self.class.name} does not support clear")
end

decrement(name, amount = 1, options = nil)

减少缓存中的整数值。

选项被传递给底层的缓存实现。

某些实现可能不支持此方法。

# File activesupport/lib/active_support/cache.rb, line 740
def decrement(name, amount = 1, options = nil)
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
end

delete(name, options = nil)

删除缓存中的一个条目。如果删除了条目,则返回 true,否则返回 false

选项被传递给底层的缓存实现。

# File activesupport/lib/active_support/cache.rb, line 676
def delete(name, options = nil)
  options = merged_options(options)
  key = normalize_key(name, options)

  instrument(:delete, key, options) do
    delete_entry(key, **options)
  end
end

delete_matched(matcher, options = nil)

删除所有与模式匹配的键的条目。

选项被传递给底层的缓存实现。

某些实现可能不支持此方法。

# File activesupport/lib/active_support/cache.rb, line 722
def delete_matched(matcher, options = nil)
  raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
end

delete_multi(names, options = nil)

删除缓存中的多个条目。返回已删除的条目数。

选项被传递给底层的缓存实现。

# File activesupport/lib/active_support/cache.rb, line 689
def delete_multi(names, options = nil)
  return 0 if names.empty?

  options = merged_options(options)
  names.map! { |key| normalize_key(key, options) }

  instrument_multi(:delete_multi, names, options) do
    delete_multi_entries(names, **options)
  end
end

exist?(name, options = nil)

如果缓存包含给定键的条目,则返回 true

选项被传递给底层的缓存实现。

# File activesupport/lib/active_support/cache.rb, line 703
def exist?(name, options = nil)
  options = merged_options(options)
  key = normalize_key(name, options)

  instrument(:exist?, key) do |payload|
    entry = read_entry(key, **options, event: payload)
    (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
  end
end

fetch(name, options = nil, &block)

从缓存中获取数据,使用给定的键。如果缓存中存在具有给定键的数据,则返回该数据。

如果缓存中没有这样的数据(缓存未命中),则将返回 nil。但是,如果传递了块,则该块将在缓存未命中时传递给该键并执行。块的返回值将写入缓存中,位于给定的缓存键下,该返回值将被返回。

cache.write('today', 'Monday')
cache.fetch('today')  # => "Monday"

cache.fetch('city')   # => nil
cache.fetch('city') do
  'Duckburgh'
end
cache.fetch('city')   # => "Duckburgh"

选项

在内部,fetch 调用 read_entry,并在缓存未命中时调用 write_entry。因此,fetch 支持与 readwrite 相同的选项。此外,fetch 支持以下选项

  • force: true - 强制缓存“未命中”,这意味着即使缓存值存在,我们也将其视为缺失。当 force 为 true 时,需要传递块,因此这始终会导致缓存写入。

    cache.write('today', 'Monday')
    cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
    cache.fetch('today', force: true) # => ArgumentError
    

    :force 选项在您调用其他方法来询问是否应该强制缓存写入时很有用。否则,直接调用 write 更清晰。

  • skip_nil: true - 防止缓存空结果

    cache.fetch('foo') { nil }
    cache.fetch('bar', skip_nil: true) { nil }
    cache.exist?('foo') # => true
    cache.exist?('bar') # => false
    
  • :race_condition_ttl - 指定过期值可以被重用以生成新值的秒数。这可用于防止缓存条目过期时的竞争条件,方法是防止多个进程同时生成同一个条目(也称为狗堆效应)。

    当一个进程遇到一个已经过期少于 :race_condition_ttl 秒的缓存条目时,它会在生成新值之前将过期时间延长 :race_condition_ttl 秒。在这个扩展的时间窗口内,当进程生成新值时,其他进程将继续使用旧值。第一个进程写入新值后,其他进程将使用新值。

    如果第一个进程在生成新值时出错,另一个进程可以在扩展的时间窗口结束后尝试生成新值。

    # Set all values to expire after one minute.
    cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1)
    
    cache.write("foo", "original value")
    val_1 = nil
    val_2 = nil
    p cache.read("foo") # => "original value"
    
    sleep 1 # wait until the cache expires
    
    t1 = Thread.new do
      # fetch does the following:
      # 1. gets an recent expired entry
      # 2. extends the expiry by 2 seconds (race_condition_ttl)
      # 3. regenerates the new value
      val_1 = cache.fetch("foo", race_condition_ttl: 2) do
        sleep 1
        "new value 1"
      end
    end
    
    # Wait until t1 extends the expiry of the entry
    # but before generating the new value
    sleep 0.1
    
    val_2 = cache.fetch("foo", race_condition_ttl: 2) do
      # This block won't be executed because t1 extended the expiry
      "new value 2"
    end
    
    t1.join
    
    p val_1 # => "new value 1"
    p val_2 # => "original value"
    p cache.fetch("foo") # => "new value 1"
    
    # The entry requires 3 seconds to expire (expires_in + race_condition_ttl)
    # We have waited 2 seconds already (sleep(1) + t1.join) thus we need to wait 1
    # more second to see the entry expire.
    sleep 1
    
    p cache.fetch("foo") # => nil
    

动态选项

在某些情况下,可能需要根据缓存值动态计算选项。为了支持这一点,将一个 ActiveSupport::Cache::WriteOptions 实例作为第二个参数传递给块。例如

cache.fetch("authentication-token:#{user.id}") do |key, options|
  token = authenticate_to_service
  options.expires_at = token.expires_at
  token
end
# File activesupport/lib/active_support/cache.rb, line 444
def fetch(name, options = nil, &block)
  if block_given?
    options = merged_options(options)
    key = normalize_key(name, options)

    entry = nil
    unless options[:force]
      instrument(:read, key, options) do |payload|
        cached_entry = read_entry(key, **options, event: payload)
        entry = handle_expired_entry(cached_entry, key, options)
        if entry
          if entry.mismatched?(normalize_version(name, options))
            entry = nil
          else
            begin
              entry.value
            rescue DeserializationError
              entry = nil
            end
          end
        end
        payload[:super_operation] = :fetch if payload
        payload[:hit] = !!entry if payload
      end
    end

    if entry
      get_entry_value(entry, name, options)
    else
      save_block_result_to_cache(name, key, options, &block)
    end
  elsif options && options[:force]
    raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
  else
    read(name, options)
  end
end

fetch_multi(*names)

从缓存中获取数据,使用给定的键。如果缓存中存在使用给定键的数据,则返回该数据。否则,对于每个没有数据的键,都会调用提供的块,并将结果写入缓存并返回。因此,您需要传递一个返回要写入缓存数据的块。如果您不想在缓存未找到时写入缓存,请使用 read_multi

返回一个包含每个名称数据的哈希表。例如

cache.write("bim", "bam")
cache.fetch_multi("bim", "unknown_key") do |key|
  "Fallback value for key: #{key}"
end
# => { "bim" => "bam",
#      "unknown_key" => "Fallback value for key: unknown_key" }

您也可以通过 options 参数指定其他选项。有关详细信息,请参阅 fetch。其他选项将传递给底层缓存实现。例如

cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
  "buzz"
end
# => {"fizz"=>"buzz"}
cache.read("fizz")
# => "buzz"
sleep(6)
cache.read("fizz")
# => nil
# File activesupport/lib/active_support/cache.rb, line 595
def fetch_multi(*names)
  raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
  return {} if names.empty?

  options = names.extract_options!
  options = merged_options(options)
  keys    = names.map { |name| normalize_key(name, options) }
  writes  = {}
  ordered = instrument_multi :read_multi, keys, options do |payload|
    if options[:force]
      reads = {}
    else
      reads = read_multi_entries(names, **options)
    end

    ordered = names.index_with do |name|
      reads.fetch(name) { writes[name] = yield(name) }
    end
    writes.compact! if options[:skip_nil]

    payload[:hits] = reads.keys.map { |name| normalize_key(name, options) }
    payload[:super_operation] = :fetch_multi

    ordered
  end

  write_multi(writes, options)

  ordered
end

increment(name, amount = 1, options = nil)

递增缓存中的整数值。

选项被传递给底层的缓存实现。

某些实现可能不支持此方法。

# File activesupport/lib/active_support/cache.rb, line 731
def increment(name, amount = 1, options = nil)
  raise NotImplementedError.new("#{self.class.name} does not support increment")
end

mute()

在块内静默日志记录器。

# File activesupport/lib/active_support/cache.rb, line 322
def mute
  previous_silence, @silence = @silence, true
  yield
ensure
  @silence = previous_silence
end

read(name, options = nil)

从缓存中读取数据,使用给定的键。如果缓存中存在使用给定键的数据,则返回该数据。否则,返回 nil

注意,如果数据使用 :expires_in:version 选项写入,则这两个条件都会在返回数据之前应用。

选项

  • :namespace - 为此调用替换存储命名空间。

  • :version - 为缓存条目指定一个版本。如果缓存版本与请求版本不匹配,则读取将被视为缓存未命中。此功能用于支持可回收的缓存键。

其他选项将由特定的缓存存储实现处理。

# File activesupport/lib/active_support/cache.rb, line 498
def read(name, options = nil)
  options = merged_options(options)
  key     = normalize_key(name, options)
  version = normalize_version(name, options)

  instrument(:read, key, options) do |payload|
    entry = read_entry(key, **options, event: payload)

    if entry
      if entry.expired?
        delete_entry(key, **options)
        payload[:hit] = false if payload
        nil
      elsif entry.mismatched?(version)
        payload[:hit] = false if payload
        nil
      else
        payload[:hit] = true if payload
        begin
          entry.value
        rescue DeserializationError
          payload[:hit] = false
          nil
        end
      end
    else
      payload[:hit] = false if payload
      nil
    end
  end
end

read_multi(*names)

一次从缓存中读取多个值。选项可以在最后一个参数中传递。

一些缓存实现可能会优化此方法。

返回一个将提供的名称映射到找到的值的哈希表。

# File activesupport/lib/active_support/cache.rb, line 536
def read_multi(*names)
  return {} if names.empty?

  options = names.extract_options!
  options = merged_options(options)
  keys    = names.map { |name| normalize_key(name, options) }

  instrument_multi :read_multi, keys, options do |payload|
    read_multi_entries(names, **options, event: payload).tap do |results|
      payload[:hits] = results.keys.map { |name| normalize_key(name, options) }
    end
  end
end

silence!()

静默日志记录器。

# File activesupport/lib/active_support/cache.rb, line 316
def silence!
  @silence = true
  self
end

write(name, value, options = nil)

使用键将值写入缓存。该值必须受 coderdumpload 方法支持。

如果写入成功,则返回 true,如果与缓存后端通信时出现错误,则返回 nil,如果写入因其他原因失败,则返回 false

默认情况下,大于 1kB 的缓存条目将被压缩。压缩允许在相同内存占用空间内存储更多数据,从而导致缓存驱逐减少和命中率提高。

选项

  • compress: false - 禁用缓存条目的压缩。

  • :compress_threshold - 压缩阈值,以字节为单位指定。大于此阈值的缓存条目将被压缩。默认值为 1.kilobyte

  • :expires_in - 为缓存条目设置相对过期时间,以秒为单位指定。:expire_in:expired_in:expires_in 的别名。

    cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
    cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
    
  • :expires_at - 为缓存条目设置绝对过期时间。

    cache = ActiveSupport::Cache::MemoryStore.new
    cache.write(key, value, expires_at: Time.now.at_end_of_hour)
    
  • :version - 为缓存条目指定一个版本。从缓存中读取时,如果缓存版本与请求版本不匹配,则读取将被视为缓存未命中。此功能用于支持可回收的缓存键。

其他选项将由特定的缓存存储实现处理。

# File activesupport/lib/active_support/cache.rb, line 662
def write(name, value, options = nil)
  options = merged_options(options)
  key = normalize_key(name, options)

  instrument(:write, key, options) do
    entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
    write_entry(key, entry, **options)
  end
end

write_multi(hash, options = nil)

Cache 存储 API 用于一次写入多个值。

# File activesupport/lib/active_support/cache.rb, line 551
def write_multi(hash, options = nil)
  return hash if hash.empty?

  options = merged_options(options)
  normalized_hash = hash.transform_keys { |key| normalize_key(key, options) }

  instrument_multi :write_multi, normalized_hash, options do |payload|
    entries = hash.each_with_object({}) do |(name, value), memo|
      memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
    end

    write_multi_entries entries, **options
  end
end

实例私有方法

key_matcher(pattern, options)

将选项中定义的命名空间添加到旨在匹配键的模式中。支持 delete_matched 的实现应该调用此方法将匹配名称的模式转换为匹配命名空间键的模式。

# File activesupport/lib/active_support/cache.rb, line 779
def key_matcher(pattern, options) # :doc:
  prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace]
  if prefix
    source = pattern.source
    if source.start_with?("^")
      source = source[1, source.length]
    else
      source = ".*#{source[0, source.length]}"
    end
    Regexp.new("^#{Regexp.escape(prefix)}:#{source}", pattern.options)
  else
    pattern
  end
end