C++ 分布式缓存:Memcached/Redis 客户端与一致性哈希

好的,没问题。

各位观众,大家好!今天咱们来聊聊C++分布式缓存,主角是Memcached和Redis客户端,再加一位神秘嘉宾:一致性哈希。别担心,就算你之前没听过这些名词,也能听懂,毕竟我可是要用最接地气的方式来讲解。

一、缓存这玩意儿到底有啥用?

想象一下,你开了一家饭店,生意火爆,每次顾客点菜都要从头开始做,那效率得多低?缓存就相当于你的厨房里提前准备好的半成品,顾客点了,直接拿出来加工一下就上桌了,速度飞快!

在计算机世界里,缓存就是把一些常用的数据放到速度更快的存储介质中(比如内存),下次再用的时候直接从缓存里拿,不用再去慢吞吞的数据库里捞了,大大提升了性能。

二、Memcached:简单粗暴的缓存小能手

Memcached是一个高性能、分布式的内存对象缓存系统。它简单、高效,特别适合缓存一些静态数据,比如用户头像、商品信息等。

1. Memcached的特点:

  • 简单: 协议简单,容易上手。
  • 快速: 基于内存存储,速度快。
  • 分布式: 可以部署在多台服务器上,形成一个缓存集群。
  • 键值对存储: 只能存储简单的键值对数据。

2. C++ Memcached客户端:libmemcached

libmemcached是一个流行的C++ Memcached客户端库,用起来很方便。

(1)安装libmemcached:

# Ubuntu/Debian
sudo apt-get install libmemcached-dev

# CentOS/RHEL
sudo yum install libmemcached-devel

# macOS (使用 Homebrew)
brew install libmemcached

(2)基本使用示例:

#include <iostream>
#include <libmemcached/memcached.h>

int main() {
  memcached_st *memc;
  memcached_return rc;

  // 创建memcached句柄
  memc = memcached_create(NULL);

  // 添加memcached服务器
  memcached_server_st *servers = NULL;
  servers = memcached_server_list_append(servers, "localhost", 11211, &rc);
  rc = memcached_server_push(memc, servers);
  memcached_server_free(servers);

  if (rc != MEMCACHED_SUCCESS) {
    std::cerr << "Couldn't add server: " << memcached_strerror(memc, rc) << std::endl;
    return 1;
  }

  // 设置缓存
  const char *key = "user_id:123";
  const char *value = "{"name": "张三", "age": 30}";
  size_t value_length = strlen(value);
  time_t expiration = 60; // 过期时间:60秒
  uint32_t flags = 0;

  rc = memcached_set(memc, key, strlen(key), value, value_length, expiration, flags);

  if (rc != MEMCACHED_SUCCESS) {
    std::cerr << "Couldn't set key: " << memcached_strerror(memc, rc) << std::endl;
    memcached_free(memc);
    return 1;
  }

  std::cout << "Set key: " << key << " with value: " << value << std::endl;

  // 获取缓存
  char *result_value;
  size_t result_length;
  uint32_t result_flags;

  result_value = memcached_get(memc, key, strlen(key), &result_length, &result_flags, &rc);

  if (rc == MEMCACHED_SUCCESS && result_value != NULL) {
    std::cout << "Got value: " << result_value << std::endl;
    free(result_value); // 记得释放内存
  } else {
    std::cout << "Key not found or error: " << memcached_strerror(memc, rc) << std::endl;
  }

  // 删除缓存
  rc = memcached_delete(memc, key, strlen(key), 0);
  if (rc != MEMCACHED_SUCCESS) {
     std::cerr << "Couldn't delete key: " << memcached_strerror(memc, rc) << std::endl;
  } else {
    std::cout << "Deleted key: " << key << std::endl;
  }

  // 释放memcached句柄
  memcached_free(memc);

  return 0;
}

(3)代码解释:

  • memcached_create(NULL):创建一个memcached句柄,相当于你拿到了一把操作Memcached的钥匙。
  • memcached_server_list_append():添加Memcached服务器的地址和端口。
  • memcached_set():设置缓存,参数分别是memcached句柄、键、键的长度、值、值的长度、过期时间、标志位。
  • memcached_get():获取缓存,参数分别是memcached句柄、键、键的长度、值的长度指针、标志位指针、返回码指针。
  • memcached_delete():删除缓存。
  • memcached_free():释放memcached句柄,用完钥匙要记得还回去。
  • memcached_strerror(): 获取错误信息,方便debug。

3. Memcached的缺点:

  • 数据类型单一: 只能存储简单的键值对。
  • 没有持久化: 重启服务器数据就没了。
  • 客户端分片: 需要客户端自己实现数据分片,比较麻烦。

三、Redis:功能强大的缓存瑞士军刀

Redis是一个开源的、基于内存的数据结构存储系统,它不仅可以作为缓存使用,还可以作为数据库、消息队列等。

1. Redis的特点:

  • 数据结构丰富: 支持字符串、哈希、列表、集合、有序集合等多种数据结构。
  • 持久化: 可以将数据持久化到磁盘,防止数据丢失。
  • 丰富的功能: 支持事务、发布/订阅、Lua脚本等功能。
  • 主从复制: 支持主从复制,提高可用性。

2. C++ Redis客户端:hiredis

hiredis是一个轻量级的C++ Redis客户端库,性能很好。

(1)安装hiredis:

# Ubuntu/Debian
sudo apt-get install libhiredis-dev

# CentOS/RHEL
sudo yum install hiredis-devel

# macOS (使用 Homebrew)
brew install hiredis

(2)基本使用示例:

#include <iostream>
#include <hiredis/hiredis.h>

int main() {
  redisContext *c;
  redisReply *reply;

  // 连接Redis服务器
  c = redisConnect("127.0.0.1", 6379);
  if (c == NULL || c->err) {
    if (c) {
      std::cerr << "Connection error: " << c->errstr << std::endl;
      redisFree(c);
    } else {
      std::cerr << "Connection error: can't allocate redis context" << std::endl;
    }
    return 1;
  }

  // 设置字符串
  reply = (redisReply *)redisCommand(c, "SET mykey myvalue");
  if (reply == NULL) {
    std::cerr << "Error executing command" << std::endl;
    redisFree(c);
    return 1;
  }

  std::cout << "SET: " << reply->str << std::endl;
  freeReplyObject(reply);

  // 获取字符串
  reply = (redisReply *)redisCommand(c, "GET mykey");
  if (reply == NULL) {
    std::cerr << "Error executing command" << std::endl;
    redisFree(c);
    return 1;
  }

  if (reply->type == REDIS_REPLY_STRING) {
    std::cout << "GET: " << reply->str << std::endl;
  } else {
    std::cout << "Key not found" << std::endl;
  }
  freeReplyObject(reply);

  // 设置过期时间
  reply = (redisReply *)redisCommand(c, "EXPIRE mykey 10"); // 设置10秒过期
   if (reply == NULL) {
    std::cerr << "Error executing command" << std::endl;
    redisFree(c);
    return 1;
  }
  std::cout << "EXPIRE: " << reply->integer << std::endl;
  freeReplyObject(reply);

  // 删除键
  reply = (redisReply *)redisCommand(c, "DEL mykey");
  if (reply == NULL) {
    std::cerr << "Error executing command" << std::endl;
    redisFree(c);
    return 1;
  }

  std::cout << "DEL: " << reply->integer << std::endl;
  freeReplyObject(reply);

  // 使用哈希
  reply = (redisReply *)redisCommand(c, "HSET user:1 name 张三 age 30");
  if (reply == NULL) {
      std::cerr << "Error executing command" << std::endl;
      redisFree(c);
      return 1;
  }
  std::cout << "HSET: " << reply->integer << std::endl;
  freeReplyObject(reply);

  reply = (redisReply *)redisCommand(c, "HGET user:1 name");
  if (reply == NULL) {
      std::cerr << "Error executing command" << std::endl;
      redisFree(c);
      return 1;
  }

  if (reply->type == REDIS_REPLY_STRING) {
      std::cout << "HGET: " << reply->str << std::endl;
  } else {
      std::cout << "Field not found" << std::endl;
  }
  freeReplyObject(reply);

  // 断开连接
  redisFree(c);

  return 0;
}

(3)代码解释:

  • redisConnect():连接Redis服务器,返回一个redisContext指针。
  • redisCommand():执行Redis命令,返回一个redisReply指针。
  • reply->type:表示返回值的类型,比如REDIS_REPLY_STRING表示字符串类型。
  • reply->str:字符串类型的值。
  • reply->integer:整数类型的值。
  • freeReplyObject():释放redisReply指针,非常重要!
  • redisFree():断开与Redis服务器的连接。

3. Redis的优点:

  • 数据类型丰富: 满足各种缓存需求。
  • 持久化: 数据更安全。
  • 功能强大: 可以做更多的事情。
  • 集群支持: Redis Cluster提供了分布式解决方案。

4. Redis的缺点:

  • 更复杂: 相比Memcached,学习成本更高。
  • 内存占用更多: 因为要维护更多的数据结构。

四、一致性哈希:让缓存集群更稳定

当你有多个缓存服务器时,需要一种方法来决定哪个键应该存储在哪个服务器上。最简单的方法是使用取模运算:server_index = hash(key) % num_servers。但是,如果增加或删除服务器,会导致大部分缓存失效,因为num_servers变了,所有键都需要重新分配。

一致性哈希就是为了解决这个问题而生的。

1. 一致性哈希的原理:

  • 哈希环: 将所有服务器和键都哈希到一个环上(通常是0 ~ 2^32-1)。
  • 服务器定位: 每个服务器在环上占据一个位置。
  • 键定位: 每个键也哈希到环上。
  • 顺时针查找: 从键的位置开始,顺时针方向找到的第一个服务器,就是该键应该存储的服务器。

2. 一致性哈希的优点:

  • 容错性好: 增加或删除服务器,只会影响相邻的键,其他键不受影响。
  • 负载均衡: 可以尽量保证每个服务器的负载均衡。

3. C++一致性哈希实现:

#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <algorithm>
#include <map>

// MurmurHash2算法 (简化版)
uint32_t MurmurHash2(const std::string& key) {
  const uint32_t m = 0x5bd1e995;
  const uint32_t r = 24;
  uint32_t h = 0 ^ key.length();
  const unsigned char * data = (const unsigned char *)key.c_str();

  while(key.length() >= 4) {
    uint32_t k = *reinterpret_cast<const uint32_t*>(data);
    k *= m;
    k ^= k >> r;
    k *= m;

    h *= m;
    h ^= k;

    data += 4;
    key = key.substr(4);
  }

  switch(key.length()) {
    case 3: h ^= data[2] << 16;
    case 2: h ^= data[1] << 8;
    case 1: h ^= data[0];
            h *= m;
  };

  h ^= h >> 13;
  h *= m;
  h ^= h >> 15;

  return h;
}

class ConsistentHash {
public:
  ConsistentHash(const std::vector<std::string>& servers, int replicas = 100) : replicas_(replicas) {
    for (const auto& server : servers) {
      AddServer(server);
    }
  }

  void AddServer(const std::string& server) {
    for (int i = 0; i < replicas_; ++i) {
      std::string virtual_node = server + "_" + std::to_string(i);
      uint32_t hash = MurmurHash2(virtual_node);
      hash_ring_[hash] = server;
    }
  }

  void RemoveServer(const std::string& server) {
    for (int i = 0; i < replicas_; ++i) {
      std::string virtual_node = server + "_" + std::to_string(i);
      uint32_t hash = MurmurHash2(virtual_node);
      hash_ring_.erase(hash);
    }
  }

  std::string GetServer(const std::string& key) {
    if (hash_ring_.empty()) {
      return ""; // 或者抛出异常
    }

    uint32_t hash = MurmurHash2(key);
    auto it = hash_ring_.lower_bound(hash); // 找到第一个大于等于hash的节点

    if (it == hash_ring_.end()) {
      it = hash_ring_.begin(); // 如果找不到,回到环的起点
    }

    return it->second;
  }

private:
  std::map<uint32_t, std::string> hash_ring_; // 哈希环:哈希值 -> 服务器名称
  int replicas_; // 虚拟节点数量
};

int main() {
  std::vector<std::string> servers = {"server1", "server2", "server3"};
  ConsistentHash consistent_hash(servers);

  std::cout << "Key 'user:123' maps to: " << consistent_hash.GetServer("user:123") << std::endl;
  std::cout << "Key 'product:456' maps to: " << consistent_hash.GetServer("product:456") << std::endl;
  std::cout << "Key 'order:789' maps to: " << consistent_hash.GetServer("order:789") << std::endl;

  consistent_hash.RemoveServer("server2");

  std::cout << "After removing server2:" << std::endl;
  std::cout << "Key 'user:123' maps to: " << consistent_hash.GetServer("user:123") << std::endl;
  std::cout << "Key 'product:456' maps to: " << consistent_hash.GetServer("product:456") << std::endl;
  std::cout << "Key 'order:789' maps to: " << consistent_hash.GetServer("order:789") << std::endl;

  return 0;
}

(4)代码解释:

  • MurmurHash2():一个简单的哈希函数,用于将服务器和键哈希到环上。
  • ConsistentHash类:
    • hash_ring_:一个map,存储哈希环,键是哈希值,值是服务器名称。
    • replicas_:每个服务器的虚拟节点数量,增加虚拟节点可以提高负载均衡性。
    • AddServer():添加服务器,为每个服务器创建多个虚拟节点,并将虚拟节点哈希到环上。
    • RemoveServer():删除服务器,删除该服务器的所有虚拟节点。
    • GetServer():根据键的哈希值,在环上找到对应的服务器。
  • lower_bound(): 在map中查找第一个不小于给定键的元素,用于在哈希环上找到合适的服务器。

五、Memcached vs Redis:怎么选?

特性 Memcached Redis
数据类型 简单的键值对 字符串、哈希、列表、集合、有序集合等
持久化 支持RDB和AOF两种持久化方式
功能 简单、快速 功能强大,可以作为缓存、数据库、消息队列等
复杂性 简单 复杂
适用场景 缓存静态数据、Session共享等 各种缓存场景、计数器、排行榜、消息队列等
集群 需要客户端分片 Redis Cluster提供了分布式解决方案

总结:

  • 如果你的需求很简单,只需要缓存一些静态数据,而且对持久化没有要求,那么Memcached是一个不错的选择。
  • 如果你的需求比较复杂,需要更多的数据类型和功能,或者需要持久化,那么Redis更适合你。
  • 在分布式环境下,一致性哈希可以帮助你更好地管理缓存集群。

六、注意事项:

  • 缓存穿透: 如果大量请求查询不存在的键,会导致请求直接打到数据库,造成压力。可以使用布隆过滤器来解决。
  • 缓存雪崩: 如果大量缓存同时失效,也会导致请求直接打到数据库。可以设置不同的过期时间来避免。
  • 缓存击穿: 如果某个热点缓存失效,会导致大量请求同时查询数据库。可以使用互斥锁或者预热缓存来解决。
  • 内存管理: 在C++中使用缓存,要注意内存管理,避免内存泄漏。释放从缓存中获取的数据,使用智能指针等技术。
  • 选择合适的哈希函数: 哈希函数的性能和均匀性对一致性哈希的效果有很大影响。MurmurHash是一个不错的选择,但也要根据实际情况进行评估和选择。
  • 监控: 对缓存进行监控,可以及时发现问题。监控缓存的命中率、延迟、错误率等指标。

好了,今天的分享就到这里。希望大家对C++分布式缓存有了更深入的了解。 记住,没有银弹,选择合适的工具才能事半功倍。

谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注