重载:在同一作用域中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载,重载不关心函数返回类型
class A{
public:
void test(int i);
void test(double i);//overload
void test(int i, double j);//overload
void test(double i, int j);//overload
int test(int i); //错误,非重载。注意重载不关心函数返回类型。
};
隐藏:指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
#include
using namespace std;
class Base
{
public:
void fun(double ,int ){ cout << "Base::fun(double ,int )" << endl; }
};
class Derive : public Base
{
public:
void fun(int ){ cout << "Derive::fun(int )" << endl; }
};
int main()
{
Derive pd;
pd.fun(1);//Derive::fun(int )
pb.fun(0.01, 1);//error C2660: “Derive::fun”: 函数不接受 2 个参数
system("pause");
return 0;
}
重写:是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
#include
using namespace std;
class Base
{
public:
virtual void fun(int i){ cout << "Base::fun(int) : " << i << endl;}
};
class Derived : public Base
{
public:
virtual void fun(int i){ cout << "Derived::fun(int) : " << i << endl;}
};
int main()
{
Base b;
Base * pb = new Derived();
pb->fun(3);//Derived::fun(int)
system("pause");
return 0;
}
多态:C++多态(polymorphism)是通过虚函数来实现的,虚函数允许子类重新定义成员函数。最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,实现一个接口,多种方法,称为多态
协程,又称微线程,纤程。协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
子程序在函数中是层级调用,通过栈实现的,调用顺序明确,但是协程的调用顺序和栈不同,协程看上去也是子程序,但是在内部可中断,转而去执行其他程序,在适当的时候再回来接着执行。
但是在其中是没有调用的过程的,所以理解起来要难些;
优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多
1.修饰普通局部变量,变量存储在静态区,在函数调用的时候只初始化一次,而且延长了局部变量的生命周期,直到程序运行结束再释放
2.修饰普通全局变量,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以
3.修饰函数,该函数只能再本文件调用,不能在其他文件调用, static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
4.静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存
5.修饰成员函数使得不需要生成对象就可以访问函数,但是在static函数内不能访问非静态成员
c++内存泄露的定义:
程序猿在申请了内存后(malloc(), new
),没有及时释放没用的内存空间,甚至消灭了指针导致该区域内存空间根本无法释放。
内存泄漏的应对:不用了的内存空间记得释放,不释放留着过年哇!
内存泄漏可能导致的后果:
根据内存泄露的原因及其恶劣的后果,我们可以通过其主要表现来发现程序是否存在内存泄漏:程序长时间运行后内存占用率一直不断的缓慢的上升,而实际上在你的逻辑中并没有这么多的内存需求
如何定位:
1.review自己的代码,查找new和delete,看看内存的申请和释放是不是成对出现的
2,如果依旧发生内存泄漏,可以记录申请和释放的对象的数目是不是一直的来判断;在类中追加一个静态变量static int count;在构造函数中执行count++;在析构函数中执行count--;执行完之后判断count的值是不是为0,如果为0,则问题并非出现在在此处,如果不为0,则是该类型对象没有完全释放
3.检查类中,申请的空间是否完全释放,尤其是存在继承父类的情况,看看子类中是否调用了父类的析构函数,有可能是因为子类析构时没有析构父类内存空间
4.对于函数中申请的临时变量,认真检查,是否存在提前跳出但是没有释放内存
智能指针:
unique_ptr |
C++ 11 | 拥有独有对象所有权语义的智能指针 |
shared_ptr |
C++ 11 | 拥有共享对象所有权语义的智能指针 |
weak_ptr |
C++ 11 | 到 std::shared_ptr 所管理对象的弱引用 |
auto_ptr |
C++ 17中移除 | 拥有严格对象所有权语义的智能指针 |
auto_ptr
已经在 C++ 17 中移除
unique_ptr:唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放
weak_ptr:weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况
shared_ptr:多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁
在空类中,c++不会生成任何的成员函数,只会生成一字节的占位符
有时可能会以为编译器会为空类生成默认构造函数等,事实上是不会的,编译器只会在需要的时候生成6个成员函数:
比如 A a;
class A{};
编译器处理后,就相当于:
class A
{
public:
A(); //默认构造函数
A(const A&); //拷贝构造函数
~A(); //析构函数
A& operator=(const A& rhs);
A* operator&(); //取地址运算符(非const)
const A* operator&() const; // 取地址运算符(const)
};
一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。
连续分配方式,是指为一个用户程序分配一个连续的内存空间。它主要包括单一连续分配、固定分区分配和动态分区分配。
单一连续分配是指:
内存在此方式下,分为系统区和用户区,系统区仅提供给用户使用,通常在低地址部分,用户区是为用户提供的、除系统区之外的内存空间。这种方式无需进行内存保护。
这种方式的优点是简单、无外部碎片,可以釆用覆盖技术,不需要额外的技术支持。缺点是只能用于单用户、单任务的操作系统中,有内部碎片,存储器的利用率极低。
内部碎片:
固定分区是可用于多道程序设计最简单的存储分配,无外部碎片,但不能实现多进程共享一个主存区,所以存储空间利用率低。固定分区分配很少用于现在通用的操作系统中,但在某些用于控制多个相同对象的控制系统中仍发挥着一定的作用。
外部碎片:
动态分区分配又称为可变分区分配,是一种动态划分内存的分区方法。这种分区方法不预先将内存划分,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统中分区的大小和数目是可变的。
动态分区在开始分配时是很好的,但是之后会导致内存中出现许多小的内存块。随着时间的推移,内存中会产生越来越多的碎片(图3-6中最后的4MB和中间的6MB,且随着进程的换入/换出,很可能会出现更多更小的内存块),内存的利用率随之下降。这些小的内存块称为外部碎片
两个类型一样:
#include
using namespace std;
template
T fun(T &a, T &b) {
if (a > b) {
return a;
}
return b;
}
int main() {
int a = 2, b = 3;
cout << fun(a, b) << endl;
float a1 = 2.5, b1 = 3.6;
cout << fun(a1, b1) << endl;
system("pause");
return 0;
}
递归:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==NULL||head->next==NULL)return head;
ListNode* temp = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return temp;
}
};
非递归:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==NULL||head->next==NULL)return head;
ListNode *pre=head,*cur=head->next;
ListNode* temp;
while(cur!=NULL){
if(pre==head){
pre->next=NULL;
}
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};
为什么要进行内存对齐?
现代计算机体系中CPU按照双字、字、字节访问存储内存,并通过总线进行传输,若未经一定规则的对齐,CPU的访址操作与总线的传输操作将会异常的复杂,所以现代编译器中都会对内存进行自动的对齐。
尽管内存是以字节为单位,但是大部分处理器并不是按照字节块来存取内存的四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度.
内存对齐规则:
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。
有效对其值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位。
了解了上面的概念后,我们现在可以来看看内存对齐需要遵循的规则:
(1) 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
(3) 结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
用法上来说都是在堆上申请内存的,但是c++的new和delete解决了malloc的众多问题。
1.new/delete是c++操作符,而malloc和free是c中的库函数
2.mallo只负责开辟内存,没有初始化功能,需要自己初始化;new 不但开辟内存,还可以进行初始化,例如 new int(4)表示开辟了4字节的内存,初始值是4,而new int[10]表示开辟了10字节的内存,初始值是0;
3.malloc开辟内存的内存的时候必须计算开辟内存的大小, 并且返回值是void* 所以需要强转成指定类型的地址;例如
int *p = (int *)malloc(sizeof(int)*100);
表示在对上开辟400字节的内存,new开辟内存会根据类型信息自行计算,开辟内存需要指定类型,返回指定类型的地址,因此不需要进行强转。例如:
int *p = new int[100]();
//开辟400字节的内存,100的整形数组,元素初始化为0
4new开辟失败返回nul,malloc开辟失败返回bad_alloc异常,需要捕获异常才能判断是否开辟成功或者失败。.new运算符其实是operator new函数的调用,它底层调用的也是malloc来开辟内存的,new它比malloc多的就是初始化功能,对于类类型来说,所谓初始化,就是调用相应的构造函数。
5.malloc开辟的内存一直都是通过free释放的,但是new单个元素内存,用的是delete,如果new[]数组,用的是delete[]来释放内存的
extern:
程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。
在函数内定义的变量是局部变量。
而在函数之外定义的变量则称为外部变量,外部变量也就是我们所讲的全局变量。它的存储方式为静态存储,其生存周期为整个程序的生存周期。
全局变量可以为本文件中的其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。
1.如果不在文件的开头定义,有效的作用范围将只限定定义处到文件结束;
如果全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
#include
int max(int x,int y);
int main(void)
{
int result;
/*外部变量声明*/
extern int g_X;
extern int g_Y;
result = max(g_X,g_Y);
printf("the max value is %d\n",result);
return 0;
}
/*定义两个全局变量*/
int g_X = 10;
int g_Y = 20;
int max(int x, int y)
{
return (x>y ? x : y);
}
全局变量 g_X 与 g_Y 是在 main 函数之后声明的,因此它的作用范围不在 main 函数中。如果我们需要在 main 函数中调用它们,就必须使用 extern 来对变量 g_X 与 g_Y 作“外部变量声明”,以扩展全局变量的作用域。也就是说,如果在变量定义之前要使用该变量,则应在使用之前加 extern 声明变量,使作用域扩展到从声明开始到本文件结束。
2.如果有多个文件想要引用其他的关键字那么只需要在引用的文件夹中加一个exern声明即可
/****max.c****/
#include
/*外部变量声明*/
extern int g_X ;
extern int g_Y ;
int max()
{
return (g_X > g_Y ? g_X : g_Y);
}
/***main.c****/
#include
/*定义两个全局变量*/
int g_X=10;
int g_Y=20;
int max();
int main(void)
{
int result;
result = max();
printf("the max value is %d\n",result);
return 0;
}
extern 'C':
C++虽然兼容C,但C++文件中函数编译后生成的符号与C语言生成的不同。因为C++支持函数重载,C++函数编译后生成的符号带有函数参数类型的信息,而C则没有
例如int add(int a, int b)
函数经过C++编译器生成.o文件后,add
会变成形如add_int_int
之类的, 而C的话则会是形如_add
, 就是说:相同的函数,在C和C++中,编译后生成的符号不同。
这就导致一个问题:如果C++中使用C语言实现的函数,在编译链接的时候,会出错,提示找不到对应的符号。此时extern "C"
就起作用了:告诉链接器去寻找_add
这类的C语言符号,而不是经过C++修饰的符号
c++调用c函数:
引用C的头文件时,需要加extern "C"
例子:
//add.h
#ifndef ADD_H
#define ADD_H
int add(int x,int y);
#endif
//add.c
#include "add.h"
int add(int x,int y) {
return x+y;
}
//add.cpp
#include
#include "add.h"
using namespace std;
int main() {
add(2,3);
return 0;
}
利用c++编译的时候进行extern C:
#include
using namespace std;
extern "C" {
#include "add.h"
}
int main() {
add(2,3);
return 0;
}
extern "C"
在C中是语法错误,需要放在C++头文件中。
虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。
这将存在两个问题:
其一,浪费存储空间;
第二,存在二义性问题;
通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性;
虚继承可以解决多种继承前面提到的两个问题:
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
在一个大的文件工程中,可能有多个文件同时包含同一个头文件,当这些文件编译链接成一个可执行文件的时候,就会大量出现重定义的错误,在头文件中实用#ifndef #define #endif能避免头文件的重定义
方法:例如要编写头文件test.h
在头文件开头写上两行:
#ifndef _TEST_H
#define _TEST_H//一般是文件名的大写
头文件结尾写上一行:#endif这样一个工程文件里同时包含两个test.h时,就不会出现重定义的错误了。
分析:当第一次包含test.h时,由于没有定义_TEST_H,条件为真,这样就会包含(执行)#ifndef _TEST_H和#endif之间的代码,当第二次包含test.h时前面一次已经定义了_TEST_H,条件为假,#ifndef _TEST_H和#endif之间的代码也就不会再次被包含,这样就避免了重定义了。主要用于防止重复定义宏和重复包含头文件
概念:
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
原因:
条件:
守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性的执行某种任务或等待某些发生的事件,他不需要用户输入就能运行并且提供某种服务,不是对整个系统就是对整个用户程序提供服务,linux系统的大多数服务器,就是通过守护进程实现的,常用的守护进程包括 系统日志syslogd、web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理
僵尸进程:
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
孤儿进程是没有父进程的进程,孤儿进程这个回收任务就落到了init进程身上,init进程就是所有进程的根进程,有点类似于二叉树的根节点。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程结束了其生命周期的时候,init0处理孤儿进程。因此孤儿进程并不会有什么危害。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理
僵尸进程的危害:
例如有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能处理了。
如果析构函数不被设置成虚函数,在删除指向派生类的基类指针的时候,就会只调用基类的析构函数而调用派生类的析构函数,造成对象析构不完全
虚函数的调用需要虚函数表指针,该指针存放在对象的内存空间中,如果构造函数声明为虚函数,那么么对象还未创建,没有虚函数表地址,更没有虚函数表指针来调用虚函数即构造函数了
如果类中的对象在堆中开辟的,并且该指针并没有进行delete操作,那么就需要手写析构函数
写的申请堆区的内存,要释放干净
系统生成的默认析构函数只会释放对象本身所占据的内存,对象通zhi过其他方式如动态内存分配(new)和打开文件等方式获得的内存和系统资源是不会被释放的。
如果你自定义了一个,系统就不会生成默认析构函数,而采用你定义的这个
将key通过一个固定的算法函数将所谓的哈希函数转换成一个数字,然后将该数字对数组长度取余,取余结果就当做数组的下标,将value存放在以该数字为下表的数组空间中,使用哈希表进行查询的时候,就是再次使用哈希表将key转换成下标,并定位到该空间获取value
当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时。冲突就难免会发生。另外,当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新记录,不仅发生冲突,而且还会发生溢出。因此,处理冲突和溢出是哈希技术中的两个重要问题。一般有开放地址法、链地址法
(
查找速度,数据量, 内存使用,可扩展性,有序性。
hash查找速度会比RB树快,而且查找速度基本和数据量大小无关,属于常数级别;而RB树的查找速度是log(n)级别。并不一定常数就比log(n) 小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash。但若你对内存使用特别严格, 希望程序尽可能少消耗内存,那么一定要小心,hash可能会让你陷入尴尬,特别是当你的hash对象特别多时,你就更无法控制了,而且 hash的构造速度较慢。
红黑树并不适应所有应用树的领域。如果数据基本上是静态的,那么让他们待在他们能够插入,并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,例如,做一个哈希表,性能可能会更好一些。
在实际的系统中,例如,需要使用动态规则的防火墙系统,使用红黑树而不是散列表被实践证明具有更好的伸缩性。Linux内核在管理vm_area_struct时就是采用了红黑树来维护内存块的。
红黑树是有序的,Hash是无序的,根据需求来选择。
拿红黑树实现的Map和Hash实现的HashMap相比:
如果只需要判断Map中某个值是否存在之类的操作,当然是Hash实现的要更加高效。
如果是需要将两个Map求并集交集差集等大量比较操作,就是红黑树实现的Map更加高效。
)
对比:
1.速度对比
物联网特别数百万设备或者用户联网,对高并发要求很大,哈希查找速度会比红黑树快,而且查找速度和数据量无关,属于常数级别,RB树的查找速度是lgn级别
红黑树查找和删除的时间复杂度都是O(log n),hash查找和删除的是复杂度都是O(1),如果红黑树的深度深如小于8,采用的是数字查找,两者性能没有多大的差异。
并非所有的场景 哈希都比红黑树快, linux高并发EPOLL模式时间管理使用的就是红黑树
2.数据预知
静态数据,基本可以预知大小,用哈希,如URL地址
动态数据,如统计IP地址、任务调度、epoll高并发管理无法判断数量多少,用红黑树更佳,如果知道设备IP在一定范围内,如只有几千,完全可以用哈希;
3.内存消耗
对内存要求严格的地方,如嵌入式系统,用红黑树,红黑树占用的内存更小,而哈希事先就应该分配足够的内存存储散列表,浪费内存;
对内存消耗无所谓的地方,如服务器有巨大的内存,用哈希。哈希最大的缺点是内存分配得小,可能元素就会冲突,冲突的元素大于8个成链表,效率还不如红黑树。 Java 的hashmap就是把哈希和红黑树结合在以前的。当同一个hash值的节点数不小于8时,不再采用单链表形式存储,而是采用红黑树。
红黑树是有序的,哈希是无序的,根据项目需求来选择,阿里巴巴的很多项目用红黑树更多,笔者认为主要还是和内存有关,
如果内存要求苛刻的项目,就用红黑树;
如果内存足够大,牺牲内存换取更快的速度,哈希完全适合。
Hiihttps开源waf大量采用哈希算法,可能和速度并发要求有关。总之,数据结构是网络安全最基础的学科。
红黑树:
如统计IP地址、epoll高并发管理无法判断数量多少,用红黑树更佳;
哈希表:
哈希表适用于那种查找性能要求高,数据元素之间无逻辑关系要求的情况
1.布隆过滤器(海量的URL去重)
2.hash 表在海量数据处理中有着广泛的应用,
请看另一道百度面试题:
题目:海量日志数据,提取出某日访问百度次数最多的那个IP。
方案:IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。
3.目前最流行的NoSql数据库之一Redis,整体的使用了哈希表思想
*******************************************************************************************************************************************************
客户端请求tomcat打到服务器上,请求量特别大的时,数据库扛不住,所以有了缓存中间件;
如果redis这一层,很多热点数据会直接打到mysql中,倒置服务器崩溃的情况,所以有了redis的存在
缓存穿透:
数据库的ID主键自增:1-N这样的主键,如果从-1请求,请求到redis中,redis肯定没有,因为redis是数据库的一个子集,数据库中请求,也没有,这样一个请求连穿两层到数据库,返回。
解决办法:中间弄一份过滤器;
过滤器怎么设计:
布隆过滤器:
误差率:他能判断这个索引一定不在里边,但是不能判断这个东西一定在里边
使用之前提前预估值的大小和能接受的误差率
布隆过滤器的设计:
三次哈希(三次哈希的函数不一样)之后,对应具体的位置设置成1,再次三次哈希取出值,看一下三个部分相乘是不是等于1
缓存击穿:一个存在的key是一个库存有10万,现在有大量的请求,如果这个key突然失效了,所有的请求就会打到mysql中,对库存进行一个扣减,这时候布隆过滤器就没用了,大量的请求去请求去数据库
Redis也有可能挂,在放一些第三方的缓存,击穿可以理解为大家一直在做请求,把他击穿掉了
防止击穿,加一些限流组件,本地缓存挂了,限定每秒的QPS
缓存雪崩:大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
**********************************************************************************************************************************************************
当大量的元素都映射到同一个桶的时候,这个桶下边就有一条长长的链表,这个之后哈希表就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去他的优势;