第一章:Golang分布式SQL数据库架构设计全景概览
现代云原生应用对数据层提出高可用、水平扩展、强一致与低延迟的复合要求。Golang凭借其轻量协程、高效并发模型、静态编译与卓越的网络性能,成为构建分布式SQL数据库控制平面与数据节点的理想语言。本章呈现一个生产就绪的分布式SQL数据库典型架构全景,聚焦核心组件协同逻辑与关键权衡。
核心分层模型
- 接入层(Proxy):无状态Go服务,负责SQL解析、连接池管理、读写分离路由及流量限流。采用
github.com/pingcap/parser进行语法树生成,结合vitess风格的shard key路由策略。 - 协调层(Coordinator):基于Raft共识算法实现的元数据管理器,使用
etcd/raft或hashicorp/raft库;持久化集群拓扑、分片映射(Shard Map)与DDL锁状态。 - 存储层(Data Node):每个节点运行嵌入式SQL引擎(如TiKV+TiDB兼容层或自研基于B+Tree的WAL日志引擎),通过gRPC与Coordinator通信,支持多副本同步写入。
一致性保障机制
分布式事务采用两阶段提交(2PC)增强版:
- 协调者向所有参与节点发送
Prepare请求,节点本地预写WAL并返回Ready或Abort; - 若全部就绪,协调者写入
Commit Record到Raft日志,再广播Commit指令; - 任一节点失败则触发
Rollback流程,并由后台GC协程清理残留临时行。
// 示例:Prepare阶段关键逻辑(伪代码)
func (n *DataNode) Prepare(ctx context.Context, txID string) error {
if !n.wal.WritePreparedRecord(txID) { // 持久化准备状态
return errors.New("wal write failed")
}
n.txState.Store(txID, TxPrepared) // 内存状态快照
return nil
}
关键设计权衡对照表
| 维度 | 强一致性选择 | 可用性优先选择 |
|---|---|---|
| 复制协议 | 同步复制(多数派确认后返回) | 半同步复制(1主+1从ack) |
| 分片策略 | 范围分片(Range-based) | 哈希分片(Hash-based) |
| DDL执行 | 全局锁 + 分阶段灰度升级 | 在线Schema变更(Online DDL) |
该架构已在中型金融级场景验证:单集群支撑50+节点、PB级数据、99.99%读写可用性,平均跨节点查询延迟
第二章:分库分表核心策略与Go语言工程化落地
2.1 基于业务特征的水平分片模型选型与Sharding Key设计实践
选择分片模型前,需深度剖析核心业务特征:高频查询维度、写入吞吐模式、跨片关联频率及数据生命周期。
Sharding Key 设计三原则
- 高基数性:避免热点(如
user_id优于status) - 查询对齐性:90%+ 查询含该字段(如订单场景优先选
user_id而非order_time) - 不可变性:禁止后续更新(如
tenant_code合规,region易变更则风险高)
典型分片策略对比
| 策略 | 适用场景 | 扩容成本 | 查询路由复杂度 |
|---|---|---|---|
| 取模(hash) | 均匀分布、ID主键场景 | 高 | 低 |
| 范围分片 | 时间序列、归档需求强 | 中 | 中(需范围扫描) |
| 一致性哈希 | 动态节点频繁增减 | 低 | 中高 |
-- 示例:基于 user_id 的取模分片(1024 逻辑库 → 32 物理库)
SELECT * FROM order_0001
WHERE user_id % 1024 BETWEEN 0 AND 31; -- 路由至物理库0
逻辑库数(1024)远大于物理库数(32),保障扩容时仅需迁移约 3% 数据;
user_id % 1024作为路由键,确保单条记录始终映射到唯一物理库,避免跨库 JOIN。
graph TD A[用户请求] –> B{解析 Sharding Key} B –> C[计算分片槽位] C –> D[定位物理库表] D –> E[执行本地查询]
2.2 Go驱动层透明分片路由实现:从sqlparser解析到动态Router注册
透明分片路由的核心在于SQL语义理解 → 分片键提取 → 路由策略匹配 → 动态Router调用的闭环。
SQL解析与分片键识别
使用 github.com/xwb1989/sqlparser 解析原始SQL,提取 WHERE 中的等值条件:
stmt, _ := sqlparser.Parse("SELECT * FROM users WHERE user_id = 123 AND status = 'active'")
where := stmt.(*sqlparser.Select).Where
keys := extractShardKeys(where) // 返回 map[string]string{"user_id": "123"}
extractShardKeys递归遍历WHERE表达式树,仅捕获AND链中形如col = val的简单等值条件;user_id被识别为预定义分片键,触发路由决策。
动态Router注册机制
支持运行时热插拔路由策略:
| 策略名 | 触发键 | 实现类型 |
|---|---|---|
| HashRouter | user_id | uint64 % N |
| RangeRouter | order_id | 查区间映射表 |
| LookupRouter | tenant_id | Redis查表 |
graph TD
A[SQL输入] --> B[sqlparser.Parse]
B --> C{是否含分片键?}
C -->|是| D[RouterRegistry.Get(key)]
C -->|否| E[默认库路由]
D --> F[执行shardID计算]
F --> G[连接池路由分发]
路由器注册示例
RouterRegistry.Register("user_id", &HashRouter{ShardCount: 8})
// 参数说明:ShardCount=8 决定哈希模数,影响分片数量与负载均衡粒度
2.3 分布式ID生成体系:Snowflake、Leaf-segment与TiDB-UUID在Go服务中的协同部署
在高并发微服务场景中,单一ID生成策略易成瓶颈。实践中常采用分层协同模式:核心业务用 Snowflake 保障低延迟与有序性;订单/日志类场景接入 Leaf-segment 减少 DB 频繁请求;TiDB-UUID 则用于跨集群元数据标识,规避时钟回拨风险。
ID路由策略
- Snowflake:
workerID从 Kubernetes Pod 标签动态注入 - Leaf-segment:预加载
step=1000,双 buffer 自动切换 - TiDB-UUID:调用
SELECT UUID()或SELECT SLEEP(0)触发全局唯一生成
性能对比(TPS,单节点)
| 方案 | 吞吐量 | 时序性 | 依赖项 |
|---|---|---|---|
| Snowflake | 120K | 强 | NTP 时钟同步 |
| Leaf-segment | 85K | 弱 | MySQL 主库 |
| TiDB-UUID | 42K | 无 | TiDB 集群健康 |
// Snowflake 初始化(含时钟校验)
func NewIDGenerator(workerID int64) *snowflake.Node {
n, _ := snowflake.NewNode(workerID)
// 强制校验系统时钟偏移 < 10ms,防回拨
if abs(time.Since(time.Now())) > 10*time.Millisecond {
panic("system clock skew too large")
}
return n
}
该初始化确保节点启动时完成时钟可信度检查,workerID 由服务注册中心统一分配,避免 ID 冲突。snowflake.Node 内部采用原子计数器+时间戳拼接,毫秒级精度下每毫秒可生成 4096 个唯一ID。
2.4 跨分片查询优化:Go协程编排的并行扫描+MergeSort聚合实战
在分片数据库中,跨分片 ORDER BY ... LIMIT N 查询天然面临数据分散、全局有序难保障的问题。传统串行拉取再排序导致高延迟与单点瓶颈。
并行扫描设计原则
- 每分片独立执行带
LIMIT N的本地有序扫描(利用索引) - 协程池控制并发度(避免连接数爆炸)
- 各分片返回流式结果(
chan *Row),非全量加载
Go协程编排核心逻辑
func parallelMergeScan(shards []Shard, limit int) []*Row {
var wg sync.WaitGroup
chans := make([]<-chan *Row, len(shards))
for i, s := range shards {
wg.Add(1)
ch := make(chan *Row, limit) // 缓冲区防阻塞
chans[i] = ch
go func(shard Shard, out chan<- *Row) {
defer close(out)
defer wg.Done()
for row := range shard.ScanSorted(limit) {
out <- row
}
}(s, ch)
}
wg.Wait()
return mergeSortChannels(chans, limit) // 基于最小堆的多路归并
}
逻辑分析:
chans[i]接收各分片有序子流;mergeSortChannels使用heap.Interface实现 N 路归并,时间复杂度 O(L·log K),K 为分片数,L 为最终返回行数。缓冲通道容量设为limit,既保响应性又防内存溢出。
性能对比(10分片,TOP-100 查询)
| 方案 | P95延迟 | 内存峰值 | 网络传输量 |
|---|---|---|---|
| 串行拉取+内存排序 | 1.2s | 480MB | 1.1GB |
| 并行扫描+MergeSort | 186ms | 42MB | 12.3MB |
graph TD
A[发起跨分片TOP-N查询] --> B[启动N个goroutine]
B --> C[每goroutine连接对应shard]
C --> D[执行本地索引扫描+LIMIT N]
D --> E[流式写入channel]
E --> F[最小堆归并N路有序流]
F --> G[输出全局TOP-N结果]
2.5 分库分表灰度迁移方案:基于Go中间件的双写比对与流量镜像验证
核心架构设计
采用「双写+比对+镜像」三阶段渐进式迁移:
- 双写层:业务请求同步写入旧库(单库)与新库(分片集群)
- 比对层:异步校验双库记录一致性,差异常量告警
- 镜像层:1% 生产流量复制至新库,不参与主链路响应
数据同步机制
// 双写中间件核心逻辑(简化)
func DualWriteMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. 原始请求处理(旧库)
oldRes := captureResponse(r, func() { next.ServeHTTP(w, r) })
// 2. 镜像请求转发(新库,非阻塞)
go mirrorToShardedDB(r.Clone(context.Background()))
// 3. 写入比对任务队列(含traceID、SQL、参数哈希)
auditQueue.Push(&CompareTask{
TraceID: r.Header.Get("X-Trace-ID"),
SQL: extractSQL(r),
Params: hashParams(r),
})
})
}
逻辑说明:
mirrorToShardedDB使用context.WithTimeout(ctx, 200ms)防止拖慢主链路;hashParams对请求参数做 SHA256 摘要,用于跨库幂等比对;auditQueue为内存队列+持久化落盘,保障比对不丢。
迁移验证能力对比
| 能力 | 双写比对 | 流量镜像 | 混合验证 |
|---|---|---|---|
| 实时性 | 秒级延迟 | 毫秒级(异步) | ✅ 组合最优 |
| 数据一致性覆盖率 | 100% 写路径 | 全覆盖+抽样验证 | |
| 对业务RT影响 | +15~20ms | ≈0ms | 可控在+5ms内 |
graph TD
A[用户请求] --> B{灰度开关}
B -->|开启| C[双写旧库+新库]
B -->|关闭| D[仅写旧库]
C --> E[异步比对服务]
C --> F[镜像流量分析器]
E --> G[差异告警/自动修复]
F --> H[新库性能画像]
第三章:分布式事务一致性保障机制
3.1 TCC模式在Go微服务中的状态机建模与Saga补偿链路编排
TCC(Try-Confirm-Cancel)本质是三阶段状态驱动的分布式事务协议,需显式建模 TRY → CONFIRM/FAIL → CANCEL 的有限状态迁移。
状态机核心结构
type TCCState uint8
const (
StateIdle TCCState = iota
StateTrySucceeded
StateConfirmed
StateCancelled
StateTryFailed
)
// 状态迁移必须满足幂等性与可逆性约束
该枚举定义了原子操作的合法生命周期;StateTrySucceeded 是唯一可向 Confirmed 或 Cancelled 迁移的中间态,保障事务边界清晰。
Saga补偿链路编排策略
| 阶段 | 触发条件 | 补偿动作语义 |
|---|---|---|
| Try | 业务预占资源 | 预留库存、冻结额度 |
| Confirm | 全局事务提交成功 | 实际扣减、生成订单 |
| Cancel | 任一Try失败或超时 | 释放预留、回滚冻结 |
补偿执行流程
graph TD
A[Try: createOrder] --> B{Success?}
B -->|Yes| C[Confirm: deductInventory]
B -->|No| D[Cancel: rollbackOrder]
C --> E{All Confirms OK?}
E -->|Yes| F[Global Commit]
E -->|No| G[Trigger Cancel Chain]
3.2 基于Percolator模型的2PC增强:Go版TiKV客户端事务封装与冲突检测优化
TiKV 客户端在 Go 生态中对 Percolator 协议进行了深度封装,将两阶段提交(2PC)与乐观并发控制(OCC)融合,显著降低写写冲突重试开销。
冲突检测前置优化
客户端在 PreWrite 阶段主动探测 key 的最新 start_ts,若发现更高时间戳的未提交事务,则直接中止本地事务,避免无效锁持有。
Go 客户端关键封装逻辑
func (t *txn) Commit() error {
// 检测写集是否被更高 start_ts 的事务预写(基于 Lock CF 中的 lock + ts 元数据)
if err := t.detectConflicts(); err != nil {
return errors.Wrap(err, "conflict detected before commit")
}
return t.twoPhaseCommit()
}
detectConflicts() 通过批量 Get Lock CF 中对应 key 的锁元数据,并比对 lock.ts < t.startTS 是否成立;若存在 lock.ts > t.startTS 且未 resolve,则判定为潜在冲突。
性能对比(10K QPS 下平均延迟)
| 场景 | 原生 Percolator | 增强版(冲突前置检测) |
|---|---|---|
| 平均 P99 延迟(ms) | 42.6 | 18.3 |
| 重试率 | 12.7% | 2.1% |
graph TD
A[Begin Txn] --> B[Read with Snapshot]
B --> C[Write Buffer]
C --> D{PreWrite + Conflict Check}
D -- No conflict --> E[Commit Primary]
D -- Conflict --> F[Abort & Retry]
E --> G[Commit Secondaries]
3.3 最终一致性保障:Go Worker池驱动的可靠消息+本地事务表(LTX)落地方案
核心设计思想
以「本地事务优先、异步补偿兜底」为原则,将业务变更与消息写入原子化落库,再由固定规模的 Go Worker 池异步投递。
数据同步机制
// LTX 表结构(含状态机)
type LocalTxRecord struct {
ID int64 `gorm:"primaryKey"`
Topic string `gorm:"index"` // 目标MQ主题
Payload []byte `gorm:"type:json"`
Status string `gorm:"default:'pending';index"` // pending → sending → sent → confirmed
CreatedAt time.Time `gorm:"index"`
}
逻辑分析:Status 字段驱动状态跃迁;CreatedAt 支持按时间窗口批量扫描;Payload 存储序列化后消息体,避免跨服务耦合。
Worker 池调度流程
graph TD
A[定时扫描 pending 记录] --> B{并发Worker取锁}
B --> C[更新 status=sending]
C --> D[调用MQ Producer]
D --> E{成功?}
E -->|是| F[update status=sent]
E -->|否| G[回退 status=pending + retry_count++]
关键参数对照表
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Worker 并发数 | 8–16 | 平衡吞吐与MQ连接压力 |
| 单次扫描上限 | 100 | 防止长事务阻塞 |
| 最大重试次数 | 3 | 避免死循环,触发告警通道 |
第四章:高并发场景下的稳定性与可观测性建设
4.1 连接池治理:go-sql-driver/mysql连接复用、超时熔断与分库级QPS限流实战
连接复用:避免高频建连开销
go-sql-driver/mysql 默认启用连接复用,但需显式配置 SetMaxOpenConns 与 SetMaxIdleConns:
db.SetMaxOpenConns(50) // 全局最大并发连接数
db.SetMaxIdleConns(20) // 空闲连接池上限
db.SetConnMaxLifetime(30 * time.Minute) // 连接最大存活时间,防长连接老化
逻辑分析:
MaxOpenConns控制并发上限,防止数据库被打满;MaxIdleConns避免空闲连接堆积导致资源泄漏;ConnMaxLifetime强制连接轮换,规避 MySQL 的wait_timeout自动断连风险。
超时熔断:三层超时协同防御
| 超时类型 | 推荐值 | 作用域 |
|---|---|---|
timeout |
3s | TCP 建连阶段 |
readTimeout |
5s | 查询结果读取阶段 |
writeTimeout |
5s | SQL 发送写入阶段 |
分库级 QPS 限流:基于 golang.org/x/time/rate 动态拦截
var dbLimiter = map[string]*rate.Limiter{
"shard_001": rate.NewLimiter(rate.Every(100*time.Millisecond), 10), // 10 QPS
"shard_002": rate.NewLimiter(rate.Every(100*time.Millisecond), 8),
}
逻辑分析:按
DSN或database name绑定独立限流器,实现分库粒度的精准控速,避免单库过载影响全局。
4.2 分布式SQL执行链路追踪:OpenTelemetry + Jaeger在Go Query Planner中的深度集成
在分布式查询规划器中,SQL执行链路由解析、优化、分片、调度到跨节点执行构成多跳调用。为精准定位延迟瓶颈,需将 OpenTelemetry SDK 植入 Query Planner 的关键阶段。
链路注入点设计
ParseSpan:SQL文本解析时创建根 SpanOptimizeSpan:逻辑/物理计划生成阶段嵌套子 SpanDispatchSpan:每个 shard target 创建独立 child Span
Go 初始化示例
import "go.opentelemetry.io/otel/sdk/trace"
func initTracer() *trace.TracerProvider {
exp, _ := jaeger.New(jaeger.WithCollectorEndpoint(
jaeger.WithEndpoint("http://jaeger:14268/api/traces"),
))
tp := trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithResource(resource.MustNewSchema1(
semconv.ServiceNameKey.String("go-query-planner"),
)),
)
otel.SetTracerProvider(tp)
return tp
}
该代码初始化 OpenTelemetry TracerProvider,对接 Jaeger Collector;WithCollectorEndpoint 指定接收地址,WithResource 注入服务元数据,确保链路在 Jaeger UI 中可按服务名过滤。
跨 Span 上下文传播
| 阶段 | Context 传递方式 | 是否注入 baggage |
|---|---|---|
| Parser → Optimizer | propagators.TraceContext{} .Inject() |
✅(携带 query_id) |
| Optimizer → Dispatcher | tracer.Start(ctx, "dispatch") |
✅(附加 shard_key) |
graph TD
A[SQL Parse] -->|ctx with SpanID| B[Logical Optimize]
B -->|child span| C[Physical Plan Gen]
C -->|fan-out ctx| D[Shard 0 Dispatch]
C -->|fan-out ctx| E[Shard 1 Dispatch]
4.3 热点数据自动发现与读写分离调度:基于eBPF+Go Agent的实时负载感知系统
系统通过 eBPF 程序在内核态无侵入采集 Redis/MySQL 的键级访问频次与延迟分布,Go Agent 每秒聚合生成热点指纹(如 key: user:10023:profile, freq: 87/s, p99lat: 14.2ms)。
数据同步机制
Go Agent 采用带权重的滑动窗口算法动态更新热点列表,避免抖动:
// 热点评分 = 频次 × (1 + log10(1 + p99lat/5)),lat 单位 ms
score := float64(freq) * (1 + math.Log10(1+float64(p99Lat)/5))
逻辑说明:
p99Lat/5将延迟归一化为轻量惩罚因子;log10抑制高延迟的过度放大;整体保留频次主导性。
调度决策流程
graph TD
A[eBPF trace: key + op + latency] --> B[Go Agent 实时聚合]
B --> C{是否进入Top-100?}
C -->|是| D[写请求路由至主库]
C -->|否| E[读请求分流至只读副本]
热点识别阈值配置
| 指标 | 默认值 | 说明 |
|---|---|---|
| 最小访问频次 | 20/s | 过滤噪声访问 |
| 持续时间窗 | 30s | 确保热点具备稳定性 |
| 副本读取比例 | 75% | 热点非命中时的默认读策略 |
4.4 多副本一致性校验:Go定时任务驱动的跨节点checksum比对与自动修复流程
核心设计思想
以轻量、幂等、可观测为原则,通过分布式定时任务触发多节点数据快照比对,避免中心化校验瓶颈。
校验流程概览
graph TD
A[定时器触发] --> B[并发拉取各节点/tables/{table}/checksum]
B --> C[聚合比对MD5/SHA256]
C --> D{是否一致?}
D -->|否| E[定位差异节点→触发增量同步]
D -->|是| F[记录健康状态]
关键代码片段
// 每30秒执行一次跨节点校验(可配置)
func startConsistencyJob() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
nodes := []string{"node-a", "node-b", "node-c"}
checksums := make(map[string]string)
for _, node := range nodes {
chk, _ := fetchChecksum(node, "user_profiles") // HTTP GET /api/v1/checksum?table=user_profiles
checksums[node] = chk
}
if !allEqual(checksums) {
triggerRepair(checksums) // 自动选择多数派为基准源
}
}
}
fetchChecksum通过HTTP调用各节点暴露的校验端点,返回表级CRC64摘要;triggerRepair基于Raft多数派原则选取权威副本,仅推送diff块而非全量重传,降低带宽开销。
修复策略对比
| 策略 | 修复粒度 | 冲突处理 | 适用场景 |
|---|---|---|---|
| 全量覆盖 | 表级 | 强制覆盖 | 小表、低频变更 |
| 增量块同步 | 主键范围 | 基于TS冲突检测 | 中大型生产表 |
| WAL回放修复 | 日志位点 | 幂等Apply | 高一致性要求场景 |
第五章:架构演进路径与未来技术展望
从单体到服务网格的渐进式重构实践
某大型保险科技平台在2019年启动架构升级,初始系统为Java Spring Boot单体应用(约280万行代码),部署于VMware虚拟机集群。团队采用“绞杀者模式”分阶段拆分:首先将保全服务独立为Spring Cloud微服务,通过API网关路由;2021年引入Istio 1.10构建服务网格,将熔断、限流、mTLS等能力下沉至Sidecar,运维配置项减少63%;2023年完成核心交易链路迁移至eBPF增强型Envoy代理,P99延迟从420ms降至87ms。关键决策点在于保留原有Dubbo注册中心作为过渡层,避免业务方一次性改造。
云原生可观测性体系落地细节
该平台构建三级可观测性栈:
- 日志层:Filebeat采集容器stdout → Kafka → Loki(按租户+环境+服务名分片)
- 指标层:Prometheus联邦集群(3个区域集群+1个全局聚合节点),自定义Exporter覆盖核保规则引擎CPU周期计数器
- 链路层:Jaeger后端替换为Tempo,TraceID注入策略强制要求HTTP Header
X-Biz-Trace-ID透传,失败率从12.7%降至0.3%
# Istio VirtualService 路由示例(生产环境灰度发布)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: policy-service
spec:
hosts:
- policy.api.insurance.com
http:
- match:
- headers:
x-deployment-tag:
exact: v2.3.1-canary
route:
- destination:
host: policy-service
subset: canary
weight: 15
- route:
- destination:
host: policy-service
subset: stable
weight: 85
边缘智能与实时决策融合架构
在车险UBI(基于使用的保险)场景中,车载OBD设备每3秒上传GPS+加速度原始数据(平均2.1KB/包)。传统架构经MQTT→K8s Ingress→Flink集群处理,端到端延迟达8.2秒。2023年重构为边缘-云协同架构:
- 边缘层:NVIDIA Jetson AGX Orin部署TensorRT优化的LSTM模型,实时识别急刹/变道行为(本地响应
- 云边协同:通过KubeEdge实现模型版本自动同步,当边缘检测置信度
- 数据治理:边缘侧采用Apache Arrow内存格式压缩,网络带宽占用降低41%
多模态AI工程化集成路径
| 理赔图像识别系统经历三次架构迭代: | 阶段 | 技术栈 | 单图处理耗时 | 人工复核率 |
|---|---|---|---|---|
| 2020 | OpenCV+传统特征 | 3.2s | 38.5% | |
| 2021 | PyTorch+ResNet50 | 1.8s | 22.1% | |
| 2024 | ONNX Runtime+ViT-L/16+CLIP文本对齐 | 420ms | 9.7% |
关键突破在于构建统一推理服务框架:所有模型通过ONNX中间表示封装,Kubernetes CRD定义InferenceService资源,支持GPU/CPU/TPU异构调度。
安全左移的基础设施即代码实践
使用Terraform 1.5+OpenPolicyAgent构建合规验证流水线:
- 每次云资源配置变更前执行OPA策略检查(如
aws_s3_bucket必须启用SSE-KMS且禁用public ACL) - AWS IAM策略自动生成工具基于最小权限原则解析K8s ServiceAccount绑定关系
- 生产环境EC2实例启动时自动注入eBPF安全模块,实时拦截
execve调用中的危险参数组合
该平台已实现PCI-DSS 4.1条款自动化审计覆盖率100%,漏洞修复平均时效从72小时压缩至4.3小时。
