学习目标
读完本文,你应该能够:
- 实现单例模式的四种写法并理解 sync.Once 的底层原理
- 手写令牌桶、漏桶、滑动窗口三种限流算法
- 实现熔断器的状态机(Closed → Open → Half-Open)
- 理解指数退避重试的实现细节
- 手写雪花算法(Snowflake)并处理时钟回拨问题
- 实现基于 Redis 和 Etcd 的分布式锁
- 使用 Functional Options 模式设计可扩展 API
一、单例模式与 sync.Once
1.1 四种写法对比
应用场景
在实际项目中,这个知识点经常应用于:
- 典型场景一:说明具体应用场景
- 典型场景二:说明另一个应用场景
- 面试要点:提醒面试中可能被问到的点
// 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 的关键设计:
done先 CAS,后用锁保证只有一个 goroutine 执行函数- panic 会被 recover,但 done 仍会被设置为 1
- 如果函数本身会 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 的滑窗计数器)。
漏桶:强制匀速流出,平滑流量,适合需要严格控制输出速率的场景。
网络传输中常用漏桶整形,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 note3.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 << 22) | (machineID << 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。 解决方案:
- 等待回拨时间过去再继续(上述代码的做法)
- 使用更长的时间戳位(如 41 位支持 69 年)
- 监控告警,发现回拨时停机排查
六、分布式锁
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。
Redis 实现滑动窗口推荐用 ZSet(O(logN)),令牌桶用 INCRBY + EXPIRE。
面试官
熔断器的 Half-Open 状态有什么作用?
候选人
Half-Open 是探测状态:
- 允许少量请求通过测试目标服务是否恢复
- 成功后回到 Closed,失败则继续 Open
- 避免一直 Open 导致服务永远无法恢复
十、本章小结
复习提示:
- 单例模式面试常考 sync.Once 的"双重检查"优化原理
- 令牌桶比漏桶更灵活,能处理突发流量;滑动窗口比固定窗口更精确
- 熔断器的三种状态转换是必背知识点
- 雪花算法关键是理解时间戳 + 机器ID + 序列号的位运算布局
- 下一篇将讲解网络与协议实现(TCP/Echo Server、HTTP Server、DNS Resolver)
参考
- Go 源码:
src/sync/once.go,src/runtime/sema.go - https://github.com/sony/gobreaker
- Snowflake 论文:https://twitter.github.io/snowflake/