# 业务基础模块 第二批的共享 Redis 中间件、可靠队列、多平台存储及其他模块见 [扩展模块](ADVANCED.md)。下文的单进程约束指本页原有默认实现。 这批能力从 `v0.7.0-preview` 提供。统一示例见 [examples/modules](examples/modules/README.md)。业务入口继续使用 `omni.New`,中间件依赖 `httpx`,不依赖具体服务端网络实现。各模块按需创建、注入 Service,再注册到应用关闭流程。 ## 缓存:cache 与 cache/redis ```go store, err := cache.NewMemory(cache.MemoryConfig{MaxEntries: 4096, MaxBytes: 32 << 20}) if err != nil { return err } application.OnShutdown(store.Close) loader, err := cache.NewLoader(store, cache.LoaderConfig{MaxLoads: 128, Timeout: 5 * time.Second}) if err != nil { return err } value, err := loader.GetOrLoad(ctx, "tenant:42:product:7", time.Minute, func(ctx context.Context) ([]byte, error) { return repository.LoadProduct(ctx, "42", "7") }) ``` - `Store` 统一 `Get/Set/Delete`,返回字节切片;业务可用 JSON 编解码。 - 内存缓存使用 LRU,同时限制条目数和 key/value 总字节数;字节上限不包含 Go 对象开销。 读写复制切片,过期时懒清理,没有每个条目一个 goroutine。 - TTL 为零表示不过期,负值拒绝;缺失返回 `cache.ErrMiss`。删除不存在的 key 成功。 - Loader 合并**同进程、同 key**的并发回源,最多同时运行 `MaxLoads` 个不同 key 的加载。 单个调用者取消只停止等待;共享加载有独立超时,加载函数必须响应 context。 超限返回 `ErrBusy`,缓存故障直接返回错误,不自动当成 miss 冲击数据库。 - 同 key 的调用必须具有相同数据含义、权限范围和 TTL;加载采用首个调用者的 context 值。 多租户 key 必须含租户标识。进程间合并、分布式锁、缓存更新与数据库事务一致性由业务另外设计。 Redis 适配器借用调用方创建的 go-redis 客户端和连接池: ```go client := goredis.NewClient(&goredis.Options{ Addr: "127.0.0.1:6379", PoolSize: 16, MaxRetries: -1, DialTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, PoolTimeout: time.Second, ContextTimeoutEnabled: true, }) // 启动时先使用有截止时间的 context 执行 client.Ping(ctx)。 store, err := rediscache.New(client, "billing:") // cache/redis 的导入别名 if err != nil { _ = client.Close(); return err } application.OnShutdown(client.Close) // store 可直接传入 cache.NewLoader。 ``` 连接失败与 Redis 错误保持原样;只有 Redis nil 映射为 `ErrMiss`。Redis TTL 精度为毫秒, 正的亚毫秒 TTL 提升到 1ms。适配器不负责关闭借用的客户端,Redis 服务端容量策略由部署配置。 连接池和超时配置参考 [go-redis 官方生产使用说明](https://redis.io/docs/latest/develop/clients/go/produsage/)。 ## 出站 HTTP:httpc ```go client, err := httpc.New(httpc.Config{ Timeout: 3*time.Second, MaxRetries: 2, MaxConnsPerHost: 32, MaxResponseBytes: 1 << 20, }) if err != nil { return err } application.OnShutdown(client.Close) err = client.JSON(ctx, "GET", serviceURL, nil, &result) ``` 客户端应在服务启动时创建并复用。默认使用独立的 `net/http.Transport` 连接池,每个目标主机 有连接上限;该上限不是整个进程的全局连接数上限。也可以注入自有 `http.RoundTripper`, 其连接限制和生命周期由调用方负责。这样出站实现同样可以替换。 总超时覆盖建连、重试、等待和读取响应体。`Do` 返回后调用方必须读取/关闭 `response.Body`; `JSON` 自动关闭,并限制解码前读取的字节数。`Do` 允许流式读取,本身不限制响应体大小。 `Close` 拒绝新请求、关闭自有池的空闲连接;在途请求应先排空。 `MaxRetries` 默认 0,最多 5。封装只对可重放的 GET/HEAD/OPTIONS 重试网络错误及 429/502/503/504;POST/PUT/PATCH/DELETE 不做应用层自动重试。标准库 Transport 和借用的 RoundTripper 自身的重放规则仍生效,不能据此承诺“只执行一次”。退避带抖动,尊重 `Retry-After`;服务器要求等待超过 `MaxBackoff` 时直接返回该响应,不提前重试。 重定向交给业务处理,3xx 不会自动跟随。 默认从 context 传递 `X-Request-ID`,日志只包含方法、主机、状态、尝试次数和收到响应头的耗时, 不记录完整 URL、查询参数、凭证和响应体。非 2xx 的 JSON 调用返回只带状态码的 `StatusError`。 ## 请求保护:middleware ```go limiter, err := middleware.NewRateLimiter(middleware.RateLimitConfig{Rate: 20, Burst: 40}) if err != nil { return err } idempotency, err := middleware.NewIdempotency(middleware.IdempotencyConfig{ Scope: func(c *httpx.Context) (string, error) { principal, _ := auth.FromContext(c) return principal.UserID, nil // 多租户应用应组合租户和用户 ID }, }) if err != nil { return err } api := application.Group("/api", middleware.Timeout(3*time.Second), auth.RequireAuth(authenticator), limiter.Middleware()) api.POST("/orders", createOrder, idempotency.Middleware()) ``` 限流为进程内令牌桶,`Rate` 为每秒额度,`Burst` 为突发容量。默认按已解析的可信客户端 IP, 可用 `Key` 按已认证身份分桶。额度不足返回 **429 + Retry-After**;key 数超过 `MaxKeys` 返回 **503**。满容量时不会踢掉活跃桶让攻击者绕过限流;空闲桶经过完整补充周期后才允许回收。 Timeout 设置协作式 deadline,不额外启动 Handler goroutine。业务及下游调用必须响应 context; 超时后尚未提交响应时返回 **504**,已经提交的响应保持原状态。它不能强制中断任意 Go 代码, 也不能替代网络层读写超时。不要在业务代码中异步持有 `httpx.Context`。 幂等保护适用于小型、非流式写接口: - 必须先认证/鉴权,再执行幂等中间件;`Scope` 必须取自服务端验证过的身份。 GET/HEAD/OPTIONS 直接通过,写请求要求 `Idempotency-Key`,长度 1..128 个非空格 ASCII 字符。 - 同 Scope + key + 方法/URL/Content-Type/body 重放首次响应;内容改变返回 **409**。 正在执行或结果不可重放的相同请求也返回 **409**,不并发执行第二次。 - 默认最多 32 个处理中请求、256 条记录、16 MiB 保存的响应头/body,单次请求体 1 MiB、 响应 body 64 KiB。请求超限 **413**,容量满 **503**,响应超限 **500**。 - panic、5xx、超时、响应超限、Set-Cookie、Cache-Control:no-store 或保存容量不足时保留失败记录, 阻止在 TTL 内自动重执行业务。业务可能已经提交,调用方应查询实际结果后处理。 - 完成记录默认保留 10 分钟;在途记录不会因 TTL 到期而允许重复执行。 重放保留原响应 body、原 `requestId` 与原 `X-Request-ID`,增加 `Idempotency-Replayed:true`; 本次访问日志仍使用本次请求 ID。 - 记录仅在当前进程有效,重启、TTL 到期和多副本切换后均无法保证去重。支付、库存等关键写入 应在数据库事务内配合唯一业务键。该实现不提供跨进程 exactly-once,也不支持 SSE/流式/升级连接。 ## 后台执行:task ```go jobs, err := task.New(task.Config{Workers: 4, QueueSize: 128, TaskTimeout: 30*time.Second}) if err != nil { return err } application.OnShutdown(jobs.Close) // 在注册数据库/缓存关闭之后注册,先排空任务 future, err := jobs.Submit(serviceContext, "refreshCatalog", refreshCatalog) if err != nil { return err } // 满队列立即返回 ErrQueueFull err = future.Wait(ctx) // 可选;不等待时,任务失败仍通过 OnError/logx 报告 err = jobs.Every("refreshCatalog", time.Minute, refreshCatalog) ``` 固定 worker 和有界队列控制资源。任务保留提交 context;需要独立于 HTTP 请求执行时使用服务 context,或明确使用 `context.WithoutCancel` 保留日志关联值。任务仍受 Runner 的超时/退出控制。 任务函数与 `OnError` 回调应及时返回,尤其要响应取消;panic 被转为失败,worker 继续工作。 `Every` 使用固定间隔,第一次在一个间隔后执行;同一计划上轮尚未完成、排队或队列满时跳过。 默认最多 128 个计划,名称不可重复。`Stats` 提供 active/queued/completed/failed/skipped。 它是进程内任务执行器,没有任务持久化、失败自动重试、cron 表达式、跨副本选主或崩溃恢复。 `Shutdown(ctx)` 停止接收和调度,排空已接受任务;截止时间到后取消任务并及时返回。 忽略取消的任务不能被 Go 强杀,可能继续运行。`Close` 默认给排空 10 秒。 ## 增加业务模块:go-omni add ```sh go build -o /tmp/go-omni ./cmd/go-omni /tmp/go-omni add orderItems --dir /path/to/billing ``` 生成 `internal/orderItems/{module.go,model,repository,service,handler,README.md}`,包含 name/createdAt 示例模型、内存 CRUD、输入校验、错误映射及可注入 Repository。模块名使用 lowerCamelCase。 在现有启动代码中导入模块后调用 `orderItems.Register(application, repository, authMiddleware)`。 脚手架不自动修改现有路由、main、go.mod 或数据库,已有目录不会覆盖。 内存 Repository 仅为可运行示例:重启丢数据,列表未分页;正式业务应实现自己的持久化 Repository。 ## 验证入口 ```sh GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race ./... go vet ./... go build ./... python3 scripts/verifyModules.py --goproxy https://goproxy.cn,direct ``` Linux runner 顺序运行两个容器:全量 race/vet/build/独立生成项目,随后真实 Redis 适配验证。 每次最多一个容器,限制 2 CPU、2 GiB(不额外使用 swap)、256 pids;Redis 测试容器禁外网, 只用内部 loopback,不发布端口。Redis 集成二进制为静态非 race,适配器单测在全量 race 中覆盖。 结束清理本轮容器,并记录提交、源文件摘要、镜像 ID、日志摘要和原有容器是否仍运行。 本轮通过结果和原始证据见 [业务模块验收](performance/modulesAcceptance.md)。