UP | HOME

应用级别缓存

Table of Contents

1 应用级别缓存

基于内存的缓存比如 Memcached 和 Redis 是应用程序和数据存储之间的一种键值存储。 由于数据保存在 RAM 中,它比存储在磁盘上的典型数据库要快多了。RAM 比磁盘限制更 多,所以例如 LRU 的缓存无效算法可以将 “热门数据” 放在 RAM 中,而对一些比较 “冷 门” 的数据不做处理

Redis 有下列附加功能

  1. 持久性选项,支持 RDB 和 AOF 的持久化操作
  2. 内置数据结构比如有序集合和列表

有多个缓存级别,分为两大类: 数据库查询和对象

  1. 行级别
  2. 查询级别
  3. 完整的可序列化对象
  4. 完全渲染的 HTML

一般来说,你应该尽量避免基于文件的缓存,因为这使得复制和自动缩放很困难。

2 数据库查询级别的缓存

当你查询数据库的时候,将查询语句的哈希值与查询结果存储到缓存中。这种方法会遇到 以下问题:

  1. 很难用复杂的查询删除已缓存结果。
  2. 如果一条数据比如表中某条数据的一项被改变,则需要删除所有可能包含已更改项的 缓存结果。

3 对象级别的缓存

将您的数据视为对象,就像对待你的应用代码一样。让应用程序将数据从数据库中组合到 类实例或数据结构中:

  1. 如果对象的基础数据已经更改了,那么从缓存中删掉这个对象
  2. 允许异步处理:workers 通过使用最新的缓存对象来组装对象

建议缓存的内容:

  1. 用户会话
  2. 完全渲染的 Web 页面
  3. 活动流
  4. 用户图数据

4 更新缓存的时机

由于你只能在缓存中存储有限的数据,所以你需要选择一个适用于你用例的缓存更新策略

4.1 缓存模式

应用从存储器读写。缓存不和存储器直接交互,应用执行以下操作:

  1. 在缓存中查找记录,如果所需数据不在缓存中
  2. 从数据库中加载所需内容
  3. 将查找到的结果存储到缓存中
  4. 返回所需内容
[App] <-----------> [Cache]
  ^
  |
  +---------------> [Database]
def get_user(self, user_id):
    user = cache.get("user.{0}", user_id)
    if user is None:
        user = db.query("SELECT * FROM users WHERE user_id = {0}", user_id)
        if user is not None:
            key = "user.{0}".format(user_id)
            cache.set(key, json.dumps(user))
    return user
  • Memcached 通常用这种方式使用
  • 添加到缓存中的数据读取速度很快。缓存模式也称为延迟加载
  • 只缓存所请求的数据,这避免了没有被请求的数据占满了缓存空间

缓存模式的缺点

  1. 请求的数据如果不在缓存中就需要经过三个步骤来获取数据,这会导致明显的延迟
  2. 如果数据库中的数据更新了会导致缓存中的数据过时。这个问题需要通过设置 TTL 强制更新缓存或者直写模式来缓解这种情况
  3. 当一个节点出现故障的时候,它将会被一个新的节点替代,这增加了延迟的时间

4.2 直写模式

应用使用缓存作为主要的数据存储,将数据读写到缓存中,而缓存负责从数据库中读写 数据

  1. 应用向缓存中添加/更新数据
  2. 缓存同步地写入数据存储
  3. 返回所需内容

由于存写操作所以直写模式整体是一种很慢的操作,但是读取刚写入的数据很快。相比 读取数据,用户通常比较能接受更新数据时速度较慢。缓存中的数据不会过时

  +----------- 3. Return to App ----------+
  |                                       |
  v                                       |
[App] -- 1. Write to Cache --> [Cache] ---+
                                  |
                           2. Store in DB
                                  |
                                  v
                              [Database]

应用代码

set_user(12345, {"foo":"bar"})

缓存代码:

def set_user(user_id, values):
    user = db.query("UPDATE Users WHERE id = {0}", user_id, values)
    cache.set(user_id, user)

直写模式的缺点:

  1. 由于故障或者缩放而创建的新的节点,新的节点不会缓存,直到数据库更新为止。缓 存应用直写模式可以缓解这个问题
  2. 写入的大多数数据可能永远都不会被读取,用 TTL 可以最小化这种情况的出现

4.3 回写模式

在回写模式中,应用执行以下操作

  1. 在缓存中增加或者更新条目
  2. 异步写入数据,提高写入性能
  +----------- 3. Return to App ----------+
  |                                       |
  v                                       |
[App] -- 1. Write to Cache --> [Cache] ---+
                                  |
                        2. Add event to Queue
                                  |
                                  v
                               [Queue]
                                  |
              4. Asynchronously: select and execute event
                                  |
                                  v
                              [Database]

回写模式的缺点

  1. 缓存可能在其内容成功存储之前丢失数据
  2. 执行直写模式比缓存或者回写模式更复杂

4.4 刷新

你可以将缓存配置成在到期之前自动刷新最近访问过的内容。 如果缓存可以准确预测将 来可能请求哪些数据,那么刷新可能会导致延迟与读取时间的降低

[App] <------------ [Cache]
                       ^
                       |
           refresh to cache with strategy
                       |
                   [Database]

刷新的缺点

  1. 不能准确预测到未来需要用到的数据可能会导致性能不如不使用刷新。

5 缓存的缺点

  1. 需要保持缓存和真实数据源之间的一致性,比如数据库根据缓存无效
  2. 需要改变应用程序比如增加 Redis 或者 Memcached
  3. 无效缓存是个难题,什么时候更新缓存是与之相关的复杂问题

Last Updated 2021-08-05 Thu 18:53. Created by Jinghui Hu at 2021-08-05 Thu 18:23.