C++ 日志系统实战第五步:日志器的设计

全是通俗易懂的讲解,如果你本节之前的知识都掌握清楚,那就速速来看我的项目笔记吧~   

本文项目代码编写收尾!

日志器类 (Logger) 设计(建造者模式)

        日志器主要用于和前端交互。当我们需要使用日志系统打印 log 时,只需创建 Logger 对象,调用该对象的 debug、info、warn、error、fatal 等方法,即可输出想打印的日志。它支持解析可变参数列表和输出格式,能做到像使用 printf 函数一样打印日志。

当前日志系统支持同步日志与异步日志两种模式。两种日志器的差异仅在于日志的落地方式:

  • 同步日志器:直接输出日志消息。
  • 异步日志器:将日志消息放入缓冲区,由异步线程进行输出。

因此,设计日志器类时,先设计一个 Logger 基类,在此基础上,继承出 SyncLogger 同步日志器和 AsyncLogger 异步日志器。

        由于日志器模块整合了多个模块,创建一个日志器时,需要设置日志器名称、日志输出等级、日志器类型、日志输出格式以及落地方向(落地方向可能有多个) ,整个日志器的创建过程较为复杂。为保持良好的代码风格,编写出优雅的代码,日志器的创建采用建造者模式。

 

logger.hpp 

#pragma once
/*
日志器实现
1.抽象基类
2.派生出日志器具体实现类
*/
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "message.hpp"
#include "looper.hpp"
#include <atomic>
#include <mutex>
#include <cstdarg>namespace mylog
{class Logger{public:Logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks) : logger_name_(logger_name),limit_level_(limit_level),sinks_(sinks.begin(), sinks.end()),format_builder_(format_builder){}using ptr = std::shared_ptr<Logger>;// 完成构造日志消息构造过程并格式化,然后调用log函数输出日志void debug(std::string file_, size_t line_, const std::string &fat, ...){// 1.判断是否达到日志等级if (limit_level_ > Level::Debug){return;}// 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息va_list args;va_start(args, fat);char *res;int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中if (len < 0){return;}va_end(args);serialize(Level::Debug, file_, line_, res); // 序列化日志消息free(res);                                  // 释放res}void info(std::string file_, size_t line_, const std::string &fat, ...){// 1.判断是否达到日志等级if (limit_level_ > Level::Info){return;}// 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息va_list args;va_start(args, fat);char *res;int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中if (len < 0){return;}va_end(args);serialize(Level::Info, file_, line_, res); // 序列化日志消息free(res);                                 // 释放res}void warning(std::string file_, size_t line_, const std::string &fat, ...){// 1.判断是否达到日志等级if (limit_level_ > Level::Warning){return;}// 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息va_list args;va_start(args, fat);char *res;int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中if (len < 0){return;}va_end(args);serialize(Level::Warning, file_, line_, res); // 序列化日志消息free(res);                                    // 释放res}void error(std::string file_, size_t line_, const std::string &fat, ...){// 1.判断是否达到日志等级if (limit_level_ > Level::Error){return;}// 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息va_list args;va_start(args, fat);char *res;int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中if (len < 0){return;}va_end(args);serialize(Level::Error, file_, line_, res); // 序列化日志消息free(res);                                  // 释放res}void fatal(std::string file_, size_t line_, const std::string &fat, ...){// 1.判断是否达到日志等级if (limit_level_ > Level::Fatal){return;}// 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息va_list args;va_start(args, fat);char *res;int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中if (len < 0){return;}va_end(args);serialize(Level::Fatal, file_, line_, res); // 序列化日志消息free(res);                                  // 释放res}protected:void serialize(Level level, std::string file, size_t line, const char *str) // 序列化日志消息{// 3.构造日志消息LogMessage message(level, file.c_str(), line, str, logger_name_.c_str());// 4.格式化日志消息std::stringstream ss;format_builder_->format(ss, message);// 5.输出日志消息log(ss.str().c_str(), ss.str().size());}// 抽象接口完成实际的落地输出——不同日志器实现类完成具体的落地输出virtual void log(const char *msg, size_t line) = 0;protected:std::mutex mutex_;                       // 日志器互斥锁std::string logger_name_;                // 日志器名称std::atomic<Level> limit_level_;         // 日志等级限制std::vector<mylog::LogSink::ptr> sinks_; // 日志输出器FormatBuilder::ptr format_builder_;      // 日志格式构造器};// 日志器具体实现类——同步日志器class sync_logger : public Logger{// 同步日志器,将日志直接通过落地模块句柄进行日志落地public:sync_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks) : Logger(logger_name, limit_level, format_builder, sinks){}void log(const char *data, size_t len) override{// 加锁std::unique_lock<std::mutex> lock(mutex_);if (sinks_.empty()){return;}for (auto &sink : sinks_){sink->log(data, len); // 调用sink的log函数输出日志}}};class Async_logger : public Logger{public:Async_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks, AsyncType async_type) : Logger(logger_name, limit_level, format_builder, sinks){looper_ = std::make_shared<Async_looper>(std::bind(&Async_logger::reallog, this, std::placeholders::_1));}void log(const char *data, size_t len) override // 将数据写入缓冲区{looper_->push(data, len);}void reallog(Buffer &buf){if (sinks_.empty()){return;}for (auto &sink : sinks_){sink->log(buf.begin(), buf.readable_size()); // 调用sink的log函数输出日志}}private:Async_looper::ptr looper_;};/*使用建造者模式来建造日志器,而不是让用户直接构造日志器,这样可以避免用户构造错误的日志器,简化用户的使用复杂度*/enum class LoggerType{Loggertype_Sync,  // 同步日志器Loggertype_Async, // 异步日志器};// 1.抽象一个建筑者类//   1.设置日志器类型//   2.将不同类型日志器的创建放到同一个日志器建造者中完成class Logger_builder{public:Logger_builder() : type_(LoggerType::Loggertype_Sync), limit_level_(Level::Debug),async_type_(AsyncType::ASYNC_SAFE){}void buildLoggerType(LoggerType type){type_ = type;}void buildLoggerName(std::string logger_name){logger_name_ = logger_name;}void buildLimitLevel(Level limit_level){limit_level_ = limit_level;}void buildFormatBuilder(const std::string &pattern){format_builder_ = std::make_shared<FormatBuilder>(pattern);}void buildAsyncType(AsyncType async_type){async_type_ = async_type;}template <typename LogSinkType, typename... Args>void buildSinks(Args &&...args){LogSink::ptr psink = std::make_shared<LogSinkType>(std::forward<Args>(args)...);sinks_.push_back(psink);}virtual Logger::ptr build() = 0;protected:AsyncType async_type_;LoggerType type_;std::string logger_name_;Level limit_level_;FormatBuilder::ptr format_builder_;std::vector<mylog::LogSink::ptr> sinks_;};// 2.派生出具体建筑者类——局部日志器的建造者 & 全局的日志器建造者class Local_logger_builder : public Logger_builder{public:Logger::ptr build() override{assert(logger_name_.empty() == false);if (format_builder_.get() == nullptr){format_builder_ = std::make_shared<FormatBuilder>();}if (sinks_.empty()){sinks_.push_back(std::make_shared<StdoutSink>());}if (type_ == LoggerType::Loggertype_Async){return std::make_shared<Async_logger>(logger_name_,limit_level_, format_builder_, sinks_, async_type_);}return std::make_shared<sync_logger>(logger_name_, limit_level_, format_builder_, sinks_);}};
}

 只有在交换的时候,生产者和消费者才会产生一次锁冲突

 

 buffer.hpp

#pragma once/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
#include <cassert>namespace mylog
{
#define BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024) // 内存阈值,没有超过阈值,内存翻倍扩容,达到阈值后,内存线性增长
#define INCREASE_BUFFER_SIZE (1 * 1024 * 1024)  // 增加缓冲区大小class Buffer{private:std::vector<char> buffer_; // 缓冲区size_t reader_index;       // 当前可读的位置size_t writer_index;       // 当前可写的位置private:void move_writer(size_t len) // 移动写指针{assert(len + writer_index <= writeable_size());writer_index += len;}void ensure_Enough_capacity(size_t len) // 确保缓冲区有足够的空间容纳len字节{if (len <= writeable_size()) // 如果写入数据超过可写空间,不需要扩容{return;}else // 如果写入数据超过可写空间,需要扩容{size_t new_size = 0;while (new_size < len){if (buffer_.size() < THRESHOLD_BUFFER_SIZE){new_size = buffer_.size() * 2; // 小于阈值,翻倍增长}else{new_size = buffer_.size() + INCREASE_BUFFER_SIZE; // 大于阈值,线性增长}buffer_.resize(new_size); // 扩容}}}public:Buffer() : buffer_(BUFFER_SIZE), reader_index(0), writer_index(0) {}void push(const char *data, size_t len) // 写入数据{// 当前可写空间不足//  if(len>writeable_size()) //如果写入数据超过可写空间//  {//      return;//1.直接返回//  }// 2.动态空间,用于极限性能测试——扩容ensure_Enough_capacity(len);// 写入数据std::copy(data, data + len, &buffer_[writer_index]);// 将写入位置后移len个字节move_writer(len);}const char *begin() // 返回缓冲区起始位置{return &buffer_[reader_index];}size_t readable_size() // 返回可读字节数{return writer_index - reader_index;}size_t writeable_size() // 返回可写字节数{// 对于扩容思路来说,不存在可写空间大小,业务总是可写return buffer_.size() - writer_index;}void move_reader(size_t len) // 移动读指针{assert(len <= readable_size());reader_index += len;}void reset() // 重置缓冲区{reader_index = 0;writer_index = 0;}void swap(Buffer &other) // 交换缓冲区{buffer_.swap(other.buffer_);std::swap(reader_index, other.reader_index);std::swap(writer_index, other.writer_index);}bool empty() // 判断缓冲区是否为空{return reader_index == writer_index;}};
}

双缓冲区异步任务处理器(AsyncLooper)设计

  • 设计思想:异步处理线程 + 数据池
    • 使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执行操作。
  • 任务池的设计思想:双缓冲区阻塞数据池
  • 优势
    • 避免了空间的频繁申请释放。
    • 尽可能的减少了生产者与消费者之间锁冲突的概率,提高了任务处理效率。

        在任务池的设计中,有很多备选方案,比如循环队列等等。但是不管是哪一种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间都具有互斥关系,因此每一次的任务添加与取出都有可能涉及锁的冲突。

        而双缓冲区不同,双缓冲区是处理器将一个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理。虽然同时多线程写入也会冲突,但是冲突并不会像每次只处理一条的时候频繁(减少了生产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。

0

 looper.hpp

/*异步器*/
#pragma once#include "buffer.hpp"
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <memory>
#include <atomic>namespace mylog
{using Functor = std::function<void(Buffer &)>;enum class AsyncType{ASYNC_SAFE,  // 安全状态,表示缓冲区满了则阻塞,防止内存资源耗尽ASYNC_UNSAFE // 非安全状态};class Async_looper{public:using ptr = std::shared_ptr<Async_looper>;Async_looper(const Functor &callback, AsyncType type = AsyncType::ASYNC_SAFE) : stop_(false), thread_(std::thread(&Async_looper::threadEntry, this)), callback_(callback), type_(type) {}void stop(){stop_ = true;con_cv_.notify_all(); // 通知消费者线程退出thread_.join();}~Async_looper(){stop();}void push(const char *data, size_t len){// 1.无线扩容——非安全; 2.固定大小——生产缓冲区满后,生产者线程阻塞;std::unique_lock<std::mutex> lock(mutex_);// 条件变量控制,若缓冲区剩余大小大于数据长度,可以添加数据,否则等待if (type_ == AsyncType::ASYNC_SAFE)pro_cv_.wait(lock, [&](){ return pro_buffer_.writeable_size() >= len; });// 添加数据到缓冲区pro_buffer_.push(data, len);// 通知消费者线程有数据可消费if (type_ == AsyncType::ASYNC_SAFE)con_cv_.notify_all();}private:// 线程入口函数——对消费缓冲区的数据进行处理,处理完毕后,初始化缓冲区void threadEntry(){while (true){// 1.判断生产缓冲区有没有数据,有就交换; .无数据,等待;{std::unique_lock<std::mutex> lock(mutex_);con_cv_.wait(lock, [&](){ return stop_ || !pro_buffer_.empty(); });// 如果停止且生产者缓冲区为空则退出if (stop_ && pro_buffer_.empty())break;// 1.交换生产者缓冲区和消费者缓冲区con_buffer_.swap(pro_buffer_);// 2.通知生产者线程可以继续添加数据if (type_ == AsyncType::ASYNC_SAFE)pro_cv_.notify_all();}// 3.被唤醒后,对消费缓冲区进行数据处理callback_(con_buffer_);// 4.初始化消费者缓冲区con_buffer_.reset();}}private:Functor callback_; // 具体对缓冲区数据进行操作的回调函数private:AsyncType type_;std::atomic<bool> stop_;Buffer pro_buffer_;              // 生产者缓冲区Buffer con_buffer_;              // 消费者缓冲区std::mutex mutex_;               // 互斥锁std::condition_variable pro_cv_; // 生产者条件变量std::condition_variable con_cv_; // 消费者条件变量std::thread thread_;             // 异步线程};
}
class Async_logger : public Logger{public:Async_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks,AsyncType async_type) : Logger(logger_name, limit_level, format_builder, sinks){looper_ = std::make_shared<Async_looper>(std::bind(&Async_logger::reallog, this, std::placeholders::_1));}void log(const char *data, size_t len) override//将数据写入缓冲区{looper_->push(data, len);}void reallog(Buffer &buf){if(sinks_.empty()){return;}for(auto &sink : sinks_){sink->log(buf.begin(), buf.writeable_size()); // 调用sink的log函数输出日志}}private:Async_looper::ptr looper_;};

单例日志器管理类设计(单例模式)

        日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了一个日志器之后,就会受到日志器所在作用域的访问属性限制。

        因此,为了突破访问区域的限制,我们创建一个日志器管理类,且这个类是一个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。

        基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的日志器进行日志输出。

 在logger.hpp里面

class LoggerManager{private:std::mutex mutex_;Logger::ptr root_logger_;                              // 默认日志器std::unordered_map<std::string, Logger::ptr> loggers_; // 日志器名称-日志器映射表private:LoggerManager(){std::unique_ptr<mylog::Local_logger_builder> builder(new mylog::Local_logger_builder());builder->buildLoggerName("root");root_logger_ = builder->build();addLogger(root_logger_);}public:static LoggerManager &getInstance(){// 在C++11之后,针对静态变量,编译器在编译的层面实现了线程安全// 当静态局部变量在没有构造完成之前,其他线程进入就会阻塞static LoggerManager eton;return eton;}void addLogger(Logger::ptr logger){if (hasLogger(logger->get_logger_name()))return;std::unique_lock<std::mutex> lock(mutex_);loggers_.insert(std::make_pair(logger->get_logger_name(), logger));}bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(mutex_);auto it = loggers_.find(name);if (it != loggers_.end()){return true;}return false;}Logger::ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(mutex_);auto it = loggers_.find(name);if (it != loggers_.end()){return it->second;}return nullptr;}Logger::ptr rootLogger(){return root_logger_;}};

日志宏 & 全局接口设计(代理模式)

  • 提供全局的日志器获取接口。
  • 使用代理模式,通过全局函数或宏函数来代理 Logger 类的 log、debug、info、warn、error、fatal 等接口,以便于控制源码文件名称和行号的输出控制,简化用户操作。
  • 当仅需标准输出日志的时候,可以通过主日志器来打印日志。且操作时只需要通过宏函数直接进行输出即可。

mylog.h

#pragma once#include"logger.hpp"namespace mylog
{Logger::ptr get_logger(const std::string &name){return mylog::LoggerManager::getInstance().getLogger(name);}Logger::ptr rootLogger(){return mylog::LoggerManager::getInstance().rootLogger();}#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define DEBUG(fmt, ...) mylog::rootLogger()->debug(fmt, ##__VA_ARGS__)#define INFO(fmt, ...) mylog::rootLogger()->info(fmt, ##__VA_ARGS__)#define WARN(fmt, ...) mylog::rootLogger()->warn(fmt, ##__VA_ARGS__)#define ERROR(fmt, ...) mylog::rootLogger()->error(fmt, ##__VA_ARGS__)#define FATAL(fmt, ...) mylog::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}

后续还有测试代码,期待共同实现~

  如果你对日志系统感到兴趣,欢迎关注我👉【A charmer】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/diannao/86778.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot + MyBatis日志前缀清除方法

在 Spring Boot 结合 MyBatis 的应用中&#xff0c;清空日志前缀&#xff08;如 > 、< 等&#xff09;需要通过 自定义 MyBatis 的日志实现 或 修改日志模板 来实现。以下是两种常用方法&#xff1a; 方法 1&#xff1a;自定义 MyBatis 日志实现&#xff08;推荐&#xf…

【消息队列】——如何实现消息保序

目录 一、哪些场景需要消息保序?二、如何实现消息保序?三、保序消息的常见问题和应对策略3.1、重复消息3.2、节点故障3.3、分区扩容四、小结本文来源:极客时间vip课程笔记 一、哪些场景需要消息保序? 消息保序问题指的是,在通过消息中间件传递消息过程中,我们希望消费者收…

Transformer模型详解

Transformer Transformer真是个细节满满的框架呢&#xff0c;大三读到根本不敢看&#xff0c;考研复试前看了看&#xff0c;以为懂了其实差得还远&#xff0c;两个多月前看了&#xff0c;还是一知半解&#xff0c;如今终于经过细细分析&#xff0c;算是知道了Transformer的基本…

火山引擎发布豆包大模型 1.6 与视频生成模型 Seedance 1.0 pro

6 月 11 日&#xff0c;在火山引擎 FORCE 原动力大会上&#xff0c;字节跳动旗下火山引擎正式发布豆包大模型 1.6、豆包・视频生成模型 Seedance 1.0 pro、豆包・语音播客模型&#xff0c;豆包・实时语音模型也在火山引擎全量上线&#xff0c;豆包大模型家族已成为拥有全模态、…

PH热榜 | 2025-06-12

1. Atlas 标语&#xff1a;几秒钟内了解定价情况 介绍&#xff1a;获取即插即用的定价页面&#xff0c;让你轻松赚钱&#xff0c;不再辛苦操劳。 产品网站&#xff1a; 立即访问 Product Hunt&#xff1a; View on Product Hunt 关键词&#xff1a;Atlas, 定价快速, 插件式…

ChatGPT革命升级!o3-pro模型重磅发布:开启AI推理新纪元

2025年6月10日&#xff0c;OpenAI以一场低调而震撼的发布&#xff0c;正式推出了新一代推理模型o3-pro&#xff0c;这标志着人工智能在复杂问题解决领域的重大突破。作为ChatGPT Pro和Team订阅用户的专属工具&#xff0c;o3-pro不仅重新定义了AI的可靠性标准&#xff0c;更以其…

NVIDIA Isaac GR00T N1.5 适用于 LeRobot SO-101 机械臂

系列文章目录 目录 系列文章目录 前言 一、简介 二、详细教程 2.1 数据集准备 2.1.1 创建或下载您的数据集 2.1.2 配置模态文件 2.2 模型微调 2.3 开环评估 2.4 部署 &#x1f389; 快乐编程&#xff01;&#x1f4bb;&#x1f6e0;️ 立即开始&#xff01; 前言 一…

【编译工具】(自动化)自动化测试工具:如何让我的开发效率提升300%并保证代码质量?

目录 引言&#xff1a;自动化测试在现代开发中的关键作用 一、自动化测试金字塔&#xff1a;构建高效的测试策略 &#xff08;1&#xff09;测试金字塔模型 &#xff08;2&#xff09;各层级代表工具 二、前端自动化测试实战&#xff1a;Jest Cypress &#xff08;1&…

R语言缓释制剂QBD解决方案之一

本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》缓释制剂包衣处方研究的R语言解决方案。 ER聚合物包衣处方优化研究 基于初步风险评估和初始可行性研究&#xff0c;进行带3个中心点的24-1分式析因DOE。药物的释放被识别为CQA。本研究的…

行为模式-命令模式

定义&#xff1a; 命令模式是一个高内聚的模式&#xff0c;其定义为&#xff1a;Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.&#xff08;将一个请求封装成…

Ubuntu 24.04 上安装与 Docker 部署 Sentinel

Ubuntu 24.04 上安装与 Docker 部署 Sentinel 一、Sentinel 简介 Sentinel 是阿里巴巴开源的分布式系统流量控制组件&#xff0c;提供流量控制、熔断降级和系统负载保护等功能。它通过可视化控制台&#xff08;Dashboard&#xff09;实现实时监控和规则管理&#xff0c;是微服…

IP 地址查询在证券交易中的应用方式

网络安全保障与IP地址查询 证券交易平台存储着海量投资者的敏感信息以及巨额资金的交易数据&#xff0c;是网络攻击的重点目标。IP 地址查询在检测异常登录行为方面至关重要。例如&#xff0c;当一个账户短时间内先在国内某城市登录&#xff0c;随后又在境外 IP 地址发起交易操…

Flutter 常用组件详解:Text、Button、Image、ListView 和 GridView

Flutter 作为 Google 推出的跨平台 UI 框架&#xff0c;凭借其高效的渲染性能和丰富的组件库&#xff0c;已经成为移动应用开发的热门选择。本文将深入探讨 Flutter 中最常用的五个基础组件&#xff1a;Text、Button、Image、ListView 和 GridView&#xff0c;帮助开发者快速掌…

docker 单机部署redis集群(一)

docker 部署redis集群 1、创建redis网卡 docker network create redis --subnet 172.38.0.0/16查看网卡信息 docker network ls docker network inspect redis2、创建redis配置 #使用脚本创建6个redis配置for port in $(seq

MySQL 索引学习笔记

1.二叉树&#xff0c;红黑树&#xff0c;B 树&#xff0c;B树 二叉树&#xff1a;就是每个节点最多只能有两个子节点的树&#xff1b; 红黑树&#xff1a;就是自平衡二叉搜索树&#xff0c;红黑树通过一下五个规则构建&#xff1a; 1.节点只能是红色或黑色&#xff1b; 2.根…

Windows安装docker及使用

下载 https://www.docker.com/ 安装 启动 此时拉取镜像会报错 Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 配置引擎 添加以…

多参表达式Hive UDF

支持的操作符 &#xff1a;跳过&#xff0c;即无条件筛选&#xff1a;等于!&#xff1a;不等于range&#xff1a;区间内&#xff0c;range[n,m]表示 between n and mnrange&#xff1a;区间外&#xff0c;即not between andin&#xff1a;集合内&#xff0c;in(n,m,j,k)表示 in…

GO后端开发内存管理及参考答案

什么是 Go 的逃逸分析&#xff08;Escape Analysis&#xff09;&#xff0c;为什么需要它&#xff1f; Go 的逃逸分析是一种编译时技术&#xff0c;用于确定变量的生命周期是否超出其创建的函数作用域。通过分析变量的使用方式&#xff0c;编译器能够判断变量是否需要在堆上分…

未来智能系统演进路线:从AGI到ASI的技术蓝图

引言&#xff1a;智能革命的下一个十年 在AI技术突破性发展的当下&#xff0c;我们正站在通用人工智能&#xff08;AGI&#xff09;向人工超级智能&#xff08;ASI&#xff09;跃迁的关键转折点。本文将系统解析未来3-10年的技术演进路径&#xff0c;通过模块化组件插件&#…

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)

说明&#xff1a; 想象一下&#xff0c;你正在用eNSP搭建一个虚拟的网络世界&#xff0c;里面有虚拟的路由器、交换机、电脑&#xff08;PC&#xff09;等等。这些设备都在你的电脑里面“运行”&#xff0c;它们之间可以互相通信&#xff0c;就像一个封闭的小王国。 但是&#…