Python + PostgreSQL + Elasticsearch + Redis 多级缓存架构学习文档

一份从零开始的实战指南,涵盖环境搭建、架构设计、代码实现与数据可视化


📖 目录


概述

本教程带你构建一个 Python + PostgreSQL + Elasticsearch + Redis 的多级缓存演示项目。你将学到:

  • Windows 上用 Docker Desktop 搭建完整的数据库 / 缓存 / 搜索引擎环境
  • 使用 uv 管理 Python 虚拟环境和依赖
  • 理解为什么互联网后端需要 "三级缓存"(DB + ES + Redis)
  • 亲手写一个 ProductService 类,体验 Cache-Aside(旁路缓存) 模式的完整流程
  • 通过多种工具(命令行、浏览器、PyCharm 插件)查看各层数据

Windows 环境部署

1.1 Docker Desktop 安装与启动

  1. 访问 Docker Desktop 官网 下载 Windows 安装包。
  2. 双击安装,建议勾选 "Use WSL 2 instead of Hyper-V"(性能更好)。
  3. 安装完成后,重启电脑
  4. 启动 Docker Desktop,任务栏右下角出现鲸鱼图标即表示运行成功。

💡 验证:打开 PowerShell 或 Git Bash,执行 docker --version,看到版本号即安装成功。

1.2 使用 Docker Compose 一键启动服务

在项目根目录(如 D:\Project\...\database_learn)创建 docker-compose.yml

version: '3.8'

services:
  postgres:
    image: postgres:15
    container_name: my_postgres
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7
    container_name: my_redis
    ports:
      - "6379:6379"

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    container_name: my_es
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false   # 本地测试关闭安全认证
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
      - "9300:9300"

volumes:
  pgdata:

启动命令(在 Git Bash 中执行):

docker-compose up -d

验证服务是否就绪:

# 查看容器状态
docker ps

# 检查 PostgreSQL
docker exec -it my_postgres pg_isready -U myuser

# 检查 Elasticsearch(浏览器访问)
# 打开 http://localhost:9200,看到 JSON 响应即正常

# 检查 Redis
docker exec -it my_redis redis-cli ping   # 应返回 PONG

🧹 清理:测试完成后,执行 docker-compose down -v 停止并删除容器及数据卷。

1.3 Python 虚拟环境配置(uv)

uv 是一个极速的 Python 包和项目管理工具,用 Rust 编写,速度比 pip 快 10~100 倍。

① 安装 uv

PowerShell(管理员模式) 中执行:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

安装完成后,关闭并重新打开 Git Bash 或 PowerShell,验证:

uv --version

② 初始化项目

在项目根目录执行:

# 初始化 pyproject.toml
uv init

# 创建虚拟环境(默认在 .venv 目录)
uv venv

# 安装依赖
uv add elasticsearch redis psycopg2-binary

③ 激活虚拟环境(Git Bash)

source .venv/Scripts/activate

激活后,命令行前出现 (.venv) 字样。之后你可以直接使用 pythonpip

💡 退出环境:执行 deactivate

1.4 PyCharm 解释器配置

为了让 PyCharm 识别你的 uv 虚拟环境,需要手动配置解释器:

  1. 打开 File → Settings → Project: ... → Python Interpreter
  2. 点击齿轮图标 ⚙️ → Add... → 选择 Existing environment
  3. Interpreter 字段,浏览并选择:

D:\Project\...\database_learn\.venv\Scripts\python.exe

  1. 勾选 Make available to all projects(可选),点击 OK
  2. 等待 PyCharm 索引完成,右下角状态栏应显示 .venv 字样。

验证配置是否成功:在 PyCharm 的终端(Terminal)中执行:

where python

如果输出路径包含 .venv\Scripts\python.exe,说明配置正确。


数据接入与可视化

2.1 PostgreSQL 接入

方式一:Git Bash 命令行(psql)

docker exec -it my_postgres psql -U myuser -d mydb

进入后执行:

-- 查看所有表
\dt

-- 查看 products 表结构(字段、类型、约束)
\d products

-- 查看表中的所有数据
SELECT * FROM products;

退出:输入 \q 回车。

方式二:PyCharm 图形化(Database Navigator 插件)

  1. 安装插件:File → Settings → Plugins,搜索 Database Navigator 并安装,重启 PyCharm。
  2. 打开 DB Browser:左侧边栏点击 DB Browser 标签(或 View → Tool Windows → DB Browser)。
  3. 添加数据源:点击 + → PostgreSQL,填写:
字段
Host localhost
Port 5432
Database mydb
User myuser
Password mypassword
  1. 点击 Test Connection,成功后点击 OK
  2. 查看数据:展开 Tables → 右键 products → View DataEdit Data

⚠️ 首次连接可能提示下载 JDBC 驱动,点击 Download 即可自动完成。

2.2 Elasticsearch 接入

Elasticsearch 通过 RESTful API 交互,因此有多种查看方式。

方式一:浏览器直接访问(最简单)

在浏览器地址栏输入:

http://localhost:9200/products/_search?pretty

返回的 JSON 中,hits.hits 数组里就是所有商品数据。

方式二:Git Bash 命令行(curl)

# 查看所有索引
curl -X GET "http://localhost:9200/_cat/indices?v"

# 查看 products 索引的映射结构(相当于表结构)
curl -X GET "http://localhost:9200/products/_mapping?pretty"

# 查询所有数据(默认10条)
curl -X GET "http://localhost:9200/products/_search?pretty"

# 查询更多数据(size=100)
curl -X GET "http://localhost:9200/products/_search?pretty&size=100"

方式三:图形化工具(GUI)

  • Elasticvue:浏览器插件或桌面应用,官网下载
  • Kibana:Elastic 官方可视化工具(功能最强大,但需单独下载)

💡 对于快速查看数据,浏览器方式最便捷;如果需要频繁操作,推荐安装 Elasticvue。

2.3 Redis 接入

Redis 是键值对存储,你的缓存键通常是 product:p999

方式一:Git Bash 命令行(redis-cli)

docker exec -it my_redis redis-cli

进入后执行:

# 查看所有键
KEYS *

# 查看 product:p999 的值(JSON 字符串)
GET product:p999

# 查看键的剩余生存时间(TTL,单位秒)
TTL product:p999

# 退出
exit

方式二:图形化工具(GUI)

Another Redis Desktop Manager(免费开源,推荐)

连接参数:

字段
Host localhost
Port 6379
Auth 无(默认空密码)

连接后即可在界面中浏览所有 Key 及其值。

方式三:PyCharm Python 控制台

在 PyCharm 中打开 Python 控制台,输入:

import redis
r = redis.Redis.from_url("redis://localhost:6379", decode_responses=True)
r.get("product:p999")

会直接打印出缓存的商品数据。


多级缓存架构设计

3.1 三级缓存架构图

下面用 Mermaid 流程图展示三级缓存的整体架构:

flowchart TD
    Client[客户端请求] --> App["ProductService
Python 应用层"] subgraph ReadFlow["读取流程(Cache-Aside 模式)"] App -->|1. GET product:id| Redis Redis -->|✅ 命中 → 返回 JSON| App Redis -->|❌ 未命中| App App -->|2. GET products/_doc| ES ES -->|✅ 命中 → 回写 Redis| App ES -->|❌ 未命中| App App -->|3. SELECT FROM products| PG PG -->|补偿写入 ES + 回写 Redis| App end subgraph WriteFlow["写入流程(Cache Invalidation 策略)"] App -->|1. UPSERT| PG App -->|2. INDEX| ES App -->|3. DELETE 缓存| Redis end Redis[(Redis
缓存层)] ES[(Elasticsearch
搜索引擎层)] PG[(PostgreSQL
Source of Truth)]

3.2 各层职责与选型理由

层级 核心职责 优势(为什么用它) 劣势(为什么不只用它)
PostgreSQL
(数据源头)
事务与持久化
(Source of Truth)
ACID 事务、数据绝对安全、支持复杂关联查询、强一致性 全文搜索慢(LIKE '%xx%' 无法走索引)、高并发读压力大
Elasticsearch
(搜索引擎)
全文搜索与聚合分析
(Search Engine)
倒排索引,毫秒级响应模糊搜索;支持海量数据聚合(Group By) 非事务性(最终一致性)、写入成本高(索引刷新消耗 IO)、不支持 JOIN
Redis
(缓存层)
热数据加速 纯内存操作,读写速度极快(微秒级),极大减轻 DB 和 ES 压力 容量有限(内存贵)、不支持复杂查询(只能根据 Key 查 Value)

如果只用两层,会发生什么?

组合 问题
DB + Redis(去掉 ES) 无法实现商品名称模糊搜索,PostgreSQL 的 LIKE '%python%' 会导致全表扫描,百万级数据直接超时
DB + ES(去掉 Redis) 每次查询都访问 ES,虽然比 DB 快,但扛不住热点商品的千万级 QPS,响应时间从 0.1ms 飙升到 10ms+
ES + Redis(去掉 DB) ❌ 不可行!ES 不是 ACID 数据库,无法保证数据不丢失,不支持事务

3.3 数据流转流程

读取流程(Cache-Aside 模式)

通过时序图可以清晰看到三种不同的读取路径:

sequenceDiagram
    participant C as 客户端
    participant S as ProductService
    participant R as Redis
    participant E as Elasticsearch
    participant P as PostgreSQL

    rect rgb(230, 255, 230)
        Note right of C: 场景一:缓存命中(~1ms,最优路径)
        C->>+S: get_product("p999")
        S->>+R: GET product:p999
        R-->>-S: ✅ JSON 数据
        S-->>-C: 返回结果
    end

    rect rgb(255, 245, 230)
        Note right of C: 场景二:缓存未命中 → ES命中(~10ms)
        C->>+S: get_product("p998")
        S->>+R: GET product:p998
        R-->>-S: ❌ nil
        S->>+E: GET products/_doc/p998
        E-->>-S: ✅ 数据
        S->>R: SETEX product:p998 30s
        S-->>-C: 返回结果
    end

    rect rgb(255, 230, 230)
        Note right of C: 场景三:全未命中 → DB 补偿(~50ms,最慢路径)
        C->>+S: get_product("p997")
        S->>+R: GET product:p997
        R-->>-S: ❌ nil
        S->>+E: GET products/_doc/p997
        E-->>-S: ❌ 404
        S->>+P: SELECT * WHERE id='p997'
        P-->>-S: ✅ 数据
        S->>E: INDEX 补偿写入 ES
        S->>R: SETEX product:p997 30s
        S-->>-C: 返回结果
    end

写入流程(Cache Invalidation)

sequenceDiagram
    participant C as 客户端
    participant S as ProductService
    participant P as PostgreSQL
    participant E as Elasticsearch
    participant R as Redis

    C->>+S: update_product("p999", "Python实战", 79.90)
    S->>+P: UPSERT products SET ...
    P-->>-S: ✅ 持久化成功
    Note over S: ACID 事务保证数据不丢
    S->>+E: INDEX products/_doc/p999
    E-->>-S: ✅ 索引建立
    Note over S: 倒排索引就绪,支持模糊搜索
    S->>+R: DELETE product:p999
    R-->>-S: ✅ 缓存失效
    Note over S: 旧数据清除
下次读请求自动拉取最新数据 S-->>-C: 更新完成

代码实战讲解

4.1 项目目录结构

database_learn/
├── docker-compose.yml              # Docker 编排文件
├── pyproject.toml                  # 项目依赖清单(uv 管理)
├── uv.lock                         # 锁定文件
├── README.md
├── src/
│   └── database_learn/             # 核心业务包
│       ├── __init__.py
│       ├── config.py               # 连接配置
│       └── service.py              # ProductService 业务类
├── scripts/
│   └── demo.py                     # 完整流程演示脚本
└── docs/
    └── learning-wiki.md            # 本文档

4.2 核心代码解析

src/database_learn/config.py —— 连接配置

PG_DSN = "dbname=mydb user=myuser password=mypassword host=localhost port=5432"
REDIS_URL = "redis://localhost:6379"
ES_HOST = "http://localhost:9200"

💡 注意:生产环境应将配置放入环境变量或 .env 文件,不应硬编码。

src/database_learn/service.py —— 核心业务服务

import json
import psycopg2
import redis
from elasticsearch import Elasticsearch, NotFoundError
from .config import PG_DSN, REDIS_URL, ES_HOST

# 全局连接(演示简化,生产应使用连接池)
pg_conn = psycopg2.connect(PG_DSN)
pg_conn.autocommit = True
redis_client = redis.Redis.from_url(REDIS_URL, decode_responses=True)
es_client = Elasticsearch(ES_HOST)


def init_db():
    """初始化 PostgreSQL 表"""
    with pg_conn.cursor() as cur:
        cur.execute("""
            CREATE TABLE IF NOT EXISTS products (
                id VARCHAR(50) PRIMARY KEY,
                name TEXT NOT NULL,
                price DECIMAL(10,2),
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
    print("✅ PostgreSQL 表已准备")


def init_es():
    """初始化 Elasticsearch 索引和映射"""
    if not es_client.indices.exists(index="products"):
        es_client.indices.create(index="products", body={
            "mappings": {
                "properties": {
                    "id": {"type": "keyword"},
                    "name": {"type": "text"},
                    "price": {"type": "float"}
                }
            }
        })
        print("✅ Elasticsearch 索引已创建")
    else:
        print("ℹ️  ES 索引已存在")


class ProductService:
    def __init__(self, cache_ttl=30):
        self.cache_ttl = cache_ttl

    # ---------- 写操作 ----------

    def update_product(self, product_id: str, name: str, price: float):
        """写入/更新:DB → ES → 删除缓存(Cache Invalidation)"""
        # 1. 写 PostgreSQL(UPSERT)
        with pg_conn.cursor() as cur:
            cur.execute(
                "INSERT INTO products (id, name, price) VALUES (%s, %s, %s) "
                "ON CONFLICT (id) DO UPDATE SET "
                "  name = EXCLUDED.name, "
                "  price = EXCLUDED.price, "
                "  updated_at = CURRENT_TIMESTAMP",
                (product_id, name, price)
            )
        print(f"📝 DB 已更新: {product_id}")

        # 2. 写 Elasticsearch
        es_client.index(index="products", id=product_id, body={
            "id": product_id,
            "name": name,
            "price": price
        })
        print(f"📄 ES 已索引: {product_id}")

        # 3. 删除 Redis 缓存(使旧数据失效)
        cache_key = f"product:{product_id}"
        redis_client.delete(cache_key)
        print(f"🗑️  缓存已删除: {cache_key}")

    # ---------- 读操作 ----------

    def get_product(self, product_id: str):
        """读取:缓存 → ES → DB(Cache-Aside 三级回源)"""
        cache_key = f"product:{product_id}"

        # --- 第1层:查 Redis 缓存 ---
        cached = redis_client.get(cache_key)
        if cached:
            print(f"✅ 缓存命中: {cache_key}")
            return json.loads(cached)

        # --- 第2层:查 Elasticsearch ---
        try:
            res = es_client.get(index="products", id=product_id)
            data = res["_source"]
            print(f"🔍 ES 查询成功: {product_id}")
        except NotFoundError:
            # --- 第3层:查 PostgreSQL(DB 兜底) ---
            print(f"⚠️  ES 未命中,回查 PostgreSQL: {product_id}")
            with pg_conn.cursor() as cur:
                cur.execute(
                    "SELECT id, name, price FROM products WHERE id = %s",
                    (product_id,)
                )
                row = cur.fetchone()
                if not row:
                    print(f"❌ 商品 {product_id} 不存在(任何层级均无数据)")
                    return None
                data = {"id": row[0], "name": row[1], "price": float(row[2])}

            # 补偿写入 ES(保证 ES 数据最终一致)
            es_client.index(index="products", id=product_id, body=data)
            print(f"📄 从 DB 补偿写入 ES: {product_id}")

        # --- 回写 Redis 缓存(设置 TTL) ---
        redis_client.setex(cache_key, self.cache_ttl, json.dumps(data))
        print(f"💾 缓存已写入: {cache_key} (TTL={self.cache_ttl}s)")
        return data

🔑 关键点解读:

  • 更新时采用"失效"(Invalidate)策略,而非更新缓存——避免并发写脏数据
  • 读取时采用 Cache-Aside 模式:先查缓存,未命中再查 ES/DB,并回写缓存
  • DB 兜底 + 补偿写 ES:即使 ES 因同步延迟没有数据,也能从 DB 拉取并补偿写回 ES,保证最终一致性

scripts/demo.py —— 运行演示

import json
from database_learn.service import ProductService, init_db, init_es, pg_conn, redis_client, es_client


def main():
    print("🚀 初始化数据库和索引...")
    init_db()
    init_es()

    service = ProductService()
    test_id = "p999"

    print("\n--- 1. 写入商品 ---")
    service.update_product(test_id, "Python 从入门到实践", 59.90)

    print("\n--- 2. 第一次读取(缓存未命中,走 ES) ---")
    data = service.get_product(test_id)
    print(f"   结果: {data}")

    print("\n--- 3. 第二次读取(应该命中缓存) ---")
    data = service.get_product(test_id)
    print(f"   结果: {data}")

    print("\n--- 4. 更新商品价格(触发缓存删除) ---")
    service.update_product(test_id, "Python 从入门到实践", 79.90)

    print("\n--- 5. 更新后再次读取(缓存失效,走 ES 拿新数据) ---")
    data = service.get_product(test_id)
    print(f"   结果: {data}")

    # 验证数据库
    print("\n--- 6. 验证数据库中的最终数据 ---")
    with pg_conn.cursor() as cur:
        cur.execute("SELECT id, name, price FROM products WHERE id = %s", (test_id,))
        row = cur.fetchone()
        print(f"   PostgreSQL: id={row[0]}, name={row[1]}, price={row[2]}")

    print("\n--- 7. 验证 Redis 缓存状态 ---")
    cached = redis_client.get(f"product:{test_id}")
    if cached:
        print(f"   Redis 缓存存在: {json.loads(cached)}")
    else:
        print("   Redis 缓存已过期或不存在")

    print("\n--- 8. 验证 Elasticsearch 数据 ---")
    res = es_client.get(index="products", id=test_id)
    print(f"   Elasticsearch: {res['_source']}")

    print("\n🎉 全部验证通过!")


if __name__ == "__main__":
    main()

4.3 运行与验证

# 确保 Docker 容器正在运行
docker ps

# 激活虚拟环境(如果尚未激活)
source .venv/Scripts/activate

# 运行演示脚本
python scripts/demo.py

预期输出:

🚀 初始化数据库和索引...
✅ PostgreSQL 表已准备
ℹ️  ES 索引已存在

--- 1. 写入商品 ---
📝 DB 已更新: p999
📄 ES 已索引: p999
🗑️  缓存已删除: product:p999

--- 2. 第一次读取(缓存未命中,走 ES) ---
🔍 ES 查询成功: p999
💾 缓存已写入: product:p999 (TTL=30s)
   结果: {'id': 'p999', 'name': 'Python 从入门到实践', 'price': 59.9}

--- 3. 第二次读取(应该命中缓存) ---
✅ 缓存命中: product:p999
   结果: {'id': 'p999', 'name': 'Python 从入门到实践', 'price': 59.9}

--- 4. 更新商品价格(触发缓存删除) ---
📝 DB 已更新: p999
📄 ES 已索引: p999
🗑️  缓存已删除: product:p999

--- 5. 更新后再次读取(缓存失效,走 ES 拿新数据) ---
🔍 ES 查询成功: p999
💾 缓存已写入: product:p999 (TTL=30s)
   结果: {'id': 'p999', 'name': 'Python 从入门到实践', 'price': 79.9}

--- 6. 验证数据库中的最终数据 ---
   PostgreSQL: id=p999, name=Python 从入门到实践, price=79.90

--- 7. 验证 Redis 缓存状态 ---
   Redis 缓存存在: {'id': 'p999', 'name': 'Python 从入门到实践', 'price': 79.9}

--- 8. 验证 Elasticsearch 数据 ---
   Elasticsearch: {'id': 'p999', 'name': 'Python 从入门到实践', 'price': 79.9}

🎉 全部验证通过!

多级缓存一致性与风险分析

本节讨论生产环境中多级缓存架构可能遇到的实际问题。理解这些风险,才能设计出更健壮的系统。

5.1 写入失败的半失败场景

写入流程是 DB → ES → Redis 三步。如果某一步失败,怎么办?

flowchart LR
    subgraph Normal["正常流程"]
        A[写 DB] --> B[写 ES] --> C[删 Redis]
    end

    subgraph Partial["部分失败场景"]
        A1[写 DB ✅] -->|成功| B1[写 ES ❌]
        B1 -->|ES 异常| C1[问题:DB有数据,ES搜不到]
        A2[写 DB ✅] -->|成功| B2[写 ES ✅]
        B2 -->|成功| C2[删 Redis ❌]
        C2 -->|Redis 超时| D2[问题:Redis 残留旧数据]
    end

应对策略:

失败场景 影响 解决方案
ES 写入失败 模糊搜索暂时查不到该商品 ① 重试机制(队列 + 定时任务补偿)② 读取时 DB 兜底自动补偿写 ES
Redis 删除失败 旧缓存残留,读取到脏数据 ① 设短 TTL(如30s),让缓存自动过期 ② 重试删除
DB 写入失败 整个操作应回滚 前置事务,DB 失败时 ES 不写(或在业务层做回滚)

当前代码的应对: 读取路径中的 DB 兜底 + 补偿写 ES(第 4.2 节)已经天然处理了"ES 写失败"的场景。

5.2 并发读写竞态条件

Cache-Aside 模式在高并发下有一个经典的竞态问题:

sequenceDiagram
    participant A as 请求A(写)
    participant B as 请求B(读)
    participant R as Redis
    participant E as Elasticsearch
    participant P as PostgreSQL

    Note over A,P: 时间轴 ↓ 并发场景
    A->>P: ① 写 DB(新数据: 100元)
    A->>E: ② 写 ES
    A->>R: ③ 删缓存
    Note over R: 此时缓存已删除

    B->>R: ④ 读缓存 → nil
    B->>P: ⑤ 读 DB → 新数据(100元)✅
    Note over B: 场景A:B读到新数据,正确

但另一种交错可能出问题:

sequenceDiagram
    participant A as 请求A(写)
    participant B as 请求B(读)
    participant R as Redis
    participant E as Elasticsearch
    participant P as PostgreSQL

    A->>P: ① 写 DB(新: 100元)
    A->>E: ② 写 ES(新: 100元)
    A->>R: ③ 删缓存

    B->>R: ④ 读缓存 → nil
    B->>E: ⑤ 读 ES → 旧: 50元 ❌
    Note over B: 如果ES主从同步延迟
可能读到旧数据 B->>R: ⑥ 回写缓存 → 旧: 50元 ❌ Note over R: 缓存被旧数据污染!

解决办法:
- 设短 TTL(如 30~60s),即使误写了旧数据,也能快速自动修正
- 延迟双删(Delayed Double Delete):写操作先删缓存,等几百毫秒再删一次,兜住并发读回写的旧数据
- 如果要求强一致性,读请求走 DB 主库,不用缓存(牺牲性能换一致性)

5.3 缓存雪崩 / 穿透 / 击穿

问题 现象 原因 对策
缓存穿透 请求打到 DB 查一个根本不存在的 key,缓存和 DB 都没有 ① 布隆过滤器 ② 缓存空值(None)并设短 TTL
缓存击穿 单 key 瞬间压垮 DB 热点 key 刚好过期,大量并发请求同时回源 ① 互斥锁(只让一个请求回源)② 热点 key 不过期
缓存雪崩 大量 key 同时过期压垮 DB 同一时间大量 key TTL 到期,或 Redis 宕机 ① TTL 加随机偏移 ② Redis 高可用(主从/哨兵)③ 本地缓存兜底

常见问题与避坑指南

Q1:为什么没加本地内存缓存(如 Python dict)?

A:本地内存是"单机缓存",在分布式部署(多个 Python 实例)下会导致数据不一致(实例 A 更新了缓存,实例 B 还是旧数据)。生产环境如需本地缓存,需配合消息队列(如 Redis Pub/Sub)同步失效消息,复杂度较高,故入门演示未引入。

Q2:为什么不用 PostgreSQL 替代 Elasticsearch?

A:PostgreSQL 的 B-Tree 索引不支持全文模糊搜索,LIKE '%xx%' 会导致全表扫描,百万级数据下查询超时。Elasticsearch 的倒排索引专为文本搜索设计,毫秒级响应。

Q3:为什么更新时要"删除缓存"而不是"更新缓存"?

A:更新缓存存在并发风险。假设两个请求同时更新同一商品,先更新的数据可能被后更新的旧数据覆盖。而删除缓存 + 被动重建(Cache-Aside) 是更安全的策略——下一次读请求会从 ES/DB 获取最新数据并回写缓存。

Q4:Docker 容器启动后,Python 连接报错"Connection refused"?

A:检查以下几点:

  • 容器是否正在运行:docker ps
  • 端口映射是否正确:docker port my_postgres
  • 是否修改过 docker-compose.yml 中的端口映射,导致代码中的端口不匹配

Q5:PyCharm 中 from database_learn.service import ... 报"未解析的引用"?

A:原因是 PyCharm 未将 src/ 目录标记为 Sources Root。解决方法:

  • 右键点击 src 文件夹 → Mark Directory as → Sources Root
  • 或通过 File → Settings → Project Structure 手动添加

Q6:Elasticsearch 返回空数据(hits 为空)?

A:检查索引名称是否正确(代码中为 products),或者数据是否已写入。可以先用浏览器访问 http://localhost:9200/_cat/indices?v 查看所有索引列表,确认 products 是否存在。

Q7:运行 uv 命令时提示"不是内部或外部命令"?

A:安装 uv 后需要重新打开终端才能生效。如果仍不行,尝试手动将 uv 安装路径(通常位于 %USERPROFILE%\.local\bin)添加到系统环境变量 PATH 中。


附录:命令速查表

Docker 相关

用途 命令
启动所有服务 docker-compose up -d
查看容器状态 docker ps
查看容器日志 docker logs my_postgres
停止并删除所有容器(保留数据) docker-compose down
停止并删除所有容器(含数据卷) docker-compose down -v

PostgreSQL

用途 命令
进入 psql 交互 docker exec -it my_postgres psql -U myuser -d mydb
查看所有表 \dt
查看表结构 \d products
查询数据 SELECT * FROM products;

Elasticsearch

用途 命令 / URL
查看所有索引 curl http://localhost:9200/_cat/indices?v
查询全部数据 curl http://localhost:9200/products/_search?pretty
查看索引映射 curl http://localhost:9200/products/_mapping?pretty
浏览器直接查询 http://localhost:9200/products/_search?pretty

Redis

用途 命令
进入 redis-cli docker exec -it my_redis redis-cli
查看所有 key KEYS *
获取值 GET product:p999
查看过期时间(秒) TTL product:p999

Python 虚拟环境

用途 命令
激活环境(Git Bash) source .venv/Scripts/activate
退出环境 deactivate
运行演示脚本 python scripts/demo.py