日志系统项目(1)前置知识点

项目介绍

本文中我们将详细的介绍一个基于多设计模式下的同步异步日志系统,本项目主要支持以下的功能:

  1. 支持多级别日志消息
  2. 支持同步日志和异步日志
  3. 支持可靠写入日志到控制台、文件以及滚动文件中
  4. 支持多线程并发写日志
  5. 支持扩展不同日志落地

开发环境

  • Centos 7
  • vscode/vim
  • g++/gdb
  • Makefile

核心技术

  • 类层次设计
  • C++ 11
  • 双缓冲区
  • 生产消费模型
  • 多线程
  • 设计模式(单例、工厂、代理、模版)

日志系统介绍

  • 生产环境的产品为了保证其稳定性及安全性不允许开发人员附加调试器去排查问题,可以借助日志系统来打印一些日志帮助开发人员解决问题
  • 对一些高频操作在少量调试册数下可能无法触发我们想要的行为,通过断点暂停的方式,我们不得不重复几十次、上百次甚至更多,导致排查问题效率非常低下,可以借助打印日志的方式查问题
  • 在分布式、多线程/多进程代码中,出现bug比较难以定位,可以借助日志系统打印log帮助定位bug

日志系统技术实现

日志系统主要包括三种类型:

  • 利用printf、std::cout等输出函数将日志信息打印到控制台
  • 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志,主要分为同步日志和异步日志方式

同步写日志

同步日志是指当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面⾯的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程运行。每次调用一次打印日志API就对应一次系统调用write写日志文件。

在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:
一方面,大量的日志打印陷入等量的write系统调用,有一定系统开销。
另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响程序性能。

异步写日志

异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同⼀个线程中运行,而是有专门的线程用于进行日志输出操作。业务线程只需要将日志放到一个内存缓冲区中不用等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程去完成(作为日志的消费者),这是一个典型的生产-消费模型。

这样做的好处是即使日志没有真的地完成输出也不会影响程序的主业务,可以提高程序的性能:
• 主线程调用日志打印接口成为非阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成

相关技术知识补充

下面我们来简要的介绍一下本项目中需要使用到的技术。

不定参函数

在初学C语言的时候,我们都用过printf函数进行打印。其中printf函数就是一个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进行数据的格式化。而这种不定参函数在实际的使用中也非常多见,在这里简单做一介绍:

// 不定参宏函数:
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, ##__VA_ARGS__); // ... 表示不定参 ##__VA_ARGS__ 表示使用不定参
int main()
{
   // printf("[%s:%d]%s-%d\n", __FILE__, __LINE__, "hello world", 666);
   // LOG("%s, %d\n", "hello world", 666);
   LOG("hello world\n"); // 不定参为空时,使用会出现问题,需要添加##__VA_ARGS__ 告诉编译器,如果不定参为空就取消前面的逗号
   return 0;
}
// C风格不定参函数:
void printNum(int count, ...)
{
  va_list ap;
  //让ap指向count参数之后的第⼀个可变参数
  va_start(ap, count); // last 表示获取的是哪一个参数后第一个不定参的地址(获取指定参数的起始地址,这里就是获取count参数后的第一个参数的起始地址)
  for (int i = 0; i < count; ++i)
  {
      int num = va_arg(ap, int);  //从可变参数中取出⼀个整形参数
      printf("param[%d]:%d\n", i, num);
  }
  va_end(ap); // 将ap指针置空
}
int main()
{
  printNum(5, 1, 2, 3, 4, 5);
  return 0;
}

// 使用vasprintf函数
// int vasprintf(char **strp, const char *fmt, va_list ap);
void myPrintf(const char* fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  char *res;
  int ret = vasprintf(&res, fmt, ap); // res需要free
  if (ret != -1)
  {
      printf(res);
      free(res);
  }
  va_end(ap);
}

int main()
{

  myPrintf("%s-%d\n", "hello", 666);
}

/*C++风格不定参函数的使用*/
void xprintf() // 模板的特化
{
    cout << endl;
}

template<typename T, typename ...Args>
void xprintf(const T& v, Args&& ...args) // 完美转发
{
    cout << v;
    if ((sizeof ...(args)) > 0)
    {
        xprintf(forward<Args>(args)...);
    }
    else
    {
        xprintf();
    }
}

int main()
{
    xprintf("hello", "world", 666);
    return 0;
}

设计模式

单例模式

一个类只能创建一个对象,即单例模式,该设计模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式,本项目使用的是懒汉模式,在第一次被使用的时候才会创建对象

  • 饿汉模式:程序启动时就会创建一个唯一的实例对象。因为单例对象已经确定,所以比较适用于多线程环境中,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提高性能。
  • 懒汉模式:第一次使用要使用单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济源(加载插件、加载网络资源等),可以选择懒汉模式,在第一次使用的时候才创建对象。
/*懒汉方式:懒加载--延迟加载的思想---一个对象在用的时候再进行实例化*/
class Singleton
{
private:
    Singleton() :_data(99)
    {
        std::cout << "懒汉模式:单例对象构造!" << std::endl;
    }
    ~Singleton() {}
    Singleton(const Singleton& ) = delete;
    Singleton operator=(const Singleton& ) = delete;

private:
    int _data;

public:
    static Singleton& getInstance() // C++11之后静态对象的创建是加锁的
    {
        static Singleton _eton;
        return _eton;
    }

    int getData()
    {
        return _data;
    }
};

int main()
{
    std::cout << Singleton::getInstance().getData() << std::endl;
    return 0;
}

其中需要注意的点是从C++ 11起静态变量能够满足thread-safe的前提下唯一的被构造与析构

工厂模式

工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建-使用的分离。
工厂模式可以分为:

  • 简单工厂模式:简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。假设有个工厂能生产出水果,当客户需要产品的时候明确告知工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。
// 简单⼯⼚模式:通过参数控制可以⽣产任何产品
//  优点:简单粗暴,直观易懂。使⽤⼀个⼯⼚⽣产同⼀等级结构下的任意产品
//  缺点:
//  1. 所有东西⽣产在⼀起,产品太多会导致代码量庞⼤
//  2. 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须修改⼯⼚⽅法。
#include <iostream>
#include <string>
#include <memory>
class Fruit
{
public:
    Fruit() {}
    virtual void show() = 0;
};
class Apple : public Fruit
{
public:
    Apple() {}
    virtual void show()
    {
        std::cout << "我是⼀个苹果" << std::endl;
    }
};
class Banana : public Fruit
{
public:
    Banana() {}
    virtual void show()
    {
        std::cout << "我是⼀个⾹蕉" << std::endl;
    }
};
class FruitFactory
{
public:
    static std::shared_ptr<Fruit> create(const std::string &name)
    {
        if (name == "苹果")
        {
            return std::make_shared<Apple>();
        }
        else if (name == "⾹蕉")
        {
            return std::make_shared<Banana>();
        }
        return std::shared_ptr<Fruit>();
    }
};
int main()
{
    std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");
    fruit->show();
    fruit = FruitFactory::create("⾹蕉");
    fruit->show();
    return 0;
}

这个模式的结构和管理产品对象的方式十分简单,但是它的扩展性非常差,当我们需要新增产品的时候,就需要去修改工厂类新增一个类型的产品创建逻辑,违背了开闭原则。

  • 工厂方法模式:在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。假设现在有A、B两种产品,则开两个工厂,工厂A负责生产产品A,工厂B负责生产产品B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类别,而只负责生产产品。
#include <iostream>
#include <string>
#include <memory>
// ⼯⼚⽅法:定义⼀个创建对象的接⼝,但是由⼦类来决定创建哪种对象,使⽤多个⼯⼚分别⽣产指定的固定产品
// 优点:
// 1. 减轻了⼯⼚类的负担,将某类产品的⽣产交给指定的⼯⼚来进⾏
// 2. 开闭原则遵循较好,添加新产品只需要新增产品的⼯⼚即可,不需要修改原先的⼯⼚类
// 缺点:对于某种可以形成⼀组产品族的情况处理较为复杂,需要创建⼤量的⼯⼚类.
class Fruit
{
public:
    Fruit() {}
    virtual void show() = 0;
};
class Apple : public Fruit
{
public:
    Apple() {}
    virtual void show()
    {
        std::cout << "我是⼀个苹果" << std::endl;
    }

private:
    std::string _color;
};
class Banana : public Fruit
{
public:
    Banana() {}
    virtual void show()
    {
        std::cout << "我是⼀个⾹蕉" << std::endl;
    }
};
class FruitFactory
{
public:
    virtual std::shared_ptr<Fruit> create() = 0;
};
class AppleFactory : public FruitFactory
{
public:
    virtual std::shared_ptr<Fruit> create()
    {
        return std::make_shared<Apple>();
    }
};
class BananaFactory : public FruitFactory
{
public:
    virtual std::shared_ptr<Fruit> create()
    {
        return std::make_shared<Banana>();
    }
};
int main()
{
    std::shared_ptr<FruitFactory> factory(new AppleFactory());
    fruit = factory->create();
    fruit->show();
    factory.reset(new BananaFactory());
    fruit = factory->create();
    fruit->show();
    return 0;
}

工厂方法模式每次增加一个产品时,都需要增加一个具体产品类和工厂类,这会使得系统中类的个数成倍增加,在一定程度上增加了系统的耦合度。

  • 抽象工厂模式:工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
#include <iostream>
#include <string>
#include <memory>
// 抽象⼯⼚:围绕⼀个超级⼯⼚创建其他⼯⼚。每个⽣成的⼯⼚按照⼯⼚模式提供对象。
//  思想:将⼯⼚抽象成两层,抽象⼯⼚ & 具体⼯⼚⼦类, 在⼯⼚⼦类种⽣产不同类型的⼦产品
class Fruit
{
public:
    Fruit() {}
    virtual void show() = 0;
};
class Apple : public Fruit
{
public:
    Apple() {}
    virtual void show()
    {
        std::cout << "我是⼀个苹果" << std::endl;
    }

private:
    std::string _color;
};
class Banana : public Fruit
{
public:
    Banana() {}
    virtual void show()
    {
        std::cout << "我是⼀个⾹蕉" << std::endl;
    }
};
class Animal
{
public:
    virtual void voice() = 0;
};
class Lamp : public Animal
{
public:
    void voice() { std::cout << "咩咩咩\n"; }
};
class Dog : public Animal
{
public:
    void voice() { std::cout << "汪汪汪\n"; }
};
class Factory
{
public:
    virtual std::shared_ptr<Fruit> getFruit(const std::string &name) = 0;
    virtual std::shared_ptr<Animal> getAnimal(const std::string &name) = 0;
};
class FruitFactory : public Factory
{
public:
    virtual std::shared_ptr<Animal> getAnimal(const std::string &name)
    {
        return std::shared_ptr<Animal>();
    }
    virtual std::shared_ptr<Fruit> getFruit(const std::string &name)
    {
        if (name == "苹果")
        {
            return std::make_shared<Apple>();
        }
        else if (name == "⾹蕉")
        {
            return std::make_shared<Banana>();
        }
        return std::shared_ptr<Fruit>();
    }
};
class AnimalFactory : public Factory
{
public:
    virtual std::shared_ptr<Fruit> getFruit(const std::string &name)
    {
        return std::shared_ptr<Fruit>();
    }
    virtual std::shared_ptr<Animal> getAnimal(const std::string &name)
    {
        if (name == "⼩⽺")
        {
            return std::make_shared<Lamp>();
        }
        else if (name == "⼩狗")
        {
            return std::make_shared<Dog>();
        }
        return std::shared_ptr<Animal>();
    }
};
class FactoryProducer
{
public:
    static std::shared_ptr<Factory> getFactory(const std::string &name)
    {
        if (name == "动物")
        {
            return std::make_shared<AnimalFactory>();
        }
        else
        {
            return std::make_shared<FruitFactory>();
        }
    }
};
int main()
{
    std::shared_ptr<Factory> fruit_factory = FactoryProducer::getFactory("⽔果");
    std::shared_ptr<Fruit> fruit = fruit_factory->getFruit("苹果");
    fruit->show();
    fruit = fruit_factory->getFruit("⾹蕉");
    fruit->show();
    std::shared_ptr<Factory> animal_factory = FactoryProducer::getFactory("动物");
    std::shared_ptr<Animal> animal = animal_factory->getAnimal("⼩⽺");
    animal->voice();
    animal = animal_factory->getAnimal("⼩狗");
    animal->voice();
    return 0;
}

抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,违背了“开闭原则”。

建造者模式

建造者模式是一种创建型设计模式,使用多个简单的对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核心类实现:

  • 抽象产品类:
  • 具体产品类:一个具体的产品对象类
  • 抽象Builder类:创建一个产品对象所需的各个部件的抽象接口
  • 具体产品的Builder类:实现抽象接口,构建各个部件
  • 指挥者Director类:统一组建过程,提供给调用者使用,通过指挥者来构造产品
/*通过苹果笔记本的构造理解建造者模式*/
#include <iostream>
#include <memory>
#include <string>

/*抽象电脑类*/
class Computer
{
public:
    Computer() {}

    void setBoard(const std::string &board)
    {
        _board = board;
    }

    void setDisplay(const std::string &display)
    {
        _display = display;
    }

    std::string shoeParamters()
    {
        std::string computer = "Computer:{\n";
        computer += "\tboard=" + _board + ",\n";
        computer += "\tdisplay=" + _display + ",\n";
        computer += "\tOs=" + _os + ",\n";
        computer += "}\n";
        return computer;
    }

    virtual void setOs() = 0;

protected:
    std::string _board;
    std::string _display;
    std::string _os;
};

/*具体产品类*/
class MacBook : public Computer
{
public:
    void setOs() override
    {
        _os = "Mac OS x12";
    }
};

/*抽象建造者类:包含创建⼀个产品对象的各个部件的抽象接⼝*/
class Builder
{
public:
    virtual void buildBoard(const std::string &board) = 0;
    virtual void buildDisplay(const std::string &display) = 0;
    virtual void buildOs() = 0;
    virtual std::shared_ptr<Computer> build() = 0;
};

/*抽象建造者类:包含创建⼀个产品对象的各个部件的抽象接⼝*/
class MacBookBuilder : public Builder
{
public:
    MacBookBuilder() : _computer(new MacBook()) {}

    void buildBoard(const std::string &board)
    {
        _computer->setBoard(board);
    }
    
    void buildDisplay(const std::string &display)
    {
        _computer->setDisplay(display);
    }

    void buildOs() override
    {
        _computer->setOs();
    }

    std::shared_ptr<Computer> build()
    {
        return _computer;
    }

private:
    std::shared_ptr<Computer> _computer;
};

/*指挥者类,提供给调⽤者使⽤,通过指挥者来构造复杂产品*/
class Director
{
public:
    Director(Builder *builder) : _builder(builder) {}
    void construct(const std::string &board, const std::string &display)
    {
        _builder->buildBoard(board);
        _builder->buildDisplay(display);
        _builder->buildOs();
    }

private:
    std::shared_ptr<Builder> _builder;
};

int main()
{
    Builder *builder = new MacBookBuilder();
    std::unique_ptr<Director> director(new Director(builder));
    director->construct("华硕主板", "三星显示器");
    std::shared_ptr<Computer> computer = builder->build();
    std::cout << computer->shoeParamters() << std::endl;
    return 0;
}

代理模式

代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引用。在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。代理模式的结构包括一个是真正的你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。代理模式分为静态代理、动态代理:

  • 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
  • 动态代理指的是,在运行时才动态生成代理类,并将其与被代理类绑定。这意味着,在运行时才能确定代理类要代理的是哪个被代理类。
    以租房为例,房东将房子租出去,但是要租房子出去,需要发布招租启示,带人看房,负责维修这些工作中有些操作并非房东能完成,因此房东为了图省事,将房子委托给中介进行租赁。代理模式实现:
/*房东要把一个房子通过中介租出去理解代理模式*/
#include <iostream>
#include <string>

class RentHouse
{
public:
    virtual void rentHouse() = 0;
};

/*房东类:将房⼦租出去*/
class LandLord : public RentHouse
{
public:
    void rentHouse()
    {
        std::cout << "将房子租出去\n";
    }
};

/*中介代理类:对租房⼦进⾏功能加强,实现租房以外的其他功能*/
class InterMediary : public RentHouse
{
public:
    void rentHouse()
    {
        std::cout << "发布招租启示\n";
        std::cout << "带人看房\n";
        _landlord.rentHouse();
        std::cout << "负责租后维修\n";
    }
private:
    LandLord _landlord;
};

int main()
{
    InterMediary intermediary;
    intermediary.rentHouse();
    return 0;
}
  • 30
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值