本节我们一步一步实现redis的基本数据类型,redis的数据类型讲解可以看简答介绍Redis的基本使用-CSDN博客
核心redis类的实现
enum class RedisDataType {
STRING,
HASH,
SET,
ZSET,
LIST
};
class Redis : public Singleton<Redis> {
friend class Singleton<Redis>;
protected:
Redis();
~Redis();
public:
std::string command(const std::string& command);
private:
DataModel* plantDataModel(const std::string& type);
void initCommandTypes();
private:
std::unordered_map<std::string, DataModel*> redis;
std::unordered_map<std::string, RedisDataType> commandTypes;
};
我们通过这个类来控制整个redis系统,这是一个单例类单例类实现方法,commandTypes是各个设计命令对应的类型,比如set对应string,hset对应hash,redis这个成员变量是所有的键值对,这个函数继承了一个单例模板类,我们通过you hao,它只对外开放一个command接口,参数和返回值都是一个字符串,下面是具体实现
std::string Redis::command(const std::string &command)
{
std::istringstream iss(command);
std::string type, key;
iss >> type;
iss >> key;
if (type.empty() || key.empty()) {
std::cout << "Wrong command" << std::endl;
return " ";
}
DataModel* model = nullptr;
if (redis.count(key) == 0) {
model = plantDataModel(type);
if (model == nullptr){
std::cout <<"( mistake )" << std::endl;
return "mistake";
}
redis[key] = model;
} else {
model = redis[key];
}
std::string result = model->executeCommand(type, iss);
return result;
}
使用std::istringstream来按空格读取指令,我们先读取第一个单词用来确定类型,第二个确定key,接下来我们检测key是否存在,如果不存在就调用plantDataModel函数来创建对应的类,如果创建失败则表示命令错误,最后我们调用executeCommand函数来执行命令,接下来我们看看如何实现redis的数据结构
数据类型
基类
class DataModel {
public:
DataModel(){ }
virtual ~DataModel(){ }
virtual std::string executeCommand(const std::string& command, std::istringstream& iss) = 0;
virtual void initCommandMap() = 0;
protected:
std::unordered_map<std::string, std::function<std::string(std::istringstream&)>> commandMap;
};
在基类中定义了两个接口,executeCommand用于执行命令,initCommandMap用于初始化命令处理map,我们通过继承这个类可以规范子类的行为,成员变量commandMap用于存放处理命令的键值对
Hash
class HashDataModel: public DataModel {
public:
HashDataModel();
~HashDataModel();
std::string executeCommand(const std::string& command, std::istringstream& iss) override;
std::vector<std::string> getAllKeys();
private:
void initCommandMap() override;
private:
std::unordered_map<std::string, std::string> hashDataModel;
};
对于哈希类型我们内部使用了一个键值对来实现,并且实现executeCommand与initCommandMap接口
std::string HashDataModel::executeCommand(const std::string& command, std::istringstream& iss)
{
if(commandMap.count(command)){
std::cout << "Executing command: " << command << " ";
return commandMap[command](iss);
}
return "error Hash " + command + " not found";
}
void HashDataModel::initCommandMap()
{
commandMap["hash"] = [this](std::istringstream& iss) -> std::string {
std::string word;
while (iss >> word) {
std::string key(std::move(word));
iss >> word;
std::string value(std::move(word));
hashDataModel[key] = value;
}
return "OK";
};
commandMap["hget"] = [this](std::istringstream& iss) -> std::string {
std::string key;
iss >> key;
if(hashDataModel.count(key)){
return hashDataModel[key];
}
return "error No key: " + key + " was found";
};
commandMap["hgetall"] = [this](std::istringstream& iss) -> std::string {
std::string result;
for(auto& key : hashDataModel){
result += key.first + " " + key.second + "\n";
}
return result;
};
}
executeCommand会先检查是否有对应的命令处理函数,如果没有就返回报错,initCommandMap的实现相对简单,只需要添加对应的命令处理函数就可以了,其他数据结构实现类型,这里不过多介绍了
网络交互
网络相关的东西都使用的Linux API实现,比如套接字与I/O多路复用,这里只做简单介绍,详细可见Linux中的epoll简单使用案例-CSDN博客与Linux网络编程,创建api的使用方式-CSDN博客
我们使用了一个单独的线程来运行网络服务,这就导致了我们想要实现主线程与子线程的数据交互,考虑到想要极高的性能这里选择了无锁队列,又因为我们的是单消费者与单生产者的关系,最终选择了环形队列的实现
struct Data {
std::string data;
int client_fd;
};
class RingBuffer {
public:
RingBuffer(int size, int efd)
: _buffer(size), _size(size), _efd(efd) { }
~RingBuffer(){ }
bool push(std::string&& data, int client_fd){
if(full()) return false;
_buffer[_tail].data = std::move(data);
_buffer[_tail].client_fd = client_fd;
_tail = (_tail + 1) & (_size - 1);
if (_efd != 0) {
write(_efd, &value, sizeof(value)); // 触发epoll事件
}
return true;
}
bool pop(std::string& data, int& client_fd){
if(empty()) return false;
data = std::move(_buffer[_head].data);
client_fd = _buffer[_head].client_fd;
_head = (_head + 1) & (_size - 1);
return true;
}
bool empty(){
return _head == _tail;
}
bool full(){
return (_tail + 1) % _size == _head;
}
int size(){
return _buffer.size();
}
int getFd(){
return _efd;
}
private:
std::vector<Data> _buffer;
int _head{0};
int _tail{0};
int _size;
int _efd;
uint64_t value = 1;
};
这种队列是固定大小的,通过两个指针来确定读与写的位置,因为读与写指针都只有一个线程操作,所以根本不需要锁,本实现中还在push函数中添加的epoll的内容,epoll可以自动通知读现在又新内容了
全部代码见myRedis