Posted in

Gin+Redis+PostgreSQL实战项目打包交付:苏州某金融科技公司Golang岗终面压轴题(含可运行代码+性能压测报告)

第一章:Gin+Redis+PostgreSQL实战项目打包交付:苏州某金融科技公司Golang岗终面压轴题(含可运行代码+性能压测报告)

本章完整复现苏州某头部金融科技公司后端岗位终面真实压轴题:构建高并发用户资产查询服务,要求同时满足低延迟响应、缓存一致性与事务安全,并完成容器化交付与压测验证。

项目架构与依赖配置

使用 Go 1.21+、Gin v1.9.1、pgx/v5(PostgreSQL)、redis-go/v9。go.mod 关键依赖如下:

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/jackc/pgx/v5 v5.4.2
    github.com/redis/go-redis/v9 v9.0.5
)

核心服务启动逻辑

main.go 中初始化连接池并注册中间件:

func main() {
    r := gin.Default()
    // PostgreSQL 连接池(最大连接数30,空闲超时5分钟)
    pgPool, _ := pgxpool.New(context.Background(), os.Getenv("PG_DSN"))
    // Redis 客户端(启用连接池与自动重连)
    redisClient := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")})
    r.Use(middleware.RedisCache(redisClient)) // 缓存中间件
    r.GET("/assets/:user_id", handlers.GetAssets(pgPool, redisClient))
    r.Run(":8080")
}

数据库与缓存协同策略

  • PostgreSQL 表 user_assets 使用 user_id 主键 + updated_at 时间戳;
  • 查询流程:先查 Redis(key=asset:{user_id}),未命中则查 DB 并写入缓存(TTL=30s);
  • 更新操作采用「先删缓存,再更新 DB」双写策略,避免脏读;

容器化交付清单

文件 作用
Dockerfile 多阶段构建,最终镜像
docker-compose.yml 启动 PostgreSQL、Redis、应用三服务
loadtest.sh 内置 wrk 压测脚本(1000并发,持续60秒)

性能压测关键结果

在 4C8G 阿里云 ECS 上实测:

  • 平均响应时间:28.3ms(P99
  • QPS:3240(Redis 缓存命中率 87.2%)
  • 错误率:0.0%(无连接超时或事务冲突)
    压测命令:wrk -t4 -c1000 -d60s http://localhost:8080/assets/12345

第二章:高并发金融场景下的架构设计与技术选型

2.1 Gin框架核心机制解析与中间件链式调用实践

Gin 的核心基于 EngineHandlerFunc 构建,请求生命周期由 ServeHTTP 触发,经路由匹配后进入中间件链(HandlersChain)。

中间件执行模型

Gin 采用洋葱模型:每个中间件可前置处理、调用 c.Next() 继续链路、再执行后置逻辑。

func authMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
            return // 阻断后续执行
        }
        c.Next() // 进入下一中间件或最终 handler
    }
}

c.Next() 是关键控制点:它暂停当前中间件,移交控制权至链中下一个函数;返回后继续执行后续语句,实现“环绕式”逻辑。

中间件注册顺序决定执行时序

注册顺序 执行阶段 说明
logger 全链首尾 记录请求开始与结束时间
auth 路由前校验 验证身份,失败则 Abort
recovery panic 捕获 始终置于链末保障稳定性
graph TD
    A[HTTP Request] --> B[Logger: Start]
    B --> C[Auth: Validate Token]
    C --> D[Recovery: Panic Guard]
    D --> E[Route Handler]
    E --> D
    D --> C
    C --> B
    B --> F[HTTP Response]

2.2 Redis在交易幂等性与缓存穿透防护中的工程化落地

幂等令牌校验机制

交易请求携带唯一idempotency-key,Redis以SET key value EX 300 NX原子写入(5分钟有效期)。NX确保首次请求成功,重复请求返回空,避免重复扣款。

# 幂等校验原子操作(Python + redis-py)
def check_idempotent(key: str) -> bool:
    return redis_client.set(
        name=f"idemp:{key}",
        value="1",
        ex=300,      # 过期时间:秒
        nx=True      # 仅当key不存在时设置
    )

逻辑分析:SET ... NX EX规避了先查后设的竞态;ex=300适配金融类业务最长处理窗口;key前缀idemp:便于集群扫描清理。

缓存穿透双保险策略

  • 空值缓存:对DB查无结果的key,写入cache:sku:123 → "null"并设短TTL(60s)
  • 布隆过滤器前置:初始化加载全量有效ID,拦截99.9%非法ID请求
防护层 响应延迟 覆盖率 维护成本
布隆过滤器 99.9% 中(需定期重建)
空值缓存 ~0.5ms 100% 低(自动过期)
graph TD
    A[客户端请求] --> B{布隆过滤器校验}
    B -->|存在| C[查Redis缓存]
    B -->|不存在| D[直接返回404]
    C -->|命中| E[返回数据]
    C -->|未命中| F[查DB]
    F -->|有数据| G[回填Redis]
    F -->|无数据| H[写空值+TTL]

2.3 PostgreSQL事务隔离级别与金融级数据一致性保障方案

PostgreSQL 提供四种标准隔离级别,金融场景需在可串行化(Serializable)与性能间精细权衡:

隔离级别 脏读 不可重复读 幻读 写偏移 实现机制
Read Uncommitted ❌(等同 Read Committed) 快照 + 行版本
Read Committed 每语句快照
Repeatable Read 事务级快照
Serializable SSI(可串行化快照隔离)
-- 启用强一致性保障(推荐金融核心账务)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 若检测到冲突,PostgreSQL 自动中止并报错:serialization_failure

此代码启用 SSI 机制:PostgreSQL 在提交前执行冲突图分析,通过 pg_stat_database.conflicts 监控写偏移事件。参数 default_transaction_isolation = 'serializable' 可全局启用,配合 synchronous_commit = on 确保 WAL 持久化。

数据同步机制

金融系统常结合逻辑复制 + 应用层幂等校验,构建跨库最终一致防线。

2.4 多层缓存协同策略:本地缓存+Redis+数据库读写分离实操

构建高并发读场景下的低延迟数据访问链路,需分层拦截请求:本地缓存(Caffeine)抗热点、Redis 承接中频访问、主从分离的数据库兜底写与最终一致性读。

缓存层级职责划分

  • 本地缓存:毫秒级响应,TTL=60s,最大容量10k,仅存储强热点(如商品基础信息)
  • Redis:分布式共享,设置逻辑过期(expire_at 字段),避免雪崩
  • 数据库:主库写 + 从库只读,读写分离中间件自动路由

数据同步机制

// Caffeine + Redis 双写一致(先更新DB,再删Redis,最后刷新本地缓存)
public void updateProduct(Long id, Product newProd) {
    productMapper.updateById(newProd);           // ① 强一致写主库
    redisTemplate.delete("prod:" + id);         // ② 删除Redis缓存(防脏读)
    caffeineCache.invalidate(id);               // ③ 清空本地缓存
}

逻辑说明:采用「更新DB → 删除缓存」策略,规避双写时序难题;caffeineCache.invalidate() 触发下次查询自动重建,降低缓存穿透风险;Redis 删除操作异步化可进一步解耦(此处为简化演示)。

读取流程时序(Mermaid)

graph TD
    A[客户端请求] --> B{本地缓存命中?}
    B -->|是| C[返回结果]
    B -->|否| D[查Redis]
    D -->|命中| C
    D -->|未命中| E[查从库]
    E --> F[写入Redis + 本地缓存]
    F --> C
层级 平均RT 容量上限 一致性模型
Caffeine 10K items 最终一致(TTL)
Redis ~5ms TB级 逻辑过期控制
MySQL从库 ~20ms 无硬限 主从延迟≤500ms

2.5 微服务边界划分与单体演进路径:从MVP到生产就绪的架构演进

微服务边界不应由技术栈定义,而应源于业务能力的内聚性与变化频率。初期MVP常以单体启动,关键在于识别“可独立部署、拥有专属数据模型、变更节奏显著不同”的业务域。

边界识别三原则

  • ✅ 领域驱动设计(DDD)中的限界上下文
  • ✅ 数据所有权明确(无共享数据库表)
  • ❌ 避免按技术分层(如“所有API服务”划为一个服务)

演进典型阶段

阶段 特征 数据耦合度
MVP单体 共享数据库,模块间直调用
服务拆分期 API网关+数据库私有化
生产就绪 异步事件驱动+最终一致性
# 事件发布示例:订单创建后解耦通知
def publish_order_created_event(order_id: str, customer_id: str):
    # 使用Kafka而非直接调用用户服务,避免同步依赖
    event = {
        "type": "OrderCreated",
        "data": {"order_id": order_id, "customer_id": customer_id},
        "timestamp": time.time()
    }
    kafka_producer.send("order-events", value=event)

逻辑分析:order-events主题作为契约载体,消费者(如积分服务、通知服务)可异步订阅,实现物理隔离;customer_id字段满足下游查询需求,避免反向查单体用户库;时间戳支持幂等与重放控制。

graph TD
    A[MVP单体] -->|逐步提取| B[用户服务]
    A -->|事件驱动剥离| C[订单服务]
    B -->|异步事件| D[积分服务]
    C -->|异步事件| D

第三章:可交付制品构建与DevOps流水线集成

3.1 Go Module依赖管理与vendor锁定在金融合规环境中的应用

金融系统要求所有第三方依赖可审计、可复现、不可篡改。go mod vendor 结合 go.sum 校验机制,构成符合等保2.0与PCI DSS的依赖锁定基础。

vendor目录的合规性加固

# 启用只读vendor模式,防止意外修改
go mod vendor && chmod -R a-w vendor/

该命令生成确定性 vendor 目录,并禁写权限,满足“配置不可变”审计项;go build -mod=vendor 强制仅从 vendor 构建,绕过 GOPROXY 风险。

依赖溯源与版本冻结策略

  • 所有 require 必须指定精确语义版本(禁止 +incompatible
  • 每次 go mod tidy 后需提交 go.modgo.sumvendor/ 三者原子快照
  • CI 流程中校验 go.sum 签名与内部私有仓库哈希白名单
审计维度 合规依据 检查方式
依赖完整性 GB/T 22239-2019 go list -m -f '{{.Path}}:{{.Version}}' all
无恶意远程拉取 PCI DSS 6.2 GO111MODULE=on go build -mod=vendor 失败即阻断
graph TD
    A[开发提交 go.mod/go.sum] --> B[CI 拉取 vendor 并校验 go.sum]
    B --> C{哈希匹配内部白名单?}
    C -->|是| D[构建并签名二进制]
    C -->|否| E[立即终止流水线]

3.2 Docker多阶段构建与最小化镜像安全加固(含CVE扫描结果嵌入)

Docker多阶段构建通过分离构建环境与运行时环境,显著缩减最终镜像体积并移除编译依赖。以下是一个典型Go应用的双阶段构建示例:

# 构建阶段:完整工具链
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o /usr/local/bin/app .

# 运行阶段:仅含可执行文件的极简基础镜像
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=builder /usr/local/bin/app /usr/local/bin/app
CMD ["/usr/local/bin/app"]

逻辑分析:--from=builder 实现跨阶段复制,CGO_ENABLED=0 确保静态链接避免libc依赖;alpine:3.20 基础镜像仅约7MB,相比golang:1.22(~900MB)大幅削减攻击面。

为实现CVE扫描结果嵌入,可在CI流程中集成Trivy并生成JSON报告:

工具 扫描模式 输出嵌入方式
Trivy --format json COPY trivy-report.json /opt/scan/
Syft SBOM生成 与Trivy联合验证漏洞上下文
graph TD
    A[源码] --> B[Build Stage]
    B --> C[静态二进制]
    C --> D[Alpine Runtime]
    D --> E[Trivy Scan]
    E --> F[JSON报告注入镜像]

3.3 Kubernetes Helm Chart封装与苏州本地IDC部署适配(含ServiceMesh灰度配置)

Helm Chart结构设计原则

遵循“环境无关逻辑 + 场景化值覆盖”范式,将核心模板(deployment.yamlservice.yaml)与苏州IDC专属配置(如values-suzhou.yaml)分离,支持helm install -f values-suzhou.yaml一键适配。

ServiceMesh灰度路由关键配置

# templates/virtualservice.yaml(Istio)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
spec:
  hosts: ["api.example.local"]
  http:
  - route:
    - destination:
        host: api-service
        subset: stable
      weight: 90
    - destination:
        host: api-service
        subset: canary
      weight: 10

逻辑分析:通过subset引用DestinationRule中定义的标签选择器(如version: v1.2),实现基于权重的灰度流量分发;weight参数需为整数且总和为100,避免Istio校验失败。

苏州IDC适配要点清单

  • 使用nodeSelector绑定苏州机房物理节点标签(region: suzhou-dc
  • 配置hostPath持久卷指向本地高性能存储路径 /data/k8s-pv
  • 启用externalTrafficPolicy: Local保留源IP,适配IDC防火墙策略
组件 苏州IDC值 生产通用值
ingress.host api.suzhou.internal api.example.com
mesh.enabled true false

第四章:全链路压测验证与生产级SLA保障体系

4.1 基于k6的金融交易链路建模:订单创建→风控校验→资金扣减→消息投递

为精准复现高并发金融交易场景,k6脚本需严格遵循业务时序与依赖约束:

核心链路建模逻辑

// 模拟完整交易链路(含错误传播与重试)
export default function () {
  const orderId = __ENV.ORDER_PREFIX + Math.floor(Math.random() * 1e6);

  // 1. 创建订单
  let res = http.post('https://api.pay/order', JSON.stringify({ orderId, amount: 99.9 }));
  check(res, { 'order created': (r) => r.status === 201 });

  // 2. 同步风控校验(强依赖)
  res = http.get(`https://api.risk/verify?orderId=${orderId}`);
  check(res, { 'risk passed': (r) => r.json().result === 'ALLOW' });

  // 3. 资金扣减(幂等+超时控制)
  res = http.post('https://api.account/deduct', 
    JSON.stringify({ orderId, amount: 99.9 }), 
    { timeout: '5s' }
  );

  // 4. 异步消息投递(带traceId透传)
  http.post('https://mq/api/publish', 
    JSON.stringify({ topic: 'order_paid', payload: { orderId }, traceId: res.headers['X-Trace-ID'] })
  );
}

该脚本通过check()显式校验每环节状态码与业务字段,确保链路完整性;timeout参数防止资金服务阻塞拖垮整条链路;X-Trace-ID实现全链路可观测性对齐。

关键依赖关系

环节 依赖前序 超时(s) 失败策略
订单创建 3 终止链路
风控校验 订单创建 2 重试2次
资金扣减 风控结果 5 终止+告警
消息投递 资金结果 1 异步补偿触发

全链路执行时序

graph TD
  A[订单创建] --> B[风控校验]
  B --> C[资金扣减]
  C --> D[消息投递]
  B -.->|拒绝| E[链路中断]
  C -.->|失败| E

4.2 Redis连接池泄漏检测与PG连接数瓶颈定位(pprof+trace双维度分析)

pprof火焰图定位 Goroutine 泄漏

通过 go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine?debug=2 启动可视化分析,发现大量 redis.(*Conn).readLoop 持续阻塞——指向未关闭的连接未归还至池。

trace 时间线交叉验证

启用 runtime/trace 后,在 Goroutines 视图中观察到 redis.Dial 调用后无对应 Close()Put(),证实连接未释放。

关键诊断代码片段

// 初始化带监控的 Redis 连接池
pool := &redis.Pool{
    MaxIdle:     16,
    MaxActive:   32,
    Wait:        true,
    IdleTimeout: 30 * time.Second,
    Dial: func() (redis.Conn, error) {
        c, err := redis.Dial("tcp", "localhost:6379")
        if err != nil {
            return nil, err
        }
        // 注入 trace span 标签便于关联分析
        trace.FromContext(c.Context()).Annotate("redis.dial", "success")
        return c, nil
    },
    TestOnBorrow: func(c redis.Conn, t time.Time) error {
        _, err := c.Do("PING")
        return err
    },
}

该配置启用了空闲连接健康检查与上下文追踪注入,使 pprof 中的 goroutine 可关联至具体 trace span ID,实现跨维度下钻。

PostgreSQL 连接瓶颈对比表

指标 Redis Pool PG Conn Pool
默认 MaxOpenConns 0(无限制) 0(需显式设)
连接复用机制 显式 Put() 自动 Close()
常见泄漏诱因 忘记 Put() defer db.Close() 缺失

定位流程图

graph TD
A[pprof 发现高驻留 goroutine] --> B{是否关联 trace span?}
B -->|是| C[定位到未 Put 的 Conn]
B -->|否| D[检查 TestOnBorrow 失败率]
C --> E[修复:defer pool.Put(conn)]

4.3 混沌工程注入:模拟网络分区、Redis主从切换、PG只读副本延迟故障

混沌实验需精准靶向关键依赖路径。以下为典型故障注入策略:

Redis主从切换模拟

使用 redis-cli 强制触发故障转移:

# 向当前主节点发送FAILOVER命令(需在从节点执行)
redis-cli -h redis-slave-01 -p 6379 CLUSTER FAILOVER
# 或通过Sentinel手动触发
redis-cli -h sentinel-01 -p 26379 SENTINEL failover mymaster

该操作触发Sentinel仲裁流程,强制提升从节点为主,验证客户端重连与连接池自动恢复能力;mymaster 为监控的主服务别名,需预先配置。

PostgreSQL只读副本延迟注入

通过调整复制参数制造可控延迟:

-- 在从库执行,人为增大wal_retrieve_delay(毫秒级阻塞)
ALTER SYSTEM SET wal_retrieve_delay = '5000';
SELECT pg_reload_conf();

配合监控指标 pg_stat_replication.sync_statepg_last_wal_receive_lsn() 差值,量化延迟程度。

故障类型 注入工具 观测指标
网络分区 tc netem 连接超时、gRPC状态码503
Redis主从切换 redis-cli INFO replication 中 role 变更
PG副本延迟 ALTER SYSTEM pg_stat_replication.replay_lag

graph TD
A[发起注入] –> B{目标组件}
B –> C[Redis: Sentinel failover]
B –> D[PG: wal_retrieve_delay]
B –> E[网络: tc netem loss/delay]
C –> F[验证客户端自动重连]
D –> G[校验查询一致性窗口]
E –> H[检测熔断与降级生效]

4.4 Prometheus+Grafana监控看板搭建:QPS/TP99/缓存命中率/DB锁等待时长四维联动告警

四维指标协同建模逻辑

QPS反映瞬时负载压力,TP99刻画尾部延迟风险,缓存命中率揭示数据局部性衰减,DB锁等待时长暴露事务竞争瓶颈——四者构成服务健康度的正交评估面。

Prometheus指标采集配置

# prometheus.yml 片段:关键 exporter 聚合
scrape_configs:
  - job_name: 'app-metrics'
    static_configs:
      - targets: ['localhost:8080']  # 应用暴露 /metrics
  - job_name: 'redis-exporter'
    static_configs:
      - targets: ['redis-exporter:9121']
  - job_name: 'mysqld-exporter'
    static_configs:
      - targets: ['mysqld-exporter:9104']

通过多源 exporter 统一拉取,确保 QPS(http_requests_total)、TP99(http_request_duration_seconds_bucket{le="0.99"})、缓存命中率(redis_keyspace_hits_total / (redis_keyspace_hits_total + redis_keyspace_misses_total))、DB锁等待(mysql_global_status_innodb_row_lock_time_avg)具备时间对齐与标签一致性。

Grafana 四维联动看板设计

面板维度 关键表达式 告警阈值
QPS趋势 sum(rate(http_requests_total{job="app-metrics",code=~"2.."}[1m])) >5000
TP99延迟 histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[1m])) >1.2s
缓存命中率 sum(rate(redis_keyspace_hits_total[1m])) / (sum(rate(redis_keyspace_hits_total[1m])) + sum(rate(redis_keyspace_misses_total[1m])) )
DB锁平均等待 mysql_global_status_innodb_row_lock_time_avg >50ms

告警联动触发逻辑

graph TD
    A[QPS突增] --> B{TP99同步升高?}
    B -->|是| C[触发“高负载+高延迟”复合告警]
    B -->|否| D[检查缓存命中率]
    D --> E[命中率<0.85?]
    E -->|是| F[关联DB锁等待>50ms]
    F --> G[判定为缓存穿透引发DB争抢]

第五章:结语——从面试压轴题到苏州金融科技团队真实交付标准

面试题不是终点,而是交付起点

在苏州某持牌消费金融公司的风控中台项目中,一道经典的“高并发场景下分布式事务最终一致性保障”面试题,直接演化为生产环境中的真实模块:订单创建→额度冻结→贷前审批→合同签署四阶段跨服务事务链。团队未采用TCC或Saga框架,而是基于RocketMQ事务消息+本地事务表+幂等校验三重机制,在QPS 3200的峰值下将事务失败率控制在0.0017%以内。

真实交付标准的量化清单

指标类别 苏州团队硬性阈值 生产监控工具 违规响应SLA
接口P99延迟 ≤180ms(含DB+缓存+RPC) SkyWalking v9.4 15分钟自动熔断
数据一致性窗口 ≤2.3秒(跨MySQL+Oracle) Flink CDC实时比对 触发告警并启动补偿作业
部署回滚耗时 ≤90秒(全链路灰度) Argo CD + 自研蓝绿网关 人工介入超时即自动回滚

代码即契约:一段被写入SOP的补偿逻辑

// 来自2023年Q4上线的授信服务补偿模块(已通过ISO27001审计)
@Transactional(rollbackFor = Exception.class)
public void compensateCreditFreeze(String applyId) {
    CreditApply apply = creditApplyMapper.selectById(applyId);
    if (apply.getStatus() == CREDIT_STATUS.FROZEN && 
        System.currentTimeMillis() - apply.getFrozenAt().getTime() > 300_000L) {
        // 超时未完成审批,自动解冻额度
        quotaService.releaseQuota(apply.getUserId(), apply.getApplyAmount());
        creditApplyMapper.updateStatus(applyId, CREDIT_STATUS.RELEASED);
        log.warn("Auto-release triggered for apply: {}", applyId);
    }
}

流程不可绕行:从需求评审到上线的强制路径

flowchart LR
    A[需求方提交PRD] --> B{架构委员会评审}
    B -->|通过| C[编写契约测试用例]
    C --> D[开发提交含覆盖率≥85%的单元测试]
    D --> E[自动化流水线执行:静态扫描+接口压测+数据一致性验证]
    E -->|全部通过| F[发布至预发环境]
    F --> G[业务方签署UAT验收单]
    G --> H[凌晨1:00-2:00窗口期上线]

压测不是表演,是交付门槛

2024年3月“双十一”备战期间,团队对反欺诈模型服务进行真实流量染色压测:

  • 使用线上1%真实请求镜像(非构造数据)
  • 注入200ms网络抖动模拟弱网环境
  • 强制触发Redis集群脑裂场景
    最终确认在节点故障率30%时,服务仍维持99.95%可用性,该结果直接写入与银行合作方的SLA附件。

文档即交付物的一部分

每个微服务必须附带三份强制文档:

  • api-contract.yaml(OpenAPI 3.0规范,含所有错误码及重试策略)
  • data-flow-diagram.drawio(标注所有数据落库时机与事务边界)
  • failure-scenario.md(记录近6个月真实故障的根因、修复方案与预防措施)

技术债清零机制

每季度末启动“技术债冲刺周”,所有成员暂停新需求开发,专注解决两类问题:

  • SonarQube标记的Blocker级漏洞(必须当日闭环)
  • 生产日志中重复出现≥5次的WARN级别异常(需补充监控或重构)
    2024年Q1累计关闭技术债137项,其中32项源自历史面试题中未覆盖的边界场景。

传播技术价值,连接开发者与最佳实践。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注