赞
踩
python
2022春招,Python面试必知必会,245道面试真题
python就是高级语言,C语言是低级语言
高级语言:执行效率低,实现效率高,目标代码大,可维护性好,代码移植型好。可坑不需要关注与底层的具体实现,很多功能都已经封装好了,只需要专注于业务逻辑地实现
低级语言:执行效率高,实现效率低,目标代码小,可维护性差,代码移植型差
无论如何实例化,都只有一个对象存在
class A(object): __instance=None __x=1 def __new__(cls,*args,**kwargs): if cls.__instance is None: cls.__instance=object.__new__(cls) return cls.__instance else: return cls.__instance def add(self): self.__x+=1 print(self.__x) a=A() a.add() b=A() b.add() #结果 2 3
实际上就是使用闭包实现的,看一个简单小代码。
分析一下,首先wrapper
就是一个装饰器函数,然后inner
是一个闭包函数,装饰器实际起作用的也就是这么个闭包函数,外部函数有一个参数,实际上就是我们这里的目标函数x()
,闭包函数需要返回该目标函数,还要带上小括号,如果有参数,需要通过闭包函数的参数传入,谭厚在括号里写上传给目标函数
需要先在目标函数前@
一下,然后调用目标函数x()
的时候,需要把闭包函数中的参数都加上。也可以用*args可变参数。
在执行的之后,会先执行闭包函数外部函数的代码,从上到下。然后会开始执行闭包函数,随后闭包函数返回目标函数,传参以后,开始执行目标函数。
def wrapper(function): print('this is outter') def inner_wrapper(x,y,k): print('this is inner dunction') print('{}+{}={}'.format(x,y,x+y)) return function(k) print('ssss') return inner_wrapper @wrapper def x(k): print('this is x()',k) x(1,3,1) #执行 this is outter ssss this is inner dunction 1+3=4 this is x() 1
其实装饰器分为三类:
函数装饰器
就是普通的闭包函数
def use_logging(func):
def wrapper(*args, **kwargs):
logging.warn("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper
def bar():
print('i am bar')
bar = use_logging(bar)
bar()
输出:WARNING:root:bar is runningi am bar
带参数函数装饰器
两层闭包函数,最外层可以在@
里带上参数传入,第二层的参数是装饰器自动传入的函数参数,第三层就是装饰器执行的闭包
def use_logging(level): def decorator(func): def wrapper(*args, **kwargs): if level == "warn": logging.warn("%s is running" % func.__name__) return func(*args) return wrapper return decorator @use_logging(level="warn") def foo(name='foo'): print("i am %s" % name) foo() 输出: WARNING:root:foo is running i am foo
类装饰器
使用比较灵活,还可以依赖内部的__call()__
方法,装饰器附加以后,就会调用这个方法
class Foo(object): def __init__(self, func): self._func = func def __call__(self): print ('class decorator runing') self._func() print ('class decorator ending') @Foo def bar(): print ('bar') bar() 输出:class decorator runing bar class decorator ending
序列(列表、元组)、映射(字典)、集合
栈stack、队列queue、双向队列deque、链表linkedlist
使用format
格式化字符串
<>^
,分别表示左对齐、右对齐和居中。默认居左.4f .4
分别为浮点数的小数有效部分、字符串有效长度大概顺序是:{填充}{对齐}{长度}{类型}{如果有数字的千分位}
示例
'{:*^30,}'.format(12345)
'************12,345************'
>>> '{:*^30b}'.format(12345)
'********11000000111001********'
S.remove(e)
:移除元素e,如果不存在报错s.discard(e)
:移除元素e。如果不存在无所谓s.update(b)
:合并集合s.difference(b)
:s和b的差集s.difference_update(b)
:s和b的差集赋值给ss.intersection(b)
:s和b的并集s.intersection_update(b)
:s和b的并集赋值给ss.union(b)
:s和b的合集先进先出FIFO
import queue
a=eueue.Queue()
#入队
a.put(xxx)
出队
a.get()
双端队列
from collections import deque
a=deque([1,2])
#右边插入数据
a.append('x')
#向左添加
a.appendleft(yyy)
pop同理
去重
list(set(target))
根据索引去除
使用pop()。不指定元素默认弹出列表尾部元素
指定下标以后弹出指定下标的元素。
列表的+=操作实质上是调用了extends,而不是不可变对象那种先相加然后创建新对象释放原对象(相加赋值)
删除元素:
remove
del l[index]
pop()
字典的键只能是不可变对象。
使用update()合并两个字典
删除元素:
del d[key]
也可以删除指定键的项popitem()
:随机删除一项defaultdict
普通字典在访问不存在的key时会报错,defaultdict不会报错,会返回一个默认值,可以在初始化的时候设置默认值的类型。
如果是int,不指定默认值访问不存在的key的时候,返回0
from collections import defaultdict
d=defaultdict(int)
d=defaultdict(str)
d=defaultdict(list)
OrderedDict
有序字典,插入的数据按照key排序存放
其实除了列表推导式,还有字典推导式和集合推导式。
集合推导式可以自动去重
#列表推导式
>>> [x for x in 'sssdddaaa']
['s', 's', 's', 'd', 'd', 'd', 'a', 'a', 'a']
#字典推导式
>>> s=[(1,'1'),(2,'2'),(3,'3')]
>>> {key:value for key,value in s}
{1: '1', 2: '2', 3: '3'}
#集合推导式
>>> {x for x in 'asdfghhgfdsa'}
{'g', 'd', 'a', 'h', 'f', 's'}
计算100以内的素数,前面的循环室遍历所有数,后面是看对每一个因子取余,如果是合数说明肯定会有一个可以除尽,也就是余数为0,即如果后面的列表没有0,说明是素数
[x for x in range(2,100) if 0 not in [x%i for i in range(2,int(math.sqrt(x))+1)]]
可变对象(对象内容是可变的):列表、字典和集合。也就这三个可变了
不可变对象(对象内容是不可变的):整型、元组和字符串
不可变对象
python中变量存放的是对象引用,相当于所有变量都是引用,对于不可变对象来说,虽然对象的值不会变化,但是引用是会变的。
比如对于不可变对象整数10,如果有三个对象值均为10,那么这三个对象的id一致,10这个引用则为3。
如下代码,一个整数先赋值,然后进行加操作,此时加操作以后,i已经是另一个对象了,因为整型是不可变的,指向75的引用,原先73的引用计数为0,应该被垃圾回收
i=73
i+=2
再比如有如下两个操作,那么其实内存中只存在一个值为1的对象,x和y都是对这个的引用,x和y的id都是一样的。
x=1
y=1
此时如果x=2
,x的引用就会发生变化,内存中就会产生一个值为2的对象,引用发生了改变,此时x和y的id不一致了。
内存中值相同的对象只能存在一个,如果需要声明一个这个值的变量,就会给这个对象引用+1
可变对象
对象的内容是可以被修改的,对象内容发生改变时,变量的引用不会变。
修改对象的值,因为引用的地址一致,所以其他引用对象的值也会改变。
如下可以看到,对于可变类型,即便值一样,内存地址也不一样,内存中可以存在多个值相同的对象,地址值不同.
>>> a=[1,2,3]
>>> id(a)
2017258406664
>>> a=[1,2,3]
>>> id(a)
2017258389896
>>> a+=[5,6]
>>> id(a)
2017258389896
值的变化不会引起创建新对象,地址不会发生改变,只会改变地址中的内容或者地址扩增(执行列表相加)
但是如果给a重新赋值的话那么就会重新创建一个对象。
这里有几个小特例
如果是赋值,就都是引用,如果是声明新变量,如下所示有一些特例
python都是引用传递,对于不可变对象没什么问题,只会给对象引用+1,对于可变对象来说,传递就是真实的内存地址。
只允许引用传递其实是为了方便内存管理,因为是基于引用计数器的,比如赋值传参等操作都会给引用+1,有一个变量不再使用的时候,会-1,如果一个对象的引用计数为0,就会被回收释放。
但是解决不了循环引用的问题,使用了其他的GC方法(标记清除和分代回收机制)。
对于不可变对象,没有什么区别,主要用于可变对象
深复制:将待复制对象完全复制一份,改变原有的待复制对象不会对新的这个对象产生影响
浅复制,只能复制最外层的对象,复杂嵌套对象属于第二层+,不是最外层:
在一个内部函数中,对一个外部函数作用域的变量进行引用,而且外部函数返回值一般为内部函数,这个内部函数就是闭包。
闭包的一个作用是可以保存当前的一些运行环境,每次运行是能记住引用的外部作用域的变量的值。
如下,还有,python是可以返回函数的。也就是说result实质上是increment()函数,而不是start()结果。
因此可以通过添加小括号调用result指的函数得到内部函数的结果。
def start(x):
def increment(y):
return x+y
return increment
result=start(1)
print(result)
print(result(8))
#输出
<function start.<locals>.increment at 0x0000011B6F321598>
9
虽然可以访问为外部函数的变量,但是无法修改其值,否则会报错找不到变量
看如下示例,for里面是没有域的概念,先用for将三个内部函数添加到列表里,然后再用一个for分别执行列表里的每一个内部函数,本来以为是0,2,4,结果是4,4,4。为什么呢
因为在追加内部函数的时候,没有保存i的值,而是在第二个for,也就是执行内部函数的时候,才去取i的值,而此时i已经是2了,所以就都是4
f=[]
for i in range(3):
def add(x):
return x*i
f.append(add)
for temp in f:
print(temp(2))
解决方案就是再加一层,把add变成闭包,利用外层函数保存每次i的值
f=[]
for i in range(3):
def outter(i):
def add(x):
return x*i
return add
f.append(outter(i))
for temp in f:
print(temp(2))
最后有一个nonlocal
关键词,表明这个变量是一个外部函数的变量,这样的话每次调用内部函数的时候,其实x都是最开始的5.
def start():
x=5
def inner():
nonlocal x
x+=1
return x
return inner
s=start()
print(s())
print(s())
print(s())
print(s())
可以查看这个函数的闭包信息,start()closure
返回一个元组包含引用的外部变量
如果内部函数没有引用外部变量,不是闭包,则返回None
如果没有外部函数没有返回内部函数,不是闭包,会报错
def start():
x=5
def inner():
return x*2
return inner
s=start()
print(s())
for i in start().__closure__:
print(i,i.cell_contents)
#输出
10
<cell at 0x000002637BD27BB8: int object at 0x00000000771E6160> 5
实际应用大概有日志吧
Pytnon内部使用引用计数、保持和追踪内存中的对象,所有对象都有引用计数
计数增加的情况:
计数减少:
优点:
缺点:
垃圾回收机制基于引用计数,当一个对象的引用计数为0的时候,认为这个对象像是可以被回收的,会立刻回收。
基于引用计数的机制的缺陷呢,可以使用标记清除和分代回收机制完善。
但是对于重写了__del__()
方法的,好像不能自动回收,得手动del。
标记清除
分为两个节点,第一阶段是将系统中的活跃对象进行标记、第二阶段是回收不活跃的对象。
第一阶段呢,将内存中的对象根据引用关系,可以构造成一个有向图,对象是有向图的节点,引用关系是边。然后从根对象(root object,全局变量、调用栈、寄存器)开始遍历,可达的节点都标记为活跃对象。
然后对不活跃的对象进行回收。
但是标记清理不能一次性对所有对象进行扫描处理。
通过gc.get_threshold
可以获取到一个阈值(700,10,10)
,第一个就是标记清除的阈值。
缺点是即便有一小部分未标记,也要扫描所有对象然后标记清除
分代回收
存活的对象越久,越容易被引用,更不应该被清除,为了克服标记清除的这个缺陷,使用分代回收
一共分为0代、1代和2代。所有对象在第一次创建或者声明的时候都是第0代,执行一次标记清除留下来的对象代数+1。
在系统中有个参数设置,当先对第0代的对象进行扫描清除,一定次数以后,对0和1代进行扫描清除,再经过一定次数以后,对0、1和2代进行清除。这两次一般默认为好像都是10次。
或者当年轻一代满了,调用垃圾回收,讲仍然存活的放在中年代里,也就是第1代,以此类推
为了方便管理内存,python对于小于256字节的内存使用pymalloc申请,超过的使用malloc
然后对于整数,-5~256有预先分配好缓存,可以直接使用,如果两个变量都赋值5,那个id地址一样。但是假设一个整型数字内存被释放,这块内存在以后仍然只能申请整型,不能用于浮点数等
对于单个字符,也是同上
对于字符串,单个单词的话,创建好一个对象以后,会在内存中记录下来,可能比如哈希表,在第二次使用的时候,可以直接引用。
python的线程底层封装了Linux的POSIX Thread。
全局解释器锁相当于互斥锁,当这个线程在运行的时候,CPython解释器会锁住当前线程,阻止别的线程运行,当多个线程交替运行的时候,就造成一种并行的假象。
使用GIL主要和CPython有关,这里面使用了引用计数的机制,当有两个线程同时对一个变量进行引用的时候,可能会造成引用计数的竞争条件导致最终计数只会增加一,当某一个线程退出的时候,计数减1,可能会满足释放对象的条件,这时候第二个线程如果访问这个对象的时候,可能就会因为找不到然后报错了。
条件竞争漏洞(Race condition)官方概念是“发生在多个线程同时访问同一个共享代码、变量、文件等没有进行锁操作或者同步操作的场景中。”
总结一下使用GIL的两个主要原因:
同时还有这么一个概念check_interval
,会一定时间间隔去检查GIL的使用情况,如果达到间隔会强制当前线程释放GIL锁,避免一个线程长时间执行。
但是即便有着GIL,也还不是线程安全的,比如有一个全局变量n,多线程里面每次n=+1,输出n。这就不是线程安全
n+1的字节码其实如下,由四个语句组成,这四个是可以被打断的,而不是一个整体原语之类的,因此不安全。
>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL 0 (n)
LOAD_CONST 1 (1)
INPLACE_ADD
STORE_GLOBAL 0 (n)
还是需要自己用锁实现线程安全
n = 0
lock = threading.Lock()
def foo():
global n
with lock:
n += 1
对于单核来说其实这样的多线程没有优势,,多核CPU的时候,还是只有一个线程运行,而且每个线程在多个CPU交替运行,会造成CPU切换资源的浪费运行变慢。(在多线程会对GIL进行抢占,如果没有得到GIL的线程就会让CPU等待,不能合理利用多核心)
因为GIL的存在,使得多线程是假的,无法发挥多核心CPU的优点。
multiprocessing
模块支持多进程的使用,差不多完整复制了threading提供的接口并且把线程改为进程。每个进程有自己的GIL,不会出现竞争。
比如Apache服务器就是父进程监听,当有新的HTTP请求到来的时候,fork出一个子进程处理请求。
创建一个进程
Process([group [, target [, name [, args [, kwargs]]]]])
一些常用的函数:
python多任务—协程(一)
介绍就不说了
这里分为send和next两种。
send
python2里面可以用yield和send生产者消费者实现
通过send()方法可以把参数传递给yeild,可以强行修改yield的值,同时send是会有返回值的,返回值就是yield后面的值。如下代码,好像也可以用next
这里第一次需要传入None,因为第一次没有一个yield可以赋值。
然后这里send(n)就是可以传入一个参数n,然后消费者里面的n的值就不是后面r的值了,而是传入的n,这个r的值就是send的返回值
#-*- coding:utf8 -*- def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER]Consuming %s...' % n) r = '200 OK' def producer(c): # 启动生成器 c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER]Producing %s...' % n) r = c.send(n) print('[PRODUCER]Consumer return: %s' % r) c.close() if __name__ == '__main__': c = consumer() producer(c)
next
也是使用yeild生成器来实现切换,可以从一个函数切换到另一个函数,如下,先构造两个生成器,然后分别调用next()
方法就可以唤醒相应的生成器,执行yeild的操作,然后保存上下文,等待下次唤醒,实现相应的切换。
import time def task_1(): while True: print("--This is task 1!--before") yield print("--This is task 1!--after") time.sleep(0.5) def task_2(): while True: print("--This is task 2!--before") yield print("--This is task 2!--after") time.sleep(0.5) if __name__ == "__main__": t1 = task_1() # 生成器对象 t2 = task_2() # print(t1, t2) while True: next(t1) # 1、唤醒生成器t1,执行到yield后,保存上下文,挂起任务;下次再次唤醒之后,从yield继续往下执行 print("\nThe main thread!\n") # 2、继续往下执行 next(t2) # 3、唤醒生成器t2,....
结果如下,运行到yield关键字的时候就会暂停,等待下一次唤醒
这是python3.4出来的功能,内置了异步IO的支持。
调用get_event_loop()然后生成三个函数。
这里每个test就是一个协程函数,yield from可以方便调用另一个生成器,因为后者sleep也是一个协程,所以线程会中断执行下一个循环,当sleep返回以后,r就能得到返回值了(这里是None),比如这里可以进行一些IO操作,在这期间主线程不会等待,会继续运行其他协程。
#-*- coding:utf8 -*-
import asyncio
@asyncio.coroutine
def test(i):
print('test_1', i)
r = yield from asyncio.sleep(1)
print('test_2', i)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [test(i) for i in range(3)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
更新一下
#-*- coding:utf8 -*-
import asyncio
async def test(i):
print('test_1', i)
await asyncio.sleep(1)
print('test_2', i)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [test(i) for i in range(3)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
这其实分为两个,一个是基础的greenlet,另一个是比较高级的Gevent
greenlet
这个创建协程以后可以调用switch()
方法手动切换线程。
如下greenlet(func)
创建两个协程,参数就是需要执行的函数名,然后手动切换的话就直接调用switch即可。
from greenlet import greenlet import time def task_1(): while True: print("--This is task 1!--") g2.switch() # 切换到g2中运行 time.sleep(0.5) def task_2(): while True: print("--This is task 2!--") g1.switch() # 切换到g1中运行 time.sleep(0.5) if __name__ == "__main__": g1 = greenlet(task_1) # 定义greenlet对象 g2 = greenlet(task_2) g1.switch() # 切换到g1中运行
Gevent
是基于Greenlet实现的网络库,通过greenlet实现协程,当协程遇到IO操作或者网络访问等,会自动切换到另一个协程,等到IO结束以后,会再切换回来。是自动做的,保证一直有协程运行,而不是一直死等着IO操作。
相比于greenlet这个的优点就是可以根据执行的操作自动切换
这个好像不太涉及切换的问题,就相当于进程或者线程,创建以后就异步执行,然后直到结束
使用这个关键字可以定义一个协程函数,调用run_until_complete
把协程对象添加到事件循环里,然后异步执行,直到这个函数执行完成。
import asyncio
async def work(x): # 通过async关键字定义一个协程
for _ in range(3):
print('Work {} is running ..'.format(x))
coroutine_1 = work(1) # 协程是一个对象,不能直接运行
# 方式一:
loop = asyncio.get_event_loop() # 创建一个事件循环
result = loop.run_until_complete(coroutine_1) # 将协程对象加入到事件循环中,并执行
print(result) # 协程对象并没有返回结果,打印None
# 方式二:
# asyncio.run(coroutine_1) #创建一个新的事件循环,并以coroutine_1为程序的主入口,执行完毕后关闭事件循环
面向对象是相当于面向过程而言的,面向过程语言是一种基于功能分析的,以算法为中心的程序设计方法,而面向对象是一种基于结构分析的,以数据为中心的程序设计思想。在面向对象语言中有一个很重要的东西,叫做类。面向对象有三大特性:封装、继承、多态。
重载主要解决的是同名函数的不同参数类型和个数的问题
都是多态的实现,重写是子类对父类的已有的方法重新写一遍,需要保证方法名、参数以及返回类型和父类保持一致;重载是同名的方法拥有不同的参数列表,如参数的个数或者类型不同,甚至顺序不同,返回值的类型没有要求,可以不同可以相同,不能从返回值相同判断重载。
__init__(self)
:在实例化对象的时候会调用__str__(self)
:适合人阅读的,return的值可以使用print()打印__repr__(self)
:解释器可以阅读的格式,可以直接在终端执行对象名就可以看到这个函数返回值__call__(self)
:在把对象当成函数使用的时候回调用这个函数,同时不影响类正常的生命周期。如a=A() a(1,2)
,其中a(1,2)就会调用call方法__new__(self)
:第一个参数是当前类,返回值是一个类的实例,在init之前调用,返回的实例作为init的第一个参数直接命名一个和父类函数名一样的函数,就可以重写,然后默认调用子类这个。
在其中可以使用super().xxx()
调用父类方法。
class Animal:
def heshui(self):
print('动物正在喝水')
class Cat(Animal):
def heshui(self):
super().heshui()
cat = Cat()
cat.heshui()
>>>动物正在喝水
强制调用父类的私有方法,比如__heshui(self)
,直接调用会发现报错,因为这其实是私有的,主要是将其名字改为了_Animal__heshui(self)
,可以如下调用
super()._Animal__heshui()
然后如果子类定义了__init__(self)
方法,是不能直接调用父类的变量等属性,因为相当于重写了父类的构造函数,需要显式调用一下父类的构造函数才行,如下
super().__init__()
{类名}.{方法名}()
调用使用,第一个参数是类本身{类名}.{方法名}()
调用使用,不需要有类的参数客户端第一次请求数据后,将返回的数据保存到缓存数据库中。
分为强制缓存和对比缓存。
强制缓存在使用的时候如果没过期,可以直接拿来用。如果过期了会像服务器申请新资源。
对比缓存不管中没中缓存,都需要使用从数据库获取到的缓存数据标识发给服务器进行验证,然后返回数据使用。
强制缓存
浏览器请求服务器资源的时候,服务器会将缓存的一些信息规则一块返回,比如Expires/Cache-control
,前者是HTTP/1.0的,后者是1.1的,前者设定一定时间后会过期,但是依赖于服务器的时间,是服务器端返回的过期时间,如果客户端时间和服务器端差得多,那就出事了
cache-control
呢,有好几个参数,可以选择缓存还是不缓存,缓存的时间等。
对比缓存
有两个标识,(Last-Modified / If-Modified-Since) (Etag / If-None-Match)
一个是最后修改时间(Last-Modified / If-Modified-Since)
需要进行比较判断才能使用缓存,在使用的时候,浏览器先从缓存数据库获取到缓存标识,然后发送给服务器,服务器判断是否过期,如果没有,返回304头部,没有实体部分,速度很快,客户端直接从数据库读取缓存。
第一次请求资源服务器会返回该资源的最后修改时间放在last-Modified
字段上,再次请求后,会把这个值放在If-Modified-Since
字段里,服务器会比较这个最后修改时间,如果资源修改时间小于等于这个(也就是说没过期),就是304;如果大于(过期了)就是返回200的状态码和资源实体部分。
还有一个是标识(Etag / If-None-Match),这个优先级更高
服务器响应会告诉客户端该资源在服务器的唯一标识,再次请求后,会将该标识和被请求资源的标识进行对比,不一致返回200+实体部分,否则304
状态码:
HTTP1.0:
HTTP1.1:
range
字段,支持断点续传HTTP2.0:
多路复用是通过帧实现的,2.0中将数据包体积再次减小至多个帧,模拟流的形式。
其中Type共有十种类型标识每个帧,通过ID标识这个帧所属的流,实现全双工非阻塞通信。
帧类型大概如下几种:
有通用首部字段、请求首部字段、响应首部字段、实体首部字段
通用头部
cache-control
:控制缓存类型Date
:创建报文的时间Accept
:接受的内容类型Accept-Charset
:接受的字符编码Accept-Encoding
:接受的编码格式Accept-Languge
:接受的语言Cache-Control
:缓存机制Content-Length
:请求体长度Content-MD5
:对请求体内容MD5编码Content-Type
:请求体MIME类型Content-If-Modified-Since
:更新时间,如果更新时间到服务器接受请求的时间资源没有发生变化,User-Agent
:用户代理字符串If-None-Match
:客户端ETAG,服务器端会对比ETag,如果没有变化返回304X-Csrf-Token
:csrf防止跨站请求伪造X-Request-ID
:标识客户端和服务器端的HTTP请求响应头部
Cache-Control
:缓存机制是否可以缓存,单位是秒Content-Encoding
:响应体的编码格式Content-Length
:响应体长度Content-Language
:响应体语言Content-MD5
:对响应体内容进行二进制MD5 base64编码Etag
:特定资源的版本号,比如摘要Last-Modified
:资源最后修改的时间Content-Type
:响应体MIME类型对于静态资源,会通过一个地理CDN技术,选择一个距离用户最近的一个服务器去获取相应资源下载。
然后可能会通过反向代理服务或者负载均衡服务。
反向代理:实质上对用户来说就是目标服务器,反向代理使得用户不会感知到内网的存在,比如正常应该是经过网关,然后再去跳转到相应的内网服务器,多个步骤,有这个以后就会看起来只有一步,降低网络和服务器的一个负载使用。代理服务器还可以配置比如防火墙之类的功能进行一个安全防护
负载均衡:在上一步之后,会根据请求流量的特征以及当前系统中每个服务器的负载情况等数据,将请求分发到合适的服务器进行处理然后返回结果。
HTTP主要有三个缺点:
基于以上几个问题,可以通过增加一些功能,比如加密传输和身份认证等,这就是HTTPS
服务器端向CA申请证书,避免中间人攻击,证书包括:证书内、做证书签名算法以及签名
签名:
CA使用证书签名对证书呢容进行哈希运算
然后使用私钥对哈希加密得到签名
验证:
浏览器得到证书,获取到证书内容、证书签名算法以及数字签名
使用公钥解密签名
使用算法对证书内容进行哈希运算
对比上面两个的值,如果一致说明可信
HTTP:
HTTPS:
HTTPS是在应用层和传输层中间的,在HTTP和TCP中间加了一层SSL(安全套接字)和TLS(传输层安全)协议,通过身份认证和加密传输保障数据传输的安全性
Client Hello
,告诉服务器端一些信息,如需要访问的域名、支持的加密(哈希)算法,一个随机数等Server Hello
,选择一个加密算法;随后并且发送随机数和自己的证书,证书中则包括了密钥的公钥、访问的网址、证书颁发机构以及过期时间等;其中私钥保存在服务器TLS
实现)。如是否过期、颁发机构、是否是自己访问的网址以及是否合法证书而不是假的。如果通过的话,对随机数,使用公钥进行加密(RSA加密),或者DH也行好像,发送给服务器端SSL和TLS其实是一个东西对称加密的东西,其实是一个东西,在最之前是SSL(安全套接字协议),经过几个版本的发布,SSL3.0升级以后命名为TLS1.0,是基于SSL之上的。
核心是非对称加密,使用公钥和私钥进行加密。
实际中,对称加密安全性完全依赖于密钥的保密性,如果通信的时候密钥被截获了,安全性就不能够被保证了,因此可以使用非对称加密先对密钥进行一个加密传输,然后双方使用私钥进行解密得到密钥,就可以用于对称加密了。
但是有这么个问题:如果使用SSL/TLS需要知道对方的公钥,公钥需要在网络上直接进行通信传输,那么如果被截获了,就可以出现中间人攻击
A和B通信,B给A发自己的公钥,但是被C截获了,C把自己的公钥发给了A,A以为这是B的,就正常用;然后给B发送数据包,被C截获,使用C的私钥解密就可以获得数据包;这其中AB都以为是和对方直接通信,然而全部被C知道了
因此就需要有一个可信的第三方来保证,CA
如何验证我访问的服务器就是正确的服务器呢。
证书是由CA生成颁发的,加密需要使用到CA的私钥。
验证,证书里面有CA的信息
如果证书被冒用了,虽然校验可以通过,但是发送的消息是经过加密的,假网站没有私钥不能解密,保证安全。
解决方法就是嵌入CA证书
cookie:
session:
幂等函数或者幂等方法,指的是不管运行一次还是运行多次,结果都是一致的,不会改变系统状态,也不会对系统造成影响。
TCP
UDP:
TCP:数据完整性要求较高的,比如FTP文件传输,邮件发送接收等
UDP:响应要求高的,比如实时通信、网络视频等
TCP是传输层的协议,解决的是数据如何在网络中传输。TCP三次握手建立连接以后,连接会一直存在,直至通信结束以后才会断开连接。
而HTTP是应用层协议,解决如何包装数据,采用请求-响应,是基于TCP的,HTTP/1.0中每一次给服务器端发送请求都会建立一个新的TCP连接,HTTP1.1中服务器端可以在一个链接中处理多个请求,提高了利用率降低开销,是一种短连接。
如果想维持连接关系,需要定时去发送数据包证明自己在线
如果高并发的时候,服务器端会存在大量的TIMEWAIT状态的连接,会导致端口等资源不足从而无法正常响应连接。
可以通过keep-alive参数设置连接持续的秒数,会一直持续这么久
序号其实是上一个数据包最后的一个字节数。
发送方发送一个序号为500的大小为10字节的数据包,接收方收到以后,会返回一个确认包,这个确认包的确认好ack=510,表明接收到了,而且期望下一个数据包的序号是510.
流量控制。窗口是缓存的一部分,暂时存放字节流,收发双方各有一个窗口,通过TCP报文的窗口字段告诉对方自己的值,便于对方设置自己的窗口大小。
一些补充:
如果第二次报文丢失,客户端会一直等待链接,但是服务器会认为连接建立,开始传输数据,但是此时客户端仍在等待链接,因此会丢弃传来的数据,然后服务器端会超时重传,造成死锁。
其实这是由TCP的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力,不然容易出现丢包的现象
如果第一个SYN包丢了,服务器端不知道客户端发来的请求,不会返回ack,客户端会进行超时重传,一共有三次,分别是5.8s、24s和48s,一共76s左右,大多数系统设定最长等待时间为75s
如果是服务器端返回的SYN+ACK丢了,客户端就不知道服务器端收没收到数据,仍然会进行超时重传。此时服务器端进入了同步已接收状态,收不到客户端返回的普通ack,也会进行重传SYN+ACK,超时时间分别是3s、6s和12s,linux默认五次好像,如果还没有回应,关闭连接。客户端也会重传SYN,服务器端收到以后立刻返回SYN+ACK
如果是最后客户端返回的普通ack丢失了,服务器端会因为不知道客户端收没收到SYN+ACK而重传,但是此时客户端已经进入连接已建立状态,会立刻发送数据包,而服务器端仍处于同步已接收状态,有两种说法:
三次握手的时候收到SYN请求以后,可以直接返回SYN+ACK报文,同步和确认。而关闭的时候,客户端发来的连接释放报文时,客户端没有数据发送了,但是服务端可能还有数据需要传输,无法立即关闭,因此需要先返回一个确认报文ACK,告诉客户端收到了FIN报文,但是服务器可能还需要发送数据,随后等服务器端处理完后,继续返回FIN报文告诉客户端可以关闭了
四个报文发送完成后理论上是可以关闭了,但是实际网络状况并不理想,可能会导致客户端无法成功发送ACK给服务器端,此时服务器端会继续发送FIN报文,TIME-WAIT就是避免ACK丢失无法到达服务器端,为了重发,等待重新发送ACK的,2MSL就是一个发送和一个回复所需的最大时间
服务器端会在超时后,继续返回FIN数据包告知客户端,客户端此时处于等待关闭状态,等待两个报文最长寿命时间,客户端要保证极端情况下也能接受到服务器的超时重发FIN报文。但是如果真的没有回应,服务器端会始终处于最终确认状态,而客户端已经关闭。
此时如果有新连接,会返回RST报文给客户端,因为出错连接过程就会被终止。
还有可能会导致,最后的ack延迟,连接已经关闭了而且有了新的链接进来,这时候丢失的ack收到了,会对新连接造成错误。
两个时间是允许丢失一次,因为网络中连续丢失两个的可能性太低了,所以避免不必要的开销,就两个MSL
TCP报文段的序号seq解决乱序问题,ACK确认包解决丢失的问题。
丢包的问题类似于TCP可靠连接?
ddos。
客户端发送连接请求报文给服务器端,服务器端返回SYN+ACK报文,此时客户端不会返回确认报文,因此服务器端会持续返回ACK报文,造成资源的消耗。
当短期内存在大量的这种半连接时,就可能会导致服务器端资源枯竭从而崩溃。
而好像三次握手失败以后,不会重发ACK,会发送RTS数据报,直接进入CLOSED状态,避免SYN泛洪
服务器端没有收到对于SYN+ACK的确认后,会尝试重传,超时3s、6s以及12s重传,如果还没有回应会直接关闭连接,这个次数可以在tcp_synack_retries修改
解决措施:
详见计网知识点文章吧
TCP存在有保活定时器。keepalive时服务器每收到客户端传来的信息都会重置定时器,当一定时间没有收到客户端消息时,一般是两个小时,服务器端会发送探测报文,之后每隔75s发送一次,连续十次没有回应后,任务客户端出现故障,连接关闭
(1)首先,每个主机都会在自己的ARP缓冲区中建立一个ARP列表,以表示IP地址和MAC地址之间的对应关系。
(2)当源主机要发送数据时,首先检查ARP列表中是否有对应IP地址的目的主机的MAC地址,如果有,则直接发送数据,如果没有,就向本网段的所有主机发送ARP数据包,该数据包包括的内容有:源主机IP地址,源主机MAC地址,目的主机的IP地址。
(3)当本网络的所有主机收到该ARP数据包时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的IP和MAC地址写入到ARP列表中,如果已经存在,则覆盖,然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址。
(4)源主机收到ARP响应包后。将目的主机的IP和MAC地址写入ARP列表,并利用此信息发送数据。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。
TTL是指生存时间,简单来说,它表示了数据包在网络中的时间,经过一个路由器后TTL就减一,这样TTL最终会减为0,当TTL为0时,则将数据包丢弃。
这样也就是因为两个路由器之间可能形成环,如果没有TTL的限制,则数据包将会在这个环上一直死转,由于有了TTL,最终TTL为0后,则将数据包丢弃。ping发送数据包里面有TTL,但是并非是必须的,即是没有TTL也是能正常工作的,traceroute正是因为有了TTL才能正常工作,ifconfig是用来配置网卡信息的,不需要TTL,netstat是用来显示路由表的,也是不需要TTL的。
稳定排序:
不稳定排序:
空间大资源充足的时候,如果要求时间效率,可以使用归并排序
快速排序的时间是最省的,但是最坏情况下不如堆排序和归并排序
小数据的话快速排序不如插入排序
当数据基本有序或者数据较少的时候,插入排序不错,冒泡排序也不错。对于基本有序的数据,插入排序的时间是快速排序的一半
堆排序适合海量数据,比如一百万个数里面找到前1000大个数字,使用小顶堆
时间复杂度:
时间复杂度:
分治思想,先分,对半分,直到分为两两一组,然后开始治,每两个合并,四个合并,八个合并等等
时间复杂度:
一趟是O(n),要进行log2n趟,因此是:O(nlog2n)
O(n2)
快速排序就是如下
先选择一个基数,然后先从右往左找到第一个比基数小的数,从左往右找到一个比基数大的数,交换两者,然后继续找,直到左右相遇,这个相遇的地方和基数交换,此时一轮排序完成。
然后以现在的基数位置分割左右两部分,递归分别排序
平均复杂度O(nlogn),最坏情况O(n2)
利用堆这种数据结构进行排序,属于选择排序的一种,最好最坏的时间复杂度都是O(nlogn),是不稳定排序。
是一个完全二叉树
两步
前缀和
前缀和就比如说是这种题型,计算一个序列的和,如第i-j项的和。
d[i]表示前i项,即[0,i-1]的和,求[i,j]的和只需要d[j+1]-d[i]就可以得到了
创建一个n+1的数组,然后i=i-1就行
preSum = new int[nums.length + 1];
// 计算 nums 的累加和
for (int i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
差分数组
当频繁对数组的数据需要进行修改的时候,就不好用前缀和了,需要使用差分数组
差分数组里,d[i]=num[i]-num[i-1],表示当前数和前一个数的差值
int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
第一个数树原始数组的数,可以根据差值反推出原数组。diff[i]表示当前res[i]和前一个数res[i-1]的差值
int res=new int[diff.length];
res[0]=diff[0];
for (int i=1;i<diff.length;i++){
res[i]=res[i-1]+diss[i];
}
这样的话,如果想给[i,j]加上3,只需要d[i]+=3,d[j+1]-=3
哈希是一种键值的数据结构。
生成哈希的方法有
如果有多个关键词的哈希值重复了,就叫哈希冲突。
冲突的解决主要有两种:开放地址法和链地址法
一致性哈希算法原理
普通分布式集群的话不能较好地处理请求与服务器之间的映射关系,某个请求由固定服务器处理,无法负载均衡。可能会存在有的太忙有的太闲,如果有宕机了,可能还会导致无法处理请求。
在分布式中用的比较多,解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题。利用哈希算法对请求和服务器进行动态映射,可以应对节点宕机的情况,会通过算法将请求重新分配到可用机器上。
这里以一个分布式memcached服务器集群为例。
如果此时添加一个节点,受影响的只有从这个节点逆时针方向到第一个节点中间的哈希值数据请求
找规律其实,可以用10或者20模拟,然后会发现最后剩下的数是包含因数2最多的数
感觉还是个找规律的题,每次
Floyd算法和迪杰斯特拉算法
Floyd:
解决给定的加权图中顶点间的最短路径的一种算法。
给定一个二维数组map[][]保存从节点 i 到节点 j 的最短路径。
从 i 到 j 有中间可以经过数个额外节点
对于每个节点k,如果map[i][j]>map[i][k]+map[k][j]
,那么map[i][j]=map[i][k]+map[k][j]
。意思就是说如果i和j经过k节点的路径最短,那么更新这个最短路径,以此类推
时间复杂度:O(n³),空间复杂度:O(n²)
迪杰斯特拉:
用于计算一个节点到其他节点的最短距离,采用了广度优先遍历的思想,在使用前需要指定一个起始点。
用位图?
一个int4个字节,32位。先QQ/32
找到在数组的某个数,然后对32取余,找到某个比特位,
可以采用桶排序。
分别把数据放在文件中,然后依次读取文件。
有一个 1GB 大小的文件,文件里每一行是一个词,每个词的大小不超过 16B,内存大小限制是 1MB,要求返回频数最高的 100 个词(Top 100)
内存有限无法全部读取到内存中,分治,把文件分割成若干个1M的小文件。
遍历大文件执行hash(x)%5000,分成5000个文件,如果均匀的话应该每个文件200kB,否则就继续细分。对每个小文件,使用哈希表统计词频,
一个IP地址32位,大概4G,不能全部加载到内存里,分治
取哈希,hash(x)%1024,存储到1024个文件里,每个文件大概4M个IP地址
然后用哈希表求每个文件里的IP次数,再取每个文件里的第一高频,就只需要比较1024个数。
同样的题目:这里有64匹马,八个跑道,几次找到最快的四匹马。
设ABCDE
先来五次:每组分别跑一次,得到每组的组内顺序,去掉每组的最后两名,留下A1,A2,A3…E1,E2,E3
再来第六次:取每组第一名跑一次,A1,B1,C1,D1,E1,假设结果就是这个顺序,则可以去掉D和E组的所有,以及C3,C2,B3。留下A1,A2,A3,B1,B2,C1
第七次:此时已知A1最快了,只需要比较剩下五匹,找出最快的两个,就分别是2,3名了
python版本和java版本
import java.util.*; public class Solution { ArrayList<ArrayList<Integer>> result=new ArrayList<>(); public ArrayList<ArrayList<Integer>> permute(int[] num) { boolean[] dp=new boolean[num.length]; dfs(num,0,dp,new ArrayList<Integer>()); return result; } void dfs(int[] num,int index,boolean[] dp,ArrayList<Integer> res){ if (num.length==index){ result.add(new ArrayList(res)); return; } for (int i=0;i<num.length;i++){ if (!dp[i]){ dp[i]=true; res.add(num[i]); dfs(num,index+1,dp,res); res.remove(res.size()-1); dp[i]=false; } } } }
class Solution: def permute(self , num: List[int]) -> List[List[int]]: # write code here self.result=[] dp=[False] * len(num) self.dfs(num,0,dp,len(num),[]) return self.result def dfs(self,num,index,dp,length,res): if index==length: temp=copy.copy(res) self.result.append(temp) return for i in range(length): if not dp[i]: dp[i]=True res.append(num[i]) self.dfs(num,index+1,dp,length,res) res.pop() dp[i]=False
def quickSort(List,left,right): if left<right: left_index=left right_index=right mid=List[left_index] while (left_index<right_index): while (left_index<right_index and List[right_index]>=mid): right_index-=1 if left_index<right_index: List[left_index]=List[right_index] while (left_index<right_index and List[left_index]<=mid): left_index+=1 if left_index<right_index: List[right_index]=List[left_index] List[left_index]=mid quickSort(List,left,left_index-1) quickSort(List,left_index+1,right) List=[5,4,3,2,1] quickSort(List,0,len(List)-1) print(List)
def mergeSort(List,left,right): if left==right: return mid=int((left+right)/2) mergeSort(List,left,mid) mergeSort(List,mid+1,right) merge(List,left,mid,right) def merge(List,left,mid,right): temp=[] i=0 left_index=left right_index=mid+1 while left_index<=mid and right_index<=right: if List[left_index]<=List[right_index]: temp.append(List[left_index]) left_index+=1 else: temp.append(List[right_index]) right_index+=1 while left_index<=mid: temp.append(List[left_index]) left_index+=1 while right_index<=right: temp.append(List[right_index]) right_index+=1 i=0 while left<=right: List[left]=temp[i] left+=1 i+=1 List=[5,4,3,2,1] mergeSort(List,0,len(List)-1) print(List)
动态规划和中心扩散,中心扩散就是从0开始遍历,然后以此向左向右找和中心值一样的,然后判断左是否等于右,如果是,则是回文记录,如果不是,则不是
class Solution: def longestPalindrome(self, s: str) -> str: n=len(s) dp=[[False]*n]*n for i in range(n): dp[i][i]=True start=0 max_len=0 for length in range(2,n+1): for left in range(n): right=left+length-1 if right>=n: break if s[right]!=s[left]: dp[left][right]=False else: if right-left<=2: dp[left][right]=True else: dp[left][right]=dp[left+1][right-1] if dp[left][right] and length>max_len: max_len=length start=left return s[start:start+max_len]
主要来源
《面试必备知识》——数据库129问
主要有以下几个部分
执行过程
对于插入语句
更新语句
使用数学的关机模型来描述实体间的关系。
优点:
缺点:
主要指非关系型的、分布式的而且一般不需要满足ACID原则的,如mongoDB、redis等。
主要分为键值存储数据库(redis)、列存储数据库(应对分布式海量数据,仍然有key指向多个列)、文档型数据库(半结构化的数据如json之类的)、图形数据库
缺点是约束较少,只能存储一些简单数据。
其实也就是数据库设计建表的原则
第一范式:要求每一列数据都是原子不可分的
比如如果一个字段叫家庭信息,存储了几口人和住在哪,这就可以把他分开为两个字段,违背了第一范式
第二范式:属性值必须完全依赖于主键
确保每一列都和主键相关,而不能和主键部分相关(对于联合主键,要和所有主键相关)
比如对于一个订单表,有订单号和产品号个主键,同时有产品数量、名称、订单价格和订单时间。数量和名称就是依赖于两个主键的,但是订单时间和金额这个字段只和订单号有关,而和产品号这个主键没关系,所以不符合这个原则。
需要将时间和金额拆分成另一个表
第三范式:所有的非主属性不依赖于其他的非主属性
每一列数据都需要和主键直接相关,而不能间接相关
比如学生表里,学号是主键,还有老师性别和年龄字段,这个就和学号不直接相关,违背了。
数据库维护着某些满足特定算法查找的数据结构,能够以某些方式指向数据,利用这个数据结构进行高级查找,就是索引。
数据库联合索引
使用索引可以提高数据库的检索速度。
如何建立索引
适合:
不适合:
主要类型有如下几种:
优点:
缺点:
在联合索引中,如果SQL语句用到了联合索引的最左边,那么就可以用联合索引进行匹配。
比如某表有索引(a,b,c),基本只有匹配上才能匹配索引。
但是碰到><between like
会停止匹配。
比如对于select * from t where a=1 and b>1 and c =1;
这样a,b可以用到(a,b,c),c索引用不到,因为b>1这里停止了不匹配
但是如下可以匹配到,因为优化器会进行优化,改变一下顺序,就可以匹配a,b,c了
select * from t where a=1 and c=1 and b=1;
//优化为
select * from t where a=1 and b=1 and c=1;
对于like,百分号%在左不走索引,在右走索引,即A可以索引,B不可以。
A:select * from student where 'name' like '王%'
B:select * from student where 'name' like '%小'
对于索引进行计算不走索引
B给age+8了做运算,所以走不了索引
A:select * from student where age = 10+8
B:select * from student where age + 8 = 18
对索引使用函数不走索引
A对索引列用了函数,不走索引
A:select * from student where concat('name','哈') ='王哈哈';
B:select * from student where name = concat('王哈','哈');
最后不等号也不走索引
唯一索引和主键索引都要求是唯一。
一张表可以有多个唯一索引,但是一般只有一个,而且允许为空。
主键是一个特殊的唯一索引,一张表最多一个主键索引,可以没有,值不得为空。
数据结构都是B+树
聚集索引
数据行的物理顺序和列值(一般是主键的列)的逻辑顺序相同,一个表只能有一个聚集索引。
比如id在前面的数据实际存放的内存地址空间也靠前。
叶子节点存放着所有的数据。
速度上会更有优势。
非聚集索引
数据行的物理顺序和列值的逻辑顺序不同,一个表可以有多个非聚集索引。
其实吧,聚集索引好像对应的是二级索引。
聚集索引的叶子节点存放的是数据的值,二级索引的叶子节点存放的是主键
在进行统计的时候,server会维护一个count变量,指定参数不为NULL的话,就+1.
count()=count(1)>count(主键)>count(字段)
比如count()
select count(*) from t_dept;
如果是*相当于参数为0,所以和1是一样的。
在遍历的时候不会去比较任何一个字段的值,因为参数是1,不是字段,所以只要有一条数据就会+1.不需要获取数据的值,所以比主键遍历会快一点
对于字段进行遍历的时候,如果没有二级索引,就会默认去使用主键索引,如果有就有二级索引。因为二级索引其实比主键索引更矮胖IO次数更少。
对于count(字段),是最慢的。
因为它会进行全盘扫描
为什么要通过遍历的方式呢,如果是MyISAM引擎,每个表会有meta元属性,维护一个变量eow_count,直接读取就可以了,复杂度O(1)。
InnoDB的话,因为事务的存在,结果可能不准确所以得遍历
二分查找
使用数组存储索引的话,通过每次对比mid,如果小于就去左半边继续找,大于就去右边继续找,以此类推。
时间复杂度会降为O(log n),但是每次需要额外计算一次中间节点
二分查找树
二分查找时间复杂度可以,但是如果插入数据的话,就比较麻烦,需要把它后面的数据依次移动一位。如果数据储存在硬盘上,这个移动时间会非常慢,相比于内存可能慢几万几十万倍。
因此就需要一个非线性的天然适合二分查找的结构,基于上面的二分查找,把所有的中间节点从中间依次向两边提起来,构建一棵树。
特点就是,节点左子树的所有值都小于该节点,右子树所有节点的值都大于当前节点值,比较好的解决了插入数据的性能问题。
但是当每次插入的时候都是最大的元素的时候,就会退化为一个链表,查找效率降为O(n)。
因为树是存储在磁盘中,每个节点的查询都需要进行一次磁盘IO操作,树越深,IO操作数越多,查询时间就越慢。
而且不利于范围查找,只能精确查找,也不适合。
自平衡二叉树(AVL树)
AVL在二叉查找树的基础上,添加了一个约束条件,要求左右子树的高度差不能超过1.这样查询的复杂度一直会维持O(log n)。在每次插入数据的时候都会修改相应的节点去满足这个条件。
但是不管什么二叉树,插入的数据越多都会导致树越深,那就会查询速度很慢,当改为三叉树或者m叉树,显然一定程度上会降低深度,减少搜索次数。
B树
因此提出了B树,B树实质上是一个M叉树。但是B树每个节点都需要保存索引信息和数据,而用户记录数据的大小可能远超过索引数据,需要更多IO操作来读取到有用的索引数据。
而且比如我们搜索到最后一层找到需要的数据,但是上面几层节点的数据也被加载到内存里了,而我们只需要他们的索引即可,这样就会浪费时间和内存资源。进行范围查询的时候需要使用中序遍历,也很慢。
B+树
因此提出了B+树
B树和B+树
B树查找波动较大,因为可能有的数据在第一个节点就找到了,可能有的数据需要找到叶子节点才会找到,而B+树只有找到叶子节点才能找到数据。
对于相同的数据量来说,因为B+树的中间节点只保存索引信息,所以可以存储更多的叶子节点数据,同时也会更加矮胖,查找的IO次数就越少
B+树删除数据的时候,直接删除对应的叶子节点就好了。B树删除的话可能会导致树的结构发生比较大的变化
查询的话,都是从根节点进行查找,但是B+树还有一个链表的特性,叶子节点通过双向链表连接起来,对数据的查询非常有帮助,比如如果需要找到在这个数据的下一个或者后几个数据,直接从叶子节点往后遍历即可,不需要重新从根节点遍历
MyISAM引擎的B+树叶子节点存放的是用户指针(数据的物理地址),指向实际的数据
InnoDB则指向的是具体的数据值
B树:平衡多路查找树,由平衡二叉树演化而来。
B+树由B树优化而来。
B+树特点:
B树和B+树相比:
用B+不用哈希
自增自动增加,保证了顺序,不需要移动相关叶子节点等,提高性能。
读
唯一索引在读取到数据后直接返回结果
而普通索引在读取之后需要继续完后判断是否存在相同的索引,这里如果索引结构是B+树的话,只需要往后移动一位即可判断是否有相同的索引,因此性能差异不大
写
唯一索引需要先判断是否有重复值,需要先随机访问到数据页,没有插入缓冲,性能较差
普通索引可以使用插入缓冲,几个操作一起执行,一起对数据页进行修改
主键索引会创建一个聚集索引,叶子节点保存的是数据的值
普通索引的叶子节点则保存的是字段的主键。
在使用普通索引进行查询的时候,其实需要搜索两个B+树,先通过普通索引的叶子节点找到对应的主键值,然后利用这个主键值,在聚集索引中进行查找,找到叶子节点中的数据值。
以上的步骤叫<font color="red>回表。
如何避免回表
尽量使用聚集索引查询
通过索引覆盖的形式。将这个字段添加到联合索引里面,或者说把这个字段也弄成索引
存储引擎分为MyISAM、InnoDB和MEMORY。
InnoDB(聚集索引)、MyISAM(非聚集索引)
其中有这个几个主要的
特性 | MyISAM | InnoDB | MEMORY |
---|---|---|---|
存储限制 | 有 | 64T | 有 |
事务安全 | 不支持 | 支持 | 不支持 |
锁机制 | 表锁 | 行锁 | 表锁 |
支持索引 | 全文索引 | 集群索引 | 哈希索引 |
数据压缩 | 支持 | 不支持 | 不支持 |
空间使用 | 低 | 高 | 无 |
内存使用 | 低 | 高 | 中 |
外键 | 不支持 | 支持 | 不支持 |
总结一下: |
本质上是一个虚拟表,行列数据来自于其他基本表,在引用视图的时候动态生成。
使得使用者只能看到视图所定义的数据,看不到基本表中的所有数据,提高了安全性,比如员工工资保密,不应该给其他人看的。
特点:
在一个用户修改数据的时候,可能会有其他用户也进行相关操作,为了保证数据从一个一致性状态顺利转为另一个一致性状态,引入事务的概念。一个事务其实就是包含了对数据库操作的一个序列,要不然全部执行,要不然都不执行。
事物具有四个特点ACID:
会出现如下三种情况:
定义了四种隔离级别。
多版本并发控制可以解决不可重复读,间隙锁解决幻读。
锁粒度分为行锁和表锁,表锁管理开销小,允许并发量夜宵写入数据的时候把整个表锁起来,行锁支持大并发。
MyISAM和MEMORY只支持表锁
InnoDB支持表锁和行锁
InnoDB行锁针对索引字段,如果update或者delete的where里面索引用不成,就会变成表锁了。
在操作数据之前先执行加锁。行锁,表锁,读锁,写锁等,都是在做操作之前先上锁,悲观锁的实现往往依靠数据库本身的锁功能实现。
开启事务后,所有增删改查都会给对应的数据行加锁(这是基于索引的,如果没有索引,上表锁)
InnoDB引擎下使用多版本并发控制mvcc实现并发,不会上锁。
在mysql里面悲观锁的调用要加上for update
如select * from db_stock where goods_id = 1 for update
事务在执行着一条语句的时候,就会给这条记录加一个锁,直到执行完成以后,会释放锁。在这个过程中,如果其他事务正常读取数据,不会有影响,但是如果有类似for update
的命令,那么这个事务需要在当前事务修改完以后执行这条语句。
悲观锁可以有效保证事务的顺序性和一致性,但是对于高并发的场景并不适用,再给一套数据加锁以后,其他事务只能进行查询操作;而且如果加锁时间过长,其他事务等待的时间也很长。
因此引入乐观。
对数据进行操作前,总是认为最好情况,认为不会有其他事务修改数据,因此不上锁。而是在修改完数据后进行判断数据是否被需改过,如果和预期的结果一致则正常更新。
可以使用版本号机制和CAS机制实现。
CAS
是一种无锁的思想,所有线程对数据库的访问操作都是没有限制的,如果存在冲突的话,使用一种CAS(比较交换)的技术鉴别线程冲突,如果检测到冲突,就重试直到没有冲突。
它包含了三个参数位V(读写的内存位置)、A(旧的预期值)、B(新的值)
机制是在执行语句以后,只有当V的值等于A的值的时候,才会修改为B的值。
如果发现V不等于A就会一直循环。
缺点是:
适用于读多写少的场景。
版本号机制
版本号机制就是说,加一个字段叫版本号,每次读取数据的时候,还会记录下对应版本号,更新的时候对比版本号,如果一致的话说明数据没有被更改,然后修改数据,版本号+1。如果版本号不一致,重新进行查询操作
还有两种锁
select … LOCL IN SHARE LOCK
SELECT … UPDATE
意向锁是不能手动添加的,是引擎自己维护的,加排他锁和共享锁之前,InnoDB会获取该数据行所在的数据表对应的意向锁。
比较重要的日志主要有三个,binlog、redo log和undo log
重做日志
是InnoDB特有的日志,使得MySQL有崩溃恢复的能力,如果数据库挂了,InnoDB会使用redo log恢复数据,保证持久性和完整性
数据库中的数据都是页为单位的,查询一条记录以后,会从硬盘上读取出相应的页,加载的数据叫做数据页,放在Buffer pool里面,之后就会从pool里面加载,不会再去重新读取硬盘,减少IO降低开销。
在更新表的时候如果Pool里面有存在的数据,就会将数据在这里面更新。然后将这个修改写在重做日志缓存中,之后刷新到redo log里面。
刷盘时机
有一个参数用来控制刷新数据的机制。
还有个单独的线程,每隔一秒就会把redo log buffer写入到page cache然后调用fsunc刷盘。就是每隔一秒刷新一下。这样
日志文件组
一般都是几个文件在一起组成变成一个日志文件组,比如四个1G文件组成一个4G的。
是一个环形数组,一个写满以后写入下一个文件。
使用write pos和checkpoint属性,分别记录当前位置和擦除位置。
每次redo写入文件,write pos后移。恢复数据时候会清空redo记录,把checkpoint后移。
两者中间的空闲部分可以用来记录数据,当write追上checkpoint时,表示满了,需要清空数据再写入
归档日志。
是一个逻辑日志,记录语句的原始逻辑,比如”给ID为2的数据行C字段加1“,属于MySQL Server层。
不管什么引擎发生表更新就会产生binlog日志。
数据备份、主备主主和主从都少不了binlog日志,需要使用binlog同步保证一致性。
有三种格式,statement、row和mixed
写入机制
事务执行中,先写入到binlog cache,然后提交以后写入到binlog文件里。
一样有三种刷盘日志
redo保证了崩溃恢复,binlog保证了数据一致性
redo log在事务的过程中可以一直写入,binlog则只有提交的时候才可以写入
有一个场景:update一个数据,c是0,更新成1。然后执行完redo,bin写入错误会发生什么。
bin里面没有相应的数据记录,在恢复的时候就没有这个,c就是0,而redo里面c是1,会出现冲突了。
解决这个问题redo log拆成两个部分,prepare和commit。
大致过程:
当恢复日志的时候发现redo处于prepare的时候,如果没有对应的binlog就会回滚事务。
当commit出问题的时候出问题了,不会影响,因为binlog能找到相应记录,直接用binlog了。
所有事务的修改都会记录在undo log里面。出现问题回滚的时候按照undo log的信息把数据回复即可,而且是持久化保存的,宕机也没事
CHAR(x)
是固定长度的字符串,读取速度快,因为是固定长度,如果值长度小于x,则空格填充。空间换时间,最长255
VARCHAR(x)
是可变长度的字符串,存取慢,不消耗多余空间,时间换空间,最长65532
如果指定x,则最长长度为x,x越大排序就越消耗内存。
INT(x)
显示字符的宽度,仍然使用4字节存储,只不过显示的长度为x
不易设置为NULL,建议NOT NULL
因为NULL在数据库中占用的内存空间更大,需要更多字节保存
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。