2021-04-20

目录

八股文

C++

C++基础

C++标准库

类设计者工具

网络

TCP三次握手

TCP四次挥手

TimeWait状态

TCP的一些机制和问题

HTTPS

SOCKET

EPOLL

EPOLL原理

操作系统

CPU的执行方式

进程

线程

协程

Linux进程管理

Linux进程调度:

系统调用

Linux内存管理

Linux的各种同步异步机制

内核定时器

Linux操作

mysql

索引

事务

性能

redis

redis数据类型

redis持久化

redis哈希

redis缓存

redis集群


八股文

C++

C++基础

const用法:

const int bufSize = 512;//修饰变量,限定它的值不能被改变
const int &r2 = 42;//常量引用
const double pi = 3.14;    const double *cptr = π//修饰指针,代表指向常量的指针
double dval = 3.14;    cptr = &dval;//指向常量的指针也可以用于指向非常量对象,但不能通过它来修改它所指向的对象的值
int errNumb = 0;    int *const curErr = &errNumb;//把*放在const关键字之前,代表常量指针。常量指针不能修改其所指向的对象,但并不意味着不能通过指针修改其对象的值,能否这样做取决于所指对象的类型
//修饰成员函数,该成员函数将不能调用非const成员函数,并且不能修改任何成员变量的值

顶层const:表示指针本身是一个常量

底层const::表示指针所指的对象是一个常量

显示转换

1.static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast

int i,j;
double slope = static_castdouble>(j)/i; //将j强制转换成浮点数进行运算
void *p = &slope;
double *dp = static_cast(p);//强制转换指针类型。但使用时必须确保转换后所得的类型就是指针所指的类型,类型不符将产生未定义的后果。

2.const_cast:只能改变运算对象的底层const

3.reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释

int *ip;
char *pc = reinterpret_cast<char *>(ip);//直接将之强制转换为char*
string str(pc);//发生错误,异常的运行时行为

4.dynamic_cast:使用形式如下,其type必须是一个类类型,并且通常情况下该类型应该含有虚函数

dynamic_cast(e) //指针类型的dynamic_cast 
/*
* 假设有一个指向base的指针bp
*/
Derived *dp = dynamic_cast(bp); //若bp指向Derived对象,该类型转换初始化dp并令其指向bp所指的Derived对象,否则类型转换的结果为0。若bp是空指针,则dp是所需类型的空指针。
dynamic_cast(e) //引用类型的dynamic_cast,e必须是左值
/*
* 由于不存空引用,所以当转换失败时,程序抛出一个std::bad_cast的异常,处理实例如下
*/
try{
    const Derived &d = dynamic_cast(b);
}catch(bad_cast){
}
dynamic_cast(e)//右值引用类型的dynamic_cast,e必须是右值

Static:存储在静态存储区,在程序刚开始运行的时候就已经初始化,并且只会初始化一次。与全局变量相比,static限制了其作用域。static全局变量也只能在该源文件中使用。

类的静态成员函数:静态成员函数不与任何对象绑定在一起,它们不包含this指针,且静态成员函数也不能声明为const。

类的静态成员:类的静态成员独立于任何对象,因此静态成员也可以是不完全类型

字节对齐:32位系统按4字节对齐,若4字节存不下这个数据,补齐。

//单例static模式实例。该实例线程不安全
class Bar{
    public:
        static Bar* GetBar()
        {
            if(mem1* == nullptr)
            {
                mem1 = new Bar(); 
            }
            return mem1;
        }
    private:
        Bar();
        static Bar* mem1;//此时Bar是不完全类型,但该定义正确
};

C++标准库

顺序容器

vector:可变大小数组,支持快速随机访问。元素保存在连续的内存空间中。在尾部之外的位置插入删除元素都可能很慢

deque:双端队列。内部实现使用分段数组。

list:双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快

forward_list:单向链表。只支持单向顺序访问。

array:固定大小的数组,支持快速随机访问。不能添加或删除元素

string:与vector相似的容器,但专门用于保存字符。随机访问快,在尾部插入/删除的速度快

容器适配器

stack:基于deque实现的,实现了后进先出

queue:基于deque实现的,实现了先进先出

priority_queue:基于vector之上实现的。大根堆。新加入的元素会排在所有优先级比它低的已有元素之前。

关联容器

有序集合

map:关联数组;保存关键字-值对。内部实现使用红黑树

set:关键字即值,即只保存关键字的容器

multimap:关键字可重复出现的map

multiset:关键字可重复出现的set

无序集合

unordered_map:以哈希函数组织的map

unordered_set:以哈希函数组织的

unordered_multimap:哈希组织的map;关键字可以重复出现

unordered_multiset:哈希组织的set;关键字可以重复出现

标准库实例

LRU算法,就是拿个链表,如果有访问,就把那个节点移到最前面。如果新节点加入,就把链表最后边的节点删掉。map就用来存节点在链表的哪个位置。

/**
* list,hash map实现LRU示例程序
*/
class Solution {
public:
    /**
     * lru design
     * @param operators int整型vector> the ops
     * @param k int整型 the k
     * @return int整型vector
     */
    vector LRU(vector >& operators, int k) {
        // write code here
        _list.clear();
        _map.clear();
        sk = k;
        vector _ans;
        for(const auto &tmp:operators)
        {
            if(tmp[0] == 1) //插入元素
            {
                set(tmp[1],tmp[2]);
            }
            else if(tmp[0] == 2) //查询元素
            {
                _ans.push_back(get(tmp[1]));
            }
        }
        return _ans;
    }
    int get(int key)
    {
        auto it = _map.find(key);
        if(it == _map.end())
        {
            return -1;
        }
        int value = it->second->second;
        _list.push_front(make_pair(key,value));
        _list.erase(it->second);
        _map[key] = _list.begin();
        return value;
    }
    void set(int key,int value)
    {
        auto it =_map.find(key);
        if(it == _map.end())
        {
            _list.push_front(make_pair(key, value));
            _map[key] = _list.begin();
        }
        else
        {
            _list.push_front(make_pair(key, value));
            _list.erase(it->second);
            _map[key] = _list.begin();
        }
        if(_list.size()>sk)
        {
            _map.erase(_list.back().first);
            _list.pop_back();
        }
    }
    list >  _list;
    unordered_map >::iterator> _map;//存key对应在链表上的哪个位置
    int sk;
};

动态内存与智能指针

shared_ptr:每当shared_ptr拷贝时,其引用计数都会递增。每当shared_ptr销毁时其引用计数都会递减。当引用计数降为0,它会销毁其指向的对象,并释放内存。使用时需要注意循环引用问题。

unique_ptr:独占它所指向的对象。某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr销毁时,它会销毁指向的对象。

weak_ptr:它指向一个由shared_ptr管理的对象,但对weak_ptr的创建和销毁不会改变shared_ptr的引用计数。

new:一次分配一个对象,或用动态数组的方式分配一组对象,与delete配对

delete:一次释放一个用new分配的对象,或用动态数组的方式分配一组对象,与new配对

allocator:相比于new它将内存分配和对象构造组合在了一起,delete将对象析构和内存释放组合在了一起。而使用allocator,可以将分配内存与初始化对象操作分开来做

//示例,n不确定时,使用new还是需要先构造好n个对象
string *const p = new string[n];
string* q=p;
while(cin>>s&&q!=p+n)
    *q++ = s;
delete[] p;//若delete时没有[],编译不会出错,而是会在没有任何警告的情况下产生行为异常
//allocator分配示例
allocator alloc;//可以分配string的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string
auto q = p;
alloc.construct(q++);//调用构造
while(q!=p)
    alloc.destory(--q);//调用析构
alloc.deallocate(p,n);//释放由alloc分配的内存

类设计者工具

继承:保持原有的基础上进行扩展

封装:隐藏对象属性和实现细节,仅对外公开接口和对象进行交互

多态:父类的指针或引用指向子类时,通过它能够调用到子类的函数,而非父类的函数

拷贝构造函数:一种构造函数,将新对象初始化为同类型另一对象的副本。当向函数传递对象,或以传值方式从函数返回对象时,会隐式使用拷贝构造函数。如果未提供拷贝构造函数,编译器会自己合成一个。

深拷贝和浅拷贝:若不主动编写拷贝构造函数和赋值函数,编译器将以”位拷贝“的方式自动生成缺省的函数。倘若类中含有指针变量,以这种方式将产生三个错误:

//拷贝构造函数生成的两个string对象
String* m_str;
b.m_str = a.m_str;
//产生了三个错误。1.b.m_str原有的内存没有释放 2.a.m_str和b.m_str指向同一片内存,会互相影响 3.析构函数时必崩,因为其中一个必然会删除一片已经被删除的内存

其解决办法就是手动重写拷贝构造函数,改为深拷贝

//假如这个类名叫Base
Base::Base(const Base& a)//拷贝构造函数必为const &,原因很简单
{
    if(a.m_str)
    {
        m_str = new string(*a.m_str);//直接new出来就是深拷贝
    }
}

右值引用:指向一个将要销毁的对象的引用。通过&&来获得右值引用。可用于移动对象,减少拷贝次数。

移动构造函数:一种构造函数,接受一个本类型的右值引用。通常,移动构造函数将数据从其参数移动到新创建的对象中。移动之后,对给定的实参执行析构函数必须是安全的。如果未提供移动构造函数,且一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。

虚函数:当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。一旦某个函数被声明为虚函数,则在他的所有派生类中,它都是虚函数。C++的实现原理是,每个类有一个vptr,虚表指针,指向虚表。继承时,子类函数会覆盖父类的虚函数。当发生虚函数调用时,其会根据偏移量寻找对应的虚函数。

抽象类:含有纯虚函数的类是抽象类,它不能被实例化。

final:关键字final,final标识的类不能再被继承

访问控制级别:public,protect,private

友元:友元函数是定义在外部,但有权访问类私有成员的函数。友元类与之类似。声明时需要在类中放置相关声明。友元是不可继承的。

虚析构函数:基类的析构函数通常应该定义成一个虚函数。否则在delete一个指向派生类对象的基类指针时将产生未定义的行为。

多重继承:是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性。

菱形继承:多重继承的情况下,相同的查找过程在所有直接基类中进行。如果名字在多个基类中都被找到,则对该名字的使用将具有二义性。此时就发生了菱形继承

虚继承:令某个类做出声明,承诺共享它的基类,这样就避免了菱形继承的二义性。C++的实现方法是,在虚继承的时候加入虚基类指针vbptr,位置大概在虚表末尾+4的位置。根据虚基类指针可以找到相应的成员变量,而不必向普通继承那样维持公共基类的相同的拷贝。

私有继承:父类的公有成员和私有成员都被降级为private,子类内部可以访问,但外部不能访问

lib库使用虚函数的二进制不兼容问题:由于虚函数是根据偏移量来寻找对应函数的,所以假如在lib库中有虚函数的类中修改,一旦改变了偏移量,就意味着会发生二进制不兼容问题,就要都重新编译。所以lib库尽量不能使用虚函数。

网络

TCP三次握手

A->B SYN

B->A SYN,ACK

A->B ACK

主要约定了通信双方的初始序列号,准备了双方的缓冲区。回答为什么是三次,可以从可靠连接,全双工方面答。

TCP四次挥手

A->B FIN

B->A ACK

B->A FIN

A->B ACK

这些都是TCP的flag字段,包括 SYN, FIN, ACK, PSH, RST, URG。为什么是四次挥手,从双工的角度考虑,A->B FIN,代表A不会再给B发送数据了,B可能还有要给A的数据。所以要等B处理完数据后,再B->A FIN。

TimeWait状态

在A收到B的FIN后,A会回复B ACK,并且A会进入进入TimeWait状态。这是为了避免回传的ACK在网络上丢失的情况。TimeWait状态若ACK丢失则会重传。若没有丢失,则通常经过两倍报文段寿命后,连接关闭。

为什么要有TimeWait状态呢?其一是因为若B没有收到A的ACK,会触发B的重传FIN。若此时的A已经关闭,则B的连接不能正常关闭。其二是为了让老的报文能够在网络中自动消失。两倍报文段的等待时间,足以让网络上的报文因超时而消失,从而在建立新的连接时,新的连接不会受老报文的影响。

其状态可以参看TCP的有限状态机(图源:Linux高性能服务器编程)

TCP的一些机制和问题

1.有序传输:发送方在数据分包时分配一个序列号并在一段时间内等待接收方对这个序列号进行确认。若一段时间内没有收到确认则重传数据包。接收方会根据序列号顺序化数据,再交付给上层。

2.可靠传输:发送方发送数据后会将已发送的数据放入缓冲区,并在一段时间内等待接收方的应答。若收到应答则释放该数据包占用的缓冲区。若一段时间没有收到应答则重传数据包,直到到达数据包重传的最大次数位置。接收方收到数据包会进行校验,表面该数据已经收到。

3.拥塞控制:TCP模块还有一个重要的任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。这就是所谓的拥塞控制。拥塞控制的四个部分:慢启动、拥塞避免、快速重传、快速恢复。在Linux下有多种实现,比如reno、vegas、和cublic算法,他们或者部分或者全部实现了上述四个部分。拥塞控制的最终受控变量是发送端向网络一次连续写入的数据量,我们称为SWND(SendWindow 发送窗口)。接收方可以通过接收通告窗口RWND来控制发送端的SWND,但这显然不够,发送端引入了一个称为拥塞窗口(CWND Congestion Window)的状态变量。

慢启动:初始拥塞窗口大小为IW(约为2~4个SMSS,发送者最大段大小),此后发送端每收到一个确认,CWND+=min(N,SMSS)。其中N是此次确认中包含的之前未被确认的字节数。这样一来,CWND将按照指数级扩大。这样的理由是,TCP模块刚开始发送数据时并不知道网络的实际情况,需要用一种试探的方式平滑地增加CWND的大小。慢启动门限ssthresh:当CWND的大小超过该值时,TCP拥塞控制将进入拥塞避免阶段

拥塞避免:拥塞避免算法使得CWND按照线性方式增加,从而减缓其扩大。有两种实现方式

-.每个RTT时间内按照CWND+=min(N,SMSS)计算新的CWND,而不论该RTT时间内发送端收到多少个确认

-.每收到一个对新数据的确认报文段,就按照CWND+=SMSS*SMSS/SWND来更新

主动控制拥塞的策略:慢启动拥塞避免都是发送端未检测到拥塞时采用的积极避免拥塞的方法。除此之外还有拥塞发生时拥塞控制的行为。首先发送方判断拥塞发生的依据有如下两个:

-.传输超时,或者说TCP重传定时器溢出

-.接收到重复确认的报文段

对于情况一仍然使用慢启动和拥塞避免,它将执行重传并调整sstresh=max(FlightSIze/2,2*SMSS),

CWMD<=SMSS;其中FlightSIze是已经发送但未收到确认的字节数。这样调整之后,CWMD将小于SMSS,那么也必然小于新的慢启动门限值ssthresh,故而拥塞控制再次进入慢启动阶段

对于情况二则使用快速重传和快速恢复

快速重传和快速恢复:发送端如果连续收到三个重复的确认报文段,就认为是拥塞发生了。然后它启动快速重传和快速恢复算法来处理拥塞

-.当收到第三个重复的确认报文段时,计算sstresh=max(FlightSIze/2,2*SMSS),并设置CWND=ssthresh+3*SMSS

-.每次收到1个重复的确认,设置CWND=CWND+SMSS,此时发送端可以发送新的TCP报文段

-.当收到新数据确认时,设置CWND=ssthreash

即快速重传和快速恢复完成后,拥塞控制将回到拥塞避免阶段

4.粘包问题:首先,接收方收到的数据都会放到缓冲区,由应用程序主动读缓冲区收到的分组。只要接收方读的速度小于发送速度,缓冲区就可能包含多个分组。那么一次读取就会读取到多个分组的数据。在应用层对这个问题有两种解决方案:1.发送数据时,将数据长度一并发送,接收方就能根据长度确定分组。2.每条数据有固定的开始符和结束符。

5.keepalive:tcp可以开启keepalive选项。开启的一方会发送保活探测报文。keepalive选项默认关闭。一般会做在逻辑层。

6.SYN-flood攻击 典型的dos攻击,相当于大量的A三次握手时只发SYN,不发第三次握手的ACK,使得B有大量的连接处于半开闭状态(SYN_RCVD),在这个状态下,B收不到A的ACK会一直重试直到次数达到上限。占用B的系统资源。linux解决方法:

提高TCP半开连接队列大小的上限:

/proc/sys/net/ipv4/tcp_max_syn_backlog

可以减少半开状态下等待ACK消息的时间或者重试发送SYN-ACK消息的次数:

/proc/sys/net/ipv4/tcp_synack_retries

还可以尝试开启TCP选项SO_REUSEADDR,使得TimeWait状态的TCP端口依然可以重复使用

7.流量控制:B告诉A的缓冲区空闲的大小,A会根据这个大小调整发送窗口的大小。

8.快速重传:快速重传算法可以概况为,一旦发送方收到了三个重复ACK后,即重传可能丢失的数据分组,而不用等待该部分超时。比如A->B 01234 B收到的顺序是02341,那么,B回给A的ACK的序列号将是ACK(1)ACK(1)ACK(1)ACK(5) ,有三个重复ACK。此时A就会主动重传报文1。尽管报文1只是因为某些问题后到达了。

9.TCP粘包半包:由于TCP是流式传输,一次从缓冲区中读取的数据可能是多个包一起,也可能只有半个数据包。解决方案:1.应用层发包时,在包头表明数据包大小2.双方规定使用固定数据包大小不够就补零3.用数据流中不可能出现的特殊符号分割数据包

10.TCP选项:SO_RCVLOWAT 和 SO_SNDLOWAT:发送缓冲区和接收缓冲区的低水位标记。一般用于判断缓冲区是否可读/可写。SO_RCVLOWAT 和 SO_SNDLOWAT用于调整接收端和发送端的缓冲区大小。SO_LINGER选项,用于控制close时在关闭TCP连接时的行为。

与UDP的区别:TCP可靠传输,基于连接。UDP会丢包但机制简单,更快。

TCP抓包: tcpdump与Wireshark

HTTPS

A->B 建立连接

B->A 安全证书,公钥(*申请安全证书需要$)

A 产生对称密钥(随机)

A->B 公钥加密后的对称密钥

B 用私钥解密,得到A产生的对称密钥

A<->B 两者通过该对称密钥加密通信

HTTP keep-alive: HTTP1.0没有keep-alive机制,每一次请求,都会先重新建立tcp连接。HTTP1.1后可以设置请求头“connection: keep-alive”开启。开启后,服务端在一个timeout的时间内不会关闭连接。在一个timeout的时间内若又发起新的http请求,则可以复用之前的tcp连接,并重置timeout计数器。

HTTP 报文格式:请求行(Method,Request_URL,HTTP-Version),消息报头,请求正文

HTTP cookie: Cookie会根据从服务端发送的响应报文中的一个称set-Cookie的首部字段中,通知客户端保存Cookie。当客户端下次再往服务端发送请求的时候,客户端会自动在请求报文中加入Cookie值发送出去。服务端接收到Cookie后,会去检查究竟是从哪一个客户端发送过来的(主要是通过对比服务端的记录),最后得到之前的状态信息。

HTTP seesion:用来追踪每个会话,用session_id来标识,当用户发送请求的时候,服务器将用户cookie里面记录的session_id和服务器内存中存放的session_id进行比对,从而找到用户相对应的session进行操作。

HTTP和HTTPS的区别:HTTP是明文传输的,HTTPS是HTTP+SSL(TLS),是加密传输的

HTTP 方法:Get:获取资源。指定的资源经过服务器解析后返回的响应内容;POST:传输内容实体。虽然GET方法也可以用来传输内容实体。POST的主要目的并不是获取响应的主体内容;PUT:传输文件。要求在请求报文的实体中包含文件内容,然后保存到请求的URI指定的位置;

HTTP2.0:二进制分帧,首部压缩,多路复用,请求优先级,服务器推送

HTTP3:用QUIC+TLS取代了HTTPS的TCP+TLS,减少了握手次数。

HTTP错误码:2-X成功,3-X重定向,4-X请求错误,5-X服务器错误

HTTP408:StatusRequestTimeout 您的 Web 服务器认为,在 建立客户端和服务器之间 IP 连接(套解字 - socket ), 和通过该套解字收到数据,之间的时间间隔太长, 所以服务器放弃该连接。 套接字连接实际上已失效 - 您的 Web 服务器已就对该特定套接字连接发出 ' 超时 ' 信号。客户端的请求必须及时重复。简单来说,就是服务器此时的socket已经超时失效了,而客户端还没来得及发送http请求。

HTTP502:服务器(不一定是 Web 服务器)正在作为一个网关或代理来完成客户访问所需网址的请求。 为了完成该请求,此服务器访问一个上游服务器, 但收到无效响应。这通常并不意味着上游服务器已关闭(对网关 / 代理无响应), 而是上游服务器和网关 / 代理在交换数据的协议上不一致。 鉴于互联网协议是相当清楚的, 它往往意味着一个或这两个机器的编程都不正确或不完全。

HTTP504:服务器(不一定是 Web 服务器)正在作为一个网关或代理来完成客户访问所需网址的请求。 为了完成您的 HTTP 请求, 该服务器访问一个上游服务器, 但没得到及时的响应。这通常意味着上游服务器已关闭(不响应网关 / 代理),而不是上游服务器和网关 / 代理在交换数据的协议上不一致。

SOCKET

socket api: socket(),connect(),bind(),listen(),accept(),close(),recv(),send(),write(),read()。

accept:获取连接。调用该方法会阻塞,返回连接的时候三次握手已经完成。

socket在什么情况下可读:

1.接收缓冲区已经接收的字节数大于低水位标记

2.接收到了FIN报文,此时recv返回0

3.是一个用于监听的socket,此时连接数非0,调用accepte必不阻塞

4.有异常错误待处理,对它读取将返回-1。要通过errno进一步获取错误码

/*
* _domain 套接字使用的协议族信息
* _type 套接字的传输类型
* __protocol 通信协议
* @return socket fd
* */
int socket (int __domain, int __type, int __protocol) __THROW;
/**
* Prepare to accept connections on socket FD.
  N connection requests will be queued before further requests are refused.
  Returns 0 on success, -1 for errors.  
* @sock fd 
* @backlog 请求队列的最大长度(请求队列:没来得及accept()的连接会进入请求队列,当请求队列满会返回错误)
*/
int listen(int sock, int backlog); //Linux
int listen(SOCKET sock, int backlog); //Windows

/* 
* __fd:socket描述字,也就是socket引用
* myaddr:要绑定给sockfd的协议地址
* __len:地址的长度
* @return:错误码
*/
int bind (int __fd, const struct sockaddr* myaddr, socklen_t __len)  __THROW;

/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
   For connectionless socket types, just set the default address to send to
   and the only address from which to accept transmissions.
   Return 0 on success, -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
int connect (int socket, struct sockaddr* servaddr, socklen_t addrlen);

/* Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  
* __fd SOCKET fd
* *addr 连接上来的地址
* @return 有错-1,没错返回一个新的socket fd,代表一个新的连接。
*/
extern int accept (int __fd, struct sockaddr *addr, socklen_t *addr_len);

/**
* 默认阻塞
* @return <0出错 0关闭 >0读取数据的长度
* 当设置 MSG_DONTWAIT 或者套接字的描述符带有 O_NONBLOCK 选项,在没有数据情况下,应该返回的是 * -1,并设置errno为 EAGAIN 或者 EWOULDBLOCK
*/
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

EPOLL

epoll_create(),epoll_ctl(),epoll_wait()

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
/**
event有以下几种

EPOLLIN:                 //触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
EPOLLOUT:                //触发该事件,表示对应的文件描述符上可以写数据;
EPOLLPRI:                //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:                //表示对应的文件描述符发生错误;
EPOLLHUP:                //表示对应的文件描述符被挂断;
EPOLLET:                 //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:            //只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

op有以下几种
EPOLL_CTL_ADD:            //注册新的fd到epfd中;
EPOLL_CTL_MOD:            //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:            //从epfd中删除一个fd;
*/

typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
 
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

工作方式:先用epoll_create创建一个epoll的文件描述符,再用epoll_ctl把被监听的socket fd添加进epoll句柄,然后用epoll_wait轮询这个epoll fd上的事件。

事件说明:

1.如何建立连接。当epoll_wait返回的事件的fd == listenfd。即用于监听的socket上有了事件

2.为什么有EPOLLOUT,因为缓冲区是有大小的,写数据可能非常耗时间,拖慢其他fd的效率。所以在处理EPOLLIN的时如果有要写数据的需求,可以把事件改成EPOLLOUT ,通过epoll_ctl ADD回epoll句柄中,然后再通过epoll_wait触发。

3.EPOLLET和EPOLLLT的区别。EPOLLET只在状态由未就绪变为就绪时,触发一次。而LT模式,只要缓冲区有没有处理完的数据就会在epoll_wait的时候一直触发。因为这个特性,EPOLLET必须配合非阻塞的套接字使用。因为要一直读到缓冲区为空,所以必然会有一次读取时的缓冲区为空,如果是阻塞模式读取到空的缓冲区会直接阻塞,那显然是不行的。

EPOLL原理

数据结构:红黑树+双向链表(就绪链表)

1.调用epoll_create时,会在内核中创建一个红黑树和一个双向链表。

2.调用epoll_ctl进行操作时,根据操作的fd值,可以很快的找到对应红黑树上的结构体--这个红黑树上的结构体(epitem)就代表这个fd。

3.然后当一个 socket 在添加到这棵 epoll 树上时,会在这个 socket 的 wait queue 里注册一个回调函数,当有事件发生的时候再调用这个回调函数(而不是唤醒进程)

4.然后这个回调函数会将这socket加入到对应的就绪链表中,并唤醒epoll_wait,就绪链表也从内核空间拷贝到内核空间。

很明显epoll就这样实现了IO多路复用。多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态。

对比select:epoll返回的就绪链表,对于select还需要遍历才能得到。并且因为select的低效率,select的默认文件描述符上限被设置成1024

对比poll:poll的文件描述符上限可以达到65535,但要得到哪些fd是就绪的,还需要一次O(n)的遍历。并且和select一样,它在用户态和内核态间拷贝效能低下

操作系统

CPU的执行方式

cpu的组成:寄存器,运算器,控制器

执行方式可以简单描述为:程序指令经过控制器的调度,被送往运算器。运算器将指令执行的结果存放在寄存器,再存储到内存中,等待应用程序使用。

进程

进程就是处于执行期的程序。通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread),当然还包括用来存放全局变量的数据段等。在操作系统看来,进程是程序的运行态的表现形式。

进程间通信方式:

1.管道pipe。匿名管道。半双工通信(可以双向发送,但同时只能是一方发送,不能两方同时发送)。只能在具有公共祖先的进程间使用。

2.命名管道fifo。半双工通信。允许在没有亲缘关系的进程间使用。

3.消息队列。消息的链接表,存放在内核中,有消息队列标识符标识。克服了管道只能传递无格式字节流和缓冲区较小的缺点。

4.共享内存。由一个进程创建,但多个进程都可以访问的共享内存。最快的IPC方式。需要和其他方式(如信号量)配合使用,来实现进程间的同步和通信。

5.信号量。信号量是一个计数器。用于控制多个进程对同一共享资源的访问。可以作为锁机制。

6.套接字。不同机器间进程通信。

线程

线程是进程中活动的对象。一个进程可以包括多个线程,这些在同一程序内的线程可以共享内存地址空间,还可以共享打开的文件和其他资源。

Linux系统的线程实现非常特别:它对线程和进程并不特别区分。下面是内核中创建线程和普通fork的实现代码,都是调用clone,只不过创建线程时指明了父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序。

//创建线程
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0);

//普通fork()
clone(SIGCHLD, 0);

线程间通信方式:

1.基于共享变量

2.基于消息

协程

在线程的基础上,分时复用多个协程。

进程/线程切换需要切换上下文,需要占用内核态时间。而协程切换直接在用户态完成。

在大量io的情况下更好用。

Linux进程管理

进程描述符:内核把进程的列表存放在任务队列的双向循环链表中。链表的每一项都是类型为task_struct,称为进程描述符的结构。该结构定义在文件中。进程描述符包含一个具体进程的所有信息。

PID:内核通过一个唯一的进程标识值或PID来标识每个进程。PID默认最大值32678。它实际上就是系统中允许同时存在的进程的最大数目。

进程状态:TASK_RUNNING(运行) <-> TASK_INTERRUPTIBLE(可中断)/TASK_UNINTERRUPTIBLE(不可中断)

TASK_RUNNING(运行) :进程是可执行的;它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的进程。

TASK_INTERRUPTIBLE(可中断): 进程正在睡眠(被阻塞),等待某些条件的达成。一旦条件达成,内核就会把他们设置成运行。处于此状态的进程也会因为收到信号而提前被唤醒并随时准备投入运行。

TASK_UNINTERRUPTIBLE(不可中断):除了就算接收到信号也不会被唤醒或准备投入运行外,这个状态和可中断相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。

进程家族:所有进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他的程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程, 每个进程可以拥有零个或多个子进程。子进程终结会通知父进程。父进程终结时若子进程还未退出,则会给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做他们的父进程。

进程终结:可以简单分为两步1.释放资源2.删除进程描述符。只释放了资源而没有删除进程描述符的进程是僵尸进程。

僵尸进程:EXIT_ZOMBIE状态的进程。这个状态下,它占用的所有内存就是内核栈、thread_info结构和task_struct结构。与它相关联的所有资源都已经被释放掉。父进程检索到信息后,会将僵尸进程所持有的剩余资源也释放。也就是说,若父进程异常,那这些僵尸进程可能永远处于这个状态。

守护进程:不随控制终端关闭而关闭的进程,生存期很长。就是根本没有控制终端。利用进程管理的一些特点可以创建这样的进程。可以由以下步骤创建:

1.fork()后父进程退出,触发孤儿进程机制,它的父进程将变为init

2.调用setsid创建一个新会话

3.改变当前工作目录(进程若挂载在工作目录中,工作目录就不能被卸载)

4.关闭不再需要的文件描述符

Linux进程调度:

调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间。

进程优先级:linux采用了两种不同的优先级范围1.nice值[-20,+19],默认值为0;越大的nice值意味着更低的优先级。2.实时优先级[0,99]。越高的实时优先级意味着进程优先级越高。

#查看到你系统中的进程列表,以及它们对应的优先级。如果有进程显示”-“,则它不是实时进程。
ps-eo state,uid,pid,ppid,rtprio,time,comm.

时间片:一个数值。它表明进程在被抢占前所能持续运行的时间。

进程调度策略:实时策略SCHED_FIFO和SCHED_RR。非实时的调度策略SCHED_NORMAL。实时策略总是能抢占非实时。

  • SCHED_FIFO:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑。同等优先级的先进先出,先ready的跑到睡,后ready的接着跑。
  • SCHED_RR:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑。同等优先级的进行时间片轮转。
  • SCHED_NORMAL:Linux2.5 :O(1)     Linux2.6+ : CFS(完全公平调度算法)。执行方法如下。

CFS:CFS具有这样的理念——进程调度的效果应如同系统具备一个理想中的完美多任务处理器。在这种系统中,每个进程将能获得1/n的处理器时间。同时,调度给他们无限小的时间周期,所以在任何可测量周期内,我们给予的n个进程每个同样多的运行时间。要实现这样的理念面临的问题是——调度时进程抢占会带来一定的代价,换入换出进程本身有消耗,同时还会影响到缓存的效率。CFS的解决方法是——允许每个进程运行一段时间,循环轮转、选择运行最少的进程作为下一个运行进程,而不再采用分配给每个进程时间片的做法。

CFS实现:相关代码在kernel/sched_fair.c中。具体有几个关键部分

1.时间记账。struct sched_entity维护每个进程运行的时间记账。sched_entity结构体中有一个变量vruntime,存放进程的虚拟运行时间。该运行时间的计算是经过了所有可运行进程总数的标准化(加权)。//TODO 有个计算公式

2.进程选择。根据原理,CFS选择下一个运行进程时,它会挑一个具有最小vruntime的进程。具体实现方法是,用一颗红黑树,存储了系统中所有的可运行进程,以vruntime作为键值。然后在红黑树上进行查找操作。

3.睡眠和唤醒。红黑树中只存了可运行的进程。那么如果一个进程进入睡眠状态,他需要从红黑树中移除,并放入等待队列中。当他被唤醒,他就会从等待队列再插入到红黑树中。

4.调度器入口。当任务切换时会执行函数schedule(),他会先选择一个优先级最高的调度器(CFS调度类只是其中之一),然后再从调度类中选择最高优先级的进程。

进程终止:代码中可以调用exit()或_exit()终止进程。exit()会清空IO缓冲区后终止,_exit()则会直接终止进程,释放内存。

系统调用

系统调用在用户空间进程和硬件设备之间添加了一个中间层。

简单来说,系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它是内核唯一的合法入口。

API和系统调用

一个API可以实现成一个系统调用,也可以通过多个系统调用来实现。比如c的printf有以下调用流程:

printf() -> c库中的printf() -> c库中的write() -> write()系统调用

Linux内存管理

linux操作系统采用虚拟内存技术。可以被进程访问的合法地址空间称为内存区域。通过内核,进程可以给自己的地址空间动态地添加或减少内存区域。内存区域可以包含各种内存对象,比如

1.可执行文件代码的内存映射,称为代码段(text section) 【低地址】

2.可执行文件的已初始化全局变量的内存映射,称为数据段 【低地址】

3.包含未初始化的全局变量,也就是bss段的零页(页面中的信息全部为0值,所以可以用于映射bss段等目的)的内存映射 【低地址】

4.用于进程用户空间栈的零页的内存映射 【高地址】

5.每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间 【?】

6.任何内存映射文件 【堆栈之间】

7.任何共享内存段 【堆栈之间】

8.任何匿名的内存映射,比如由malloc分配的内存【 高地址】

内存描述符:内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符由mm_struct结构体表示。内存描述符,在进程创建的时候被分配。通常每一个进程的进程描述符(task_struct)中有一个域mm,它指向一个唯一的mm_struct结构体,代表这个进程唯一的地址空间。对线程而言,它的task_struct的mm域就指向父进程的内存描述符。

内存描述符中有一个mm_user域,记录正在使用该地址的进程数目。当进程退出时,将降低用户计数。当该内存描述符的引用计数也为0时,该内存描述符将归还到mm_cachep_slab中。

struct mm_struct{
    ...
    atomic_t mm_users;//使用地址空间的用户数
    atomic_t mm_count;//主使用计数器
    ...
}

虚拟内存:应用程序访问的地址,是虚拟内存的地址。操作系统会根据虚拟内存的地址翻译成真实的地址。

虚拟内存区域由vm_area_struct结构体描述,其*vm_mm域指向和这个虚拟内存区域相关的内存描述符的结构体。内存区域的位置就在[vm_start,vm_end]之中。vm_flag标志内存区域所包含页面的行为和信息。

struct vm_area_struct{
    ...
    struct mm_struct *vm_mm;//相关的mm_struct结构体
    unsigned long vm_start;//区间的首地址
    unsigned long vm_end;//区间的尾地址
    unsigned long vm_flag;//标志
    ...
}

内存分页:用分页的方式来记录虚拟内存地址和真实内存地址的关系。每页有4096字节。那么,虚拟地址的最后12位(2^12 == 4096)代表偏移量,对应在该页上的位置。而虚拟地址的前一部分则是页编号。

多级分页表:分页有多级。页编号也进一步的被分为多个部分。

Linux中使用三级页表完成地址转换。顶级页表PGD,它的表项指向二级页表,二级页表PMD,它的表项指向PTE,最后一级页表PTE指向物理页表

mmap(): 在用户空间中可以通过mmap()系统调用获取内核函数do_mmap()的功能。内核使用do_mmap()函数会将一个地址区间加入到进程的地址空间中——无论是扩展已存在的内存区域还是创建一个新的区域。即实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。 

munmap(): 和mmap相反的系统调用,从特定的进程地址空间中删除指定地址区间。

Linux的各种同步异步机制

原子操作:不会被线程调度机制打断的操作。举例,i++实际分为三步1.将i的值放到寄存器2.对寄存器内的数加一3.将寄存器的数值写回i。所以很明显,i++不是原子操作。

内核提供了atomic_t,atomic64_t,原子位操作等支持。

自旋锁:Linux内核中最常见的锁。自旋锁最多只能被一个可执行线程持有。一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋。自旋锁不可递归。

信号量:Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用(被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列中的那个任务将被唤醒,并获得该信号量。

信号量可以同时允许任意数量的持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量可以在声明信号量时指定。信号量声明如下:

//静态地声明信号量,count是信号量的使用数量
struct semaphore name;
sema_init(&name,count);
//创建更为普通的互斥信号量
static DECLARE_MUTEX(name);
init_MUTEX(name);

互斥体:行为和使用计数为1的信号量类似。

mutex_init(&mutex);//动态初始化
mutex_lock(&mutex);//为指定的mutex上锁
mutex_unlock(&mutex);//为指定的mutex解锁

共享内存:A,B两个进程共享内存是指,同一块物理内存地址被映射到进程A,B各自的地址空间。进程A可以看到B对共享内存中的数据更新,反之亦然。

在Linux中也提供了一组函数接口用于使用共享内存。共享内存函数由shmget、shmat、shmdt、shmctl四个函数组成。也可以使用mmap函数,通过共享文件磁盘地址的方式共享内存。文件的存储映射部分,在进程地址空间的堆栈之间。该部分的最大限制可以在linux下用命令查看。单个共享内存段最大字节数,一般为32M。

信号:

内核给进程发送信号,是在进程所在的进程表项的信号域设置对应的信号的位。进程检查信号的时机是:进程即将从内核态返回用户态时。如果进程睡眠了,要看睡眠能不能被中断,如果能被中断则唤醒。

SIGHUP和控制台操作有关,当控制台被关闭时系统会向拥有控制台sessionID的所有进程发送HUP信号,默认HUP信号的action是 exit,如果远程登陆启动某个服务进程并在程序运行时关闭连接的话会导致服务进程退出,所以一般服务进程都会用nohup工具启动(该命令就是让忽略该信号)或写成一个 daemon(利用setsid进行)。

SIGINT 终止进程,通常我们的Ctrl+C就发送的这个消息。

SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl- / )来控制. 进程收到该消息退出时会产生core文件。

SIGKILL 消息编号为9,我们经常用kill -9来杀死进程发送的就是这个消息,程序收到这个消息立即终止,这个消息不能被捕获,封锁或这忽略,所以是杀死进程的终极武器。

SIGTERM 是不带参数时kill默认发送的信号,默认是杀死进程。

SIGSTOP 停止进程的执行,同SIGKILL一样不可以被应用程序所处理,注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行。

SIGCONT 当SIGSTOP发送到一个进程时,通常的行为是暂停该进程的当前状态。如果发送SIGCONT信号,该进程将仅恢复执行。除了其他目的,SIGSTOP和SIGCONT用于Unix shell中的作业控制,无法捕获或忽略SIGCONT信号。

内核定时器

硬件为内核提供了一个系统定时器用以计算流逝的时间。系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称作节拍率。当时钟中断发生时,内核就通过一种特殊的中断处理程序对其进行处理,

Linux操作

netstat 显示网络状态。可以得知整个系统的网络状况

netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]

#查看占用该端口号的进程
#-p 显示socket所属的进程的PID和名字
#-a 显示结果也包含监听socket
#-n 显示ip和端口号而不是主机名和服务名

netstat -apn | grep <端口号> 

#windows下查看该进程占用哪些端口号
#findstr windows下同grep
#-o windows下相当于linux下的-p
#所以这个相当于linux下的 netstat -anp | grep <进程号>
netstat -ano | findstr '<进程号>' 

tcpdump 命令用于倾倒网络传输数据。执行tcpdump指令可列出经过指定网络界面的数据包文件头,在Linux操作系统中,你必须是系统管理员。

tcpdump [-adeflnNOpqStvx][-c<数据包数目>][-dd][-ddd][-F<表达文件>][-i<网络界面>][-r<数据包文件>][-s<数据包大小>][-tt][-T<数据包类型>][-vv][-w<数据包文件>][输出数据栏位]

#抓取网口eth0上 指定目标地址之间的数据包,开启以太网帧的头部显示
#-e 开启以太网帧的头部显示
#-n 使用ip地址显示主机而不是主机名;使用数字表示端口号,而不是服务名称
#-t 不打印时间戳
#-i 指定网卡。-i any可以监听所有网卡
tcpdump -i eth0 -ent '(dst 192.168.1.109 and src 192.168.1.108) or (dst 92.168.1.108 and src 192.168.1.109)'       #例子来源,观察arp通信过程

#抓取网口eth0上有关域名服务的数据包,最大长度为500
#-s 设置抓包抓取长度。若还是超长就显示被截断的数据包
#port domain 过滤数据包,只显示domain(域名)相关的包
tcpdump -i eth0 -nt -s 500 port domain #观察DNS通信过程

#抓取本地回路上的数据包
#-x 以十六进制的形式显示数据包
#lo loop,本地回环
tcpdump -ntx -i lo #用talent连接自己,观察ipv4的头部结构


#抓取网卡eth0上 指定目标地址之间的数据包
tcpdump -i eth0 -nt '(scr 192.168.1.109 and dst 192.168.1.108) or (scr 192.168.1.108 and dst 192.168.1.109)' #抓取tcp的经典操作




          

ipcs 用于查看进程间通信信息

ipcrm 用于删除一个ipc 

ipcrm -M shmkey  #移除用shmkey创建的共享内存段
ipcrm -m shmid    #移除用shmid标识的共享内存段
ipcrm -Q msgkey  #移除用msqkey创建的消息队列
ipcrm -q msqid  #移除用msqid标识的消息队列
ipcrm -S semkey  #移除用semkey创建的信号
ipcrm -s semid  #移除用semid标识的信号

mysql

InnoDB:存储引擎。支持事务,设计目标主要面向在线事务处理(OLTP)的应用。其特点是行锁设计、支持外键,并支持类似于Orale的非锁定读——即默认读取操作不会产生锁。

E-R模型:实体联系模型。提供不受任何DBMS约束的面向用户的表达方法。

索引

1.B+树索引:根据键值快速找到数据。可以分为聚集索引和辅助索引。聚集索引就是按每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据。辅助索引的叶节点不包含行数据,但包含指向其主键索引的指针。用辅助索引查找数据时,会先得到叶子节点所指向的主键值,然后再用主键索引来查找完整的行数据。

2.全文索引:全文检索是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。倒排索引实现。它在辅助表中存储了单词与单词自身在一个或多个文档所在位置之间的映射。

3.哈希索引:只做等值比较的查询。基于哈希表实现。

B+树:一颗m阶的B+树可以这样定义

1.每个节点最多可以有m个元素;

2.除根节点外,每个节点最少有m/2个元素;

3.如果根节点不是叶节点,那么它最少有两个孩子节点;

4.所有叶子节点都在同一层

5.一个有k个孩子节点的非叶子节点有k-1个元素,按升序排列

6.某个元素的左子树中的元素都比它小,右子树中的元素都大于等于它

7.非叶子节点只存放关键字和指向下一个孩子节点的索引,记录只存放在叶子节点中;

8.相邻叶子节点之间用指针相连;

B树和B+树的区别:

1.B树的非叶子节点存数据,B+树只有叶子节点存数据

2.B树的叶子节点指针为null,B+树的相邻叶子节点用指针相连

mysql查询优化:

事务

ACID:原子性,一致性,隔离性,持久性

隔离级别:READ UNCOMMITED读未提交,READ COMMITED读已提交,REPEATABLE READ可重复读,SERIALIZABLE串行化

悲观锁:在修改数据之前先锁定,后修改的方式。悲观锁又分为S锁(Share Lock,共享锁,又称读锁)和X锁(Exclusive Lock,排他锁,又称写锁)

意向锁:mysql中的锁是行锁,但也有表锁。它就是意向锁。

乐观锁:只有在正式提交数据更新时,才会对数据是否冲突进行检测。

乐观锁的CAS:当多个线程尝试更新同一个变量时,只有其中一个线程可以修改这个变量,而其他的线程都失败。失败的线程不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS操作包括三个操作数:V(内存位置),A(原值),B(新值)。当V中的值等于原值,处理器会将它的值修改为新值(也就是说,这里是根据内存位置里的值和原值是否相等来判断是否发生过抢占的)。

ABA问题:乐观锁的CAS(compare and set)机制会出现ABA问题。ABA可以这样描述:

1.线程一读取数据,此时数据值是A

2.线程二读取数据,并将数据值修改为B

3.线程二又再次把数据修改回A

4.线程一再次读取数据,读取到的数据值还是A,线程一也会更改数据值。

显然发生过抢占,但线程一还是更改了数据值。这会带来一系列问题。解决方法是,在数据值上加上版号。

事务控制语句:

START TRANSACTION|BEGIN:显示地开启事务

COMMIT:提交事务

ROLLBACK:结束用户事务,并撤销所有修改

SAVEPOINT:在事务中创建一个保存点

RELEASE SAVEPOINT:删除一个事务的保存点

ROLLBACK TO [SAVEPOINT]:把事务回滚到标记点

SET TRANSACTION:设置事务的隔离级别

事务的实现:InnoDB 对事务的实现主要依赖以下四个功能,1.redo 2.undo 3.purge 4.group commit

redo log 称为重做日志,数据库发生故障时会通过redo log重写事务,用来保证事务持久性。

undo log用来帮助事务回滚和MVCC功能,用来保证事务的一致性。

purge功能用于最终完成delete和update操作

group commit功能即一次fsync可以刷新确保多个事务日志被写入文件。

性能

qps:每秒处理的查询数

tps:每秒处理的事务数

iops:每秒磁盘进行的io操作次数

mysql InnoDB性能调优:

1.cpu:InnoDB主要后台操作都在单独的master thread中完成,对多核利用不佳。但可以通过修改参数innodb_read_to_threads和innodb_write_io_threads来增大IO的线程。OLTP本身对CPU的要求不是很高。

2.内存:InnoDB存储引擎缓存数据和索引,将他们缓存于一个很大的缓存池中。内存越大缓存池就能越大,缓存命中率就将越高,其性能也就越高。但若缓冲池的大小大于数据本身的大小,再调大缓冲池也就没意义了。

3.硬盘:机械硬盘可以将多块机械硬盘设置成RAID,一般是raid10。固态硬盘访问延时较低。

redis

key-value数据库,运行在内存中。单线程结构。

redis数据类型

String:普通key-value存储;

Hash:value是一个hashmap。这个hashmap里的值可以通过key+field存取;

List:简单字符串双向链表

Set:String类型的无序集合,由哈希表实现

SortedSet:有序集合,关联分数,哈希表+跳表实现。跳表就是在链表的基础上添加索引层,使其可以通过跳跃的方式而不是遍历来到达某个节点。如插入语句:

#插入一个分数为1的value
ZADD runoobkey 1 redis 

Pub/Sub :发布/订阅。订阅对应频道后,可以收到该订阅频道的消息。

Transcations:redis事务。没有原子特性。会将他们放入缓存队列顺序执行。

redis持久化

1.RDB持久化:在指定的时间间隔内将内存中的数据集快照写入磁盘。实际操作是fork一个子进程,先将数据集写入临时文件,写入成功后再替换之前的文件,用二进制压缩存储。

2.AOF持久化:以记录日志的方式记录服务器的每一个操作写、删操作。查询操作不会记录。

redis哈希

redis哈希对象的底层存储可以使用ziplist和hashtable。当存储数据量较小时,即同时满足以下两个条件时,redis使用ziplist编码:

1.当哈希对象保存的键值对数量小于hash-max-ziplist-entries(默认512)

2.当哈希对象保存的键值对的所有键和值都小于hash-max-ziplist-value(默认64)

ziplist:特殊的双向链表——没有维护pre,next指针,而是记录了上一个entry的长度和当前enter的长度,根据这个长度判断下一个节点在什么地方。

hashtable:用redis的dict实现。底层使用一个dict。所有元素都可以用key值计算HashKey,然后用拉链法解决冲突,最后会将元素插入到dict的某个链上。其机制:

1.冲突链:当hash值冲突时,会将数据放入该位置的冲突链

2.rehash:当冲突链过长势必导致查询速度变慢,这时需要使用rehash。这里是用负载因子判断:当前保存的节点数目/哈希表大小

rehash的方法:

1.建一个新的哈希表,若是扩容其大小是第一个大于原表使用长度*2^(n+1)的整数,若是缩容其大小是第一个大于原表使用长度*2^n的整数。

2.根据新哈希表的大小重新哈希。单线程的redis由于性能问题不会一次性将整个hash表迁移到新表。而是采用渐进式的方式,将其分散到定时函数和每一次对该表的操作上。rehash完成后会删除原表。

3.在线rehash时,记录rehashindex。增删改查操作可以捎带在新哈希表中完成。若当前的增删改查操作的index大于rehashindex,则操作原哈希表的同时捎带操作新哈希表。反之直接操作新哈希表。当新哈希表哈希完成时将rehashindex标记为-1,代表rehash完成

redis缓存

缓存淘汰机制:redis实现了LRU(最近最少使用)和LFU(最不常用)两种淘汰策略

缓存更新机制:

1.Cache-Aside:读操作时,先检查数据在不在缓存上,若不在则从数据库中查询出来并存到缓存中。写操作时,先更新数据库,再删除缓存。

2.Read-Through/Write-Through:读操作时,先查询数据在缓存中是否存在,若存在则返回缓存里的数据。若不存在则由缓存组件去数据库查询更新该数据。写操作时,先查询数据在缓存中是否存在,若存在就先写缓存。由缓存系统去同步更新数据库中的数据。若不存在就直接写数据库。

3.Write Back:写操作时只写缓存。由缓存去异步的更新数据库。缺点是,宕机时会发生数据丢失。

缓存问题:1.缓存雪崩 2.缓存穿透 3.缓存击穿4.缓存降级5.缓存预热

redis集群

集群模式:

1.主从模式:有且仅有一个主节点,有多个从节点。主节点会一直同步数据给从节点来保持主从同步。主节点可读可写,从节点只读。

2.Sentinel模式:哨兵模式。在主从模式的基础上,启动哨兵实例。哨兵可以不断监听主节点是否运行正常。若主节点运行不正常,则会在从服务器中选择一个新的主节点。选取方式是raft算法。

3.Cluster模式:无中心集群。所有节点互联。所有节点都保存数据和整个集群的状态。所有的物理节点映射到[0-16383]槽位上。这些槽位会分别均摊分配到各个节点中。当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值决定映射在哪个槽位中。每个节点客户端可以与任何一个节点直连,客户端查询key时,若key不在当前节点上,该节点会告诉客户端在哪一个节点上。然后客户端再去主动连接。

节点失效:超过半数已上节点认为某个节点失效,那么那个节点fail。若该节点是master节点,则他的其中一个从节点会变成master节点。若他没有从节点了,那么整个集群失效。

raft共识算法:raft算法是实现分布式共识的一种算法,主要用来管理日志复制的一致性

raft基本概念:

0.三种角色:leader,cadidate,follower

1.复制状态机:简单来说,如果每个节点的日志一样,对日志的执行也一样,那他们的状态最终会一样

2.任期:每个任期开始一个领导人选举,如果赢得选举则在任期的剩余时间担任领导人,若选举失败立即开启下一任期

3.心跳和超时机制:两个定时器,选举定时器和heartbeat timeout(领导的心跳)

raft工作机制:

1.leader选举:一开始,所有角色都以follower的状态启动,同时启动选举定时器(时间随机,降低冲突的概率)。若一个节点发现在一个定时器的时间内没有收到领导的心跳,那么它会成为候选人直到(1.该节点成为leader2.其他节点成为leader3.选举任期结束,开始下一轮任期)。候选人会向其他节点发送投票请求,得到半数以上节点同意,那么它将成为leader。leader会向其他节点发送心跳,节点收到心跳就会重置选举定时器。

2.日志复制:client向leader提交命令,leader先不执行,将其写入日志中,并将该命令标记为uncommitted状态,复制状态机不会执行该命令。然后leader会把命令并发复制给所有节点,直到所有节点都保存了命令到日志中。然后leader才会提交命令,并将命令状态置为已提交,并同步已提交状态给所有的节点。

3.安全性:节点在投票时,会比较自己的日志和候选人的日志,如果自己的日志更新,那该节点就不会投票给候选人。这类似于区块链始终认可更长的链。这保证了新leader也有该网络的正确日志

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值