我用 Go 实现了类似 Spring Cache 的注解式缓存框架

17 阅读5分钟

背景

在 Java 生态中,Spring Cache 几乎是缓存的标准解决方案。只需要在方法上添加 @Cacheable 注解,就能自动实现缓存逻辑,业务代码完全无侵入。

但在 Go 语言中,由于缺乏注解系统和运行时 AOP 机制,实现类似的功能一直是个挑战。现有的 Go 缓存库大多需要手动编写缓存逻辑,或者侵入业务代码。

作为一个追求极致开发体验的开发者,我决定挑战这个难题 —— 在 Go 中实现一个真正优雅的注解式缓存框架

项目地址github.com/coderiser/g…


目标

我想要实现的框架需要满足:

  1. 零侵入 - 业务代码不需要任何缓存逻辑
  2. 注解驱动 - 通过简单注解声明缓存行为
  3. 自动代理 - 运行时自动拦截方法调用
  4. 多后端支持 - Memory / Redis 可插拔
  5. 高性能 - 缓存命中延迟 < 1ms

技术挑战与解决方案

挑战 1:Go 没有注解

Java 的 @Cacheable 是语言级别的注解,但 Go 没有这个特性。

解决方案:使用注释 + AST 解析

// @cacheable(cache="users", key="#id", ttl="30m")
func (s *UserService) GetUser(id string) (*User, error) {
    // 业务代码
}

通过 go/packagesgo/ast 在编译时扫描注释,提取缓存配置。


挑战 2:Go 没有运行时 AOP

Spring 可以在运行时动态代理方法,但 Go 不支持这种机制。

解决方案反射 + 动态代理

type Proxy interface {
    Call(methodName string, args []reflect.Value) []reflect.Value
}

func (p *proxyImpl) Call(methodName string, args []reflect.Value) []reflect.Value {
    // 1. 检查缓存
    // 2. 命中则返回
    // 3. 未命中则调用原方法
    // 4. 写入缓存
}

挑战 3:如何最小化用户集成成本?

最初的方案需要用户手动调用 cache.Decorate(),但这不够优雅。

最终方案接口模式 + DecorateAndReturn

// 1. 定义接口
type UserServiceInterface interface {
    GetUser(id string) (*User, error)
}

// 2. 实现接口并添加注解
type UserService struct {
    db *gorm.DB
}

// @cacheable(cache="users", key="#id", ttl="30m")
func (s *UserService) GetUser(id string) (*User, error) {
    var u User
    err := s.db.Where("id = ?", id).First(&u).Error
    return &u, err
}

// 3. init() 创建代理
var UserService UserServiceInterface

func init() {
    manager := core.NewCacheManager()
    autoDecorate := proxy.GetAutoDecorate(manager)
    decorated, err := autoDecorate.DecorateAndReturn(&UserService{})
    if err != nil {
        UserService = &UserService{}
        return
    }
    UserService = decorated.(UserServiceInterface)
}

// 使用时(完全透明)
user, _ := UserService.GetUser("123")  // 自动缓存!

核心功能

1. @Cacheable 缓存读取

// @cacheable(cache="users", key="#id", ttl="30m")
func (s *UserService) GetUser(id string) (*User, error) {
    var u User
    s.db.First(&u, id)
    return &u, nil
}
  • 首次调用执行原方法,结果写入缓存
  • 后续调用直接返回缓存结果
  • 支持 SpEL 表达式动态生成 Key

2. @CachePut 强制更新

// @cacheput(cache="users", key="#user.Id")
func (s *UserService) UpdateUser(user *User) error {
    return s.db.Save(user).Error
}
  • 始终执行原方法
  • 方法执行后强制更新缓存

3. @CacheEvict 删除缓存

// @cacheevict(cache="users", key="#userId", before=true)
func (s *UserService) DeleteUser(userId string) error {
    return s.db.Delete(&User{}, userId).Error
}
  • 支持 before=true 在方法执行前删除
  • 支持 allEntries=true 清空整个缓存

SpEL 表达式

框架集成了 expr 引擎,支持强大的表达式语法:

// 引用参数
@cacheable(cache="orders", key="#userId + '_' + #status")

// 引用返回值(条件过滤)
@cacheable(cache="data", key="#id", unless="#result == nil")

// 复杂表达式
@cacheable(cache="products", key="category:#catId:page:#page")

// 三元表达式
@cacheable(cache="config", key="#env == 'prod' ? 'p_' + #id : 't_' + #id")

🛡️ 缓存异常保护

缓存穿透(查询不存在的数据)

// 自动空值缓存
if result == nil {
    cache.Set(key, nilMarker, 5*time.Minute)
}

缓存击穿(热点 Key 过期)

// Singleflight 单飞模式
group.Do(key, func() (interface{}, error) {
    // 只让一个请求查数据库
})

缓存雪崩(大量 Key 同时过期)

// TTL 随机偏移
ttl := baseTTL + time.Duration(rand.Int63n(jitter))

性能表现

场景延迟
Memory 命中< 1ms
Redis 命中< 5ms
SpEL 求值< 50μs
代码生成< 1s (100 方法)

测试覆盖率:83%+


快速开始

1. 安装

go get github.com/coderiser/go-cache

2. 定义服务接口和实现

package service

// 定义接口
type UserServiceInterface interface {
    GetUser(id string) (*User, error)
}

// 实现接口
type UserService struct {
    db *gorm.DB
}

// 添加缓存注解
// @cacheable(cache="users", key="#id", ttl="30m")
func (s *UserService) GetUser(id string) (*User, error) {
    var u User
    err := s.db.Where("id = ?", id).First(&u).Error
    return &u, err
}

3. 初始化(使用 DecorateAndReturn)

package service

import (
    "github.com/coderiser/go-cache/pkg/core"
    "github.com/coderiser/go-cache/pkg/proxy"
)

var UserService UserServiceInterface

func init() {
    manager := core.NewCacheManager()
    autoDecorate := proxy.GetAutoDecorate(manager)
    decorated, err := autoDecorate.DecorateAndReturn(&UserService{})
    if err != nil {
        UserService = &UserService{}
        return
    }
    UserService = decorated.(UserServiceInterface)
}

4. 生成元数据

# 安装代码生成器
go install github.com/coderiser/go-cache/cmd/generator@latest

# 生成注解元数据
go-cache-gen ./...

5. 使用(完全透明)

// 通过接口调用,自动应用缓存
user, err := UserService.GetUser("123")  // 自动缓存!

🎯 Go 语言接口模式说明

由于 Go 语言没有运行时注解,本框架采用接口模式

核心思路

  1. 定义接口:为服务定义清晰的接口
  2. 注解标注:在实现的方法上添加 // @cacheable(...) 注释
  3. 代码生成:使用 go-cache-gen 生成注解元数据
  4. 代理装饰:通过 DecorateAndReturn 创建代理对象
  5. 接口调用:通过接口变量调用,自动应用缓存

为什么需要接口?

Go 的反射系统无法直接修改方法调用,但可以通过:

  • 创建代理对象实现相同接口
  • 拦截接口方法调用
  • 在调用前后执行缓存逻辑

技术选型

组件技术理由
SpEL 引擎expr纯 Go 实现,性能优秀
Redis 客户端go-redis/v9社区活跃,功能完整
AST 解析go/packages官方标准库
动态代理reflect运行时反射

📚 文档

📦 安装

go get github.com/coderiser/go-cache

🧪 测试

# 运行测试
go test ./...

# 覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# 性能基准
go test -bench=. -benchmem ./...

📊 性能表现

场景延迟
Memory 命中< 1ms
Redis 命中< 5ms
SpEL 求值< 50μs

测试覆盖率:83%+


总结

在 Go 中实现注解式缓存框架确实有挑战,但通过合理的技术选型和架构设计,我们成功实现了:

✅ 零侵入业务代码
✅ 注解驱动缓存逻辑
✅ 运行时自动代理(接口模式)
✅ 多后端支持
✅ 高性能表现

虽然 Go 没有 Java 那样的注解和 AOP,但我们找到了 Go 语言下的最优解 —— 接口模式 + DecorateAndReturn,既保持了 Go 的简洁性,又获得了接近 Spring Cache 的开发体验。


Made with ❤️ by Go-Cache Team