DB 접근 Part 2: Redis

아이펀 엔진은 Redis 서버와 통신할 수 있는 RedisClient class 를 제공합니다. RedisClient 는 Redis Commands 에 대응되는 함수들을 제공합니다.

아래 설명들은 모든 기능이나 함수를 설명하고 있지 않습니다. 자세한 내용은 RedisClient class 를 참고하세요.

Note

RedisClient 는 Redis Server Version 2.8.4 를 기준으로 작성되었습니다.

연결 초기화

설정값 지정

class RedisClient {
  static Ptr<RedisClient> Create(server_ip, server_port, auth_pass,
                                 connection_count [, invoke_as_event = true]);
};
public class RedisClient
{
  static public RedisClient Create (server_ip, server_port, auth_pass,
                                    connection_count [, invoke_as_event = true]);
}

연결 초기화를 위한 설정값을 지정합니다.

server_ip

Redis 서버의 IP 를 입력합니다. 예) 127.0.0.1

server_port

Redis 서버의 Port 를 입력합니다. 예) 6379

auth_pass

Redis 서버에 설정된 auth pass 를 입력합니다.

connection_count

Connection Pool 의 연결 수를 입력합니다.

invoke_as_event

비동기 함수에 전달한 콜백 함수를 이벤트 로 처리할지 결정합니다. 기본값은 true 입니다. false 를 입력할 경우 별도 스레드에서 호출합니다.

Note

연결 수가 2 개 이상이면 병렬적으로 수행되며, 따라서 Redis 명령 실행 순서가 보장되지 않습니다. 만약 Redis 명령 실행 순서가 보장되어야 한다면 비동기 명령 실행 순서와 태그 을 참고하세요.

Note

Redis Subscribe 명령을 수행하면, 연결풀에서 Subscribe 처리를 담당할 연결을 하나 선택하며 이후부터는 Subscribe 만을 처리하도록 설정합니다. 따라서 만약 연결 수가 1 개라면 Subscribe 명령을 수행한 뒤에는 Get 과 같은 Sub 외 명령은 수행되지 않고 내부큐에 쌓이기만 합니다. (Unsubscribe 를 하면 그때부터 큐에 쌓인 명령을 처리합니다.) 이 경우에는 반드시 연결 수를 2 개 이상으로 설정해야 합니다.

Connection Pool 초기화

class RedisClient {
  void Initialize();
};
public class RedisClient
{
  public void Start();
}

Create() 함수 호출시 입력한 연결 수만큼 connection pool 을 초기화합니다. 이 함수가 호출된 이후부터 Redis 명령을 실행할 수 있습니다.

명령 실행

지원하는 명령

현재 지원중인 Redis 명령은 다음과 같습니다. Del, Exists 와 같은 함수는 비동기 함수이고 동기함수는 함수명 끝에 Sync 가 붙습니다.

각 명령에 대한 자세한 설명은 Redis Commands 를 참고하세요.

Keys

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

Strings

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

Hashes

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

Lists

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

Sets

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

Sorted Sets

ZCard, ZCount, ZScore, ZRange, ZRangeByScore, ZRank, ZRevRange, ZRevRangeByScore, ZRevRank, ZAdd, ZIncrBy, ZRem, ZRemRangeByRank, ZRemRangeByScore

Pub/Sub

Publish, Subscribe, PSubscribe, Unsubscribe, PUnsubscribe

직접 명령 처리하기

만약 위 Redis 명령 외 다른 명령이 필요하거나 해당 명령을 직접 처리하고 싶다면 다음 함수로 처리할 수 있습니다.

class RedisClient {
  void ExecuteCommand(const string &command_name,
                      const std::vector<string> *arguments,
                      const Callback &callback,
                      const SerializationTag &tag = kDefaultSerializationTag);

  Ptr<Reply> ExecuteCommandSync(const string &command_name,
                                const std::vector<string> *arguments,
                                Result *result = NULL);
};
public class RedisClient
{
  public void ExecuteCommand (string command_name,
                              List<string> arguments,
                              Callback callback,
                              Guid tag = default(Guid))

  public Reply ExecuteCommandSync (string command_name,
                                   List<string> arguments,
                                   out RedisClient.Result out_result)
}

위 함수로 명령을 실행하기 위해서는 Redis 서버의 응답이 담긴 Reply 구조체와 Redis Commands 에서 설명하는 각 Redis 명령에 대한 반환값을 이해해야 합니다.

class RedisClient {
  struct Reply {
    enum Type {
      kString = 1,
      kArray = 2,
      kInteger = 3,
      kNil = 4,
      kStatus = 5,
      kError = 6
    };

    DECLARE_CLASS_PTR(Reply);

    Type type;
    int64_t integer;
    string str;
    std::vector<Ptr<Reply> > elements;
  };
};
public class RedisClient
{
  public enum ReplyType
  {
    kString = 1,
    kArray = 2,
    kInteger = 3,
    kNil = 4,
    kStatus = 5,
    kError = 6
  }

  public struct Reply
  {
    public ReplyType Type { get; }
    public long Integer { get; }
    public string Str { get; }
    public List<Reply> Elements { get; }
  }
}
kString

다음과 같이 string 을 반환한 경우입니다. C++ 은 str 로, C# 은 Str 로 값을 얻을 수 있습니다.

redis> GET mykey
"hello"
kArray

다음과 같이 array 를 반환한 경우입니다. C++ 은 elements, C# 은 Elements 로 값을 얻을 수 있습니다.

redis> KEYS *
1) "one"
2) "two"
kInteger

다음과 같이 integer 를 반환한 경우입니다. C++ 은 integer, C# 은 Integer 로 값을 얻을 수 있습니다.

redis> HLEN myhash
(integer) 2
kNil

다음과 같이 nil 을 반환한 경우입니다. 이에 대한 값은 없으므로 C++ 은 type, C# 은 Type 만 확인하면 됩니다.

redis> GET notfoundmykey
(nil)
kStatus

다음과 같이 status 를 반환한 경우입니다. C++ 은 str 로, C# 은 Str 로 값을 얻을 수 있습니다.

redis> SET mykey value
OK
kError

다음과 같이 error 를 반환한 경우입니다. C++ 은 str 로, C# 은 Str 로 값을 얻을 수 있습니다.

redis> SET mykey value value2
(error) ERR syntax error

비동기 명령 실행 순서와 태그

Redis 명령을 비동기 함수로 실행하면 기본적으로 실행된 순서대로 큐에 들어갑니다. 하지만 Redis 명령을 처리하는 연결의 수가 2 개 이상이면 Redis 명령은 병렬적으로 실행됩니다.

이것은 실행 순서가 보장되어야 하는 상황에서는 곤란할 수 있습니다. RedisClient 는 이벤트 실행순서와 이벤트 태그 와 같이 처리 순서가 보장되어야 하는 Redis 명령을 같은 태그로 묶는 기능을 지원합니다.

비동기 함수는 다음과 같이 마지막 인자로 tag 를 입력받습니다. tag 를 기본값으로 놔둘 경우 각 Redis 명령은 병렬적으로 실행됩니다.

class RedisClient {
  typedef Uuid SerializationTag;
  static const SerializationTag kDefaultSerializationTag;

  void Del(const string &key, const IntegerCallback &callback,
           const SerializationTag &tag = kDefaultSerializationTag);
};
public class RedisClient {
  public void Del (string key, IntegerCallback callback,
                   Guid tag = default(Guid))
};

만약 다음과 같이 tag 값을 입력할 경우 각 Redis 명령은 순차적으로 실행되어 OnDeleted1 => OnDeleted2 순서로 콜백 함수가 불립니다.

Ptr<RedisClient> the_redis_client;

void Initialize() {
  // ...
}

void OnDeleted1(const RedisClient::Result &result, int64_t value) {
}

void OnDeleted2(const RedisClient::Result &result, int64_t value) {
}

void Example() {
  RedisClient::SerializationTag tag = RandomGenerator::GenerateUuid();
  the_redis_client->Del("hello1", OnDeleted1, tag);
  the_redis_client->Del("hello2", OnDeleted2, tag);
}
Ptr<RedisClient> the_redis_client;

void Initialize()
{
  // ...
}

void OnDeleted1(RedisClient.Result result, long value)
{
}

void OnDeleted2(RedisClient.Result result, long value)
{
}

void Example()
{
  System.Guid tag = RandomGenerator.GenerateUuid ();
  the_redis_client.Del("hello1", OnDeleted1, tag);
  the_redis_client.Del("hello2", OnDeleted2, tag);
}

사용 예제

예제 - 유저 데이터 저장

아래는 유저가 로그인하면 유저의 데이터를 HMSet 으로 저장하는 예제입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Ptr<RedisClient> the_redis_client;

void Initialize() {
  the_redis_client = RedisClient::Create("127.0.0.1", 6379, "", 4);
  the_redis_client->Initialize();
}

void OnUserDataWrote(const RedisClient::Result &result, const string &value) {
  if (result.type == RedisClient::kError) {
    LOG(ERROR) << "Error: type=" << result.type
               << ", code=" << result.error_code
               << ", desc=" << result.error_desc;
    return;
  }

  LOG_ASSERT(value == "OK");

  LOG(INFO) << "User data wrote.";
}

void OnLogin(const Ptr<Session> &session, const Json &message) {
  string user_id = message["user_id"].GetString();
  string login_time = WallClock::GetTimestring(WallClock::Now());

  // std::vector<std::pair<string, string > >
  RedisClient::StringPairList field_values;
  field_values.push_back(std::make_pair(user_id, login_time));

  the_redis_client->HMSet("user:data", field_values, OnUserDataWrote);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
RedisClient the_redis_client;

void Initialize()
{
  the_redis_client = RedisClient.Create("127.0.0.1", 6379, "", 4);
  the_redis_client.Initialize();
}

void OnUserDataWrote(RedisClient.Result result, string value)
{
  if (result.Type == RedisClient.ResultType.kError) {
    Log.Error("Error: type={0}, code={1}, desc={2}",
              result.Type, result.ErrorCode, result.ErrorDesc);
    return;
  }

  Log.Assert(value == "OK");

  Log.Info("User data wrote.");
}

void OnLogin (Session session, JObject message)
{
  string user_id = (string) message["user_id"];
  string login_time = WallClock.GetTimestring();

  List<Tuple<string, string>> field_values = new List<Tuple<string, string>> ();
  field_values.Add (Tuple.Create (user_id, login_time));

  the_redis_client.HMSet("user:data", field_values, OnUserDataWrote);
}

예제 - ExecuteCommand()

아래는 HMSet 으로 처리했던 예제를 ExecuteCommand() 함수로 저장하는 예제입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Ptr<RedisClient> the_redis_client;

void Initialize() {
  the_redis_client = RedisClient::Create("127.0.0.1", 6379, "", 4);
  the_redis_client->Initialize();
}

void OnUserDataWrote(const RedisClient::Result &result,
                     const Ptr<RedisClient::Reply> &reply) {
  if (result.type == RedisClient::kError) {
    LOG(ERROR) << "Error: type=" << result.type
               << ", code=" << result.error_code
               << ", desc=" << result.error_desc;
    return;
  }

  LOG_ASSERT(reply)
  LOG_ASSERT(reply->type == RedisClient::Reply::kStatus &&
             reply->str == "OK");

  LOG(INFO) << "User data wrote.";
}

void OnLogin(const Ptr<Session> &session, const Json &message) {
  string key = "user:data";
  string user_id = message["user_id"].GetString();
  string login_time = WallClock::GetTimestring(WallClock::Now());

  std::vector<string> arguments;
  arguments.push_back(key);
  arguments.push_back(user_id);
  arguments.push_back(login_time);

  the_redis_client->ExecuteCommand("HMSET", &arguments, OnUserDataWrote);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
RedisClient the_redis_client;

void Initialize()
{
  the_redis_client = RedisClient.Create("127.0.0.1", 6379, "", 4);
  the_redis_client.Initialize();
}

void OnUserDataWrote(RedisClient.Result result, RedisClient.Reply reply)
{
  if (result.Type == RedisClient.ResultType.kError) {
    Log.Error("Error: type={0}, code={1}, desc={2}",
              result.Type, result.ErrorCode, result.ErrorDesc);
    return;
  }

  Log.Assert(reply.Type == RedisClient.ReplyType.kStatus &&
             reply.Str == "OK");

  Log.Info("User data wrote.");
}

void OnLogin (Session session, JObject message)
{
  string key = "user:data";
  string user_id = (string) message["user_id"];
  string login_time = WallClock.GetTimestring();

  List<string> arguments = new List<string> ();
  arguments.Add (key);
  arguments.Add (user_id);
  arguments.Add (login_time);

  the_redis_client.ExecuteCommand("HMSET", arguments, OnUserDataWrote);
}

레디스(Redis) 서버 관리

태그(Tag) 로 레디스 서버 구분

레디스 서버를 여러 개 사용하는 경우 태그를 지정하고 각각의 레디스 서버를 구분하고, 레디스 명령을 실행 할 서버를 태그로 지정할 수 있습니다.

태그는 MANIFEST.json 의 Redis 항목에서 redis_mode 에 따라 redis_servers 또는 redis_sentinel_servers 항목에 레디스 서버 목록을 설정 할 때에 이름을 지정하는 것으로 사용할 수 있습니다.

예제 - 태그를 이용한 레디스 서버 구분

아래의 MANIFEST.json 은 두 개의 Redis 서버를 지정하되, 하나는 guild, 다른 하나는 party 라는 tag 를 준 경우입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
"Redis": {
  "enable_redis": true,

  "redis_mode": "redis",

  "redis_servers": {
    "guild": {
      "address": "127.0.0.1:6379",
      "auth_pass": ""
    },
    "party": {
      "address": "127.0.0.1:7379",
      "auth_pass": ""
    }
  }
}

Note

만약 태그를 지정하고 싶지 않으면 “”(empty string) 으로 입력하면 됩니다.

다음은 위에서 보여드린 레디스 설정을 사용하는 서버에서 길드를 생성하는 함수와 파티를 생성하는 함수에서 레디스 태그 지정을 사용해서 서로 다른 레디스 서버에 명령을 전송하는 예제입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const string kGuildTag("guild");
const string kPartyTag("party");

void CreateGuild(const string &guild_name, const string &guild_master_name) {
  Redis::Result result =
      Redis::HSet(guild_name, "guild_master", guild_master_name, kGuildTag);
  if (result == kResultError) {
    return;
  }
}

void CreateParty(const string &party_name,
                 const std::vector<string> &party_members) {
  Redis::Result result =
      Redis::LPush(party_name, party_members, NULL, kPartyTag);
  if (result == kResultError) {
    return;
  }
}

C# 버전은 추후 지원 예정입니다.

Redis 레플리케이션

아이펀 엔진은 Redis Sentinel 을 통한 Failover 를 지원합니다.

Note

Redis Sentinel 설정과 관련된 더 자세한 내용은 Redis Sentinel 을 참고해주세요.

연결 초기화

Sentinel 설정값 지정

아이펀 엔진은 sentinel_addresses 에 나열된 서버들과 통신하여 Master 서버를 알아내고, Master 서버가 변경되었다면 새로운 Master 서버로 자동 재연결합니다.

class RedisClient {
  static Ptr<RedisClient> Create(const string &master_name, const vector<string> &sentinel_addresses,
                                 const string &auth_pass, size_t connection_count
                                 [, bool invoke_as_event = true][, size_t database = 0]);
};
public class RedisClient
{
  public static RedisClient Create (string master_name, List<string> sentinel_addresses,
                                    string auth_pass, ulong connection_count
                                    [, bool invoke_as_event = true])
}

연결 초기화를 위한 설정값을 지정합니다.

master_name

Redis Sentinel 에 설정한 Master name 을 입력합니다.

sentinel_addresses

Redis Sentinel 서버들의 주소를 입력합니다. 예) “192.168.0.1:26379,192.168.0.2:26379”

auth_pass

Redis 서버에 설정된 auth pass 를 입력합니다.

connection_count

Connection Pool 의 연결 수를 입력합니다.

invoke_as_event

비동기 함수에 전달한 콜백 함수를 이벤트 로 처리할지 결정합니다. 기본값은 true 입니다. false 를 입력할 경우 별도 스레드에서 호출합니다.

Note

현재 Master 와 연결이 끊어지는 경우, Redis Sentinel 에 의해 Master가 변경 되기 전까지는 현재 Master 에 대해 재연결을 시도합니다. 또한 Master 변경 과정 중 발생하는 사용자의 요청에 대하여 Redis Client 는 내부적으로 요청을 큐잉한 뒤 Master가 변경되면 다시 요청을 처리하기 시작합니다.

Master 변경 통지 받기

Redis Sentinel 에 의해 Master 가 변경되면 아이펀 엔진이 자동으로 새로운 Master 와 연결을 맺게 됩니다. 그와 별개로 Master 가 변경되었다는 것을 통지 받고 싶다면 SetSentinelSwitchMasterCallback() 함수를 이용해 callback 을 등록할 수 있습니다.

Note

최초 Sentinel 연결 후 Master를 알아내는 경우에도 이 콜백이 호출됩니다.

class RedisClient {
  typedef boost::function<
      void (const string &/*master_name*/,
            const string &/*old_master_address*/,
            const string &/*new_master_address*/)>
                SentinelMasterSwitchedCallback;

  void SetSentinelMasterSwitchedCallback(
      const SentinelMasterSwitchedCallback &cb);
};
public class RedisClient
{
  public delegate void SentinelMasterSwitchedCallback(string master_name, string old_master_name, string new_master_name);

  public void SetSentinelMasterSwitchedCallback (SentinelMasterSwitchedCallback callback)
}

master_name

Redis Sentinel 에 설정한 Master name 입니다.

old_master_address

이전 Master 의 주소입니다.

new_master_address

새 Master 의 주소입니다.

Redis 클러스터링

Note

아이펀 엔진 자체가 Redis 클러스터링을 지원하지는 않습니다.