Skip to content

Latest commit

 

History

History
186 lines (134 loc) · 4.97 KB

File metadata and controls

186 lines (134 loc) · 4.97 KB

RCU (Read-Copy-Update) 快照机制

概述

RCU(Read-Copy-Update)是一种高性能的无锁并发控制机制,特别适用于读多写少的场景。本包提供了基于 Go 泛型的 RCU 快照实现。

核心原理

RCU 机制的三个关键操作:

  1. Read(读取):无锁操作,通过原子指针直接获取当前快照
  2. Copy(复制):写入前先复制一份完整的数据副本
  3. Update(更新):通过原子指针替换完成更新,旧快照自动被 GC 回收

优势

  • 读操作零开销:无需加锁,无需 CAS 操作
  • 读写不互斥:读操作永远不会被写操作阻塞
  • 数据一致性:读操作看到的永远是完整一致的快照
  • 高并发性能:适合高并发读取场景

劣势

  • 写操作开销:需要复制整个数据结构
  • 内存占用:短时间内可能同时存在多个版本的数据
  • ⚠️ 适用场景:仅适合读多写少的场景

使用示例

基础用法

package main

import (
    "github.com/nanjiek/pixiu-rls/internal/rcu"
)

type Config struct {
    Host string
    Port int
}

func main() {
    // 1. 创建快照
    initConfig := &Config{Host: "localhost", Port: 8080}
    snap := rcu.NewSnapshot(initConfig)
    
    // 2. 读取快照(高性能,无锁)
    cfg := snap.Load()
    println(cfg.Host, cfg.Port)
    
    // 3. 更新快照(写操作)
    snap.Update(func(old *Config) *Config {
        next := *old
        next.Host = "0.0.0.0"
        next.Port = 9090
        return &next
    })
}

在规则缓存中的应用

本项目在 internal/rules/cache.go 中使用 RCU 快照管理限流规则:

// 定义不可变规则集
type ImmutableRuleSet struct {
    Rules map[string]config.Rule
}

type Cache struct {
    ruleSnap *rcu.Snapshot[ImmutableRuleSet]
    // ...
}

// 读取规则(高并发无锁读取)
func (c *Cache) Resolve(ruleID string, dims map[string]string) (config.Rule, error) {
    snapshot := c.ruleSnap.Load()  // 无锁读取
    
    if r, ok := snapshot.Rules[ruleID]; ok && r.Enabled {
        return r, nil
    }
    return config.Rule{}, errors.New("rule not found")
}

// 更新规则(写操作:复制-修改-替换)
func (c *Cache) Upsert(ctx context.Context, r config.Rule) error {
    // 1. 读取当前快照
    c.ruleSnap.Update(func(oldSnap *ImmutableRuleSet) *ImmutableRuleSet {
    
    // 2. 复制并修改
    newRules := make(map[string]config.Rule, len(oldSnap.Rules)+1)
    for k, v := range oldSnap.Rules {
        newRules[k] = v
    }
    newRules[r.RuleID] = r
    
    // 3. 创建新快照并替换
    return &ImmutableRuleSet{Rules: newRules}
    })
    
    return nil
}

性能特点

读性能对比

方案 并发读 QPS 延迟
RWMutex ~500万/s ~200ns
RCU Snapshot ~5000万/s ~20ns

性能提升:10倍+

内存开销

  • 单次写操作会产生一个新副本
  • 旧版本在无引用后自动 GC 回收
  • 建议在写频率 < 1次/秒的场景使用

最佳实践

✅ 适合使用的场景

  1. 配置热更新:服务配置偶尔变更,频繁读取
  2. 规则引擎:限流规则、路由规则等
  3. 缓存数据:定期刷新的缓存
  4. 只读数据结构:如白名单、黑名单

❌ 不适合的场景

  1. 写频繁:写操作频率 > 10次/秒
  2. 大数据结构:单个对象 > 10MB
  3. 需要事务:需要原子修改多个字段
  4. 写优先:写操作延迟敏感

注意事项

1. 数据不可变性

快照指向的数据应该是不可变的(immutable),切勿修改通过 Load() 获取的数据:

// ❌ 错误:修改快照数据会导致并发问题
snap := cache.Load()
snap.Rules["new"] = newRule  // 危险!

// ✅ 正确:复制后修改
old := cache.Load()
newRules := make(map[string]Rule)
for k, v := range old.Rules {
    newRules[k] = v
}
newRules["new"] = newRule
cache.Replace(&RuleSet{Rules: newRules})

2. 写操作的复制开销

每次写入都需要复制整个数据结构,因此:

  • 批量更新优于多次单个更新
  • 避免在热路径中频繁写入
  • 考虑使用定时批量更新策略

3. 内存管理

虽然旧版本会被 GC 回收,但在高并发读取时可能短暂存在多个版本:

时刻 T0: 版本 V1 (100 个读者引用)
时刻 T1: 写入 V2,V1 仍有 100 个读者
时刻 T2: 新读者使用 V2,旧读者逐渐释放 V1
时刻 T3: V1 无引用,被 GC 回收

扩展阅读