目录

一、云数据库 RDS

1、RDS 是什么

2、核心概念

3、企业级铁律

4、系列怎么选(先定架构)

5、内容了解

6、生产 vs 测试对照

7、常见坑

小结

二、Redis + RDS 缓存最小示例(Cache-Aside)

一、云数据库 RDS

目标:会按企业规范选库、连库、备份、做高可用,而不是只会“买一个 MySQL”。

官方参考:高可用与容灾 · 搭建高可用架构 · 读写分离


1、RDS 是什么

RDS(Relational Database Service) = 阿里云托管的关系型数据库。

常见引擎:MySQL、PostgreSQL、SQL Server、MariaDB 等。

和“自己在 ECS 上装 MySQL”比:

ECS 自建库RDS

要管什么

安装、备份、主备切换、补丁、监控…

主要管业务库表与账号

高可用

自己搭主从

产品级主备/多可用区

备份恢复

自己脚本

自动备份 + 控制台恢复

适合

极特殊定制

绝大多数企业业务库

总结:

ECS 负责算,OSS 负责存文件,RDS 负责存结构化业务数据(订单、用户、库存…)。


2、核心概念

概念描述

实例

一套数据库服务(有规格、存储、网络)

主节点

负责写(也可读)

备节点

热备,故障时可切换;高可用系列备库一般不给业务直连读

只读实例

专供读请求扩展的副本

数据库代理

前置代理,可做读写分离等

白名单

哪些 IP/网段能连这个库

连接地址

内网/外网域名+端口;生产优先内网


3、企业级铁律

  1. 生产不用基础版(单节点),用 高可用 / 集群 + 多可用区
  2. 禁止白名单 0.0.0.0/0;生产优先 VPC 内网访问
  3. 应用走内网地址;外网地址仅临时运维,且收紧白名单
  4. 开启自动备份 + 日志备份;定期做恢复演练
  5. 账号最小权限:应用账号别用高权限账号;禁共用 root
  6. 可维护时间窗口避开业务高峰
  7. 读多写少 上只读实例 + 数据库代理,别死扛主库
  8. 敏感数据 考虑 SSL、TDE(透明加密)、SQL 审计

4、系列怎么选(先定架构)

以 RDS MySQL 为例(其他引擎类似思路):

系列特点企业用法

基础版

单节点

仅开发/测试

高可用版

一主一备,自动切换

大多数生产默认

集群版

一主多备、备可读、更灵活扩展

更高可用/读扩展诉求

部署建议:

  • 多可用区:主备跨 AZ,同城容灾,通常不额外加“容灾费”
  • 单可用区:机器级冗余有,但扛不住整个可用区故障

官方建议:生产选多可用区高可用/集群。见 搭建高可用架构


5、内容了解

(1)创建一台“企业规范”的 RDS(示例)

承接之前的 VPC:

建议

引擎

MySQL 8.0(或公司标准版本)

系列

高可用

部署

多可用区

网络

与 ECS 同一 VPC

交换机

放在数据库交换机(如 10.10.11.0/24 / 10.10.12.0/24

规格

按业务起步,可监控后再升配

存储

SSD/ESSD,预留增长空间

白名单

只加应用交换机网段或 ECS 私网 IP

账号

业务库专用账号,最小权限

备份

自动备份开启,保留按合规(如 7~30 天+)

维护窗口

业务低峰(如凌晨)

创建后立刻检查:

  1. 是否只有内网地址在用
  2. 白名单有没有 0.0.0.0/0
  3. 备份策略是否生效
  4. ECS 能否内网连通,公网是否应关闭

(2)应用怎么连库(正确姿势)

ALB → ECS/ACK(应用)──内网──► RDS

                                                                   ↑

                                                        白名单放行应用网段

连接串注意:

  • 用 RDS 内网地址
  • 连接池合理(别每个请求新建连接)
  • 密码放配置中心/KMS,别写进镜像
  • 生产库 不要 给开发笔记本长期开公网

ECS 安全组与 RDS 白名单要同时满足:只放行业务端口(如 3306),来源是应用网段。


(3)高可用的理解

高可用系列大致是:

主节点(可读写) ←实时同步→ 备节点(热备)

                  │

             主挂了 → 自动切换到备(秒级,对应用尽量透明)

要知道:

  • 切换时可能有短暂闪断,应用应有重连
  • 多可用区比单可用区更能抗机房级故障
  • 切换后连接地址通常不变(以产品说明为准),但要在应用层做好重试

(4)读写分离(读扛不住时)

场景:查询很多、写入相对少,主库 CPU/连接被 SELECT 打满。

做法:

  1. 创建 只读实例(可多个,建议跨 AZ)
  2. 开通 数据库代理
  3. 应用连接 代理地址(不是直连主实例地址)
  4. 代理:写 → 主;读 → 只读(按权重)

应用 ──► 数据库代理

                        ├─ INSERT/UPDATE        → 主实例

                        └─ SELECT        → 只读1 / 只读2

注意:

  • 直连主地址 = 不会自动读写分离
  • 只读有同步延迟,强一致读要走主库或强制主库 hint
  • 建议至少 2 个只读,避免单只读故障影响面过大

官方:读写分离实践


(5)备份与恢复

能力作用

数据备份

库的定期全量/增量备份

日志备份

binlog 等,支撑更细时间点恢复

恢复到新实例 / 时间点恢复

误删、误更新后的救命手段

跨地域备份

地域级灾难备份(更高要求)

企业要求:

  • 自动备份必须开
  • 保留周期满足合规
  • 每季度至少做一次恢复演练(备份不能“假备份”)
  • 大变更前可手动备份

(6)安全与审计

建议

白名单

最小网段;禁用 0.0.0.0/0

SSL

外网或高安全场景开启

TDE

静态数据加密(敏感行业)

SQL 审计 / SQL 洞察

追溯谁执行了什么 SQL

账号治理

一人一账号;应用账号分离;定期审权限

RAM

谁能在控制台删库、改白名单,要严控

和堡垒机/运维:人连库尽量经可控通道,避免人人直连生产。


(7)监控与容量

至少盯这些:

  • CPU、内存、磁盘、IOPS
  • 连接数、慢 SQL
  • 主从延迟(有只读时)
  • 空间水位(提前扩容)

空间到 80%~85% 就要告警;别等写满业务停摆。
慢 SQL 要用索引/改写治理,而不是只靠无限升配。


(8)和前后知识串起来

  • RAM:谁能创建/删除 RDS、改白名单
  • VPC:RDS 放数据库交换机,与应用分网段
  • 安全组/白名单:只让应用访问 3306
  • ECS/ACK:应用内网连 RDS
  • ALB/ESS:无状态扩缩;会话/数据在 RDS/Redis/OSS
  • OSS:大文件、备份包;不要把大文件塞数据库 BLOB 滥用
  • 堡垒机:人访问跳板,不是把 RDS 暴露公网

典型企业链路:

用户 → ALB → ECS(可 ESS 伸缩)

                            ├─ RDS(交易数据)

                            ├─ Redis(缓存/会话,后续可学)

                            └─ OSS(文件)


6、生产 vs 测试对照

测试生产

系列

基础版可

高可用/集群 + 多 AZ

白名单

可临时放宽,用完收回

严格网段,禁全开放

备份

可短保留

按合规保留 + 演练

规格

按压测/监控定,留余量

公网

可临时开

默认关

审计

可选

建议开


7、常见坑

后果

生产用基础版

单点故障直接停业

白名单 0.0.0.0/0

暴露暴力破解与拖库风险

ECS 与 RDS 不同 VPC 却指望直连

不通或要额外打通

连主地址却以为开了读写分离

读仍打主库

忽略只读延迟

刚写入立刻读只读,读不到

从不做恢复演练

真出事才发现备份不能用

把 RDS 当文件存储

膨胀、备份变慢、性能差


小结

RDS 是托管关系数据库:生产选高可用多 AZ,内网访问、白名单收紧,备份可恢复,读多了上只读+代理,安全靠账号/SSL/审计,和应用放同一 VPC、分交换机部署。

二、Redis + RDS 缓存最小示例(Cache-Aside)

模式:先查 Redis → 没有再查 RDS → 回写 Redis(带过期);更新时 先改 RDS,再删缓存。


1、流程

读:GET key
  ├─ Redis 命中 → 直接返回
  └─ 未命中 → 查 RDS → SETEX 回写 → 返回

写:更新业务
  └─ UPDATE RDS → DEL key(下次读再重建缓存)

2、Python 最小示例

依赖:

pip install redis pymysql
import json
import redis
import pymysql
from pymysql.cursors import DictCursor

# ---------- 连接(改成你的内网地址) ----------
r = redis.Redis(
    host="r-xxxxxxxx.redis.rds.aliyuncs.com",
    port=6379,
    password="your_redis_password",
    decode_responses=True,
)

def get_db():
    return pymysql.connect(
        host="rm-xxxxxxxx.mysql.rds.aliyuncs.com",
        user="app_user",
        password="your_db_password",
        database="shop",
        charset="utf8mb4",
        cursorclass=DictCursor,
    )

TTL = 600          # 正常缓存 10 分钟
NULL_TTL = 60      # 空值缓存 1 分钟(防穿透)
NULL_FLAG = "__NULL__"

def cache_key(product_id: int) -> str:
    return f"product:prod:detail:{product_id}"

# ---------- 读:Redis → RDS ----------
def get_product(product_id: int) -> dict | None:
    key = cache_key(product_id)

    # 1) 先查缓存
    cached = r.get(key)
    if cached is not None:
        if cached == NULL_FLAG:
            return None
        return json.loads(cached)

    # 2) 未命中,查数据库
    conn = get_db()
    try:
        with conn.cursor() as cur:
            cur.execute(
                "SELECT id, name, price FROM product WHERE id=%s",
                (product_id,),
            )
            row = cur.fetchone()
    finally:
        conn.close()

    # 3) 回写缓存
    if row is None:
        r.setex(key, NULL_TTL, NULL_FLAG)  # 防穿透
        return None

    r.setex(key, TTL, json.dumps(row, ensure_ascii=False))
    return row

# ---------- 写:先 DB,再删缓存 ----------
def update_product_price(product_id: int, price: float) -> None:
    conn = get_db()
    try:
        with conn.cursor() as cur:
            cur.execute(
                "UPDATE product SET price=%s WHERE id=%s",
                (price, product_id),
            )
        conn.commit()
    finally:
        conn.close()

    # 删缓存,避免读到旧数据
    r.delete(cache_key(product_id))

# ---------- 试用 ----------
if __name__ == "__main__":
    print(get_product(10086))          # 第一次查 RDS,第二次走 Redis
    update_product_price(10086, 99.9)  # 改价并删缓存
    print(get_product(10086))          # 重新从 RDS 加载

3、Node.js 最小示例

npm i ioredis mysql2
const Redis = require("ioredis");
const mysql = require("mysql2/promise");

const redis = new Redis({
  host: "r-xxxxxxxx.redis.rds.aliyuncs.com",
  port: 6379,
  password: "your_redis_password",
});

const pool = mysql.createPool({
  host: "rm-xxxxxxxx.mysql.rds.aliyuncs.com",
  user: "app_user",
  password: "your_db_password",
  database: "shop",
});

const TTL = 600;
const NULL_TTL = 60;
const NULL_FLAG = "__NULL__";

const cacheKey = (id) => `product:prod:detail:${id}`;

async function getProduct(productId) {
  const key = cacheKey(productId);

  // 1) Redis
  const cached = await redis.get(key);
  if (cached !== null) {
    if (cached === NULL_FLAG) return null;
    return JSON.parse(cached);
  }

  // 2) RDS
  const [rows] = await pool.query(
    "SELECT id, name, price FROM product WHERE id = ?",
    [productId]
  );
  const row = rows[0] || null;

  // 3) 回写
  if (!row) {
    await redis.set(key, NULL_FLAG, "EX", NULL_TTL);
    return null;
  }
  await redis.set(key, JSON.stringify(row), "EX", TTL);
  return row;
}

async function updateProductPrice(productId, price) {
  await pool.query("UPDATE product SET price = ? WHERE id = ?", [
    price,
    productId,
  ]);
  await redis.del(cacheKey(productId));
}

(async () => {
  console.log(await getProduct(10086));
  await updateProductPrice(10086, 99.9);
  console.log(await getProduct(10086));
  process.exit(0);
})();

4、配套表结构(测试用)

CREATE TABLE product (
  id    BIGINT PRIMARY KEY,
  name  VARCHAR(64) NOT NULL,
  price DECIMAL(10,2) NOT NULL
);

INSERT INTO product (id, name, price) VALUES (10086, '示例商品', 88.00);

5、要点

说明

Key 带业务前缀

如 product:prod:detail:{id}

必须设 TTL

避免缓存永不过期占满内存

空值短 TTL

防缓存穿透

更新先 DB 再删缓存

简单稳妥;别只改 Redis

用内网地址

ECS 与 Redis/RDS 同 VPC

连接池

生产用池,别每次新建裸连

总结

读:Redis 优先,未命中查 RDS 再回写;写:改 RDS 后删除对应 Key。

更多推荐