Applying cache

The first step is to download dependency with Redis as the connection driver:

$ go get github.com/garyburd/redigo/redis

The Redigo is our communication interface with Redis. We will use Redis as a cache tool of our microservice.

Now, we will create the cache.go file. This file is responsible for delivering us a configured instance of the cache. Similar to the other files we've ever created, let's declare the package where we are working and the dependencies, as follows:

    package main 
 
    import ( 
      "log" 
      "time" 
 
      redigo "github.com/garyburd/redigo/redis" 
    ) 

Then, we create an interface to create a pool of connections to the Redis and a struct with all the settings of the connection. Note that the instance of our pool will also be in the struct.

Pool is the interface to pool of Redis:

      type Pool interface {
Get() redigo.Conn
}

Cache is the struct with cache configuration:

      type Cache struct {

Enable bool

MaxIdle int

MaxActive int

IdleTimeoutSecs int

Address string

Auth string

DB string

Pool *redigo.Pool

}

Now, we'll create the method of our struct responsible for giving us a new connection pool.  Redigo has a struct called Pool, which, when configured correctly, returns exactly what we need. In our cache configuration, we have the option Enable. If the option is enabled, we will apply the settings to return connection pooling; if it is not enabled, we simply ignore this process and return null. This implies that the pool will be validated at final; if something is wrong, we launch a fatal error and stop the server, followed by provisioning the service:

// NewCachePool return a new instance of the redis pool
func (cache *Cache) NewCachePool() *redigo.Pool {
if cache.Enable {
pool := &redigo.Pool{
MaxIdle: cache.MaxIdle,
MaxActive: cache.MaxActive,
IdleTimeout: time.Second * time.Duration(cache.IdleTimeoutSecs),
Dial: func() (redigo.Conn, error) {
c, err := redigo.Dial("tcp", cache.Address)
if err != nil {
return nil, err
}
if _, err = c.Do("AUTH", cache.Auth); err != nil {
c.Close()
return nil, err
}
if _, err = c.Do("SELECT", cache.DB); err != nil {
c.Close()
return nil, err
}
return c, err
},
TestOnBorrow: func(c redigo.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
c := pool.Get() // Test connection during init
if _, err := c.Do("PING"); err != nil {
log.Fatal("Cannot connect to Redis: ", err)
}
return pool
}
return nil
}

Now, we create the method that searches our cache and that enters the data in our cache. The getValue method receives, as a parameter, the search key in the cache. The setValue function receives, as parameters, the key and the value that should be inserted in the cache:

func (cache *Cache) getValue(key interface{}) (string, error) { 
     if cache.Enable { 
                 conn := cache.Pool.Get() 
                 defer conn.Close() 
                 value, err := redigo.String(conn.Do("GET", key)) 
                 return value, err 
     } 
     return "", nil 
} 
 
func (cache *Cache) setValue(key interface{}, value interface{}) error { 
     if cache.Enable { 
                 conn := cache.Pool.Get() 
                 defer conn.Close() 
                 _, err := redigo.String(conn.Do("SET", key, value)) 
                 return err 
     } 
     return nil 
} 

In this way, our file, cache.go, is mounted and ready to be used in our application. However, we have to make some changes to our cache file before it is used. We will start by changing our file main.go.

In the file main.go, let's add a new import. When we import flags, we can receive information directly from the command line, and use them in our configuration of Redis:

    import ( 
      "flag" 
      "fmt" 
      "github.com/jmoiron/sqlx" 
      _ "github.com/lib/pq" 
      "log" 
      "os" 
    )    

What we must do now is add the options that can be passed to the command line. This change also happens in the file main.go. First, we create an instance of our Cache, then add to the pointer of this instance, the settings. All settings have a default value if no argument is passed on the command line.

The order of the settings is as follows:

  1. Address: This is where Redis runs
  2. Auth: This is the password used to connect to the Redis
  3. DB: This is the Redis Bank that will be used as cache
  4. MaxIdle: This denotes the maximum number of connections that can be idle
  5. MaxActive: This denotes the maximum number of connections that can be active
  6. IdleTimeoutSecs: This is the time a connection timeout leads to enter activity

At the end of all settings, we'll create a new pool of connections with the NewCachePool method and pass the pointer to our cache instance, as follows:

    func main() { 
     cache := Cache{Enable: true} 
      
     flag.StringVar( 
                 &cache.Address,  
                 "redis_address",  
                 os.Getenv("APP_RD_ADDRESS"),  
                 "Redis Address", 
     ) 
 
     flag.StringVar( 
                 &cache.Auth, 
                 "redis_auth", 
                 os.Getenv("APP_RD_AUTH"),  
                 "Redis Auth", 
     ) 
      
     flag.StringVar( 
                 &cache.DB, 
                 "redis_db_name", 
                 os.Getenv("APP_RD_DBNAME"), 
                 "Redis DB name", 
     ) 
      
     flag.IntVar( 
                 &cache.MaxIdle, 
                 "redis_max_idle", 
                 10, 
                 "Redis Max Idle", 
     ) 
 
     flag.IntVar( 
                 &cache.MaxActive, 
                 "redis_max_active", 
                 100, 
                 "Redis Max Active", 
     ) 
      
     flag.IntVar( 
                 &cache.IdleTimeoutSecs, 
                 "redis_timeout", 
                 60, 
                 "Redis timeout in seconds", 
     ) 
      
     flag.Parse() 
     cache.Pool = cache.NewCachePool() 
     ... 

Another change that should be made within main.go is to pass the App cache to the initialize method:

    ...
a.Initialize(
cache,
db,
)
...

We edit the app.go file to effectively use the cache according to the diagram given earlier. The first change is in the struct of the App, because it happens to store the cache:

    type App struct { 
      DB         *sqlx.DB 
      Router *mux.Router 
      Cache   Cache 
    } 

Now, we should make sure that the initialize method receives the cache and passes the value to the instance of the App:

    func (a *App) Initialize(cache Cache, db *sqlx.DB) {   
 
      a.Cache = cache 
      a.DB = db 
      a.Router = mux.NewRouter() 
      a.initializeRoutes() 
    } 

Now, we can apply the cache in any part of the App; let's modify the getUser method to use the cache structure that we talked about earlier. Two parts of the method should be changed, so that the cache is applied.

First, rather than seek user data directly in PostgreSQL, we check if the data is already in the cache. If the data is already in the cache, we're not even going to get the data in the base.

The second amendment is that if the data is not in cache, perform a search in the database, and register this same data into the cache before returning a response to the request. In this way, in a subsequent search, the data will be in the cache, and the query on the database will not be performed:

    func (a *App) getUser(w http.ResponseWriter, r *http.Request) { 
      vars := mux.Vars(r) 
      id, err := strconv.Atoi(vars["id"]) 
      if err != nil { 
       respondWithError(w, http.StatusBadRequest, "Invalid product ID") 
       return 
     } 
 
     if value, err := a.Cache.getValue(id); err == nil && len(value) != 0 { 
       w.Header().Set("Content-Type", "application/json") 
       w.WriteHeader(http.StatusOK) 
       w.Write([]byte(value)) 
       return 
     } 
 
     user := User{ID: id} 
     if err := user.get(a.DB); err != nil { 
       switch err { 
        case sql.ErrNoRows: 
           respondWithError(w, http.StatusNotFound, "User not found") 
        default: 
           respondWithError(w, http.StatusInternalServerError, err.Error()) 
       } 
       return 
     } 
 
     response, _ := json.Marshal(user) 
     if err := a.Cache.setValue(user.ID, response); err != nil { 
       respondWithError(w, http.StatusInternalServerError, err.Error()) 
       return 
     } 
 
     w.Header().Set("Content-Type", "application/json") 
     w.WriteHeader(http.StatusOK) 
     w.Write(response) 
    } 

With the preceding amendments, after consultation in the database, the cache is replaced by query values and responds without the need for access to the database. This approach is sufficient for most cases but, in some scenarios, where the load of requests is high or the database may be very demanding, even having a cache, the database can become a point of slowness. The next cache strategy is very interesting for this kind of problem.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.96.5