Files
zhuangang/plan/week05-project1-redis-docker.md
T

14 KiB
Raw Blame History

转岗计划 · 第 5 周学习计划(项目一 · 下:Redis 缓存与限流 + 消息队列入门 + Docker 部署)

所属阶段:第一段 Go 全栈基础的最后一周。总纲见同目录 README.md,上周计划 week04-auth-project1.md,下周计划 week06-python-basics.md。

目标:给“团队任务看板”补上招聘里常见的三样东西:Redis(缓存、限流)、消息队列(用 Redis Streams 入门,了解 RabbitMQ 的基本概念)、Docker 部署(Docker Compose 一键启动,部署到自己的 Ubuntu 服务器,加一条 CI 流水线)。

原则:这三样都浅学够用。Go 全栈部分到本周结束,之后的主线是 Python 和 AI 应用。本周不追求高并发、集群这些深入话题。

前置条件:第 4 周的项目一功能完成,测试通过。

参考资料:


最终项目结构(只列新增和修改)

task-board/
├── docker-compose.yml                   # 全套:postgres、redis、backend、frontend(nginx)
├── docker-compose.dev.yml               # 开发用:只起 postgres 和 redis
├── .env.example                         # 部署用的环境变量示例
├── deploy/
│   ├── nginx.conf                       # 前端静态文件 + /api 反向代理到后端
│   └── deploy.md                        # 部署到 Ubuntu 服务器的步骤
├── .ci/                                 # 按你的代码托管平台放到对应目录(见第 7 天)
│   └── ci.yml
├── backend/
│   ├── Dockerfile                       # 多阶段构建
│   ├── .dockerignore
│   ├── cmd/
│   │   ├── server/main.go               # 原来的 main.go 移到这里
│   │   └── worker/main.go               # 消息消费者(通知处理)
│   └── internal/
│       ├── cache/
│       │   ├── redis.go                 # 连接和通用 Get/Set
│       │   ├── board_cache.go           # 看板任务列表缓存
│       │   └── board_cache_test.go
│       ├── ratelimit/
│       │   ├── limiter.go               # 固定窗口限流
│       │   └── limiter_test.go
│       ├── middleware/
│       │   └── ratelimit.go             # 登录接口限流中间件
│       ├── queue/
│       │   ├── stream.go                # Redis Streams 生产和消费
│       │   └── stream_test.go
│       └── notify/
│           ├── events.go                # 事件结构:任务被指派
│           ├── store.go                 # 通知表读写
│           └── handler.go               # 消费者处理逻辑(幂等)
│   └── db/migrations/
│       ├── 000002_notifications.up.sql
│       └── 000002_notifications.down.sql
└── frontend/
    ├── Dockerfile                       # 构建静态文件,用 nginx 提供
    └── src/components/NotificationBell.tsx

第 1 天:Redis 基础

学习内容

  • Redis 是什么:内存里的键值数据库,常用来做缓存、计数、限流、会话、排行榜、简单队列。
  • 五种常用类型:String、Hash、List、Set、Sorted Set;过期时间 EXPIRE / TTL。
  • 用 redis-cli 动手,Go 里用 go-redis:go get github.com/redis/go-redis/v9。

要建的文件

  • docker-compose.dev.yml:postgres:16 + redis:7,Redis 端口 6379。原来 backend/docker-compose.yml 的数据库服务挪到这里。
  • backend/internal/cache/redis.go:
    • func NewRedis(ctx context.Context, url string) (*redis.Client, error):连接并 Ping。
    • func GetJSON[T any](ctx, rdb, key string) (T, bool, error)、func SetJSON(ctx, rdb, key string, v any, ttl time.Duration) error(顺便练一下 Go 泛型)。
  • internal/config/config.go:新增 RedisURL。
  • backend/sql/practice/redis_cli.md:记录你在 redis-cli 里敲过的命令和结果(每种类型至少 2 条)。

运行方式:docker compose -f docker-compose.dev.yml up -d,docker compose -f docker-compose.dev.yml exec redis redis-cli

完成标准:能说出 Redis 为什么快、数据放内存的风险(重启丢数据,了解 RDB 和 AOF 两种持久化即可)。


第 2 天:缓存

学习内容

  • 旁路缓存(Cache Aside):读时先查缓存,没有再查数据库并写回缓存;写时先更新数据库,再删除缓存。
  • 三个常见问题,面试常问:缓存穿透(查不存在的数据)、缓存击穿(热点 key 过期瞬间大量请求)、缓存雪崩(大量 key 同时过期)。各自的常用办法:缓存空值、互斥锁 / 单飞(singleflight)、过期时间加随机值。

要建的文件

  • internal/cache/board_cache.go:
    • type BoardCache struct { rdb *redis.Client; ttl time.Duration }
    • func (c *BoardCache) GetTasks(ctx, boardID int64) (map[string][]model.Task, bool, error)
    • func (c *BoardCache) SetTasks(ctx, boardID int64, tasks map[string][]model.Task) error:过期时间 = 基础时间 + 随机几十秒。
    • func (c *BoardCache) Invalidate(ctx, boardID int64) error
    • key 统一格式:board:{id}:tasks。
  • 修改 internal/service/task_service.go:List 先查缓存;Create、Update、Move、Delete 成功后删除缓存。用 golang.org/x/sync/singleflight 合并同一看板的并发查询。
  • Redis 不可用时降级:缓存出错只记日志,继续查数据库,不让整个接口失败。
  • internal/cache/board_cache_test.go:没设 TEST_REDIS_URL 时跳过;测试读写、过期、删除。

完成标准:能画出旁路缓存的读写流程,能解释为什么是“删缓存”而不是“更新缓存”;停掉 Redis 后接口照常可用。


第 3 天:限流

学习内容

  • 为什么要限流:防暴力破解登录、防刷接口、保护下游(以后调用大模型接口时同样要限流,因为有额度和费用)。
  • 固定窗口计数:INCR key + 首次设置 EXPIRE;用 Lua 脚本或 MULTI 保证两步一起执行。了解滑动窗口和令牌桶的思路即可。

要建的文件

  • internal/ratelimit/limiter.go:
    • type Limiter struct { rdb *redis.Client }
    • func (l *Limiter) Allow(ctx, key string, limit int, window time.Duration) (allowed bool, remaining int, err error)
  • internal/ratelimit/limiter_test.go:同一个 key 第 limit+1 次被拒绝;窗口过去后恢复。
  • internal/middleware/ratelimit.go:func RateLimit(l *ratelimit.Limiter, keyFn func(*gin.Context) string, limit int, window time.Duration) gin.HandlerFunc,超限返回 429,响应头带 Retry-After。
  • router.go:登录接口按“IP + 邮箱”限流(比如每分钟 5 次),其他接口按用户 ID 宽松限流。

完成标准:用 api.http 连续错误登录 6 次,第 6 次返回 429;能说出固定窗口在窗口边界的问题。


第 4 天:消息队列入门(Redis Streams)

学习内容

  • 消息队列解决什么问题:解耦(指派任务的接口不用关心通知怎么发)、异步(接口快速返回)、削峰。
  • 基本概念:生产者、消费者、消费者组、确认(ACK)、重试、死信、“至少一次”投递导致的重复消费——所以消费者必须幂等。
  • Redis Streams 命令:XADD、XREADGROUP、XACK、XPENDING、XAUTOCLAIM。
  • RabbitMQ 只了解概念(交换机、队列、路由键、确认),对比一下:Redis Streams 轻量,适合小项目;RabbitMQ / Kafka 是专门的消息中间件,可靠性和功能更强。本周不装 RabbitMQ,读完官方入门教程的前两篇即可。

要建的文件

  • db/migrations/000002_notifications.up.sql / .down.sql:notifications(id, user_id, event_id TEXT UNIQUE, type, payload JSONB, read_at, created_at)。event_id 唯一约束用来做幂等。
  • internal/notify/events.go:type TaskAssigned struct { EventID string; BoardID, TaskID, AssigneeID, ByUserID int64; At time.Time }。
  • internal/queue/stream.go:
    • func Publish(ctx, rdb, stream string, v any) (string, error):XADD,消息体 JSON。
    • func Consume(ctx, rdb, stream, group, consumer string, handle func(ctx, id string, body []byte) error) error:创建消费者组(已存在就忽略)→ 循环 XREADGROUP → 处理成功才 XACK;启动时先用 XAUTOCLAIM 认领别的消费者挂掉后遗留的消息。
  • internal/notify/store.go:InsertNotification,用 ON CONFLICT (event_id) DO NOTHING。
  • internal/notify/handler.go:处理 TaskAssigned → 写通知表。
  • 修改 task_service.go:创建或修改任务时负责人有变化,就 Publish 一个 TaskAssigned 事件(发布失败只记日志,不影响主流程,并在 README 里写明这个取舍)。
  • cmd/worker/main.go:独立的消费者进程,支持优雅关机。
  • 新接口:GET /api/notifications、POST /api/notifications/:id/read。
  • internal/queue/stream_test.go:同一条消息处理两次,通知表只有一行。

运行方式:一个终端 go run ./cmd/server,另一个 go run ./cmd/worker;指派任务后查通知接口。

完成标准:能说清“至少一次投递 + 幂等消费”;把 worker 停掉再指派任务,重新启动 worker 后通知能补上。


第 5 天:Docker 镜像

学习内容

  • 镜像、容器、卷、网络;Dockerfile 常用指令。
  • 多阶段构建:第一阶段编译,第二阶段只放编译好的二进制,镜像小、没有源码。
  • 容器里的配置都走环境变量;服务之间用 Compose 的服务名互相访问(比如 postgres:5432)。

要建的文件

  • backend/Dockerfile:
    • 阶段一 golang:1.x 镜像:先复制 go.mod、go.sum 下载依赖(利用缓存),再复制源码,CGO_ENABLED=0 go build 出 server 和 worker 两个二进制。
    • 阶段二用体积小的基础镜像(alpine 或 distroless),非 root 用户运行。
  • backend/.dockerignore
  • frontend/Dockerfile:阶段一 node 镜像 npm ci && npm run build;阶段二 nginx 镜像放静态文件和 deploy/nginx.conf。
  • deploy/nginx.conf:/ 指向静态文件(单页应用的路由回退到 index.html),/api/ 反向代理到 backend:8080。
  • 前端接口地址改成相对路径 /api,开发时用 Vite 代理。

运行方式:docker build -t task-board-backend ./backend,docker images 看镜像大小。

完成标准:能说出多阶段构建的好处;后端镜像不包含源码和 Go 工具链。


第 6 天:Docker Compose 一键启动 + 前端通知

要建的文件

  • task-board/docker-compose.yml:
    • postgres、redis(都带具名卷和健康检查)。
    • migrate:用 golang-migrate 的官方镜像执行迁移,执行完就退出。
    • backend:depends_on 等数据库健康和迁移完成;环境变量从 .env 读。
    • worker:同一个镜像,启动命令换成 worker。
    • frontend:对外暴露 80 端口。
  • task-board/.env.example:POSTGRES_PASSWORD、JWT_SECRET、REDIS_URL 等,写清楚哪些必须改。
  • frontend/src/components/NotificationBell.tsx:每 30 秒拉一次通知列表,显示未读数,点开可标记已读。

运行方式:cp .env.example .env(改密码和密钥)→ docker compose up -d --build → 打开 http://localhost。

完成标准:一台只装了 Docker 的机器,一条命令就能跑起完整系统。


第 7 天:部署到服务器 + CI + 收尾

学习内容

  • 部署的最小流程:服务器装 Docker → 拉代码 → 配 .env → docker compose up -d --build;更新时 git pull 再执行一次。
  • CI:每次推送自动跑检查和测试。不同代码托管平台的配置文件位置不同(GitHub Actions 是 .github/workflows/,GitLab 是 .gitlab-ci.yml,Gitea 的 Actions 和 GitHub 写法接近),先确认你的 git.gozeros.com 是哪种平台、有没有开启 CI,没有的话就先写好文件、在本地用脚本跑同样的步骤。
  • 了解即可:Kubernetes 是更大规模的容器编排,招聘里没有点名要求,本计划不学。

要建的文件和修改

  • deploy/deploy.md:在 Ubuntu 服务器上部署的完整步骤,包括防火墙只开 80(和 22)、.env 权限设为 600、查看日志 docker compose logs -f backend、备份数据库 pg_dump。
  • .ci/ci.yml(放到你平台对应的目录):后端 go vet、go test -race(起 postgres 和 redis 服务容器跑集成测试);前端 npm ci && npm run build;最后 docker build 检查镜像能构建。
  • 在你的 Ubuntu 服务器上按 deploy.md 部署一次(服务器是内网或没有域名也没关系,能在内网访问即可;HTTPS 放到第 15 周和 AI 项目一起处理)。
  • 更新 task-board/README.md:架构图加上 Redis、worker、Nginx;新增“缓存策略”“限流”“消息队列与幂等”“部署”四节;本地开发和一键部署两种运行方式。
  • 提交并推送到 zhuangang 仓库。确认 .env 没有提交。

完成标准

  • go vet ./...、go test ./... -race 和集成测试都通过。
  • 服务器上能访问项目一,重启服务器后 docker compose 能自动拉起(服务加 restart: unless-stopped)。
  • 能用 3 分钟讲清楚项目一的完整架构:分层、鉴权、缓存、限流、消息队列、部署。
  • 在总纲 README.md 的进度记录里勾掉第 5 周和“第一段 Go 全栈基础”。

本周自查清单

  • 会用 Redis 五种常用类型,知道持久化的两种方式
  • 会写旁路缓存,能讲清穿透、击穿、雪崩和对应办法
  • Redis 挂了接口不跟着挂(降级)
  • 会用 Redis 写固定窗口限流
  • 理解消息队列的作用,能讲清“至少一次投递 + 幂等消费”
  • 会写 Redis Streams 的生产者和消费者组,知道 RabbitMQ 的基本概念
  • 会写多阶段构建的 Dockerfile 和多服务的 Docker Compose
  • 项目一已部署到服务器,有 CI 配置

下周预告:第 6 周开始 Python。从这里起 Python 是主语言,Go 是加分项。