

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# Memcached 缓存策略
<a name="Strategies"></a>

在以下主题中，您可以找到填充和维护 Memcached 缓存的策略。

为填充并维护缓存而执行的策略取决于要缓存的数据以及针对这些数据的访问模式。例如，您可能不想对游戏站点上排名前 10 排行榜和趋势新闻报道执行相同的策略。在本节的剩余内容中，我们将讨论常见缓存维护策略及其优点和缺点。

**Topics**
+ [只读副本](#Strategies.ReadReplicas)
+ [延迟加载](#Strategies.LazyLoading)
+ [直写](#Strategies.WriteThrough)
+ [添加 TTL](#Strategies.WithTTL)
+ [相关主题](#Strategies.SeeAlso)

## 只读副本
<a name="Strategies.ReadReplicas"></a>

通常，您可以通过创建副本并从中读取副本而不是主缓存节点，来显著提高 ElastiCache 无服务器缓存的性能。有关更多信息，请参阅 [使用只读副本的最佳实践](ReadReplicas.md)。

## 延迟加载
<a name="Strategies.LazyLoading"></a>

顾名思义，*延迟加载*是一种仅在需要时将数据加载到缓存中的缓存策略。它的工作原理如下。

Amazon ElastiCache 是一种内存键值存储，位于您的应用程序和其访问的数据存储（数据库）之间。当应用程序请求数据时，它会先向 ElastiCache 缓存发出请求。如果数据在缓存中且最新，则 ElastiCache 会将数据返回到应用程序。如果数据不在缓存中或已过期，则应用程序会请求数据存储中的数据。然后，数据存储将数据返回到应用程序。之后，应用程序会将从存储接收的数据写入缓存。这样就可以在下次请求时更快地检索这些数据。

当数据位于缓存中且未过期时，就会发生*缓存命中*：

1. 应用程序请求缓存中的数据。

1. 缓存将数据返回给应用程序。

当数据不在缓存中或已过期时，就会发生*缓存未命中*：

1. 应用程序请求缓存中的数据。

1. 缓存不具有所请求的数据，因此返回了 `null`。

1. 应用程序请求数据库中的数据并收到数据。

1. 应用程序使用新数据更新缓存。

### 延迟加载的优点和缺点
<a name="Strategies.LazyLoading.Evaluation"></a>

延迟加载的优点如下：
+ 仅对请求的数据进行缓存。

  由于大部分数据从未被请求过，因此延迟加载避免了向缓存中填入未请求的数据。
+ 节点故障对应用程序来说并不致命。

  当某个节点发生故障并由新的空节点替换时，应用程序会继续运行，但延迟会增加。向新节点发出请求时，每次缓存未命中都会导致在数据库中进行查询。同时会将数据副本添加到缓存中，以便后续请求从缓存中进行检索。

延迟加载的缺点如下：
+ 缓存未命中会导致性能损失。每次缓存未命中都会导致 3 次往返：

  1. 初次从缓存中请求数据

  1. 查询数据库中的数据

  1. 将数据写入缓存

   这些未命中会导致在数据到达应用程序时出现显著延迟。
+ 过时数据。

  如果仅在缓存未命中时将数据写入缓存，则缓存中的数据会过时。出现此结果的原因在于，在数据库中更改数据时未更新缓存。要解决此问题，您可以使用 [直写](#Strategies.WriteThrough) 和 [添加 TTL](#Strategies.WithTTL) 策略。

### 延迟加载伪代码示例
<a name="Strategies.LazyLoading.CodeExample"></a>

以下代码是延迟加载逻辑的伪代码示例。

```
// *****************************************
// function that returns a customer's record.
// Attempts to retrieve the record from the cache.
// If it is retrieved, the record is returned to the application.
// If the record is not retrieved from the cache, it is
//    retrieved from the database, 
//    added to the cache, and 
//    returned to the application
// *****************************************
get_customer(customer_id)

    customer_record = cache.get(customer_id)
    if (customer_record == null)
    
        customer_record = db.query("SELECT * FROM Customers WHERE id = {0}", customer_id)
        cache.set(customer_id, customer_record)
    
    return customer_record
```

对于此示例，获取数据的应用程序代码如下。

```
customer_record = get_customer(12345)
```

## 直写
<a name="Strategies.WriteThrough"></a>

直写策略会在将数据写入数据库时在缓存中添加或更新数据。

### 直写的优点和缺点
<a name="Strategies.WriteThrough.Evaluation"></a>

直写的优点如下：
+ 缓存中的数据永不过时。

  由于每次将缓存中的数据写入数据库时都会更新这些数据，因此缓存中的数据始终为最新数据。
+ 直写性能损失与读取性能损失比较。

  每次写入都涉及两次往返：

  1. 对缓存进行写入

  1. 对数据库进行写入

   这将增加流程的延迟。即便如此，与检索数据时的延迟相比，最终用户通常更能容忍更新数据时的延迟。有一个内在的意义，即更新的工作量更大，因而花费的时间会更长。

直写的缺点如下：
+ 缺失的数据。

  如果启动新节点（无论是由于节点故障还是横向扩展），都会出现数据缺失。此数据将持续保持丢失状态直到将其添加或更新到数据库。您可以通过实现[延迟加载](#Strategies.LazyLoading)和使用直写来最大限度减少此情况。
+ 缓存扰动。

  大多数数据从不会被读取，这是一种资源浪费。通过[添加存活时间 (TTL) 值](#Strategies.WithTTL)，可以最大程度地减少空间浪费。

### 直写伪代码示例
<a name="Strategies.WriteThrough.CodeExample"></a>

以下代码是直写逻辑的伪代码示例。

```
// *****************************************
// function that saves a customer's record.
// *****************************************
save_customer(customer_id, values)

    customer_record = db.query("UPDATE Customers WHERE id = {0}", customer_id, values)
    cache.set(customer_id, customer_record)
    return success
```

对于此示例，获取数据的应用程序代码如下。

```
save_customer(12345,{"address":"123 Main"})
```

## 添加 TTL
<a name="Strategies.WithTTL"></a>

延迟加载允许过时数据，但不会失败并产生空节点。直写可确保数据始终为最新数据，但直写可能会失败并产生空节点，还可能向缓存填充过多数据。您可对每次写入添加存活时间 (TTL) 值，充分利用每种策略的优势。同时，您可以并在很大程度上避免多余数据混淆缓存。

*存活时间（TTL）*是一个整数值，此值指定密钥过期之前的秒数。Valkey 或 Redis OSS 可以指定此值的秒数或毫秒数。Memcached 指定此值（以秒为单位）。当应用程序尝试读取过期密钥时，其处理方式是当做未找到该密钥。应用程序会在数据库中查询该密钥并更新缓存。这种方法不能保证值不会过时。不过，其可以防止数据过时太久，并要求不时从数据库中刷新缓存中的值。

有关更多信息，请参阅 [Valkey 和 Redis OSS 命令](https://valkey.io/commands)或 [Memcached `set` 命令](https://www.tutorialspoint.com/memcached/memcached_set_data.htm)。

### TTTL 伪代码示例
<a name="Strategies.WithTTL.CodeExample"></a>

以下代码为具有 TTL 的直写逻辑伪代码示例。

```
// *****************************************
// function that saves a customer's record.
// The TTL value of 300 means that the record expires
//    300 seconds (5 minutes) after the set command 
//    and future reads will have to query the database.
// *****************************************
save_customer(customer_id, values)

    customer_record = db.query("UPDATE Customers WHERE id = {0}", customer_id, values)
    cache.set(customer_id, customer_record, 300)

    return success
```

以下代码为具有 TTL 的延迟加载逻辑伪代码示例。

```
// *****************************************
// function that returns a customer's record.
// Attempts to retrieve the record from the cache.
// If it is retrieved, the record is returned to the application.
// If the record is not retrieved from the cache, it is 
//    retrieved from the database, 
//    added to the cache, and 
//    returned to the application.
// The TTL value of 300 means that the record expires
//    300 seconds (5 minutes) after the set command 
//    and subsequent reads will have to query the database.
// *****************************************
get_customer(customer_id)

    customer_record = cache.get(customer_id)
    
    if (customer_record != null)
        if (customer_record.TTL < 300)
            return customer_record        // return the record and exit function
            
    // do this only if the record did not exist in the cache OR
    //    the TTL was >= 300, i.e., the record in the cache had expired.
    customer_record = db.query("SELECT * FROM Customers WHERE id = {0}", customer_id)
    cache.set(customer_id, customer_record, 300)  // update the cache
    return customer_record                // return the newly retrieved record and exit function
```

对于此示例，获取数据的应用程序代码如下。

```
save_customer(12345,{"address":"123 Main"})
```

```
customer_record = get_customer(12345)
```

## 相关主题
<a name="Strategies.SeeAlso"></a>
+ [内存中的数据存储](elasticache-use-cases.md#elasticache-use-cases-data-store)
+ [选择引擎和版本](SelectEngine.md)
+ [缩放 ElastiCache](Scaling.md)