c++简单实现redis

发布于:2025-03-21 ⋅ 阅读:(8) ⋅ 点赞:(0)

本节我们一步一步实现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