【项目日记(一)】-仿mudou库one thread oneloop式并发服务器实现

1、模型框架

客户端处理思想:事件驱动模式

事件驱动处理模式:谁触发了我就去处理谁。

( 如何知道触发了)技术支撑点:I/O的多路复用 (多路转接技术)

1、单Reactor单线程在单个线程中进行事件驱动并处理
对所有客户端进行IO事件监控、哪个客户端触发了事件,就去处理谁
处理:接收它的请求,进行业务处理,进行响应。
优点:单线程操作,操作都是串行化的,思想简单,(不需要考虑进程或者线程间的通信问题,以及安全问题)
缺点:所有的事件监控和业务处理都是在一个线程中完成的,因此很容易造成性能瓶颈
适用场景:客户端数量较少,且业务处理快速简单的场景
2、 单Reactor多线程:一个Reactor线程 + 业务线程池
对所有客户端进行IO事件监控、哪个客户端触发了事件,就去处理谁
(Reactor线程)处理:仅仅进行IO操作
然后将事件进行派发给业务线程
优点:充分利用cpu多核资源,处理效率可以更高,降低了代码的耦合度(IO操作和业务处理进行分离)
缺点:在单个的Reactor线程中,包含了对所有客户端的事件监控,以及所有客户端的IO操作,不利于高并发场景(即每一个时刻都有很多客户端连接请求,我还在处理上一个client的IO操作的话就来不及进行新的client的连接处理)
3、多Reactor多线程:基于单Reator多线程的缺点考虑,如果IO的时候,有连接到来无法处理,因此将连接单独拎出来。
因此让一个Reactor线程仅仅进行新连接处理,让其他的Reactor线程进行IO处理,IO Reactor线程拿到数据分发给业务线程池进行处理。因此,多Reactor多线程模式,也叫主从Reactor模型
主Reactor线程:进行新连接事件监控
从属Reactor线程:进行IO事件监控
业务线程池:进行业务处理
优点:充分利用CPU多核资源,并且可以进行合理分配
但是:执行流并不是越多越好,因为执行流多了,反而会增加cpu切换调度成本。(所以在有些多Reactor多线程模式中从属Reactor线程也会充当业务处理函数。

2、 功能模块划分:

SERVER模块:实现Reactor模型的TCP服务器;
协议模块:对当前的Reactor模型服务器提供应⽤层协议支持
2.1、server模块:  对所有的连接以及线程进⾏管理
⽽具体的管理也分为三个⽅⾯:
监听连接管理:对监听连接进⾏管理。
有监听套接字来获取新连接。所以,首先要有一个监听套接字来获取新连接。
通信连接管理:对通信连接进⾏管理。
获取新连接之后就有了通信套接字。 然后对不同的事件进行处理
超时连接管理:对超时连接进⾏管理
对于超时的连接进行释放来归还资源。
连接模块:
Buffer模块:Buffer模块是⼀个缓冲区模块,⽤于实现通信中⽤⼾态的接收缓冲区和发送缓冲区功能
Socket模块: Socket模块是对套接字操作封装的⼀个模块,主要实现的socket的各项操作
Channel模块: Channel模块是对⼀个描述符需要进⾏的IO事件管理的模块,实现对描述符可读,可写,错误...事件的 管理操作,以及Poller模块对描述符进⾏IO事件监控就绪后,根据不同的事件,回调不同的处理函数功能
Connection模块:
Connection模块是对Buffer模块,Socket模块,Channel模块的⼀个整体封装,实现了对⼀个通信套 接字的整体的管理,每⼀个进⾏数据通信的套接字(也就是accept获取到的新连接)都会使⽤Connection进⾏管理。
Acceptor模块:
Acceptor模块是对Socket模块,Channel模块的⼀个整体封装,实现了对⼀个监听套接字的整体的管理。
TimerQueue模块:
TimerQueue模块是实现固定时间定时任务的模块,可以理解就是要给定时任务管理器,向定时任务管理器中添加⼀个任务,任务将在固定时间后被执⾏,同时也可以通过刷新定时任务来延迟任务的执⾏。
连接监控模块:
Poller模块:
Poller模块是对epoll进⾏封装的⼀个模块,主要实现epoll的IO事件添加,修改,移除,获取活跃连接功能。


bind函数

bind作⽤也可以简单理解为给⼀个函数绑定好参数,然后返回⼀个参数已经设定好或者预留好
的函数,
想要基于print函数,适配⽣成⼀个新的函数,这个函数固定第1个参数传递hello变量,
第⼆个参数预留出来,在调⽤的时候进⾏设置
#includ <iostream>
#include <string>
#include <functional>void print(const std::string &str,int num)
{std::cout << str <<  num << std::endl;
}int main()
{//print("hello");auto func = std::bind(print, "hello",std::placeholders::_1);func(10);//----打印结果 hello 10auto func = std::bind(print, "hello",std::placeholders::_1, std::placeholders::_2);func(10,20);// ---打印结果 hello 10 20即传入的参数是传给第二个、第三个以及之后的参数//func();//直接调用func()就相当于调用print和传入hello参数//std::placeholders::_1, std::placeholders::_2  预留一个参数 预留两个参数return 0;
}

bind函数作用:当我们设计线程池或者任务池的时候,比如要设置一个任务队列,这个任务队列里面要包含两个信息,任务要处理的数据以及这个数据要如何被处理(处理数据的方法) 所以我们要给任务池中添加函数进去、再添加一个数据进去

#includ <iostream>
#include <string>
#include <functional>
#include <vector>void print(const std::string &str,int num)
{std::cout << str <<  num << std::endl;
}int main()
{//using定义类型别名 Task 代表std::function<void()>类型//std::function 是 C++ 标准库 <functional> 头文件里的一个模板类,//它属于通用的多态函数包装器。//其作用是存储、复制和调用任何可调用对象//std::function<void()> 是 std::function 的一个具体实例化using Task = std::function<void()>;std::vector<Task> arry; //一个任务数组  arry.push_back(std::bind(print, "hello",10)); //任务组中放入的是数据和对数据的处理方法//bind它的作用是创建一个新的可调用对象,这个新对象会绑定指定的函数和参数。arry.push_back(std::bind(print, "nihao",20));arry.push_back(std::bind(print, "hhhhh",30));for(auto &f:arry){f();  //f() 调用存储在 f 中的可调用对象,也就是执行之前绑定的 print 函数。}return 0;
}

定时器:

定时去销毁不活跃的连接

1、int  timerfd_create(int  clockid,  int  flags)

创建一个定时器 (linux下一切皆文件)所有定时器的操作也是当作文件去操作的

clockid:CLOCK_REALTIME----以系统时间作为计时基准值(如果系统时间发生改变就会出问题)(一般不用)

CLOCK_MONOTONIC---以系统启动时间进行递增的一个基准值(定时器不会随着系统时间的改变而改变)

返回值:文件描述符

flags: 0 --- 阻塞操作

linux下一切皆文件,定时器的操作也是跟文件操作并没有区别,而是定时器定时原理每隔一段时间(定时器的超时时间),系统就会给这个描述符对应的定时器写入一个8字节的数据

创建一个定时器,定时器定立的超时时间是3s,也就是说每3s计算一次超时

从启动开始,每隔3s,也就是每3s计算一次超时

从启动开始,每隔3s中,系统都会给描述符写入一个1,表示从上一次读取数据到现在超时了1次

假设30s之后开始读数据,则这个时候会读取到10,表示上一次读取数据到现在超时了10次

2、int  timerfd_settime(int  fd, int  flags,  struct  itimerspec  *new, struct  itimerspce  *old);

功能:启动定时器

fd:timerfd_create函数的返回值---文件描述符---创建的定时器的标识符

flags:默认设置为0---使用相对时间(相对于当前的超时时间往后延长多少时间之后的超时)

struct  itimerspec  *new:设置的超时时间

struct timespec {time_t tv_sec; /* 秒 */long tv_nsec; /* 纳秒 */};struct itimerspec {struct timespec it_interval; /* 第⼀次之后的超时间隔时间 */struct timespec it_value; /* 第⼀次超时时间 */};

struct  itimerspce  *old:用于接收当前定时器原有的超时时间设置,保存起来以便于还原(不需要还原也可以直接传空)        

定时器的基本代码:

#include <stdio.h>
#include <unistd.h>
#inlcude <fcntl.h>
#include <stdint.h>
#include <sys/timerfd.h>int main()
{int timerfd_create(CLOCK_MONOTONIC,0);if(timerfd < 0){perror("timerfd_create error");return -1;}struct itimerspec itime;itime.it_value.tv_sec = 1;itime.it_value.tv_nsec = 0;//第一次超时时间为1s后itime.it_interval.tv_sec = 1;itime.it_interval.tv_nsec = 0;//第一次超时后,每次超时的时间间隔timerfd_settime(timerfd, 0, &itime, NULL);while(1){uint64_t times; //8字节大小int ret = read(timerfd, &times, 8);if(ret < 0){perror("read error");return -1;}printf("超时了,距离上一次超时了%d次\n",times);}close(timerfd);return 0;
}

定时器的作用:高并发的服务器需要定时的去清理不活跃的连接,定义一个定时器,每隔一秒去检测,(每隔一秒把连接拿过来遍历一下,看谁是非活跃超时了)超时了就把它释放掉,每隔一秒来一次。

那如果有上万个连接,全遍历一遍效率就会很低很低。

这时候⼤家就会想到,我们可以针对所有的连接,根据每个连接最近⼀次通信的系统时间建立一个小根堆,这样只需要每次针对堆顶部分的连接逐个释放,直到没有超时的连接为⽌,这样也可以大大提高处理的效率。
另一种方案: 时间轮
设置一个tick滴答指针,指向哪里就代表哪里任务超时了
如果tick滴答,以秒为计时单位,如果当前的数组有7个元素,那么最大定时时间就只有7s
如果定时器想要设置超大时间定时任务 (不可能去设置一个超大的数组吧)
可以采⽤多层级的时间轮,有秒针轮,分针轮,时针轮    
设置以天为单位的时间轮:
存在的问题:
1、上面这样的数组,同一时刻的定时任务只能添加一个,需要考虑如何在同一时刻支持添加多个定时任务?

解决方法: 将时间轮的一维数组设计为二维数组(每一个元素都是一个数组)
2、假设当前的定时任务是一个连接的非活跃销毁任务,这个任务什么时候添加到时间轮中比较合适?
 
一个连接30s内都没有通信,则是一个非活跃连接,这时候就销毁
但是一个连接如果在建立的时候添加了一个30s后的销毁任务,但是这个连接30s内人家有数据通信,在第30s的时候不是一个非活跃连接
思想:需要在一个连接有IO事件产生的时候,延迟定时任务的执行
作为一个时间定时器,本身并不关注任务类型,只要是时间到了就需要被执行
解决方案:类的析构函数  +  职能指针share_ptr, 通过这两个技术可以实现定时任务的延时
1、使用一个类,对定时任务进行封装,类实例化的每一个对象,就是一个定时任务对象,当对象被销毁的时候,再去执行定时任务( 将定时任务的执行放到析构函数中
2、share_ptr用于对new的对象进行空间管理,当share_ptr对一个对象进行管理的时候,内部有一个计数器,计数器为0的时候,则释放所管理的对象。
int *a = new  int;
std::share_ptr<int>  pi(a);
std::share_ptr<int>  pi1(pi);
a对象只有在pi计数为0的时候,才会被释放
当针对pi又构建了一个shared_ptr对象pi1,则pi和pi1计数器为2
但是如果时针对原始对象进行构造,并不会跟pi和pi1共享计数
当pi和pi1中任意一个被释放的时候,只是计数器-1,因此它们管理的a对象并没有被释放,只有当pi和pi1都被释放了,计数器为0了,才会释放管理的a对象
基于这个思想,我们可以使用share_ptr来管理定时器任务对象
例如:对象被销毁的时候,任务(task)才会被执行() 智能指针里面有一个ptr指向task,将智能指针放到定时数组里面,两秒之后,智能指针被释放计数器为0,tsak会被释放掉,就会执行任务。如果在两秒之间,连接又发送了数据,这个连接变为活跃的,我们就针对share_ptr再生成一个share_ptr,计数器就变为了2,把智能指针添加到时间轮里面去,第一次不会执行task,只有第二次会执行task。
#include<iostream>
#include<vector>
#include <unordered_map>
#include <cstdint>
#include <functional>
#include <memory>
#include <unistd.h>using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;class TimerTask  //这个类代表定时器任务
{
private:uint64_t _id; //定时器任务对象IDuint32_t _timeout; //定时器任务的超时时间bool _canceled;  //false表示没有被取消  true表示被取消了TaskFunc _task_cb; //定时器对象要执行的定时任务ReleaseFunc _release; //用于删除 TimerWheel中保存的定时器对象信息public:TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb) :_id(id),_timeout(delay),//外界自己传入_task_cb(cb),_canceled(false){}~TimerTask()  //执行定时器任务{if(_canceled == false)_task_cb(); //当定时任务触发时,需要执行的具体操作//在析构的时候执行是因为 定时器的任务是销毁不活跃的连接 那么 他的本质任务就是销毁 即可以在类对象析构的时候任务对象被销毁//具体执行什么函数会自己设置 在这个任务构造的时候 需要自己传入的参数第三个_release();// 从TimerWheel 的 _timers 哈希表中删除当前定时器任务的信息 --调用这个函数就是调用TimerWheel类中的RemoveTimer(因为下面的bind函数)}void Cancel(){_canceled = true; //true代表已经被取消}void SetRelease(const ReleaseFunc &cb)  //传入的参数是函数{_release = cb; }uint32_t DelayTime(){return _timeout;}
};class TimerWheel  //管理这些定时器任务
{
private:using WeakTask = std::weak_ptr<TimerTask>;using PtrTask = std::shared_ptr<TimerTask>;int _capacity; //表盘最大数量---就是最大延迟时间//用于管理 TimerTask 对象的生命周期,确保任务对象在被添加到时间轮中并且还有其他地方引用时不会被提前销毁。std::vector<std::vector<PtrTask>> _wheel; //时间轮二维数组里面放的不是任务task而是对任务的share_ptr指针int _tick; //tick走到哪里哪里执行  (即释放哪里的对象)执行哪里的任务//为了避免因哈希表对任务对象的引用而导致对象无法被正常销毁的情况,同时又能在需要时获取到任务对象进行操作。std::unordered_map<uint64_t, WeakTask> _timers; //放入的WeakTask类型,只有这样在后面构造share_ptr的时候才会共享计数,而且自身也不影响计数
private:void RemoveTimer(uint64_t id) //从管理(map)中删除{auto it = _timers.find(id);if(it != _timers.end()){_timers.erase(it);}}
public:TimerWheel():_capacity(60), _tick(0),_wheel(_capacity) {}void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)//添加定时任务 --第三个参数就是定时器任务触发时,具体需要执行的任务{PtrTask pt(new TimerTask(id, delay, cb));pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));//将RemoveTimer绑定一个参数,得到的函数,作为参数传递给SetRelease函数int pos = (_tick + delay) % _capacity;_wheel[pos].push_back(pt);//数组_timers[id] = WeakTask(pt); //_timers哈希表中,值为id的元素(如果有就跟新,如果没有就新创建)  WeakTask(pt)----以pt这个 std::shared_ptr为参数构建了一个std::weak_ptr<TimerTask> 类型的弱引用}void TimerRefresh(uint64_t id)//刷新/延迟定时任务{//通过保存的定时器对象的weak_ptr构造一个share_ptr出来,添加到轮子中auto it = _timers.find(id);if(it == _timers.end()){return;//没找到定时任务,无法进行刷新,无法延迟}PtrTask pt = it->second.lock(); //lock获取weak_ptr管理的对象对应的share_ptr//it->second代表  与id对应的 std::weak_ptr<TimerTask> 对象//std::weak_ptr 类的一个成员函数,它的作用是尝试创建一个指向 std::weak_ptr 所观察对象的 std::shared_ptr//从 _timers 哈希表中找到与给定 id 对应的 std::weak_ptr<TimerTask> 对象,//然后调用其 lock() 方法尝试获取一个指向该 TimerTask 对象的 std::shared_ptr。//如果该 TimerTask 对象还存在(即其引用计数不为 0),则 lock() 方法会返回一个有效的 std::shared_ptr,//并将其赋值给 pt;如果该 TimerTask 对象已经被销毁(引用计数为 0),则 lock() 方法会返回一个空的 std::shared_ptr。//为什么这样写????//由于 _timers 中存储的是 std::weak_ptr,我们不能直接通过它来操作对象。//因此,需要调用 lock() 方法获取一个 std::shared_ptr,这样才能确保在操作对象时,对象是存在的。//同时,使用 std::shared_ptr 操作对象可以保证在操作期间对象不会被意外销毁,因为 std::shared_ptr 会增加对象的引用计数。int dalay = pt->DelayTime();//DelayTime() 这个时间外界自己传入int pos = (_tick + dalay) % _capacity;_wheel[pos].push_back(pt); //重新更新位置}void TimerCancel(uint64_t id){auto it = _timers.find(id);if(it != _timers.end()){return;//没找到定时任务,无法进行刷新,无法延迟}PtrTask pt = it->second.lock(); //lock获取weak_ptr管理的对象对应的share_ptrif(pt)pt->Cancel();}//这个函数应该每秒被执行一次,相当于秒针向后走了一步void RunTimerTask(){_tick = (_tick + 1) % _capacity;_wheel[_tick].clear();//清空指定位置的数组,就会把数组中保存的所有管理定时器对象的share_ptr释放掉//它会调用 std::vector 的 clear 方法,将该槽对应的 std::vector<PtrTask> 中的所有 std::shared_ptr<TimerTask> 移除。//当 std::shared_ptr 被移除时,如果该 std::shared_ptr 是最后一个指向 TimerTask 对象的强引用,//那么它所管理的 TimerTask 对象的引用计数会变为 0,从而触发 TimerTask 对象的析构函数 ~TimerTask()。}
};
class Test
{
public:Test(){std::cout << "构造" << std::endl;}~Test(){std::cout << "构造" << std::endl;}
};
void DelTest(Test *t)
{delete t;
}int main()
{TimerWheel tw;Test *t = new Test();tw.TimerAdd(888,5,std::bind(DelTest,t));//设置具体的任务id、延时时间、以及定时器触发时具体要执行的任务for(int i = 0; i < 5; i++){tw.TimerRefresh(888);//刷新定时任务tw.RunTimerTask();//向后移动秒针sleep(1);}while(1){tw.RunTimerTask();sleep(1);}return 0;
}

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

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

相关文章

Go语言实现OAuth 2.0认证服务器

文章目录 1. 项目概述1.1 OAuth2 流程 2. OAuth 2.0 Storage接口解析2.1 基础方法2.2 客户端管理相关方法2.3 授权码相关方法2.4 访问令牌相关方法2.5 刷新令牌相关方法 2.6 方法调用时序2.7 关键注意点3. MySQL存储实现原理3.1 数据库设计3.2 核心实现 4. OAuth 2.0授权码流程…

结合 Python 与 MySQL 构建你的 GenBI Agent_基于 MCP Server

写在前面 商业智能(BI)正在经历一场由大型语言模型(LLM)驱动的深刻变革。传统的 BI 工具通常需要用户学习复杂的界面或查询语言,而生成式商业智能 (Generative BI, GenBI) 则旨在让用户通过自然语言与数据交互,提出问题,并获得由 AI 生成的数据洞察、可视化建议甚至完整…

Linux中常用命令

目录 1. linux目录结构 2. linux基本命令操作 2.1 目录操作命令 2.2 文件操作命令 2.3 查看登录用户命名 2.4 文件内容查看命令 2.5 系统管理类命令 3. bash通配符 4. 压缩与解压缩命令 4.1 压缩和解压缩 4.2 测试网络连通性命令 ping 4.3 vi编辑器 4.4 管道操作(…

C++ 与 MySQL 数据库优化实战:破解性能瓶颈,提升应用效率

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家、CSDN平台优质创作者&#xff0c;高级开发工程师&#xff0c;数学专业&#xff0c;10年以上C/C, C#, Java等多种编程语言开发经验&#xff0c;拥有高级工程师证书&#xff1b;擅长C/C、C#等开发语言&#xff0c;熟悉Java常用开…

tcp特点+TCP的状态转换图+time_wait详解

tcp特点TCP的状态转换图time wait详解 目录 一、tcp特点解释 1.1 面向连接 1.1.1 连接建立——三次握手 1.1.2 连接释放——四次挥手 1.2 可靠的 1.2.1 应答确认 1.2.2 超时重传 1.2.3 乱序重排 1.2.4 去重 1.2.5 滑动窗口进行流量控制 1.3 流失服务&#xff08;字节…

探秘 Ruby 与 JavaScript:动态语言的多面风采

1 语法特性对比&#xff1a;简洁与灵活 1.1 Ruby 的语法优雅 Ruby 的语法设计旨在让代码读起来像自然语言一样流畅。它拥有简洁而富有表现力的语法结构&#xff0c;例如代码块、符号等。 以下是一个使用 Ruby 进行数组操作的简单示例&#xff1a; # 定义一个数组 numbers [1…

点评项目回顾

表结构 基于Session实现登录流程 发送验证码&#xff1a; 用户在提交手机号后&#xff0c;会校验手机号是否合法&#xff0c;如果不合法&#xff0c;则要求用户重新输入手机号 如果手机号合法&#xff0c;后台此时生成对应的验证码&#xff0c;同时将验证码进行保存&#xf…

OpenShift介绍,跟 Kubernetes ,Docker关系

1. OpenShift 简介 OpenShift是一个开源项目,基于主流的容器技术Docker及容器编排引擎Kubernetes构建。可以基于OpenShift构建属于自己的容器云平台。OpenShift的开源社区版本叫OpenShift Origin,现在叫OKD。 OpenShift 项目主页:https://www.okd.io/。OpenShift GitHub仓库…

Ubuntu服务器性能调优指南:从基础工具到系统稳定性提升

一、性能监控工具的三维应用 1.1 监控矩阵构建 通过组合工具搭建立体监控体系&#xff1a; # 实时进程监控 htop --sort-keyPERCENT_CPU# 存储性能采集 iostat -dx 2# 内存分析组合拳 vmstat -SM 1 | awk NR>2 {print "Active:"$5"MB Swpd:"$3"…

计算机视觉——基于MediaPipe实现人体姿态估计与不良动作检测

概述 正确的身体姿势是个人整体健康的关键。然而&#xff0c;保持正确的身体姿势可能会很困难&#xff0c;因为我们常常会忘记。本博客文章将逐步指导您构建一个解决方案。最近&#xff0c;我们使用 MediaPipe POSE 进行身体姿势检测&#xff0c;效果非常好&#xff01; 一、…

LSTM结合LightGBM高纬时序预测

1. LSTM 时间序列预测 LSTM 是 RNN&#xff08;Recurrent Neural Network&#xff09;的一种变体&#xff0c;它解决了普通 RNN 训练时的梯度消失和梯度爆炸问题&#xff0c;适用于长期依赖的时间序列建模。 LSTM 结构 LSTM 由 输入门&#xff08;Input Gate&#xff09;、遗…

六、adb通过Wifi连接

背景 收集是荣耀X40,数据线原装全新的&#xff0c;USB连上之后&#xff0c;老是断&#xff0c;电脑一直叮咚叮咚的响个不停&#xff0c;试试WIFI 连接是否稳定&#xff0c;需要手机和电脑用相同的WIFI. 连接 1.通过 USB 连接手机和电脑(打开USB调试等这些都略过) adb device…

如何理解前端开发中的“换皮“

"换皮"在前端开发中是一个常见的术语&#xff0c;通常指的是在不改变网站或应用核心功能和结构的情况下&#xff0c;只改变其外观和视觉表现。以下是关于前端"换皮"的详细理解&#xff1a; 基本概念 定义&#xff1a;换皮(Skinning)是指保持应用程序功能不…

从 Vue 到 React:深入理解 useState 的异步更新

目录 从 Vue 到 React&#xff1a;深入理解 useState 的异步更新与函数式写法1. Vue 的响应式回顾&#xff1a;每次赋值立即生效2. React 的状态更新是异步且批量的原因解析 3. 函数式更新&#xff1a;唯一的正确写法4. 对比 Vue vs React 状态更新5. React useState 的核心源码…

使用Redis实现分布式限流

一、限流场景与算法选择 1.1 为什么需要分布式限流 在高并发系统中&#xff0c;API接口的突发流量可能导致服务雪崩。传统的单机限流方案在分布式环境下存在局限&#xff0c;需要借助Redis等中间件实现集群级流量控制。 1.2 令牌桶算法优势 允许突发流量&#xff1a;稳定速…

快速搭建WordPress网站的主题

WP快主题(wpkuai.com )是一款由知名WordPress专业团队打造的专业化WordPress主题&#xff0c;旨在让用户使用该wordpress主题快速搭建网站。 WP快主题专注于快速搭建WordPress网站的主题解决方案。其主题设计注重简洁性与高效性&#xff0c;旨在帮助用户快速完成网站的搭建和部…

STM32江科大----------PID算法

声明&#xff1a;本人跟随b站江科大学习&#xff0c;本文章是观看完视频后的一些个人总结和经验分享&#xff0c;也同时为了方便日后的复习&#xff0c;如果有错误请各位大佬指出&#xff0c;如果对你有帮助可以点个赞小小鼓励一下&#xff0c;本文章建议配合原视频使用❤️ 如…

将JSON格式的SQL查询转换为完整SQL语句的实战解析

一、背景与需求 在现代数据处理中,JSON格式因其灵活性和可读性,常被用于定义SQL查询的结构。然而,直接编写JSON格式的SQL指令后,如何将其转换为可执行的SQL语句是开发者常遇到的挑战。本文将通过一个Python函数和多个实际案例,解析如何将JSON结构转换为完整的SQL语句,并…

java CountDownLatch用法简介

CountDownLatch倒计数锁存器 CountDownLatch&#xff1a;用于协同控制一个或多个线程等待在其他线程中执行的一组操作完成&#xff0c;然后再继续执行 CountDownLatch用法 构造方法&#xff1a;CountDownLatch(int count)&#xff0c;count指定等待的条件数&#xff08;任务…

Leetcode - 双周赛135

目录 一、3512. 使数组和能被 K 整除的最少操作次数二、3513. 不同 XOR 三元组的数目 I三、3514. 不同 XOR 三元组的数目 II四、3515. 带权树中的最短路径 一、3512. 使数组和能被 K 整除的最少操作次数 题目链接 本题实际上求的就是数组 nums 和的余数&#xff0c;代码如下&…