Basic Structure of a Cache System
Cached data needs to be stored in memory in order to be accessed quickly. What data structure should be used to store the data items? In general, a hash table is used to store the data items, as this provides better performance for accessing the data. In the Go language, we don't need to implement our own hash table because the built-in type map
already implements a hash table. So, we can directly store the cache data items in a map
.
Since the cache system also supports cleaning expired data, the cache data items should have a lifespan. This means that the cache data items need to be encapsulated and saved in the cache system. To do this, we first need to implement the cache data item. Create a new directory cache
in the GOPATH/src
directory, and create a source file cache.go
:
package cache
import (
"encoding/gob"
"fmt"
"io"
"os"
"sync"
"time"
)
type Item struct {
Object interface{} // The actual data item
Expiration int64 // Lifespan
}
// Check if the data item has expired
func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
}
In the above code, we define a Item
structure which has two fields. Object
is used to store data objects of any type, and Expiration
stores the expiration time of the data item. We also provide an Expired()
method for the Item
type, which returns a boolean value indicating whether the data item has expired. It's important to note that the expiration time of a data item is a Unix timestamp measured in nanoseconds. How do we determine if a data item has expired? It's actually quite simple. We record the expiration time of each data item, and the cache system periodically checks each data item. If the expiration time of a data item is earlier than the current time, the data item is removed from the cache system. To do this, we'll use the time module to implement periodic tasks.
With this, we can now implement the framework of the cache system. The code is as follows:
const (
// Flag for no expiration time
NoExpiration time.Duration = -1
// Default expiration time
DefaultExpiration time.Duration = 0
)
type Cache struct {
defaultExpiration time.Duration
items map[string]Item // Store cache data items in a map
mu sync.RWMutex // Read-write lock
gcInterval time.Duration // Expiration data cleaning interval
stopGc chan bool
}
// Clean expired cache data items
func (c *Cache) gcLoop() {
ticker := time.NewTicker(c.gcInterval)
for {
select {
case <-ticker.C:
c.DeleteExpired()
case <-c.stopGc:
ticker.Stop()
return
}
}
}
In the above code, we have implemented the Cache
structure, which represents the cache system structure. The items
field is a map used to store the cache data items. As you can see, we have also implemented the gcLoop()
method, which schedules the DeleteExpired()
method to be periodically executed using a time.Ticker
. A ticker
created using time.NewTicker()
will send data from its ticker.C
channel at the specified gcInterval
interval. We can use this characteristic to periodically execute the DeleteExpired()
method.
To ensure that the gcLoop()
function can end normally, we listen for data from the c.stopGc
channel. If there is data sent to this channel, we stop the execution of gcLoop()
. Also note that we define the NoExpiration
and DefaultExpiration
constants, where the former represents a data item that never expires and the latter represents a data item with a default expiration time. How do we implement DeleteExpired()
? See the code below:
// Delete a cache data item
func (c *Cache) delete(k string) {
delete(c.items, k)
}
// Delete expired data items
func (c *Cache) DeleteExpired() {
now := time.Now().UnixNano()
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.items {
if v.Expiration > 0 && now > v.Expiration {
c.delete(k)
}
}
}
As you can see, the DeleteExpired()
method is quite simple. We just need to iterate through all the data items and delete the expired ones.