# 业务基础模块 第二批的共享 Redis 中间件、可靠队列、多平台存储及其他模块见 [扩展模块](ADVANCED.md)。下文的单进程约束指本页原有默认实现。 这批能力从 `v0.7.0-preview` 提供。统一示例见 [examples/modules](examples/modules/README.md)。业务入口继续使用 `omni.New`,中间件依赖 `httpx`,不依赖具体服务端网络实现。各模块按需创建、注入 Service,再注册到应用关闭流程。 ## 请求追踪与基础指标 `omni.New()` 的默认链包含 RequestID、访问日志(可配置开关)和 Recovery;无需重复挂载。业务通过 `c.Context()` 传递 requestId,`logx.FromContext(ctx)` 取得该 App 的 Logger。手动组装 httpx 链时,也可使用 `middleware.RequestID("X-Request-ID")`、`middleware.AccessLog()` 和 `middleware.Recovery()`。 轻量进程指标直接使用 `metrics`,无需 OTel collector: ```go // import omni "git.yuntuo.hk/k2safe/go-omni" // import "git.yuntuo.hk/k2safe/go-omni/metrics" app := omni.New() measurements := metrics.New() app.Use(measurements.Middleware()) app.GET("/metrics", measurements.Handler()) // 也可以直接读取 measurements.Snapshot()。 ``` 记录请求数、失败数、处理中数量、累计耗时与 HTTP 状态分布;返回的是 JSON 快照,并非 Prometheus 文本格式。指标接口是否暴露以及访问权限由项目配置。需要 HTTP/SQL/Redis spans 和标准 OTel exporter 时,使用 [telemetry 示例](examples/platform/main.go)。需要跨域时配置 `server.cors`,由框架在路由与鉴权前处理预检;默认关闭。已有代理 CORS 可继续使用,避免两处重复设置。见 [CORS 配置与示例](cors/README.md)。 ## 缓存: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{ Proxy: httpc.ProxyConfig{Mode: httpc.ProxyDirect}, Timeout: 3*time.Second, DialTimeout: time.Second, TLSHandshakeTimeout: time.Second, ResponseHeaderTimeout: time.Second, MaxConnsPerHost: 32, MaxIdleConnsPerHost: 16, MaxIdleConns: 64, IdleConnTimeout: 60*time.Second, MaxResponseBytes: 1<<20, }) if err != nil { return err } application.OnShutdown(client.Close) err = client.JSON(ctx, "GET", serviceURL, nil, &result, httpc.WithHeader("Authorization", "Bearer "+token), ) ``` 启动时创建并复用 Client。代理可选环境、直连、固定地址;不配置时保留标准库原代理策略。JSON 支持单次请求头和响应限额,不保存 Client 全局 Token。新阶段超时与空闲连接参数可选,0 沿用默认。 成功响应读取有界;output=nil 流式丢弃,不整体缓存。小错误体只在大小/时间预算内尝试读完以复用连接;未知或大错误体及时关闭。Do 保留标准流式 Request/Response,由调用者限量读取并关闭 body。Close 拒绝新调用并关闭自有池空闲连接,项目先排空在途工作。 自定义 Transport 借用、不修改、不关闭;不能同时指定池/代理/阶段选项。总超时持续覆盖 HTTP 等待、重试和读取 body,但不抢占 JSON 编解码或不遵守 context 的自定义代码。MaxRetries 默认 0,只对可重放 GET/HEAD/OPTIONS 重试;3xx 不跟随。标准库和借用 Transport 自身仍可能重放,不能据此承诺业务只执行一次。 完整参数、NO_PROXY、TLS、安全日志、重试、签名兼容说明见 [httpc 指南](httpc/README.md),可运行 [HTTP Client 示例](examples/httpClient/README.md)。 ## 请求保护: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)。