一、C/C++语言部分
static关键字
在C语言中,关键字static可以用来修饰变量和函数,其中:
- static加在局部变量的前面改变其存储类型使之称为局部静态变量,会延长它的生存周期,但是注意不会改变其作用域。
- static加在全局变量的前面会限制该变量作用域为文件作用域,也就是说static全局变量只能在定义该变量的文件中使用,不能被其他文件使用。
- 加在函数定义或声明的前面,也是限制函数作用域到文件作用域。
在C++中,除了保留上面的特性外,还可以用来修饰类的成员变量和函数,使之称为静态成员变量和静态成员函数,统称为静态成员。
- sizeof不将静态成员变量的大小计算在内
而且在使用静态成员变量的时候必须初始化。 - 可以通过类名来访问静态成员(需要访问权限public)
静态成员是属于类的,不属于某个对象,即使没有对象存在,类的静态成员也存在了,因此静态成员除了能和不同成员一样通过对象访问外,也可以通过类名访问,形式为 类名::成员名。 - 静态成员函数不能访问类的非静态成员
这是由于类的非静态成员必须在类实例化对象后才有内存空间。
static声明的函数是否可以在其他文件中调用?
不可以被其他文件直接调用,但是可以用间接的方式
- 通过函数指针的方式
- 通过非static的方式,定义一个普通的函数,让这个函数调用static函数,然后把这个不同函数在头文件中声明
const关键字
- 定义的时候必须进行初始化。
- 指针可以是const指针,也可以是指向const对象的指针。
- 定义为const的形参,即在函数内部是不能被修改的。
- 类的成员函数可以被声明为常成员函数,不能修改类的成员变量。
- 类的成员函数可以返回的是常对象,即被const声明的对象。
- 类的成员变量是常成员变量,不能在声明时初始化,必须在构造函数的列表进行初始化。 只读的性质由编译器赋予,人为修改编译不通过
“extern C”
作为C语言的扩展,C++保留了一部分过程式语言的特点,因为它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的设计语言,为了支持函数的重载,C++对全局函数的处理有着明显的不同。
函数在被C++编译后在符号库中的名字与C语言的不同,加入某个函数的原型是void foo(int x, int y);该函数被C编译器编译后在符号库中的名字是_foo,而C++编译器则会产生_foo_int_int之类的名字,C++就是靠这种机制来实现函数重载的。
被“extern C”修饰的函数或者变量是按照C语言方式编译和链接的,概括来说真实目的就是:实现C++和C的混合编程。
memcpy的写法
版本一:未考虑地址重叠的情况
void *memcpy(void *dest, void *src, int count) {
void *ptr = dest;
if (dest == NULL || src == NULL || count <= 0)
return NULL;
while (count--) {
*(char *)dest = *(char *)src++;
}
return ptr;
}
完善版本:
void *memcpy(void *dst, const void *src, int size){
char *psrc;
char *pdst;
if(NULL == dst || NULL == src || size <= 0)
return NULL;
if((src < dst) && (char *)src + size > (char *)dst) {
// 自后向前拷贝
psrc = (char *)src + size - 1;
pdst = (char *)dst + size - 1;
while(size--) {
*pdst-- = *psrc--;
}
} else {
psrc = (char *)src;
pdst = (char *)dst;
while(size--) {
*pdst++ = *psrc++;
}
}
return dst;
}
函数指针的作用:调用函数或者作为函数的参数
实现非定长的结构体:长度为0的数组(a[0])
野指针:指向一个已删除对象或未申请访问受限内存区域的指针。主要是指向的内存区域不合法,不合法在:1.所指向的内存未申请;2.所指向的内存被释放;
strcpy和memcpy的区别
malloc和new的区别
- malloc和new都是在堆上开辟内存的,但是malloc只负责开辟内存,没有初始化的的功能,需要用户自己初始化;new不但开辟内存,还可以初始化;
- malloc是函数,开辟内存需要传入字节数;new是运算符,开辟内存需要指定类型,返回指定类型的指针;
- malloc开辟内存失败返回NULL,new开辟内存失败会抛出bad_alloc类型的异常,需要捕获异常才可以判断内存开辟成功还是失败,new运算符其实是operator new函数的调用,它底层调用也是malloc来开辟内存的,new比malloc多了初始化功能,对于类类型来说,所谓初始化,就是调用了对应的构造函数。
- malloc开辟的内存永远通过free来释放;而new单个元素,用delete,new[]数组,使用delete[]
- malloc开辟内存只有一种,而new有四种分别是普通的new(内存开辟失败抛出bad_alloc异常)、nothrow版本的new、const new以及定位new。
内存池
哈希表解决冲突的方法
- 链地址法(拉链法)– 将所有冲突的关键字存储在一个线性链表中
- 开放地址法 – 通常有三种:线性探测(x+1,x+2…)、二次探测(x+1,x+4,x+9)、再哈希法(用不同的哈希函数再做一次哈希化,用这个结果作为步长)
重载、重写和重定义
重载:同一个类中的函数具有相同的名称,但是参数的列表不相同
重写:也叫覆盖,子类重新定义父类中有相同名称或者参数的虚函数,主要在继承关系中出现。
重定义:派生类对基类函数的重定义,派生类函数名和基类某函数同名。(参数列表、函数返回值无要求)。特殊情况,如果是虚函数,且(名字、参数列表、返回值)完全一样,属于重写。
哪些函数不能声明为虚函数?
普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数
C++虚函数表、虚表指针和内存分布
C++11的新特性
- “语法糖”:nullptr,auto自动类型推导,范围for循环,初始化列表,lambda表达式等
- 右值引用和移动语义
- 智能指针
- C++11多线程编程:thread库及其配套的同步原语mutex、lock_guard、condition_variable以及异步的std::furture
参考文章:C++11新特性梳理
右值引用和移动语义
左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不在存在的临时对象。
区分它们的便捷方法:看能不能表达式取地址,如果能,则为左值,否则为右值
如果能直接使用临时对象已经申请的资源,既能节省资源,又能结束节省资源申请和释放的时间,C++11移动语义可以实现这一点。
要实现移动语义,要增加两个函数:移动构造函数和移动赋值函数
拷贝构造的参数常量左值引用,移动构造的参数是右值引用,它不是重新分配一块空间,将要拷贝的对象复制过来,而是“偷”过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr。
对于一个左值,肯定是调用拷贝构造函数,但是有些左值是局部变量,生命周期短,能不能也移动而不是拷贝?C++11 std::move()方法来将左值转换成右值,从而方便应用移动语义。其实可以理解为告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是使用移动构造函数
完美转发:通过一个函数将将参数继续转交给另一个函数处理,原参数可能是右值,也可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。通常使用std::forward()函数模板和universal references通用引用类型来实现完美转发。
具体参看:[c++11]我理解的右值引用、移动语义和完美转发
智能指针
shared_ptr的实现:
template <typename T>
class SmartPointer {
private:
T *_ptr;
size_t *_count;
public:
SmartPointer(T *ptr = nullptr) : _ptr(ptr) {
if (_ptr) {
_count = new size_t(1);
} else {
_count = new size_t(0);
}
}
SmartPointer(const SmartPointer &src) {
if (this != &src) {
this->_ptr = src._ptr;
this->_count = src._count;
(*this->count)++;
}
}
SmartPointer& operator=(const SmartPointer &src) {
if (this->_ptr == src._ptr) {
return *this;
}
if (this->_ptr) {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count; // 释放堆内容
}
}
this->_ptr = src._ptr;
this->_count = src._count;
(*this->_count)++;
}
T& operator*() {
assert(this->_ptr == nullptr);
return *this->_ptr;
}
T* operator->() {
assert(this->_ptr == nullptr);
return *this->_ptr;
}
~SmartPointer() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
size_t use_count() {
return *this->_count;
}
};
vector的push_back实现和扩容复杂度
二、计算机网络
OSI模型和TCP/IP模型
OSI七层模型及其包含的协议:
- 物理层:通过媒介传输比特,确定机械及电气规范,传输单位为bit,主要协议:IEEE802.3
- 数据链路层:将比特组装成帧和点对点的传输,传输单位为帧,主要协议:MAC、VLAN、PPP
- 网络层:负责将数据包从源到宿的传递和网际互联,传输单位为包,主要协议:IP、ARP、ICMP
- 传输层:提供端到端的可靠报文传递和错误恢复,传输单位为报文,主要协议:TCP、UDP
- 会话层:建立、管理和终止会话,传输单位为SPDU,主要协议:RPC、NFS
- 表示层:对数据进行翻译、加密和压缩,传输单位为PPDU,主要协议:JPEG、ASII
- 应用层:允许访问OSI环境的手段,传输单位为APDU,主要协议:FTP、HTTP、DNS
TCP/IP 4层模型包括:
- 网络接口层:MAC、VLAN
- 网络层:IP、ARP、ICMP
- 传输层:TCP、UDP
- 应用层:HTTP、DNS、SMTP
TCP三次握手和四次挥手
客户端TCP状态迁移:
CLOSED -> SYN_SENT -> ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
服务器TCP状态迁移:
CLOSED -> LISTEN -> SYN_REVD -> ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED
- 为什么三次握手?
为了防止已经失效的连接请求报文段突然又传送到了服务端,产生错误。主要目的是防止服务端一直等待,浪费资源。详细原因如下:
client发出的第一个连接请求由于长时间滞留,以致延误到连接释放后的某个时间到达server,server收到后向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么新的连接就建立了,但是现在client并没有发出建立连接的请求,不会理采server,也不会向server发送数据,server会一直等待client发来数据。
- 为什么需要四次握手? 通信 TCP需要支持半关闭连接,一开始建立的连接是全双工的,A <=> B 双方都可以读写。支持半关闭意味着,TCP 支持 A 和 B 双方独立关闭通道。因此会有两次独立的关闭写通道的请求。一次关闭请求(FIN),对应一个 ACK。所以就有了四次挥手。
ARP:地址解析协议,根据IP地址获取物理地址。
滑动窗口
ping命令:相当于一个应用程序,位于应用层,但是使用的协议是ICMP,属于网络层
TCP的可靠传输
TCP拥塞控制
TCP和UDP的区别
都属于传输层协议
- TCP是面向连接的,UDP是无连接的;
- TCP是可靠的,UDP是不可靠的;
- TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多的通信模式;
- TCP是面向字节流的,UDP是面向报文的;
- TCP有拥塞控制机制;UDP没有拥塞控制,适合媒体通信;
- TCP首部开销(20个字节)比UDP的首部开销(8个字节)要大;
关于HTTP和HTTPS
请求报文构成:1.请求行 [请求方法 URL 协议/版本] 2. 请求头[Request Header] 3.请求正文
响应报文构成:1.状态行[协议版本号 响应码] 2.响应头 3.响应正文
post和get的区别:
- 都包含请求头请求行,post多了请求body
- get多用来查询,请求参数放在url中,不会对服务器上的内容产生作用。post用来提交,如把帐号密码放入body中
- get是直接添加到url后面的,直接就可以在url中看到内容,而post是放在报文内部的,用户无法直接看到
- get提交的数据长度是有限制的,因为url长度有限制,具体的长度限制视浏览器而定。而post没有x
状态响应码
状态码分类:
- 1XX - 信息型,服务器收到请求,需要请求这继续操作
- 2XX - 成功型,请求成功收到,理解并处理
- 3XX - 重定向,楈枒进一步的操作以完成请求
- 4XX - 客户端错误,请求包含语法错误或无法完成请求
- 5XX - 服务器错误,服务器在处理请求的过程中发生了错误
常见的状态码:
200 OK (客户端请求成功)、301(资源被永久转移到其他url)、302(临时跳转)、400 Bad Request(客户端请求有语法错误)、401 Unauthorized(请求未经授权)、404(请求资源不存在)、500(服务器内部发生错误)、503 Server Unavailable(服务器当前不能处理客户端请求,一段时间后可能恢复正常)
HTTPS
由于HTTP存在一些问题,例如:请求信息明文传输、数据的完整性没有校验、没有验证对方的身份。为了解决上述问题,用到了HTTPS,一般理解为HTTP+SSL/TLS,通过SSL证书来验证服务器的身份,并为浏览器和服务器之间的通讯进行加密。
HTTPS与HTTP的区别:
- HTTP协议是以明文的方式在网络中传输数据,而HTTPS协议传输的数据则是经过SSL加密的,HTTPS具有更高的安全性
- HTTPS在TCP三次握手阶段之后,还需要进行SSL的handshake,协商加密使用的对称加密密钥
- HTTPS协议需要服务端申请证书,浏览器端安装对应的根证书
- HTTP协议的端口是80,HTTPS协议的端口是443
HTTPS的优缺点:
优点:1.传输过程中使用密钥加密,更安全 2.可以认证用户和服务器,确保数据发送到正确的用户和服务器
缺点:1.握手阶段延时较高:由于在进行HTTP会话前还需要进行SSL握手,因此HTTPS协议握手阶段延时增加 2.部署成本高:需要购买证书;加解密计算占用CPU资源较多,需要的服务器配置和数目高
HTTPS通信过程
HTTPS在真正请求数据时,先会与服务器有几次握手验证,以验明相互的身份,如下图:
验证流程:
- 客户端发起一个https的请求,把自身支持的一系列Cilpher(密钥算法套件)发送给服务器
- 服务器接收到客户端所有的Clipher后与自身支持的对比,如果不支持就断开连接,反之则会从中选出一种加密算法和Hash算法以证书的形式返回给客户端,证书中还包含了公钥、颁证机构、网址、失效日期等
- 客户端收到服务器响应后,会做以下几件事:
- 验证证书的合法性
- 生成随机密码,然后用证书中的公钥加密
- 把握手消息取Hash值,然后用随机数加密“握手消息+握手消息Hash值(签名)”发给服务器(保证握手消息传输过程中没有被篡改),最后把之前生成的信息发送给服务器
- 服务器拿到密文,用自己的私钥解密握手消息取出随机数,再用随机数密码 解密 握手消息与Hash值,并与传输过来的Hash值对比。然后,用随机密码加密一段握手消息(握手消息+握手消息Hash值)给客户端
- 客户端用随机数解密并计算握手消息的Hash值,比对一致,握手过程结束。之后所有的通信数据紧挨给由随机密码并利用对称加密算法进行加密
Cookie和Session
原理与区别
cookie采用的是客户端的会话状态的一种存储机制,它是服务器在本地机器上存储的小段文本或者是内存中的一段数据,并随每一个请求发送至同一服务器。注:cookie由服务器生成,通过response响应头的set-Cookie字段进行设置,包含用户id、密码、浏览过的网页、停留时间等,内容进行了加密。
session是一种服务器端的信息管理机制,它把这些信息已文件的新式存放在服务器的硬盘空间上(这是默认情况,也可以用memcache把这种数据放到内存里面),当客户端向服务器发出请求时,要求服务器端产生一个session,服务器会先检查一下,客户端的cookie里面有没有session_id,是否过期。如果有这样的session_id的话,服务器端会根据cookie里的session_id把服务器的session检索出来。如果没有则会重新建立一个。
如果禁用cookie,session也不能用了嘛?
可以使用,不过是通过其他方式获取session_id,比如可以跟在url的后面,或者以表单的形式提交到服务器端,从而使服务器端了解客户端的状态。
为什么说session比cookie更安全?
要想攻破session,得分两步:
- 得到session_id。攻破cookie后,你要得到session_id,session_id是要有登录,或者启动session_start才会有,你不知道什么时候有人登录。
- 取有效session_id。session_id是加密的,第二次session_start的时候,前一次的session_id就没有用了,session过期时session_id也会失效,想在短时间内攻破加密的session_id很难,session是针对某一次通信而言,会话结束session也就随着消失了。
三、操作系统
进程和线程的区别
- 根本区别:进程是操作系统资源分配的基本单位,线程是任务调度和执行的基本单位。
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作十分昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小的多,同时创建一个线程的开销也比进程要小的多。
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以(IPC)进行。不过如何处理同步和互斥是编写多线程程序的难点。
- 但是多进程程序更为健壮,多线程程序只要一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
进程切换
- 切换页目录以使用新的地址空间
- 切换内核栈和硬件上下文
对于线程切换,第一步不需要做的,第二步是进程和线程切换都要做的。
进程间通信
IPC的通常有管道(包括命名管道和无名管道)、消息队列、信号量、共享存储、socket、streams等,其中socket和streams支持不同主机上的两个进程IPC。
- 管道,常指无名管道,半双工 ,只能用于具有亲缘关系的进程间的通信(父子进程和兄弟进程),pipe()。
- FIFO,也称为命名管道,可以在无关进程间交换数据, mkfifo()。
- 消息队列,是消息的链接表,存放在内核中,一个消息队列由一个标识符(即队列ID)来标识。
- 信号量,是一个计数器,用于实现进程间的互斥和同步,而不是用于存储进程间通信数据,如要在进程间传递数据需要结合共享内存。
- 共享内存,指两个或多个进程共享一个给定的存储区。
线程间通信
- 锁机制:包括互斥锁、条件变量、读写锁
- 互斥锁提供了排他方式防止数据被并发修改
- 读写锁允许多个线程同时读共享数据,而对写操作是互斥的
- 条件变量可以以原子的方式阻塞线程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的,条件变量始终与互斥锁一起使用。
- 信号量机制
- 信号机制:类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程中的用于数据交换的通信机制。
内存管理
Linux下有哪些信号
- SIGABRT(程序异常终止信号,abort函数产生该信号)
- SIGALRM(alarm函数超时时产生该信号,默认动作是程序终止)
- SIGCHLD(子进程终止或停止时产生该信号,默认被忽略)
- SIGHUP(终端连接断开信号)
- SIGPIPE(当管道读端已关闭,继续往管道中写,产生该信号)
- SIGURG (紧急情况-套接字)
- SIGINT(中断信号,终端中输入ctrl+c,可中断前台进程)
- SIGKILL(不能被捕获)、SIGTERM(可以被捕获)
Linux I/O和标准I/O区别
- 标准I/O函数具有良好的移植性
- 标准I/O函数可以利用缓冲提高性能
其中区分I/O函数缓冲和套接字缓冲,套接字的缓冲是为了实现TCP协议而设立的,保存数据丢失时再发送。与之相反,I/O函数缓冲的主要目的是为了提高性能。
epoll两种模式
LT工作模式:当文件描述符上的事件就绪后,如果事务没有处理完成或没有处理,那么下一次epoll会提醒应用程序
ET工作模式:当文件描述符上的事件就绪后,如果事务没有处理完成或者没有处理,那么下一次epoll则不会提醒应用程序,这就要求我么在收到一次提醒后,必须将当下的数据处理完成。
LT是阻塞与非阻塞模式二者都可以,但是ET模式需要采用非阻塞模式。
Reason:为了一次性的把缓冲区的数据读取完,必须写一个while循环来read,直到缓冲区的数据被读完,但是如果设置成阻塞模式,无法知道数据什么时候被读完,因为读完数据时,while会卡在read,一直在等待。
如果设置成非阻塞,当数据被读完,read就会返回,然后将error设置成EAGAIN并退出while。
epoll为什么高效
- 从调用方式来看:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(意味着诶次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,就会很低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再将fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了,每次epoll_ctll只是对这个数据结构进行简单的维护。
- 内核使用了slab机制,为epoll提供快速的数据结构:在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动时),同时会开辟出epoll自己的内核高速缓冲cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配号你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
- epoll的第三个优势在于:当我们在调用epoll_ctr往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没有数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎样维护的呢?当我们执行epoll_ctr时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后来就把fd(socket)插入到准备就绪list链表里了。
虚拟内存
fd最大多少?与什么有关?如何修改?
shell级别限制:ulimit -n命令可以查看默认fd最多多少个,一般默认为1024,可以通过ulimit -n 1200修改,当前用户所有进程能打开的最大文件数量
用户级限制:修改/etc/security/limit.conf实现对用户的限制
系统级限制:修改/proc/sys/fs/file-max
与内存的大小有关
关于死锁
死锁产生的四个必要条件
- 互斥条件
- 不可剥夺条件
- 请求和保持条件
- 循环等待条件
破坏死锁
通过破坏四个必要条件来破坏死锁
预防死锁、避免死锁(银行家算法
)、检测死锁、解除死锁
Linux基本命令
查看可执行文件ELF readelf、objdump(反汇编)
四、数据库
三范式
第一范式(1NF):要求数据库表的每一列都是不可分割的原子数据项
第二范式(2NF):在1NF的基础上,必须有主键,没有包含主键的列必须完全依赖于主键
第三范式(3NF):在2NF的基础上,非主键列必须直接依赖于主键,不能传递依赖(消除了传递依赖)
SQL优化
- 只返回所需要的数据
- 尽量不要写
select
的语句 - 合理写
where
子句,不要写没有where的语句 - 适当建立索引,但以下几点会进行全表扫描
- 左模糊查询
%...
- 使用不等操作符
- 左模糊查询
- 尽量不要写
事务
-
定义:作为单个逻辑工作单元执行的一系列操作,要么完全地执行成功,要么完全地不执行。
-
四大特性(ACID):原子性、一致性、隔离性、持久性
-
一致性:指一个事务的执行前后数据库必须处于一致性状态,事务的执行结果必须是数据库从一个一致性状态变到另一个一致性状态
-
隔离性:一个事务的执行不能干扰其他事务,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能相互干扰
-
-
如果不考虑事务的隔离性,会发生的问题:
- 脏读:一个事务处理过程里读取了另一个未提交事务中的数据
- 不可重复读:对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了
- 虚读(幻读):幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读
-
四种隔离级别
- 串行化:可避免脏读、不可重复读、幻读的发生
- 可重复读:可避免脏读、不可重复读的发生
- 读已提交:可避免脏读的发生
- 读未提交:最低级别,任何情况都无法保证
-
锁模式 – 独占锁、共享锁、更新锁
-
锁粒度
- 行锁、页锁、表锁、数据库锁
存储引擎
五、算法与数据结构/智力题目
算法与数据结构
快速排序
void quick_sort(vector<int> &arr, int low, int high) {
if (low < high) {
int index = partition(arr, low, high);
quick_sort(arr, low, index -1);
quick_sort(arr, index + 1, high);
}
}
int partition(vector<int> &arr, int low, int high) {
int tmp = arr[low];
while (low < high) {
while (low < high && arr[high] >= tmp) {
high--;
}
arr[low] = arr[high];
while(low < high && arr[low] <= tmp) {
low++;
}
arr[hihg] = arr[low];
}
arr[low] = tmp;
return low;
}
智力题目
100层楼,有两个鸡蛋,有唯一一层从该楼层以及以下楼层鸡蛋不会碎,最快需要多少次
(X+1)*X/2 > 100
如何用数组实现链表功能
数组中存放一个结构体,一个表示数据,另外一个表示其下一个节点在数组中的index,以便快速插入删除。
找第一个出现一次的字符
建一个26大小的数组统计出现字符的次数,然后去找第一个出现结果为1的字符