Redis Support

iFun Engine provides an interface to easily access Redis server. Since you can include any external libraries into the iFun Engine game server project, you may be able to access Redis by yourself. The interface that iFun Engine provides, however, is a thin wrapper of efficient Hiredis and compatible with the iFun Engine’s event threads. So, it’s worth taking a look.

Interface

Note

Please refer to iFun Engine API document for details. It’s been designed around the official Redis Commands . So, it should be easy to understand.

Interface functions can be categorized like follows:

Tip

Asynchronous version is suffixed with Async like Redis::DelAsync.

  • Keys

    • Del, Exists, Expire, Persist, PExpire, PTTL, Rename, TTL
  • Strings

    • Append, BitCount, BitOp, Decr, DecrBy, Get, GetBit, GetRange, GetSet, Incr, IncrBy, IncrByFloat, MGet, MSet, PSetEx, Set, SetBit, SetEx, SetNx, StrLen
  • Hashes

    • HDel, HExists, HGet, HGetAll, HIncrBy, HIncrByFloat, HKeys, HLen, HMGet, HMSet, HSet, HSetNx, HVals
  • Lists

    • LIndex, LInsert, LLen, LPop, LPush, LRange, LRem, LSet, LTrim, RPop, RPush
  • Sets

    • SAdd, SCard, SDiff, SDiffStore, SInter, SInterStore, SIsMember, SMembers, SMove, SPop, SRandMember, SRem, SUnion, SUnionStore
  • Sorted Sets

    • ZAdd, ZCard, ZCount, ZIncrBy, ZRem, ZRevRange, ZRevRank, ZScore
  • Pub/Sub

    • Publish, Subscribe, Unsubscribe, PUnsubscribe

    Note

    Pub/Sub Subscribe, and PSubscribe are asynchronous and synchronous ones are not supported.

  • Transaction

    • Discard, Exec, Multi

In addition to the commands listed above, you can directly invoke a Redis command via Redis::ExecuteCommand() For details, please refer to Using ExecuteCommand().

Redis Access Example

Suppose we want to cache some of user data on user login using Redis HMSet .

Synchronous Example

#include <funapi.h>

// Say, this is a handler for the client's login message.
void OnLogin(const Ptr<Session> &session, const Json &message) {
  // Specifies a tag of a target Redis server.
  // In this example, we assume the default Redis server with an empty string.
  // Please see the Redis tag section below for details about tagging.
  string tag = "";

  // Extracts the user id.
  string user_id = message["user_id"].GetString();

  // Say, we have finished login-related steps.
  ...

  // Say, we have computed a login time.
  string login_time = ...;

  // Suppose, we want to store user login time as well as user id.
  std::vector<std::pair<std::string, std::string > > fields_values;
  fields_values.push_back(std::make_pair(user_id, login_time));

  // Calls Redis::HMSet(...) with the constructed parameter.
  // We assume "user:login" as the key.
  Redis::Result redis_result = Redis::HMSet("user:login", fields_values, tag);
  if (redis_result == Redis::kResultError) {
    // Error handling...
    return;
  }

  // Sends the login result to the client.
  Json json_response;
  json_response["result"] = true;

  session->SendMessage("login", json_response);
}

Asynchronous Example

#include <funapi.h>

// HMSetAsync takes a callback in the type of Redis::OnNonReturned.
// Since we also need a session, we pass it using the boost::bind.
 void OnLoginDataSaved(const Ptr<Session> &session, const Redis::Result &result) {
   Json json_response;

   if (result == Redis::kResultError) {
     // Error handling
     json_response["result"] = false;
     session->SendMessage("login", json_response);
     return;
   }

   // Sends the login result to the client.
   json_response["result"] = true;
   session->SendMessage("login", json_response);

   // If we need to call extra async Redis commands,
   // this is a good place to trigger the next one.
 }

 void OnLogin(const Ptr<Session> &session, const Json &message) {
   // Specifies a tag of a target Redis server.
   // In this example, we assume the default Redis server with an empty string.
   // Please see the Redis tag section below for details about tagging.
   string tag = "";

   // Extracts the user id.
   string user_id = message["user_id"].GetString();

   // Say, we have finished login-related steps.
   ...

   // Say, we have computed a login time.
   string login_time = ...;

   // Suppose, we want to store user login time as well as user id.
   std::vector<std::pair<std::string, std::string > > fields_values;
   fields_values.push_back(std::make_pair(user_id, login_time));

   // We assume "user:login" as the key.
   // Please note that we used boost::bind to pass a session object while
   // making the callback in the type of OnNonReturned
   Redis::HMSetAsync("user:login", fields_values,
       boost::bind(&OnLoginDataSaved, session, _1), tag);
 }

Using ExecuteCommand()

Though iFun Engine provides 1:1 matching for some of Redis functions, you may want to run raw Redis command directly. In this case, you can use Redis::ExecuteCommand(). But please be aware that you also need to handle return values by yourself, since iFun Engine does not know what you are running via the method. You may want to refer to struct Redis::Response and the return type of each Redis Command.

Warning

Redis::ExecuteCommand() cannot handle the``Pub/Sub`` commands. You must use the provided Pub/Sub methods.

Warning

Redis::ExecuteCommand() cannot handle the Transaction command. You must use the provided Transaction interface.

// Returned by Redis::ExecuteCommand().
struct Response {
  enum Type {
    // It means the result is in string.
    // Actual return value is stored in the member field named "str"
    // E.g.)
    //     redis> GET mykey
    //     "hello"
    kString = 1,

    // It means the result is in array.
    // Actual return value is stored in the member field named "elements"
    // E.g.)
    //     redis> KEYS *
    //     "one"
    //     "two"
    kArray = 2,

    // It means the result is in integer.
    // Actual return value is stored in the member field named "integer"
    // E.g.)
    //     redis> HLEN myhash
    //     (integer) 2
    kInteger = 3,

    // It means the result is nil.
    // E.g.)
    //     redis> GET notfoundmykey
    //     (nil)
    kNil = 4,

    // It means the result is status string.
    // Actual return value is stored in the member field named "str"
    // E.g.)
    //     redis> SET mykey value
    //     OK
    kStatus = 5,

    // It means there was an error.
    // Actual error value is stored in the member field named "str"
    // E.g.)
    //     redis> SET mykey value value2
    //     "ERR syntax error"
    kError = 6
  };

  // Field to store response type.
  // Remaining fields may be valid according to this value.
  Type type;

  // Valid only if type == kInteger
  int64_t integer;

  // Valid only if type == kString, kStatus, or kError
  std::string str;

  // Valid only if type == kArray
  std::vector<Ptr<Response> > elements;
};

Suppose we want to use Redis::ExecuteCommand() to run Redis HMSET instead of calling Redis::HMSet() in the example above.

Redis::ExecuteCommand() Synchronous Example

#include <funapi.h>

void OnLogin(const Ptr<Session> &session, const Json &message) {
  // Specifies a tag of a target Redis server.
  // In this example, we assume the default Redis server with an empty string.
  // Please see the Redis tag section below for details about tagging.
  string tag = "";

  // We assume "user:login" as the key.
  string key = "user:login";

  // Extracts the user id.
  string user_id = message["user_id"].GetString();

  // Say, we have finished login-related steps.
  ...

  // Say, we have computed a login time.
  string login_time = ...;

  // Suppose, we want to store user login time as well as user id.
  // Please remember that HMSET is used like this: HMSET key field value
  std::vector<string> arguments;
  arguments.push_back(key);
  arguments.push_back(user_id);
  arguments.push_back(login_time);

  // If your value is not of string, you can use boost::lexical_cast:
  //     int64_t gold = 10;
  //     arguments.push_back(boost::lexical_cast<string>(gold));

  Ptr<Redis::Response> response = Redis::ExecuteCommand("HMSET", &arguments, tag);
  if (not response || response->type == Redis::Response::kError) {
    // Error handling...
    return;
  }

  Json json_response;
  // HMSET returns a response with Status in type and "OK" in str.
  if (response->type == Redis::Response::kStatus && response->str == "OK") {
    json_response["result"] = true;
  } else {
    json_response["result"] = false;
  }

  session->SendMessage("login", json_response);
}

Redis::ExecuteCommandAsync() Asynchronous Example

#include <funapi.h>

void OnUserDataSaved(const Ptr<Session> &session, const Ptr<Redis::Response> &response) {
  Json json_response;

  if (not response || response->type == Redis::Response::kError) {
    // Error handling...
    return;
  }

  Json json_response;
  // HMSET returns a response with Status in type and "OK" in str.
  if (response->type == Redis::Response::kStatus && response->str == "OK") {
    json_response["result"] = true;
  } else {
    json_response["result"] = false;
  }

  session->SendMessage("login", json_response);
}

void OnLogin(const Ptr<Session> &session, const Json &message) {
  // Specifies a tag of a target Redis server.
  // In this example, we assume the default Redis server with an empty string.
  // Please see the Redis tag section below for details about tagging.
  string tag = "";

  // We assume "user:login" as the key.
  string key = "user:login";

  // Extracts the user id.
  string user_id = message["user_id"].GetString();

  // Say, we have finished login-related steps.
  ...

  // Say, we have computed a login time.
  string login_time = ...;

  // Suppose, we want to store user login time as well as user id.
  // Please remember that HMSET is used like this: HMSET key field value
  std::vector<string> arguments;
  arguments.push_back(key);
  arguments.push_back(user_id);
  arguments.push_back(login_time);

  // If your value is not of string, you can use boost::lexical_cast:
  //     int64_t gold = 10;
  //     arguments.push_back(boost::lexical_cast<string>(gold));

  // Initiates an async command and passes OnUserDataSaved() as a callback.
  Redis::ExecuteCommand("HMSET", &arguments,
      boost::bind(&OnUserDataSaved, session, _1), tag);
}

Distinguishing Redis Servers with Tags

If you have many Redis servers and need to talk to a specific one at a time, you may need to distinguish Redis servers. For this purpose, you can give tags to each Redis server in MANIFEST.json. (Please see REDIS-related MANIFEST.json for more information.)

Suppose we have two Redis servers: one is for guild data, and another one for party data.

Since we are not using the Redis sentinel, we set redis_mode to redis

Note

If using the Redis Sentinel, please set redis_mode to sentinel and enter sentinel servers information in the redis_sentinel_servers field.

"Redis": {
  // Sets to true to enable Redis interface.
  "enable_redis": true,

  // Regular redis, not sentinel.
  "redis_mode": "redis",

  "redis_servers": {
    // Specifies Redis server endpoint for guild data.
    "guild": {
      "address": "127.0.0.1:6379",
      "auth_pass": ""
    },
    // Specifies Redis server endpoint for party data.
    "party": {
      "address": "127.0.0.1:7379",
      "auth_pass": ""
    }
  },

  // This part will be ignored as the "redis_mode" is "redis".
  "redis_sentinel_servers": {
    "": {
      "master_name": "mymaster",
      "addresses": ["127.0.0.1:26379"],
      "auth_pass": ""
    }
  },

  "redis_async_threads_size": 4

Note

If you don’t feel like giving a tag, leave it as an empty string “”. “” is also valid tag name and all the Redis servers are assigned it, by default.

Suppose, we store data in the guild Redis server. Here’s an synchronous example:

const string kGuildTag("guild");

void CreateGuild(const string &guild_name, const string &guild_master_name) {
  // Creates a guild entry using the HSet command.
  // We are passing kGuildTag as the last parameter.
  // This tells the method to talk to the server with the tag as specified in
  // MANIFEST.json
  Redis::Result result =
      Redis::HSet(guild_name, "guild_master", guild_master_name, kGuildTag);
  if (result == kResultError) {
    // Error handling
    return;
  }
}

Suppose we create a party entry in the party redis server.

const string kPartyTag("party");

void CreateParty(const string &party_name,
                 const std::vector<string> &party_members) {
  // Creates an entry using the LPush command and add a party member.
  // We are passing kPartyTag as the last parameter.
  // This tells the method to talk to the server with the tag as specified in
  // MANIFEST.json
  Redis::Result result =
      Redis::LPush(party_name, party_members, NULL, kPartyTag);
  if (result == kResultError) {
    // Error handling
    return;
  }
}

Redis Clustering

Note

Clustering is not supported directly by iFun Engine at the moment. But the master-slave style can be achieved by Redis Sentinel.

Redis Replication

iFun Engine recommends Redis Sentinel for Redis failovers.

Please set redis_mode to sentinel in Redis MANIFEST.json. Also, adds the server information in redis_sentinel_servers.

iFun Engine figures out the master Redis node by communicating with the servers listed in redis_sentinel_servers and transparently switches to a new master if the current master goes down.

Notification for Master Changes

You can register a callback notified when a new master gets elected. Callback should be in the type of OnSentinelMasterSwitched and registered via SubscribeSentinelSwitchMasterChannelAsync.

먼저 Callback 함수의 형태와 등록하기 위한 API 를 살펴보겠습니다.

typedef boost::function<void (
    // Master name as specified in MANIFEST.json.
    // This is to identify which sentinel cluster has changed the master.
    const string &master_name,

    // ip:port string for an old master.
    const string &old_master_address,

    // ip:port string for a new master.
    const string &new_master_address)> OnSentinelMasterSwitched;

static void SubscribeSentinelSwitchMasterChannelAsync(
    // Callback instance.
    const OnSentinelMasterSwitched &cb,

    // Tag to identify which sentinel cluster we are interested in.
    // The tag is as specified in MANIFEST.json
    const string &tag = "");

This is an example to register a callback.

// This will be invoked once a new master gets elected.
void MasterSwitched(const string &master_name,
                    const string &old_master_address,
                    const string &new_master_address) {
  LOG(INFO) << "master name: " << master_name
            << ", old master address: " << old_master_name
            << ", new master address: " << new_master_name;
}

void Example() {
  // Register the callback via Redis::SubscribeSentinelSwitchMasterChannelAsync().
  // We omitted a tag, which means the default (i.e., "") one will be chosen.
  Redis::SubscribeSentinelSwitchMasterChannelAsync(MasterSwitched);
}

MANIFEST.json Configuration

  • Component name: Redis

  • Arguments

    • enable_redis: true to enable the Redis feature. false, otherwise.

    • redis_mode: either "redis" or "sentinel". It defaults to "redis". For "redis", redis_servers should be populated, and for "sentinel", redis_sentinel_servers should be filled up.

    • redis_servers: Required only if redis_mode is set to "redis". It should include connection information of Redis servers.

      // Each server is identified by a unique tag.
      "redis_servers": {
        // Please provide a unique tag for each server.
        // The tag will be passed to the iFun Engine's Redis methods.
        // ""(empty string) is also possible.
        "<tag>": {
          // ip:port of the Redis server.
          // "127.0.0.1:6379", by default.
          "address": "127.0.0.1:6379",
      
          // Specifies a password token.
          // ""(empty string), by default.
          "auth_pass": ""
        },
        ...
      }
      
    • redis_sentinel_servers: Only if redis_mode is set to "sentinel". It should include connection information of Redis Sentinel servers.

      // Each sentinel cluster is identified by a unique tag.
      "redis_sentinel_servers": {
        // Please provide a unique tag for each sentinel cluster.
        // The tag will be passed to the iFun Engine's Redis methods.
        // ""(empty string) is also possible.
        "<tag>": {
          // The name configured to the Redis Sentinel.
          // "mymaster", by default.
          "master_name": "mymaster",
      
          // A list of ip:port in the JSON Array
          // ["127.0.0.1:26379"], by default.
          "addresses": ["127.0.0.1:26379"],
      
          // Specifies a password token.
          // ""(empty string), by default.
          "auth_pass": ""
        },
        ...
      }
      

      Note

      Tag can be used to issue Redis commands to a specific Redis server or sentinel cluster. Please see Distinguishing Redis Servers with Tags more information.

    • redis_async_threads_size: Number of threads dedicated to async redis commands handling. It defaults to 4.