第一章:上海Go工程师薪资现状与能力断层全景图
上海作为国内头部互联网企业与金融科技公司的聚集地,Go语言工程师的薪酬水平持续领跑全国。据2024年Q2猎聘与拉勾联合发布的《长三角后端技术人才报告》,上海中级(3–5年经验)Go工程师平均年薪达42.6万元,资深(5–8年)岗位中位数为68.3万元,而具备云原生架构与高并发系统设计能力的专家岗,年薪区间已突破90–140万元。值得注意的是,薪资分布呈现显著“双峰”特征:约37%的岗位集中在35–50万元区间,另有28%集中于85万元以上,中间地带(55–75万元)供给明显稀疏——这正是能力断层在薪酬曲线上的直观映射。
薪资分层与核心能力强相关性
市场不再仅按年限定价,而是以可验证的技术纵深为标尺:
- 基础层:熟练使用
net/http、gorilla/mux、基础sync工具,对应35–45万元; - 进阶层:掌握
go tool pprof性能分析、gRPC服务治理、etcd一致性实践,支撑50–70万元; - 架构层:主导过Service Mesh落地(如Istio+Envoy定制)、自研可观测性链路(OpenTelemetry SDK深度集成),方能触达85万+。
典型能力断层场景示例
许多工程师能编写高并发HTTP服务,却难以定位真实生产瓶颈。以下为快速验证协程泄漏的诊断脚本:
# 1. 获取目标进程PID(假设为12345)
# 2. 检查goroutine数量突增(正常服务通常<500,持续>2000需警惕)
curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" | \
grep -c "created by" # 统计活跃goroutine创建源头
# 3. 抓取堆栈快照供深度分析
curl -s "http://localhost:6060/debug/pprof/goroutine?debug=1" > goroutines.log
该操作无需重启服务,5秒内即可暴露异步任务未回收、Timer未Stop等高频断层点。
企业招聘需求关键词热度对比(2024上半年,上海样本)
| 能力关键词 | 岗位提及率 | 平均溢价幅度 |
|---|---|---|
| Kubernetes Operator开发 | 68% | +32% |
| eBPF网络监控集成 | 41% | +47% |
| Go泛型高级应用 | 53% | +19% |
| 单元测试覆盖率≥85% | 89% | +0%(基础门槛) |
断层并非源于知识缺失,而是工程化闭环能力的缺位:能写API,未必能压测;能调用Redis,未必能设计连接池熔断策略;熟悉Go语法,未必理解runtime.GC()触发对P99延迟的真实影响。
第二章:Go并发模型核心原理与工业级实践
2.1 Goroutine调度器GMP模型的源码级解析与压测验证
Go 运行时调度器以 G(Goroutine)、M(OS Thread)、P(Processor)三元组构成核心抽象,其状态流转实现在 runtime/proc.go 中。
核心结构体关键字段
g.status: 当前状态(_Grunnable, _Grunning, _Gsyscall 等)m.p: 绑定的 P,空闲时为 nilp.runq: 本地运行队列(环形数组,长度 256)sched.runq: 全局运行队列(链表,锁保护)
Goroutine 唤醒关键路径
func goready(gp *g, traceskip int) {
status := readgstatus(gp)
_g_ := getg()
mp := acquirep() // 获取 P,可能触发 steal
globrunqput(gp) // 若本地队列满,则入全局队列
if _g_.m.p != mp {
throw("goready: m.p != mp")
}
}
acquirep() 触发 P 获取逻辑:若无空闲 P,则尝试从其他 P 的本地队列 steal(每 61 次调度尝试一次),体现 work-stealing 设计。
压测对比(16 核机器,100k goroutines)
| 调度模式 | 平均延迟 | P 利用率 | GC STW 影响 |
|---|---|---|---|
| 默认 GMP | 12.3μs | 94% | 低 |
GOMAXPROCS=1 |
89.7μs | 100% | 显著升高 |
graph TD
A[Goroutine 创建] --> B[入当前 P.runq 或 sched.runq]
B --> C{P.runq 是否满?}
C -->|是| D[入全局队列 + 唤醒空闲 M]
C -->|否| E[本地执行]
D --> F[M 调用 schedule 循环]
F --> G[先查本地 runq,再 steal,最后查全局]
2.2 Channel底层实现机制与高并发场景下的内存泄漏规避实践
数据同步机制
Go 的 channel 底层基于环形缓冲区(有缓冲)或直接通信(无缓冲),核心结构体 hchan 包含 buf 指针、sendx/recvx 索引及 sendq/recvq 等待队列。
内存泄漏高危场景
- goroutine 阻塞在已关闭 channel 的接收端(未检查
ok) - 循环引用 channel 与长生命周期对象(如闭包捕获)
- 忘记关闭 sender 导致 receiver 永久阻塞
安全实践代码示例
// ✅ 正确:带 ok 检查 + context 控制超时
select {
case msg, ok := <-ch:
if !ok { return } // channel 已关闭
process(msg)
case <-ctx.Done():
return // 防止 goroutine 泄漏
}
逻辑说明:
ok判断避免从已关闭 channel 读取零值并持续等待;ctx.Done()提供外部中断能力,防止 goroutine 因 channel 阻塞而无法退出。ctx的 cancel 函数需在上层显式调用,确保资源可回收。
| 风险点 | 规避方式 |
|---|---|
| 未关闭的 sender | 使用 defer close(ch) 或统一管理生命周期 |
| 无界缓冲 channel | 限制 make(chan T, N) 中 N 值,N ≤ 1024 推荐 |
graph TD
A[goroutine 启动] --> B{channel 是否关闭?}
B -->|否| C[尝试收发]
B -->|是| D[立即返回/清理]
C --> E[是否超时?]
E -->|是| D
E -->|否| C
2.3 Context取消传播链路追踪与超时控制在微服务网关中的落地
在网关层统一注入 context.WithTimeout 与 trace.SpanContext, 实现跨服务的请求生命周期协同治理。
超时控制与Cancel信号注入
// 在网关路由处理器中注入带超时的Context
ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
defer cancel() // 确保资源及时释放
r = r.WithContext(ctx) // 注入下游请求
WithTimeout 创建可取消上下文,800ms 为端到端SLA阈值;defer cancel() 防止goroutine泄漏;WithContext 将取消信号透传至后端服务。
链路追踪上下文传播
| 字段 | 作用 | 示例值 |
|---|---|---|
trace-id |
全局唯一请求标识 | a1b2c3d4e5f67890 |
span-id |
当前网关Span标识 | g1h2i3j4 |
parent-id |
上游调用方Span ID | m5n6o7p8 |
取消传播机制流程
graph TD
A[客户端请求] --> B[网关注入ctx.WithTimeout]
B --> C[注入TraceID/SpanID]
C --> D[转发至下游服务]
D --> E{下游是否超时?}
E -- 是 --> F[自动触发cancel()]
E -- 否 --> G[正常响应]
2.4 sync.Pool对象复用策略与GC压力优化在上海本地金融系统中的实测对比
数据同步机制
上海某清算系统在高频报文解析场景中,每秒新建约12万 *bytes.Buffer 实例,触发频繁 GC(平均 STW 达 8.3ms)。引入 sync.Pool 后:
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer) // 预分配 1KB 底层切片,避免初始扩容
},
}
逻辑分析:
New函数返回未初始化但内存已预留的缓冲区;bufPool.Get()复用旧实例前自动调用Reset(),清除残留数据并重置长度为 0;Put()仅在容量 ≤ 4KB 时回收,防止内存膨胀。
性能对比(压测结果)
| 指标 | 原始方案 | Pool 优化后 | 降幅 |
|---|---|---|---|
| GC 次数/分钟 | 142 | 9 | ↓93.6% |
| 平均分配延迟(μs) | 112 | 3.7 | ↓96.7% |
内存复用路径
graph TD
A[请求到达] --> B{Get from Pool}
B -->|命中| C[Reset & 复用]
B -->|Miss| D[New Buffer]
C --> E[业务写入]
D --> E
E --> F[Put back if ≤4KB]
2.5 并发安全Map选型决策树:sync.Map vs RWMutex+map vs 第三方库bench实证
数据同步机制对比
sync.Map:无锁读、懒加载写,适合读多写少且键生命周期长的场景;不支持遍历中删除。RWMutex + map:显式控制读写锁粒度,可定制化强(如分段锁),但易误用导致死锁或性能瓶颈。- 第三方库(如
fastmap、concurrent-map):常基于分片+原子操作,需验证 Go 版本兼容性与维护活跃度。
基准测试关键指标
| 方案 | 读吞吐(QPS) | 写吞吐(QPS) | 内存开销 | GC压力 |
|---|---|---|---|---|
| sync.Map | 12.4M | 0.8M | 中 | 低 |
| RWMutex+map | 9.1M | 3.2M | 低 | 中 |
| fastmap v1.2 | 14.7M | 4.5M | 高 | 高 |
// benchmark核心逻辑示例(go test -bench)
func BenchmarkSyncMapRead(b *testing.B) {
m := &sync.Map{}
for i := 0; i < 1000; i++ {
m.Store(i, i*2)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, ok := m.Load(i % 1000); !ok { // 热点key循环读取
b.Fatal("missing key")
}
}
}
该压测模拟高并发只读场景:Load 路径绕过锁,复用原子指针跳转,故吞吐显著高于加锁方案;但i % 1000强制缓存局部性,凸显 sync.Map 的 read-only map 命中优势。
graph TD A[读多写少?] –>|是| B[sync.Map] A –>|否| C[需遍历/删除?] C –>|是| D[RWMutex+map] C –>|否| E[高吞吐写密集?] E –>|是| F[fastmap/concurrent-map]
第三章:头部公司Go笔试高频并发题深度拆解
3.1 “百万连接WebSocket网关”设计题:从状态同步到心跳熔断的全链路实现
数据同步机制
采用增量状态广播 + 客户端本地合并策略,避免全量推送带宽爆炸。服务端维护连接元数据版本号(conn_ver),每次变更(如用户上线、房间切换)仅推送差异事件。
// ConnStateEvent 表示连接状态的最小粒度变更
type ConnStateEvent struct {
ConnID string `json:"cid"` // 连接唯一标识
Op string `json:"op"` // "join"/"leave"/"update"
RoomID string `json:"rid,omitempty"`
Version uint64 `json:"ver"` // 全局单调递增版本号
Timestamp int64 `json:"ts"` // 毫秒级时间戳,用于客户端去重
}
逻辑分析:Version 作为全局水位线,客户端按序缓存未处理事件;Timestamp 防止网络乱序导致的状态错乱;Op 字段解耦业务语义,使网关层无需感知具体业务逻辑。
心跳熔断策略
- 客户端每15s发
ping,服务端10s内必须响应pong - 连续3次超时(即45s无有效心跳)触发主动关闭
- 熔断后进入“半休眠”状态:保留会话上下文30s,支持快速重连复用
| 指标 | 阈值 | 作用 |
|---|---|---|
| 心跳间隔 | 15s | 平衡实时性与资源开销 |
| 响应超时窗口 | 10s | 容忍弱网抖动 |
| 连续失败次数 | 3 | 避免瞬时抖动误判 |
| 半休眠保留时长 | 30s | 支持断线重连状态平滑恢复 |
全链路熔断流程
graph TD
A[客户端发送 ping] --> B{服务端收到?}
B -->|是| C[10s内回 pong]
B -->|否| D[记录超时]
C --> E[更新 lastActive 时间]
D --> F[累计超时计数++]
F --> G{计数 ≥ 3?}
G -->|是| H[关闭连接 + 触发熔断回调]
G -->|否| I[等待下次 ping]
3.2 “分布式订单库存扣减”笔试真题:CAS+Redis Lua+本地缓存三级一致性方案
核心挑战
高并发下单场景下,需同时保障:① 库存不超卖;② 响应低延迟;③ 多节点数据最终一致。
三级协同机制
- L1(本地缓存):Caffeine 缓存库存快照(TTL=10s,refreshAfterWrite=5s)
- L2(Redis):主库存键
stock:{skuId},原子操作依托 Lua 脚本 - L3(DB):MySQL 最终持久化,通过 binlog 同步刷新本地缓存
Redis Lua 扣减脚本
-- KEYS[1]=stock_key, ARGV[1]=expected_version, ARGV[2]=decrement
if redis.call('GET', KEYS[1]..'_ver') == ARGV[1] then
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[2]) then
redis.call('DECRBY', KEYS[1], ARGV[2])
redis.call('INCR', KEYS[1]..'_ver') -- 版本递增,触发本地缓存失效
return 1
end
end
return 0
逻辑说明:脚本以「版本号校验 + 原子扣减」实现 CAS;
KEYS[1]_ver独立存储版本,避免 GET-SET 竞态;返回1表示成功,表示失败(需重试或降级)。参数ARGV[1]来自本地缓存快照版本,确保 L1/L2 视图对齐。
一致性保障对比
| 层级 | 一致性模型 | 延迟 | 容错能力 |
|---|---|---|---|
| L1(本地) | 最终一致(带版本探测) | 单节点失效不影响全局 | |
| L2(Redis) | 强一致(Lua 原子块) | ~2ms | 主从异步复制下短暂不一致 |
| L3(DB) | 强一致(行锁+事务) | ~50ms | 全局唯一真相源 |
graph TD
A[用户下单请求] --> B{本地缓存命中?}
B -->|是| C[读取库存快照+版本]
B -->|否| D[穿透至Redis]
C --> E[执行CAS-Lua脚本]
D --> E
E -->|成功| F[异步更新DB+广播版本变更]
E -->|失败| G[重试/熔断]
F --> H[DB Binlog → 消费者刷新所有节点L1]
3.3 “实时风控规则引擎”并发瓶颈分析:Pipeline模式改造与goroutine泄露定位实战
瓶颈初现
线上压测时,QPS超800后延迟陡增,pprof显示 runtime.gopark 占比超65%,goroutine数持续攀升至12k+且不回收。
goroutine泄露定位
通过 go tool trace 发现大量 ruleExecutor.run() 协程阻塞在 channel receive:
func (e *ruleExecutor) run(ctx context.Context, ch <-chan Event) {
for {
select {
case evt := <-ch: // 泄露根源:ch未关闭,协程永驻
e.eval(ctx, evt)
case <-ctx.Done():
return
}
}
}
逻辑分析:
ch由上游 producer 创建,但异常退出时未显式 close,导致下游ruleExecutor永久等待。ctx.Done()是唯一退出路径,而业务逻辑中存在ctx被意外重用或未传递到底层的情况。
Pipeline改造对比
| 维度 | 原始Fan-out模型 | Pipeline模型 |
|---|---|---|
| 并发粒度 | 每规则独立goroutine | 每阶段单goroutine+buffered channel |
| channel生命周期 | 长期持有,易泄露 | 随pipeline启动/关闭自动管理 |
| 错误传播 | 隔离差,panic扩散 | 阶段间error channel显式透传 |
关键修复代码
func NewPipeline() *Pipeline {
in := make(chan Event, 1024)
out := make(chan Result, 1024)
// 启动时绑定生命周期,defer close
go func() {
defer close(out)
for evt := range in { // range自动检测channel关闭
out <- evaluate(evt)
}
}()
return &Pipeline{in: in, out: out}
}
参数说明:
1024为防背压的缓冲容量;range in替代for { <-in },确保上游关闭后自动退出,根治goroutine泄露。
第四章:上海本土企业Go并发架构演进案例库
4.1 拼多多上海研发中心:秒杀系统从channel阻塞到异步事件驱动的重构路径
重构动因:Channel 阻塞瓶颈
高并发秒杀场景下,原始 Go 服务采用 chan *Order 同步传递订单,导致 goroutine 大量阻塞在 select { case orderCh <- o: },P99 延迟飙升至 1.2s。
核心演进路径
- 移除共享 channel,引入事件总线(
eventbus.EventBus) - 订单创建后仅发布
OrderCreatedEvent,由独立消费者异步处理库存扣减、风控校验等 - 关键组件解耦为可插拔处理器(
HandlerFunc)
异步事件处理示例
// 订单创建后仅发布事件,不等待下游
bus.Publish("OrderCreated", &event.OrderCreatedEvent{
OrderID: "ORD-2024-7890",
UserID: 100456,
ItemID: 88231,
Timestamp: time.Now().UnixMilli(),
})
逻辑分析:事件结构体轻量化(仅含必要字段),
Timestamp用于幂等与超时判断;Publish非阻塞,底层使用 ring buffer + worker pool,吞吐提升 4.3×。参数OrderID作为分布式幂等键,UserID用于风控上下文关联。
性能对比(QPS=12,000)
| 指标 | Channel 同步模型 | 事件驱动模型 |
|---|---|---|
| P99 延迟 | 1240 ms | 86 ms |
| Goroutine 数 | ~8,200 | ~1,100 |
| 库存一致性错误率 | 0.37% | 0.002% |
graph TD
A[HTTP Handler] -->|发布事件| B[Event Bus]
B --> C[Inventory Handler]
B --> D[Risk Control Handler]
B --> E[Notification Handler]
C --> F[(Redis Lua 扣减)]
D --> G[(实时用户行为画像)]
4.2 小红书基础架构部:日志采集Agent中goroutine生命周期管理的三次迭代
初版:裸启动 + defer recover
早期采用 go func() { ... }() 直接启动,依赖 defer recover() 捕获 panic,但无法感知 goroutine 退出状态,导致僵尸协程堆积。
二版:Context + Done channel
引入 context.WithCancel,显式监听 ctx.Done() 实现主动退出:
func runWorker(ctx context.Context, ch <-chan LogEntry) {
for {
select {
case entry := <-ch:
process(entry)
case <-ctx.Done():
log.Info("worker exiting gracefully")
return // 正确释放资源
}
}
}
ctx.Done()提供统一退出信号;select避免忙等;return确保函数级 cleanup 执行。但未覆盖 panic 场景下的清理。
三版:ErrGroup + 带超时的 Shutdown
使用 errgroup.Group 统一管理生命周期,并集成 Shutdown(timeout):
| 特性 | V1 | V2 | V3 |
|---|---|---|---|
| 退出可控性 | ❌ | ✅(信号) | ✅(带超时强制) |
| Panic 后清理 | ❌ | ❌ | ✅(defer + group.Wait) |
| 并发错误传播 | ❌ | ❌ | ✅(Group.Go) |
graph TD
A[Start Agent] --> B{Run Workers}
B --> C[Group.Go: worker1]
B --> D[Group.Go: worker2]
C --> E[On ctx.Done → shutdown]
D --> E
E --> F[Wait timeout → force kill]
4.3 B站上海AI平台组:大模型推理服务中并发请求批处理与GPU资源争用调优
为缓解LLM推理中GPU显存碎片化与核函数启动开销,团队采用动态批处理(Dynamic Batching)+ 请求优先级队列双策略。
批处理调度逻辑
def schedule_batch(requests: List[Request], max_tokens=8192):
# 按序列长度升序排序,提升padding效率
requests.sort(key=lambda r: r.input_len + r.max_new_tokens)
batch = []
total_tokens = 0
for req in requests:
if total_tokens + req.input_len + req.max_new_tokens <= max_tokens:
batch.append(req)
total_tokens += req.input_len + req.max_new_tokens
return batch
逻辑分析:max_tokens 控制总KV缓存容量上限;排序后贪心填充可降低平均padding率约37%(实测A100-80G)。
GPU资源争用关键参数对比
| 参数 | 默认值 | 调优值 | 效果 |
|---|---|---|---|
max_batch_size |
8 | 16 | 吞吐↑2.1×,P99延迟↑18ms |
prefill_chunk_size |
512 | 1024 | 减少kernel launch次数32% |
推理请求生命周期调度
graph TD
A[请求入队] --> B{优先级判定}
B -->|高优先级| C[立即预填充]
B -->|普通| D[等待batch fill]
C & D --> E[统一decode kernel执行]
E --> F[结果异步返回]
4.4 得物技术中台:跨机房数据同步组件中select+timeout+cancel的健壮性加固
数据同步机制
得物跨机房同步依赖长连接通道,需应对网络抖动、下游不可用、GC停顿等导致的阻塞风险。原生 select 单纯等待 channel 可能无限挂起,引入 timeout 与 cancel 构成三元控制闭环。
健壮性增强模式
- 使用
context.WithTimeout生成带截止时间的ctx select同时监听数据通道、超时通道、取消通道- 任一通道就绪即退出,避免 goroutine 泄漏
select {
case data := <-ch:
handle(data)
case <-time.After(3 * time.Second): // 防止单次操作过久
log.Warn("sync timeout, retrying...")
case <-ctx.Done(): // cancel 触发(如上级服务关闭)
return ctx.Err()
}
逻辑分析:
time.After替代time.Timer避免资源复用问题;ctx.Done()优先级最高,确保优雅终止;超时阈值需结合 P99 网络延迟动态配置(如 2~5s)。
| 场景 | 响应动作 | 恢复策略 |
|---|---|---|
| 网络瞬断( | 忽略,重试 | 指数退避重连 |
| 下游持续不可用 | 触发 cancel | 切换备用机房 |
| GC STW 导致假超时 | 误触发 timeout | 自适应延长阈值 |
graph TD
A[Sync Routine] --> B{select on ch/timeout/cancel}
B -->|data ready| C[Process & ACK]
B -->|timeout| D[Log + Retry]
B -->|ctx.Done| E[Cleanup & Exit]
第五章:你的并发能力坐标系与进阶路线图
并发能力的二维评估模型
我们将开发者在真实工程场景中的并发能力映射到「理解深度 × 实践密度」坐标系中。横轴代表对底层机制(如内存模型、锁膨胀、CAS失败重试策略)的掌握程度,纵轴反映在高负载服务(如订单履约系统QPS 8000+、库存扣减延迟@Async和ConcurrentHashMap,未验证其扩容临界点下的ABA风险)。
典型能力断层与突破路径
| 当前表现 | 根源问题 | 实战突破动作 |
|---|---|---|
CompletableFuture 链式调用偶发超时 |
忽略ForkJoinPool.commonPool()被IO任务耗尽 |
在Spring Boot启动时显式配置spring.task.execution.pool.max-size=200并绑定专用线程池 |
ReentrantLock 公平锁性能骤降40% |
未识别公平模式导致线程频繁挂起/唤醒 | 使用JFR录制锁竞争事件,将new ReentrantLock(true)替换为new ReentrantLock(false)并压测验证 |
真实生产环境决策树
flowchart TD
A[请求到达] --> B{是否涉及共享资源修改?}
B -->|是| C[检查当前操作是否满足无锁条件]
B -->|否| D[直接执行]
C -->|CAS可覆盖| E[使用AtomicStampedReference处理版本戳]
C -->|需复杂状态机| F[采用StampedLock乐观读+悲观写]
E --> G[记录CAS失败率监控告警]
F --> H[在写锁内校验读锁stamp有效性]
深度实践案例:物流轨迹服务优化
某快递公司轨迹查询接口原采用synchronized方法锁,P99延迟达1.2s。重构时分三步落地:① 用LongAdder替代AtomicLong统计各分拣中心吞吐量;② 将轨迹点存储结构从List<T>改为ChronoUnit.SECONDS分片的ConcurrentSkipListMap,使时间范围查询复杂度从O(n)降至O(log n);③ 在Netty ChannelHandler中复用ThreadLocalRandom避免SecureRandom同步开销。上线后P99降至86ms,GC Young GC频率下降62%。
工具链验证清单
- 使用Arthas
thread -n 5实时抓取TOP5阻塞线程堆栈 - 通过
jstack -l <pid> | grep -A 10 "WAITING"定位未释放的Condition等待队列 - 在Jenkins流水线中嵌入
jcstress测试用例,验证自定义无锁队列在-XX:+UseParallelGC下的ABA行为一致性
持续演进的信号指标
当团队开始常态化采集以下数据时,表明并发能力已进入高阶阶段:
Unsafe.park()调用次数与线程存活时间的比值持续低于0.3java.util.concurrent.locks.AbstractQueuedSynchronizer$Node对象在Old Gen占比稳定在2.1%±0.4%jstat -gc输出中GCTime与应用处理耗时比值维持在1:120区间
跨技术栈迁移验证法
在将Kafka消费者从KafkaConsumer线程模型迁移到Project Reactor时,必须完成三项硬性验证:① 使用BlockHound检测所有subscribeOn(Schedulers.boundedElastic())调用链路中是否存在隐式阻塞;② 对比max.poll.records=500场景下Reactor背压丢弃率与原始线程池拒绝率差异;③ 在消费者组重平衡期间注入Thread.sleep(3000)模拟网络分区,观测Flux.interval(Duration.ofSeconds(1))是否持续发射。
