使用 Memorystore 缓存数据

在执行某些任务时,高性能的可扩缩 Web 应用通常会先使用内存中的分布式数据缓存,当其不可用时才会使用可靠的永久性存储空间。我们建议您使用 Memorystore for Redis 作为缓存服务。请注意,Memorystore for Redis 不提供免费层级。如需了解详情,请参阅 Memorystore 价格

开始之前,请确保您的应用不会超出 Memorystore for Redis 配额

何时使用内存缓存

会话数据、用户偏好设置以及网页查询返回的其他数据都非常适合进行缓存。通常,如果频繁运行的查询所返回的结果集不需要立即显示在您的应用中,便可以缓存结果。如此,后续请求可以检查缓存,只有当结果缺失或过期时才会查询数据库。

如果您将某个值仅存储在 Memorystore 中,而没有将其备份到永久性存储空间,请确保在该值过期及从缓存中移除时,您的应用仍可正常运作。例如,如果用户会话数据的突然缺失导致会话出现故障,则应考虑除了在 Memorystore 中存储该数据之外,还应将其存储在数据库中。

了解 Memorystore 权限

与 Google Cloud 服务之间进行的每一次交互操作都需要经过授权。例如,如需与 Memorystore 托管的 Redis 数据库进行交互,您的应用需要提供有权访问 Memorystore 的账号的相应凭据。

默认情况下,您的应用会提供 App Engine 默认服务账号的凭据,该账号有权访问与您应用同属一个项目中的数据库。

如果满足以下任何条件,则您需要使用明确提供凭据的备用身份验证技术:

  • 您的应用和 Memorystore 数据库位于不同的 Google Cloud 项目中。

  • 您已更改分配给默认 App Engine 服务账号的角色。

如需了解备用身份验证方法,请参阅为服务器到服务器的生产应用设置身份验证

使用 Memorystore 概览

如需在您的应用中使用 Memorystore,请执行以下操作:

  1. 设置 Memorystore for Redis,此操作需要您在 Memorystore 上创建 Redis 实例并创建无服务器 VPC 访问通道,以供您的应用与 Redis 实例通信。

  2. 安装适用于 Redis 的客户端库并使用 Redis 命令缓存数据。

    Memorystore for Redis 与适用于 Redis 的任何客户端库都兼容。

    Go

    本指南介绍如何使用 redigo 客户端库从您的应用发送 Redis 命令。

    Java

    本指南介绍如何使用 Jedis 客户端库从您的应用发送 Redis 命令。如需详细了解如何使用 Jedis,请参阅 Jedis Wiki

    Node.js

    本指南介绍如何使用 node_redis 客户端库从您的应用发送 Redis 命令。

    PHP

    本指南介绍如何使用 PHPRedis 客户端库从您的应用发送 Redis 命令。

    Python

    本指南介绍如何使用 redis-py 3.0 客户端库从您的应用发送 Redis 命令。

    Ruby

    本指南介绍如何使用 redis-rb 客户端库从您的应用发送 Redis 命令。

  3. 测试您的更新

  4. 将应用部署到 App Engine

设置 Memorystore for Redis

如需设置 Memorystore for Redis,请执行以下操作:

  1. 在 Memorystore 中创建 Redis 实例

    当系统提示您为 Redis 实例选择地区时,请选择您的 App Engine 应用所在的地区

  2. 记下您创建的 Redis 实例的 IP 地址和端口号。您在自己的代码中创建 Redis 客户端时,将会用到这些信息。

  3. 将 App Engine 连接到 VPC 网络。您的应用只能通过 VPC 连接器与 Memorystore 通信。

    请务必按照将您的应用配置为使用连接器中所述,将 VPC 连接信息添加到 app.yaml 文件中。

安装依赖项

Go

如需使应用在 App Engine 中运行时能够使用 redigo 客户端库,请将该客户端库添加到应用的依赖项中。例如,如果您使用 go.mod 文件声明依赖项,请将下面一行内容添加到 go.mod 文件中:

module github.com/GoogleCloudPlatform/golang-samples/tree/master/memorystore/redis

详细了解如何为 Go 应用指定依赖项

Java

如需使应用在 App Engine 中运行时能够使用 Jedis 客户端库,请将该客户端库添加到应用的依赖项中。例如,如果您使用 Maven,请在 pom.xml 文件中添加以下依赖项:

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>5.1.0</version>
</dependency>

Node.js

如需使应用在 App Engine 中运行时能够使用 node_redis 客户端库,请将该客户端库添加到应用的 package.json 文件中。

例如:

{
  "name": "memorystore-redis",
  "description": "An example of using Memorystore(Redis) with Node.js",
  "version": "0.0.1",
  "private": true,
  "license": "Apache Version 2.0",
  "author": "Google Inc.",
  "engines": {
    "node": ">=16.0.0"
  },
  "dependencies": {
    "redis": "^4.0.0"
  }
}

详细了解如何为 Node.js 应用指定依赖项

PHP

如需使应用在 App Engine 中运行时能够使用 PHPRedis 客户端库,请将 redis.so 扩展程序添加到应用的 php.ini 文件中。例如:

; Enable the Redis extension on App Engine
extension=redis.so

如需详细了解如何在 App Engine 中启用 PHP 扩展程序,请参阅可动态加载的扩展程序

Python

如需使应用在 App Engine 中运行时能够使用 redis-py 客户端库,请将下面一行内容添加到应用的 requirements.txt 文件中:

  redis

当您部署应用时,App Engine Python 3 运行时会将所有库上传到应用的 requirements.txt 文件中。

对于本地开发,建议您在虚拟环境(例如 venv)中安装依赖项。

Ruby

如需使应用在 App Engine 中运行时能够使用 redis-rb 客户端库,请将该客户端库添加到应用的 Gemfile 文件中。

  source "https://cloud.google.com/memorystore"

  gem "redis-rb"

创建 Redis 客户端

如需与 Redis 数据库进行交互,您的代码需要创建 Redis 客户端才能管理与 Redis 数据库的连接。以下部分介绍如何使用 Redis 客户端库创建 Redis 客户端。

指定环境变量

Redis 客户端库使用两个环境变量为 Redis 数据库组建网址:

  • 用于标识您在 Memorystore 中创建的 Redis 数据库的 IP 地址的变量。
  • 用于标识您在 Memorystore 中创建的 Redis 数据库的端口号的变量。

我们建议您在应用的 app.yaml 文件中定义这些变量,而不是直接在您的代码中定义这些变量。这样可以更轻松地在不同的环境(例如本地环境和 App Engine)中运行您的应用。 如需详细了解环境变量,请参阅 app.yaml 参考页面

Go

例如,请将下面几行内容添加到 app.yaml 文件中:

  env_variables:
       REDISHOST: '10.112.12.112'
       REDISPORT: '6379'

Java

例如,请将下面几行内容添加到 app.yaml 文件中:

  env_variables:
       redis.host: '10.112.12.112'
       redis.port: '6379'

Node.js

例如,请将下面几行内容添加到 app.yaml 文件中:

  env_variables:
       REDISHOST: '10.112.12.112'
       REDISPORT: '6379'

PHP

例如,请将下面几行内容添加到 app.yaml 文件中:

  env_variables:
       REDIS_HOST: '10.112.12.112'
       REDIS_PORT: '6379'

Python

例如,请将下面几行内容添加到 app.yaml 文件中:

  env_variables:
       REDISHOST: '10.112.12.112'
       REDISPORT: '6379'

Ruby

例如,请将下面几行内容添加到 app.yaml 文件中:

  env_variables:
       REDISHOST: '10.112.12.112'
       REDISPORT: '6379'

导入 Redis 并创建客户端

Go

定义 REDISHOSTREDISPORT 环境变量后,请使用下面几行代码导入 redigo 库,创建连接池,然后从池中检索 Redis 客户端:


// Command redis is a basic app that connects to a managed Redis instance.
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/gomodule/redigo/redis"
)

var redisPool *redis.Pool

func incrementHandler(w http.ResponseWriter, r *http.Request) {
	conn := redisPool.Get()
	defer conn.Close()

	counter, err := redis.Int(conn.Do("INCR", "visits"))
	if err != nil {
		http.Error(w, "Error incrementing visitor counter", http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "Visitor number: %d", counter)
}

func main() {
	redisHost := os.Getenv("REDISHOST")
	redisPort := os.Getenv("REDISPORT")
	redisAddr := fmt.Sprintf("%s:%s", redisHost, redisPort)

	const maxConnections = 10
	redisPool = &redis.Pool{
		MaxIdle: maxConnections,
		Dial:    func() (redis.Conn, error) { return redis.Dial("tcp", redisAddr) },
	}

	http.HandleFunc("/", incrementHandler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	log.Printf("Listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

Java

使用 Jedis 库时,建议您创建一个 JedisPool,然后使用该池创建客户端。以下代码行使用您之前定义的 redis.hostredis.port 环境变量来创建池:


package com.example.redis;

import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@WebListener
public class AppServletContextListener implements ServletContextListener {

  private Properties config = new Properties();

  private JedisPool createJedisPool() throws IOException {
    String host;
    Integer port;
    config.load(
        Thread.currentThread()
            .getContextClassLoader()
            .getResourceAsStream("application.properties"));
    host = config.getProperty("redis.host");
    port = Integer.valueOf(config.getProperty("redis.port", "6379"));

    JedisPoolConfig poolConfig = new JedisPoolConfig();
    // Default : 8, consider how many concurrent connections into Redis you will need under load
    poolConfig.setMaxTotal(128);

    return new JedisPool(poolConfig, host, port);
  }

  @Override
  public void contextDestroyed(ServletContextEvent event) {
    JedisPool jedisPool = (JedisPool) event.getServletContext().getAttribute("jedisPool");
    if (jedisPool != null) {
      jedisPool.destroy();
      event.getServletContext().setAttribute("jedisPool", null);
    }
  }

  // Run this before web application is started
  @Override
  public void contextInitialized(ServletContextEvent event) {
    JedisPool jedisPool = (JedisPool) event.getServletContext().getAttribute("jedisPool");
    if (jedisPool == null) {
      try {
        jedisPool = createJedisPool();
        event.getServletContext().setAttribute("jedisPool", jedisPool);
      } catch (IOException e) {
        // handle exception
      }
    }
  }
}

要从池中创建客户端,请使用 JedisPool.getResource() 方法。 例如:


package com.example.redis;

import java.io.IOException;
import java.net.SocketException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@WebServlet(name = "Track visits", value = "")
public class VisitCounterServlet extends HttpServlet {

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    try {
      JedisPool jedisPool = (JedisPool) req.getServletContext().getAttribute("jedisPool");

      if (jedisPool == null) {
        throw new SocketException("Error connecting to Jedis pool");
      }
      Long visits;

      try (Jedis jedis = jedisPool.getResource()) {
        visits = jedis.incr("visits");
      }

      resp.setStatus(HttpServletResponse.SC_OK);
      resp.getWriter().println("Visitor counter: " + String.valueOf(visits));
    } catch (Exception e) {
      resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
    }
  }
}

Node.js

定义了 REDISHOSTREDISPORT 环境变量后,您可以使用下面几行代码导入 node_redis 库并创建 Redis 客户端:

'use strict';
const http = require('http');
const redis = require('redis');

const REDISHOST = process.env.REDISHOST || 'localhost';
const REDISPORT = process.env.REDISPORT || 6379;

const client = redis.createClient(REDISPORT, REDISHOST);
client.on('error', err => console.error('ERR:REDIS:', err));

// create a server
http
  .createServer((req, res) => {
    // increment the visit counter
    client.incr('visits', (err, reply) => {
      if (err) {
        console.log(err);
        res.status(500).send(err.message);
        return;
      }
      res.writeHead(200, {'Content-Type': 'text/plain'});
      res.end(`Visitor number: ${reply}\n`);
    });
  })
  .listen(8080);

PHP

定义了 REDIS_HOSTREDIS_PORT 环境变量后,您可以使用下面几行代码创建 Redis 客户端:

<?php
/**
 * Copyright 2019 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Only serve traffic from "/"
switch (@parse_url($_SERVER['REQUEST_URI'])['path']) {
    case '/':
        break;
    default:
        http_response_code(404);
        exit('Not Found');
}

// Connect to Memorystore from App Engine.
if (!$host = getenv('REDIS_HOST')) {
    throw new Exception('The REDIS_HOST environment variable is required');
}

# Memorystore Redis port defaults to 6379
$port = getenv('REDIS_PORT') ?: '6379';

try {
    $redis = new Redis();
    $redis->connect($host, $port);
} catch (Exception $e) {
    return print('Error: ' . $e->getMessage());
}

$value = $redis->incr('counter');

printf('Visitor number: %s', $value);

Python

定义 REDISHOSTREDISPORT 环境变量后,请使用下面几行代码导入 redis-py 库并创建客户端:

  import redis

  redis_host = os.environ.get('REDISHOST', 'localhost')
  redis_port = int(os.environ.get('REDISPORT', 6379))
  redis_client = redis.Redis(host=redis_host, port=redis_port)

如果您对其他应用使用了旧版 redis-py,则可能使用过 StrictClient 类,而不是 Client。但是,redis-py 现在建议使用 Client,而不是 StrictClient

Ruby

没有关于此运行时的更多信息。

使用 Redis 命令在缓存中存储和检索数据

虽然 Memorystore Redis 数据库支持大多数 Redis 命令,但您只需使用几个命令即可在缓存中存储和检索数据。下表建议了一些可用于缓存数据的 Redis 命令。如需了解如何从您的应用调用这些命令,请参阅客户端库的文档。

任务 Redis 命令
在数据缓存中创建一个条目,
并为该条目设置到期时间
SETNX
MSETNX
从缓存中检索数据 GET
MGET
替换现有的缓存值 SET
MSET
递增或递减缓存数值 INCR
INCRBY
DECR
DECRBY
从缓存中删除条目 DEL
UNLINK
支持与缓存进行并发交互 请参阅有关 Redis 事务的详细信息。

对于 Python,redis-py 客户端库要求所有事务都在流水线中进行。

测试更新

当您在本地测试应用时,请考虑运行 Redis 的本地实例以避免与生产数据交互(Memorystore 不提供模拟器)。如需在本地安装并运行 Redis,请按照 Redis 文档中的说明进行操作。请注意,目前无法在 Windows 上本地运行 Redis。

如需详细了解如何测试应用,请参阅测试和部署应用

部署应用

一旦您的应用能够在本地开发服务器上无错误地正常运行,请执行以下操作:

  1. 在 App Engine 上测试应用

  2. 如果应用正常运行,未发生错误,请使用流量分配功能为更新后的应用缓慢增加流量。请先仔细监控应用是否存在任何数据库问题,然后再将更多流量路由到更新后的应用。