This project is an implement of Profile server whci can save user profiles and then provide someone's profile. It is using Dynamo db and Redis cache using golang.
As well as shown the diagram, this use case is very simple in which user1 can create or update the profile and then user2 may retrieve the profile of user1. It is a very usuful case and normally happen in the server side but requires high reliable and fast operation.
This diagram shows how to use Redis and DynamoDB in order to manage profile data.
This diagram represents the structure of class where the profile service is the generalization of Service Interface and BaseService Class. Also, there are two type of database, Redis cache and Dynamo DB.
// Configuration loading
var configFileName string = "configs/config.json"
conf := config.GetInstance()
if !conf.Load(configFileName) {
log.E("Failed to load config file: %s", configFileName)
os.Exit(1)
}
log.D("Configuration has been loaded.")
// Initiate Dynamo database
dberror := dynamo.NewDatabase(conf.Dynamo)
if dberror != nil {
log.D("Faile to open dynamodb: %v", dberror.Error())
}
// Initiate radis for in-memory cache
rediscache.NewRedisCache(conf.Redis)
// Insert is the api to append an Item
func Insert(w http.ResponseWriter, r *http.Request) {
// parse the data
var item data.UserProfile
_ = json.NewDecoder(r.Body).Decode(&item)
log.D("item: %+v", item)
if err := dynamo.Write(item); err != nil {
log.E("Got error calling PutItem: %v", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
// put the item into rediscache
key := item.UID // UID to identify the profile
_, rErr := rediscache.SetCache(key, &item)
if rErr != nil {
log.E("Error of setCache: %v", rErr)
w.WriteHeader(http.StatusServiceUnavailable)
return
}
log.D("Successfully inserted in redis cache")
w.WriteHeader(http.StatusOK)
}
// Retrieve is the api to search an Item
func Retrieve(w http.ResponseWriter, r *http.Request) {
uid := strings.Split(r.URL.Path, "/")[2]
log.D("Looking for uid: %v ...", uid)
// search in redis cache
cache, err := rediscache.GetCache(uid)
if err != nil {
log.E("Error: %v", err)
}
if cache != nil {
log.D("value from cache: %+v", cache)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cache)
} else {
log.D("No data in redis cache then search it in database.")
// search in database
item, err := dynamo.Read(uid)
if err != nil {
log.D("Fail to read: %v", err.Error())
w.WriteHeader(http.StatusNotFound)
return
}
log.D("%v", item)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(item)
}
}
// NewDatabase is initiate the SQL database
func NewDatabase(cfg config.DynamoConfig) error {
// Create database
log.I("start newsession...")
sess, sessErr := session.NewSession(&aws.Config{
Region: aws.String(cfg.Region),
Endpoint: aws.String(cfg.Endpoint),
Retryer: client.DefaultRetryer{
NumMaxRetries: 2,
MinRetryDelay: 0,
MinThrottleDelay: 0,
MaxRetryDelay: 60 * time.Second,
MaxThrottleDelay: 0,
},
})
if sessErr != nil {
log.E("%v", sessErr.Error())
return sessErr
}
db = dynamodb.New(sess)
// Create table Movies
tableName = "Profiles"
input := &dynamodb.CreateTableInput{
AttributeDefinitions: []*dynamodb.AttributeDefinition{
{
AttributeName: aws.String("UID"),
AttributeType: aws.String("S"),
},
},
KeySchema: []*dynamodb.KeySchemaElement{
{
AttributeName: aws.String("UID"),
KeyType: aws.String("HASH"),
},
},
ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(10),
WriteCapacityUnits: aws.Int64(10),
},
TableName: aws.String(tableName),
}
_, err := db.CreateTable(input)
if err != nil {
log.E("Got error calling CreateTable: %v", err.Error())
return err
}
log.I("Created the table %v", tableName)
log.I("Successfully connected to Dynamo database: %v", cfg.Endpoint)
return nil
}
// Write is to write an item to dynamodb
func Write(item data.UserProfile) error {
av, err := dynamodbattribute.MarshalMap(item)
if err != nil {
return err
}
Input := &dynamodb.PutItemInput{
Item: av,
TableName: aws.String(tableName),
}
_, err = db.PutItem(Input)
if err != nil {
return err
}
log.I("Successfully write the item: %-v", av)
return nil
}
// Read is to retrive an item from dynamodb
func Read(uid string) (data.UserProfile, error) {
var item data.UserProfile
result, err := db.GetItem(&dynamodb.GetItemInput{
TableName: aws.String(tableName),
Key: map[string]*dynamodb.AttributeValue{
"UID": {
S: aws.String(uid),
},
},
})
if err != nil {
log.D("fail to read : %v", err.Error())
return item, err
}
if len(result.Item) == 0 {
return item, errors.New("No result on query")
}
err = dynamodbattribute.UnmarshalMap(result.Item, &item)
if err != nil {
log.D("Failed to unmarshal Record, %v", err.Error())
return item, err
}
log.I("Successfully quaried in database: %+v", item)
return item, nil
}
var pool *redis.Pool
var ttl int
// NewRedisCache is to set the configuration for redis
func NewRedisCache(cfg config.RedisConfig) {
pool = newPool(cfg)
ttl = cfg.TTL
log.I("Successfully connected to redis cache: %v:%v (ttl: %v)", cfg.Host, cfg.Port, cfg.TTL)
}
func newPool(cfg config.RedisConfig) *redis.Pool {
return &redis.Pool{
MaxIdle: cfg.PoolMaxIdle,
MaxActive: cfg.PoolMaxActive,
IdleTimeout: time.Duration(cfg.PoolIdleTimeout) * time.Second,
Wait: true,
Dial: func() (redis.Conn, error) {
url := "redis://" + cfg.Host + ":" + cfg.Port
return redis.DialURL(
url,
redis.DialPassword(cfg.Password),
redis.DialConnectTimeout(time.Duration(cfg.ConnTimeout)*time.Millisecond),
)
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
}
}
// GetCache is to get the data from redis
func GetCache(key string) (*data.UserProfile, error) {
c := pool.Get()
defer c.Close()
raw, err := redis.String(c.Do("GET", key))
if err == redis.ErrNil {
return nil, nil
} else if err != nil {
return nil, err
}
var value *data.UserProfile
err = json.Unmarshal([]byte(raw), &value)
if err != nil {
log.E("%v: %v", key, err)
return nil, err
}
return value, err
}
// SetCache is to record the data in redis
func SetCache(key string, value *data.UserProfile) (interface{}, error) {
raw, err := json.Marshal(*value)
if err != nil {
log.E("%v: %v", key, err)
return nil, err
}
c := pool.Get()
defer c.Close()
log.D("key: %s, value: %+v, ttl: %v", key, string(raw), ttl)
if ttl == 0 {
return c.Do("SET", key, raw)
} else {
return c.Do("SETEX", key, ttl, raw)
}
}
$ docker run -d --name redis redis:latest
$ docker pull amazon/dynamodb-local
$ docker run -p 8000:8000 amazon/dynamodb-local
$ curl -i localhost:8080/add -H "Content-Type: application/json" -d '{"UID":"kyopark","Name":"John","Email":"john@mail.com","Age":25}'
$ curl -i localhost:8080/search/kyopark
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 25 Apr 2020 01:27:59 GMT
Content-Length: 65
{"UID":"kyopark","Name":"John","Email":"john@mail.com","Age":25}
https://github.com/awsdocs/aws-doc-sdk-examples/tree/master/go/example_code/dynamodb