你好,我是轩脉刃。

上面两节课把数据库操作接入到hade框架中了,现在我们能使用容器中的ORM服务来操作数据库了。在实际工作中,一旦数据库出现性能瓶颈,除了优化数据库本身之外,另外一个常用的方法是使用缓存来优化业务请求。所以这节课,我们来讨论一下,hade框架如何提供缓存支持。

现在的Web业务,大部分都是使用Redis来做缓存实现。但是,缓存的实现方式远不止Redis一种,比如在Redis出现之前,Memcached一般是缓存首选;在单机上,还可以使用文件来存储数据,又或者直接使用进程的内存也可以进行缓存实现。

缓存服务的底层使用哪个存储方式,和具体的业务架构原型相关。我个人在不同业务场景中用过不少的缓存存储方案,不过业界用的最多的Redis,还是优点比较突出。相比文件存储,它能集中分布式管理;而相比Memcached,优势在于多维度的存储数据结构。所以,顺应潮流,我们hade框架主要也针对使用Redis来实现缓存服务。

我们这节课会创建两个服务,一个是Redis服务,提供对Redis的封装,另外一个是缓存服务,提供一系列对“缓存”的统一操作。而这些统一操作,具体底层是由Redis还是内存进行驱动的,这个可以根据配置决定。

下面我们一个个来讨论吧。

Redis服务

首先封装一个可以对Redis进行操作的服务。和封装ORM一样,我们自己并不实现Redis的底层传输协议和操作封装,只将Redis“创建连接”的过程封装在hade中就行了。

这里我们就选择go-redis这个库来实现对Redis的连接。这个库目前也是Golang开源社区最常用的Redis库,有12.8k的star数,使用的是BSD 协议,可以引用,可以修改,但是修改的同时要保留版权声明,这里我们并不需要修改,所以BSD已经足够了。这个库目前是v8版本,可以使用 go get github.com/go-redis/redis/v8 来引入它。

go-redis的连接非常简单,我们看官网的例子,就看创建连接部分:

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的各种配置信息,来看一些重要的配置,都做了注释:

// 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的配置封装一样,我们想要给用户提供一个配置即用的缓存服务,需要做如下三个事情:

在framework/contract/redis.go中,我们首先定义RedisConfig数据结构,这个结构单纯封装redis.Options就行了,没有其他额外的参数需要设置:

// RedisConfig 为hade定义的Redis配置结构
type RedisConfig struct {
   *redis.Options

同时为这个RedisConfig定义一个唯一标识,来标识一个redis.Client。这里我们选用了Addr、DB、UserName、Network 四个字段值来标识。基本上这四个字段加起来能标识“用什么账号登录哪个Redis地址的哪个database”了

// 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中继续写入:

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一样,我们要提供多层次的修改方案,包括默认配置、按照配置项进行配置,以及手动配置:

在实现这三个函数之前,有必要先看一下我们的Redis配置文件cofig/testing/redis.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

我们重点把注意力放在GetClient方法的实现上,写在framework/provider/redis/service.go中。类似gorm的GetDB方法,它是一个单例模式,就是一个RedisConfig,只产生一个redis.Client,用一个map加上一个lock来初始化Redis实例:

// 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是否已经实例化了:

// 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方法为设置缓存。

// 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两种。

// SetForever 设置某个key和值到缓存,不带超时时间
SetForever(ctx context.Context, key string, val string) error

在设置了某个key之后,会不会需要修改这个缓存key的缓存时长呢?完全是有可能的,比如将某个key的缓存时长加大,或者想要获取某个key的缓存时长,所以我们再把注意力放在缓存时长的操作上,提供对缓存时长的操作函数SetTTL和GetTTL:

// 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接口:

// 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:

// 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的接口:

// 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的实现能力:

// RememberFunc 缓存的Remember方法使用,Cache-Aside模式对应的对象生成方法
type RememberFunc func(ctx context.Context, container framework.Container) (interface{}, error)

然后,我们为缓存服务定义一个Remember方法,来实现这个Cache-Aside模式。

// 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中写的测试:

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的缓存:

// Del 删除某个key
Del(ctx context.Context, key string) error
// DelMany 删除某些key
DelMany(ctx context.Context, keys []string) error

到这里缓存协议就定义完成了,一共16个方法,要好好理解下这些方法的定义,还是那句话,理解如何定义协议比实现更为重要。

实现

下面来实现这个缓存服务。前面一再强调了,Redis只是缓存的一种实现,Redis之外,我们可以用不同的存储来实现缓存,甚至,可以使用内存来实现。目前hade框架支持内存和Redis实现缓存,这里我们就先看看如何用Redis来实现缓存。

由于缓存有不同实现,所以和日志服务一样,要使用配置文件来cache.yaml中的driver字段,来区别使用哪个缓存。如果driver为redis,表示使用Redis来实现缓存,如果为memory,表示用内存来实现缓存。当然如果使用Redis的话,就需要同时带上Redis连接的各种参数,参数关键字都类似前面说的Redis服务的配置。

一个典型的cache.yaml的配置如下:

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路径:

// 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中,返回:

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就很符合这个场景。具体的代码如下:

// 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函数就实现为如下:

// 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这个方法:

// 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我们实现差不多了。

验证

来做验证,我们为缓存服务写一个简单的路由,在这个路由中:

// 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")
}

增加对应的路由:

r.GET("/demo/cache/redis", api.DemoRedis)

在浏览器中请求地址: http://localhost:8888/demo/cache/redis

查看控制台输出的日志:

可以明显看到cacheService.Get的数据为bar,打印了出来。验证正确!

本节课我们主要修改了framework目录下Redis和cache相关的代码。目录截图也放在这里供你对比查看,所有代码都已经上传到geekbang/27分支了。

小结

除DB之外,缓存是我们最常使用的一个存储了,今天我们先是实现了Redis的服务,再用Redis服务实现了一个缓存服务。

第一部分的Redis服务,同上一节课ORM的逻辑一样,我们只是将go-redis库进行了封装,具体怎么使用,还是依赖你在实际工作中多使用、多琢磨,网上也有很多go-redis库的相关资料。

在第二部分实现的过程中,相信你现在能理解,一个服务的接口设计,就是一个“我们想要什么服务”的思考过程。比如在缓存服务接口设计中,我们定义了16个方法,囊括了Get/Set/Del/Remember等一系列方法,你可以对照思维导图复习一下。但这些方法并不是随便拍脑袋出来的,是因为有设置缓存、获取缓存、删除缓存等需求,才这样设计的。

思考题

目前hade框架支持内存和Redis实现缓存,我们今天展示了Redis的实现。缓存服务的内存缓存如何实现呢?可以先思考一下,如果是你来实现会如何设计呢?如果有兴趣,你可以自己动手操作一下。完成之后,你可以比对GitHub分支上我已经实现的版本,看看有没有更好的方案。

欢迎在留言区分享你的思考。感谢你的收听,如果你觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。我们下节课见~