# 27|缓存服务:如何基于Redis实现封装? 你好,我是轩脉刃。 上面两节课把数据库操作接入到hade框架中了,现在我们能使用容器中的ORM服务来操作数据库了。在实际工作中,一旦数据库出现性能瓶颈,除了优化数据库本身之外,另外一个常用的方法是使用缓存来优化业务请求。所以这节课,我们来讨论一下,hade框架如何提供缓存支持。 现在的Web业务,大部分都是使用Redis来做缓存实现。但是,缓存的实现方式远不止Redis一种,比如在Redis出现之前,Memcached一般是缓存首选;在单机上,还可以使用文件来存储数据,又或者直接使用进程的内存也可以进行缓存实现。 缓存服务的底层使用哪个存储方式,和具体的业务架构原型相关。我个人在不同业务场景中用过不少的缓存存储方案,不过业界用的最多的Redis,还是优点比较突出。相比文件存储,它能集中分布式管理;而相比Memcached,优势在于多维度的存储数据结构。所以,顺应潮流,我们hade框架主要也针对使用Redis来实现缓存服务。 我们这节课会创建两个服务,一个是Redis服务,提供对Redis的封装,另外一个是缓存服务,提供一系列对“缓存”的统一操作。而这些统一操作,具体底层是由Redis还是内存进行驱动的,这个可以根据配置决定。 下面我们一个个来讨论吧。 ## Redis服务 首先封装一个可以对Redis进行操作的服务。和封装ORM一样,我们自己并不实现Redis的底层传输协议和操作封装,只将Redis“创建连接”的过程封装在hade中就行了。 这里我们就选择[go-redis](https://github.com/go-redis/redis)这个库来实现对Redis的连接。这个库目前也是Golang开源社区最常用的Redis库,有12.8k的star数,使用的是BSD 协议,可以引用,可以修改,但是修改的同时要保留版权声明,这里我们并不需要修改,所以BSD已经足够了。这个库目前是v8版本,可以使用 `go get github.com/go-redis/redis/v8` 来引入它。 go-redis的连接非常简单,我们看官网的例子,就看创建连接部分: ```go import (     "context"     "github.com/go-redis/redis/v8" ) var ctx = context.Background() func ExampleClient() { // 创建连接     rdb := redis.NewClient(&redis.Options{         Addr:     "localhost:6379",         Password: "", // no password set         DB:       0,  // use default DB     })     ... } ``` **核心的redis.NewClient方法,返回的是一个\*redis.Client结构,它就相当于Gorm中的DB数据结构,就是我们要实例化Redis的实例**。这个结构是一个封装了300+个Redis操作的数据结构,你可以使用 `go doc github.com/go-redis/redis/v8.Client` 来观察它封装的Redis操作。 ### 配置 redis.NewClient方法还有一个参数:\*redis.Options 数据结构。这个数据结构就相当于Gorm中的gorm.Config,里面封装了实例化redis.Client的各种配置信息,来看一些重要的配置,都做了注释: ```go // redis的连接配置 type Options struct { // 网络情况 // Default is tcp. Network string // host:port 格式的地址 Addr string // redis的用户名 Username string // redis密码 Password string // redis的database DB int // 连接超时 // Default is 5 seconds. DialTimeout time.Duration // 读超时 // Default is 3 seconds. ReadTimeout time.Duration // 写超时 // Default is ReadTimeout. WriteTimeout time.Duration // 最小空闲连接数 MinIdleConns int // 最大连接时长 MaxConnAge time.Duration // 空闲连接时长 // Default is 5 minutes. -1 disables idle timeout check. IdleTimeout time.Duration ... } ``` 这些配置项相信你也非常熟悉了,既有连接请求的配置项,也有连接池的配置项。 和Gorm的配置封装一样,我们想要给用户提供一个配置即用的缓存服务,需要做如下三个事情: * 自定义一个数据结构,封装redis.Options结构 * 让刚才自定义的结构能生成一个唯一标识(类似Gorm的DSN) * 支持通过配置文件加载这个结构,同时,支持通过Option可变参数来修改它 在framework/contract/redis.go中,我们首先定义**RedisConfig数据结构**,这个结构单纯封装redis.Options就行了,没有其他额外的参数需要设置: ```go // RedisConfig 为hade定义的Redis配置结构 type RedisConfig struct { *redis.Options ``` 同时为这个RedisConfig定义一个唯一标识,来标识一个redis.Client。这里我们选用了Addr、DB、UserName、Network 四个字段值来标识。**基本上这四个字段加起来能标识“用什么账号登录哪个Redis地址的哪个database”了**: ```go // UniqKey 用来唯一标识一个RedisConfig配置 func (config *RedisConfig) UniqKey() string { return fmt.Sprintf("%v_%v_%v_%v", config.Addr, config.DB, config.Username, config.Network) } ``` RedisConfig结构定义完成,下面想要把它加载并支持可修改,我们要结合实例化redis.Client对象来说。 ### 初始化连接 如何封装Redis的连接实例,这个同Gorm的封装一样,使用Option可变参数的方式。还是在 framework/contract/redis.go中继续写入: ```go package contract ... const RedisKey = "hade:redis" // RedisOption 代表初始化的时候的选项 type RedisOption func(container framework.Container, config *RedisConfig) error // RedisService 表示一个redis服务 type RedisService interface { // GetClient 获取redis连接实例 GetClient(option ...RedisOption) (*redis.Client, error) } ``` 定义了一个RedisService,表示Redis服务对外提供的协议,它只有一个GetClient方法,通过这个方法能获取到Redis的一个连接实例redis.Client。 你能看到GetClient方法有一个可变参数RedisOption,这个可变参数是一个函数结构,参数中带有传递进入了的RedisConfig指针,所以**这个RedisOption是有修改RedisConfig结构的能力的**。 那具体提供哪些RedisOption函数呢?和ORM一样,我们要提供多层次的修改方案,包括默认配置、按照配置项进行配置,以及手动配置: * GetBaseConfig获取redis.yaml根目录下的Redis配置,作为默认配置 * GetConfigPath 根据指定配置路径获取Redis配置 * WithRedisConfig 可以直接修改RedisConfig中的redis.Options配置信息 在实现这三个函数之前,有必要先看一下我们的Redis配置文件cofig/testing/redis.yaml: ```yaml timeout: 10s # 连接超时 read_timeout: 2s # 读超时 write_timeout: 2s # 写超时 write: host: localhost # ip地址 port: 3306 # 端口 db: 0 #db username: jianfengye # 用户名 password: "123456789" # 密码 timeout: 10s # 连接超时 read_timeout: 2s # 读超时 write_timeout: 2s # 写超时 conn_min_idle: 10 # 连接池最小空闲连接数 conn_max_open: 20 # 连接池最大连接数 conn_max_lifetime: 1h # 连接数最大生命周期 conn_max_idletime: 1h # 连接数空闲时长 ``` 和database.yaml的配置一样,根级别的作为默认配置,二级配置作为单个Redis的配置,并且二级配置会覆盖默认配置。这里还有一个小心思,特意将这些配置项都和database.yaml保持一致了,这样使用者在配置的时候能减少学习成本。 这三个方法的具体实现和Gorm没有什么太大的区别。基本方法就是使用容器中的配置服务、读取配置信息,然后修改参数中的RedisConfig指针,你可以参考分支中的代码文件[framework/provider/redis/config.go](https://github.com/gohade/coredemo/blob/geekbang/27/framework/provider/redis/config.go)。 我们重点把注意力放在**GetClient方法的实现**上,写在framework/provider/redis/service.go中。类似gorm的GetDB方法,它是一个单例模式,就是一个RedisConfig,只产生一个redis.Client,用一个map加上一个lock来初始化Redis实例: ```go // HadeRedis 代表hade框架的redis实现 type HadeRedis struct { container framework.Container // 服务容器 clients map[string]*redis.Client // key为uniqKey, value为redis.Client (连接池) lock *sync.RWMutex } ``` 在GetClient函数中,首先还是获取基本Redis配置 redisConfig,使用参数opts对redisConfig进行修改,最后判断当前redisConfig是否已经实例化了: * 如果已经实例化,返回实例化redis.Client; * 如果未实例化,实例化redis.Client,返回实例化的redis.Client。 ```go // GetClient 获取Client实例 func (app *HadeRedis) GetClient(option ...contract.RedisOption) (*redis.Client, error) { // 读取默认配置 config := GetBaseConfig(app.container) // option对opt进行修改 for _, opt := range option { if err := opt(app.container, config); err != nil { return nil, err } } // 如果最终的config没有设置dsn,就生成dsn key := config.UniqKey() // 判断是否已经实例化了redis.Client app.lock.RLock() if db, ok := app.clients[key]; ok { app.lock.RUnlock() return db, nil } app.lock.RUnlock() // 没有实例化gorm.DB,那么就要进行实例化操作 app.lock.Lock() defer app.lock.Unlock() // 实例化gorm.DB client := redis.NewClient(config.Options) // 挂载到map中,结束配置 app.clients[key] = client return client, nil } ``` 这里只讲了Redis服务的接口和服务实现的关键函数,其中provider的实现基本上和ORM的一致,没有什么特别,就不在这里重复列出代码了。 到这里我们就将Redis的服务融合进入hade框架了。但Redis只是缓存服务的一种实现,我们这节课最终目标是想实现一个缓存服务。 ## 缓存服务 缓存服务的使用方式其实非常多,我们可以设置有超时/无超时的缓存,也可以使用计数器缓存,一份好的缓存接口的设计,能对应用的缓存使用帮助很大。 所以这一部分,相比缓存服务的具体实现,**缓存服务的协议设计直接影响了这个服务的可用性**,我们要重点理解对缓存协议的设计。 ### 协议 实现一个服务的三步骤,服务协议、服务提供者、服务实例。就先从协议开始,我们希望这个缓存服务提供哪些能力呢? 首先,缓存协议一定是有两个方法,一个设置缓存、一个获取缓存。设定为Get方法为获取缓存,Set方法为设置缓存。 ```go // Get 获取某个key对应的值 Get(ctx context.Context, key string) (string, error) // Set 设置某个key和值到缓存,带超时时间 Set(ctx context.Context, key string, val string, timeout time.Duration) error ``` 同时,注意设置缓存的时候,又区分出两种需求,我们需要设置带超时时间的缓存,也需要设置不带超时时间的、永久的缓存。所以,Set方法衍生出Set和SetForever两种。 ```go // SetForever 设置某个key和值到缓存,不带超时时间 SetForever(ctx context.Context, key string, val string) error ``` 在设置了某个key之后,会不会需要修改这个缓存key的缓存时长呢?完全是有可能的,比如将某个key的缓存时长加大,或者想要获取某个key的缓存时长,所以我们再把注意力放在缓存时长的操作上,提供对缓存时长的操作函数SetTTL和GetTTL: ```go // SetTTL 设置某个key的超时时间 SetTTL(ctx context.Context, key string, timeout time.Duration) error // GetTTL 获取某个key的超时时间 GetTTL(ctx context.Context, key string) (time.Duration, error) ``` 再来,Get和Set目前对应的value值为string,但是我们希望value值能不仅仅是一个字符串,它还可以直接是一个对象,这样缓存服务就能存储和获取一个对象出来,能大大方便缓存需求。 所以我们定义两个GetObj和SetObj方法,来实现对象的缓存存储和获取,但是这个对象在实际存储的时候,又势必要进行序列化和反序列的过程,所以我们对存储和获取的对象再增加一个要求,让它实现官方库的BinaryMarshaler和BinaryUnMarshaler接口: ```go // GetObj 获取某个key对应的对象, 对象必须实现 https://pkg.go.dev/encoding#BinaryUnMarshaler GetObj(ctx context.Context, key string, model interface{}) error // SetObj 设置某个key和对象到缓存, 对象必须实现 https://pkg.go.dev/encoding#BinaryMarshaler SetObj(ctx context.Context, key string, val interface{}, timeout time.Duration) error // SetForeverObj 设置某个key和对象到缓存,不带超时时间,对象必须实现 https://pkg.go.dev/encoding#BinaryMarshaler SetForeverObj(ctx context.Context, key string, val interface{}) error ``` 现在,我们已经可以一个key进行缓存获取和设置了,但是有时候要同时对多个key做缓存的获取和设置,来设置对多个key进行操作的方法GetMany和SetMany: ```go // GetMany 获取某些key对应的值 GetMany(ctx context.Context, keys []string) (map[string]string, error) // SetMany 设置多个key和值到缓存 SetMany(ctx context.Context, data map[string]string, timeout time.Duration) error ``` 在实际业务中,我们还会有一些计数器的需求,需要将计数器存储到缓存,同时也要能对这个计数器缓存进行增加和减少的操作。可以为计数器缓存设计Calc、Increment、Decrement的接口: ```go // Calc 往key对应的值中增加step计数 Calc(ctx context.Context, key string, step int64) (int64, error) // Increment 往key对应的值中增加1 Increment(ctx context.Context, key string) (int64, error) // Decrement 往key对应的值中减去1 Decrement(ctx context.Context, key string) (int64, error) ``` 缓存的使用有一种Cache-Aside模式,可以提升“获取数据”的性能。可能你没有听过这个名字,但其实我们都用过,这个模式描述的就是在实际操作之前,先去缓存中查看有没有对应的数据,如果有的话,不进行操作,如果没有的话才进行实际操作生成数据,并且把数据存储在缓存中。 我们希望缓存服务也能支持这种Cache-Aside模式。如何支持呢? 首先,要有一个生成数据的通用方法结构,我们定义为RememberFunc,让这个函数将服务容器传递进去,这样在具体的实现中,使用者就可以从服务容器中获取各种各样的具体注册服务了,能大大增强这个RemeberFunc的实现能力: ```go // RememberFunc 缓存的Remember方法使用,Cache-Aside模式对应的对象生成方法 type RememberFunc func(ctx context.Context, container framework.Container) (interface{}, error) ``` 然后,我们为缓存服务定义一个Remember方法,来实现这个Cache-Aside模式。 ```go // Remember 实现缓存的Cache-Aside模式, 先去缓存中根据key获取对象,如果有的话,返回,如果没有,调用RememberFunc 生成 Remember(ctx context.Context, key string, timeout time.Duration, rememberFunc RememberFunc, model interface{}) error ``` 它的参数来仔细看下。除了context之外,有一个key,代表这个缓存使用的key,其次是timeout 代表缓存时长,接着是前面定义的 RememberFunc了,代表如果缓存中没有这个key,就调用RememberFunc函数来生成数据对象。 这个数据对象从哪里输出呢?就是这里的最后一个参数model了,当然这个Obj必须实现BinaryMarshaler和BinaryUnmarshaler接口。这样定义之后,Remember的具体实现就简单了。 看这个我在单元测试代码provider/cache/services/redis\_test.go中写的测试: ```go type Bar struct { Name string } func (b *Bar) MarshalBinary() ([]byte, error) { return json.Marshal(b) } func (b *Bar) UnmarshalBinary(bt []byte) error { return json.Unmarshal(bt, b) } Convey("remember op", func() { objNew := Bar{} objNewFunc := func(ctx context.Context, container framework.Container) (interface{}, error) { obj := &Bar{ Name: "bar", } return obj, nil } err = mc.Remember(ctx, "foo_remember", 1*time.Minute, objNewFunc, &objNew) So(err, ShouldBeNil) So(objNew.Name, ShouldEqual, "bar") }) ``` 我们定义了Bar结构,它实现了BinaryMarshaler和BinaryUnmarshaler接口,并且定义了一个objNewFunc方法实现了前面我们定义的RememberFunc。 之后可以使用Remember方法来为这个方法设置一个Cache-Aside缓存,它的key为foo\_remember,缓存时长为1分钟。 最后回看一下我们对缓存的协议定义,各种缓存的设置和获取方法都有了,还差删除缓存的方法对吧。所以来定义删除单个key的缓存和删除多个key的缓存: ```go // Del 删除某个key Del(ctx context.Context, key string) error // DelMany 删除某些key DelMany(ctx context.Context, keys []string) error ``` 到这里缓存协议就定义完成了,一共16个方法,要好好理解下这些方法的定义,还是那句话,理解如何定义协议比实现更为重要。 ![](https://static001.geekbang.org/resource/image/da/e5/da9b83e6856e5fd523bc270981846fe5.jpg?wh=2364x2273) ## 实现 下面来实现这个缓存服务。前面一再强调了,Redis只是缓存的一种实现,Redis之外,我们可以用不同的存储来实现缓存,甚至,可以使用内存来实现。目前hade框架支持内存和Redis实现缓存,这里我们就先看看如何用Redis来实现缓存。 由于缓存有不同实现,所以和日志服务一样,**要使用配置文件来cache.yaml中的driver字段,来区别使用哪个缓存**。如果driver为redis,表示使用Redis来实现缓存,如果为memory,表示用内存来实现缓存。当然如果使用Redis的话,就需要同时带上Redis连接的各种参数,参数关键字都类似前面说的Redis服务的配置。 一个典型的cache.yaml的配置如下: ```go driver: redis # 连接驱动 host: 127.0.0.1 # ip地址 port: 6379 # 端口 db: 0 #db timeout: 10s # 连接超时 read_timeout: 2s # 读超时 write_timeout: 2s # 写超时 #driver: memory # 连接驱动 ``` 那对应到具体实现上,区分使用哪个缓存驱动,我们会在服务提供者provider中来进行。在provider中,注意下Register方法,注册具体的服务实例方法时,要先读取配置中的cache.driver路径: ```go // Register 注册一个服务实例 func (l *HadeCacheProvider) Register(c framework.Container) framework.NewInstance { if l.Driver == "" { tcs, err := c.Make(contract.ConfigKey) if err != nil { // 默认使用console return services.NewMemoryCache } cs := tcs.(contract.Config) l.Driver = strings.ToLower(cs.GetString("cache.driver")) } // 根据driver的配置项确定 switch l.Driver { case "redis": return services.NewRedisCache case "memory": return services.NewMemoryCache default: return services.NewMemoryCache } } ``` 如果是Redis驱动,我们使用service.NewRedisCache来初始化一个Redis连接,定义RedisCache结构来存储redis.Client。 **在初始化的时候,先确定下容器中是否已经绑定了Redis服务,如果没有的话,做一下绑定操作**。这个行为能让我们的缓存容器更为安全。 接着使用cache.yaml中的配置,来初始化一个redis.Client,这里使用的redisService.GetClient和redis.WithConfigPath,都是上面设计Redis服务的时候刚设计实现的方法。最后将redis.Client 封装到RedisCache中,返回: ```go import ( "context" "errors" redisv8 "github.com/go-redis/redis/v8" "github.com/gohade/hade/framework" "github.com/gohade/hade/framework/contract" "github.com/gohade/hade/framework/provider/redis" "sync" "time" ) // RedisCache 代表Redis缓存 type RedisCache struct { container framework.Container client *redisv8.Client lock sync.RWMutex } // NewRedisCache 初始化redis服务 func NewRedisCache(params ...interface{}) (interface{}, error) { container := params[0].(framework.Container) if !container.IsBind(contract.RedisKey) { err := container.Bind(&redis.RedisProvider{}) if err != nil { return nil, err } } // 获取redis服务配置,并且实例化redis.Client redisService := container.MustMake(contract.RedisKey).(contract.RedisService) client, err := redisService.GetClient(redis.WithConfigPath("cache")) if err != nil { return nil, err } // 返回RedisCache实例 obj := &RedisCache{ container: container, client: client, lock: sync.RWMutex{}, } return obj, nil } ``` 好,有Redis缓存的实例了,下面来看16个方法的实现。 Set系列的方法一共有Set/SetObj/SetMany/SetForever/SetForeverObj/SetTTL 6个,其他5个相对简单一些,在生成的redis.Client结构中都有对应实现,我们直接使用redis.Client调用即可,就不赘述了。其中SetMany方法相对复杂些,我们着重说明下。 在Redis中,SetMany这种为多个key设置缓存的方法,一般可以遍历key,然后一个个调用Set方法,但是这样效率就低了。更好的实现方式是使用pipeline。 什么是Redis的pipeline呢?Redis的客户端和服务端的交互,采用的是客户端-服务端模式,就是每个客户端的请求发送到Redis服务端,都会有一个完整的响应。所以,向服务端发送n个请求,就对应有n次响应。那么**对于这种n个请求且n个请求没有上下文逻辑关系,我们能不能批量发送,但是只发送一次请求,然后只获取一次响应呢**? Redis的pipeline就是这个原理,它将多个请求合成为一个请求,批量发送给Redis服务端,并且只从服务端获取一次数据,拿到这些请求的所有结果。 我们的SetMany就很符合这个场景。具体的代码如下: ```go // SetMany 设置多个key和值到缓存 func (r *RedisCache) SetMany(ctx context.Context, data map[string]string, timeout time.Duration) error { pipline := r.client.Pipeline() cmds := make([]*redisv8.StatusCmd, 0, len(data)) for k, v := range data { cmds = append(cmds, pipline.Set(ctx, k, v, timeout)) } _, err := pipline.Exec(ctx) return err } ``` 先用redis.Client.Pipeline() 来创建一个pipeline管道,然后用一个redis.StatusCmd数组来存储要发送的所有命令,最后调用一次pipeline.Exec来一次发送命令。 Set方法就讲到这里,Get系列的方法一共有4个,Get/GetObj/GetMany/GetTTL。 在实现Get系列方法的时候有地方需要注意下,因为**Get是有可能Get一个不存在的key的**,对于这种不存在的key是否返回error,是一个可以稍微思考的话题。 比如Get这个方法,返回的是string和error,如果对于一个不存在的key,返回了空字符串+空error的组合,而对于一个设置了空字符串的key,也返回空字符串+空error的组合,这里其实是丢失了“是否存在key”的信息的。 所以,对于这些不存在的key,我们设计返回一个 ErrKeyNotFound 的自定义error。像Get函数就实现为如下: ```go // Get 获取某个key对应的值 func (r *RedisCache) Get(ctx context.Context, key string) (string, error) { val, err := r.client.Get(ctx, key).Result() // 这里判断了key是否为空 if errors.Is(err, redisv8.Nil) { return val, ErrKeyNotFound } return val, err } ``` 其他Get相关的实现没有什么难点。 除了Get系列和Set系列,其他的方法有Calc、Increment、Decrement、Del、DelMany 都没有什么太复杂的逻辑,都是redis.Client的具体封装。 最后看下Remember这个方法: ```go // Remember 实现缓存的Cache-Aside模式, 先去缓存中根据key获取对象,如果有的话,返回,如果没有,调用RememberFunc 生成 func (r *RedisCache) Remember(ctx context.Context, key string, timeout time.Duration, rememberFunc contract.RememberFunc, obj interface{}) error { err := r.GetObj(ctx, key, obj) // 如果返回为nil,说明有这个key,且有数据,obj已经注入了,返回nil if err == nil { return nil } // 有err,但是并不是key不存在,说明是有具体的error的,不能继续往下执行了,返回err if !errors.Is(err, ErrKeyNotFound) { return err } // 以下是key不存在的情况, 调用rememberFunc objNew, err := rememberFunc(ctx, r.container) if err != nil { return err } // 设置key if err := r.SetObj(ctx, key, objNew, timeout); err != nil { return err } // 用GetObj将数据注入到obj中 if err := r.GetObj(ctx, key, obj); err != nil { return err } return nil } ``` 前面说过Remember方法是Cache-Aside模式的实现,它的逻辑是先判断缓存中是否有这个key,如果有的话,直接返回对象,如果没有的话,就调用RememberFunc方法来实例化这个对象,并且返回这个实例化对象。 好了,这里的framework/provider/cache/redis.go我们实现差不多了。 ## 验证 来做验证,我们为缓存服务写一个简单的路由,在这个路由中: * 获取缓存服务 * 设置foo为key的缓存,值为bar * 获取foo为key的缓存,把值打印到控制台 * 删除foo为key的缓存 ```go // DemoCache cache的简单例子 func (api *DemoApi) DemoCache(c *gin.Context) { logger := c.MustMakeLog() logger.Info(c, "request start", nil) // 初始化cache服务 cacheService := c.MustMake(contract.CacheKey).(contract.CacheService) // 设置key为foo err := cacheService.Set(c, "foo", "bar", 1*time.Hour) if err != nil { c.AbortWithError(500, err) return } // 获取key为foo val, err := cacheService.Get(c, "foo") if err != nil { c.AbortWithError(500, err) return } logger.Info(c, "cache get", map[string]interface{}{ "val": val, }) // 删除key为foo if err := cacheService.Del(c, "foo"); err != nil { c.AbortWithError(500, err) return } c.JSON(200, "ok") } ``` 增加对应的路由: ```go r.GET("/demo/cache/redis", api.DemoRedis) ``` 在浏览器中请求地址: [http://localhost:8888/demo/cache/redis](http://localhost:8888/demo/cache/redis): ![](https://static001.geekbang.org/resource/image/2a/50/2acd5f030ab6b4d5ce8e05ec0c961850.png?wh=583x174) 查看控制台输出的日志: ![](https://static001.geekbang.org/resource/image/6c/14/6cf8ba131ba431303f5f77560015f814.png?wh=617x130) 可以明显看到cacheService.Get的数据为bar,打印了出来。验证正确! 本节课我们主要修改了framework目录下Redis和cache相关的代码。目录截图也放在这里供你对比查看,所有代码都已经上传到[geekbang/27](https://github.com/gohade/coredemo/tree/geekbang/27)分支了。 ![](https://static001.geekbang.org/resource/image/04/03/04d8a0896596b62ab57848a882d82903.png?wh=355x1116) ## 小结 除DB之外,缓存是我们最常使用的一个存储了,今天我们先是实现了Redis的服务,再用Redis服务实现了一个缓存服务。 第一部分的Redis服务,同上一节课ORM的逻辑一样,我们只是将go-redis库进行了封装,具体怎么使用,还是依赖你在实际工作中多使用、多琢磨,网上也有很多go-redis库的相关资料。 ![](https://static001.geekbang.org/resource/image/da/e5/da9b83e6856e5fd523bc270981846fe5.jpg?wh=2364x2273) 在第二部分实现的过程中,相信你现在能理解,**一个服务的接口设计,就是一个“我们想要什么服务”的思考过程**。比如在缓存服务接口设计中,我们定义了16个方法,囊括了Get/Set/Del/Remember等一系列方法,你可以对照思维导图复习一下。但这些方法并不是随便拍脑袋出来的,是因为有设置缓存、获取缓存、删除缓存等需求,才这样设计的。 ### 思考题 目前hade框架支持内存和Redis实现缓存,我们今天展示了Redis的实现。缓存服务的内存缓存如何实现呢?可以先思考一下,如果是你来实现会如何设计呢?如果有兴趣,你可以自己动手操作一下。完成之后,你可以比对GitHub分支上我已经实现的版本,看看有没有更好的方案。 欢迎在留言区分享你的思考。感谢你的收听,如果你觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。我们下节课见~