Go 设计模式与工程实践详解(限流/熔断/雪花算法)

2022-02-11T10:00:00+08:00 | 8分钟阅读 | 更新于 2022-02-11T10:00:00+08:00

@
设计模式与工程设计模式并发模式分布式组件单例/DCLsync.Once中间件链令牌桶熔断器指数退避雪花算法分布式锁Functional Options漏桶/滑动窗口Redis SET NX策略模式状态机设计Etcd 方案泛型 Map/Filter舱壁模式时钟回拨处理Lua 滑动窗口限流

学习目标

读完本文,你应该能够:

  • 实现单例模式的四种写法并理解 sync.Once 的底层原理
  • 手写令牌桶、漏桶、滑动窗口三种限流算法
  • 实现熔断器的状态机(Closed → Open → Half-Open)
  • 理解指数退避重试的实现细节
  • 手写雪花算法(Snowflake)并处理时钟回拨问题
  • 实现基于 Redis 和 Etcd 的分布式锁
  • 使用 Functional Options 模式设计可扩展 API

一、单例模式与 sync.Once

1.1 四种写法对比

应用场景

在实际项目中,这个知识点经常应用于:

  1. 典型场景一:说明具体应用场景
  2. 典型场景二:说明另一个应用场景
  3. 面试要点:提醒面试中可能被问到的点
// 1. 饿汉式(线程安全,但提前初始化)
var singleton1 *Singleton = &Singleton{}

// 2. 懒汉式 + Mutex(有锁开销)
var (
    singleton2 *Singleton
    once2      sync.Mutex
)
func GetSingleton2() *Singleton {
    once2.Lock()
    defer once2.Unlock()
    if singleton2 == nil {
        singleton2 = &Singleton{}
    }
    return singleton2
}

// 3. 双重检查 DCL(高性能)
var (
    singleton3 *Singleton
    once3      sync.Once
)
func GetSingleton3() *Singleton {
    if singleton3 == nil {
        once3.Do(func() {
            singleton3 = &Singleton{}
        })
    }
    return singleton3
}

// 4. sync.Once 标准写法
var once4 sync.Once
var singleton4 *Singleton
func GetSingleton4() *Singleton {
    once4.Do(func() {
        singleton4 = &Singleton{}
    })
    return singleton4
}

1.2 sync.Once 源码分析

// src/sync/once.go
type Once struct {
    done uint32  // 是否已完成(原子操作)
    m    Mutex  // 保护 next f 的互斥锁
}

func (o *Once) Do(f func()) {
    // 快速路径:已执行过直接返回
    if atomic.LoadUint32(&o.done) == 1 {
        return
    }
    // 慢路径:加锁后再次检查
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        // 双重检查 + 原子标记
        o.call(f)
    }
}

func (o *Once) call(f func()) {
    // 使用 goroutine 运行 f,确保 panic 时也能设置 done
    done := uint32(1)
    var m mutex
    go func() {
        defer func() {
            if panicValue := recover(); panicValue != nil {
                m.unlockRecovery(panicValue)
                throw("sync: once failed with panic")
            }
        }()
        f()
        atomic.StoreUint32(&o.done, 1)
        m.unlock()
    }()
    m.lock()
}
复习提示:

sync.Once 的关键设计:

  1. done 先 CAS,后用锁保证只有一个 goroutine 执行函数
  2. panic 会被 recover,但 done 仍会被设置为 1
  3. 如果函数本身会 panic,需要自己在函数内部 recover

二、限流器(Rate Limiter)

2.1 令牌桶算法

type TokenBucket struct {
    tokens     float64
    maxTokens  float64
    rate       float64  // 每秒生成的令牌数
    lastTime   time.Time
    mu         sync.Mutex
}

func NewTokenBucket(maxTokens, rate float64) *TokenBucket {
    return &TokenBucket{
        tokens:   maxTokens,
        maxTokens: maxTokens,
        rate:     rate,
        lastTime: time.Now(),
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()
    
    now := time.Now()
    elapsed := now.Sub(tb.lastTime).Seconds()
    tb.tokens += elapsed * tb.rate
    if tb.tokens > tb.maxTokens {
        tb.tokens = tb.maxTokens
    }
    tb.lastTime = now
    
    if tb.tokens >= 1 {
        tb.tokens--
        return true
    }
    return false
}

2.2 漏桶算法

type LeakBucket struct {
    capacity   float64
    water      float64
    rate       float64  // 漏水速率(请求/秒)
    lastTime   time.Time
    mu         sync.Mutex
}

func (lb *LeakBucket) Allow() bool {
    lb.mu.Lock()
    defer lb.mu.Unlock()
    
    now := time.Now()
    elapsed := now.Sub(lb.lastTime).Seconds()
    lb.water -= elapsed * lb.rate
    if lb.water < 0 {
        lb.water = 0
    }
    lb.lastTime = now
  

### 应用场景

在实际项目中,这个知识点经常应用于:

1. **典型场景一**:说明具体应用场景
2. **典型场景二**:说明另一个应用场景
3. **面试要点**:提醒面试中可能被问到的点

  
    if lb.water + 1 <= lb.capacity {
        lb.water++
        return true
    }
    return false
}
面试官
令牌桶和漏桶有什么区别?各适用于什么场景?
候选人
令牌桶:允许突发流量(桶满时可以一次性消费多个令牌),适合有突发需求的场景。
漏桶:强制匀速流出,平滑流量,适合需要严格控制输出速率的场景。

网络传输中常用漏桶整形,API 限流常用令牌桶(如 Redis 的滑窗计数器)。

2.3 滑动窗口限流(Redis Lua 版)

-- Redis Lua 脚本:滑动窗口限流
local key = KEYS[1]
local window = tonumber(ARGV[1])  -- 窗口大小(秒)
local limit = tonumber(ARGV[2])    -- 限流值
local now = tonumber(ARGV[3])      -- 当前时间戳

local start = now - window
redis.call('ZREMRANGEBYSCORE

### 应用场景

在实际项目中,这个知识点经常应用于:

1. **典型场景一**:说明具体应用场景
2. **典型场景二**:说明另一个应用场景
3. **面试要点**:提醒面试中可能被问到的点

', key, '-inf', start)
local count = redis.call('ZCARD', key)
if count < limit then
    redis.call('ZADD', key, now, now .. math.random(1000))
    redis.call('EXPIRE', key, window)
    return 1
end
return 0

三、熔断器(Circuit Breaker)

3.1 状态机设计

stateDiagram-v2
    [*] --> Closed
    Closed --> Open : 失败率超过阈值
    Open --> HalfOpen : 超时时间到达
    HalfOpen --> Closed : 成功
    HalfOpen --> Open : 失败
    
    note right of Closed
        正常处理请求
        记录失败次数
    end note
    note right of Open
        拒绝所有请求
        等待恢复时间
    end note
    note right of HalfOpen
        允许少量测试请求
        根据结果决定状态
    end note

3.2 代码实现

type State int

const (
    Closed State = iota
    Open
    HalfOpen
)

type CircuitBreaker struct {
    mu               sync.Mutex
    state            State
    failureCount     int
    successCount     int
    failureThreshold int
    halfOpenMax      int
    lastFailTime     time.Time
    resetTimeout     time.Duration
}

func NewCircuitBreaker(failureThreshold, halfOpenMax int, timeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        state:            Closed,
        failureThreshold: failureThreshold,
        halfOpenMax:      halfOpenMax,
        resetTimeout:     timeout,
    }
}

func (cb *CircuitBreaker) Execute(fn func() error) error {
    cb.mu.Lock()
    shouldAllow := cb.shouldAllowRequest()
    cb.mu.Unlock()
    
    if !shouldAllow {
        return fmt.Errorf("circuit breaker is open")
    }
    
    err := fn()
    
    cb.mu.Lock()
    defer cb.mu.Unlock()
    if err != nil {
        cb.failureCount++
        cb.lastFailTime = time.Now()
        if cb.failureCount >= cb.failureThreshold && cb.state == Closed {
            cb.state = Open
        }
    } else {
        cb.failureCount = 0
        if cb.state == HalfOpen {
            cb.successCount++
            if cb.successCount >= cb.halfOpenMax {
                cb.state = Closed
            }
        }
    }
    return err
}

func (cb *CircuitBreaker) shouldAllowRequest() bool {
    switch cb.state {
    case Closed:
        return true
    case Open:
        return time.Since(cb.lastFailTime) > cb.resetTimeout
    case HalfOpen:
        return cb.successCount + cb.failureCount < cb.halfOpenMax
    default:
        return false
    }
}

四、指数退避重试

func RetryWithBackoff(fn func() error, maxRetries int, baseDelay time.Duration) error {
    var err error
    delay := baseDelay
    
    for i := 0; i < maxRetries; i++ {
        err = fn()
        if err == nil {
            return nil
        }
        
        // 指数退避 + 随机抖动
        jitter := time.Duration(rand.Int63n(int64(delay)))
        time.Sleep(delay + jitter)
        delay *= 2
        if delay > 30*time.Second {
            delay = 30 * time.Second
        }
    }
    
    return fmt.Errorf("after %d retries, last error: %w", maxRetries, err)
}

五、雪花算法(Snowflake)

type Snowflake struct {
    mu          sync.Mutex
    machineID   int64   // 机器 ID(0-1023)
    lastTimestamp int64  // 上次时间戳(毫秒)
    sequence    int64   // 序列号(0-4095)
}

const (
    machineBits   = 10
    timestampBits = 41
    sequenceBits  = 12
    machineMax    = -1 ^ (-1 << machineBits)  // 1023
	sequenceMask  = -1 ^ (-1 << sequenceBits)  // 4095
)

**雪花算法 ID 位布局(64 位)**

<svg viewBox="0 0 1000 300" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif">
  <defs>
    <marker id="s1" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
      <path d="M0,0 L10,4 L0,8 z" fill="#B03030"/>
    </marker>
  </defs>
  <g stroke="#B03030" stroke-width="2.5" fill="#fff">
    <rect x="40" y="55" width="920" height="130" rx="12"/>
    <text x="500" y="84" text-anchor="middle" fill="#B03030" font-size="14" font-weight="700" stroke="none">Snowflake ID(64 位整数)</text>

    <rect x="60" y="105" width="120" height="60" rx="6"/>
    <text x="120" y="132" text-anchor="middle" fill="#1a1a1a" font-size="12.5" stroke="none">1 bit</text>
    <text x="120" y="152" text-anchor="middle" fill="#666" font-size="11" stroke="none">符号位(恒0)</text>

    <rect x="195" y="105" width="320" height="60" rx="6" stroke-width="3.5"/>
    <text x="355" y="132" text-anchor="middle" fill="#B03030" font-size="13" font-weight="700" stroke="none">41 bit 时间戳</text>
    <text x="355" y="152" text-anchor="middle" fill="#666" font-size="11" stroke="none">毫秒,可用 ~69 年</text>

    <rect x="530" y="105" width="200" height="60" rx="6"/>
    <text x="630" y="132" text-anchor="middle" fill="#1a1a1a" font-size="12.5" stroke="none">10 bit 机器ID</text>
    <text x="630" y="152" text-anchor="middle" fill="#666" font-size="11" stroke="none">0~1023</text>

    <rect x="745" y="105" width="195" height="60" rx="6"/>
    <text x="842" y="132" text-anchor="middle" fill="#1a1a1a" font-size="12.5" stroke="none">12 bit 序列号</text>
    <text x="842" y="152" text-anchor="middle" fill="#666" font-size="11" stroke="none">0~4095/ms</text>
  </g>

  <g stroke="#B03030" stroke-width="2" fill="none" marker-end="url(#s1)">
    <path d="M355 200 L355 240"/>
    <path d="M630 200 L630 240"/>
    <path d="M842 200 L842 240"/>
  </g>

  <g text-anchor="middle" fill="#B03030" font-size="12" font-family="-apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif">
    <text x="500" y="262" font-weight="700">ID = (timestamp &lt;&lt; 22) | (machineID &lt;&lt; 12) | sequence</text>
    <text x="355" y="218">位宽 41</text>
    <text x="630" y="218">位宽 10</text>
    <text x="842" y="218">位宽 12</text>
  </g>
</svg>

> **看图要点**:面试追问「64 位怎么分配的、为什么这么分」。核心是位运算拼接:高 41 位时间戳保证趋势递增且可解码出时间;中 10 位机器 ID 支持 1024 个节点;低 12 位序列号在 1ms 内区分 4096 个 ID。同毫秒序列号用尽则等到下一毫秒。时钟回拨会撞 ID——解法:等回拨过去、或停机告警。追问:41 位时间戳上限约 69 年,从自定义 epoch 起算可延长。

func NewSnowflake(machineID int64) *Snowflake {
    return &Snowflake{machineID: machineID & machineMax}
}

func (sf *Snowflake) NextID() int64 {
    sf.mu.Lock()
    defer sf.mu.Unlock()
    
    timestamp := time.Now().UnixMilli()
    
    if timestamp == sf.lastTimestamp {
        sf.sequence = (sf.sequence + 1) & sequenceMask
        if sf.sequence == 0 {
            // 同一毫秒内序列号用完,等待下一毫秒
            timestamp = sf.waitNextMilli(timestamp)
        }
    } else {
        sf.sequence = 0
    }
    
    sf.lastTimestamp = timestamp
    return (timestamp << 22) | (sf.machineID << 12) | sf.sequence
}

func (sf *Snowflake) waitNextMilli(lastTimestamp int64) int64 {
    timestamp := time.Now().UnixMilli()
    for timestamp <= lastTimestamp {
        timestamp = time.Now().UnixMilli()
    }
    return timestamp
}
复习提示:

时钟回拨问题:如果系统时钟回拨,雪花算法会生成重复 ID。 解决方案:

  1. 等待回拨时间过去再继续(上述代码的做法)
  2. 使用更长的时间戳位(如 41 位支持 69 年)
  3. 监控告警,发现回拨时停机排查

六、分布式锁

6.1 Redis SET NX 实现

func acquireLock(client *redis.Client, key, value string, ttl time.Duration) bool {
    ok, err := client.SetNX(key, value, ttl).Result()
    if err != nil || !ok {
        return false
    }
    return true
}

func releaseLock(client *redis

### 应用场景

在实际项目中,这个知识点经常应用于:

1. **典型场景一**:说明具体应用场景
2. **典型场景二**:说明另一个应用场景
3. **面试要点**:提醒面试中可能被问到的点

.Client, key, value string) bool {
    // 必须用 Lua 脚本保证原子性
    script := `
    if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
    else
        return 0
    end`
    ok, err := client.Eval(script, []string{key}, value).Result()
    return err == nil && ok.(int64) == 1
}

6.2 Etcd 实现

func acquireEtcdLock(ctx context.Context, client *clientv3.Client, key, value string, ttl int64) (*lease.LeaseKeepResponse, error) {
    leaseResp, err := client.Grant(ctx, ttl)
    if err != nil {
        return nil, err
    }
    
    txn := client.Txn(

### 应用场景

在实际项目中,这个知识点经常应用于:

1. **典型场景一**:说明具体应用场景
2. **典型场景二**:说明另一个应用场景
3. **面试要点**:提醒面试中可能被问到的点

ctx)
    resp, err := txn.If(etcd.Compare(etcd.Value(key), "=")).Then(
        etcd.Put(key, value, etcd.WithLease(leaseResp.ID)),
    ).Else(
        etcd.Get(key),
    ).Commit()
    
    if err != nil || !resp.Succeeded {
        client.Revoke(ctx, leaseResp.ID)
        return nil, fmt.Errorf("lock not acquired")
    }
    
    keepResp, err := client.KeepAlive(ctx, leaseResp.ID)
    return keepResp, err
}

七、Functional Options 模式

type Server struct {
    host     string
    port     int
    timeout  time.Duration
    tls      bool
    logger   *log.Logger
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) { s.host = host }
}

func WithPort(port int) Option {
    return func(s *Server) { s.port = port }
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) { s.timeout = timeout }
}

func WithTLS(enabled bool) Option {
    return func(s *Server) { s.tls = enabled }
}

func NewServer(options ...Option) *Server {
    s := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
    }
    for _, opt := range options {
        opt(s)
    }
    return s
}

// 使用
srv := NewServer(
    WithPort(9090),
    WithTimeout(60*time.Second),
    WithTLS(true),
)

八、Middleware 链

type Handler func(http.ResponseWriter, *http.Request)
type Middleware func(Handler) Handler

func Chain(h Handler, middlewares ...Middleware) Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

func Logging(next Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    }
}

func Recovery(next Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
            }
        }()
        next(w, r)
    }
}

func Auth(next Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if !validateToken(token) {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next(w, r)
    }
}

九、自测题

面试官
限流算法选型:固定窗口 vs 滑动窗口 vs 令牌桶,各有什么优缺点?
候选人
固定窗口:最简单,但临界突刺问题(窗口边界前后各 burst 一次) 滑动窗口:解决突刺问题,但需要更多存储(记录每个小窗口的计数) 令牌桶:允许一定突发,适合 API 限流

Redis 实现滑动窗口推荐用 ZSet(O(logN)),令牌桶用 INCRBY + EXPIRE。
面试官
熔断器的 Half-Open 状态有什么作用?
候选人

Half-Open 是探测状态:

  • 允许少量请求通过测试目标服务是否恢复
  • 成功后回到 Closed,失败则继续 Open
  • 避免一直 Open 导致服务永远无法恢复

十、本章小结

复习提示:
  • 单例模式面试常考 sync.Once 的"双重检查"优化原理
  • 令牌桶比漏桶更灵活,能处理突发流量;滑动窗口比固定窗口更精确
  • 熔断器的三种状态转换是必背知识点
  • 雪花算法关键是理解时间戳 + 机器ID + 序列号的位运算布局
  • 下一篇将讲解网络与协议实现(TCP/Echo Server、HTTP Server、DNS Resolver)

参考

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!