宝典
宝典
一、基础知识
输入与输出
a=1,b=2,不用中间变量交换 a 和 b 的值?
方法一:
1. a = a+b
2. b = a-b
3. a = a-b
方法二:
1. a = a^b
2. b =b^a
3. a = a^b
方法三:
1. a,b = b,a
print 调用 Python 中底层的什么方法?
print 方法默认调用 sys.stdout.write 方法,即往控制台打印字符串。
代码中要修改不可变数据会出现什么问题? 抛出什么异常?
代码不会正常运行,抛出 TypeError 异常。
下面这段代码的输出结果将是什么?请解释?
1. class Parent(object):
2. x = 1
3. class Child1(Parent):
4. pass
5. class Child2(Parent):
6. pass
7. print(Parent.x, Child1.x, Child2.x) #1 1 1
8. Child1.x = 2
9. print(parent.x, Child1.x, Child2.x) #1 2 1
10. parent.x = 3
11. print(Parent.x, Child1.x, Child2.x) #3 2 3
结果为:
1、#继承自父类的类属性 x,所以都一样,指向同一块内存地址。
2、#更改 Child1,Child1 的 x 指向了新的内存地址。
3、#更改 Parent,Parent 的 x 指向了新的内存地址。
简述你对 input()函数的理解?
在 Python3 中,input()获取用户输入,不论用户输入的是什么,获取到的都是字符串类型的。
条件与循环
阅读下面的代码,写出 A0,A1 至 An 的最终值
1. A0 = dict(zip(('a','b','c','d','e'),(1,2,3,4,5)))
2. A1 = range(10)
3. A2 = [i for i in A1 if i in A0]
4. A3 = [A0[s] for s in A0]
5. A4 = [i for i in A1 if i in A3]
6. A5 = {i:i*i for i in A1}
7. A6 = [[i,i*i] for i in A1]
答:
1. A0 = {'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4}
2. A1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3. A2 = []
4. A3 = [1, 3, 2, 5, 4]
5. A4 = [1, 2, 3, 4, 5]
6. A5 = {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
7. A6 = [[0, 0], [1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36],[7, 49],[8, 64], [9,81]]
以下 Python 程序的输出?
1. for i in range(5,0,-1):
2. print(i)
答:
5 4 3 2 1
文件操作
4G内存怎么读取一个5G的数据?
方法一:
可以通过生成器,分多次读取,每次读取数量相对少的数据(比如 500MB)进行处理,处理结束后再读取后面的 500MB 的数据。
方法二:
可以通过 linux 命令 split 切割成小文件,然后再对数据进行处理,此方法效率比较高。可以按照行数切割,可以按照文件大小切割。
现在考虑有一个 jsonline 格式的文件 file.txt 大小约为 10K,之前处理文件的代码如下所示:
1. def get_lines():
2. l = []
3. with open(‘file.txt’,‘rb’) as f:
4. for eachline in f:
5. l.append(eachline)
6. return l
7. if __name__ == ‘__main__’:
8. for e in get_lines():
9. process(e) #处理每一行数据
现在要处理一个大小为 10G 的文件,但是内存只有4G,如果在只修改 get_lines 函数而其他代码保持不变的情况下,应该如何实现?需要考虑的问题都有哪些?
1. def get_lines():
2. l = []
3. with open(‘file.txt’,’rb’) as f:
4. data = f.readlines(60000)
5. l.append(data)
6. yield l
要考虑到的问题有:
内存只有 4G 无法一次性读入 10G 的文件,需要分批读入。分批读入数据要记录每次读入数据的位置。分批每次读入数据的大小,太小就会在读取操作上花费过多时间。
read、readline 和 readlines 的区别?
- read:读取整个文件。
- readline:读取下一行,使用生成器方法。
- readlines:读取整个文件到一个迭代器以供我们遍历。
异常
在 except中 return后还会不会执行 finally中的代码?怎么抛出自定义异常?
会继续处理 finally 中的代码;用 raise 方法可以抛出自定义异常。
介绍一下 except 的作用和用法?
except: #捕获所有异常
except: <异常名>: #捕获指定异常
except:<异常名 1, 异常名 2> : 捕获异常1或者异常2
except:<异常名>,<数据>:捕获指定异常及其附加的数据
except:<异常名 1,异常名 2>:<数据>:捕获异常名1或者异常名2及附加的数据
模块与包
常用的 Python 标准库都有哪些?
os 操作系统,time 时间,random 随机,pymysql 连接数据库,threading 线程,multiprocessing 进程,queue 队列。
第三方库:django 和 flask 也是第三方库,requests,virtualenv,selenium,scrapy,xadmin,celery,re,hashlib,md5。
常用的科学计算库(如 Numpy,Scipy,Pandas)。
赋值、浅拷贝和深拷贝的区别?
-
赋值
在 Python 中,对象的赋值就是简单的对象引用,这点和 C++不同,如下所示:
a = [1,2,"hello",['python', 'C++']] b = a
在上述情况下,a 和 b 是一样的,他们指向同一片内存,b 不过是 a 的别名,是引用。
我们可以使用
b is a
去判断,返回 True,表明他们地址相同,内容相同,也可以使用 id()函数来查看两个列表的地址是否相同。赋值操作(包括对象作为参数、返回值)不会开辟新的内存空间,它只是复制了对象的引用。也就是说除了 b 这个名字之外,没有其他的内存开销。修改了 a,也就影响了 b,同理,修改了 b,也就影响了 a。
-
浅拷贝(shallow copy)
浅拷贝会创建新对象,其内容非原对象本身的引用,而是原对象内第一层对象的引用。
浅拷贝有三种形式:切片操作、工厂函数、copy 模块中的 copy 函数。比如上述的列表 a;
-
切片操作:
b = a[:]
或者b = [x for x in a]
; -
工厂函数:
b = list(a)
; -
copy 函数:
b = copy.copy(a)
;
浅拷贝产生的列表 b 不再是列表 a 了,使用 is 判断可以发现他们不是同一个对象,使用 id 查看,他们也不指向同一片内存空间。但是当我们使用
id(x) for x in a
和id(x) for x in b
来查看 a 和 b 中元素的地址时,可以看到二者包含的元素的地址是相同的。在这种情况下,列表 a 和 b 是不同的对象,修改列表 b 理论上不会影响到列表 a。
但是要注意的是,浅拷贝之所以称之为浅拷贝,是它仅仅只拷贝了一层,在列表 a 中有一个嵌套的list,如果我们修改了它,情况就不一样了。
比如:
a[3].append('java')
。查看列表 b,会发现列表 b 也发生了变化,这是因为我们修改了嵌套的 list,修改外层元素,会修改它的引用,让它们指向别的位置,修改嵌套列表中的元素,列表的地址并未发生变化,指向的都是用一个位置。 -
-
深拷贝(deep copy)
深拷贝只有一种形式,copy 模块中的 deepcopy()函数。
深拷贝和浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。因此,它的时间和空间开销要高。
同样的对列表 a,如果使用 b = copy.deepcopy(a),再修改列表 b 将不会影响到列表 a,即使嵌套的列表具有更深的层次,也不会产生任何影响,因为深拷贝拷贝出来的对象根本就是一个全新的对象,不再与原来的对象有任何的关联。
拷贝的注意点:对于不可变数据类型,如数字、字符,以及其他的“原子”类型,没有拷贝一说,产生的都是原对象的引用。如果可变数据类型包含不可变数据类型,即使采用了深拷贝,也只能得到浅拷贝。
Python 里面如何生成随机数?
Python 中用于生成随机数的模块是 random,在使用前需要 import。如下例子可以酌情列举:
-
random.random():生成一个 0-1 之间的随机浮点数;
-
random.uniform(a, b):生成[a,b]之间的浮点数;
-
random.randint(a, b):生成[a,b]之间的整数;
-
random.randrange(a, b, step):在指定的集合[a,b)中,以 step 为基数随机取一个数;
-
random.shuffle(lst): 将序列的所有元素随机排序;
-
random.choice(sequence):从特定序列中随机取一个元素,这里的序列可以是字符串,列表,元组等;
输入某年某月某日,判断这一天是这一年的第几天?(可以用 Python 标准库)
1. import datetime
2. def dayofyear():
3. year = input("请输入年份:")
4. month = input("请输入月份:")
5. day = input("请输入天:")
6. date1 = datetime.date(year=int(year),month=int(month),day=int(day))
7. date2 = datetime.date(year=int(year),month=1,day=1)
8. return (date1 - date2 + 1).days
说明一下 os.path 和 sys.path 分别代表什么?
os.path 主要是用于对系统路径文件的操作。
sys.path 主要是对 Python 解释器的系统环境参数的操作(动态的改变 Python 解释器搜索路径)。
Python 中的 os 模块常见方法?
- os.remove()删除文件
- os.rename()重命名文件
- os.walk()生成目录树下的所有文件名
- os.chdir()改变目录
- os.mkdir/makedirs 创建目录/多层目录
- os.rmdir/removedirs 删除目录/多层目录
- os.listdir()列出指定目录的文件
- os.getcwd()取得当前工作目录
- os.chmod()改变目录权限
- os.path.basename()去掉目录路径,返回文件名
- os.path.dirname()去掉文件名,返回目录路径
- os.path.join()将分离的各部分组合成一个路径名
- os.path.split()返回(dirname(),basename())元组
- os.path.splitext()(返回 filename,extension)元组
- os.path.getatime\ctime\mtime 分别返回最近访问、创建、修改时间
- os.path.getsize()返回文件大小
- os.path.exists()是否存在
- os.path.isabs()是否为绝对路径
- os.path.isdir()是否为目录
- os.path.isfile()是否为文件
更多详见:
Python3 OS 文件/目录方法
os.path
os
--- 多种操作系统接口
os.path
--- 常用路径操作
Python 的 sys 模块常用方法?
- sys.argv 命令行参数 List,第一个元素是程序本身路径
- sys.modules.keys() 返回所有已经导入的模块列表
- sys.exc_info() 获取当前正在处理的异常类,exc_type、exc_value、exc_traceback 当前处理的异常详细信息
- sys.exit(n) 退出程序,正常退出时
exit(0)
- sys.hexversion 获取 Python 解释程序的版本值,16 进制格式如:0x020403F0
- sys.version 获取 Python 解释程序的版本信息
- sys.maxint 最大的 Int 值
- sys.maxunicode 最大的 Unicode 值
- sys.modules 返回系统导入的模块字段,key 是模块名,value 是模块
- sys.path 返回模块的搜索路径,初始化时使用 PYTHONPATH 环境变量的值
- sys.platform 返回操作系统平台名称
- sys.stdout 标准输出
- sys.stdin 标准输入
- sys.stderr 错误输出
- sys.exc_clear() 用来清除当前线程所出现的当前的或最近的错误信息
- sys.exec_prefix 返回平台独立的 python 文件安装的位置
- sys.byteorder 本地字节规则的指示器,big-endian 平台的值是'big',little-endian 平台的值是'little'
- sys.copyright 记录 python 版权相关的东西
- sys.api_version 解释器的 C 的 API 版本
- sys.version_info 元组则提供一个更简单的方法来使你的程序具备 Python 版本要求功能
更多详见:
sys
--- 系统相关的参数和函数
unittest 是什么?
在 Python 中,unittest 是 Python 中的单元测试框架。它拥有支持共享搭建、自动测试、在测试中暂停代码、将不同测试迭代成一组等的功能。
模块和包是什么?
在 Python 中,模块是搭建程序的一种方式。每一个 Python 代码文件都是一个模块,并可以引用其他的模块,比如对象和属性。
一个包含许多 Python 代码的文件夹是一个包。一个包可以包含模块和子文件夹。
Python 特性
什么是 Python?
Python 是一种编程语言,它有对象、模块、线程、异常处理和自动内存管理,可以加入其他语言的对比。
Python 是一种解释型语言,Python 在代码运行之前不需要解释。
Python 是动态类型语言,在声明变量时,不需要说明变量的类型。
Python 适合面向对象的编程,因为它支持通过组合与继承的方式定义类。
在 Python 语言中,函数是第一类对象。
Python 代码编写快,但是运行速度比编译型语言通常要慢。
Python 用途广泛,常被用走"胶水语言",可帮助其他语言和组件改善运行状况。
使用 Python,程序员可以专注于算法和数据结构的设计,而不用处理底层的细节。
Python 是强语言类型还是弱语言类型?
Python 是强类型的动态脚本语言。
强类型:不允许不同类型相加。
动态:不使用显示数据类型声明,且确定一个变量的类型是在第一次给它赋值的时候。
脚本语言:一般也是解释型语言,运行代码只需要一个解释器,不需要编译。
什么是 Python 自省?
Python 自省是 Python 具有的一种能力,使程序员面向对象的语言所写的程序在运行时,能够获得对象的 Python 类型。Python 是一种解释型语言,为程序员提供了极大的灵活性和控制力。
Python 中的作用域?
Python 中,一个变量的作用域总是由在代码中被赋值的地方所决定。当 Python 遇到一个变量的话。它会按照这的顺序进行搜索:
本地作用域(Local)--->当前作用域被嵌入的本地作用域(Enclosing locals)--->全局/模块作用域
(Global)--->内置作用域(Built-in)。
什么是 Python 的命名空间?
在 Python 中,所有的名字都存在于一个空间中,它们在该空间中存在和被操作——这就是命名空间。它就好像一个盒子,每一个变量名字都对应装着一个对象。当查询变量的时候,会从该盒子里面寻找相应的对象。
谈一下什么是解释性语言,什么是编译性语言?
计算机不能直接理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序。
解释性语言在运行程序的时候才会进行翻译。
编译型语言写的程序在执行之前,需要一个专门的编译过程,把程序编译成机器语言(可执行文件)。
Python 中有日志吗?怎么使用?
有日志。
Python 自带 logging 模块,调用 logging.basicConfig()方法,配置需要的日志等级和相应的参数,Python 解释器会按照配置的参数生成相应的日志。
Python 是如何进行类型转换的?
内建函数封装了各种转换函数,可以使用目标类型关键字强制类型转换,进制之间的转换可以用int(‘str’, base=’n’)将特定进制的字符串转换为十进制,再用相应的进制转换函数将十进制转换为目标进制。
可以使用内置函数直接转换的有:
函数 | 描述 |
---|---|
int(x [,base]) | 将x转换为一个整数 |
float(x) | 将x转换到一个浮点数 |
complex(real [,imag]) | 创建一个复数 |
str(x) | 将对象 x 转换为字符串 |
repr(x) | 将对象 x 转换为表达式字符串 |
eval(str) | 用来计算在字符串中的有效Python表达式,并返回一个对象 |
tuple(s) | 将序列 s 转换为一个元组 |
list(s) | 将序列 s 转换为一个列表 |
set(s) | 转换为可变集合 |
dict(d) | 创建一个字典。d 必须是一个 (key, value)元组序列。 |
frozenset(s) | 转换为不可变集合 |
chr(x) | 将一个整数转换为一个字符 |
ord(x) | 将一个字符转换为它的整数值 |
hex(x) | 将一个整数转换为一个十六进制字符串 |
oct(x) | 将一个整数转换为一个八进制字符串 |
Python2 与 Python3 的区别?
-
核心类差异
-
Python3 对 Unicode 字符的原生支持。
Python2 中使用 ASCII 码作为默认编码方式导致 string 有两种类型 str 和 unicode,Python3 只支持 unicode 的 string。Python2 和 Python3 字节和字符对应关系为:
-
Python3 采用的是绝对路径的方式进行 import。
Python2 中相对路径的 import 会导致标准库导入变得困难(想象一下,同一目录下有 file.py,如何同时导入这个文件和标准库 file)。Python3 中这一点将被修改,如果还需要导入同一目录的文件必须使用绝对路径,否则只能使用相关导入的方式来进行导入。
-
Python2中存在老式类和新式类的区别,Python3统一采用新式类。新式类声明要求继承 object,必须用新式类应用多重继承。
-
Python3 使用更加严格的缩进。Python2 的缩进机制中,1 个 tab 和 8 个 space 是等价的,所
以在缩进中可以同时允许 tab 和 space 在代码中共存。这种等价机制会导致部分 IDE 使用存在问题。Python3 中 1 个 tab 只能找另外一个 tab 替代,因此 tab 和 space 共存会导致报错:TabError:inconsistent use of tabs and spaces in indentation.
Python2 中使用 ASCII 码作为默认编码方式导致 string 有两种类型 str 和 unicode,Python3 只
支持 unicode 的 string。Python2 和 Python3 字节和字符对应关系为:
-
-
废弃类差异
- print 语句被 Python3 废弃,统一使用 print 函数
- exec 语句被 python3 废弃,统一使用 exec 函数
- execfile 语句被 Python3 废弃,推荐使用 exec(open("./filename").read())
- 不相等操作符"<>"被 Python3 废弃,统一使用"!="
- xrange 函数被 Python3 废弃,统一使用 range,Python3 中 range 的机制也进行修改并提高了大数据集生成效率
- Python3 中这些方法再不再返回 list 对象:dictionary 关联的 keys()、values()、items(),zip(),map(),filter(),但是可以通过 list 强行转换:
1. mydict={"a":1,"b":2,"c":3} 2. mydict.keys() #<built-in method keys of dict object at 0x000000000040B4C8> 3. list(mydict.keys()) #['a', 'c', 'b']
- 迭代器 iterator 的 next()函数被 Python3 废弃,统一使用 next(iterator)
- raw_input 函数被 Python3 废弃,统一使用 input 函数
- 字典变量的 has_key 函数被Python废弃,统一使用 in 关键词
- file 函数被 Python3 废弃,统一使用 open 来处理文件,可以通过 io.IOBase 检查文件类型
- apply 函数被 Python3 废弃
- 异常 StandardError 被 Python3 废弃,统一使用 Exception
-
修改类差异
-
浮点数除法操作符“/”和“//”的区别
-
“ / ”:
Python2:若为两个整形数进行运算,结果为整形,但若两个数中有一个为浮点数,则结果为浮点数;
Python3:为真除法,运算结果不再根据参加运算的数的类型。
-
“//”:
Python2:返回小于除法运算结果的最大整数;从类型上讲,与"/"运算符返回类型逻辑一致。
Python3:和 Python2 运算结果一样。
-
-
异常抛出和捕捉机制区别
Python2:
1. raise IOError, "file error" #抛出异常 2. except NameError, err: #捕捉异常
Python3
1. raise IOError("file error") #抛出异常 2. except NameError as err: #捕捉异常
-
for 循环中变量值区别
Python2,for 循环会修改外部相同名称变量的值
1. i = 1 2. print ('comprehension: ', [i for i in range(5)]) 3. print ('after: i =', i ) #i=4
Python3,for 循环不会修改外部相同名称变量的值
1. i = 1 2. print ('comprehension: ', [i for i in range(5)]) 3. print ('after: i =', i ) #i=1
-
round 函数返回值区别
Python2,round 函数返回 float 类型值
1. isinstance(round(15.5),int) #True
Python3,round 函数返回 int 类型值
1. isinstance(round(15.5),float) #True
-
比较操作符区别
Python2 中任意两个对象都可以比较
1. ll < 'test' #True
Python3 中只有同一数据类型的对象可以比较
1. ll < 'test' # TypeError: unorderable types: int() < str()
-
-
第三方工具包差异
我们在 pip 官方下载源 pypi 搜索 Python2.7 和 Python3.5 的第三方工具包数可以发现,Python2.7版本对应的第三方工具类目数量是 28523,Python3.5 版本的数量是 12457,这两个版本在第三方工具包支持数量差距相当大。
我们从数据分析的应用角度列举了常见实用的第三方工具包(如下表),并分析这些工具包在Python2.7 和 Python3.5 的支持情况:
分类 | 工具名 | 用途 |
---|---|---|
数据收集 | scrapy | 网页采集,爬虫 |
数据收集 | scrapy-redis | 分布式爬虫 |
数据收集 | selenium | web 测试,仿真浏览器 |
数据处理 | beautifulsoup | 网页解释库,提供 lxml 的支持 |
数据处理 | lxml | xml 解释库 |
数据处理 | xlrd | excel 文件读取 |
数据处理 | xlwt | excel 文件写入 |
数据处理 | xlutils | excel 文件简单格式修改 |
数据处理 | pywin32 | excel 文件的读取写入及复杂格式定制 |
数据处理 | Python-docx | Word 文件的读取写入 |
数据分析 | numpy | 基于矩阵的数学计算库 |
数据分析 | pandas | 基于表格的统计分析库 |
数据分析 | scipy | 科学计算库,支持高阶抽象和复杂模型 |
数据分析 | statsmodels | 统计建模和计量经济学工具包 |
数据分析 | scikit-learn | 机器学习工具库 |
数据分析 | gensim | 自然语言处理工具库 |
数据分析 | jieba | 中文分词工具库 |
数据存储 | MySQL-python | mysql 的读写接口库 |
数据存储 | mysqlclient | mysql 的读写接口库 |
数据存储 | SQLAlchemy | 数据库的 ORM 封装 |
数据存储 | pymssql | sql server 读写接口库 |
数据存储 | redis | redis 的读写接口 |
数据存储 | PyMongo | mongodb 的读写接口 |
数据呈现 | matplotlib | 流行的数据可视化库 |
数据呈现 | seaborn | 美观的数据可是湖库,基于 matplotlib |
工具辅助 | jupyter | 基于 web 的 python IDE,常用于数据分析 |
工具辅助 | chardet | 字符检查工具 |
工具辅助 | ConfigParser | 配置文件读写支持 |
工具辅助 | requests | HTTP 库,用于网络访问 |
- 工具安装问题
-
windows 环境
Python2 无法安装 mysqlclient。Python3 无法安装 MySQL-python、flup、functools32、Gooey、Pywin32、 webencodings。
matplotlib 在 python3 环境中安装报错:The following required packages can not be built:freetype, png。需要手动下载安装源码包安装解决。
scipy 在 Python3 环境中安装报错,numpy.distutils.system_info.NotFoundError,需要自己手工下载对应的安装包,依赖 numpy,pandas 必须严格根据 python 版本、操作系统、64 位与否。运行matplotlib 后发现基础包 numpy+mkl 安装失败,需要自己下载,国内暂无下载源
-
centos 环境
Python2 无法安装 mysql-python 和 mysqlclient 包,报错:EnvironmentError: mysql_config not found,解决方案是安装 mysql-devel 包解决。使用 matplotlib 报错:no module named _tkinter,安装 Tkinter、tk-devel、tc-devel 解决。
pywin32 也无法在 centos 环境下安装。
-
关于 Python 程序的运行方面,有什么手段能提升性能?
- 使用多进程,充分利用机器的多核性能
- 对于性能影响较大的部分代码,可以使用 C 或 C++编写
- 对于 IO 阻塞造成的性能影响,可以使用 IO 多路复用来解决
- 尽量使用 Python 的内建函数
- 尽量使用局部变量
你所遵循的代码规范是什么?请举例说明其要求?
PEP8 规范。
-
变量
-
常量:大写加下划线 USER_CONSTANT。
-
私有变量 : 小写和一个前导下划线 _private_value。
Python 中不存在私有变量一说,若是遇到需要保护的变量,使用小写和一个前导下划线。但这只是程序员之间的一个约定,用于警告说明这是一个私有变量,外部类不要去访问它。但实际上,外部类还是可以访问到这个变量。
-
内置变量 : 小写,两个前导下划线和两个后置下划线 _class_。
两个前导下划线会导致变量在解释期间被更名。这是为了避免内置变量和其他变量产生冲突。用户定义的变量要严格避免这种风格。以免导致混乱。
-
-
函数和方法
总体而言应该使用小写和下划线。但有些比较老的库使用的是混合大小写,即首单词小写,之后每个单词第一个字母大写,其余小写。但现在,小写和下划线已成为规范。
-
私有方法 :小写和一个前导下划线。
这里和私有变量一样,并不是真正的私有访问权限。同时也应该注意一般函数不要使用两个前导下划线(当遇到两个前导下划线时,Python 的名称改编特性将发挥作用)。
-
特殊方法 :小写和两个前导下划线,两个后置下划线。
这种风格只应用于特殊函数,比如操作符重载等。
-
函数参数 : 小写和下划线,缺省值等号两边无空格
-
-
类
类总是使用驼峰格式命名,即所有单词首字母大写其余字母小写。类名应该简明,精确,并足以从中理解类所完成的工作。常见的一个方法是使用表示其类型或者特性的后缀,例如:SQLEngine,MimeTypes 对于基类而言,可以使用一个 Base 或者 Abstract 前缀 BaseCookie,AbstractGroup。
-
模块和包
除特殊模块 _init_ 之外,模块名称都使用不带下划线的小写字母。
若是它们实现一个协议,那么通常使用 lib 为后缀,例如:import smtplib; import os; import sys
-
关于参数
-
不要用断言来实现静态类型检测。断言可以用于检查参数,但不应仅仅是进行静态类型检测。Python 是动态类型语言,静态类型检测违背了其设计思想。断言应该用于避免函数不被毫无意义的调用。
-
不要滥用 args 和 **kwargs。args 和 **kwargs 参数可能会破坏函数的健壮性。它们使签名变得模糊,而且代码常常开始在不应该的地方构建小的参数解析器。
-
其他
-
使用 has 或 is 前缀命名布尔元素
例如:is_connect = True,has_member = False
-
用复数形式命名序列
例如:members = ['user_1', 'user_2']
-
用显式名称命名字典
例如:person_address =
-
避免通用名称
诸如 list, dict, sequence 或者 element 这样的名称应该避免。
-
避免现有名称
诸如 os, sys 这种系统已经存在的名称应该避免。
-
-
一些数字
- 一行列数 : PEP 8 规定为 79 列。根据自己的情况,比如不要超过满屏时编辑器的显示列数。
- 一个函数 : 不要超过 30 行代码, 即可显示在一个屏幕类,可以不使用垂直游标即可看到整个函数。
- 一个类 : 不要超过 200 行代码,不要有超过 10 个方法。一个模块 不要超过 500 行。
-
验证脚本
可以安装一个 pep8 脚本用于验证你的代码风格是否符合 PEP8。
https://www.liaoxuefeng.com/wiki/896043488029600/900004111093344
dumps,loads 与 dump,load 的区别?
- json.dumps()将 pyhton 的 dict 数据类型编码为 json 字符串;
- json.loads()将 json 字符串解码为 dict 的数据类型;
- json.dump(x,y) x 是 json 对象, y 是文件对象,最终是将 json 对象写入到文件中;
- json.load(y) 从文件对象 y 中读取 json 对象。
什么是可变、不可变类型?
可变不可变指的是内存中的值是否可以被改变,不可变类型指的是对象所在内存块里面的值不可以
改变,有数值、字符串、元组;可变类型则是可以改变,主要有列表、字典、集合。
- 不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组);
- 可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。
更多详见:
Python3 基本数据类型
Python 主要的内置数据类型都有哪些? print dir( ‘a ’) 的输出?
内建类型:布尔类型、数字、字符串、列表、元组、字典、集合;
输出字符串‘a’的内建方法;
Python 中 is 和==的区别?
is 判断的是 a 对象是否就是 b 对象,是通过 id 来判断的。
==判断的是 a 对象的值是否和 b 对象的值相等,是通过 value 来判断的。
字典
dict:字典,字典是一组键(key)和值(value)的组合,通过键(key)进行查找,没有顺序, 使用大括号”{}”;
应用场景:
dict使用键和值进行关联的数据;
现有字典 d={‘a’:24,‘g’:52,‘i’:12,‘k’:33}请按字典中的 value值进行排序?
sorted(d.items(),key = lambda x:x[1])
说一下字典和 json 的区别?
字典是一种数据结构,json 是一种数据的表现形式,字典的 key 值只要是能 hash 的就行,json 的键必须是字符串。
存入字典里的数据有没有先后排序?
存入的数据不会自动排序,可以使用 sort 函数对字典进行排序。
字典推导式?
d = {key: value for (key, value) in iterable}
现有字典 d={‘a’:24,’g’:52,’l’:12,’k’:33}请按字典中的value值进行排序?
sorted(d.items(),key = lambda x:x[1])
更多字典相关内容详见:
Python3 字典
字符串
str:字符串是 Python 中最常用的数据类型。我们可以使用引号'或"来创建字符串。
如何理解 Python 中字符串中的\字符?
有三种不同的含义:1、转义字符;2、路径名中用来连接路径名;3、编写太长代码手动软换行。
请反转字符串“aStr”?
方法一:
print(‘aStr’[::-1])
方法二:
''.join(reversed(‘aStr’))
将字符串"k:1|k1:2|k2:3|k3:4",处理成 Python 字典:{k:1, k1:2, ... } # 字典里的 K 作为字符串处理
1. str1 = "k:1|k1:2|k2:3|k3:4"
2. def str2dict(str1):
3. dict1 = {}
4. for iterms in str1.split('|'):
5. key,value = iterms.split(':')
6. dict1[key] = value
7. return dict1
更多字典字符串相关内容详见:
Python3 字符串
列表
list:是 Python 中使用最频繁的数据类型,在其他语言中通常叫做数组,通过索引进行查找,使用方括号”[]”,列表是有序的集合。
应用场景:定义列表使用 [ ] 定义,数据之间使用 “,”分割。
列表的索引从 0 开始:索引就是数据在列表中的位置编号,索引又可以被称为下标
【注意】: 从列表中取值时,如果超出索引范围,程序会产生异常。
IndexError: list index out of range
列表的常用操作:
序号 | 方法 |
---|---|
1 | list.append(obj) 在列表末尾添加新的对象 |
2 | list.count(obj) 统计某个元素在列表中出现的次数 |
3 | list.extend(seq) 在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表) |
4 | list.index(obj) 从列表中找出某个值第一个匹配项的索引位置 |
5 | list.insert(index, obj) 将对象插入列表 |
6 | list.pop([index=-1]) 移除列表中的一个元素(默认最后一个元素),并且返回该元素的值 |
7 | list.remove(obj) 移除列表中某个值的第一个匹配项 |
8 | list.reverse() 反向列表中元素 |
9 | list.sort( key=None, reverse=False) 对原列表进行排序 |
10 | list.clear() 清空列表 |
11 | list.copy() 复制列表 |
14 | del list[index] 删除指定索引的数据 |
15 | max(list) 返回列表元素最大值 |
16 | min(list) 返回列表元素最小值 |
17 | len(list) 列表元素个数 |
18 | in 判断列表中是否包含某元素 |
请按 alist 中元素的 age 由大到小排序
1. alist = [{'name':'a','age':20},{'name':'b','age':30},{'name':'c','age':25}]
2. def sort_by_age(list1):
3. return sorted(alist,key=lambda x:x['age'],reverse=True)
下面代码的输出结果将是什么?
1. list = ['a', 'b', 'c', 'd', 'e']
2. print list[10:]
下面的代码将输出[],不会产生IndexError错误。就像所期望的那样,尝试用超出成员的个数的index来获取某个列表的成员。例如,尝试获取 list[10]和之后的成员,会导致 IndexError。然而,尝试获取列表的切片,开始的 index 超过了成员个数不会产生 IndexError,而是仅仅返回一个空列表。这成为特别让人恶心的疑难杂症,因为运行的时候没有错误产生,导致 bug 很难被追踪到。
写一个列表生成式,产生一个公差为 11 的等差数列
1. print([x*11 for x in range(10)])
给定两个列表,怎么找出他们相同的元素和不同的元素?
1. list1 = [1,2,3]
2. list2 = [3,4,5]
3. set1 = set(list1)
4. set2 = set(list2)
5. print(set1&set2)
6. print(set1^set2)
请写出一段 Python 代码实现删除一个 list 里面的重复元素?
方法一:
1. l1 = ['b','c','d','b','c','a','a']
2. l2 = list(set(l1))
3. print(l2)
如果想要保持他们原来的排序:
方法一:
1. l1 = ['b','c','d','b','c','a','a']
2. l2 = list(set(l1))
3. l2.sort(key=l1.index)
4. print(l2)
方法二:
1. l1 = ['b','c','d','b','c','a','a']
2. l2 = sorted(set(l1),key=l1.index)
3. print(l2)
方法三:
1. l1 = ['b', 'c', 'd', 'b', 'c', 'a', 'a']
2. l2 = []
3. for i in l1:
4. if not i in l2:
5. l2.append(i)
6. print(l2)
有如下数组 list = range(10)我想取以下几个数组,应该如何切片?
1. [1,2,3,4,5,6,7,8,9]
2. [1,2,3,4,5,6]
3. [3,4,5,6]
4. [9]
5. [1,3,5,7,9]
答:
1. [1:]
2. [1:7]
3. [3:7]
4. [-1]
5. [1::2]
下面这段代码的输出结果是什么?请解释?
1. def extendlist(val, list=[]):
2. list.append(val)
3. return list
4.
5. list1 = extendlist(10)
6. list2 = extendlist(123, [])
7. list3 = extendlist('a')
8.
9. print("list1 = %s" %list1)
10. print("list2 = %s" %list2)
11. print("list3 = %s" %list3)
输出结果:
list1 = [10, 'a']
list2 = [123]
list3 = [10, 'a']
新的默认列表只在函数被定义的那一刻创建一次。当 extendList 被没有指定特定参数 list 调用时,这组 list 的值随后将被使用。这是因为带有默认参数的表达式在函数被定义的时候被计算,不是在调用的时候被计算。
将以下 3 个函数按照执行效率高低排序
1. def f1(lIn):
2. l1 = sorted(lIn)
3. l2 = [i for i in l1 if i<0.5]
4. return [i*i for i in l2]
5.
6.
7. def f2(lIn):
8. l1 = [i for i in l1 if i<0.5]
9. l2 = sorted(l1)
10. return [i*i for i in l2]
11.
12.
13. def f3(lIn):
14. l1 = [i*i for i in lIn]
15. l2 = sorted(l1)
16. return [i for i in l1 if i<(0.5*0.5)]
按执行效率从高到低排列:f2、f1 和 f3。要证明这个答案是正确的,你应该知道如何分析自己代码的性能。Python中有一个很好的程序分析包,可以满足这个需求。
1. import random
2. import cProfile
3. lIn = [random.random() for i in range(100000)]
4. cProfile.run('f1(lIn)')
5. cProfile.run('f2(lIn)')
6. cProfile.run('f3(lIn)')
获取 1~100 被 6 整除的偶数?
1. def A():
2. alist = []
3. for i in range(1,100):
4. if i % 6 == 0:
5. alist.append(i)
6. last_num = alist[-3:]
7. print(last_num)
更多列表相关内容详见:
Python3 列表
元组
tuple:元组,元组将多样的对象集合到一起,不能修改,通过索引进行查找,使用括号”()”;
应用场景:把一些数据当做一个整体去使用,不能修改;
更多元组相关内容详见:
Python3 元组
集合
set:set 集合,在 Python 中的书写方式为{},集合与之前列表、元组类似,可以存储多个数据,但是这些数据是不重复的。集合对象还支持 union(联合), intersection(交), difference(差)和sysmmetric_difference(对称差集)等数学运算.
运算符 | 含义 |
---|---|
a&b | 交集:共有的部分 |
a|b | 并集:总共的部分 |
b - a | 差集:另一个集合中没有的部分 |
a ^ b | 对称差集(在 a 或 b 中,但不会同时出现在二者中) |
更多集合相关内容详见:
Python3 集合
二、Python高级
类
_init_ 和 __new__ 的区别?
_init_ 在对象创建后,对对象进行初始化。
__new__ 是在对象创建之前创建一个对象,并将该对象返回给 _init_。
Python 中类方法、类实例方法、静态方法有何区别?
类方法:是类对象的方法,在定义时需要在上方使用“@classmethod”进行装饰,形参为 cls,表示类对象,类对象和实例对象都可调用;
类实例方法:是类实例化对象的方法,只有实例对象可以调用,形参为 self,指代对象本身;
静态方法:是一个任意函数,在其上方使用“@staticmethod”进行装饰,可以用对象直接调用,静态方法实际上跟该类没有太大关系。
Python 中的可变对象和不可变对象?
不可变对象:该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
可变对象:该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。
Python 中,数值类型(int 和 float)、字符串 str、元组 tuple 都是不可变类型。而列表 list、字典 dict、集合set 是可变类型。
Python 的魔法方法
魔法方法就是可以给你的类增加魔力的特殊方法,如果你的对象实现 (重载)了这些方法中的某一个,那么这个方法就会在特殊的情况下被 Python 所调用,你可以定义自己想要的行为,而这一切都是自动发生的。 它们经常是两个下划线包围来命名的(比如 _init__,__lt_),Python 的魔法方法是非常强大的,所以了解其使用方法也变得尤为重要!
- _init_ 构造器,当一个实例被创建的时候初始化的方法。但是它并不是实例化调用的第一个方法。
- __new__才是实例化对象调用的第一个方法,它只取下 cls 参数,并把其他参数传给 __init__。 __new__很少使用,但是也有它适合的场景,尤其 是当类继承自一个像元组或者字符串这样不经常改变的类型的时候。
- __call__ 允许一个类的实例像函数一样被调用 。
- __getitem__ 定义获取容器中指定元素的行为,相当于 self[key] 。
- __getattr__ 定义当用户试图访问一个不存在属性的时候的行为 。
- __setattr__ 定义当一个属性被设置的时候的行为 。
- __getattribute__ 定义当一个属性被访问的时候的行为 。
面向对象中怎么实现只读属性?
将对象私有化,通过共有方法提供一个读取数据的接口。
1.class person:
2. def __init__(self,x):
3. self.__age = 10;
4. def age(self):
5. return self.__age;
6.t = person(22)
7.# t.__age = 100
8.print(t.age())
最好的方法
1.class MyCls(object):
2. __weight = 50
3.
4. @property #以访问属性的方式来访问 weight 方法
5. def weight(self):
6. return self.__weight
7.
8.if __name__ == '__main__':
9. obj = MyCls()
10. print(obj.weight)
11. obj.weight = 12
1.Traceback (most recent call last):
2.50
3. File "C:/PythonTest/test.py", line 11, in <module>
4. obj.weight = 12
5.AttributeError: can't set attribute
谈谈你对面向对象的理解?
面向对象是相对于面向过程而言的。面向过程语言是一种基于功能分析的、以算法为中心的程序设计方法;而面向对象是一种基于结构分析的、以数据为中心的程序设计思想。在面向对象语言中有一个有很重要东西,叫做类。
面向对象有三大特性:封装、继承、多态。
更多面向对象相关内容详见:
Python3 面向对象
Python 中如何动态获取和设置对象的属性?
1. if hasattr(Parent,'x'):
2. print(getattr(Parent,'x'))
3. setattr(Parent,'x',3)
4. print(getattr(Parent,'x'))
内存管理与垃圾回收机制
Python 的内存管理机制及调优手段?
内存管理机制:引用计数、垃圾回收、内存池。
引用计数:引用计数是一种非常高效的内存管理手段, 当一个 Python 对象被引用时其引用计数增加 1, 当其不再被一个变量引用时则计数减 1。当引用计数等于 0 时,对象被删除。
垃圾回收 :
-
引用计数:引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集技术。当 Python 的某个对象的引用计数降为 0 时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为 1。如果引用被删除,对象的引用计数为 0,那么该对象就可以被垃圾回收。不过如果出现循环引用的话,引用计数机制就不再起有效的作用了。
-
标记清除:如果两个对象的引用计数都为 1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被回收的,也就是说,它们的引用计数虽然表现为非 0,但实际上有效的引用计数为 0。所以先将循环引用摘掉,就会得出这两个对象的有效计数。
-
分代回收:从前面“标记-清除”这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。
举个例子:
当某些内存块 M 经过了 3 次垃圾收集的清洗之后还存活时,我们就将内存块 M 划到一个集合A 中去,而新分配的内存都划分到集合 B 中去。当垃圾收集开始工作时,大多数情况都只对集合 B 进行垃圾回收,而对集合 A 进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合 B 中的某些内存块由于存活时间长而会被转移到集合 A 中,当然,集合 A 中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。
内存池:
- Python 的内存机制呈现金字塔形状,-1,-2 层主要有操作系统进行操作;
- 第 0 层是 C 中的 malloc,free 等内存分配和释放函数进行操作;
- 第 1 层和第 2 层是内存池,有 Python 的接口函数 PyMem_Malloc 函数实现,当对象小于256K 时有该层直接分配内存;
- 第 3 层是最上层,也就是我们对 Python 对象的直接操作
Python 在运行期间会大量地执行 malloc 和 free 的操作,频繁地在用户态和核心态之间进行切换,这将严重影响 Python 的执行效率。为了加速 Python 的执行效率,Python 引入了一个内存池机制,用于管理对小块内存的申请和释放。
Python 内部默认的小块内存与大块内存的分界点定在 256 个字节,当申请的内存小于 256 字节时,PyObject_Malloc 会在内存池中申请内存;当申请的内存大于 256 字节时,PyObject_Malloc 的行为将蜕化为 malloc 的行为。当然,通过修改 Python 源代码,我们可以改变这个默认值,从而改变 Python 的默认内存管理行为。
调优手段:
- 手动垃圾回收
- 调高垃圾回收阈值
- 避免循环引用(手动解循环引用和使用弱引用)
内存泄露是什么?如何避免?
指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。导致程序运行速度减慢甚至系统崩溃等严重后果。
有 _del_() 函数的对象间的循环引用是导致内存泄漏的主凶。
避免方式:
- 不使用一个对象时使用:del object 来删除一个对象的引用计数就可以有效防止内存泄漏问题。
- 通过 Python 扩展模块 gc 来查看不能回收的对象的详细信息。
- 可以通过 sys.getrefcount(obj) 来获取对象的引用计数,并根据返回值是否为 0 来判断是否内存泄漏。
函数
Python 函数调用的时候参数的传递方式是值传递还是引用传递?
Python 的参数传递有:位置参数、默认参数、可变参数、关键字参数。
函数的传值到底是值传递还是引用传递,要分情况:
- 不可变参数用值传递:像整数和字符串这样的不可变对象,是通过拷贝进行传递的,因为你无论如何都不可能在原处改变不可变对象
- 可变参数是引用传递的:比如像列表,字典这样的对象是通过引用传递、和 C 语言里面的用指针传递数组很相似,可变对象能在函数内部改变。
对缺省参数的理解?
缺省参数指在调用函数的时候没有传入参数的情况下,调用默认的参数,在调用函数的同时赋值时,所传入的参数会替代默认参数。
*args 是不定长参数,他可以表示输入参数是不确定的,可以是任意多个。
**kwargs 是关键字参数,赋值的时候是以键 = 值的方式,参数是可以任意多对在定义函数的时候不确定会有多少参数会传入时,就可以使用两个参数。
为什么函数名字可以当做参数用?
Python 中一切皆对象,函数名是函数在内存中的空间,也是一个对象。
Python 中 pass 语句的作用是什么?
在编写代码时只写框架思路,具体实现还未编写就可以用 pass 进行占位,使程序不报错,不会进行任何操作。
有这样一段代码,print(c)会输出什么,为什么?
1. a = 10
2. b = 20
3. c = [a]
4. a = 15
答:10 。对于字符串、数字,传递是相应的值。
更多函数相关内容详见:
Python3 函数
内建函数
map 函数?
从参数方面来讲:map()包含两个参数,第一个参数是一个函数,第二个是序列(列表 或元组)。其中,函数(即 map的第一个参数位置的函数)可以接收一个或多个参数。
从对传进去的数值作用来讲:map()是将传入的函数依次作用到序列的每个元素,每个元素都是独自被函数“作用”一次 。
更多内置函数相关内容详见:
Python3 内置函数
递归函数停止的条件?
递归的终止条件一般定义在递归函数内部,在递归调用前要做一个条件判断,根据判断的结果选择是继续调用自身,还是 return返回终止递归。
终止的条件:
-
判断递归的次数是否达到某一限定值
-
判断运算的结果是否达到某个范围等,根据设计的目的来选择
回调函数是如何通信的?
回调函数是把函数的地址作为参数传递给另一个函数,将整个函数当作一个对象,赋值给调用的函数。
hasattr() getattr() setattr() 函数使用详解?
hasattr(object, name)函数:判断一个对象里面是否有name属性或者name方法,返回bool值,有name属性(方法)返回True,否则返回 False。
注意:name 要使用引号括起来。
1. class function_demo(object):
2. name = 'demo'
3. def run(self):
4. return "hello function"
5. functiondemo = function_demo()
6. res = hasattr(functiondemo, 'name') #判断对象是否有 name 属性,True
7. res = hasattr(functiondemo, "run") #判断对象是否有 run 方法,True
8. res = hasattr(functiondemo, "age") #判断对象是否有 age 属性,Falsw
9. print(res)
getattr(object, name[,default]) 函数:获取对象 object 的属性或者方法,如果存在则打印出来,如果不存在,打印默认值,默认值可选。
注意:如果返回的是对象的方法,则打印结果是:方法的内存地址,如果需要运行这个方法,可以在后面添加括号()。
1. functiondemo = function_demo()
2. getattr(functiondemo, 'name') #获取 name 属性,存在就打印出来--- demo
3. getattr(functiondemo, "run") #获取 run 方法,存在打印出 方法的内存地址---<bound method function_demo.run of <__main__.function_demo object at 0x10244f320>>
4. getattr(functiondemo, "age") #获取不存在的属性,报错如下:
5. Traceback (most recent call last):
6. File "/Users/liuhuiling/Desktop/MT_code/OpAPIDemo/conf/OPCommUtil.py", line 39, in <module>
7. res = getattr(functiondemo, "age")
8. AttributeError: 'function_demo' object has no attribute 'age'
9. getattr(functiondemo, "age", 18) #获取不存在的属性,返回一个默认值
setattr(object,name,values)函数:给对象的属性赋值,若属性不存在,先创建再赋值
1.class function_demo(object):
2. name = 'demo'
3. def run(self):
4. return "hello function"
5.functiondemo = function_demo()
6.res = hasattr(functiondemo, 'age') # 判断 age 属性是否存在,False
7.print(res)
8.setattr(functiondemo, 'age', 18 ) #对 age 属性进行赋值,无返回值
9.res1 = hasattr(functiondemo, 'age') #再次判断属性是否存在,True
综合使用:
1.class function_demo(object):
2. name = 'demo'
3. def run(self):
4. return "hello function"
5.functiondemo = function_demo()
6.res = hasattr(functiondemo, 'addr') # 先判断是否存在 if res:
7. addr = getattr(functiondemo, 'addr')
8. print(addr)else:
9. addr = getattr(functiondemo, 'addr', setattr(functiondemo, 'addr', '北京首都'))
10. #addr = getattr(functiondemo, 'addr', '美国纽约')
11. print(addr)
Lambda
什么是 lambda 函数? 有什么好处?
lambda 函数是一个可以接收任意多个参数(包括可选参数)并且返回单个表达式值的函数
- lambda 函数比较轻便,即用即仍,很适合需要完成一项功能,但是此功能只在此一处使用,连名字都很随意的情况下;
- 匿名函数,一般用来给 filter, map 这样的函数式编程服务;
- 作为回调函数,传递给某些应用,比如消息处理
下面这段代码的输出结果将是什么?请解释。
1. def multipliers():
2. return [lambda x : i * x for i in range(4)]
3. print [m(2) for m in multipliers()]
上面代码输出的结果是[6, 6, 6, 6] (不是我们想的[0, 2, 4, 6])。
你如何修改上面的 multipliers 的定义产生想要的结果?
上述问题产生的原因是 Python 闭包的延迟绑定。这意味着内部函数被调用时,参数的值在闭包内进行查找。因此,当任何由 multipliers()返回的函数被调用时,i 的值将在附近的范围进行查找。那时,不管返回的函数是否被调用,for 循环已经完成,i 被赋予了最终的值 3。
因此,每次返回的函数乘以传递过来的值 3,因为上段代码传过来的值是 2,它们最终返回的都是 6。(3*2)碰巧的是,《The Hitchhiker’s Guide to Python》也指出,在与 lambdas 函数相关也有一个被广泛被误解的知识点,不过跟这个 case 不一样。由 lambda 表达式创造的函数没有什么特殊的地方,它其实是和 def 创造的函数式一的。下面是解决这一问题的一些方法。
方法一:使用Python生成器。
1. def multipliers():
2. for i in range(4): yield lambda x : i * x
方法二:创造一个闭包,利用默认函数立即绑定。
1. def multipliers():
2. return [lambda x, i=i : i * x for i in range(4)]
设计模式
请手写一个单例
1. class A(object):
2. __instance = None
3. def __new__(cls, *args, **kwargs):
4. if cls.__instance is None:
5. cls.__instance = object.__new__(cls)
6. return cls.__instance
7. else:
8. return cls.__instance
单例模式的应用场景有哪些?
单例模式应用的场景一般发现在以下条件下:
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置。
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。 1.网站的计数器 2.应用配置 3.多线程池 4.数据库配置,数据库连接池 5.应用程序的日志应用....
工厂模式
class Person:
def __init__(self):
self.name = None
self.gender = None
def getName(self):
return self.name
def getGender(self):
return self.gender
class Male(Person):
def __init__(self, name):
print("Hello Mr." + name)
class Female(Person):
def __init__(self, name):
print("Hello Miss." + name)
class Factory:
def getPerson(self, name, gender):
if gender == 'M':
return Male(name)
if gender == 'F':
return Female(name)
if __name__ == '__main__':
factory = Factory()
person = factory.getPerson("Chetan", "M")
对装饰器的理解 ,并写出一个计时器记录方法执行性能的装饰器?
装饰器本质上是一个 Python 函数,它可以在让其他函数在不需要做任何代码的变动的前提下增加额外的功能。装饰器的返回值也是一个函数的对象,它经常用于有切面需求的场景。 比如:插入日志、性能测试、事务处理、缓存、权限的校验等场景 有了装饰器就可以抽离出大量的与函数功能本身无关的雷同代码并发并继续使用。
1. import time
2. def timeit(func):
3. def wrapper():
4. start = time.clock()
5. func()
6. end =time.clock()
7. print('used:', end - start)
8. return wrapper
9. @timeit
10. def foo():
11. print('in foo()')
12. foo()
解释一下什么是闭包?
在函数内部再定义一个函数,并且这个函数用到了外边函数的变量,那么将这个函数以及用到的一些变量称之为闭包。
生成器、迭代器的区别?
迭代器是一个更抽象的概念,任何对象,如果它的类有 next 方法和 iter 方法返回自己本身,对于 string、list、dict、tuple 等这类容器对象,使用 for 循环遍历是很方便的。在后台 for 语句对容器对象调用 iter()函数,iter()是 python 的内置函数。iter()会返回一个定义了 next()方法的迭代器对象,它在容器中逐个访问容器内元素,next()也是 python 的内置函数。在没有后续元素时,next()会抛出一个StopIteration 异常。
生成器(Generator)是创建迭代器的简单而强大的工具。它们写起来就像是正规的函数,只是在需要返回数据的时候使用 yield 语句。每次 next()被调用时,生成器会返回它脱离的位置(它记忆语句最后一次执行的位置和所有的数据值)
区别:生成器能做到迭代器能做的所有事,而且因为自动创建了 iter()和 next()方法,生成器显得特别简洁,而且生成器也是高效的,使用生成器表达式取代列表解析可以同时节省内存。除了创建和保存程序状态的自动方法,当生成器终结时,还会自动抛出 StopIteration 异常。
Python 中 yield 的用法?
yield 就是保存当前程序执行状态。你用 for 循环的时候,每次取一个元素的时候就会计算一次。用 yield 的函数叫 generator,和 iterator 一样,它的好处是不用一次计算所有元素,而是用一次算一次,可以节省很多空间。generator每次计算需要上一次计算结果,所以用 yield,否则一 return,上次计算结果就没了。
1. >>> def createGenerator():
2. ... mylist = range(3)
3. ... for i in mylist:
4. ... yield i*i
5. ...
6. >>> mygenerator = createGenerator() # create a generator
7. >>> print(mygenerator) # mygenerator is an object!
8. <generator object createGenerator at 0xb7555c34>
9. >>> for i in mygenerator:
10. ... print(i)
11. 0
12. 1
13. 4
请尝试用“一行代码”实现将 1-N 的整数列表以 3 为单位分组,比如 1-100分组后为?
1. print([[x for x in range(1,100)][i:i+3] for i in range(0,len(list_a),3)])
正则表达式
Python 里 match 与 search 的区别?
match()函数只检测 RE 是不是在 string 的开始位置匹配,search()会扫描整个 string 查找匹配;也就是说 match()只有在 0 位置匹配成功的话才有返回,如果不是开始位置匹配成功的话,match()就返回 none。
Python 字符串查找和替换?
1. re.findall(r’目的字符串’,’原有字符串’) #查询
2. re.findall(r'cast','itcast.cn')[0]
3. re.sub(r‘要替换原字符’,’要替换新字符’,’原始字符串’)
4. re.sub(r'cast','heima','itcast.cn')
用 Python 匹配 HTML tag 的时候,<.> 和 <.?> 有什么区别?
<.*>是贪婪匹配,会从第一个“<”开始匹配,直到最后一个“>”中间所有的字符都会匹配到,中间可能会包含“<>”。
<.*?>是非贪婪匹配,从第一个“<”开始往后,遇到第一个“>”结束匹配,这中间的字符串都会匹配到,但是不会有“<>”。
请写出下列正则关键字的含义?
模式 | 描述 |
---|---|
^ | 匹配字符串的开头 |
$ | 匹配字符串的末尾。 |
. | 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。 |
[...] | 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k' |
[^...] | 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。 |
re* | 匹配0个或多个的表达式。 |
re+ | 匹配1个或多个的表达式。 |
re? | 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式 |
re | 匹配n个前面表达式。例如,"o{2}"不能匹配"Bob"中的"o",但是能匹配"food"中的两个o。 |
re | 精确匹配n个前面表达式。例如,"o{2,}"不能匹配"Bob"中的"o",但能匹配"foooood"中的所有o。"o{1,}"等价于"o+"。"o{0,}"则等价于"o*"。 |
re | 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式 |
a| b | 匹配a或b |
(re) | 匹配括号内的表达式,也表示一个组 |
(?imx) | 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。 |
(?-imx) | 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。 |
(?: re) | 类似 (...), 但是不表示一个组 |
(?imx: re) | 在括号中使用i, m, 或 x 可选标志 |
(?-imx: re) | 在括号中不使用i, m, 或 x 可选标志 |
(?#...) | 注释. |
(?= re) | 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。 |
(?! re) | 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功。 |
(?> re) | 匹配的独立模式,省去回溯。 |
\w | 匹配数字字母下划线 |
\W | 匹配非数字字母下划线 |
\s | 匹配任意空白字符,等价于 [\t\n\r\f]。 |
\S | 匹配任意非空字符 |
\d | 匹配任意数字,等价于 [0-9]。 |
\D | 匹配任意非数字 |
\A | 匹配字符串开始 |
\Z | 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。 |
\z | 匹配字符串结束 |
\G | 匹配最后匹配完成的位置。 |
\b | 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。 |
\B | 匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。 |
\n, \t, 等。 | 匹配一个换行符。匹配一个制表符, 等 |
\1...\9 | 匹配第n个分组的内容。 |
\10 | 匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。 |
更多正则相关内容详见:
Python3 正则表达式
系统编程
进程、线程和协程
更多相关内容详见:
multiprocessing
--- 基于进程的并行
threading
--- 基于线程的并行协程与任务
进程:程序运行在操作系统上的一个实例,就称之为进程。进程需要相应的系统资源:内存、时间片、pid。
创建进程步骤
- 首先要导入 multiprocessing 中的 Process;
- 实例化一个 Process 对象并传递参数;
- 使用 start()启动进程;
- 结束进程。
Process 语法结构
Process([group [, target [, name [, args [, kwargs]]]]])
-
target:如果传递了函数的引用,可以让这个子进程就执行函数中的代码
-
args:给 target 指定的函数传递的参数,以元组的形式进行传递
-
kwargs:给 target 指定的函数传递参数,以字典的形式进行传递
-
name:给进程设定一个名字,可以省略
-
group:指定进程组,大多数情况下用不到
Process 创建的实例对象的常用方法有:
-
start():启动子进程实例(创建子进程)
-
is_alive():判断进程子进程是否还在活着
-
join(timeout):是否等待子进程执行结束,或者等待多少秒
-
terminate():不管任务是否完成,立即终止子进程
Process 创建的实例对象的常用属性:
-
name:当前进程的别名,默认为 Process-N,N 为从 1 开始递增的整数
-
pid:当前进程的 pid(进程号)
给子进程指定函数传递参数 Demo
1.import os
2. from multiprocessing import Process
2.import time
3.
4.def pro_func(name, age, **kwargs):
5. for i in range(5):
6. print("子进程正在运行中,name=%s, age=%d, pid=%d" %(name, age, os.getpid()))
7. print(kwargs)
8. time.sleep(0.2)
9.
10.if __name__ == '__main__':
11. # 创建 Process 对象
12. p = Process(target=pro_func, args=('小明',18), kwargs={'m': 20})
13. # 启动进程
14. p.start()
15. time.sleep(1)
16. # 1 秒钟之后,立刻结束子进程
17. p.terminate()
18. p.join()
注意:进程间不共享全局变量。
进程之间的通信-Queue
在初始化Queue()对象时,(例如 q=Queue(),若在括号中没有指定最大可接受的消息数量,或数
量为负值时,那么就代表可接受的消息数量没有上限-直到内存的尽头)
- Queue.qsize():返回当前队列包含的消息数量。
- Queue.empty():如果队列为空,返回 True,反之 False。
- Queue.full():如果队列满了,返回 True,反之 False。
- Queue.get([block[,timeout]]):获取队列中的一条消息,然后将其从队列中移除,block 默认值为True。
- 如果 block 使用默认值,且没有设置 timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了 timeout,则会等待timeout 秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
- 如果 block 值为 False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;
- Queue.get_nowait():相当 Queue.get(False);
- Queue.put(item,[block[, timeout]]):将 item 消息写入队列,block 默认值为 True;
- 如果 block 使用默认值,且没有设置 timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了 timeout,则会等待timeout 秒,若还没空间,则抛出"Queue.Full"异常;
- 如果 block 值为 False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常;
- Queue.put_nowait(item):相当 Queue.put(item, False);
进程间通信
1.from multiprocessing import Process, Queueimport os, time, random
2.# 写数据进程执行的代码:def write(q):
3. for value in ['A', 'B', 'C']:
4. print('Put %s to queue...' % value)
5. q.put(value)
6. time.sleep(random.random())
7.# 读数据进程执行的代码:def read(q):
8. while True:
9. if not q.empty():
10. value = q.get(True)
11. print('Get %s from queue.' % value)
12. time.sleep(random.random())
13. else:
14. break
15.if __name__=='__main__':
16. # 父进程创建 Queue,并传给各个子进程:
17. q = Queue()
18. pw = Process(target=write, args=(q,))
19. pr = Process(target=read, args=(q,))
20. # 启动子进程 pw,写入:
21. pw.start()
22. # 等待 pw 结束:
23. pw.join()
24. # 启动子进程 pr,读取:
25. pr.start()
26. pr.join()
27. # pr 进程里是死循环,无法等待其结束,只能强行终止:
28. print('')
29. print('所有数据都写入并且读完')
进程池 Pool
1.# -*- coding:utf-8 -*-
2.from multiprocessing import Poolimport os, time, random
3.def worker(msg):
4. t_start = time.time()
5. print("%s 开始执行,进程号为%d" % (msg,os.getpid()))
6. # random.random()随机生成 0~1 之间的浮点数
7. time.sleep(random.random()*2)
8. t_stop = time.time()
9. print(msg,"执行完毕,耗时%0.2f" % (t_stop-t_start))
10.
11.po = Pool(3) # 定义一个进程池,最大进程数 3
12.for i in range(0,10):
13. # Pool().apply_async(要调用的目标,(传递给目标的参数元祖,))
14. # 每次循环将会用空闲出来的子进程去调用目标
15. po.apply_async(worker,(i,))
16.
17.print("----start----")
18.po.close() # 关闭进程池,关闭后 po 不再接收新的请求
19.po.join() # 等待 po 中所有子进程执行完成,必须放在 close 语句之后
20.print("-----end-----")
multiprocessing.Pool 常用函数解析
- apply_async(func[, args[, kwds]]) :使用非阻塞方式调用 func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args 为传递给 func 的参数列表,kwds 为传递给 func的关键字参数列表;
- close():关闭 Pool,使其不再接受新的任务;
- terminate():不管任务是否完成,立即终止;
- join():主进程阻塞,等待子进程的退出, 必须在 close 或 terminate 之后使用;
进程池中使用 Queue
如果要使用 Pool 创建进程,就需要使用 multiprocessing.Manager()中的 Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
1.from multiprocessing import Manager,Poolimport os,time,random
2.def reader(q):
3. print("reader 启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
4. for i in range(q.qsize()):
5. print("reader 从 Queue 获取到消息:%s" % q.get(True))
6.def writer(q):
7. print("writer 启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
8. for i in "itcast":
9. q.put(i)
10.if __name__=="__main__":
11. print("(%s) start" % os.getpid())
12. q = Manager().Queue() # 使用 Manager 中的 Queue
13. po = Pool()
14. po.apply_async(writer, (q,))
15.
16. time.sleep(1) # 先让上面的任务向 Queue 存入数据,然后再让下面的任务开始从中取数据
17.
18. po.apply_async(reader, (q,))
19. po.close()
20. po.join()
21. print("(%s) End" % os.getpid())
谈谈你对多进程,多线程,以及协程的理解,项目是否用?
- 进程:一个运行的程序(代码)就是一个进程,没有运行的代码叫程序,进程是系统资源分配的最小单位,进程拥有自己独立的内存空间,所以进程间数据不共享,开销大。
- 线程: 调度执行的最小单位,也叫执行路径,不能独立存在,依赖进程存在,一个进程至少有一个线程,叫主线程,而多个线程共享内存(数据共享,共享全局变量),从而极大地提高了程序的运行效率。
- 协程:是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
什么是多线程竞争?
线程是非独立的,同一个进程里线程是数据共享的,当各个线程访问数据资源时会出现竞争状态。即:数据几乎同步会被多个线程占用,造成数据混乱,即所谓的线程不安全。那么怎么解决多线程竞争问题?-- 锁
锁(Lock)是 Python 提供的对线程控制的对象。有互斥锁、可重入锁。
- 锁的好处:确保了某段关键代码(共享数据资源)只能由一个线程从头到尾完整地执行能解决多线程资源竞争下的原子操作问题。
- 锁的坏处:阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了锁的致命问题:死锁。
什么是死锁呢?
若干子线程在系统资源竞争时,都在等待对方对某部分资源解除占用状态,结果是谁也不愿先解锁,互相干等着,程序无法执行下去,这就是死锁。
GIL 锁(有时候,面试官不问,你自己要主动说,增加 b 格,尽量别一问一答的尬聊,不然最后等到的一句话就是:你还有什么想问的么?)
GIL锁 全局解释器锁(只在 cpython 里才有)
作用:限制多线程同时执行,保证同一时间只有一个线程执行,所以 cpython 里的多线程其实是伪多线程!
所以 Python 里常常使用协程技术来代替多线程,协程是一种更轻量级的线程,进程和线程的切换时由系统决定,而协程由我们程序员自己决定,而模块 gevent 下切换是遇到了耗时操作才会切换。
三者的关系:进程里有线程,线程里有协程。
什么是线程安全,什么是互斥锁?
每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
同一个进程中的多线程之间是共享系统资源的,多个线程同时对一个对象进行操作,一个线程操作尚未结束,另一个线程已经对其进行操作,导致最终结果出现错误,此时需要对被操作对象添加互斥锁,保证每个线程对该对象的操作都得到正确的结果。
为什么要用线程池?
- 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
说说下面几个概念:同步,异步,阻塞,非阻塞?
- 同步:多个任务之间有先后顺序执行,一个执行完下个才能执行。
- 异步:多个任务之间没有先后顺序,可以同时执行有时候一个任务可能要在必要的时候获取另一个同时执行的任务的结果,这个就叫回调!
- 阻塞:如果卡住了调用者,调用者不能继续往下执行,就是说调用者阻塞了。
- 非阻塞:如果不会卡住,可以继续执行,就是说非阻塞的。
同步异步相对于多任务而言,阻塞非阻塞相对于代码执行而言。
什么是僵尸进程和孤儿进程?怎么避免僵尸进程?
- 孤儿进程:父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
- 僵尸进程:进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中的这些进程是僵尸进程。
- 避免僵尸进程的方法:
- fork 两次用孙子进程去完成子进程的任务;
- 用 wait()函数使父进程阻塞;
- 使用信号量,在 signal handler 中调用 waitpid,这样父进程不用阻塞。
- 避免僵尸进程的方法:
Python 中的进程与线程的使用场景?
多进程适合在 CPU 密集型操作(cpu 操作指令比较多,如位数多的浮点运算)。
多线程适合在 IO 密集型操作(读写数据操作较多的,比如爬虫)。
并行(parallel)和并发(concurrency)?
并行:同一时刻多个任务同时在运行。
并发:在同一时间间隔内多个任务都在运行,但是并不会在同一时刻同时运行,存在交替执行的情况。
实现并行的库有:multiprocessing
实现并发的库有:threading
程序需要执行较多的读写、请求和回复任务的需要大量的 IO 操作,IO 密集型操作使用并发更好。CPU 运算量大的程序使用并行会更好。
线程是并发还是并行,进程是并发还是并行?
线程是并发,进程是并行;
进程之间相互独立,是系统分配资源的最小单位,同一个进程中的所有线程共享资源。
IO 密集型和 CPU 密集型区别?
IO 密集型:系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存)的读/写。
CPU 密集型:大部份时间用来做计算、逻辑判断等 CPU 动作的程序称之 CPU 密集型。
网络编程
更多网络编程相关内容详见:
socket
--- 底层网络接口
UDP 总结
udp 客户端:
- 创建客户端套接字
- 发送/接收数据
- 关闭套接字
1.import socket
2.def main():
3. # 1、创建 udp 套接字
4. # socket.AF_INET 表示 IPv4 协议 AF_INET6 表示 IPv6 协议
5. # socket.SOCK_DGRAM 数据报套接字,只要用于 udp 协议
6. udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
7.
8. # 2、准备接收方的地址
9. # 元组类型 ip 是字符串类型 端口号是整型
10. dest_addr = ('192.168.113.111', 8888)
11. # 要发送的数据
12. send_data = "我是要发送的数据"
13. # 3、发送数据
14. udp_socket.sendto(send_data.encode("utf-8"), dest_addr)
15. # 4、等待接收方发送的数据 如果没有收到数据则会阻塞等待,直到收到数据
16. # 接收到的数据是一个元组 (接收到的数据, 发送方的 ip 和端口)
17. # 1024 表示本次接收的最大字节数
18. recv_data, addr = udp_socket.recvfrom(1024)
19. # 5、关闭套接字
20. udp_socket.close()
21.if __name__ == '__main__':
22. main()
编码的转换
str -->bytes: encode() 编码
bytes--> str: decode() 解码
UDP 服务器端:
- 创建 socket 套接字
- 绑定端口号
- 接收/发送数据
- 关闭套接字
1.import socket
2.def main():
3. # 1、创建 udp 套接字
4. # socket.AF_INET 表示 IPv4 协议 AF_INET6 表示 IPv6 协议
5. # socket.SOCK_DGRAM 数据报套接字,只要用于 udp 协议
6. udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
7. # 2、绑定端口
8. # 元组类型 ip 一般不写 表示本机的任何的一个 ip
9. local_addr = ('', 7777)
10. udp_socket.bind(local_addr)
11. # 3、准备接收方的地址
12. # 元组类型 ip 是字符串类型 端口号是整型
13. dest_addr = ('192.168.113.111', 8888)
14. # 要发送的数据
15. send_data = "我是要发送的数据"
16. # 4、发送数据
17. udp_socket.sendto(send_data.encode("utf-8"), dest_addr)
18. # 5、等待接收方发送的数据 如果没有收到数据则会阻塞等待,直到收到数据
19. # 接收到的数据是一个元组 (接收到的数据, 发送方的 ip 和端口)
20. # 1024 表示本次接收的最大字节数
21. recv_data, addr = udp_socket.recvfrom(1024)
22. # 6、关闭套接字
23. udp_socket.close()
24.if __name__ == '__main__':
25. main()
注意点:绑定端口要在发送数据之前进行绑定。
TCP 总结
TCP 客户端
- 创建 TCP 的 socket 套接字
- 连接服务器
- 发送数据给服务器端
- 接收服务器端发送来的消息
- 关闭套接字
1.import socket
2.def main():
3. # 1、创建客户端的 socket
4. # socket.AF_INET 表示 IPv4 协议 AF_INET6 表示 IPv6 协议
5. # socket.SOCK_STREAM 流式套接字,只要用于 TCP 协议
6. client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
7. # 2、构建目标地址
8. server_ip = input("请输入服务器端的 IP 地址:")
9. server_port = int(input("请输入服务器端的端口号:"))
10. # 3、连接服务器
11. # 参数:元组类型 ip 是字符串类型 端口号是整型
12. client_socket.connect((server_ip, server_port))
13. # 要发送给服务器端的数据
14. send_data = "我是要发送给服务器端的数据"
15. # 4、发送数据
16. client_socket.send(send_data.encode("gbk"))
17. # 5、接收服务器端恢复的消息, 没有消息会阻塞
18. # 1024 表示接收的最大字节数
19. recv_date= client_socket.recv(1024)
20. print("接收到的数据是:", recv_date.decode('gbk'))
21. # 6、关闭套接字
22. client_socket.close()
23.if __name__ == '__main__':
24. main()
TCP 服务器端
- 创建 TCP 服务端的 socket
- bing 绑定 ip 地址和端口号
- listen 使套接字变为被动套接字
- accept 取出一个客户端连接,用于服务
- recv/send 接收和发送消息
- 关闭套接字
1.import socket
2.
3.def main():
4. # 1、创建 tcp 服务端的 socket
5. server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
6.
7. # 2、绑定
8. server_socket.bind(('', 8888))
9.
10. # 3、listen 使套接字变为被动套接字
11. server_socket.listen(128)
12.
13. # 4、如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务
14. # client_socket 用来为这个客户端服务
15. # tcp_server_socket 就可以省下来专门等待其他新客户端的链接
16. client_socket, client_addr = server_socket.accept()
17.
18. # 5、接收客户端发来的消息
19. recv_data = client_socket.recv(1024)
20. print("接收到客户端%s 的数据:%s" % (str(client_addr), recv_data.decode('gbk')))
21.
22. # 6、回复数据给客户端
23. client_socket.send("收到消息".encode('gbk'))
24.
25. # 7、关闭套接字
26. client_socket.close()
27. server_socket.close()
28.
29.if __name__ == '__main__':
30. main()
注意点:
- tcp 服务器一般都需要绑定,否则客户端找不到服务器
- tcp 客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的 ip、port 等信息就好,本地客户端可以随机
- tcp 服务器中通过 listen 可以将 socket 创建出来的主动套接字变为被动的,这是做 tcp 服务器时必须要做的
- 当客户端需要链接服务器时,就需要使用 connect 进行链接,udp 是不需要链接的而是直接发送,但是 tcp 必须先链接,只有链接成功才能通信
- 当一个 tcp 客户端连接服务器时,服务器端会有 1 个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen 后的套接字是被动套接字,用来接收新的客户端的连接请求的,而 accept 返回的新套接字是标识这个新客户端的
- 关闭 listen 后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
- 关闭 accept 返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用 close 后,服务器端会 recv 解阻塞,并且返回的长度为 0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线;同理,当服务器断开 tcp 连接的时候客户端同样也会收到 0 字节数据。
怎么实现强行关闭客户端和服务器之间的连接?
在 socket 通信过程中不断循环检测一个全局变量(开关标记变量),一旦标记变量变为关闭,则调用 socket 的 close 方法,循环结束,从而达到关闭连接的目的。
简述 TCP 和 UDP 的区别以及优缺点?
UDP 是面向无连接的通讯协议,UDP 数据包括目的端口号和源端口号信息。
优点:UDP 速度快、操作简单、要求系统资源较少,由于通讯不需要连接,可以实现广播发送
缺点:UDP 传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,也不重复发送,不可靠。
TCP 是面向连接的通讯协议,通过三次握手建立连接,通讯完成时四次挥手
优点:TCP 在数据传递时,有确认、窗口、重传、阻塞等控制机制,能保证数据正确性,较为可靠。
缺点:TCP 相对于 UDP 速度慢一点,要求系统资源较多。
简述浏览器通过 WSGI 请求动态资源的过程?
- 发送 http 请求动态资源给 web 服务器
- web 服务器收到请求后通过 WSGI 调用一个属性给应用程序框架
- 应用程序框架通过引用 WSGI 调用 web 服务器的方法,设置返回的状态和头信息。
- 调用后返回,此时 web 服务器保存了刚刚设置的信息
- 应用程序框架查询数据库,生成动态页面的 body 的信息
- 把生成的 body 信息返回给 web 服务器
- web 服务器把数据返回给浏览器
描述用浏览器访问 www.baidu.com 的过程
- 要先使用 rarp 获取默认网关的 mac 地址
- 组织数据发送给默认网关(ip 还是 dns 服务器的 ip,但是 mac 地址是默认网关的 mac 地址)
- 默认网关拥有转发数据的能力,把数据转发给路由器
- 路由器根据自己的路由协议,来选择一个合适的较快的路径转发数据给目的网关
- 目的网关(dns 服务器所在的网关),把数据转发给 dns 服务器
- dns 服务器查询解析出 baidu.com 对应的 ip 地址,并原路返回请求这个域名的 client 得到了 baidu.com 对应的 ip 地址之后,会发送 tcp 的 3 次握手,进行连接
- 使用 http 协议发送请求数据给 web 服务器
- web 服务器收到数据请求之后,通过查询自己的服务器得到相应的结果,原路返回给浏览器。
- 浏览器接收到数据之后通过浏览器自己的渲染功能来显示这个网页。
- 浏览器关闭 tcp 连接,即 4 次挥手结束,完成整个访问过程
Post 和 Get 请求的区别?
GET 请求:请求的数据会附加在 URL 之后,以?分割 URL 和传输数据,多个参数用&连接。URL 的编码格式采用的是 ASCII 编码,而不是 uniclde,即是说所有的非 ASCII 字符都要编码之后再传输。
POST 请求:POST 请求会把请求的数据放置在 HTTP 请求包的包体中。上面的 item=bandsaw 就是实际的传输数据。
GET 请求的数据会暴露在地址栏中,而 POST 请求则不会。
传输数据的大小:在 HTTP 规范中,没有对 URL 的长度和传输的数据大小进行限制。但是在实际开发过程中,对于 GET,特定的浏览器和服务器对 URL 的长度有限制。因此,在使用 GET 请求时,传输数据会受到 URL 长度的限制。对于 POST,由于不是 URL 传值,理论上是不会受限制的,但是实际上各个服务器会规定对 POST提交数据大小进行限制,Apache、IIS 都有各自的配置。
安全性:POST 的安全性比 GET 的高。这里的安全是指真正的安全,而不同于上面 GET 提到的安全方法中的安全,上面提到的安全仅仅是不修改服务器的数据。比如,在进行登录操作,通过 GET 请求,用户名和密码都会暴露再 URL 上,因为登录页面有可能被浏览器缓存以及其他人查看浏览器的历史记录的原因,此时的用户名和密码就很容易被他人拿到了。除此之外,GET 请求提交的数据还可能会造成 Cross-site request frogery 攻击。
效率:GET 比 POST 效率高。
POST 请求的过程
- 浏览器请求 tcp 连接(第一次握手)
- 服务器答应进行 tcp 连接(第二次握手)
- 浏览器确认,并发送 post 请求头(第三次握手,这个报文比较小,所以 http 会在此时进行第一次数据发送)
- 服务器返回 100 continue 响应
- 浏览器开始发送数据
- 服务器返回 200 ok 响应
GET 请求的过程
- 浏览器请求 tcp 连接(第一次握手)
- 服务器答应进行 tcp 连接(第二次握手)
- 浏览器确认,并发送 get 请求头和数据(第三次握手,这个报文比较小,所以 http 会在此时进行第一次数据发送)
- 服务器返回 200 OK 响应
cookie 和 session 的区别?
- cookie 数据存放在客户的浏览器上,session 数据放在服务器上。
- cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗考虑到安全应当使用session。
- session 会在一定时间内保存在服务器上。当访问增多,会比较占用服务器的性能考虑到减轻服务器性能方面,应当使用 cookie。
- 单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie。
建议: 将登陆信息等重要信息存放为 SESSION;其他信息如果需要保留,可以放在 cookie 中
HTTP 协议状态码有什么用,列出你知道的 HTTP 协议的状态码,然后讲出他们都表示什么意思?
通过状态码告诉客户端服务器的执行状态,以判断下一步该执行什么操作。
常见的状态机器码有:
- 100-199:表示服务器成功接收部分请求,要求客户端继续提交其余请求才能完成整个处理过程。
- 200-299:表示服务器成功接收请求并已完成处理过程,常用 200(OK 请求成功)。
- 300-399:为完成请求,客户需要进一步细化请求。302(所有请求页面已经临时转移到新的 url)。304、307(使用缓存资源)。
- 400-499:客户端请求有错误,常用 404(服务器无法找到被请求页面),403(服务器拒绝访问,权限不够)。
- 500-599:服务器端出现错误,常用 500(请求未完成,服务器遇到不可预知的情况)。
请简单说一下三次握手和四次挥手?
三次握手过程:
- 首先客户端向服务端发送一个带有 SYN 标志,以及随机生成的序号 100(0 字节)的报文
- 服务端收到报文后返回一个报文(SYN200(0 字节),ACk1001(字节+1))给客户端
- 客户端再次发送带有 ACk 标志 201(字节+)序号的报文给服务端
至此三次握手过程结束,客户端开始向服务端发送数据。
补充:SYN:请求询问,ACk:回复,回应。
- 客户端向服务端发起请求:我想给你通信,你准备好了么?
- 服务端收到请求后回应客户端:I'ok,你准备好了么
- 客户端礼貌的再次回一下客户端:准备就绪,咱们开始通信吧!
整个过程跟打电话的过程一模一样:1 喂,你在吗 2 在,我说的你听得到不 3 恩,听得到(接下来请开始你的表演)
四次挥手过程:
由于 TCP 连接是可以双向通信的(全双工),因此每个方向都必须单独进行关闭(这句话才是精辟,后面四个挥手过程都是其具体实现的语言描述)
四次挥手过程,客户端和服务端都可以先开始断开连接
- 客户端发送带有 fin 标识的报文给服务端,请求通信关闭
- 服务端收到信息后,回复 ACK 答应关闭客户端通信(连接)请求
- 服务端发送带有 fin 标识的报文给客户端,也请求关闭通信
- 客户端回应 ack 给服务端,答应关闭服务端的通信(连接)请求
说一下什么是 tcp 的 2MSL?
主动发送 fin 关闭的一方,在 4 次挥手最后一次要等待一段时间我们称这段时间为 2MSL。TIME_WAIT 状态的存在有两个理由:
- 让 4 次挥手关闭流程更加可靠
- 防止丢包后对后续新建的正常连接的传输造成破坏
为什么客户端在 TIME-WAIT 状态必须等待 2MSL 的时间?
- 为了保证客户端发送的最后一个 ACK 报文段能够达到服务器。这个 ACK 报文段可能丢失,因而使处在 LAST-ACK 状态的服务器收不到确认。服务器会超时重传 FIN+ACK 报文段,客户端就能在 2MSL 时间内收到这个重传的 FIN+ACK 报文段,接着客户端重传一次确认,重启计时器。最好,客户端和服务器都正常进入到 CLOSED 状态。如果客户端在 TIME-WAIT 状态不等待一段时间,而是再发送完 ACK 报文后立即释放连接,那么就无法收到服务器重传的 FIN+ACK 报文段,因而也不会再发送一次确认报文。这样,服务器就无法按照正常步骤进入 CLOSED 状态。
- 防止已失效的连接请求报文段出现在本连接中。客户端在发送完最后一个 ACK 确认报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
其他
说说 HTTP 和 HTTPS 区别?
HTTP 协议传输的数据都是未加密的,也就是明文的,因此使用 HTTP 协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了 SSL(Secure Sockets Layer)协议用于
对 HTTP 协议传输的数据进行加密,从而就诞生了 HTTPS。简单来说,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,要比 http 协议安全。
HTTPS 和 HTTP 的区别主要如下:
- https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用。
- http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议。
- http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。
- http 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全。
谈一下 HTTP 协议以及协议头部中表示数据类型的字段?
HTTP 协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web)服务器传输超文本到本地浏览器的传送协议。
HTTP 是一个基于 TCP/IP 通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。
HTTP 是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990 年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在 WWW 中使用的是 HTTP/1.0 的第六版,HTTP/1.1 的规范化工作正在进行之中,而且 HTTP-NG(Next Generation of HTTP)的建议已经提出。
HTTP 协议工作于客户端-服务端架构为上。浏览器作为 HTTP 客户端通过 URL 向 HTTP 服务端即 WEB 服务器发送所有请求。Web 服务器根据接收到的请求后,向客户端发送响应信息。
表示数据类型字段: Content-Type
HTTP 请求方法都有什么?
根据 HTTP 标准,HTTP 请求可以使用多种请求方法。
HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD 方法。
HTTP1.1 新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
- GET 请求指定的页面信息,并返回实体主体。
- HEAD 类似于 get 请求,只不过返回的响应中没有具体的内容,用于获取报头。
- POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。
- PUT 从客户端向服务器传送的数据取代指定的文档的内容。
- DELETE请求服务器删除指定的页面。
- CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
- OPTIONS 允许客户端查看服务器的性能。
- TRACE 回显服务器收到的请求,主要用于测试或诊断。
使用 Socket 套接字需要传入哪些参数 ?
- Address Family表示套接字应用场景
- Type表示类型。
- family
- AF_UNIX(Unix 域,用于同一台机器上的进程间通讯)
- AF_INET(对于 IPV4 协议的 TCP 和 UDP)
- type 参数
- SOCK_STREAM(流套接字)
- SOCK_DGRAM(数据报文套接字)
- SOCK_RAW(raw 套接字)
HTTP 常见请求头?
- Host (主机和端口号)
- Connection (链接类型)
- Upgrade-Insecure-Requests (升级为 HTTPS 请求)
- User-Agent (浏览器名称)
- Accept (传输文件类型)
- Referer (页面跳转处)
- Accept-Encoding(文件编解码格式)
- Cookie (Cookie)
- x-requested-with :XMLHttpRequest (是 Ajax 异步请求)
七层模型? IP ,TCP/UDP ,HTTP ,RTSP ,FTP 分别在哪层?
- IP: 网络层
- TCP/UDP: 传输层
- HTTP、RTSP、FTP: 应用层协议
url 的形式?
形式: scheme://host[:port#]/path/…/[?query-string][#anchor]
- scheme:协议(例如:http, https, ftp)
- host:服务器的 IP 地址或者域名
- port:服务器的端口(如果是走协议默认端口,80 or 443)
- path:访问资源的路径
- query-string:参数,发送给 http 服务器的数据
- anchor:锚(跳转到网页的指定锚点位置)
其他
面向对象和面向过程的区别
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以 易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。
代码优化从哪些方面考虑?有什么想法?
-
优化算法时间复杂度。
算法的时间复杂度对程序的执行效率影响最大,在 Python 中可以通过选择合适的数据结构来优化时间复杂度,如 list 和 set 查找某一个元素的时间复杂度分别是 O(n)和 O(1)。不同的场景有不同的优化方式,总得来说,一般有分治,分支界限,贪心,动态规划等思想。
-
减少冗余数据。
-
合理使用 copy 与 deepcopy。
-
使用 dict 或 set 查找元素。
set 的 union,intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的问题可以转换为 set 来操作。
-
合理使用生成器(generator)和 yield。
-
优化循环。
每种编程语言都会强调需要优化循环。当使用 Python 的时候,你可以依靠大量的技巧使得循环运行得更快。然而,开发者经常漏掉的一个方法是:避免在一个循环中使用点操作。每一次你调用方法 str.upper,Python 都会求该方法的值。然而,如果你用一个变量代替求得的值,值就变成了已知的,Python 就可以更快地执行任务。优化循环的关键,是要减少 Python 在循环内部执行的工作量,因为 Python 原生的解释器在那种情况下,真的会减缓执行的速度。(注意:优化循环的方法有很多,这只是其中的一个。例如,许多程序员都会说,列表推导是在循环中提高执行速度的最好方式。这里的关键是,优化循环是程序取得更高的执行速度的更好方式之一。)
-
优化包含多个判断表达式的顺序。
-
使用 join 合并迭代器中的字符串。
-
选择合适的格式化字符方式。
-
不借助中间变量交换两个变量的值。
-
使用 if is。
-
使用级联比较 x < y < z。
-
while 1 比 while True 更快。
-
使用**而不是 pow。
-
使用 cProfile, cStringIO 和 cPickle 等用 c 实现相同功能(分别对应 profile, StringIO, pickle) 的包。
-
使用最佳的反序列化方式。
-
使用 C 扩展(Extension)。
-
并行编程
因为 GIL 的存在,Python 很难充分利用多核 CPU 的优势。但是,可以通过内置的模块multiprocessing 实现下面几种并行模式:
-
多进程:对于 CPU 密集型的程序,可以使用 multiprocessing 的 Process,Pool 等封装好的类,通过多进程的方式实现并行计算。但是因为进程中的通信成本比较大,对于进程之间需要大量数据交互的程序效率未必有大的提高。
-
多线程:对于 IO 密集型的程序,multiprocessing.dummy 模块使用 multiprocessing 的接口封装 threading,使得多线程编程也变得非常轻松(比如可以使用 Pool 的 map 接口,简洁高效)。
-
分布式:multiprocessing 中的 Managers 类提供了可以在不同进程之共享数据的方式,可以在此基础上开发出分布式的程序
不同的业务场景可以选择其中的一种或几种的组合实现程序性能的优化。
-
-
使用性能分析工具
除了上面在 ipython 使用到的 timeit 模块,还有 cProfile。cProfile 的使用方式也非常简单:python-mcProfile,filename.py 是要运行程序的文件名,可以在标准输出中看到每一个函数被调用的次数和运行的时间,从而找到程序的性能瓶颈,然后可以有针对性地优化。
-
PyPy
PyPy 是用 RPython(CPython 的子集)实现的 Python,根据官网的基准测试数据,它比 CPython实现的 Python 要快 6 倍以上。快的原因是使用了 Just-in-Time(JIT)编译器,即动态编译器,与静态编译器(如 gcc,javac 等)不同,它是利用程序运行的过程的数据进行优化。由于历史原因,目前 pypy中还保留着 GIL,不过正在进行的 STM 项目试图将 PyPy 变成没有 GIL 的 Python。如果 python程序中含有 C 扩展(非 cffi 的方式),JIT 的优化效果会大打折扣,甚至比 CPython 慢(比 Numpy)。所以在 PyPy 中最好用纯 Python 或使用 cffi 扩展。
三、前端
Html
CSS
什么是 CSS 初始化?有什么好处?
CSS 初始化是指重设浏览器的样式。不同的浏览器默认的样式可能不尽相同,如果没对 CSS 初始化往往会出现浏览器之间的页面差异。
好处:能够统一标签在各大主流浏览器中的默认样式,使得我们开发网页内容时更加方便简洁,同时减少 CSS 代码量,节约网页下载时间。
简述浮动的特征和清除浮动的方法?
浮动的特征:
- 浮动元素有左浮动(float:left)和右浮动(float:right)两种。
- 浮动的元素会向左或向右浮动,碰到父元素边界、其他元素才停下来。
- 相邻浮动的块元素可以并在一行,超出父级宽度就换行。
- 浮动让行内元素或块元素转化为有浮动特性的行内块元素(此时不会有行内块元素间隙问题)。
- 父元素如果没有设置尺寸(一般是高度不设置),父元素内整体浮动的子元素无法撑开父元素,父元素需要清除浮动。
清除浮动的方法:
-
父级上增加属性 overflow:hidden。
-
在最后一个子元素的后面加一个空的 div,给它样式属性 clear:both。
-
使用成熟的清浮动样式类,clearfix。
1. .clearfix:after,.clearfix:before{ content: "";display: table;} 2. .clearfix:after{ clear:both;} 3. .clearfix{zoom:1;}
JavaScript
AJAX 是什么?如何使用 AJAX?
ajax(异步的 javascript 和 xml) 能够刷新局部网页数据而不是重新加载整个网页。
- 创建 xmlhttprequest 对象,var xmlhttp =new XMLHttpRequest();XMLHttpRequest对象用来和服务器交换数据。
- 使用 xmlhttprequest 对象的 open()和 send()方法发送资源请求给服务器。
- 使用 xmlhttprequest 对象的 responseText 或 responseXML 属性获得服务器的响应。
- onreadystatechange 函数,当发送请求到服务器,我们想要服务器响应执行一些功能就需要使用onreadystatechange 函数,每次 xmlhttprequest 对象的 readyState 发生改变都会触发onreadystatechange 函数。
jQurey
vue.js
四、Web
Flask
Flask 中正则 URL 的实现?
@app.route('
-
写正则类,继承 BaseConverter,将匹配到的值设置为 regex 的值。
1. class RegexUrl(BaseConverter): 2. def __init__(self, url_map, *args): 3. super(RegexUrl, self).__init__(url_map) 4. self.regex = args[0]
-
把正则类赋值给我们定义的正则规则。
5. app.url_map.converters['re'] = RegexUrl
-
在 URL 中使用正则。
6. @app.route('/regex/<re("[a-z]{3}"):id>') 7. def regex111(id): 8. return 'id:%s'%id
Flask 中请求上下文和应用上下文的区别和作用?
current_app、g 是应用上下文。
request、session 是请求上下文。
手动创建上下文的两种方法:
1. with app.app_context()
2. app = current_app._get_current_object()
两者区别:
请求上下文:保存了客户端和服务器交互的数据。
应用上下文:flask 应用程序运行过程中,保存的一些配置信息,比如程序名、数据库连接、应用信息等。
两者作用:
请求上下文(request context):Flask 从客户端收到请求时,要让视图函数能访问一些对象,这样才能处理请求。请求对象是一个很好的例子,它封装了客户端发送的 HTTP 请求。要想让视图函数能够访问请求对象,一个显而易见的方式是将其作为参数传入视图函数,不过这会导致程序中的每个视图函数都增加一个参数,除了访问请求对象,如果视图函数在处理请求时还要访问其他对象,情况会变得更糟。为了避免大量可有可无的参数把视图函数弄得一团糟,Flask使用上下文临时把某些对象变为全局可访问。
应用上下文(application context):它的字面意思是应用上下文,但它不是一直存在的,它只是 request context 中的一个对 app的代理(人),所谓 local proxy。它的作用主要是帮助 request 获取当前的应用,它是伴 request 而生,随 request 而灭的。
Flask 中数据库设置?
名字 | 备注 |
---|---|
SQLALCHEMY_DATABASE_URI | 用于连接的数据库URI。例如:sqlite:/ll/tmp/test.dbmysql://username:password@server/db |
SQLALCHEMY_BINDS | —个映射binds 到连接URI的字典。更多binds 的信息见用Binds操作多个数据库。 |
SQLALCHEMY_ECHO | 如果设置为Ture , SQLAlchemy 会记录所有发给stderr 的语句,这对调试有用。(打印sql语句) |
SQLALCHEMY_RECORD_QUERIES | 可以用于显式地禁用或启用查询记录。查询记录在调试或测试模式自动启用。更多信息见get_debug_querieso. |
SQLALCHEMY_NATIVE_UNICODE | 可以用于显式禁用原生unicode 支持。当使用不合适的指定无编码的数据库默认值时,这对于一些数据库适配器是必须的(比如Ubuntu 上某些版本的PostgresQL )。 |
SQLALCHEMY_POOL_SlZE | 数据库连接池的大小。默认是引擎默认值(通常是5) |
SQLALCHEMY_POOL_TIMEOUT | 设定连接池的连接超时时间。默认是10。 |
SQLALCHEMY_POOL_RECYCLE | 多少秒后自动回收连接。这对 MySQL是必要的,它默认移除闲置多于8小时的连接。注意如果使用了MySQL , Flask-SQLALchemy自动设定这个值为2小时。 |
SQLALCHEMY_COMMIT_ON_TEARDOWN | 可以配置请求执行完逻辑之后自动提交,而不用我们每次都手动调用 session.commit() |
SQLALCHEMY_TRACK_MODIFICATIONS | 显示打印的数据以及 sql 语句,建议不设置,默认为 False |
常用的 SQLAlchemy 查询过滤器?
过滤器 | 说明 |
---|---|
filter() | 把过滤器添加到原查询上,返回一个新查询 |
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询 |
limit | 使用指定的值限定原查询返回的结果 |
offset() | 偏移原查询返回的结果,返回一个新查询 |
order_by() | 根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() | 根据指定条件对原查询结果进行分组,返回一个新查询 |
对 Flask 蓝图(Blueprint)的理解?
蓝图的定义
蓝图 /Blueprint 是 Flask 应用程序组件化的方法,可以在一个应用内或跨越多个项目共用蓝图。使用蓝图可以极大地简化大型应用的开发难度,也为 Flask 扩展提供了一种在应用中注册服务的集中式机制。
蓝图的应用场景
- 把一个应用分解为一个蓝图的集合。这对大型应用是理想的。一个项目可以实例化一个应用对象,初始化几个扩展,并注册一集合的蓝图。
- 以 URL 前缀和/或子域名,在应用上注册一个蓝图。 URL 前缀/子域名中的参数即成为这个蓝图下的所有视图函数的共同的视图参数(默认情况下)。
- 在一个应用中用不同的 URL 规则多次注册一个蓝图。
- 通过蓝图提供模板过滤器、静态文件、模板和其它功能。一个蓝图不一定要实现应用或者视图函数。
- 初始化一个 Flask 扩展时,在这些情况中注册一个蓝图。
蓝图的缺点
不能在应用创建后撤销注册一个蓝图而不销毁整个应用对象。
使用蓝图的三个步骤
-
创建 一个蓝图对象
blue = Blueprint("blue",__name__)
-
在这个蓝图对象上进行操作 ,例如注册路由、指定静态文件夹、注册模板过滤器...
@blue.route('/') def blue_index(): return 'Welcome to my blueprint'
-
在应用对象上注册这个蓝图对象
app.register_blueprint(blue,url_prefix='/blue')
Flask 中 WTF 表单数据验证?
在 Flask 中,为了处理 web 表单,我们一般使用 Flask-WTF 扩展,它封装了 WTForms,并且它有验证表单数据的功能。
WTForms 支持的 HTML 标准字段:
字段对象 | 说明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文件字段 |
DateField | 文本字段,值为 datetime.date 文本格式 |
DateTimeField | 文本字段,值为 datetime.datetime 文本格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为 decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为 True 和 False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMutipleField | 下拉列表,可选择多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
WTForms 常用验证函数:
验证函数 | 说明 |
---|---|
InputRequired | 确保字段中有数据 |
DataRequired | 确保字段中有数据并且数据为真 |
EqualTo | 比较两个字段的值,常用于比较两次密码输入 |
Length | 验证输入的字符串长度 |
NumberRange | 验证输入的值在数字范围内 |
URL | 验证 URL |
AnyOf | 验证输入值在可选列表中 |
NoneOf | 验证输入值不在可选列表中 |
使用 Flask-WTF 需要配置参数 SECRET_KEY。
CSRF_ENABLED 是为了 CSRF(跨站请求伪造)保护。 SECRET_KEY 用来生成加密令牌,当 CSRF 激活的时候,该设置会根据设置的密匙生成加密令牌。
Flask 项目中如何实现 session 信息的写入?
Flask 中有三个 session:
- 数据库中的 session,例如:db.session.add()
- 在 flask_session 扩展中的 session,使用:from flask_session import Session,使用第三方扩展的 session 可以把信息存储在服务器中,客户端浏览器中只存储 sessionid。
- flask 自带的 session,是一个请求上下文, 使用:from flask import session。自带的session 把信息加密后都存储在客户端的浏览器 cookie 中。
项目接口实现后路由访问不到怎么办?
- 可以通过 postman 测试工具测试
- 看 log 日志信息找到错误信息的大概位置
- 断点调试
Flask 中 url_for 函数?
-
URL 反转:根据视图函数名称得到当前所指向的 url。
-
url_for() 函数最简单的用法是以视图函数名作为参数,返回对应的 url,还可以用作加载静态文件。
<link rel="stylesheet" href="{{url_for('static',filename='css/index.css')}}">
该条语句就是在模版中加载 css 静态文件。
-
url_for 和 redirect 区别
-
url_for 是用来拼接 URL 的,可以使用程序 URL 映射中保存的信息生成 URL。url_for() 函数最简单的用法是以视图函数名作为参数, 返回对应的 URL。例如,在示例程序中 hello.py 中调用url_for('index') 得到的结果是 /。
-
redirect 是重定向函数,输入一个 URL 后,自动跳转到另一个 URL 所在的地址,例如,你在函数 中写 return redirect('https://www.baidu.com') 页面就会跳转向百度页面。
1. from flask import Flask,redirect,url_for 2. app = Flask(__name__) 3. @app.route('/') 4. def index(): 5. login_url = url_for('login') 6. return redirect(login_url) 7. return u'这是首页' 8. 9. @app.route('/login/') 10.def login(): 11. return u'这是登陆页面' 12. 13. @app.route('/question/<is_login>/') 14. def question(is_login): 15. if is_login == '1': 16. return u'这是发布问答的页面' 17. else: 18. return redirect(url_for('login')) 19. 20. if __name__ == '__main__': 21. app.run(debug=True)
-
Flask 中请求钩子的理解和应用?
请求钩子是通过装饰器的形式实现的,支持以下四种:
- before_first_request 在处理第一个请求前运行
- before_request:在每次请求前运行
- after_request:如果没有未处理的异常抛出,在每次请求后运行
- teardown_request:即使有未处理的异常抛出,在每次请求后运行
应用:
1. @api.after_request
2. def after_request(response):
3. """设置默认的响应报文格式为 application/json"""
4. # 如果响应报文response的Content-Type是以text开头,则将其改为默认的 json 类型
5. if response.headers.get("Content-Type").startswith("text"):
6. response.headers["Content-Type"] = "application/json"
7. return respon
一个变量后写多个过滤器是如何执行的?
{{ expression | filter1 | filter2 | ... }} 即表达式(expression)使用 filter1 过滤后再将 filter1 的结果去使用 filter2 过滤。
如何把整个数据库导出来,再导入指定数据库中?
导出:
mysqldump [-h 主机] -u 用户名 -p 数据库名 > 导出的数据库名.sql
导入指定的数据库中:
方法一:
mysqldump [-h 主机] -u 用户名 -p 数据库名 < 导出的数据库名.sql
方法二:
先创建好数据库,因为导出的文件里没有创建数据库的语句,如果数据库已经建好,则不用再创建
create database example charset=utf8;(数据库名可以不一样)
切换数据库:
use example;
导入指定 sql 文件:
mysql>source /path/example.sql;
Flask 和 Django 路由映射的区别?
在 django 中,路由是浏览器访问服务器时,先访问的项目中的 url,再由项目中的 url 找到应用中url,这些 url 是放在一个列表里,遵从从前往后匹配的规则。在 flask 中,路由是通过装饰器给每个视图函数提供的,而且根据请求方式的不同可以一个 url 用于不同的作用。
跨站请求伪造和跨站请求保护的实现?
图中 Browse 是浏览器,WebServerA 是受信任网站/被攻击网站 A,WebServerB 是恶意网站/点击网站 B。
- 一开始用户打开浏览器,访问受信任网站 A,输入用户名和密码登陆请求登陆网站 A。
- 网站 A 验证用户信息,用户信息通过验证后,网站 A 产生 Cookie 信息并返回给浏览器。
- 用户登陆网站 A 成功后,可以正常请求网站 A。
- 用户未退出网站 A 之前,在同一浏览器中,打开一个 TAB 访问网站 B。
- 网站 B 看到有人方式后,他会返回一些攻击性代码。
- 浏览器在接受到这些攻击性代码后,促使用户不知情的情况下浏览器携带 Cookie(包括sessionId)信息,请求网站 A。这种请求有可能更新密码,添加用户什么的操作。
从上面 CSRF 攻击原理可以看出,要完成一次 CSRF 攻击,需要被攻击者完成两个步骤:
- 登陆受信任网站 A,并在本地生成 COOKIE。
- 在不登出 A 的情况下,访问危险网站 B。
如果不满足以上两个条件中的一个,就不会受到 CSRF 的攻击,以下情况可能会导致 CSRF:
- 登录了一个网站后,打开一个 tab 页面并访问另外的网站。
- 关闭浏览器了后,本地的 Cookie 尚未过期,你上次的会话还没有已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了……)
解决办法:就是在表单中添加 from.csrf_token。
Flask(__name__)中的__name__可以传入哪些值?
可以传入的参数:
- 字符串:‘hello’;但是‘abc’,不行,因为 abc 是 python 内置的模块
- _name_,约定俗成
不可以插入的参数:
- python 内置的模块,re,urllib,abc 等
- 数字
更多flask相关内容详见:
Flask文档
Django
Django 创建项目的命令?
django-admin startproject 项目名称
python manage.py startapp 应用app名
Django 创建项目后,项目文件夹下的组成部分(对 mvt 的理解)?
项目文件夹下的组成部分:
- manage.py 是项目运行的入口,指定配置文件路径。
- 与项目同名的目录,包含项目的配置文件。
- __init.py__ 是一个空文件,作用是这个目录可以被当作包使用,也可以做一些初始化操作。
- settings.py 是项目的整体配置文件。
- urls.py 是项目的 URL 配置文件。
- wsgi.py 是项目与 WSGI 兼容的 Web 服务器。
对 MVC,MVT 解读的理解?
MVC:
M:Model,模型,和数据库进行交互
V:View,视图,负责产生 Html 页面
C:Controller,控制器,接收请求,进行处理,与 M 和 V 进行交互,返回应答。
- 用户点击注按钮,将要注册的信息发送给网站服务器。
- Controller 控制器接收到用户的注册信息,Controller 会告诉 Model 层将用户的注册信息保存到数据库
- Model 层将用户的注册信息保存到数据库
- 数据保存之后将保存的结果返回给 Model 模型,
- Model 层将保存的结果返回给 Controller 控制器。
- Controller 控制器收到保存的结果之后,或告诉 View 视图,view 视图产生一个 html 页面。
- View 将产生的 Html 页面的内容给了 Controller 控制器。
- Controller 将 Html 页面的内容返回给浏览器。
- 浏览器接受到服务器 Controller 返回的 Html 页面进行解析展示。
MVT:
M:Model,模型,和 MVC 中的 M 功能相同,和数据库进行交互。
V:view,视图,和 MVC 中的 C 功能相同,接收请求,进行处理,与 M 和 T 进行交互,返回应答。
T:Template,模板,和 MVC 中的 V 功能相同,产生 Html 页面
- 用户点击注册按钮,将要注册的内容发送给网站的服务器。
- View 视图,接收到用户发来的注册数据,View 告诉 Model 将用户的注册信息保存进数据库。
- Model 层将用户的注册信息保存到数据库中。
- 数据库将保存的结果返回给 Model
- Model 将保存的结果给 View 视图。
- View 视图告诉 Template 模板去产生一个 Html 页面。
- Template 生成 html 内容返回给 View 视图。
- View 将 html 页面内容返回给浏览器。
- 浏览器拿到 view 返回的 html 页面内容进行解析,展示。
Django 中 models 利用 ORM 对 Mysql 进行查表的语句(多个语句)?
字段查询:
-
all():返回模型类对应表格中的所有数据。
-
get():返回表格中满足条件的一条数据,如果查到多条数据,则抛异常:MultipleObjectsReturned,查询不到数据,则抛异常:DoesNotExist。
-
filter():参数写查询条件,返回满足条件 QuerySet 集合数据。
-
条件格式:模型类属性名__条件名=值
注意:此处是模型类属性名,不是表中的字段名
-
判等 exact
1. BookInfo.object.filter(id=1) 2. BookInfo.object.filter(id__exact=1)此处的__exact 可以省略
-
模糊查询 like
例:查询书名包含'传'的图书。contains 1. contains BookInfo.objects.filter(btitle__contains=’传’)
-
空查询 where 字段名 isnull
1. BookInfo.objects.filter(btitle__isnull=False)
-
范围查询 where id in (1,3,5)
1. BookInfo.objects.filter(id__in=[1,3,5])
-
比较查询 gt lt(less than) gte(equal) lte
1. BookInfo.objects.filter(id__gte=3)
-
日期查询
1. BookInfo.objects.filter(bpub_date__year = 1980) 2. BookInfo.objects.filter(bpub_date__gt = date(1980,1,1))
-
-
exclude:返回不满足条件的数据。
1. BookInfo.objects.exclude(id=3)
-
F 对象:用于类属性之间的比较条件。
1. from django.db.models import F 2. 例:where bread > bcomment BookInfo.objects.filter(bread__gt = F(‘bcomment’)) 3. 例:BookInfo.objects.filter(bread__gt=F(‘bcomment’)*2)
-
Q 对象:用于查询时的逻辑条件。可以对 Q 对象进行&|~操作。
1. from django.db.models import Q 2. BookInfo.objects.filter(id__gt=3, bread__gt=30) 3. BooInfo.objects.filter(Q(id__gt=3) & Q(bread__gt=3)) 4. 例:BookInfo.objects.filter(Q(id__gt=3) | Q(bread__gt=30)) 5. 例:BookInfo.objects.filter(~Q(id=3))
-
order_by :对查询结果进行排序。返回 QuerySet
1. 例:BookInfo.objects.all().order_by('id') 2. 例:BookInfo.objects.all().order_by('-id') 3. 例:BookInfo.objects.filter(id__gt=3).order_by('-bread')
-
聚合:对查询结果进行聚合操作。
-
aggregate:调用这个函数来使用聚合。返回的是聚合后的数据字典
1. from django.db.models import Sum,Count,Max,Min,Avg 2. 例:BookInfo.objects.aggregate(Count('id')) 3. {'id__count': 5} 注意返回值类型及键名 4. 例:BookInfo.objects.aggregate(Sum(‘bread’)) 5. {‘bread__sum’: 120} 注意返回值类型及键名
-
内置聚合函数:Avg(平均值)、Count(统计个数)、Max(最大值)、Min(最小值)、StdDev(标准差)、Sum(求和)、Variance(方差)
例:统计所有图书的数目。 1. BookInfo.objects.all().count() 例:统计 id 大于 3 的所有图书的数目。 1. BookInfo.objects.filter(id__gt = 3).count()
-
模型类关系
一对多关系
models.ForeignKey() 定义在多的类中
多对多关系
models.ManyToManyField() 定义在哪个类中都可以
一对一关系
models.OneToOneField() 定义在哪个类中都可以
django 中间件的使用?
Django 在中间件中预置了六个方法,这六个方法的区别在于不同的阶段执行,对输入或输出进行干预,方法如下:
-
初始化:无需任何参数,服务器响应第一个请求的时候调用一次,用于确定是否启用当前中间件。
1.def __init__(): 2. pass
-
处理请求前:在每个请求上调用,返回 None 或 HttpResponse 对象。
1.def process_request(request): 2. pass
-
处理视图前:在每个请求上调用,返回 None 或 HttpResponse 对象。
1.def process_view(request, view_func, view_args, view_kwargs): 2. pass
-
处理模板响应前:在每个请求上调用,返回实现了 render 方法的响应对象。
1.def process_template_response(request, response): 2. pass
-
处理响应后:所有响应返回浏览器之前被调用,在每个请求上调用,返回 HttpResponse 对象。
1.def process_response(request, response): 2. pass
-
异常处理:当视图抛出异常时调用,在每个请求上调用,返回一个 HttpResponse 对象。
1.def process_exception(request,exception): 2. pass
django 开发中数据库做过什么优化?
- 设计表时,尽量少使用外键,因为外键约束会影响插入和删除性能;
- 使用缓存,减少对数据库的访问;
- 在 orm 框架下设置表时,能用 varchar 确定字段长度时,就别用 text;
- 可以给搜索频率高的字段属性,在定义时创建索引;
- Django orm 框架下的 Querysets 本来就有缓存的;
- 如果一个页面需要多次连接数据库,最好一次性取出所有需要的数据,减少对数据库的查询次数;
- 若页面只需要数据库里某一个两个字段时,可以用 QuerySet.values();
- 在模板标签里使用 with 标签可以缓存 Qset 的查询结果。
django 如何提升性能(高并发)?
对一个后端开发程序员来说,提升性能指标主要有两个一个是并发数,另一个是响应时间网站性能的优化一般包括 web 前端性能优化,应用服务器性能优化,存储服务器优化。
对前端的优化主要有:
- 减少 http 请求,减少数据库的访问量,比如使用雪碧图。
- 使用浏览器缓存,将一些常用的 css,js,logo 图标,这些静态资源缓存到本地浏览器,通过设置 http 头中的 cache-control 和 expires 的属性,可设定浏览器缓存,缓存时间可以自定义。
- 对 html,css,javascript 文件进行压缩,减少网络的通信量。
对我个人而言,我做的优化主要是以下三个方面:
- 合理的使用缓存技术,对一些常用到的动态数据,比如首页做一个缓存,或者某些常用的数据做个缓存,设置一定的过期时间,这样减少了对数据库的压力,提升网站性能。
- 使用 celery 消息队列,将耗时的操作扔到队列里,让 worker 去监听队列里的任务,实现异步操作,比如发邮件,发短信。
- 就是代码上的一些优化,补充:nginx 部署项目也是项目优化,可以配置合适的配置参数,提升效率,增加并发量。
- 如果太多考虑安全因素,服务器磁盘用固态硬盘读写,远远大于机械硬盘,这个技术现在没有普及,主要是固态硬盘技术上还不是完全成熟, 相信以后会大量普及。
- 另外还可以搭建服务器集群,将并发访问请求,分散到多台服务器上处理。
- 最后就是运维工作人员的一些性能优化技术了。
启动 Django 服务的方法?
runserver 方法是调试 Django 时经常用到的运行方式,它使用 Django 自带的 WSGI Server 运行,主要在测试和开发中使用,并且 runserver 开启的方式也是单进程 。
怎样测试 django 框架中的代码?
在单元测试方面,Django 继承 python 的 unittest.TestCase 实现了自己的django.test.TestCase,编写测试用例通常从这里开始。测试代码通常位于 app 的 tests.py 文件中(也可以在 models.py 中编写,一般不建议)。在 Django 生成的 depotapp 中,已经包含了这个文件,并且其中包含了一个测试。
用例的样例:
1. python manage.py test: 执行所有的测试用例
2. python manage.py test app_name: 执行该 app 的所有测试用例
3. python manage.py test app_name.case_name: 执行指定的测试用例
一些测试工具:unittest 或者 pytest
有过部署经验?用的什么技术?可以满足多少压力?
- 有部署经验,在阿里云服务器上部署的
- 技术有:nginx + uwsgi 的方式来部署 Django 项目
- 无标准答案(例:压力测试一两千)
Django 中哪里用到了线程?哪里用到了协程?哪里用到了进程?
- Django 中耗时的任务用一个进程或者线程来执行,比如发邮件,使用 celery。
- 部署 django 项目的时候,配置文件中设置了进程和协程的相关配置。
django 关闭浏览器,怎样清除 cookies 和 session?
-
设置 Cookie
1. def cookie_set(request): 2. response = HttpResponse("<h1>设置 Cookie,请查看响应报文头</h1>") 3. response.set_cookie('h1', 'hello django') 4. return response
-
读取 Cookie
1. def cookie_get(request): 2. response = HttpResponse("读取 Cookie,数据如下:<br>") 3. if request.COOKIES.has_key('h1'): 4. response.write('<h1>' + request.COOKIES['h1'] + '</h1>') 5. return response
-
以键值对的格式写会话
1. request.session['键']=值
-
根据键读取值
request.session.get('键',默认值)
-
清除所有会话,在存储中删除值部分
1. request.session.clear()
-
清除会话数据,在存储中删除会话的整条数据
1. request.session.flush()
-
删除会话中的指定键及值,在存储中只删除某个键及对应的值
1. del request.session['键']
-
设置会话的超时时间,如果没有指定过期时间则两个星期后过期
如果 value 是一个整数,会话将在 value 秒没有活动后过期。
如果 value 为 0,那么用户会话的 Cookie 将在用户的浏览器关闭时过期。
如果 value 为 None,那么会话在两周后过期。
1. request.session.set_expiry(value)
Session 采用的是在服务器端保持状态的方案,而 Cookie 采用的是在客户端保持状态的方案。但是禁用 Cookie 就不能得到 Session。因为 Session 是用 Session ID 来确定当前对话所对应的服务器 Session,而 Session ID 是通过 Cookie 来传递的,禁用 Cookie 相当于失去了 SessionID,也就得不到 Session。
cookie 可以有过期时间,这样浏览器就知道什么时候可以删除 cookie 了。 如果 cookie 没有设置过期时间,当用户关闭浏览器的时候,cookie 就自动过期了。你可以改变SESSION_EXPIRE_AT_BROWSER_CLOSE 的设置来控制 session 框架的这一行为。缺省情况下,SESSION_EXPIRE_AT_BROWSER_CLOSE 设置为 False ,这样,会话 cookie 可以在用户浏览器中保持有效达 SESSION_COOKIE_AGE 秒(缺省设置是两周,即 1,209,600 秒)如果你不想用户每次打开浏览器都必须重新登陆的话,用这个参数来帮你。如果SESSION_EXPIRE_AT_BROWSER_CLOSE设置为 True,当浏览器关闭时,Django 会使 cookie 失效。
SESSION_COOKIE_AGE:设置 cookie 在浏览器中存活的时间。
有用过 Django REST framework 吗?
Django REST framework 是一个强大而灵活的 Web API 工具。使用 RESTframework 的理由有:
- Web browsable API 对开发者有极大的好处
- 包括 OAuth1a 和 OAuth2 的认证策略
- 支持 ORM 和非 ORM 数据资源的序列化
- 全程自定义开发——如果不想使用更加强大的功能,可仅仅使用常规的 function-based views额外的文档和强大的社区支持
简述 Django 下的(内建的)缓存机制?
一个动态网站的基本权衡点就是,它是动态的。 每次用户请求页面,服务器会重新计算。从开销处理的角度来看,这比你读取一个现成的标准文件的代价要昂贵的多。这就是需要缓存的地方。
Django 自带了一个健壮的缓存系统来保存动态页面这样避免对于每次请求都重新计算。方便起见,Django 提供了不同级别的缓存粒度:可以缓存特定视图的输出、可以仅仅缓存那些很难生产出来的部分、或者可以缓存整个网站 Django 也能很好的配合那些“下游”缓存, 比如 Squid 和基于浏览器的缓存。这里有一些缓存不必要直接去控制但是可以提供线索, (via HTTPheaders)关于网站哪些部分需要缓存和如何缓存。
设置缓存:
缓存系统需要一些设置才能使用。 也就是说,你必须告诉他你要把数据缓存在哪里。是数据库中,文件系统或者直接在内存中。 这个决定很重要,因为它会影响你的缓存性能,是的,一些缓存类型要比其他的缓存类型更快速。
你的缓存配置是通过 setting 文件的 CACHES 配置来实现的。 这里有 CACHES 所有可配置的变量值。
Django HTTP 请求的处理流程?
Django 和其他 Web 框架的 HTTP 处理的流程大致相同,Django 处理一个 Request 的过程是首先通过中间件,然后再通过默认的 URL 方式进行的。我们可以在 Middleware 这个地方把所有Request 拦截住,用我们自己的方式完成处理以后直接返回 Response。
-
加载配置
Django 的配置都在 “Project/settings.py” 中定义,可以是 Django 的配置,也可以是自定义的配置,并且都通过 django.conf.settings 访问,非常方便。
-
启动
最核心动作的是通过 django.core.management.commands.runfcgi 的 Command 来启动,它运行 django.core.servers.fastcgi 中的 runfastcgi,runfastcgi 使用了 flup 的 WSGIServer 来启动 fastcgi 。而 WSGIServer 中携带了 django.core.handlers.wsgi 的 WSGIHandler 类的一个实例,通过 WSGIHandler 来处理由 Web 服务器(比如 Apache,Lighttpd 等)传过来的请求,此时才是真正进入 Django 的世界。
-
处理 Request
当有 HTTP 请求来时,WSGIHandler 就开始工作了,它从 BaseHandler 继承而来。WSGIHandler 为每个请求创建一个 WSGIRequest 实例,而 WSGIRequest 是从http.HttpRequest 继承而来。接下来就开始创建 Response 了。
-
创建 Response
BaseHandler 的 get_response 方法就是根据 request 创建 response,而具体生成response 的动作就是执行 urls.py 中对应的 view 函数了,这也是 Django 可以处理“友好 URL ”的关键步骤,每个这样的函数都要返回一个 Response 实例。此时一般的做法是通过 loader 加载template 并生成页面内容,其中重要的就是通过 ORM 技术从数据库中取出数据,并渲染到Template 中,从而生成具体的页面了。
-
处理 Response
Django 返回 Response 给 flup,flup 就取出 Response 的内容返回给 Web 服务器,由后者返回给浏览器。
总之,Django 在 fastcgi 中主要做了两件事:处理 Request 和创建 Response,而它们对应的核心就是“ urls 分析”、“模板技术”和“ ORM 技术”。
如图所示,一个 HTTP 请求,首先被转化成一个 HttpRequest 对象,然后该对象被传递给Request 中间件处理,如果该中间件返回了 Response,则直接传递给 Response 中间件做收尾处理。否则的话 Request 中间件将访问 URL 配置,确定哪个 view 来处理,在确定了哪个 view 要执行,但是还没有执行该 view 的时候,系统会把 request 传递给 view 中间件处理器进行处理,如果该中间件返回了 Response,那么该 Response 直接被传递给 Response 中间件进行后续处理,否则将执行确定的 view 函数处理并返回 Response,在这个过程中如果引发了异常并抛出,会被 Exception中间件处理器进行处理。
Django 里 QuerySet 的 get 和 filter 方法的区别?
-
输入参数
get 的参数只能是 model 中定义的那些字段,只支持严格匹配。
filter 的参数可以是字段,也可以是扩展的 where 查询关键字,如 in,like 等。
-
返回值
get 返回值是一个定义的 model 对象。
filter 返回值是一个新的 QuerySet 对象,然后可以对 QuerySet 在进行查询返回新的 QuerySet对象,支持链式操作,QuerySet 一个集合对象,可使用迭代或者遍历,切片等,但是不等于 list 类型(使用一定要注意)。
-
异常
get 只有一条记录返回的时候才正常,也就说明 get 的查询字段必须是主键或者唯一约束的字段。当返回多条记录或者是没有找到记录的时候都会抛出异常
filter 有没有匹配的记录都可以
django 中当一个用户登录 A 应用服务器(进入登录状态),然后下次请求被 nginx代理到 B 应用服务器会出现什么影响?
如果用户在 A 应用服务器登陆的 session 数据没有共享到 B 应用服务器,那么之前的登录状态就没有了。
跨域请求问题 django 怎么解决的(原理)
- 启用中间件
- post 请求
- 验证码
- 表单中添加 csrf_token 标签
Django 对数据查询结果排序怎么做,降序怎么做,查询大于某个字段怎么做?
排序使用 order_by()
降序需要在排序字段名前加-
查询字段大于某个值:使用 filter(字段名_gt=值)
Django 重定向你是如何实现的?用的什么状态码?
使用 HttpResponseRedirect
redirect
状态码:302,301
生成迁移文件和执行迁移文件的命令是什么?
python manage.py makemigrations
python manage.py migrate
查询集返回列表的过滤器有哪些?
- all() :返回所有的数据
- filter():返回满足条件的数据
- exclude():返回满足条件之外的数据,相当于 sql 语句中 where 部分的 not 关键字
- order_by():排序
判断查询集正是否有数据?
exists():判断查询集中否有数据,如果有则返回 True,没有则返回 False。
Django 本身提供了 runserver,为什么不能用来部署?
runserver 方法是调试 Django 时经常用到的运行方式,它使用 Django 自带的 WSGI Server 运行,主要在测试和开发中使用,并且 runserver 开启的方式也是单进程 。
uWSGI 是一个 Web 服务器,它实现了 WSGI 协议、uwsgi、http 等协议。注意 uwsgi 是一种通信协议,而 uWSGI 是实现 uwsgi 协议和 WSGI 协议的 Web 服务器。uWSGI 具有超快的性能、低内存占用和多 app 管理等优点,并且搭配着 Nginx 就是一个生产环境了,能够将用户访问请求与应用 app 隔离开,实现真正的部署。相比来讲,支持的并发量更高,方便管理多进程,发挥多核的优势,提升性能。
查询集两大特性?惰性执行?
惰性执行、缓存 。
创建查询集不会访问数据库,直到调用数据时,才会访问数据库,调用数据的情况包括迭代、序列化、与 if 合用
HttpRequest 和 HttpResponse 是什么?干嘛用的?
HttpRequest 是 django 接受用户发送多来的请求报文后,将报文封装到 HttpRequest 对象中去。
HttpResponse 返回的是一个应答的数据报文。render 内部已经封装好了HttpResponse 类。
视图的第一个参数必须是 HttpRequest 对象,两点原因:表面上说,他是处理 web 请求的,所以必须是请求对象,根本上说,他是基于请求的一种 web 框架,所以,必须是请求对象。
因为 view 处理的是一个 request 对象,请求的所有属性我们都可以根据对象属性的查看方法来获取具体的信息:格式:request.属性
-
request.path 请求页面的路径,不包含域名
-
request.get_full_path 获取带参数的路径
-
request.method 页面的请求方式
-
request.GET GET 请求方式的数据
-
request.POST POST 请求方式的数据
-
request.COOKIES 获取 cookie
-
request.session 获取 session
-
request.FILES 上传图片(请求页面有 enctype="multipart/form-data"属性时 FILES 才有数据。?a=10 的键和值时怎么产生的,键是开发人员在编写代码时确定下来的,值时根据数据生成或者用户填写的,总之是不确定的。
403 错误:表示资源不可用,服务器理解客户的请求,但是拒绝处理它,通常由于服务器上文件和目录的权限设置导致的 web 访问错误。如何解决:
- 把中间件注释。
- 在表单内部添加
request.GET.get()取值时如果一键多值情况,get 是覆盖的方式获取的。getlist()可以获取多个值。
在一个有键无值的情况下,该键名 c.get的值返回空。有键无值:c.getlist 返回的是空列表;在无键无值也没有默认值的情况下,返回的是 None
HttpResponse 常见属性:
- content: 表示返回的内容
- charset: 表示 response 采用的编码字符集,默认是 utf-8
- status_code:返回的 HTTP 响应状态码 3XX 是对请求继续进一步处理,常见的是重定向。
常见方法:
- init:创建 httpResponse 对象完成返回内容的初始化
- set_cookie:设置 Cookie 信息:格式:set_cookies('key','value',max_age=None,expires=None)
- max_age 是一个整数,表示指定秒数后过期,expires 指定过期时间,默认两个星期后过期。
- write 向响应体中写数据
应答对象:
-
方式一:render(request,"index.html") 返回一个模板
render(request,"index.html", context) 返回一个携带动态数据的页面
-
方式二:render_to_response("index.html") 返回一个模板页面
-
方式三:redirect("/") 重定向
-
方式四:HttpResponseRdeirect("/") 实现页面跳转功能
-
方式五:HttpResponse("itcast1.0")在返回到额页面中添加字符串内容
-
方式六:JsonResponse () 返回的页面中添加字符串内容。
JsonResponse 创建对象时候接收字典作为参数,返回的对象是一个 json 对象。能接收 Json 格式数据的场景,都需要使用 view 的 JsonResponse 对象返回一个 json 格式数据
ajax 的使用场景,页面局部刷新功能。ajax 接收 Json 格式的数据。
在返回的应答报文中,可以看到 JsonResponse 应答的 content-Type 内容是 application/json
ajax 实现网页局部刷新功能:ajax 的 get()方法获取请求数据;ajax 的 each()方法遍历输出这些数据
什么是反向解析
使用场景:模板中的超链接,视图中的重定向
使用:在定义 url 时为 include 定义 namespace 属性,为 url 定义 name 属性
在模板中使用 url 标签:{% url 'namespace_value:name_value'%}
在视图中使用 reverse 函数:redirect(reverse('namespce_value:name_value’))
根据正则表达式动态生成地址,减轻后期维护成本。
注意反向解析传参数,主要是在我们的反向解析的规则后面添加了两个参数,两个参数之间使用空格隔开:<a href="{% url 'booktest:fan2' 2 3 %}">位置参数
Django 日志管理
- 导包:import logging
- 初始化:logging.basicConfig()
- 写日志logger.info("some info ...)
可用函数有:logger.debug() logger.info() logger.warning() logger.error()
Django 文件管理:对于 django 来说,项目中的 css,js,图片都属于静态文件,我们一般会将静态文件放到一个单独的目录中,以方便管理,在 html 页面调用时,也需要指定静态文件的路径。静态文件可以放在项目根目录下,也可以放在应用的目录下,由于这些静态文件在项目中是通用的,所以推荐放在项目的根目录下。
在生产中,只要和静态文件相关的所有访问,基本上没有 django 什么事,一般都是由 nignx 软件代劳了。
请说明 Django 中的 web 认证登陆机制?
首先,前端通过 Web 表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个 HTTP POST 请求。建议的方式是通过 SSL 加密的传输(https 协议),从而避免敏感信息被嗅探。
后端核对用户名和密码成功后,将用户的 id 等其他信息作为 JWT Payload(负载),将其与头部分别进行 Base64 编码拼接后签名,形成一个 JWT。形成的 JWT 就是一个形同lll.zzz.xxx 的字符串。
后端将 JWT 字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在 localStorage 或 sessionStorage 上,退出登录时前端删除保存的 JWT 即可。前端在每次请求时将 JWT 放入 HTTP Header 中的 Authorization 位。(解决 XSS 和 XSRF问题)后端检查是否存在,如存在验证 JWT 的有效性。例如,检查签名是否正确;检查 Token 是否过期;检查 Token 的接收方是否是自己(可选)。
验证通过后后端使用 JWT 中包含的用户信息进行其他逻辑操作,返回相应结果。
Django 作为后端怎么实现给客户端推送消息,app 里的推送如何实现?
Django 后台可以使用 django-push-notifications 推送模块来进行消息推送;也可以在 Django 中使用 Message 框架向模板中推送消息内容
app 中的推送,有两种方式,第一种是自己研发的,但由于研发成本高,所以大多数都采用第二种方式,也就是使用第三方工具进行推送,如:极光推送、个推、百度云推送、华为推送等
如何对 django 框架进行二次开发,增大并发数
二次开发,简单的说就是在现有的软件上进行定制修改,功能的扩展,然后达到自己想要的功能,一般来说都不会改变原有系统的内核。
开发过程中会选择其他组件或者自己开发一部分组件来替代 django 的组件。
关于提高并发量:
- 在 django 框架中引入 celery 实现分布式异步非阻塞地执行一些需要耗时的任务。
- 原生 orm 的效率不如直接写 sql 的效率高,必要的时候我们会选择直接用 sql 来实现后端操作,因此提高程序的效率。
- 缓存组件的引入,引入 drf 框架前后端分离的设计模式,不使用后端渲染页面,减少服务器负载,从而提高并发量。
- 对 django 的文件管理系统进行二次开发,使用 fastdfs 或者第三方文件存储系统替代django 的文件存储系统,以此来提高并发量
Xadmin 的引入是二次开发了 django-admin,但是目的是为了拓展更多的功能。
Django 中的自定义过滤器,可以传几个参数?为什么?
自定义过滤器只是一个接受一个或两个参数的 Python 函数:变量的值(输入)并不必要是一个字符串。参数的值,这个可以有一个默认的值或者完全留空。
Django 中用户上传头像,怎样避免图片名重复?
MD5 时间戳,图片名称可能会重复,但是上传图片的时间生成的 MD5 字符串是唯一的,可以以此来作为图片保存的方式,就避免了图片重名导致覆盖的惨剧
Tornado
Tornado 的核是什么?
Tornado 的核心是 ioloop 和 iostream 这两个模块,前者提供了一个高效的 I/O 事件循环,后者则封装了 一个无阻塞的 socket 。通过向 ioloop 中添加网络 I/O 事件,利用无阻塞的 socket ,再搭配相应的回调函数,便可达到梦寐以求的高效异步执行。
其他
HTTPS 有什么优点和缺点
优点:
- 使用 HTTPS 协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
- HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,要比 http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
- HTTPS 是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本
缺点:
- HTTPS 协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用
- HTTPS 协议还会影响缓存,增加数据开销和功耗,甚至已有安全措施也会受到影响也会因此而受到影响。
- SSL 证书需要钱。功能越强大的证书费用越高。个人网站、小网站没有必要一般不会用。
- HTTPS 连接服务器端资源占用高很多,握手阶段比较费时对网站的相应速度有负面影响。
- HTTPS 连接缓存不如 HTTP 高效。
HTTPS 是如何实现安全传输数据的
HTTPS 其实就是在 HTTP 跟 TCP 中间加多了一层加密层 TLS/SSL。SSL 是个加密套件,负责对 HTTP的数据进行加密。TLS 是 SSL 的升级版。现在提到 HTTPS,加密套件基本指的是 TLS。原先是应用层将数据直接给到 TCP 进行传输,现在改成应用层将数据给到 TLS/SSL,将数据加密后,再给到 TCP 进行传输。
TTL,MSL,RTT?
MSL:报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
TTL:TTL 是 time to live 的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个 ip 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。RFC 793 中规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。TTL 与 MSL 是有关系的但不是简单的相等的关系,MSL 要大于等于 TTL。
RTT: RTT 是客户到服务器往返所花时间(round-trip time,简称 RTT),TCP 含有动态估算 RTT的算法。TCP 还持续估算一个给定连接的 RTT,这是因为 RTT 受网络传输拥塞程序的变化而变化。
验证码过期时间怎么设置?
将验证码保存到数据库或 session,设置过期时间为 1 分钟,然后页面设置一个倒计时(一般是前端js 实现 这个计时)的展示,一分钟过后再次点击获取新的信息。
Python 中三大框架各自的应用场景?
django:主要是用来搞快速开发的,他的亮点就是快速开发,节约成本,正常的并发量不过 10000,如果要实现高并发的话,就要对 django 进行二次开发,比如把整个笨重的框架给拆掉,自己写 socket实现 http 的通信,底层用纯 c,c++写提升效率,ORM 框架给干掉,自己编写封装与数据库交互的框架,因为啥呢,ORM 虽然面向对象来操作数据库,但是它的效率很低,使用外键来联系表与表之间的查询;
flask:轻量级,主要是用来写接口的一个框架,实现前后端分离,提升开发效率,Flask 本身相当于一个内核,其他几乎所有的功能都要用到扩展(邮件扩展 Flask-Mail,用户认证 Flask-Login),都需要用第三方的扩展来实现。比如可以用 Flask-extension 加入 ORM、窗体验证工具,文件上传、身份验证等。Flask 没有默认使用的数据库,你可以选择 MySQL,也可以用 NoSQL。其 WSGI 工具箱采用 Werkzeug(路由模块),模板引擎则使用 Jinja2。这两个也是 Flask 框架的核心。Python 最出名的框架要数 Django,此外还有 Flask、Tornado 等框架。虽然 Flask 不是最出名的框架,但是 Flask 应该算是最灵活的框架之一,这也是 Flask 受到广大开发者喜爱的原因。
Tornado: Tornado 是一种 Web 服务器软件的开源版本。Tornado 和现在的主流 Web 服务器框架(包括大多数 Python 的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。得利于其非阻塞的方式和对 epoll 的运用,Tornado 每秒可以处理数以千计的连接,因此 Tornado是实时 Web 服务的一个 理想框架。
什么是 restful api,谈谈你的理解?
REST:Representational State Transfer 的缩写,翻译:“具象状态传输”。一般解释为“表现层状态转换”。
REST 是设计风格而不是标准。是指客户端和服务器的交互形式。我们需要关注的重点是如何设计REST 风格的网络接口。
REST 的特点:
- 具象:一般指表现层,要表现的对象就是资源。比如,客户端访问服务器,获取的数据就是资源。比如文字、图片、音视频等。
- 表现:资源的表现形式。txt 格式、html 格式、json 格式、jpg 格式等。浏览器通过 URL 确定资源的位置,但是需要在 HTTP 请求头中,用 Accept 和 Content-Type 字段指定,这两个字段是对资源表现的描述。
- 状态转换:客户端和服务器交互的过程。在这个过程中,一定会有数据和状态的转化,这种转化叫做状态转换。其中,GET 表示获取资源,POST 表示新建资源,PUT 表示更新资源,DELETE 表示删除资源。HTTP 协议中最常用的就是这四种操作方式。
RESTful 架构:
- 每个 URL 代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个 http 动词,对服务器资源进行操作,实现表现层状态转换。
如何设计符合 RESTful 风格的 API
-
域名:
将 api 部署在专用域名下:
http://api.example.com
或者将 api 放在主域名下:
http://www.example.com/api/
-
版本:
将 API 的版本号放在 url 中。
http://www.example.com/app/1.0/info http://www.example.com/app/1.2/info
-
路径:
路径表示 API 的具体网址。每个网址代表一种资源。 资源作为网址,网址中不能有动词只能有名词,一般名词要与数据库的表名对应。而且名词要使用复数。
错误示例:
http://www.example.com/getGoods http://www.example.com/listOrders
正确示例:
#获取单个商品 http://www.example.com/app/goods/1 #获取所有商品 http://www.example.com/app/goods
-
使用标准的 HTTP 方法:
对于资源的具体操作类型,由 HTTP 动词表示。 常用的 HTTP 动词有四个。
-
GET SELECT :从服务器获取资源。
-
POST CREATE :在服务器新建资源。
-
PUT UPDATE :在服务器更新资源(客户端提供改变后的完整资源)。
-
DELETE DELETE :从服务器删除资源。
-
PATCH:在服务器更新资源(客户端提供改变的属性)
-
HEAD:获取资源的元数据
-
OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的
#获取指定商品的信息 GET http://www.example.com/goods/ID #新建商品的信息 POST http://www.example.com/goods #更新指定商品的信息 PUT http://www.example.com/goods/ID #删除指定商品的信息 DELETE http://www.example.com/goods/ID
-
-
过滤信息:
如果资源数据较多,服务器不能将所有数据一次全部返回给客户端。API 应该提供参数,过滤返
回结果。
#指定返回数据的数量 http://www.example.com/goods?limit=10 #指定返回数据的开始位置 http://www.example.com/goods?offset=10 #指定第几页,以及每页数据的数量 http://www.example.com/goods?page=2&per_page=20
-
状态码:
服务器向用户返回的状态码和提示信息,常用的有:
- 200 OK :服务器成功返回用户请求的数据
- 201 CREATED :用户新建或修改数据成功。
- 202 Accepted:表示请求已进入后台排队。
- 400 INVALID REQUEST :用户发出的请求有错误。
- 401 Unauthorized :用户没有权限。
- 403 Forbidden :访问被禁止。
- 404 NOT FOUND :请求针对的是不存在的记录。
- 406 Not Acceptable :用户请求的的格式不正确。
- 500 INTERNAL SERVER ERROR :服务器发生错误。
-
错误信息:
一般来说,服务器返回的错误信息,以键值对的形式返回。
{
error: 'Invalid API KEY'
}
-
响应结果:
- GET /collection:返回资源对象的列表(数组);
- GET /collection/resource:返回单个资源对象;
- POST /collection:返回新生成的资源对象;
- PUT /collection/resource:返回完整的资源对象;
- PATCH /collection/resource:返回完整的资源对象;
- DELETE /collection/resource:返回一个空文档
针对不同结果,服务器向客户端返回的结果应符合以下规范。
#返回商品列表 GET http://www.example.com/goods #返回单个商品 GET http://www.example.com/goods/cup #返回新生成的商品 POST http://www.example.com/goods #返回一个空文档 DELETE http://www.example.com/goods
-
使用链接关联相关的资源(Hypermedia API):
在返回响应结果时提供链接其他 API 的方法,使客户端很方便的获取相关联的信息。
-
其他:
服务器返回的数据格式,应该尽量使用 JSON,避免使用 XML。
什么 csrf 攻击原理?如何解决?
简单来说就是: 你访问了信任网站 A,然后 A 会用保存你的个人信息并返回给你的浏览器一个cookie,然后呢,在 cookie 的过期时间之内,你去访问了恶意网站 B,它给你返回一些恶意请求代码,要求你去访问网站 A,而你的浏览器在收到这个恶意请求之后,在你不知情的情况下,会带上保存在本地浏览器的 cookie 信息去访问网站 A,然后网站 A 误以为是用户本身的操作,导致来自恶意网站 C 的攻击代码会被执:发邮件,发消息,修改你的密码,购物,转账,偷窥你的个人信息,导致私人信息泄漏和账户财产安全收到威胁
请简述浏览器是如何获取一枚网页的?
- 在用户输入目的 URL 后,浏览器先向 DNS 服务器发起域名解析请求;
- 在获取了对应的 IP 后向服务器发送请求数据包;
- 服务器接收到请求数据后查询服务器上对应的页面,并将找到的页面代码回复给客户端;
- 客户端接收到页面源代码后,检查页面代码中引用的其他资源,并再次向服务器请求该资源;
- 在资源接收完成后,客户端浏览器按照页面代码将页面渲染输出显示在显示器上;
电商网站库存问题
一般团购,秒杀,特价之类的活动,这样会使访问量激增,很多人抢购一个商品,作为活动商品,库存肯定是很有限的。控制库存问题,数据库的事务功能是控制库存超卖的有效方式。
-
在秒杀的情况下,肯定不能如此频率的去读写数据库,严重影响性能问题,必须使用缓存,将需要秒杀的商品放入缓存中,并使用锁来处理并发情况,先将商品数量增减(加锁、解析)后在进行其他方面的处理,处理失败再将数据递增(加锁、解析),否则表示交易成功。
-
这个肯定不能直接操作数据库的,会挂的。直接读库写库对数据库压力太大了,要用到缓存。
-
首先,多用户并发修改同一条记录时,肯定是后提交的用户将覆盖掉前者提交的结果了。这个直接可以使用加乐观锁的机制去解决高并发的问题。
提高并发量方式
- 前端方面:部分按钮不能让用户不停点击,设置短期失效。
- 后端方面:缓存、celery 异步、页面静态化、fastdfs 分布式文件存储、第三方文件存储系统。
- 部署方面:Redis 集群的使用、MySQL 集群的使用、nginx+uwsig 的部署方式。
- 数据库方面:数据库设计角度、数据库查询角度、数据库配置方案角度。
业务需求是做音乐和视频的,这样的前后端交互如何开发?
像音频和视频之类的占内存数据,我们一般不会直接存储在数据库中,而是存储到一个专门的服务器,数据库只是存储该数据的路径。在项目中,我们通过使用视频点播实现音视频上传到阿里云的 OSS(对象存储)服务器中存储,后端数据库中保存视频的 id;然后前端引入阿里云播放器的 js 文件,创建播放器对象进行操作。使用视频点播实现音视频上传、存储、处理和播放的整体流程如下:
- 用户获取上传授权。
- VoD 下发 上传地址和凭证 及 VideoId。
- 用户上传视频保存视频 ID(VideoId)。
- 用户服务端获取播放凭证。
- VoD 下发带时效的播放凭证。
- 用户服务端将播放凭证下发给客户端完成视频播放。
视频点播:阿里云视频点播(ApsaraVideo for VoD)是集音视频采集、编辑、上传、自动化转码处理、媒体资源管理、高效云剪辑处理、分发加速、视频播放于一体的一站式音视频点播解决方案,整体服务构建在阿里云强大的基础设施服务之上,提供端到端的视频全链路服务,帮助企业和开发者快速搭建安全、弹性、高可定制的视频点播平台和应用。
MTS:媒体处理(ApsaraVideo Media Processing,原 MTS)是一种多媒体数据处理服务。它以经济、弹性和高可扩展的转换方法,将多媒体数据转码成适合在全平台播放的格式。并基于海量数据深度学习,对媒体的内容、文字、语音、场景多模态分析,实现智能审核、内容理解、智能编辑。
CDN:全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。
五、服务器
Nginx
简述一下什么是Nginx,它有什么优势和功能?
Nginx是一个web服务器和方向代理服务器,用于HTTP、HTTPS、SMTP、POP3和IMAP协议。因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。
Nginx---Ngine X,是一款免费的、自由的、开源的、高性能HTTP服务器和反向代理服务器;也是 一个IMAP、POP3、SMTP代理服务器;Nginx以其高性能、稳定性、丰富的功能、简单的配置和 低资源消耗而闻名。
也就是说Nginx本身就可以托管网站(类似于Tomcat一样),进行Http服务处理,也可以作为反向代理服务器 、负载均衡器和HTTP缓存。
Nginx 解决了服务器的C10K(就是在一秒之内连接客户端的数目为10k即1万)问题。它的设计不像传统的服务器那样使用线程处理请求,而是一个更加高级的机制—事件驱动机制,是一种异步事件驱动结构。
优点:
-
更快
这表现在两个方面:一方面,在正常情况下,单次请求会得到更快的响应;另一方面,在高峰期(如有 数以万计的并发请求),Nginx可以比其他Web服务器更快地响应请求。
-
高扩展性,跨平台
Nginx的设计极具扩展性,它完全是由多个不同功能、不同层次、不同类型且耦合度极低的模块组成。 因此,当对某一个模块修复Bug或进行升级时,可以专注于模块自身,无须在意其他。而且在HTTP模块 中,还设计了HTTP过滤器模块:一个正常的HTTP模块在处理完请求后,会有一串HTTP过滤器模块对请求的结果进行再处理。这样,当我们开发一个新的HTTP模块时,不但可以使用诸如HTTP核心模块、 events模块、log模块等不同层次或者不同类型的模块,还可以原封不动地复用大量已有的HTTP过滤器 模块。这种低耦合度的优秀设计,造就了Nginx庞大的第三方模块,当然,公开的第三方模块也如官方 发布的模块一样容易使用。
Nginx的模块都是嵌入到二进制文件中执行的,无论官方发布的模块还是第三方模块都是如此。这使得第三方模块一样具备极其优秀的性能,充分利用Nginx的高并发特性,因此,许多高流量的网站都倾向于开发符合自己业务特性的定制模块。
-
高可靠性:用于反向代理,宕机的概率微乎其微
高可靠性是我们选择Nginx的最基本条件,因为Nginx的可靠性是大家有目共睹的,很多家高流量网站都在核心服务器上大规模使用Nginx。Nginx的高可靠性来自于其核心框架代码的优秀设计、模块设计的简单性;另外,官方提供的常用模块都非常稳定,每个worker进程相对独立,master进程在1个 worker进程出错时可以快速“拉起”新的worker子进程提供服务。
-
低内存消耗
一般情况下,10 000个非活跃的HTTP Keep-Alive连接在Nginx中仅消耗2.5MB的内存,这是Nginx支 高并发连接的基础。
-
单机支持10万以上的并发连接
这是一个非常重要的特性!随着互联网的迅猛发展和互联网用户数量的成倍增长,各大公司、网站都需要应付海量并发请求,一个能够在峰值期顶住10万以上并发请求的Server,无疑会得到大家的青睐。理论上,Nginx支持的并发连接上限取决于内存,10万远未封顶。当然,能够及时地处理更多的并发请 求,是与业务特点紧密相关的。
-
热部署
master管理进程与worker工作进程的分离设计,使得Nginx能够提供热部署功能,即可以在7×24小时 不间断服务的前提下,升级Nginx的可执行文件。当然,它也支持不停止服务就更新配置项、更换日志文件等功能。
-
最自由的BSD许可协议
这是Nginx可以快速发展的强大动力。BSD许可协议不只是允许用户免费使用Nginx,它还允许用户在自己的项目中直接使用或修改Nginx源码,然后发布。这吸引了无数开发者继续为Nginx贡献自己的智慧。
以上7个特点当然不是Nginx的全部,拥有无数个官方功能模块、第三方功能模块使得Nginx能够满足绝大部分应用场景,这些功能模块间可以叠加以实现更加强大、复杂的功能,有些模块还支持Nginx与Perl、Lua等脚本语言集成工作,大大提高了开发效率。这些特点促使用户在寻找一个Web服务器时更多考虑Nginx。 选择Nginx的核心理由还是它能在支持高并发请求的同时保持高效的服务
解释Nginx用途
Nginx服务器的最佳用法是在网络上部署动态HTTP内容,使用SCGI、WSGI应用程序服务器、用于脚本 的FastCGI处理程序。它还可以作为负载均衡器。
Nginx是如何处理一个HTTP请求的呢?
Nginx 是一个高性能的 Web 服务器,能够同时处理大量的并发请求。它结合多进程机制和异步机制 , 异步机制使用的是异步非阻塞方式 ,接下来就给大家介绍一下 Nginx 的多线程机制和异步非阻塞机制 。
-
多进程机制
服务器每当收到一个客户端时,就有 服务器主进程 ( master process )生成一个 子进程( worker process )出来和客户端建立连接进行交互,直到连接断开,该子进程就结束了。
使用进程的好处是各个进程之间相互独立,不需要加锁,减少了使用锁对性能造成影响,同时降低编程的复杂度,降低开发成本。其次,采用独立的进程,可以让进程互相之间不会影响 ,如果一个进程发生异常退出时,其它进程正常工作, master 进程则很快启动新的 worker 进程,确保服务不会中断,从 而将风险降到最低。
缺点是操作系统生成一个子进程需要进行内存复制等操作,在资源和时间上会产生一定的开销。当有大量请求时,会导致系统性能下降 。
-
异步非阻塞机制
每个工作进程使用异步非阻塞方式,可以处理多个客户端请求 。
当某个工作进程 接收到客户端的请求以后,调用 IO 进行处理,如果不能立即得到结果,就去处理其他请求 (即为非阻塞);而客户端 在此期间也无需等待响应,可以去处理其他事情(即为异步)。
当 IO 返回时,就会通知此工作进程 ;该进程得到通知,暂时挂起当前处理的事务去响应客户端请求 。
nginx 是如何进行分流处理和负载均衡的?
分流处理:
- 根据 IP 分流
- 根据 URL 分流
- 根据权重
- 根据响应时间
NGINX 负载均衡分发请求的几种方式:
-
轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器 down 掉,能自动剔除。
-
weight
指定轮询几率,weight 和访问比率成正比,用于后端服务器性能不均的情况。
-
ip_hash
每个请求按访问 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器,可以解决 session 的问题
-
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
-
url_hash(第三方)
按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器,后端服务器为缓存时比较有效。
ngnix 的正向代理与反向代理?
首先,代理服务器一般指局域网内部的机器通过代理服务器发送请求到互联网上的服务器,代理服务器 一般作用在客户端。例如:GoAgentFQ软件。我们的客户端在进行FQ操作的时候,我们使用的正是 正向代理,通过正向代理的方式,在我们的客户端运行一个软件,将我们的HTTP请求转发到其他不同 的服务器端,实现请求的分发。
反向代理服务器作用在服务器端,它在服务器端接收客户端的请求,然后将请求分发给具体的服务器进行处理,然后再将服务器的相应结果反馈给客户端。Nginx就是一个反向代理服务器软件。
从上图可以看出:客户端必须设置正向代理服务器,当然前提是要知道正向代理服务器的IP地址,还有代理程序的端口。
反向代理正好与正向代理相反,对于客户端而言代理服务器就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端。
列举一些Nginx的特性
Nginx服务器的特性包括:
- 反向代理/L7负载均衡器
- 嵌入式Perl解释器
- 动态二进制升级
- 可用于重新编写URL,具有非常好的PCRE支持
在Nginx中,如何使用未定义的服务器名称来阻止处理请求?
只需将请求删除的服务器就可以定义为:
Server{
listen 80;
server_name "";
return 444;
}
这里,服务器名被保留为一个空字符串,它将在没有“主机”头字段的情况下匹配请求,而一个特殊的 Nginx的非标准代码444被返回,从而终止连接。
一般推荐 worker 进程数与CPU内核数一致,这样一来不存在大量的子进程生成和管理任务,避免了进 程之间竞争CPU 资源和进程切换的开销。而且 Nginx 为了更好的利用 多核特性 ,提供了 CPU 亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来 Cache 的失效。
对于每个请求,有且只有一个工作进程对其处理。首先,每个 worker 进程都是从 master进程 fork 过来。在 master 进程里面,先建立好需要 listen 的 socket(listenfd)之后,然后再 fork 出多个 worker 进程。
所有 worker 进程的 listenfd 会在新连接到来时变得可读 ,为保证只有一个进程处理该连接,所有 worker 进程在注册 listenfd 读事件前抢占 accept_mutex ,抢到互斥锁的那个进程注册 listenfd 读事件 ,在读事件里调用 accept 接受该连接。
当一个 worker 进程在 accept 这个连接之后,就开始读取请求、解析请求、处理请求,产生数据后, 再返回给客户端 ,最后才断开连接。这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由 worker 进程来处理,而且只在一个 worker 进程中处理。
在 Nginx 服务器的运行过程中, 主进程和工作进程需要进程交互。交互依赖于 Socket 实现的管道来实现。
请解释Nginx服务器上的Master和Worker进程分别是什么?
- 主程序 Master process 启动后,通过一个 for 循环来接收和处理外部信号;
- 主进程通过 fork() 函数产生 worker 子进程 ,每个子进程执行一个for循环来实现Nginx服务器对事件的接收和处理 。
谈一下你对 uWSGI 和 nginx 的理解?
uWSGI 是一个 Web 服务器,它实现了 WSGI 协议、uwsgi、http 等协议。Nginx 中HttpUwsgiModule 的作用是与 uWSGI 服务器进行交换。WSGI 是一种 Web 服务器网关接口。它是一个 Web 服务器(如 nginx,uWSGI 等服务器)与 web 应用(如用 Flask 框架写的程序)通信的一种规范。
要注意 WSGI / uwsgi / uWSGI 这三个概念的区分。
uwsgi 是一种线路协议而不是通信协议,在此常用于在 uWSGI 服务器与其他网络服务器的数据通信。
uWSGI 是实现了 uwsgi 和 WSGI 两种协议的 Web 服务器。
nginx 是一个开源的高性能的 HTTP 服务器和反向代理:
- 作为 web 服务器,它处理静态文件和索引文件效果非常高;
- 它的设计非常注重效率,最大支持 5 万个并发连接,但只占用很少的内存空间;
- 稳定性高,配置简洁;
- 强大的反向代理和负载均衡功能,平衡集群中各个服务器的负载压力应用。
说说 nginx 和 uWISG 服务器之间如何配合工作的?
首先浏览器发起 http 请求到 nginx 服务器,Nginx 根据接收到请求包,进行 url 分析,判断访问的资源类型,如果是静态资源,直接读取静态资源返回给浏览器,如果请求的是动态资源就转交给 uwsgi服务器,uwsgi 服务器根据自身的 uwsgi 和 WSGI 协议,找到对应的 Django 框架,Django 框架下的应用进行逻辑处理后,将返回值发送到 uwsgi 服务器,然后 uwsgi 服务器再返回给 nginx,最后 nginx将返回值返回给浏览器进行渲染显示给用户。
如果可以,画图讲解效果更佳。
apache 和 nginx 的区别?
Nginx 相对 Apache 的优点:
- 轻量级,同样起 web 服务,比 apache 占用更少的内存及资源;
- 抗并发,nginx 处理请求是异步非阻塞的,支持更多的并发连接,而 apache 则是阻塞型的,在高
- 并发下 nginx 能保持低资源低消耗高性能;
- 配置简洁;
- 高度模块化的设计,编写模块相对简单;
- 社区活跃。
Apache 相对 Nginx 的优点:
- rewrite ,比 nginx 的 rewrite 强大;
- 模块超多,基本想到的都可以找到;
- 少 bug ,nginx 的 bug 相对较多;
- 超稳定。
zookeeper
ZooKeeper 是什么?
ZooKeeper 是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。
Zookeeper 保证了如下分布式一致性特性:
- 顺序一致性
- 原子性
- 单一视图
- 可靠性
- 实时性(最终一致性)
客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求,这些请求会同时发给其他 zookeeper 机器并且达成一致后,请求才会返回成功。因此,随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。
有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper 最新的 zxid。
ZooKeeper 提供了什么?
- 文件系统
- 通知机制
Zookeeper 文件系统
Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。
Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。
ZAB 协议?
ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。
ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。
当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步,当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
四种类型的数据节点 Znode
- PERSISTENT-持久节点:除非手动删除,否则节点一直存在于 Zookeeper 上
- EPHEMERAL-临时节点:临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper 连接断开不一定会 话失效),那么这个客户端创建的所有临时节点都会被移除。
- PERSISTENT_SEQUENTIAL-持久顺序节点:基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
- EPHEMERAL_SEQUENTIAL-临时顺序节点:基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
Zookeeper Watcher 机制 -- 数据变更通知
Zookeeper 允许客户端向服务端的某个 Znode 注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。
工作机制:
- 客户端注册 watcher
- 服务端处理 watcher
- 客户端回调 watcher
Watcher 特性总结:
- 一次性:无论是服务端还是客户端,一旦一个 Watcher 被 触 发 ,Zookeeper 都会将其从相应的存储中移除。 这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送 事件通知,无论对于网络还是服务端的压力都非常大。
- 客户端串行执行:客户端 Watcher 回调的过程是一个串行同步的过程。
- 轻量
- Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。
- 客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象实体传递到服务端, 仅仅是在客户端请求中使用 boolean 类型属性进行了标记。
- watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的,这就存在一个问 题,不同的客户端和服务器之间通过 socket 进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。 Zookeeper 只能保证最终的一致性,而无法保证强一致性。
- 注册 watcher getData、exists、getChildren
- 触发 watcher create、delete、setData
- 当一个客户端连接到一个新的服务器上时,watch 将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到 watch 的。而当 client 重新连接时,如果需要的话,所有先前注册过 的 watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch 可能会丢失:对于一个未创建的 znode的 exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 watch 事件可能会被丢失。
客户端注册 Watcher 实现
- 调用 getData()/getChildren()/exist()三个 API,传入 Watcher 对象
- 标记请求 request,封装 Watcher 到 WatchRegistration
- 封装成 Packet 对象,发服务端发送 request
- 收到服务端响应后,将 Watcher 注册到 ZKWatcherManager 中进行管理
- 请求返回,完成注册。
服务端处理 Watcher 实现
-
服务端接收 Watcher 并存储
接收到客户端请求,处理请求判断是否需要注册 Watcher,需要的话将数据节点的节点路径和 ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接,实现了 Watcher 的 process 接口,此时 可以看成一个 Watcher 对象)存储在WatcherManager 的 WatchTable 和 watch2Paths 中去。
-
Watcher 触发
以服务端接收到 setData() 事务请求触发 NodeDataChanged 事件为例:
-
封装 WatchedEvent
将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个 WatchedEvent 对象
-
查询 Watcher
从 WatchTable 中根据节点路径查找 Watcher
-
没找到;说明没有客户端在该数据节点上注册过 Watcher
-
找到;提取并从 WatchTable 和 Watch2Paths 中删除对应 Watcher(从这里可以看出 Watcher 在 服务端是一次性的,触发一次就失效了)
-
-
调用 process 方法来触发
Watcher 这里 process 主要就是通过 ServerCnxn 对应的 TCP 连接发送 Watcher 事件通知。
客户端回调 Watcher
客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。
客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。
ACL 权限控制机制
UGO(User/Group/Others):目前在 Linux/Unix 文件系统中使用,也是使用最广泛的权限控制方式。是一种粗粒度的文件系统权限控制模式。
ACL(Access Control List):访问控制列表
包括三个方面:
- 权限模式(Scheme)
- IP:从 IP 地址粒度进行权限控制
- Digest:最常用,用类似于 username:password 的权限标识来进行权限配置,便于区分不同应 用来进行权限控制
- World:最开放的权限控制方式,是一种特殊的 digest 模式,只有一个权限标识“world:anyone”
- Super:超级用户
- 授权对象:授权对象指的是权限赋予的用户或一个指定实体,例如 IP 地址或是机器灯。
- 权限 Permission:
- CREATE:数据节点创建权限,允许授权对象在该 Znode 下创建子节点
- DELETE:子节点删除权限,允许授权对象删除该数据节点的子节点
- READ:数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等
- WRITE:数据节点更新权限,允许授权对象对该数据节点进行更新操作
- ADMIN:数据节点管理权限,允许授权对象对该数据节点进行 ACL 相关设置操作
Chroot 特性
3.2.0 版本后,添加了 Chroot 特性,该特性允许每个客户端为自己设置一个命名空间。如果一个客户端设置了 Chroot,那么该客户端对服务器的任何操作,都将会被限制在其自己的命名空间下。
通过设置 Chroot,能够将一个客户端应用于 Zookeeper 服务端的一颗子树相对应,在那些多个应用公用一个 Zookeeper 进群的场景下,对实现不同应用间的相互隔离非常有帮助。
会话管理
分桶策略:将类似的会话放在同一区块中进行管理,以便于 Zookeeper 对会话进行不同区块的隔离处理以及同一区块的统一处理。
分配原则:每个会话的“下次超时时间点”(ExpirationTime)
计算公式:
ExpirationTime_ = currentTime + sessionTimeout
ExpirationTime = (ExpirationTime_ / ExpirationInrerval + 1) * ExpirationInterval
ExpirationInterval 是指 Zookeeper 会话超时检查时间间隔,默认 tickTime
服务器角色
- Leader
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
- 集群内部各服务的调度者
- Follower
- 处理客户端的非事务请求,转发事务请求给 Leader 服务器
- 参与事务请求 Proposal 的投票
- 参与 Leader 选举投票
- Observer
- 3.0 版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力
- 处理客户端的非事务请求,转发事务请求给 Leader 服务器
- 不参与任何形式的投票
Zookeeper 下 Server 工作状态
服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。
- LOOKING:寻 找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因 此需要进入 Leader 选举状态。
- FOLLOWING:跟随者状态。表明当前服务器角色是 Follower。
- LEADING:领导者状态。表明当前服务器角色是 Leader。
- OBSERVING:观察者状态。表明当前服务器角色是 Observer。
数据同步
整个集群完成 Leader 选举之后,Learner(Follower 和 Observer 的统称)回向Leader 服务器进行注 册。当 Learner 服务器想 Leader 服务器完成注册后,进入数据同步环节。
数据同步流程:(均以消息传递的方式进行)
- Learner 向 Learder 注册
- 数据同步
- 同步确认
- Zookeeper 的数据同步通常分为四类:
- 直接差异化同步(DIFF 同步)
- 先回滚再差异化同步(TRUNC+DIFF 同步)
- 仅回滚同步(TRUNC 同步)
- 全量同步(SNAP 同步)
- Zookeeper 的数据同步通常分为四类:
在进行数据同步前,Leader 服务器会完成数据同步初始化:
peerLastZxid:从 learner 服务器注册时发送的 ACKEPOCH 消息中提取 lastZxid(该Learner 服务器最后处理的 ZXID)
minCommittedLog:
-
Leader 服务器 Proposal 缓存队列 committedLog 中最小 ZXIDmaxCommittedLog
-
Leader 服务器 Proposal 缓存队列 committedLog 中最大 ZXID直接差异化同步(DIFF 同步) ·
场景:peerLastZxid 介于 minCommittedLog 和 maxCommittedLog之间先回滚再差异化同步 (TRUNC+DIFF 同步)
场景:当新的 Leader 服务器发现某个 Learner 服务器包含了一条自己没有的事务记录,那么就需要让该 Learner 服务器进行事务回滚--回滚到 Leader服务器上存在的,同时也是最接近于 peerLastZxid 的 ZXID仅回滚同步(TRUNC 同步)
场景:peerLastZxid 大于 maxCommittedLog
全量同步(SNAP 同步)
场景一:peerLastZxid 小于 minCommittedLog
场景二:Leader 服务器上没有 Proposal 缓存队列且 peerLastZxid 不等于 lastProcessZxid
zookeeper 是如何保证事务的顺序一致性的?
zookeeper 采用了全局递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch( 时期; 纪元; 世; 新时代)用来标识 leader 周期,如果有新的 leader 产生出来,epoch会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都 能执行并且能够成功,那么就会开始执行。
分布式集群中为什么会有 Master?
在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果, 这样可以大大减少重复计算,提高性能,于是就需要进行leader 选举。
集群最少要几台机器,集群规则是怎样的?
集群规则为 2N+1 台,N>0,即 3 台。
zk 节点宕机如何处理?
Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他节点会继续提供服务。
如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不会丢失;
如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。 ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在 ZK节点挂得太多,只剩一 半或不到一半节点能工作,集群才失效。 所以 3 个节点的 cluster 可以挂掉 1 个节点(leader 可以得到 2 票>1.5); 2 个节点的 cluster 就不能挂掉任何 1 个节点了(leader 可以得到 1 票<=1)
Zookeeper 有哪几种几种部署模式?
部署模式:单机模式、伪集群模式、集群模式。
集群支持动态添加机器吗?
其实就是水平扩容了,Zookeeper 在这方面不太好。两种方式:
- 全部重启:关闭所有 Zookeeper 服务,修改配置之后启动。不影响之前客户端的会话。
- 逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。
3.5 版本开始支持动态扩容。
Zookeeper 对节点的 watch 监听通知是永久的吗?为什么不是永久的?
不是。官方声明:一个 Watch 事件是一个一次性的触发器,当被设置了 Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了 Watch 的客户端,以便通知它们。
为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。
一般是客户端执行 getData(“/节点 A”,true),如果节点 A 发生了变更或删除,客户端会得到它的 watch 事件,但是在之后节点 A 又发生了变更,而客户端又没有设置 watch 事件,就不再给客户端发送。 在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。
chubby 是什么,和 zookeeper 比你怎么看?
chubby 是 google 的,完全实现 paxos 算法,不开源。zookeeper 是 chubby的开源实现,使用 zab 协议,paxos 算法的变种。
说几个 zookeeper 常用的命令
常用命令:ls get set create delete 等。
ZAB 和 Paxos 算法的联系与区别?
相同点:
- 两者都存在一个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行
- Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交
- ZAB 协议中,每个 Proposal 中都包含一个 epoch 值来代表当前的 Leader周期,Paxos 中名字为 Ballot
不同点:
- ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统
Zookeeper 的典型应用场景
Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布和订阅。
通过对 Zookeeper 中丰富的数据节点进行交叉使用,配合 Watcher 事件通知机制,可以非常方便的构建一系列分布式应用中年都会涉及的核心功能,如:
-
数据发布/订阅
介绍:数据发布/订阅系统,即所谓的配置中心,顾名思义就是发布者发布数据供订阅者进行数据订阅。
目的:
- 动态获取数据(配置信息)
- 实现数据(配置信息)的集中式管理和数据的动态更新
- 设计模式
- Push 模式
- Pull 模式
- 数据(配置信息)特性
- 数据量通常比较小
- 数据内容在运行时会发生动态更新
- 集群中各机器共享,配置一致。如:机器列表信息、运行时开关配置、数据库配置信息等
- 基于 Zookeeper 的实现方式
- 数据存储:将数据(配置信息)存储到 Zookeeper 上的一个数据节点
- 数据获取:应用在启动初始化节点从 Zookeeper 数据节点读取数据,并在该节点上注册一个数据变更 Watcher
- 数据变更:当变更数据时,更新 Zookeeper 对应节点数据,Zookeeper会将数据变更通知发到各客户端,客户端接到通知后重新读取变更后的数据即可。
-
负载均衡
zk 的命名服务:命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局的路径,这个路径就 可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。
-
分布式通知和协调
对于系统调度来说:操作人员发送通知实际是通过控制台改变某个节点的状态,然后 zk 将这些变化发 送给注册了这个节点的 watcher 的所有客户端。 对于执行情况汇报:每个工作进程都在某个目录下创建一个临时节点。并携带工作的进度数据,这样汇 总的进程可以监控目录子节点的变化获得工作进度的实时的全局情况。
-
zk 的命名服务(文件系统)
命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局的路径,即是唯一的 路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等 等。
-
zk 的配置管理(文件系统、通知机制)
程序分布式的部署在不同的机器上,将程序的配置信息放在 zk 的 znode 下,当有配置发生改变时,也 就是 znode 发生变化时,可以通过改变 zk 中某个目录节点的内容,利用 watcher 通知给各个客户端, 从而更改配置。
-
Zookeeper 集群管理(文件系统、通知机制)
所谓集群管理无在乎两点:是否有机器退出和加入、选举 master。 对于第一点,所有机器约定在父目录下创建临时目录节点,然后监听父目录节点 的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper 的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个兄弟目录被删除,于是,所有人都知道:它上船了。 新机器加入也是类似,所有机器收到通知:新兄弟目录加入,highcount 又有了,对于第二点,我们稍 微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为 master 就好。
-
Zookeeper 分布式锁(文件系统、通知机制)
有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另 一个是控制时序。
- 对于第一类,我们将 zookeeper 上的一个 znode 看作是一把锁,通过 createznode的方式来实现。所 有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的 distribute_lock 节点就释放出锁。
- 对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选 master 一样,编号最小的获得锁,用完删除,依次方便。 Zookeeper 队列管理(文件系统、通知机制)
两种类型的队列:
- 同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。
- 队列按照 FIFO 方式进行入队和出队操作。
第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。
第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下 创建 PERSISTENT_SEQUENTIAL 节点,创建成功时Watcher 通知等待的队列,队列删除序列号最小的 节点用以消费。此场景下Zookeeper 的 znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必 担心队列消息的丢失问题。
zookeeper 负载均衡和 nginx 负载均衡区别
zk 的负载均衡是可以调控,nginx 只是能调权重,其他需要可控的都需要自己写插件;但是 nginx 的吞吐量比 zk 大很多,应该说按业务选择用哪种方式。
kafka
如何获取 topic 主题的列表
bin/kafka-topics.sh --list --zookeeper localhost:2181
生产者和消费者的命令行是什么?
生产者在主题上发布消息:
bin/kafka-console-producer.sh --broker-list 192.168.43.49:9092 --topicHello-Kafka
注意这里的 IP 是 server.properties 中的 listeners 的配置。接下来每个新行就是输入一条新消息。
消费者接受消息:
bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topicHello-Kafka --from-beginning
consumer 是推还是拉?
Kafka 最初考虑的问题是,customer 应该从 brokes 拉取消息还是 brokers 将消息推送到 consumer, 也就是 pull 还 push。在这方面,Kafka 遵循了一种大部分消息系统共同的传统的设计:producer 将消息推送到 broker,consumer 从broker 拉取消息。
一些消息系统比如 Scribe 和 Apache Flume 采用了 push 模式,将消息推送到下游的 consumer。这样做有好处也有坏处:由 broker 决定消息推送的速率,对于不同消费速率的 consumer 就不太好处理 了。消息系统都致力于让 consumer 以最大的速率最快速的消费消息,但不幸的是,push 模式下,当 broker 推送的速率远大于 consumer 消费的速率时,consumer 恐怕就要崩溃了。最终 Kafka 还是选取了传统的 pull 模式。
Pull 模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据 。Push 模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推 送。如果为了避免 consumer 崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪 费。Pull 模式下,consumer 就可以根据自己的消费能力去决定这些策略。 Pull 有个缺点是,如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询,直到新消息到 t 达。为了避免这点,Kafka 有个参数可以让 consumer阻塞知道新消息到达(当然也可以阻塞知道 消息的数量达到某个特定的量这样就可以批量发送)。
讲讲 kafka 维护消费状态跟踪的方法
大部分消息系统在 broker 端的维护消息被消费的记录:一个消息被分发到consumer 后 broker 就马上进行标记或者等待 customer 的通知后进行标记。这样也可以在消息在消费后立马就删除以减少空间占用。
但是这样会不会有什么问题呢?如果一条消息发送出去之后就立即被标记为消费过的,一旦 consumer 处理消息时失败了(比如程序崩溃)消息就丢失了。为了解决这个问题,很多消息系统提供了另外一个功能:当消息被发送出去之后仅仅被标记为已发送状态,当接到 consumer 已经消费成功的通知后才标记为已被消费的状态。这虽然解决了消息丢失的问题,但产生了新问题,首先如果 consumer处理消息成功了但是向 broker 发送响应时失败了,这条消息将被消费两次。第二个问题时,broker 必须维护每条消息的状态,并且每次都要先锁住消息然后更改状态然后释放锁。这样麻烦又来了,且不说要维护大量的状态数据,比如如果消息发送出去但没有收到消费成功的通知,这条消息将一直处于被锁定的状 态,Kafka 采用了不同的策略。Topic 被分成了若干分区,每个分区在同一时间只被一个 consumer 消 费。这意味着每个分区被消费的消息在日志中的位置仅仅是一个简单的整数:offset。这样就很容易标 记每个分区消费状态就很容易了,仅仅需要一个整数而已。这样消费状态的跟踪就很简单了。
这带来了另外一个好处:consumer 可以把 offset 调成一个较老的值,去重新消费老的消息。这对传统 的消息系统来说看起来有些不可思议,但确实是非常有用的,谁规定了一条消息只能被消费一次呢?
讲一下主从同步
Kafka允许topic的分区拥有若干副本,这个数量是可以配置的,你可以为每个topci配置副本的数量。 Kafka会自动在每个个副本上备份数据,所以当一个节点down掉时数据依然是可用的。 Kafka的副本功能不是必须的,你可以配置只有一个副本,这样其实就相当于只有一份数据。
为什么需要消息系统,mysql 不能满足需求吗?
- 解耦: 允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
- 冗余: 消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队 列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
- 扩展性: 因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过 程即可。
- 灵活性 & 峰值处理能力: 在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理 这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
- 可恢复性: 系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理 消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
- 顺序保证: 在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按 照特定的顺序来处理。(Kafka 保证一个 Partition 内的消息的有序性)
- 缓冲: 有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
- 异步通信: 很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入 队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
Zookeeper 对于 Kafka 的作用是什么?
Zookeeper 是一个开放源码的、高性能的协调服务,它用于 Kafka 的分布式应用。 Zookeeper 主要用于在集群中不同节点之间进行通信 在 Kafka 中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取除此之外,它还执行其他活动,如: leader 检测、分布式同步、配置管理、识别新节点何时离 开或连接、集群、节点实时状态等等。
数据传输的事务定义有哪三种?
和 MQTT 的事务定义一样都是 3 种。
- 最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输
- 最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
- 精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输 一次,这是大家所期望的
Kafka 判断一个节点是否还活着有那两个条件?
- 节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接
- 如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久
Kafka 与传统 MQ 消息系统之间有三个关键区别
- Kafka 持久化日志,这些日志可以被重复读取和无限期保留
- Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力 和高可用性
- Kafka 支持实时的流式处理
讲一讲 kafka 的 ack 的三种机制
request.required.acks 有三个值 0 1 -1(all)
- 0:生产者不会等待 broker 的 ack,这个延迟最低但是存储的保证最弱当 server 挂掉的时候就会丢数据。
- 1:服务端会等待 ack 值 leader 副本确认接收到消息后发送 ack 但是如果 leader挂掉后他不确保是否复制完成新 leader 也会导致数据丢失。
- -1(all):服务端会等所有的 follower 的副本受到数据后才会受到 leader 发出的ack,这样数据不会丢失
消费者如何不自动提交偏移量,由应用提交?
将 auto.commit.offset 设为 false,然后在处理一批消息后 commitSync() 或者异步提交 commitAsync()
即:
ConsumerRecords<> records = consumer.poll();
for (ConsumerRecord<> record : records){
。。。
tyr{
consumer.commitSync()
}
。。。
消费者故障,出现活锁问题如何解决?
出现“活锁”的情况,是它持续的发送心跳,但是没有处理。为了预防消费者在这种情况下一直持有分区,我们使用 max.poll.interval.ms 活跃检测机制。 在此基础上,如果你调用的 poll 的频率大于最大 间隔,则客户端将主动地离开组,以便其他消费者接管该分区。 发生这种情况时,你会看到 offset 提 交失败(调用commitSync()引发的 CommitFailedException)。这是一种安全机制,保障只有活动 成员能够提交 offset。所以要留在组中,你必须持续调用 poll。
消费者提供两个配置设置来控制 poll 循环:
- max.poll.interval.ms:增大 poll 的间隔,可以为消费者提供更多的时间去处理返回的消息(调用 poll(long)返回的消息,通常返回的消息都是一批)。缺点是此值越大将会延迟组重新平衡。
- max.poll.records:此设置限制每次调用 poll 返回的消息数,这样可以更容易的预测每次 poll 间隔要处理的最大值。通过调整此值,可以减少 poll 间隔,减少重新平衡分组的
对于消息处理时间不可预测地的情况,这些选项是不够的。 处理这种情况的推荐方法是将消息处理移到 另一个线程中,让消费者继续调用 poll。 但是必须注意确保已提交的 offset 不超过实际位置。另外, 你必须禁用自动提交,并只有在线程完成处理后才为记录手动提交偏移量(取决于你)。 还要注意,你需要 pause 暂停分区,不会从 poll 接收到新消息,让线程处理完之前返回的消息(如果你的处理能力 比拉取消息的慢,那创建新线程将导致你机器内存溢出)。
如何控制消费的位置 kafka
使用 seek(TopicPartition, long)指定新的消费位置。用于查找服务器保留的最早和最新的 offset 的特殊的方法也可用(seekToBeginning(Collection) 和seekToEnd(Collection))
kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费?
Kafka 分布式的单位是 partition,同一个 partition 用一个 write ahead log 组织,所以可以保证 FIFO 的顺序。不同 partition 之间不能保证顺序。但是绝大多数用户都可以通过 message key 来定义,因为 同一个 key 的 message 可以保证只发送到同一个 partition。
Kafka 中发送 1 条消息的时候,可以指定(topic, partition, key) 3 个参数。partiton 和 key 是可选的。 如果你指定了 partition,那就是所有消息发往同 1个 partition,就是有序的。并且在消费端,Kafka 保 证,1 个 partition 只能被1 个 consumer 消费。或者你指定 key( 比如 order id),具有同 1 个 key 的所有消息,会发往同 1 个 partition。
kafka 的高可用机制是什么?
这个问题比较系统,回答出 kafka 的系统特点,leader 和 follower 的关系,消息读写的顺序即可。
kafka 如何减少数据丢失
Kafka到底会不会丢数据(data loss)? 通常不会,但有些情况下的确有可能会发生。下面的参数配置及 Best practice列表可以较好地保证数据的持久性(当然是trade-off,牺牲了吞吐量)。
- block.on.buffer.full = true
- acks = all
- retries = MAX_VALUE
- max.in.flight.requests.per.connection = 1
- 使用KafkaProducer.send(record, callback)
- callback逻辑中显式关闭producer:close(0)
- unclean.leader.election.enable=false
- replication.factor = 3
- min.insync.replicas = 2
- replication.factor > min.insync.replicas
- enable.auto.commit=false
- 消息处理完成之后再提交位移
kafka 如何不消费重复数据?比如扣款,我们不能重复的扣。
其实还是得结合业务来思考,我这里给几个思路:
- 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
- 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
- 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全 局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一 下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理 了,保证别重复处理相同的消息即可。
- 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会 报错,不会导致数据库中出现脏数据。
MQ
什么是消息队列?
消息队列 Message Queue,简称 MQ。
是一种应用间的通信方式,主要由三个部分组成。
生产者:Producer
-
消息的产生者与调用端
-
主要负责消息所承载的业务信息的实例化
-
是一个队列的发起方
代理:Broker
-
主要的处理单元
-
负责消息的存储、投递、及各种队列附加功能的实现
-
是消息队列最核心的组成部分
消费者:Consumer
-
一个消息队列的终端
-
也是消息的调用端
-
具体是根据消息承载的信息,处理各种业务逻辑。 消息队列的应用场景较多,
常用的可以分为三种:
-
异步处理
主要应用于对实时性要求不严格的场景,
比如:用户注册发送验证码、下单通知、发送优惠券等等。服务方只需要把协商好的消息发送到消息队列,剩下的由消费消息的服务去处理,不用等待消费服务返回结果。
-
应用解耦
应用解耦可以看作是把相关但耦合度不高的系统联系起来。
比如订单系统与 WMS、EHR 系统,有关联但不哪么紧密,每个系统之间只需要把约定的消息发送到 MQ,另外的系统去消费即可。解决了各个系统可以采用不同的架构、语言来实现,从而大大增加了系统的灵活性。
-
流量削峰
流量削峰一般应用在大流量入口且短时间内业务需求处理不完的服务中心,为了权衡高可用,把大量的并行任务发送到 MQ 中,依据 MQ 的存储及分发功能,平稳的处理后续的业务,起到一个大流量缓冲的作用。
目前市面上常见的消息队列中间件主要有ActiveMQ、RabbitMQ、Kafka、RocketMQ 这几种。
在架构技术选型的时候一般根据业务的需求选择合适的中间件:比如中小型公司,低吞吐量的一般用 ActiveMQ、RabbitMQ 较为合适,大数据高吞吐量的大型公司一般选用 Kafka 和 RocketMQ。
为什么使用MQ
核心:解耦,异步,削峰
-
解耦:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃......A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用 MQ,A 系统产生一 条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统 压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超 时等情况。
就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。
-
异步:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库 要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应 给用户,总时长是 3 + 5 = 8ms。
-
削峰:减少高峰时期对服务器压力。
MQ优缺点
优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。
缺点有以下几个:
- 系统可用性降低:系统引入的外部依赖越多,越容易挂掉。万一 MQ 挂了,MQ 一挂,整套系统崩溃,你不就完了?
- 系统复杂度提高:硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?问题一大堆。
- 一致性问题:A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那 里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别?
对于吞吐量来说kafka和RocketMQ支撑高吞吐,ActiveMQ和RabbitMQ比他们低一个数量级。对于延 迟量来说RabbitMQ是最低的。
-
从社区活跃度:按照目前网络上的资料,RabbitMQ 、activeM 、ZeroMQ 三者中,综合来看,RabbitMQ 是首选。
-
持久化消息比较:ActiveMq 和RabbitMq 都支持。持久化消息主要是指我们机器在不可抗力因素等情况下挂掉了,消息 不会丢失的机制。
-
综合技术实现:可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统 等等。
RabbitMq / Kafka 最好,ActiveMq 次之,ZeroMq 最差。当然ZeroMq 也可以做到,不过自己必须手动写代码实现,代码量不小。尤其是可靠性中的:持久性、投递确认、发布者证实和高可用性。
-
高并发:毋庸置疑,RabbitMQ 最高,原因是它的实现语言是天生具备高并发高可用的erlang 语言。
-
比较关注的比较, RabbitMQ 和 Kafka
RabbitMq 比Kafka 成熟,在可用性上,稳定性上,可靠性上, RabbitMq 胜于 Kafka (理论上)。
另外,Kafka 的定位主要在日志等方面, 因为Kafka 设计的初衷就是处理日志的,可以看做是一个日志 (消息)系统一个重要组件,针对性很强,所以 如果业务方面还是建议选择 RabbitMq 。
还有就是,Kafka 的性能(吞吐量、TPS )比RabbitMq 要高出来很多。
如何保证高可用的?
RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以 RabbitMQ 为例 子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群 模式。
-
单机模式:就是 Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式 普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量 的,就是说让集群中多个节点来服务某个 queue 的读写操作。
-
镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像 集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每 个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消 息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台, 就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节 点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据 同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点) 还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第 一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重! RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的 完整数据。
Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这 个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放 一部分数据。这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每 个机器就放一部分数据。Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候, leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度 太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可 以提高容错性。因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都 有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一 旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会 返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)消费的时候, 只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息 才会被消费者读到。
如何保证消息的可靠传输?如果消息丢了怎么办
数据的丢失问题,可能出现在生产者、MQ、消费者中
-
生产者丢失:生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥 的,都有可能。此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异 常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提 交事务channel.txCommit。吞吐量会下来,因为太耗性能。所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的 消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个ack消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一 定时间还没接收到这个消息的回调,那么你可以重发。事务机制和cnofirm机制最大的不同在于,事务 机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就 可以发送下一个消息,然后那个消息RabbitMQ 接收了之后会异步回调你一个接口通知你这个消息接收 到了。所以一般在生产者这块避免数据丢失,都是用confirm机制的。
-
MQ中丢失:就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之 后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会 丢。设置持久化有两个步骤:创建 queue 的时候将其设置为持久化,这样就可以保证 RabbitMQ 持久 化 queue 的元数据,但是不会持久化 queue 里的数据。第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上 去。必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。持久化可以跟生产者那边的confirm机制配合起来,只有消息被持 久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢 了,生产者收不到ack,你也是可以自己重发的。注意,哪怕是你给 RabbitMQ 开启了持久化机制,也 有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。
-
消费端丢失:你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了, RabbitMQ 认为你都消费了,这数据就丢了。这个时候得用 RabbitMQ 提供的ack机制,简单来说,就 是你关闭 RabbitMQ 的自动ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的 时候,再在程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那 RabbitMQ 就认为你还 没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
如何保证消息的顺序性
先看看顺序会错乱的场景:RabbitMQ:一个 queue,多个 consumer,这不明显乱了;
解决:
如何解决消息队列的延时以及过期失效问题?消息队列满了以后 该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
消息积压处理办法:临时紧急扩容:
- 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。
- 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
- 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
- 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
- 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
- MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消 息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑 了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是 批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据 了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们 就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把 白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
- mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了, 咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消 费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
设计MQ的思路
比如说这个消息队列系统,我们从以下几个角度来考虑一下:
- 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设 计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移, 增加机器,不就可以存放更多数据,提供更高的吞吐量了?
- 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就 丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能 是很高的,这就是 kafka 的思路。
- 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。
- 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。
Elasticsearch
elasticsearch在项目中如何使用及过程的搭建
- 在工程文件中 settings 中,注册 haystack 应用并添加配置。
- 在被检索的应用目录下创建 search_indexes.py 文件
- 在 templates 下面新建目录 search/indexes/应用名,指定索引字段,使用命令生成索引文件。
- 项目 urls.py 文件配置 url
elasticsearch是怎么和数据库关联起来的
- 关系型数据库中的数据库(DataBase),等价于 ES 中的索引(Index)。
- 一个数据库下面有 N 张表(Table),等价于 1 个索引 Index 下面有 N 多类型(Type)
- 一个数据库表(Table)下的数据由多行(ROW)多列(column,属性)组成,等价于 1 个 Type 由多个文档(Document)和多 Field 组成。
- 在一个关系型数据库里面,schema 定义了表、每个表的字段,还有表和字段之间的关系。与之对应的,在 ES 中:Mapping 定义索引下的 Type 的字段处理规则,即索引如何建立、索引类型、是否保存原始索引 JSON 文档、是否压缩原始 JSON 文档、是否需要分词处理、如何进行分词处理等。
- 在数据库中的增 insert、删 delete、改 update、查 search 操作等价于 ES 中的增PUT、删 Delete、改 POST、查 GET。
对 es(elasticsearch)存储底层的原理你是如何理解的
-
Elasticsearch 对复杂分布式机制的透明隐藏特性
- 分片机制
- shard 副本
- 集群发现机制
- shard 负载均衡
elasticsearch 是分布式的,意味着索引可以被分成分片(仅保存数据的一部分),每个分片可以有 0 个或多个副本。每个节点(运行中的 es 实例)托管一个或多个分片。用户可以将请求发送到集群中的任何节点,每个节点都知道任意文档所处的位置。无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端
-
Elasticsearch 的垂直扩容与水平扩容
- 垂直扩容:采购更强大的服务器,成本非常高昂,而且会有瓶颈,假设世界上最强大的服务器容量就是 10T,但是当你的总数据量达到 5000T 的时候,你要采购多少台最强大的服务器啊
- 水平扩容:业界经常采用的方案,采购越来越多的普通服务器,性能比较一般,但是很多普通服务器组织在一起,就能构成强大的计算和存储能力
-
节点平等的分布式架构
- 节点对等,每个节点都能接收所有的请求
- 自动请求路由
- 响应收集
-
master 节点
- 创建或删除索引
- 增加或删除节点
-
基于_version 进行乐观锁并发控制
搜索的原理:倒排索引,用来存储在全文搜索下某个单词在文档下的存储位置的映射。分析(分词标准化),是由分析器完成的。
相关性排序,最相关的排在前面,主要通过检索词频率,反向文档频率,字段长度准则。
haystack 在使用过程中遇到的问题有哪些?
- 要想添加、删除和修改数据时,自动生成索引,需要在 settings 文件中指定:HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor‘
- haystack 建立数据索引,要先创建索引类。然后创建 text 字段索引值模板文件。
- 最后手动生成初始索引
elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数 据大小,分片有多少,以及一些调优手段 。
面试官:想了解应聘者之前公司接触的 ES 使用场景、规模,有没有做过比较大规模的索引设计、规划、调优。
解答:如实结合自己的实践场景回答即可。 比如:ES 集群架构 13 个节点,索引根据通道不同共 20+索引,根据日期,每日递增 20+,索引:10 分片,每日递增 1 亿+数据,每个通道每天索引大小控制:150GB 之内。
仅索引层面调优手段:
- 设计阶段调优
- 根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引;
- 使用别名进行索引管理;
- 每天凌晨定时对索引做 force_merge 操作,以释放空间;
- 采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink操作,以缩减存 储;
- 采取 curator 进行索引的生命周期管理;
- 仅针对需要分词的字段,合理的设置分词器;
- Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。……..
- 写入调优
- 写入前副本数设置为 0;
- 写入前关闭 refresh_interval 设置为-1,禁用刷新机制;
- 写入过程中:采取 bulk 批量写入;
- 写入后恢复副本数和刷新间隔;
- 尽量使用自动生成的 id。
- 查询调优
- 禁用 wildcard;
- 禁用批量 terms(成百上千的场景);
- 充分利用倒排索引机制,能 keyword 类型尽量 keyword;
- 数据量大时候,可以先基于时间敲定索引再检索;
- 设置合理的路由机制。
- 其他调优
- 部署调优,业务调优等。
- 上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。
elasticsearch 的倒排索引是什么
面试官:想了解你对基础概念的认知。
解答:通俗解释一下就可以。
传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。
而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。
学术的解答方式:
倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。
加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。
lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点:
- 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
- 查询速度快。O(len(str))的查询时间复杂度。
elasticsearch 索引数据多了怎么办,如何调优,部署
面试官:想了解大数据量的运维能力。
解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如 其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。
如何调优:
-
动态索引层面
基于模板+时间+rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为: blog_index_时间戳的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非 常大,接近于上线 2 的32 次幂-1,索引存储达到了 TB+甚至更大。
一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。
-
存储层面
冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。
对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索 效率。
-
部署层面
一旦之前没有规划,这里就属于应急策略。
结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。
elasticsearch 是如何实现 master 选举的
面试官:想了解 ES 集群的底层原理,不再只关注业务层面了。
解答:
前置前提:
- 只有候选主节点(master:true)的节点才能成为主节点。
- 最小主节点数(min_master_nodes)的目的是防止脑裂。
核对了一下代码,核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。选举流 程大致描述如下:
-
第一步:确认候选主节点数达标,elasticsearch.yml 设置的值 discovery.zen.minimum_master_nodes;
-
第二步:比较:先判定是否具备 master 资格,具备候选主节点资格的优先返回; 若两节点都为候选主节点,则 id 小的值会主节点。注意这里的 id 为 string 类型。
题外话:获取节点 id 的方法。
1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name 2ip port heapPercent heapMax id name复制代码
详细描述一下 Elasticsearch 索引文档的过程
面试官:想了解 ES 的底层原理,不再只关注业务层面了。
解答:
这里的索引文档应该理解为文档写入 ES,创建索引的过程。
文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程。
记住官方文档中的这个图。
第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。)
第二步:节点 1 接受到请求后,使用文档_id 来确定文档属于分片 0。请求会被转到另外的节点,假定 节点 3。因此分片 0 的主分片分配到节点 3 上。
第三步:节点 3 在主分片上执行写操作,如果成功,则将请求并行转发到节点 1和节点 2 的副本分片 上,等待结果返回。所有的副本分片都报告成功,节点 3 将向协调节点(节点 1)报告成功,节点 1 向 请求客户端报告写入成功。
如果面试官再问:第二步中的文档获取分片的过程?
回答:借助路由算法获取,路由算法就是根据路由和文档 id 计算目标的分片 id 的过程。
1shard = hash(_routing) % (num_of_primary_shards)复制代码
详细描述一下 Elasticsearch 搜索的过程?
面试官:想了解 ES 搜索的底层原理,不再只关注业务层面了。
解答:
搜索拆解为“query then fetch” 两个阶段。
query 阶段的目的:定位到位置,但不取。
步骤拆解如下:
- 假设一个索引数据有 5 主+1 副本 共 10 分片,一次请求会命中(主或者副本分片中)的一个。
- 每个分片在本地进行查询,结果返回到本地有序的优先队列中。
- 第 2)步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。
fetch 阶段的目的:取数据。 路由节点获取所有文档,返回给客户端。
Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法
面试官:想了解对 ES 集群的运维能力。
解答:
- 关闭缓存 swap;
- 堆内存设置为:Min(节点内存/2, 32GB);
- 设置最大文件句柄数;
- 线程池+队列大小根据业务需要做调整;
- 磁盘存储 raid 方式——存储有条件使用 RAID10,增加单节点性能以及避免单节点存储故障。
lucence 内部结构是什么?
面试官:想了解你的知识面的广度和深度。
解答:
Lucene 是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。可以基于这个脉络展开一 些。
Elasticsearch 是如何实现 Master 选举的?
- Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发 现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分;
- 对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节 点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。
- 如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选 举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。
- 补充:master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节 点可以关闭 http 功能*。
Elasticsearch 中的节点(比如共 20 个),其中的 10 个
选了一个 master,另外 10 个选了另一个 master,怎么办?
- 当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量
- (discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题;
- 当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data节点,避免脑裂问题。
客户端在和集群连接时,如何选择特定的节点执行请求的?
TransportClient 利用 transport 模块远程连接一个 elasticsearch 集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的 transport 地址,并以轮询的方式与这些地址进行通信。
详细描述一下 Elasticsearch 索引文档的过程。
协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片。
shard = hash(document_id) % (num_of_primary_shards)复制代码
- 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer,然后定时 (默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffer 到 Filesystem Cache 的过程就 叫做 refresh;
- 当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中 , 当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush;
- 在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交 点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。
- flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时;
补充:关于 Lucene 的 Segement:
- Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。
- 段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。
- 对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。
- 为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。
详细描述一下 Elasticsearch 更新和删除文档的过程。
- 删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;
- 磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del 文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。
- 在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del 文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。
详细描述一下 Elasticsearch 搜索的过程。
-
搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch;
-
在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在 本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。
PS:在搜索的时候是会查询 Filesystem Cache 的,但是有部分数据还在 MemoryBuffer,所以搜索是 近实时的。
-
每个分片返回各自优先队列中 所有文档的 ID 和排序值给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
-
接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。 每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
-
补充:Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。
在 Elasticsearch 中,是怎么根据一个词找到对应的倒排索引的?
- Lucene的索引过程,就是按照全文检索的基本过程,将倒排表写成此文件格式的过程。
- Lucene的搜索过程,就是按照此文件格式将索引进去的信息读出来,然后计算每篇文档打分 (score)的过程。
Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法?
-
64GB 内存的机器是非常理想的, 但是 32GB 和 16GB 机器也是很常见的。少于 8GB 会适得其反。
-
如果你要在更快的 CPUs 和更多的核心之间选择,选择更多的核心更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。
-
如果你负担得起 SSD,它将远远超出任何旋转介质。 基于 SSD 的节点,查询和索引性能都有提升。如果你负担得起,SSD 是一个好的选择。
-
即使数据中心们近在咫尺,也要避免集群跨越多个数据中心。绝对要避免集群跨越大的地理距离。
-
请确保运行你应用程序的 JVM 和服务器的 JVM 是完全一样的。 在Elasticsearch 的几个地方,使用 Java 的本地序列化。
-
通过设置 gateway.recover_after_nodes、gateway.expected_nodes、 gateway.recover_after_time 可以在集群重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟。
-
Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。最好使用单播代替组播。
-
不要随意修改垃圾回收器(CMS)和各个线程池的大小。
-
把你的内存的(少于)一半给 Lucene(但不要超过 32 GB!),通过ES_HEAP_SIZE 环境变量设 置。
-
内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个100 微秒的操作可能变成 10 毫秒。 再想想那么多 10 微秒的操作时延累加起来。 不难看出 swapping 对于性能是多么可怕。
-
Lucene 使用了大量的文件。同时,Elasticsearch 在节点和 HTTP 客户端之间进行通信也使用了大量的套接字。 所有这一切都需要足够的文件描述符。你应该增加你的文件描述符,设置一个很大的 值,如 64,000。
补充:索引阶段性能提升方法
- 使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
- 存储:使用 SSD
- 段和合并:Elasticsearch 默认值是 20 MB/s,对机械磁盘应该是个不错的设置。如果你用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并 限流。另外还可以增加index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的 值,比如 1 GB,这可以在一次清空触发的时候在事务日志里积累出更大的段。
- 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的index.refresh_interval 改到 30s。
- 如果你在做大批量导入,考虑通过设置 index.number_of_replicas: 0 关闭副本。
对于 GC 方面,在使用 Elasticsearch 时要注意什么?
- 倒排词典的索引需要常驻内存,无法 GC,需要监控 data node 上 segmentmemory 增长趋势。
- 各类缓存,field cache, filter cache, indexing cache, bulk queue 等等,要设置合理的大小,并且要应该根据最坏的情况来看 heap 是否够用,也就是各类缓存全部占满的时候,还有 heap 空间可以分配给其他任务吗?避免采用 clear cache等“自欺欺人”的方式来释放内存。
- 避免返回大量结果集的搜索与聚合。确实需要大量拉取数据的场景,可以采用scan & scroll api 来实现。
- cluster stats 驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过 tribe node 连接。
- 想知道 heap 够不够,必须结合实际应用场景,并对集群的 heap 使用情况做持续的监控。
- 根据监控数据理解内存需求,合理配置各类circuit breaker,将内存溢出风险降低到最低
Elasticsearch 对于大数据量(上亿量级)的聚合如何实现?
Elasticsearch 提供的首个近似聚合是 cardinality 度量。它提供一个字段的基数,即该字段的 distinct 或者 unique 值的数目。它是基于 HLL 算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用 量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。
在并发情况下,Elasticsearch 如果保证读写一致?
- 可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;
- 另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用 时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。
- 对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查 询主分片,确保文档是最新版本。
如何监控 Elasticsearch 集群状态?
Marvel 让你可以很简单的通过 Kibana 监控 Elasticsearch。你可以实时查看你的集群健康状态和性能,也可以分析过去的集群、索引和节点指标。
介绍下你们电商搜索的整体技术架构
介绍一下你们的个性化搜索方案?
基于word2vec和Elasticsearch实现个性化搜索
- 基于word2vec、Elasticsearch和自定义的脚本插件,我们就实现了一个个性化的搜索服务,相对于原有的实现,新版的点击率和转化率都有大幅的提升;
- 基于word2vec的商品向量还有一个可用之处,就是可以用来实现相似商品的推荐;
- 使用word2vec来实现个性化搜索或个性化推荐是有一定局限性的,因为它只能处理用户点击历史这样的时序数据,而无法全面的去考虑用户偏好,这个还是有很大的改进和提升的空间;
是否了解字典树?
常用字典数据结构如下所示
Trie 的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 它有 3 个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
可以看到,trie 树每一层的节点数是 26^i 级别的。所以为了节省空间,我们还可以用动态链表, 或者用数组来模拟动态。而空间的花费,不会超过单词数×单词长度。
实现:对每个结点开一个字母集大小的数组,每个结点挂一个链表,使用左儿子右兄弟表示法记 录这棵树;
对于中文的字典树,每个节点的子节点用一个哈希表存储,这样就不用浪费太大的空间,而且查询速度上可以保留哈希的复杂度 O(1)。
拼写纠错是如何实现的?
-
拼写纠错是基于编辑距离来实现;编辑距离是一种标准的方法,它用来表示经过插入、删除和替换操作从一个字符串转换到另外一个字符串的最小操作步数;
-
编辑距离的计算过程:比如要计算 batyu 和 beauty 的编辑距离,先创建一个7×8 的表(batyu 长度为 5,coffee 长度为 6,各加 2),接着,在如下位置填入黑色数字。其他格的计算过程是取以下三个值的最小值:
如果最上方的字符等于最左方的字符,则为左上方的数字。否则为左上方的数字+1。(对于 3,3 来说为 0)
左方数字+1(对于 3,3 格来说为 2)
上方数字+1(对于 3,3 格来说为 2)
最终取右下角的值即为编辑距离的值 3。
对于拼写纠错,我们考虑构造一个度量空间(Metric Space),该空间内任何关系满足以下三条基本条件:
d(x,y) = 0 -- 假如 x 与 y 的距离为 0,则 x=y
d(x,y) = d(y,x) -- x 到 y 的距离等同于 y 到 x 的距离
d(x,y) + d(y,z) >= d(x,z) -- 三角不等式
- 根据三角不等式,则满足与 query 距离在 n 范围内的另一个字符转 B,其与 A的距离最大为 d+n,最小为 d-n。
- BK 树的构造就过程如下:每个节点有任意个子节点,每条边有个值表示编辑距离。所有子节点到 父节点的边上标注 n 表示编辑距离恰好为 n。比如,我们有棵树父节点是”book”和两个子节点”cake”和” books”,”book”到”books”的边标号 1,”book”到”cake”的边上标号 4。从字典里构造好树后,无论何时 你想插入新单词时,计算该单词与根节点的编辑距离,并且查找数值为d(neweord, root)的边。递归得 与各子节点进行比较,直到没有子节点,你就可以创建新的子节点并将新单词保存在那。比如,插入” boo”到刚才上述例子的树中,我们先检查根节点,查找 d(“book”, “boo”) = 1 的边,然后检查标号为1 的边的子节点,得到单词”books”。我们再计算距离 d(“books”, “boo”)=2,则将新单词插在”books”之 后,边标号为 2。
- 查询相似词如下:计算单词与根节点的编辑距离 d,然后递归查找每个子节点标号为 d-n 到 d+n(包含)的边。假如被检查的节点与搜索单词的距离 d 小于 n,则返回该节点并继续查询。比如输 入 cape 且最大容忍距离为 1,则先计算和根的编辑距离 d(“book”, “cape”)=4,然后接着找和根节点之 间编辑距离为 3 到5 的,这个就找到了 cake 这个节点,计算 d(“cake”, “cape”)=1,满足条件所以返回 cake,然后再找和 cake 节点编辑距离是 0 到 2 的,分别找到 cape 和cart 节点,这样就得到 cape 这 个满足条件的结果。
Linux
更多详见:
Linux 教程
怎么清屏?怎么退出当前命令?怎么执行睡眠?怎么查看当前用 户 id?查看指定帮助用什么命令?
清屏: clear
退出当前命令: ctrl+c 彻底退出
执行睡眠 : ctrl+z 挂起当前进程 fg 恢复后台
查看当前用户 id: ”id“:查看显示目前登陆账户的 uid 和 gid 及所属分组及用户名
查看指定帮助: 如 man adduser 这个很全 而且有例子; adduser --help 这个告诉你一些常用参数; info adduesr;
Ls 命令执行什么功能? 可以带哪些参数,有什么区别?
ls 执行的功能: 列出指定目录中的目录,以及文件
哪些参数以及区别: a 所有文件 l 详细信息,包括大小字节数,可读可写可执行的权限等
建立软链接(快捷方式),以及硬链接的命令
软链接: ln -s slink source
硬链接: ln link source
软连接和硬链接的区别?
软连接类似 Windows 的快捷方式,当删除源文件时,那么软链接也失效了。硬链接可以理解为源文件的一个别名,多个别名所代表的是同一个文件。当 rm 一个文件的时候,那么此文件的硬链接数减1,当硬链接数为 0 的时候,文件被删除。
查看文件内容有哪些命令可以使用?
vi 文件名 #编辑方式查看,可修改
cat 文件名 #显示全部文件内容
more 文件名 #分页显示文件内容
less 文件名 #与 more 相似,更好的是可以往前翻页
tail 文件名 #仅查看尾部,还可以指定行数
head 文件名 #仅查看头部,还可以指定行数
Linux 下命令有哪几种可使用的通配符?分别代表什么含义?
“?”可替代单个字符。
“*”可替代任意多个字符。
方括号“[charset]”可替代 charset 集中的任何单个字符,如[a-z],[abABC]
用什么命令对一个文件的内容进行统计?(行号、单词数、字节数)
wc 命令 - c 统计字节数 - l 统计行数 - w 统计字数
怎么查看当前进程?怎么执行退出?怎么查看当前路径?
查看当前进程: ps
执行退出: exit
查看当前路径: pwd
Linux 中进程有哪几种状态?在 ps 显示出来的信息中,分别用什么符号表示的?
- 不可中断状态:进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指进程不响应异步 信号。
- 暂停状态/跟踪状态:向进程发送一个 SIGSTOP 信号,它就会因响应该信号而进入 TASK_STOPPED 状态;当进程正在被跟踪时,它处于 TASK_TRACED 这个特殊的状态。正被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。
- 就绪状态:在 run_queue 队列里的状态
- 运行状态:在 run_queue 队列里的状态
- 可中断睡眠状态:处于这个状态的进程因为等待某某事件的发生(比如等待socket 连接、等待信 号量),而被挂起
- zombie 状态(僵尸):父亲没有通过 wait 系列的系统调用会顺便将子进程的尸体 (task_struct)也释放掉
- 退出状态
用符号表示
- D 不可中断 Uninterruptible(usually IO)
- R 正在运行,或在队列中的进程
- S 处于休眠状态
- T 停止或被追踪
- Z 僵尸进程
- W 进入内存交换(从内核 2.6 开始无效)
- X 死掉的进程
利用 ps 怎么显示所有的进程? 怎么利用 ps 查看指定进程的信息?
ps -ef (system v 输出)
ps -aux bsd 格式输出
ps -ef | grep pid复制代码
怎么使一个命令在后台运行?
一般都是使用 & 在命令结尾来让程序自动运行。(命令后可以不追加空格)
哪个命令专门用来查看后台任务?
job -l
把后台任务调到前台执行使用什么命令?把停下的后台任务在后台执行起来用什么命令?
把后台任务调到前台执行 fg
把停下的后台任务在后台执行起来 bg
终止进程用什么命令? 带什么参数?
kill [-s <信息名称或编号>][程序] 或 kill [-l <信息编号>]
kill-9 pid
查看当前谁在使用该主机用什么命令? 查找自己所在的终端信息用什么命令?
查找自己所在的终端信息:who am i
查看当前谁在使用该主机:who
使用什么命令查看用过的命令列表?
history
使用什么命令查看磁盘使用空间? 空闲空间呢?
df -hl复制代码
文件系统 容量 已用 可用 已用% 挂载点
Filesystem Size Used Avail Use% Mounted on /dev/hda2 45G 19G 24G 44% /
/dev/hda1 494M 19M 450M 4% /boot复制代码
日志以什么格式,存放在哪里?
日志以文本可以存储在“/var/log/”目录下后缀名为.log。日志以文本可以存储在“/var/log/”目录下后缀名为.log。
更多Linux目录结构详见:
Linux 系统目录结构
你平时是怎么查看日志的?
Linux查看日志的命令有多种: tail、cat、tac、head、echo等,本文只介绍几种常用的方法。
-
tail
最常用的一种查看方式
命令格式: tail[必要参数][选择参数][文件]
- -f 循环读取
- -q 不显示处理信息
- -v 显示详细的处理信息
- -c<数目> 显示的字节数 -n<行数> 显示行数
- -q, --quiet, --silent 从不输出给出文件名的首部
- -s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒
例如:
tail -n 10 test.log 查询日志尾部最后10行的日志; tail -n +10 test.log 查询10行之后的所有日志; tail -fn 10 test.log 循环实时查看最后1000行记录(最常用的)
一般还会配合着grep搜索用,例如 :
tail -fn 1000 test.log | grep '关键字'
如果一次性查询的数据量太大,可以进行翻页查看,例如:
tail -n 4700 aa.log |more -1000 可以进行多屏显示(ctrl + f 或者 空格键可以快捷键)
-
head
跟tail是相反的head是看前多少行日志
head -n 10 test.log 查询日志文件中的头10行日志; head -n -10 test.log 查询日志文件除了最后10行的其他所有日志;
head其他参数参考tail
-
cat
cat 是由第一行到最后一行连续显示在屏幕上
一次显示整个文件 :
$ cat filename
从键盘创建一个文件 :
$ cat > filename
将几个文件合并为一个文件:
$ cat file1 file2 > file 只能创建新文件,不能编辑已有文件
将一个日志文件的内容追加到另外一个 :
$ cat -n textfile1 > textfile2
清空一个日志文件:
$ cat : >textfile2
注意:> 意思是创建,>>是追加。千万不要弄混了。
cat其他参数参考tail
-
more
more命令是一个基于vi编辑器文本过滤器,它以全屏幕的方式按页显示文本文件的内容,支持vi中的关 键字定位操作。more名单中内置了若干快捷键,常用的有H(获得帮助信息),Enter(向下翻滚一 行),空格(向下滚动一屏),Q(退出命令)。more命令从前向后读取文件,因此在启动时就加载整个文件。
该命令一次显示一屏文本,满屏后停下来,并且在屏幕的底部出现一个提示信息,给出至今己显示的该 文件的百分比:–More–(XX%)
more的语法:more 文件名
- Enter 向下n行,需要定义,默认为1行
- Ctrl f 向下滚动一屏
- 空格键 向下滚动一屏
- Ctrl b 返回上一屏
- = 输出当前行的行号
- :f 输出文件名和当前行的行号
- v 调用vi编辑器
- !命令 调用Shell,并执行命令
- q退出more
-
sed
这个命令可以查找日志文件特定的一段 , 根据时间的一个范围查询,可以按照行号和时间范围查询
-
按照行号
sed -n '5,10p' filename 这样你就可以只查看文件的第5行到第10行。
-
按照时间段
sed -n '/2014-12-17 16:17:20/,/2014-12-17 16:17:36/p' test.log
-
-
less
less命令在查询日志时,一般流程是这样的
less log.log shift + G 命令到文件尾部 然后输入 ?加上你要搜索的关键字例如 ?1213 按 n 向上查找关键字 shift+n 反向查找关键字 less与more类似,使用less可以随意浏览文件,而more仅能向前移动,不能向后移动,而且 less 在查看 之前不会加载整个文件。 less log2013.log 查看文件 ps -ef | less ps查看进程信息并通过less分页显示 history | less 查看命令历史使用记录并通过less分页显示 less log2013.log log2014.log 浏览多个文件
常用命令参数:
less与more类似,使用less可以随意浏览文件,而more仅能向前移动,不能向后移动,而且 less 在查看 之前不会加载整个文件。
less log2013.log 查看文件 ps -ef | less ps查看进程信息并通过less分页显示 history | less 查看命令历史使用记录并通过less分页显示 less log2013.log log2014.log 浏览多个文件
常用命令参数:
- -b <缓冲区大小> 设置缓冲区的大小
- -g 只标志最后搜索的关键词
- -i 忽略搜索时的大小写
- -m 显示类似more命令的百分比
- -N 显示每行的行号
- -o <文件名> 将less 输出的内容在指定文件中保存起来
- -Q 不使用警告音
- -s 显示连续空行为一行
- /字符串:向下搜索"字符串"的功能
- ?字符串:向上搜索"字符串"的功能
- n:重复前一个搜索(与 / 或 ? 有关)
- N:反向重复前一个搜索(与 / 或 ? 有关)
- b 向后翻一页
- h 显示帮助界面
- q 退出less 命令
一般本人查日志配合应用的其他命令
history // 所有的历史记录
history | grep XXX // 历史记录中包含某些指令的记录
history | more // 分页查看记录
history -c // 清空所有的历史记录
!! 重复执行上一个命令
查询出来记录后选中 : !323
Linux 查看某个服务的端口?
netstat -anp | grep service_name
ubuntu 系统如何设置开机自启动一个程序?
直接修改/etc/rc0.d ~ /etc/rc6.d 和/etc/rcS.d 文件夹的内容,添加需启动的程序,S 开头的表示启动,K 开头的表示不启动。
搜索文件用什么命令? 格式是怎么样的?
find <指定目录> <指定条件> <指定动作> whereis 加参数与文件名 locate 只加文件名 find 直接搜索磁盘,较慢。 find / -name "string*"
在 linux 中 find 和 grep 的区别
Linux 系统中 grep 命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行
打印出来。grep 全称是 Global Regular Expression Print,表示全局正则表达式版本,它的使用权限
是所有用户。
linux 下的 find:
功能:在目录结构中搜索文件,并执行指定的操作。此命令提供了相当多的查找条件,功能很强大。
语法:find 起始目录寻找条件操作
说明:find 命令从指定的起始目录开始,递归地搜索其各个子目录,查找满足寻找条件的文件并对
之采取相关的操作。
简单点说,grep 是查找匹配条件的行,find 是搜索匹配条件的文件。
查找命令的可执行文件是去哪查找的? 怎么对其进行设置及添加?
whereis [-bfmsu][-B <目录>...][-M <目录>...][-S <目录>...][文件...]
补充说明:whereis 指令会在特定目录中查找符合条件的文件。这些文件的烈性应属于原始代码,二进制文件,或是帮助文件。
- -b 只查找二进制文件。
- -B <目录> 只在设置的目录下查找二进制文件。
- -f 不显示文件名前的路径名称。
- -m 只查找说明文件。
- -M <目录> 只在设置的目录下查找说明文件。
- -s 只查找原始代码文件。
- -S <目录> 只在设置的目录下查找原始代码文件。
- -u 查找不包含指定类型的文件。
- w -h ich 指令会在 PATH 变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。
- -n 指定文件名长度,指定的长度必须大于或等于所有文件中最长的文件名。
- -p 与-n 参数相同,但此处的包括了文件的路径。
- -w 指定输出时栏位的宽度。
- -V 显示版本信息
通过什么命令查找执行命令?
which 只能查可执行文件
whereis 只能查二进制文件、说明文档,源文件等
怎么对命令进行取别名?
alias la='ls -a'复制代码
du 和 df 的定义,以及区别?
-
du 显示目录或文件的大小
-
df 显示每个<文件>所在的文件系统的信息,默认是显示所有文件系统。(文件系统分配其中的一些磁 盘块用来记录它自身的一些数据,如 i 节点,磁盘分布图,间接块,超级块等。这些数据对大多数用户 级的程序来说是不可见的,通常称为 Meta Data。) du 命令是用户级的程序,它不考虑 Meta Data, 而 df命令则查看文件系统的磁盘分配图并考虑 Meta Data。
df 命令获得真正的文件系统数据,而 du 命令只查看文件系统的部分情况。
awk 详解
awk '{pattern + action}' {
filenames
}
#cat /etc/passwd |awk -F ':' '{print 1"t"7}' //-F 的意思是以':'分隔 root
/bin/bash
daemon /bin/sh 搜索/etc/passwd 有 root 关键字的所有行
#awk -F: '/root/' /etc/passwd root:x:0:0:root:/root:/bin/bash复制代码
当你需要给命令绑定一个宏或者按键的时候,应该怎么做呢?
可以使用 bind 命令,bind 可以很方便地在 shell 中实现宏或按键的绑定。在进行按键绑定的时候,我 们需要先获取到绑定按键对应的字符序列。
比如获取 F12 的字符序列获取方法如下:先按下 Ctrl+V,然后按下 F12 .我们就可以得到 F12 的字符序列 ^[[24~。
接着使用 bind 进行绑定。
[root@localhost ~]# bind ‘”e[24~":"date"'复制代码
注意:相同的按键在不同的终端或终端模拟器下可能会产生不同的字符序列。
【附】也可以使用 showkey -a 命令查看按键对应的字符序列。
Linux 重定向命令有哪些?有什么区别?
-
重定向>
Linux 允许将命令执行结果重定向到一个文件,本应显示在终端上的内容保存到指定文件中。如:ls > test.txt ( test.txt 如果不存在,则创建,存在则覆盖其内容 )。
-
重定向>>
>>这个是将输出内容追加到目标文件中。如果文件不存在,就创建文件;如果文件存在,则将新的
内容追加到那个文件的末尾,该文件中的原有内容不受影响。
常用的 Linux 命令?
pwd 显示工作路径
ls 查看目录中的文件
cd /home 进入 '/ home' 目录'
mkdir dir1 创建一个叫做 'dir1' 的目录'
rm -f file1 删除一个叫做 'file1' 的文件',-f 参数,忽略不存在的文件,从不给出提示。
rmdir dir1 删除一个叫做 'dir1' 的目录'
groupadd group_name 创建一个新用户组
groupdel group_name 删除一个用户组
tar -cvf archive.tar file1 创建一个非压缩的 tarball
tar -tf archive.tar 显示一个包中的内容
tar -xvf archive.tar 释放一个包
netstat 查看网络是否连通
ifconfig 查看 ip 地址及接口信息
env 查看各类环境变量
更多详见:
Linux 命令大全
Linux 的基本命令(怎么区分一个文件还是文件夹)
ls -F 在显示名称的时候会在文件夹后添加“/”,在文件后面加“*”
Linux 关机命令有哪些?
关机的命令有 shutdown –h now halt poweroff 和 init 0 , 重启系统的命令有 shutdown –r now reboot init 6。
更多详见:
Linux 系统启动过程
你的系统目前有许多正在运行的任务,在不重启机器的条件下, 有什么方法可以把所有正在运行的进程移除呢?
使用 linux 命令 ’disown -r ’可以将所有正在运行的进程移除。
bash shell 中的 hash 命令有什么作用?
linux 命令’hash’管理着一个内置的哈希表,记录了已执行过的命令的完整路径,用该命令可以打印出你 所使用过的命令以及执行的次数。
[root@localhost ~]# hash
hits command
2 /bin/ls
2 /bin/su复制代码
哪一个 bash 内置命令能够进行数学运算。
bash shell 的内置命令 let 可以进行整型数的数学运算。
#! /bin/bash
…
…
let c=a+b
…
…复制代码
怎样一页一页地查看一个大文件的内容呢?
通过管道将命令”cat file_name.txt” 和 ’more’ 连接在一起可以实现这个需要
[root@localhost ~]# cat file_name.txt | more复制代码
数据字典属于哪一个用户的?
数据字典是属于’SYS’用户的,用户‘SYS’ 和 ’SYSEM’是由系统默认自动创建的
怎样查看一个 linux 命令的概要与用法?假设你在/bin 目录中 偶然看到一个你从没见过的的命令,怎样才能知道它的作用和用法呢?
使用命令 whatis 可以先出显示出这个命令的用法简要,比如,你可以使用 whatiszcat 去查看‘zcat’的介绍以及使用简要。
[root@localhost ~]# whatis zcat
zcat [gzip] (1) – compress or expand files复制代码
使用哪一个命令可以查看自己文件系统的磁盘空间配额呢?
使用命令 repquota 能够显示出一个文件系统的配额信息
Git
git 常用命令?
- git status 查看当前仓库状态
- git diff 比较版本的区别
- git log 查看 git 操作日志
- git reset 回溯历史版本
- git add 将文件添加到暂存区
- git commit 将文件提交到服务器
- git checkout 切换到指定分支
- git rm 删除指定文件
- git clone 下载一个项目和它的整个代码历史到指定文件夹;若省略path,则会在当前路径下生成一个与远程仓库同名的项目。clone操作是一个从无到有的克隆操作,不需要
git init
初始化。 - git fetch 下载远程仓库的所有变动
- git pull 取回远程仓库的变化,并与本地分支合并。即:
git pull = git fetch + git merge
git 合并文件有冲突,如何处理?
- git merge 冲突了,根据提示找到冲突的文件,解决冲突如果文件有冲突,那么会有类似的标记
- 修改完之后,执行 git add 冲突文件名
- git commit 注意:没有-m 选项 进去类似于 vim 的操作界面,把 conflict 相关的行删除掉直接 push 就可以了,因为刚刚已经执行过相关 merge 操作了。
更多详见:
解决冲突
Docker
总结:docker改变了什么
- 面向产品:产品交付
- 面向开发:简化环境配置
- 面向测试:多版本测试
- 面向运维:环境一致性
- 面向架构:自动化扩容(微服务)
聊聊:docker是怎么工作的?
实际上docker使用了常见的CS架构,也就是client-server模式,docker client负责处理用户输入的各种命令,比如docker build、docker run,通过Socket连接到server,真正工作的其实是server,也就是docker daemon,值得注意的是,docker client和docker daemon可以运行在同一台机器上。
聊聊:docker的组成包含哪几大部分
一个完整的docker有以下几个部分组成:
- docker client,客户端,为用户提供一系列可执行命令,用户用这些命令实现跟 docker daemon 交互;
- docker daemon,守护进程,一般在宿主主机后台运行,等待接收来自客户端的请求消息;
- docker image,镜像,镜像run之后就生成为docker容器;
- docker container,容器,一个系统级别的服务,拥有自己的ip和系统目录结构;运行容器前需要本地存在对应的镜像,如果本地不存在该镜像则就去镜像仓库下载。
docker 使用客户端-服务器 (C/S) 架构模式,使用远程api来管理和创建docker容器。docker 容器通过 docker 镜像来创建。容器与镜像的关系类似于面向对象编程中的对象与类。
聊聊:docker技术的三大核心概念是什么?
- 镜像:镜像是一种轻量级、可执行的独立软件包,它包含运行某个软件所需的所有内容,我们把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量和配置文件等),这个打包好的运行环境就是image镜像文件。
- 容器:容器是基于镜像创建的,是镜像运行起来之后的一个实例,容器才是真正运行业务程序的地方。如果把镜像比作程序里面的类,那么容器就是对象。
- 镜像仓库:存放镜像的地方,研发工程师打包好镜像之后需要把镜像上传到镜像仓库中去,然后就可以运行有仓库权限的人拉取镜像来运行容器了。
聊聊:Docker 安全么?
Docker 利用了 Linux 内核中很多安全特性来保证不同容器之间的隔离,并且通过签名机制来对镜像进行验证。大量生产环境的部署证明,Docker 虽然隔离性无法与虚拟机相比,但仍然具有极高的安全性。
聊聊:Docker 与 虚拟机 有何不同?
Docker 不是虚拟化方法。它依赖于实际实现基于容器的虚拟化或操作系统级虚拟化的其他工具。为此,Docker 最初使用 LXC 驱动程序,然后移动到libcontainer 现在重命名为 runc。Docker 主要专注于在应用程序容器内自动部署应用程序。应用程序容器旨在打包和运行单个服务,而系统容器则设计为运行多个进程,如虚拟机。因此,Docker 被视为容器化系统上的容器管理或应用程序部署工具。
- 容器不需要引导操作系统内核,因此可以在不到一秒的时间内创建容器。此功能使基于容器的虚拟化比其他虚拟化方法更加独特和可取。
- 由于基于容器的虚拟化为主机增加了很少或没有开销,因此基于容器的虚拟化具有接近本机的性能。
- 对于基于容器的虚拟化,与其他虚拟化不同,不需要其他软件。
- 主机上的所有容器共享主机的调度程序,从而节省了额外资源的需求。
- 与虚拟机映像相比,容器状态(Docker 或 LXC 映像)的大小很小,因此容器映像很容易分发。
- 容器中的资源管理是通过 cgroup 实现的。Cgroups 不允许容器消耗比分配给它们更多的资源。虽然主机的所有资源都在虚拟机中可见,但无法使用。这可以通过在容器和主机上同时运行 top 或 htop 来实现。所有环境的输出看起来都很相似。
特性 | Docker | 虚拟机 |
---|---|---|
启动速度 | 秒级 | 分钟级 |
交付/部署 | 开发、测试、生产环境一致 | 无成熟体系 |
性能 | 近似物理机 | 性能损耗大 |
体量 | 极小(MB) | 较大(GB) |
迁移/扩展 | 跨平台,可复制 | 较为复杂 |
聊聊:docker与传统虚拟机的区别什么?
- 传统虚拟机是需要安装整个操作系统的,然后再在上面安装业务应用,启动应用,通常需要几分钟去启动应用,而docker是直接使用镜像来运行业务容器的,其容器启动属于秒级别;
- Docker需要的资源更少,Docker在操作系统级别进行虚拟化,Docker容器和内核交互,几乎没有性能损耗,而虚拟机运行着整个操作系统,占用物理机的资源就比较多;
- Docker更轻量,Docker的架构可以共用一个内核与共享应用程序库,所占内存极小;同样的硬件环境,Docker运行的镜像数远多于虚拟机数量,对系统的利用率非常高;
- 与虚拟机相比,Docker隔离性更弱,Docker属于进程之间的隔离,虚拟机可实现系统级别隔离;
- Docker的安全性也更弱,Docker的租户root和宿主机root相同,一旦容器内的用户从普通用户权限提升为root权限,它就直接具备了宿主机的root权限,进而可进行无限制的操作。虚拟机租户root权限和宿主机的root虚拟机权限是分离的,并且虚拟机利用如Intel的VT-d和VT-x的ring-1硬件隔离技术,这种技术可以防止虚拟机突破和彼此交互,而容器至今还没有任何形式的硬件隔离;
- Docker的集中化管理工具还不算成熟,各种虚拟化技术都有成熟的管理工具,比如:VMware vCenter提供完备的虚拟机管理能力;
- Docker对业务的高可用支持是通过快速重新部署实现的,虚拟化具备负载均衡,高可用、容错、迁移和数据保护等经过生产实践检验的成熟保障机制,Vmware可承诺虚拟机99.999%高可用,保证业务连续性;
- 虚拟化创建是分钟级别的,Docker容器创建是秒级别的,Docker的快速迭代性,决定了无论是开发、测试、部署都可以节省大量时间;
- 虚拟机可以通过镜像实现环境交付的一致性,但镜像分发无法体系化,Docker在Dockerfile中记录了容器构建过程,可在集群中实现快速分发和快速部署。
聊聊:Docker与LXC ( Linux Container)有何不同?
LXC利用Linux上相关技术实现了容器支持; Docker早期版本中使用了LXC技术,后期演化为新的 libcontainer, 在如下的几个方面进行了改进:
- 移植性: 通过抽象容器配置, 容器可以实现从一个平台移植到另一个平台;
- 镜像系统: 基于AUFS的镜像系统为容器的分发带来了很多的便利, 同时共同的镜像层只需要存储一份,实现高效率的存储;
- 版本管理: 类似于Git的版本管理理念, 用户可以更方便地创建、 管理镜像文件;
- 仓库系统: 仓库系统大大降低了镜像的分发和管理的成本;
- 周边工具: 各种现有工具(配置管理、 云平台)对Docker的支持, 以及基于Docker 的PaaS、CI等系统, 让Docker的应用更加方便和多样化。
聊聊:Docker 容器有几种状态?
四种状态:运行、已暂停、重新启动、已退出。
Docker的配置文件放在那里。如何修改配置?
Ubuntu系统下Docker的配置文件是/etc/default/docker,CentOS系统配置文件存放在/etc/sysconfig/docker
如何更改Docker的默认存储设置?
Docker的默认存放位置是/var/lib/docker,如果希望将Docker的本地文件存储到其他分区,可以使用Linux软连接的方式来做。
聊聊:docker常用命令?
- docker pull 拉取或者更新指定镜像
- docker push 将镜像推送至远程仓库
- docker rm 删除容器
- docker rmi 删除镜像
- docker images 列出所有镜像
- docker ps 列出所有容器
启动nginx容器(随机端口映射),并挂载本地文件目录到容器html的命令是?
docker run -d -P --name nginx2 -v /home/nginx:/usr/share/nginx/html nginx
进入容器的方法有哪些?
- 使用 docker attach 命令
- 使用 exec 命令,例如:docker exec -i -t 784fd3b294d7 /bin/bash
attach是直接进入容器启动命令的终端,不会启动新的进程;exec则是在容器里面打开新的终端,会启动新的进程;一般建议已经exec进入容器。
容器与主机之间的数据拷贝命令是?
docker cp 命令用于容器与主机之间的数据拷贝
- 主机到容器:docker cp /www 96f7f14e99ab:/www/
- 容器到主机:docker cp 96f7f14e99ab:/www /tmp/
本地的镜像文件都存放在哪里?
与 Docker 相关的本地资源都存放在/var/lib/docker/目录下,其中 container 目录存放容器信息,graph 目录存放镜像信息,aufs 目录下存放具体的内容文件。
退出容器时候自动删除?
使用 –rm 选项,例如 sudo docker run –rm -it ubuntu
如何批量清理临时镜像文件?
可以使用 sudo docker rmi $(sudo docker images -q -f danging=true)命令
如何查看镜像支持的环境变量?
使用 sudo docker run IMAGE env
容器退出后,通过 docker ps 命令查看不到,数据会丢失么?
容器退出后会处于终止(exited)状态,此时可以通过 docker ps -a 查看,其中数据不会丢失,还可以通过 docker start 来启动,只有删除容器才会清除数据。
如何停止所有正在运行的容器?
使用 docker kill $(sudo docker ps -q)
如何清理批量后台停止的容器?
使用 docker rm $(sudo docker ps -a -q)
很多应用容器都是默认后台运行的,怎么查看它们的输出和日志信息?
使用docker logs,后面跟容器的名称或者ID信息
如何临时退出一个正在交互的容器的终端,而不终止它?
按 Ctrl+p,后按 Ctrl+q,如果按 Ctrl+c 会使容器内的应用进程终止,进而会使容器终止。
聊聊:什么是 Docker Swarm?
Docker Swarm 是 Docker 的本机群集。它将 Docker 主机池转变为单个虚拟Docker 主机。Docker Swarm 提供标准的 Docker API,任何已经与 Docker守护进程通信的工具都可以使用 Swarm 透明地扩展到多个主机。
聊聊:容器内部机制?
每个容器都在自己的命名空间中运行,但使用与所有其他容器完全相同的内核。发生隔离是因为内核知道分配给进程的命名空间,并且在API调用期间确保进程只能访问其自己的命名空间中的资源。
聊聊:什么是Docker Hub?
Docker hub是一个基于云的注册表服务,允许您链接到代码存储库,构建镜像并测试它们,存储手动推送的镜像以及指向Docker云的链接,以便您可以将镜像部署到主机。它为整个开发流程中的容器镜像发现,分发和变更管理,用户和团队协作以及工作流自动化提供了集中资源。
聊聊:镜像与 UnionFS
Linux 的命名空间和控制组分别解决了不同资源隔离的问题,前者解决了进程、网络以及文件系统的隔离,后者实现了 CPU、内存等资源的隔离,但是在 Docker 中还有另一个非常重要的问题需要解决 - 也就是镜像。
Docker 镜像其实本质就是一个压缩包,我们可以使用命令将一个 Docker 镜像中的文件导出,你可以看到这个镜像中的目录结构与 Linux 操作系统的根目录中的内容并没有太多的区别,可以说 Docker 镜像就是一个文件。
聊聊:存储驱动
Docker 使用了一系列不同的存储驱动管理镜像内的文件系统并运行容器,这些存储驱动与Docker 卷(volume)有些不同,存储引擎管理着能够在多个容器之间共享的存储。
聊聊:docker容器之间怎么隔离?
Linux中的PID、IPC、网络等资源是全局的,而NameSpace机制是一种资源隔离方案,在该机制下这些资源就不再是全局的了,而是属于某个特定的NameSpace,各个NameSpace下的资源互不干扰。
虽然有了NameSpace技术可以实现资源隔离,但进程还是可以不受控的访问系统资源,比如CPU、内存、磁盘、网络等,为了控制容器中进程对资源的访问,Docker采用control groups技术(也就是cgroup),有了cgroup就可以控制容器中进程对系统资源的消耗了,比如你可以限制某个容器使用内存的上限、可以在哪些CPU上运行等等。
有了这两项技术,容器看起来就真的像是独立的操作系统了。
聊聊:仓库( Repository)、 注册服务器( Registry)、 注册索引( Index)有何关系?
- 仓库是存放一组关联镜像的集合,比如同一个应用的不同版本的镜像。
- 注册服务器是存放实际的镜像文件的地方。
- 注册索引则负责维护用户的账号、权限、搜索、标签等的管理。因此,注册服务器利用注册索引来实现认证等管理。
聊聊:如何在生产中监控 Docker?
Docker 提供 docker stats 和 docker 事件等工具来监控生产中的 Docker。
我们可以使用这些命令获取重要统计数据的报告。
Docker 统计数据:当我们使用容器 ID 调用 docker stats 时,我们获得容器的CPU,内存使用情况等。它类似于 Linux 中的 top 命令。
Docker 事件:Docker 事件是一个命令,用于查看 Docker 守护程序中正在进行的活动流。 一些常见的 Docker 事件:attach,commit,die,detach,rename,destroy 等。我们还可以使用各种选项来限制或过滤我们感兴趣的事件。
聊聊:如何在多个环境中使用Docker?
可以进行以下更改:
- 删除应用程序代码的任何卷绑定,以便代码保留在容器内,不能从外部更改
- 绑定到主机上的不同端口
- 以不同方式设置环境变量(例如,减少日志记录的详细程度,或启用电子邮件发送)
- 指定重启策略(例如,重启:始终)以避免停机
- 添加额外服务(例如,日志聚合器)
因此,您可能希望定义一个额外的Compose文件,例如production.yml,它指定适合生产的配置。此配置文件只需要包含您要从原始Compose文件中进行的更改。
聊聊:Docker能在非Linux平台(比如 macOS或 Windows)上运行么?
可以。macOS目前需要使用 docker for mac等软件创建一个轻量级的Linux虚拟机层。 由于成熟度不高,暂时不推荐在Windows环境中使用Docker。
说说: centos镜像几个G,但是docker centos镜像才几百兆,这是为什么?
一个完整的Linux操作系统包含Linux内核和rootfs根文件系统,即我们熟悉的/dev、/proc/、/bin等目录。我们平时看到的centOS除了rootfs,还会选装很多软件,服务,图形桌面等,所以centOS镜像有好几个G也不足为奇。而对于容器镜像而言,所有容器都是共享宿主机的Linux 内核的,并且docker镜像只需要提供一个很小的rootfs根文件系统即可,只需要包含即我们熟悉的/dev、/proc/、/bin等目录,这是最基本的命令,工具,程序库即可,所以,docker镜像才会这么小。
构建Docker镜像应该遵循哪些原则?
整体原则上,尽量保持镜像功能的明确和内容的精简,要点包括:
- 尽量选取满足需求但较小的基础系统镜像,建议选择debian:wheezy镜像,仅有86MB大小
- 清理编译生成文件、安装包的缓存等临时文件
- 安装各个软件时候要指定准确的版本号,并避免引入不需要的依赖
- 从安全的角度考虑,应用尽量使用系统的库和依赖
- 使用Dockerfile创建镜像时候要添加.dockerignore文件或使用干净的工作目录
说说: 容器的copy-on-write特性,修改容器里面的内容会修改镜像吗?
我们知道,镜像是分层的,镜像的每一层都可以被共享,同时,镜像是只读的。
当一个容器启动时,一个新的可写层被加载到镜像的顶部,这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
实际上,docker hub中99%的镜像都是通过在base镜像中安装和配置需要的软件构建出来的
新的镜像是从base镜像一层一层叠加生成的,每安装一个软件,就在现有的基础增加一层
为什么docker镜像要采用这种分层结构呢?
最大的一个好处是:共享资源
比如:有多个镜像都从相同的base镜像构建而来,那么docker host只需在磁盘上保存一份base镜像: 同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而其镜像的每一层都可以被共享
问题是: 如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如/etc下的文件,这时其他容器的/etc是否也会被修改? 答案是不会。修改会被限制在单个容器内, 这就是容器copy-on-write特性。所有对容器的改动无论添加、删除、还是修改文件,都只会发生在容器层中,因为只有容器层是可写的,容器层下面的所有镜像层都是只读的。镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。
在容器层中,用户看到的是一个叠加之后的文件系统。
- 添加文件时:在容器中创建文件时,新文件被添加到容器层中。
- 读取文件:在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存。
- 修改文件:在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
- 删除文件:在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。
说说: Dockerfile的基本指令有哪些?
- FROM 指定基础镜像(必须为第一个指令,因为需要指定使用哪个基础镜像来构建镜像);
- MAINTAINER 设置镜像作者相关信息,如作者名字,日期,邮件,联系方式等;
- COPY 复制文件到镜像;
- ADD 复制文件到镜像(ADD与COPY的区别在于,ADD会自动解压tar、zip、tgz、xz等归档文件,而COPY不会,同时ADD指令还可以接一个url下载文件地址,一般建议使用COPY复制文件即可,文件在宿主机上是什么样子复制到镜像里面就是什么样子这样比较好);
- ENV 设置环境变量;
- EXPOSE 暴露容器进程的端口,仅仅是提示别人容器使用的哪个端口,没有过多作用;
- VOLUME 数据卷持久化,挂载一个目录;
- WORKDIR 设置工作目录,如果目录不在,则会自动创建目录;
- RUN 在容器中运行命令,RUN指令会创建新的镜像层,RUN指令经常被用于安装软件包;
- CMD 指定容器启动时默认运行哪些命令,如果有多个CMD,则只有最后一个生效,另外,CMD指令可以被docker run之后的参数替换;
- ENTRYOINT 指定容器启动时运行哪些命令,如果有多个ENTRYOINT,则只有最后一个生效,另外,如果Dockerfile中同时存在CMD和ENTRYOINT,那么CMD或docker run之后的参数将被当做参数传递给ENTRYOINT;
说说: Dockerfile的整个构建镜像过程
- 首先,创建一个目录用于存放应用程序以及构建过程中使用到的各个文件等;
- 然后,在这个目录下创建一个Dockerfile文件,一般建议Dockerfile的文件名就是Dockerfile;
- 编写Dockerfile文件,编写指令,如,使用FORM指令指定基础镜像,COPY指令复制文件,RUN指令指定要运行的命令,ENV设置环境变量,EXPOSE指定容器要暴露的端口,WORKDIR设置当前工作目录,CMD容器启动时运行命令,等等指令构建镜像;
- Dockerfile编写完成就可以构建镜像了,使用docker build -t 镜像名:tag . 命令来构建镜像,最后一个点是表示当前目录,docker会默认寻找当前目录下的Dockerfile文件来构建镜像,如果不使用默认,可以使用-f参数来指定dockerfile文件,如:docker build -t 镜像名:tag -f /xx/xxx/Dockerfile ;
- 使用docker build命令构建之后,docker就会将当前目录下所有的文件发送给docker daemon,顺序执行Dockerfile文件里的指令,在这过程中会生成临时容器,在临时容器里面安装RUN指定的命令,安装成功后,docker底层会使用类似于docker commit命令来将容器保存为镜像,然后删除临时容器,以此类推,一层层的构建镜像,运行临时容器安装软件,直到最后的镜像构建成功。
说说: Dockerfile构建镜像出现异常,如何排查?
首先,Dockerfile是一层一层的构建镜像,期间会产生一个或多个临时容器,构建过程中其实就是在临时容器里面安装应用,如果因为临时容器安装应用出现异常导致镜像构建失败,这时容器虽然被清理掉了,但是期间构建的中间镜像还在,那么我们可以根据异常时上一层已经构建好的临时镜像,将临时镜像运行为容器,然后在容器里面运行安装命令来定位具体的异常。
当启动容器的时候提示:exec format error?如何解决问题
检查启动命令是否有可执行权限,进入容器手工运行脚本进行排查。
使用docker port命令映射容器的端口时,系统报错Error:No public port‘80’published for…,是什么意思?
创建镜像时Dockerfile要指定正确的EXPOSE的端口,容器启动时指定PublishAllport=true
参考博客:[Docker面试题(史上最全 + 持续更新)](
其他
字符集和字符编码
字符是各种文字和符号的总称,包括各个国家文字、标点符号、图形符号、数字等。
字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集有:ASCII 字符集、ISO 8859 字符集、GB2312 字符集、BIG5 字符集、GB18030 字符集、Unicode字符集等。
字符编码就是以二进制的数字来对应字符集的字符。
常见的编码字符集(简称字符集)如下所示:
- Unicode:也叫统一字符集,它包含了几乎世界上所有的已经发现且需要使用的字符(如中文、日文、英文、德文等)。
- ASCII:ASCII 既是编码字符集,又是字符编码。早期的计算机系统只能处理英文,所以 ASCII 也就成为了计算机的缺省字符集,包含了英文所需要的所有字符。
- GB2312:中文字符集,包含 ASCII 字符集。ASCII 部分用单字节表示,剩余部分用双字节表示。
- GBK:GB2312 的扩展,但完整包含了 GB2312 的所有内容。
- GB18030:GBK 字符集的超集,常叫大汉字字符集,也叫 CJK(Chinese,Japanese,Korea)字符集,包含了中、日、韩三国语。
注意:Unicode 字符集有多种编码方式,如 UTF-8、UTF-16 等;ASCII 只有一种;大多数 MBCS(包括 GB2312)也只有一种。
Celery 分布式任务队列?
情景:用户发起 request,并等待 response 返回。在本些 views 中,可能需要执行一段耗时的程序,那么用户就会等待很长时间,造成不好的用户体验,比如发送邮件、手机验证码等。
使用 celery 后,情况就不一样了。解决:将耗时的程序放到 celery 中执行。
将多个耗时的任务添加到队列 queue 中,也就是用 redis 实现 broker 中间人,然后用多个 worker 去监听队列里的任务去执行。
- 任务 task:就是一个 Python 函数。
- 队列 queue:将需要执行的任务加入到队列中。
- 工人 worker:在一个新进程中,负责执行队列中的任务。
- 代理人 broker:负责调度,在布置环境中使用 redis。
Jieba 分词
Jieba 分词支持三种分词模式:
- 精确模式:试图将句子最精确地切开,适合文本分析;
- 全模式:把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;
- 搜索引擎模式:在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词
功能:分词,添加自定义词典,关键词提取,词性标注,并行分词
Tokenize:返回词语在原文的起始位置,ChineseAnalyzer for Whoosh 搜索引擎。
hadoop 和 spark 的开发模式
Hadoop:MapRedcue 由 Map 和 Reduce 两个阶段,并通过 shuffle 将两个阶段连接起来的。但是套用 MapReduce 模型解决问题,不得不将问题分解为若干个有依赖关系的子问题,每个子问题对应一个 MapReduce 作业,最终所有这些作业形成一个 DAG。
Spark:是通用的 DAG 框架,可以将多个有依赖关系的作业转换为一个大的 DAG。核心思想是将 Map 和 Reduce 两个操作进一步拆分为多个元操作,这些元操作可以灵活组合,产生新的操作,并经过一些控制程序组装后形成一个大的 DAG 作业。
-
速度快:Apache Spark 拥有先进的 DAG 调度器、查询优化器以及物理执行引擎从而高性能的实现批处理和流数据处理。
-
易用性(可以使用 Java,Scala,Python,R 以及 SQL 快速的写 Spark 应用)
Spark 提供 80 个以上高级算子便于执行并行应用,并且可以使用 Scala、Python、R以及 SQL 的 shell 端交互式运行 Spark 应用。
-
通用性(支持 SQL,流数据处理以及复杂分析)
Spark 拥有一系列库,包括 SQL 和 DataFrame,用于机器学习的 MLib,支持图计算、GraphX 以及流计算模块 Streaming。你可以在一个应用中同时组合这些库。
-
支持多种模式运行(平台包括 Hadoop,Apache Mesos,Kubernete,standalone 或者云上,也可以获取各种数据源上的数据)
Spark 可以直接运行以自身的 standalone 集群模式运行,也可以在亚马逊 EC2 上运行,不过企业级用的比较多的是 Hadoop Yarn 模式,当然也有 Mesos 和 Kubernetes 模式。可以获取不限于来自于 HDFS、Apache Cassandra、Apache HBase 和 Apache Hive 等上百种数据源。
zookeeper 和 kafka 分布式框架的使用以及作用
Zookeeper 框架概念:
ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的Chubby 一个开源的实现,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
Zookeeper 应用方向:
- zookeeper 是一个精简的文件系统。这点它和 hadoop 有点像,但是 zookeeper这个文件系统是管理小文件的,而 hadoop 是管理超大文件的。
- zookeeper 提供了丰富的“构件”,这些构件可以实现很多协调数据结构和协议的操作。例如:分布式队列、分布式锁以及一组同级节点的“领导者选举”算法。
- zookeeper 是高可用的,它本身的稳定性是相当之好,分布式集群完全可以依赖zookeeper 集群的管理,利用 zookeeper 避免分布式系统的单点故障的问题。
- zookeeper 采用了松耦合的交互模式。这点在 zookeeper 提供分布式锁上表现最为明显,zookeeper 可以被用作一个约会机制,让参入的进程不在了解其他进程(或网络)的情况下能够彼此发现并进行交互,参入的各方甚至不必同时存在,只要在 zookeeper留下一条消息,在该进程结束后,另外一个进程还可以读取这条信息,从而解耦了各个节点之间的关系。
- zookeeper 为集群提供了一个共享存储库,集群可以从这里集中读写共享的信息,避免了每个节点的共享操作编程,减轻了分布式系统的开发难度。
- zookeeper 的设计采用的是观察者的设计模式,zookeeper 主要是负责存储和管理大家关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应,从而实现集群中类似 Master/Slave 管理模式。
Zookeeper 的作用:
- Zookeeper 用来注册服务和进行负载均衡,由于哪个服务由哪个机器来提供需要让调用者知道,即 ip 地址和服务名称的对应关系必须一致所以通过硬编码的方式把这种对应关系在调用方业务代码中实现。但是如果提供服务的机器挂掉而调用者无法知晓,若不更改代码则会继续请求挂掉的机器提供服务。
- Zookeeper 通过心跳机制可以检测挂掉的机器并将挂掉机器的 ip 和服务对应关系从列表中删除。至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添加机器来提高运算能力。通过添加新的机器向 Zookeeper 注册服务,服务的提供者多了能服务的客户就多了
Kafka 框架概念:
Kafka 是由 Apache 软件基金会开发的一个开源流处理平台,由 Scala 和 Java 编写。Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 对于像 Hadoop 一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka 的目的是通过 Hadoop 的并行加载机制来统一线上和离线的消息处理,也是为了通过集群来提供实时的消息。
Kafka 的应用方向:
-
Messaging
对于一些常规的消息系统,kafka 是个不错的选择;partitons/replication 和容错,可以使kafka 具有良好的扩展性和性能优势.不过到目前为止,我们应该很清楚认识到,kafka 并没有提供 JMS 中的"事务性""消息传输担保(消息确认机制)""消息分组"等企业级特性;kafka 只能使用作为"常规"的消息系统,在一定程度上,尚未确保消息的发送与接收绝对可靠(比如,消息重发,消息发送丢失等
-
Websit activity tracking
kafka 可以作为"网站活性跟踪"的最佳工具;可以将网页/用户操作等信息发送到 kafka中并实时监控,或者离线统计分析等
-
Log Aggregation
kafka的特性决定它非常适合作为"日志收集中心";application可以将操作日志批量异步的发送到 kafka 集群中,而不是保存在本地或者 DB 中;kafka 可以批量提交消息/压缩消息等,这对 producer 端而言,几乎感觉不到性能的开支.此时 consumer 端可以使 hadoop等其他系统化的存储和分析系统.
Kafka 的作用:
- 生产者的负载与消费者的负载解耦
- 消费者按照自己的能力 fetch 数据
- 消费者可以自定义消费的数量
Openstack 和 K8S 技术的使用
OpenStack:
OpenStack 覆盖了网络、虚拟化、操作系统、服务器等各个方面。它是一个正在开发中的云计算平台项目,根据成熟及重要程度的不同,被分解成核心项目、孵化项目,以及支持项目和相关项目。每个项目都有自己的委员会和项目技术主管,而且每个项目都不是一成不变的,孵化项目可以根据发展的成熟度和重要性,转变为核心项目。截止到 Icehouse 版本,下面列出了 10 个核心项目(即 OpenStack 服务)。
- 计算(Compute):Nova。一套控制器,用于为单个用户或使用群组管理虚拟机实例的整个生命周期,根据用户需求来提供虚拟服务。负责虚拟机创建、开机、关机、挂起、暂停、调整、迁移、重启、销毁等操作,配置 CPU、内存等信息规格。自 Austin 版本集成到项目中。
- 对象存储(Object Storage):Swift。一套用于在大规模可扩展系统中通过内置冗余及高容错机制实现对象存储的系统,允许进行存储或者检索文件。可为 Glance 提供镜像存储,为 Cinder 提供卷备份服务。自 Austin 版本集成到项目中
- 镜像服务(Image Service):Glance。一套虚拟机镜像查找及检索系统,支持多种虚拟机镜像格式(AKI、AMI、ARI、ISO、QCOW2、Raw、VDI、VHD、VMDK),有创建上传镜像、删除镜像、编辑镜像基本信息的功能。自 Bexar 版本集成到项目中。
- 身份服务(Identity Service):Keystone。为 OpenStack 其他服务提供身份验证、服务规则和服务令牌的功能,管理 Domains、Projects、Users、Groups、Roles。自 Essex版本集成到项目中。
- 网络&地址管理(Network):Neutron。提供云计算的网络虚拟化技术,为 OpenStack其他服务提供网络连接服务。为用户提供接口,可以定义 Network、Subnet、Router,配置 DHCP、DNS、负载均衡、L3 服务,网络支持 GRE、VLAN。插件架构支持许多主流的网络厂家和技术,如 OpenvSwitch。自 Folsom 版本集成到项目中。
- 块存储 (Block Storage):Cinder。为运行实例提供稳定的数据块存储服务,它的插件驱动架构有利于块设备的创建和管理,如创建卷、删除卷,在实例上挂载和卸载卷。自Folsom 版本集成到项目中。
- UI 界面 (Dashboard):Horizon。OpenStack 中各种服务的 Web 管理门户,用于简化用户对服务的操作,例如:启动实例、分配 IP 地址、配置访问控制等。自 Essex 版本集成到项目中。
- 测量 (Metering):Ceilometer。像一个漏斗一样,能把 OpenStack 内部发生的几乎所有的事件都收集起来,然后为计费和监控以及其它服务提供数据支撑。自 Havana 版本集成到项目中。
- 部署编排 (Orchestration):Heat2 。提供了一种通过模板定义的协同部署方式,实现云基础设施软件运行环境(计算、存储和网络资源)的自动化部署。自 Havana 版本集成到项目中。
- 数据库服务(Database Service):Trove。为用户在 OpenStack 的环境提供可扩展和可靠的关系和非关系数据库引擎服务。自 Icehouse 版本集成到项目中。
Kubernetes:
是一个全新的基于容器技术的分布式架构领先方案。Kubernetes(k8s)是 Google 开源的容器集群管理系统(谷歌内部:Borg)。在 Docker 技术的基础上,为容器化的应用提供部署运行、资源调度、服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理的便捷性。
Kubernetes 是一个完备的分布式系统支撑平台,具有完备的集群管理能力,多扩多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和发现机制、內建智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制以及多粒度的资源配额管理能力。同时 Kubernetes 提供完善的管理工具,涵盖了包括开发、部署测试、运维监控在内的各个环节。
Kubernetes 中,Service 是分布式集群架构的核心,一个 Service 对象拥有如下关键特征:
- 拥有一个唯一指定的名字
- 拥有一个虚拟 IP(Cluster IP、Service IP、或 VIP)和端口号
- 能够体统某种远程服务能力
- 被映射到了提供这种服务能力的一组容器应用上
- Service 的服务进程目前都是基于 Socket 通信方式对外提供服务,比如 Redis、Memcache、MySQL、Web Server,或者是实现了某个具体业务的一个特定的 TCP Server进程,虽然一个 Service 通常由多个相关的服务进程来提供服务,每个服务进程都有一个独立的 Endpoint(IP+Port)访问点,但 Kubernetes 能够让我们通过服务连接到指定的Service 上。有了 Kubernetes 内奸的透明负载均衡和故障恢复机制,不管后端有多少服务进程,也不管某个服务进程是否会由于发生故障而重新部署到其他机器,都不会影响我们对服务的正常调用,更重要的是这个 Service 本身一旦创建就不会发生变化,意味着在Kubernetes 集群中,我们不用为了服务的 IP 地址的变化问题而头疼了。
- 容器提供了强大的隔离功能,所有有必要把为 Service 提供服务的这组进程放入容器中进行隔离。为此,Kubernetes 设计了 Pod 对象,将每个服务进程包装到相对应的 Pod 中,使其成为 Pod 中运行的一个容器。为了建立 Service 与 Pod 间的关联管理,Kubernetes给每个 Pod 贴上一个标签 Label,比如运行 MySQL 的 Pod 贴上 name=MySQL 标签,给运行 PHP 的 Pod 贴上 name=php 标签,然后给相应的 Service 定义标签选择器 Label Selector,这样就能巧妙的解决了 Service 于 Pod 的关联问题。
- 在集群管理方面,Kubernetes 将集群中的机器划分为一个 Master 节点和一群工作节点 Node,其中,在 Master 节点运行着集群管理相关的一组进程 kube-apiserver、kube-controller-manager 和 kube-scheduler,这些进程实现了整个集群的资源管理、Pod调度、弹性伸缩、安全控制、系统监控和纠错等管理能力,并且都是全自动完成的。Node作为集群中的工作节点,运行真正的应用程序,在 Node 上 Kubernetes 管理的最小运行单元是 Pod。Node 上运行着 Kubernetes 的 kubelet、kube-proxy 服务进程,这些服务进程负责 Pod 的创建、启动、监控、重启、销毁以及实现软件模式的负载均衡器。
OpenStack 与 K8S 结合主要有两种方案。一是 K8S 部署在 OpenStack 平台之上,二是 K8S 和 OpenStack 组件集成。
-
首先第一种方案目前也是大多数用户选择的方案,这种方式的优点是 K8S 能够快速部署、弹性扩容,并且通过虚拟机的多租户间接实现了容器的多租户,隔离性好。
缺点是容器跑在虚拟机上,多多少少计算性能可能会有点损耗,网络的多层 overlay 嵌套也可能导致性能下降。
OpenStack Magnum 项目是该方案实现的代表,该项目为 OpenStack 提供容器编排服务,通过该组件,用户可以快速部署一个 K8S、Mesos 以及 Swarm 集群,原理和OpenStack 大多数的高级服务实现差不多,先通过 heat 完成资源编排(创建虚拟机、volume、安全组等),然后通过镜像里面的 heat-container-agent 以及一些脚本完成 K8S、Mesos 以及 Swarm 集群的安装配置。当然,通过 Ironic,Magnum 支持将容器编排组件直接部署在物理机(裸机)上。
-
第二种方案是 K8S 与 OpenStack 的各个组件集成,在 OpenStack 社区以及 K8S 社区的共同努力下,目前可以集成的组件还是挺多的,下面简单介绍下。
-
K8S 与 OpenStack Keystone 集成
K8S 可以和 OpenStack Keystone 集成,即 K8S 可以使用 Keystone 认证,参考keystone authentication kubernetes-cluster。
-
K8S 与 OpenStack Glance 集成
这个没有必要,因为 Docker 的镜像是分层的,使用 Registry 或者 Harbor 即可。当然如果有必要可以使用 Glance 存储 Docker 镜像作为备份,不过更建议备份到 OpenStack Swift,Registry 以及 Harbor 都原生支持使用 Swift 作为存储后端。
-
K8S 与 OpenStack Neutron 集成
前面提到的通过 Magnum 把容器部署在虚拟机,其实并没有根本改变 K8S 的网络模型,K8S 的底层网络依然还是诸如 Flannel、Contrail 等网络模型,和 Neutron 其实没有多大关系。另外,前面也说了,容器运行在虚拟机中不仅可能会导致计算性能损耗,网络的多层 Overlay 嵌套也可能会大大降低容器的网络性能。
其实社区已经实现 K8S 直接 OpenStack Neutron 网络集成,即 kuryr-kubernetes项目。K8S 的 pod 与 OpenStack 虚拟机是平等公民,共享 Neutron 网络服务,K8S 网络具备和 OpenStack 虚拟机等同的功能,比如安全组、防火墙、QoS 等。
不过遗憾的是,目前 kuryr 还不支持多租户,Kuryr 使用 Neutron 的 network 以及subnet 都是配置写死的,而不是创建 port 时指定。
-
K8S 与 Cinder 集成
目前 K8S 已经实现了很多 volume 插件,PV 支持对接各种存储系统,比如 Ceph RBD、GlusterFS、NFS 等等,参考 kubernetes persistent volumes,其中就包含了 Cinder,即K8S 可以使用 Cinder 提供 volume 服务,这样 K8S 和 Nova 共享一套存储系统,都是 Cinder的消费者。Cinder 屏蔽了底层存储系统,K8S 直接对接 Cinder,省去了一堆 plugins 的安装配置。
-
K8S 与 Manila 集成
前面提到 K8S 与 Cinder 集成,其实 K8S 还支持与 OpenStack Manila 服务集成,目前该插件已经包含在 K8S 的 external storage 项目中。
-
rabbitmq 是什么,在项目中具体的使用场景
RabbitMQ,用在实时的对可靠性要求比较高的消息传递上。学过 websocket 的来理解rabbitMQ 应该是非常简单的了,websocket 是基于服务器和页面之间的通信协议,一次握手,多次通信。 而 rabbitMQ 就像是服务器之间的 socket,一个服务器连上 MQ 监听,而另一个服务器只要通过 MQ 发送消息就能被监听服务器所接收。但是 MQ 和 socket 还是有区别的,socket 相当于是页面直接监听服务器。而 MQ 就是服务器之间的中转站,例如邮箱,一个人投递信件给邮箱,另一个人去邮箱取,他们中间没有直接的关系,所以耦合度相比 socket 小了很多。
服务器之间通信的相对于其他通信在中间做了一个中间仓库。好处 1:降低了两台服务器之间的耦合,哪怕是一台服务器挂了,另外一台服务器也不会报错或者休克,反正他监听的是 MQ,只要服务器恢复再重新连上 MQ 发送消息,监听服务器就能再次接收。好处 2:MQ 作为一个仓库,本身就提供了非常强大的功能,例如不再是简单的一对一功能,还能一对多,多对一,自己脑补保险箱场景,只要有特定的密码,谁都能存,谁都能取。也就是说能实现群发消息和以此衍生的功能。
什么是负载均衡?
负载均衡的诞生背景
在互联网发展早期,由于用户量较少、业务需求也比较简单。对于软件应用,我们只需要一台高配的服务器即可完成业务的支撑,这样的软件架构称为单体架构。
随着用户量的增加,服务器的请流量也随之增加,在这个过程中单体架构会产生两个问题。
- 软件的性能逐步下降,访问延迟越来越高
- 容易出现单点故障
为了解决这个问题,我们引入了集群化部署的架构,也就是把一个软件应用同时部署在多个服务器上
架构的变化带来了两个问题:
-
客户端请求如何均匀的分发到多台目标服务器上?
-
如何检测目标服务器的健康状态,使得客户端请求不向已经宕机的服务器发送请求。
为了解决这两个问题,引入了负载均衡的设计,简单来说,负载均衡机制的核心目的是让客户端的请求合理均匀的分发到多台目标服务器,由于请求被多个节点分发,使得服务端的性能得到有效的提升。
如何实现负载均衡呢?
常见的实现方案有三种!
-
基于 DNS 实现负载均衡
先来说一下基于 DNS 实现负载均衡的方式,它的实现方式比较简单,只需要在DNS 服务器上针对某个域名做多个 IP 映射即可。
它的工作原理是: 当用户通过域名访问某个网站时,会先通过 DNS 服务器进行域名解析得到一个 IP 地址,DNS 服务器可以随机分配一个 IP 地址进行访问,这样就可以实现目标服务集群的请求分发。
除此之外,DNS 还可以根据不同的地域分配就近机房的 IP,比如长沙的小伙伴,可能会得到在湖南范围内最近的一个机房的 IP,在这个模式下可以实现「就近原则」实现请求处理,缩短了通信距离从而提升网站访问效率。
DNS 实现负载均衡的优点是: 配置简单,实现成本低,无需额外的开发和维护。
不过缺点也很明显:
由于 DNS 多级缓存的特性,当我们修改 DNS 配置之后,会因为缓存导致 IP 变 更不及时,从而影响负载均衡的效果。
-
基于硬件实现负载均衡
硬件负载设备,我们可以简单把它理解成一个网络设备,类似于网络交换机,它
- 它的性能很好,每秒能够处理百万级别的请求,
- 支持多种负载均衡算法,我们可以非常灵活的配置不同的负载策略
- 它还具备防火墙等安全功能。
- 硬件负载是商业产品,有专门的售后来支持,所以企业不需要花精力去做维 护。
F5 是比较常见的硬件负载设备,由于硬件负载设备价格比较贵,一般应用在大型银行、政府、电信等领域。
-
基于软件实现负载均衡
所谓软件负载,就是通过一些开源软件或者商业软件来完成负载均衡的功能。常见的软件负载技术有:Nginx、LVS、HAProxy 等。
目前互联网企业绝大部分采用的都是软件负载,主要原因是:
- 免费,企业不需要投入较高的成本。
- 开源,不同企业对于负载均衡的要求有差异,所以可以基于开源软件上做二次开发
- 灵活性较高
这三种方式,没有好坏之分,只有是否合适,因此大家可以根据实际情况选择。
负载均衡的作用范围
负载均衡是作用在网络通信上,来实现请求的分发。
而在网络架构中,基于 OSI 模型,又分为 7 层网络模型
也就是意味着我们可以在网络的某些分层上做请求分发处理,因此根据这样一个特性,对于负载均衡的作用范围又可以分为:
-
二层负载:基于 Mac 地址来实现请求分发,一般采用虚拟 Mac 的方式实现,服务器收到请求后,通过动态分配后端服务的实际 Mac 地址进行响应从而实现负载均衡
-
三层负载:基于 IP 层负载,一般通过虚拟 IP 的方式实现,外部请求访问虚拟 IP,服务器收到请求后根据后端实际 IP 地址进行转发。
-
四层负载:通过请求报文中的目标地址和端口进行负载,Nginx、F5、LVS 等 都可以实现四层负载
-
七层负载: 七层负载是基于应用层负载,也就是服务器端可以根据 http 协议中请求的报文信息来决定把请求分发到哪个目标服务器上,比如 Cookie、消息体、RequestHeader 等。
最后一个,就是负载均衡的常用算法
所谓负载均衡算法,就是决定当前客户端请求匹配到目标服务器集群中的具体哪个节点。
常见的负载均衡算法有:
-
轮训,也就是多个服务器按照顺序轮训返回,这样每个服务器都能获得相同的请求次数
-
随机,根据随机算法获得一个目标服务地址(就像古时候皇帝翻牌子),由于该算法具备随机性,因此每个服务器获得的请求数量不一定均等。
-
一致性 hash,也就是对于具有相同 hash 码的请求,永远发送到同一个节点上。
-
最小连接数,根据目标服务器的请求数量来决定请求分发的权重,也就是目标服务集群中,请求更少的节点将会获得更多的请求。这是负载均衡中比较好的策略,真正能够实现目标服务器的请求均衡。
以上就是关于负载均衡相关的内容,当然,负载均衡还有很多值得去挖掘的,比如负载算法如何实现?网络分层模型的原理等。
六、爬虫
常用库与模块
试列出至少三种目前流行的大型数据库的名称:、、,其中您最熟悉的是_,从__________年开始使用
(考察对数据可的熟悉程度)Oracle,Mysql,SQLServer、MongoDB 根据自己情况(推荐 Mysql 、MongoDB)。
列举您使用过的 Python 网络爬虫所用到的网络数据包?
requests、urllib、urllib2、httplib2
列举您使用过的 Python 网络爬虫所用到的解析数据包
Re、json、jsonpath、BeautifulSoup、pyquery、lxml
爬取数据后使用哪个数据库存储数据的,为什么?
一般爬虫使用的数据库,是根据项目来定的。如需求方指定了使用什么数据库、如果没指定,那么决定权就在爬虫程序员手里,如果自选的话,mysql 和 mongodb 用的都是比较多的。但不同的数据库品种有各自的优缺点,不同的场景任何一种数据库都可以用来存储,但是某种可能会更好。比如如果抓取的数据之间的耦合性很高,关系比较复杂的话,那么 mysql 可能会是更好的选择。如果抓取的数据是分版块的,并且它们之间没有相似性或关联性不强,那么可能 mongodb 会更好。另外主流的几种永久存储数据库,都是具备处理高并发、具备存储大量数据的能力的,只是由于各自的实现机制不一样,因此优化方案也是不尽相同。总结就是:数据库的选择尽量从项目的数据存在的特性来考虑,还有一个问题就是开发人员最擅长那种数据库。
MongoDB 是使用比较多的数据库,这里以 MongoDB 为例,大家需要结合自己真实开发环境回答。
与关系型数据库相比,MongoDB 的优点如下:
-
弱一致性(最终一致),更能保证用户的访问速度
举例来说,在传统的关系型数据库中,一个 COUNT 类型的操作会锁定数据集,这样可以保证得到“当前”情况下的较精确值。这在某些情况下,例如通过 ATM 查看账户信息的时候很重要,但对于 Wordnik 来说,数据是不断更新和增长的,这种“较精确”的保证几乎没有任何意义,反而会产生很大的延迟。他们需要的是一个“大约”的数字以及更快的处理速度。
但某些情况下 MongoDB 会锁住数据库。如果此时正有数百个请求,则它们会堆积起来,造成许多问题。我们使用了下面的优化方式来避免锁定。
每次更新前,我们会先查询记录。查询操作会将对象放入内存,于是更新则会尽可能的迅速。在主/从部署方案中,从节点可以使用“-pretouch”参数运行,这也可以得到相同的效果。使用多个 mongod 进程。我们根据访问模式将数据库拆分成多个进程。
-
文档结构的存储方式,能够更便捷的获取数据
对于一个层级式的数据结构来说,如果要将这样的数据使用扁平式的,表状的结构来保存数据,这无论是在查询还是获取数据时都十分困难。
-
内置 GridFS,支持大容量的存储
GridFS 是一个出色的分布式文件系统,可以支持海量的数据存储。内置了 GridFS 了 MongoDB,能够满足对大数据集的快速范围查询。
-
内置 Sharding
提供基于 Range 的 Auto Sharding 机制:一个 collection 可按照记录的范围,分成若干个段,切分到不同的 Shard 上。Shards 可以和复制结合,配合 Replica sets 能够实现 Sharding+fail-over,不同的 Shard 之间可以负载均衡。查询是对 客户端是透明的。客户端执行查询,统计,MapReduce等操作,这些会被MongoDB 自动路由到后端的数据节点。这让我们关注于自己的业务,适当的时候可以无痛的升级。MongoDB 的 Sharding 设计能力较大可支持约 20 petabytes,足以支撑一般应用。
这可以保证 MongoDB 运行在便宜的 PC 服务器集群上。PC 集群扩充起来非常方便并且成本很低,避免了“sharding”操作的复杂性和成本。
-
第三方支持丰富。(这是与其他的 NoSQL 相比,MongoDB 也具有的优势)
现在网络上的很多 NoSQL 开源数据库完全属于社区型的,没有官方支持,给使用者带来了很大的风险。而开源文档数据库 MongoDB 背后有商业公司 10gen 为其提供商业培训和支持。而且 MongoDB 社区非常活跃,很多开发框架都迅速提供了对 MongDB 的支持。不少知名大公司和网站也在生产环境中使用MongoDB,越来越多的创新型企业转而使用 MongoDB 作为和Django,RoR 来搭配的技术方案。
-
性能优越
在使用场合下,千万级别的文档对象,近 10G 的数据,对有索引的 ID 的查询不会比 mysql 慢,而对非索引字段的查询,则是全面胜出。 mysql 实际无法胜任大数据量下任意字段的查询,而mongodb 的查询性能实在让我惊讶。写入性能同样很令人满意,同样写入百万级别的数据,mongodb 比我以前试用过的 couchdb 要快得多,基本 10 分钟以下可以解决。补上一句,观察过程中 mongodb 都远算不上是 CPU 杀手。
与 redis 相比较
-
mongodb 文件存储是 BSON 格式类似 JSON,或自定义的二进制格式。
mongodb 与 redis 性能都很依赖内存的大小,mongodb 有丰富的数据表达、索引;最类似于关系数据库,支持丰富的查询语言,redis 数据丰富,较少的 IO ,这方面 mongodb 优势明显。
-
mongodb 不支持事物,靠客户端自身保证,redis 支持事物,比较弱,仅能保证事物中的操作按顺序执行,这方面 redis 优于 mongodb。
-
mongodb 对海量数据的访问效率提升,redis 较小数据量的性能及运算,这方面 mongodb性能优于 redis .monbgodb 有 mapredurce 功能,提供数据分析,redis 没有 ,这方面 mongodb 优于 redis 。
你用过的爬虫框架或者模块有哪些?谈谈他们的区别或者优缺点?
Python 自带:urllib、urllib2
第三方:requests,aiohttp
框架: Scrapy、pyspider
urllib 和 urllib2 模块都做与请求 URL 相关的操作,但他们提供不同的功能。
urllib2:urllib2.urlopen 可以接受一个 Request 对象或者 url,(在接受 Request 对象时候,并以此可以来设置一个 URL 的 headers),urllib.urlopen 只接收一个url。
urllib 有 urlencode,urllib2 没有,因此总是 urllib,urllib2 常会一起使用的原因。
scrapy 是封装起来的框架,它包含了下载器,解析器,日志及异常处理,基于多线程,twisted 的方式处理,对于固定单个网站的爬取开发,有优势,但是对于多网站爬取,并发及分布式处理方面,不够灵活,不便调整与括展。
request 是一个 HTTP 库, 它只是用来进行请求,对于 HTTP 请求,他是一个强大的下载库,解析全部自己处理,灵活性更高,高并发与分布式部署也非常灵活,对于功能可以更好实现
aiohttp 是一个基于 python3 的 asyncio 协程机制实现的一个 http 库,相比 requests,aiohttp自身就具备了异步功能。但只能在 python3 环境使用。
Scrapy 优点:
- scrapy 是异步的;
- 采取可读性更强的 xpath 代替正则;
- 强大的统计和 log 系统;
- 同时在不同的 url 上爬行;
- 支持 shell 方式,方便独立调试;
- 写 middleware,方便写一些统一的过滤器;
- 通过管道的方式存入数据库;
Scrapy 缺点:
-
基于 python 的爬虫框架,扩展性比较差;
-
基于 twisted 框架,运行中的 exception 是不会干掉 reactor,并且异步框架出错后是不会停掉
-
其他任务的,数据出错后难以察觉。
Pyspider 是一个重量级的爬虫框架,为什么说重量级?我们知道 scrapy 没有数据库集成、没有分布式、断点续爬的支持、没有 UI 控制界面等等、如果基于 scrapy 想要实现这些功能,都需要自行开发,但 pyspider 都已经集成了。但也正因如此 pyspider 另一个问题就是,扩展性太差,学习难度较大。
写爬虫是用多进程好?还是多线程好? 为什么?
一般情况下,在选择是使用多进程还是多线程时,主要考虑的业务到底是 IO 密集型(多线程)还是计算密集型(多进程)。在爬虫中,请求的并发业务属于是网络的 IO 类型业务,因此网络并发适宜使用多线程;但特殊需求下,比如使用phantomjs 或者 chrome-headless 来抓取的爬虫,应当是多进程的,因为每一个phan/chro 实例就是一个进程了,并发只能是多进程。此外爬虫中还是数据处理业务,如果数据处理业务是一个比较耗时的计算型操作,那么对数据处理部分应当设为多进程,但更多可能会考虑将该部分数据处理操作和爬虫程序解耦,也就是先把数据抓取下来,事后单独运行另外的程序解析数据。
常见的反爬虫和应对方法?
反爬虫简单的说指的是:网站根据某些因素断定该请求是一个爬虫程序发出的请求,然后对该请求采取了一定的制裁措施。
因此反爬虫应该从两方面来探讨:
- 网站如何断定该请求是一个爬虫:请求头、请求频率、IP 地址、cookie 等(持续更新请求头、cookie、用户 cookie 池、代理 IP 池、设置一定的延时)
- 网站如何制裁这个它认为是爬虫的请求(假数据、空数据、不返回响应、验证码、4xx 状态码等)
通过 Headers 反爬虫:
基于用户行为反爬虫:
还有一部分网站是通过检测用户行为,例如同一 IP 短时间内多次访问同一页面,或者同一账户短时间内多次进行相同操作。
大多数网站都是前一种情况,对于这种情况,使用 IP 代理就可以解决。可以专门写一个爬虫,爬取网上公开的代理 ip,检测后全部保存起来。这样的代理 ip 爬虫经常会用到,最好自己准备一个。有了大量代理 ip 后可以每请求几次更换一个 ip,这在 requests 或者 urllib2 中很容易做到,这样就能很容易的绕过第一种反爬虫。
对于第二种情况,可以在每次请求后随机间隔几秒再进行下一次请求。有些有逻辑漏洞的网站,可以通过请求几次,退出登录,重新登录,继续请求来绕过同一账号短时间内不能多次进行相同请求的限制。
动态页面的反爬虫:
上述的几种情况大多都是出现在静态页面,还有一部分网站,我们需要爬取的数据是通过 ajax 请求得到,或者通过 JavaScript 生成的。首先用 Fiddler 对网络请求进行分析。如果能够找到 ajax 请求,也能分析出具体的参数和响应的具体含义,我们就能采用上面的方法,直接利用 requests 或者 urllib2模拟 ajax 请求,对响应的 json 进行分析得到需要的数据。
能够直接模拟 ajax 请求获取数据固然是极好的,但是有些网站把 ajax 请求的所有参数全部加密了。
我们根本没办法构造自己所需要的数据的请求。这种情况下就用selenium+phantomJS,调用浏览器内核,并利用 phantomJS 执行 js 来模拟人为操作以及触发页面中的 js 脚本。从填写表单到点击按钮再到滚动页面,全部都可以模拟,不考虑具体的请求和响应过程,只是完完整整的把人浏览页面获取数据的过程模拟一遍。
用这套框架几乎能绕过大多数的反爬虫,因为它不是在伪装成浏览器来获取数据(上述的通过添加Headers 一定程度上就是为了伪装成浏览器),它本身就是浏览器,phantomJS 就是一个没有界面的浏览器,只是操控这个浏览器的不是人。利 selenium+phantomJS 能干很多事情,例如识别点触式(12306)或者滑动式的验证码,对页面表单进行暴力破解等。
需要登录的网页,如何解决同时限制 ip,cookie,session(其中有一些是动态生成的)在不使用动态爬取的情况下?
解决限制 IP 可以使用代理 IP 地址池、服务器;
不适用动态爬取的情况下可以使用反编译 JS 文件获取相应的文件,或者换用其他平台(比如手机端)看看是否可以获取相应的 json 文件。
验证码的解决?
图形验证码:干扰、杂色不是特别多的图片可以使用开源库 Tesseract 进行识别,太过复杂的需要借助第三方打码平台。
点击和拖动滑块验证码可以借助 selenium、无图形界面浏览器(chromedirver 或者 phantomjs)和 pillow 包来模拟人的点击和滑动操作,pillow 可以根据色差识别需要滑动的位置。
手动打码(有的验证码确实无解)
写一个邮箱地址的正则表达式?
[A-Za-z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$
编写过哪些爬虫中间件?
user-agent、代理池等。
“极验”滑动验证码如何破解?
- selenium 控制鼠标实现,速度太机械化,成功率比较低
- 计算缺口的偏移量(推荐博客:http://blog.csdn.net/paololiu/article/details/52514504?>
- “极验”滑动验证码需要具体网站具体分析,一般牵扯算法乃至深度学习相关知识。
注意:在《Python 3网络爬虫开发实战第二版》有具体案例
爬的那些内容数据量有多大,多久爬一次,爬下来的数据是怎么存储?
京东整站的数据大约在 1 亿左右,爬下来的数据存入数据库,mysql 数据库中如果有重复的 url 建议去重存入数据库,可以考虑引用外键。评分,评论如果做增量,Redis 中 url 去重,评分和评论建议建立一张新表用 id 做关联。
多久爬一次这个问题要根据公司的要求去处理,不一定是每天都爬。
Mongo 建立唯一索引键(id)可以做数据重复。前提是数据量不大 。2 台电脑几百万的情况,数据库需要做分片 (数据库要设计合理)。
例:租房的网站数据量每天大概是几十万条 ,每周固定爬取。
cookie 过期的处理问题?
因为 cookie 存在过期的现象,一个很好的处理方法就是做一个异常类,如果有异常的话,cookie 抛出异常类在执行程序。
动态加载又对及时性要求很高怎么处理?
分析出数据接口 API(利用 chrome 或者抓包工具),然后利用 requests 等 http 库实现爬虫,如果有需要执行 js 代码,可借助 js2py、pyexecjs 等工具实现。
Selenium+Phantomjs
尽量不使用 sleep 而使用 WebDriverWait
谈一谈你对 Selenium 和 PhantomJS 了解
Selenium 是一个 Web 的自动化测试工具,可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。Selenium 自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。但是我们有时候需要让它内嵌在代码中运行,所以我们可以用一个叫 PhantomJS 的工具代替真实的浏览器。Selenium 库里有个叫 WebDriver 的 API。WebDriver 有点儿像可以加载网站的浏览器,但是它也可以像 BeautifulSoup 或者其他Selector 对象一样用来查找页面元素,与页面上的元素进行交互 (发送文本、点击等),以及执行其他动作来运行网络爬虫。
PhantomJS 是一个基于 Webkit 的“无界面”(headless)浏览器,它会把网站加载到内存并执行页面上的 JavaScript,因为不会展示图形界面,所以运行起来比完整的浏览器要高效。相比传统的 Chrome或 Firefox 浏览器等,资源消耗会更少。
如果我们把 Selenium 和 PhantomJS 结合在一起,就可以运行一个非常强大的网络爬虫了,这个爬虫可以处理 JavaScript、Cookie、headers,以及任何我们真实用户需要做的事情。
主程序退出后,selenium 不保证 phantomJS 也成功退出,最好手动关闭 phantomJS 进程。(有可能会导致多个 phantomJS 进程运行,占用内存)。
WebDriverWait 虽然可能会减少延时,但是目前存在 bug(各种报错),这种情况可以采用 sleep。
phantomJS 爬数据比较慢,可以选择多线程。如果运行的时候发现有的可以运行,有的不能,可以尝试将 phantomJS 改成 Chrome。
代理 IP 里的“透明”“匿名”“高匿”分别是指?
透明代理的意思是客户端根本不需要知道有代理服务器的存在,但是它传送的仍然是真实的 IP。你要想隐藏的话,不要用这个。
普通匿名代理能隐藏客户机的真实 IP,但会改变我们的请求信息,服务器端有可能会认为我们使用了代理。不过使用此种代理时,虽然被访问的网站不能知道你的 ip 地址,但仍然可以知道你在使用代理,当然某些能够侦测 ip 的网页仍然可以查到你的 ip。
高匿名代理不改变客户机的请求,这样在服务器看来就像有个真正的客户浏览器在访问它,这时客户的真实 IP 是隐藏的,服务器端不会认为我们使用了代理。
设置代理有以下两个好处:
- 让服务器以为不是同一个客户端在请求
- 防止我们的真实地址被泄露,防止被追究
requests 返回的 content 和 text 的区别?
-
response.text 返回的是 Unicode 型数据;
response.content 返回的是 bytes 类型,也就是二进制数据;
-
获取文本使用response.text;
获取图片、文件使用 response.content
-
response.text
- 类型:str
- 解码类型: 根据 HTTP 头部对响应的编码作出有根据的推测,推测的文本编码
- 如何修改编码方式:response.encoding=”gbk”
response.content
- 类型:bytes
- 解码类型: 没有指定
- 如何修改编码方式:response.content.decode(“utf8”)
robots 协议
Robots 协议:网站通过 Robots 协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。
为什么 requests 请求需要带上 header?
让我们的爬虫请求看起来就是一个正常的浏览器请求
原因是:模拟浏览器,欺骗服务器,获取和浏览器一致的内容
header 的形式:字典
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"}
用法: requests.get(url,headers=headers)
requests 使用小技巧
-
reqeusts.util.dict_from_cookiejar 把 cookie 对象转化为字典
requests.get(url,cookies={})
-
设置请求不用 SSL 证书验证
response = requests.get("https://www.12306.cn/mormhweb/ ", verify=False)
-
设置超时
response = requests.get(url,timeout=10)
-
配合状态码判断是否请求成功
assert response.status_code == 200
通用爬虫与聚焦爬虫
通用爬虫 :通常指搜索引擎的爬虫
聚焦爬虫 :针对特定网站的爬虫
通用搜素引擎的局限性:
-
通用搜索引擎所返回的网页里 90%的内容无用。
-
图片、数据库、音频、视频多媒体的内容通用搜索引擎无能为力。不同用户搜索的目的不全相同,但是返回内容相同。
平常怎么使用代理的?
- 自己维护代理池
- 付费购买(目前市场上有很多 ip 代理商,可自行百度了解,建议看看他们的接口文档(API&SDK))
IP 存放在哪里?怎么维护 IP?对于封了多个 ip 的,怎么判定 IP 没被封?
存放在数据库(redis、mysql 等)。
维护多个代理网站:
一般代理的存活时间往往在十几分钟左右,定时任务,加上代理 IP 去访问网页,验证其是否可用,如果返回状态为 200,表示这个代理是可以使用的。
怎么获取加密的数据?
-
Web 端加密可尝试移动端(app)
-
解析加密,看能否破解
-
反爬手段层出不穷,js 加密较多,只能具体问题具体分析
分析前端 js 文件,找到加密解密数据的 js 代码,用 Python 代码实现或者利用 js2py 或 pyexecjs等执行 js 代码
假如每天爬取量在 5、6 万条数据,一般开几个线程,每个线程 ip 需要加锁限定吗?
5、6 万条数据相对来说数据量比较小,线程数量不做强制要求(做除法得一个合理值即可)
多线程使用代理,应保证不在同时一刻使用一个代理 IP
一般请求并发量主要考虑网站的反爬程度来定。
怎么监控爬虫的状态
- 使用 python 的 STMP 包将爬虫的状态信心发送到指定的邮箱
- Scrapyd、pyspider
- 引入日志
- 集成日志处理平台来进行监控,如 elk
如何定时启动你的爬虫项目
- 最简单的方法:直接使用 Timer 类
- 使用 sched
- 使用 Crontab
Scrapy
谈谈你对 Scrapy 的理解?
scrapy 是一个为了爬取网站数据,提取结构性数据而编写的应用框架,我们只需要实现少量代码,就能够快速的抓取到数据内容。Scrapy 使用了 Twisted 异步网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求。
scrapy 框架的工作流程:
- 首先 Spiders(爬虫)将需要发送请求的 url(requests)经 ScrapyEngine(引擎)交给 Scheduler(调度器)。
- Scheduler(排序,入队)处理后,经 ScrapyEngine,DownloaderMiddlewares(可选,主要有 User_Agent, Proxy 代理)交给 Downloader。
- Downloader 向互联网发送请求,并接收下载响应(response)。将响应(response)经ScrapyEngine,SpiderMiddlewares(可选)交给 Spiders。
- Spiders 处理 response,提取数据并将数据经 ScrapyEngine 交给 ItemPipeline 保存(可以是本地,可以是数据库)。提取 url 重新经 ScrapyEngine 交给 Scheduler 进行下一个循环。直到无 Url请求程序停止结束。
怎么样让 scrapy 框架发送一个 post 请求(具体写出来)
使用 FormRequest
1. class mySpider(scrapy.Spider):
2. # start_urls = ["http://www.taobao.com/"]
3. def start_requests(self):
4. url = 'http://http://www.taobao.com//login'
5. # FormRequest 是 Scrapy 发送 POST 请求的方法
6. yield scrapy.FormRequest(
7. url = url,
8. formdata = {"email" : "xxx", "password" : "xxxxx"},
9. callback = self.parse_page
10. )
11. def parse_page(self, response):
12. # do something
怎么判断网站是否更新?
静态页面可以考虑使用 http 的 head 方法查看网站的最后更新时间。如果前者行不通,则需要对数据进行标识,如利用加密算法生成指纹,然后定期发起请求获取数据,比对指纹是否一致,如果一致,说明网站没有更新;反之说明更新。
图片、视频爬取怎么绕过防盗连接,或者说怎么获取正确的链接地址?
自定义 Referer(建议自行 Google 相关知识)。
你爬出来的数据量大概有多大?大概多长时间爬一次?
无标准答案,根据自己爬取网站回答即可(几百万,几千万,亿级)。根据项目需求来定,另外并发量根据网站反爬和机器数量带宽大小来计算
增量爬取
实现一个持久化的请求队列
增量爬取即保存上一次状态,本次抓取时与上次比对,如果不在上次的状态中,便视为增量,保存下来。对于 scrapy 来说,上一次的状态是抓取的特征数据和上次爬取的request 队列(url 列表),request 队列可以通过 request 队列,可以通过scrapy.core.scheduler 的 pending_requests 成员得到,在爬虫启动时导入上次爬取的特征数据,并且用上次 request 队列的数据作为 start url 进行爬取,不在上一次状态中的数据便保存。
选用 BloomFilter 原因:对爬虫爬取数据的保存有多种形式,可以是数据库,可以是磁盘文件等,不管是数据库,还是磁盘文件,进行扫描和存储都有很大的时间和空间上的开销,为了从时间和空间上提升性能,故选用 BloomFilter 作为上一次爬取数据的保存。保存的特征数据可以是数据的某几项,即监控这几项数据,一旦这几项数据有变化,便视为增量持久化下来,根据增量的规则可以对保存的状态数据进行约束。比如:可以选网页更新的时间,索引次数或是网页的实际内容,cookie 的更新等。
爬虫向数据库存数据开始和结束都会发一条消息,是 scrapy 哪个模块实现的?
Scrapy 使用信号来通知事情发生,因此答案是 signals 模块。
爬取下来的数据如何去重,说一下具体的算法依据
-
通过 MD5 生成电子指纹来判断页面是否改变
-
nutch 去重。nutch 中 digest 是对采集的每一个网页内容的 32 位哈希值,如果两个网页内容完全一样,它们的 digest 值肯定会一样。
-
数据量不大时,可以直接放在内存里面进行去重,python 可以使用 set()进行去重。当去重数据需要持久化时可以使用 redis 的 set 数据结构。
-
当数据量再大一点时,可以用不同的加密算法先将长字符串压缩成 16/32/40 个字符,再使用上面两种方法去重。
-
当数据量达到亿(甚至十亿、百亿)数量级时,内存有限,必须用“位”来去重,才能够满足需求。Bloomfilter 就是将去重对象映射到几个内存“位”,通过几个位的 0/1 值来判断一个对象是否已经存在。
然而 Bloomfilter 运行在一台机器的内存上,不方便持久化(机器 down 掉就什么都没啦),也不方便分布式爬虫的统一去重。如果可以在 Redis 上申请内存进行 Bloomfilter,以上两个问题就都能解决了。
simhash 最牛逼的一点就是将一个文档最后转换成一个 64 位的字节,暂且称之为特征字,然后判断重复只需要判断他们的特征字的距离是不是<n(根据经验这个 n 一般取值为 3),就可以判断两个文档是否相似。
-
可见 scrapy_redis 是利用 set 数据结构来去重的,去重的对象是 request 的 fingerprint(其实就是用 hashlib.sha1()对 request 对象的某些字段信息进行压缩)。其实 fp 就是 request 对象加密压缩后的一个字符串(40 个字符,0~f)。
怎么设置深度爬取?
通过在 settings.py 中设置 depth_limit 的值可以限制爬取深度,这个深度是与 start_urls 中定义 url 的相对值。也就是相对 url 的深度。若定义 url 为http://www.domz.com/game/,depth_limit=1 那么限制爬取的只能是此 url 下一级的网页。深度大于设置值的将被忽视。
Scrapy-redis
scrapy 和 scrapy-redis 有什么区别?为什么选择 redis 数据库?
scrapy 是一个 Python 爬虫框架,爬取效率极高,具有高度定制性,但是不支持分布式。而scrapy-redis 一套基于 redis 数据库、运行在 scrapy 框架之上的组件,可以让 scrapy 支持分布式策略,Slaver 端共享 Master 端 redis 数据库里的 item 队列、请求队列和请求指纹集合。
为什么选择 redis 数据库,因为 redis 支持主从同步,而且数据都是缓存在内存中的,所以基于 redis的分布式爬虫对请求和数据的高频读取效率非常高。
分布式爬虫主要解决什么问题?
- ip
- 带宽
- cpu
- io
什么是分布式存储?
传统定义:分布式存储系统是大量 PC 服务器通过 Internet 互联,对外提供一个整体的服务。
分布式存储系统具有以下的几个特性:
可扩展 :分布式存储系统可以扩展到几百台甚至几千台这样的一个集群规模,系统的整体性能线性增长。
低成本 :分布式存储系统的自动容错、自动负载均衡的特性,允许分布式存储系统可以构建在低成本的服务器上。另外,线性的扩展能力也使得增加、减少服务器的成本低,实现分布式存储系统的自动运维。
高性能 :无论是针对单台服务器,还是针对整个分布式的存储集群,都要求分布式存储系统具备高性能。
易用 :分布式存储系统需要对外提供方便易用的接口,另外,也需要具备完善的监控、运维工具,并且可以方便的与其他的系统进行集成。
分布式存储系统的挑战主要在于数据和状态信息的持久化,要求在自动迁移、自动容错和并发读写的过程中,保证数据的一致性。
容错:可以快速检测到服务器故障,并自动的将在故障服务器上的数据进行迁移。
负载均衡:新增的服务器在集群中保障负载均衡?数据迁移过程中保障不影响现有的服务。
事务与并发控制:实现分布式事务。
易用性:设计对外接口,使得设计的系统易于使用。
你所知道的分布式爬虫方案有哪些?
三种分布式爬虫策略:
-
Slaver 端从 Master 端拿任务(Request/url/ID)进行数据抓取,在抓取数据的同时也生成新任务,并将任务抛给 Master。Master 端只有一个 Redis 数据库,负责对 Slaver 提交的任务进行去重、加入待爬队列。
优点: scrapy-redis 默认使用的就是这种策略,我们实现起来很简单,因为任务调度等工作 scrapy-redis 都已经帮我们做好了,我们只需要继承 RedisSpider、指定 redis_key 就行了。
缺点: scrapy-redis 调度的任务是 Request 对象,里面信息量比较大(不仅包含 url,还有 callback 函数、headers 等信息),导致的结果就是会降低爬虫速度、而且会占用Redis 大量的存储空间。当然我们可以重写方法实现调度 url 或者用户 ID。
-
Master 端跑一个程序去生成任务(Request/url/ID)。Master 端负责的是生产任务,并把任务去重、加入到待爬队列。Slaver 只管从 Master 端拿任务去爬。
优点: 将生成任务和抓取数据分开,分工明确,减少了 Master 和 Slaver 之间的数据交流;Master 端生成任务还有一个好处就是:可以很方便地重写判重策略(当数据量大时,优化判重的性能和速度还是很重要的)。
缺点: 像 QQ 或者新浪微博这种网站,发送一个请求,返回的内容里面可能包含几十个待爬的用户 ID,即几十个新爬虫任务。但有些网站一个请求只能得到一两个新任务,并且返回的内容里也包含爬虫要抓取的目标信息,如果将生成任务和抓取任务分开反而会降低爬虫抓取效率。毕竟带宽也是爬虫的一个瓶颈问题,我们要秉着发送尽量少的请求为原则,同时也是为了减轻网站服务器的压力,要做一只有道德的 Crawler。所以视情况而定。
-
Master 中只有一个集合,它只有查询的作用。Slaver 在遇到新任务时询问 Master 此任务是否已爬,如果未爬则加入 Slaver 自己的待爬队列中,Master 把此任务记为已爬。它和策略一比较像,但明显比策略一简单。策略一的简单是因为有 scrapy-redis 实现了scheduler 中间件,它并不适用于非 scrapy 框架的爬虫。
优点: 实现简单,非 scrapy 框架的爬虫也适用。Master 端压力比较小,Master 与Slaver 的数据交流也不大。
缺点:“健壮性”不够,需要另外定时保存待爬队列以实现“断点续爬”功能。各 Slaver的待爬任务不通用。
如果把 Slaver 比作工人,把 Master 比作工头。策略一就是工人遇到新任务都上报给工头,需要干活的时候就去工头那里领任务;策略二就是工头去找新任务,工人只管从工头那里领任务干活;策略三就是工人遇到新任务时询问工头此任务是否有人做了,没有的话工人就将此任务加到自己的“行程表”。
除了 scrapy-redis,有做过其他的分布式爬虫吗?
Celery、gearman 等,参考其他分布式爬虫策略。
在爬取的时候遇到某些内容字段缺失怎么判断及处理?
判读字段缺失,做异常处理即可。
什么是 scrapy-redis 中的指纹,是如何去重的?
指纹:通过 sha1 加密,把请求体,请求方式,请求 url 放在一起。然后进行 16 进制的转义符字符串生成指纹。生成一个字符串,放到数据库中作为唯一标示。
去重:url 中按照 url 去重:
- 按照 url 去重,有一个列表,发送请求之前从数据表中看一下这个 url 有没有请求过,请求过了就不用看了
- 内容判断,从数据库中查数据的表示,如果请求过了就在不在请求了。
JS逆向
网站加密和混淆技术
网站为了保护其数据而采取的一些措施可以归类为两大类 :
-
URL/API参数加密
网站运营者首先想到的防护措施可能是对某些数据接口的参数进行加密,比如说给某些URL的参数加上校验码,给一些ID信息编码,给某些API请求加上token、sign 等签名,这样这些请求发送到服务器时,服务器会通过客户端发来的一些请求信息以及双方约定好的密钥等来对当前的请求进行校验,只有校验通过,才返回对应数据结果。
比如说客户端和服务端约定一种接口校验逻辑,客户端在每次请求服务端接口的时候都会附带一个sign参数,这个sign参数可能是由当前时间信息、请求的URL、请求的数据、设备的ID、双方约定好的密钥经过一些加密算法构造而成的,客户端会实现这个加密算法来构造sign,然后每次请求服务器的时候附带上这个参数。服务端会根据约定好的算法和请求的数据对sign进行校验、只有校验通过、才返回对应的数据,否则拒绝响应。
当然,登录状态的校验也可以看作此类方案、比如一个API的调用必须传一个token,这个token必须在用户登录之后才能获取,如果请求的时候不带该token,API就不会返回任何数据。
倘若没有这种措施,那么URL 或者API接口基本上是完全可以公开访问的、这意味着任何人都可以直接调用来获取数据,几乎是零防护的状态,这样是非常危险的,而且数据也可以被轻易地被爬虫爬取。因此,对 URL/API参数进行加密和校验是非常有必要的。
-
JavaScript压缩、混淆和加密
如果你不想让自己的数据被轻易获取,不想他人了解 JavaScript逻辑的实现、或者想降低被不怀好意的人甚至是黑客攻击的风险,那么就需要用到JavaScript压缩、混淆和加密技术了。
这里压缩、混绢和加密技术简述如下。
-
代码压缩:去除JavaScript代码中不必要的空格、换行等内容,使源码都压缩为几行内容,降低代码的可读性,当然同时也能提高网站的加载速度。
-
代码混淆:使用变量替换、字符串阵列化、控制流平坦化、多态变异、僵尸函数、调试保护等手段,使代码变得难以阅读和分析,达到最终保护的目的。但这不影响代码的原有功能,是理想、实用的JavaScript保护方案。
- 变量名混淆:将带有含义的变量名、方法名、常量名随机变为无意义的类乱码字符串。降低代码的可读性,如转成单个字符或十六进制字符串。
- 字符串混淆:将字符串阵列化集中放置并可进行 MD5或Base64加密存储,使代码中不出现明文字符串,这样可以避免使用全局搜索字符串的方式定位到入口。
- 对象键名替换:针对JavaScript对象的属性进行加密转化,隐藏代码之间的调用关系。
- 控制流平坦化:打乱函数原有代码的执行流程及函数调用关系,使代码逻辑变得混乱无序。
- 无用代码注入:随机在代码中插入不会被执行到的无用代码,进一步使代码看起来更加混乱。
- 调试保护:基于调试器特性,对当前运行环境进行检验,加入一些debugger语句,使其在调试模式下难以顺利执行JavaScript 代码。
- 多态变异:使JavaScript代码每次被调用时,将代码自身立刻自动发生变异,变为与之前完全不同的代码,即功能完全不变,只是代码形式变异,以此杜绝代码被动态分析和调试。
- 域名锁定:使JavaScript 代码只能在指定域名下执行。
- 代码自我保护:如果对JavaScript代码进行格式化,则无法执行,导致浏览器假死。
- 特殊编码:将JavaScript完全编码为人不可读的代码,如表情符号、特殊表示内容等等。
在前端开发中,现在JavaScript混淆的主流实现是javascript-obfuscator和 terser 这两个库。它们都能提供一些代码混淆功能,也都有对应的webpack 和Rollup打包工具的插件。利用它们,我们可以非常方便地实现页面的混淆,最终输出压缩和混淆后的JavaScript代码,使得JavaScript代码的可读性大大降低。
-
代码加密:可以通过某种手段将JavaScript代码进行加密、转成人无法阅读或者解析的代码,如借用WebAssembly技术,可以直接将JavaScript代码用C/C++实现,JavaScript调用其编译后形成的文件来执行相应的功能。
-
JavaScript Hook的使用
Hook 技术又叫钩子技术,指在程序运行的过程中,对其中的某个方法进行重写,在原先的方法前后加入我们自定义的代码。相当于在系统没有调用该函数之前,钩子程序就先捕获该消息,得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,也可以强制结束消息的传递。
要对JavaScript代码进行Hook操作,就需要额外在页面中执行一些有关Hook逻辑的自定义代码。那么问题来了?怎样才能在浏览器中方便地执行我们所期望执行的JavaScript代码呢?这里推荐一个插件,叫作Tampermonkey。这个插件的功能非常强大,利用它我们几乎可以在网页中执行任何JavaScript代码,实现我们想要的功能。
无限debugger的原理与绕过
原理:在无限for循环、无限 while循环、无限递归调用等中执行debugger语句。
绕过方式:
- 因为debugger其实就是对应的一个断点,它相当于用代码显式地声明了一个断点,要解除它,我们只需要禁用这个断点就好了。
- 禁用所有断点。全局禁用开关位于Sources面板的右上角,叫作 Deactivate breakpoints。
- 在debugger语句所在的行的行号上单击鼠标右键,此时会出现一个快捷菜单,点击Never pause here选项,意思是从不在此处暂停。
- 将远程的 JavaScript 文件替换成本地的JavaScript文件
AST技术
AST的全称叫作Abstract Syntax Tree,中文翻译叫作抽象语法树。如果你对编译原理有所了解的话,一段代码在执行之前,通常要经历这么三个步骤。
- 词法分析:一段代码首先会被分解成一段段有意义的词法单元,比如说const name = 'Germey'这段代码,它就可以被拆解成四部分:const、name 、=、'Cermey',每一个部分都具备一定的含义。
- 语法分析:接着编译器会尝试对一个个词法单元进行语法分析,将其转换为能代表程序语法结构的数据结构。比如,const就被分析为VariableDeclaration类型,代表变量声明的具体定义;name就被分析为Identifier类型,代表一个标识符。代码内容多了,这一个个词法就会有依赖、嵌套等关系,因此表示语法结构的数据结构就构成了一个树状的结构,也就成了语法树,即 AST。
- 指令生成:最后将AST转换为实际真正可执行的指令并执行即可。
AST是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这种数据结构其实可以类别成一个大的JSON对象。前面我们也介绍过JSON对象,它可以包含列表、字典并且层层嵌套,因此它看起来就像一棵树、有树根、树干、树枝和树叶,无论多大,都是一棵完整的树。
在前端开发中,AST技术应用非常广泛,比如 webpack 打包工具的很多压缩和优化插件,Babel插件、Vue和React的脚手架工具的底层等都运用了AST技术。有了AST,我们可以方便地对JavaScript代码进行转换和改写,因此还原混绢后的JavaScript 代码也就不在话下了。
JavaScript混淆方式多种多样,如表达式还原、字符串还原、无用代码剔除、反控制流平坦化等。
其他混淆方式
除了基于javascript-obfuscator的混淆,还有其他混淆方式,比如AAEncode、JJEncode 、JSFuck的还原方法。
AAEncode的还原:AAEncode是一种JavaScript代码混淆算法,利用它,我们可以将JavaScript代码转换成颜文字表示的JavaScript 代码。
JJEncode的还原:JJEncode也是一种JavaScript代码混淆算法,其原理和AAEncode大同小异,利用它,我们可以将JavaScript代码转换成颜文字表示的 JavaScript 代码。
JSFuck 的还原:JSFuck也是一种特殊的混淆方案,是基于开源的JSFuck库来实现的。
逆向步骤
JavaScript逆向可以分为三大部分:寻找人口、调试分析和模拟执行。下面我们来分别介绍。
-
寻找入口:这是非常关键的一步,逆向在大部分情况下就是找一些加密参数到底是怎么来的,比如一个请求中token .sign等参数到底是在哪里构造的,这个关键逻辑可能写在某个关键的方法里面或者隐藏在某个关键变量里面。一个网站加载了很多JavaScript文件、那么怎么从这么多JavaScript代码里面找到关键的位置,那就是一个关键问题。这就是寻找入口。
常见的分析入口的方法包括查看请求、搜索参数、分析发起调用、断点、Hook等。还有很多其他方法,比如使用Pyppeteer、PlayWright里面内置的API实现一些数据拦截和过滤功能,也可以使用一些抓包软件对一些请求进行拦截和分析,还可以使用一些第三方工具或浏览器插件来辅助分析。
-
调试分析:找到入口之后,比如说我们可以定位到某个参数可能是在某个方法里面执行的了,那么里面的逻辑究竞是怎样的,里面调用了多少加密算法,经过了多少变量赋值和转换等,这些我们需要先把整体思路搞清楚,以便于我们后面进行模拟调用或者逻辑改写。在这个过程中,我们主要借助于浏览器的调试工具进行断点调试分析,或者借助于一些反混淆工具进行代码的反混淆等。
常见的方法包括格式化、断点调试、反混淆等操作。
-
模拟执行:经过调试分析之后,我们差不多已经搞清楚整个逻辑了,但我们的最终目的还是写爬虫,怎么爬到数据才是根本,因此这里就需要对整个加密过程进行逻辑复写或者模拟执行,以把整个加密流程模拟出来,比如输入是一些已知变量,调用之后我们就可以拿到一些token内容,再用这个token来进行数据爬取即可。
常见的方法包括Python改写或模拟执行、JavaScript模拟执行+API、浏览器模拟执行等。
自定义框架
其他
HTTP 头有什么字段?
每个 HTTP 请求和响应都会带有相应的头部信息。默认情况下,在发送 XHR 请求的同时,还会发送下列头部信息:
- Accept:浏览器能够处理的内容类型
- Accept-Charset:浏览器能够显示的字符集
- Accept-Encoding:浏览器能够处理的压缩编码
- Accept-Language:浏览器当前设置的语言 Connection:浏览器与服务器之间连接的类型
- Cookie:当前页面设置的任何
- Cookie Host:发出请求的页面所在的域
- Referer:发出请求的页面的 URL
- User-Agent:浏览器的用户代理字符串
HTTP 响应头部信息:
- Date:表示消息发送的时间,时间的描述格式由 rfc822 定义
- server:服务器名字。
- Connection:浏览器与服务器之间连接的类型
- content-type:表示后面的文档属于什么 MIME 类型
- Cache-Control:控制 HTTP 缓存
你所知道的分布式爬虫方案有哪些?
主从式分布爬虫
所谓主从模式,就是由一台服务器充当 master,若干台服务器充当 slave,master 负责管理所有连接上来的 slave,包括管理 slave 连接、任务调度与分发、结果回收并汇总等;每个 slave 只需要从 master 那里领取任务并独自完成任务最后上传结果即可,期间不需要与其他 slave 进行交流。
对于主从分布式爬虫,不同的服务器承担不同的角色分工,其中有一台专门负责对其他服务器提供 URL 分发服务,其他机器则进行实际的网页下载。URL 服务器维护待抓取 URL队列,并从中获得待抓取网页的 URL,分配给不同的抓取服务器,另外还要对抓取服务器之间的工作进行负载均衡,使得各服务器承担的工作量大致相等,不至于出现忙闲不均的情况。抓取服务器之间没有通信联系,每个待抓取服务器只和 URL 服务器进行消息传递。
对等式分布爬虫
在对等式分布爬虫体系中,服务器之间不存在分工差异,每台服务器承担相同的功能,各自负担一部分 URL 的抓取工作,下图是其中一种对等式分布爬虫,Mercator 爬虫采用此种体系结构。
由于没有 URL 服务器存在,每台待抓取服务器的任务分工就成为问题。在下图所示的体系结构下,有服务器自己来判断某个 URL 是否应该由自己来抓取,或者将这个 URL 传递给相应的服务器。至于采取的判断方法,则是对网址的主域名进行哈希计算,之后取模(即hash【域名】% m,这里的 m 为服务器个数),如果计算所得的值和服务器编号匹配,则自己下载该网页,否则将网址转发给对应编号的抓取服务器
- 对等分布式爬虫(哈希取模)
- 对等分布式爬虫(一致性哈希)
七、数据库
MySQL
mysql有哪些数据类型
-
整数类型 ,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3 字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。
长度:整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。 例子,假定类型设定为INT(5),属性为UNSIGNED ZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。
-
实数类型,包括FLOAT、DOUBLE、DECIMAL。
DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。 而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。 计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。
-
字符串类型,包括VARCHAR、CHAR、TEXT、BLOB
VARCHAR用于存储可变长字符串,它比定长类型更节省空间。 VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。 VARCHAR存储的内容超出设置的长度时,内容会被截断。 CHAR是定长的,根据定义的字符串长度分配足够的空间。 CHAR会根据需要使用空格进行填充方便比较。 CHAR适合存储很短的字符串,或者所有值都接近同一个长度。 CHAR存储的内容超出设置的长度时,内容同样会被截断。
使用策略:
- 对于经常变更的数据来说,CHAR比VARCHAR更好,因为CHAR不容易产生碎片。
- 对于非常短的列,CHAR比VARCHAR在存储空间上更有效率。
- 使用时要注意只分配需要的空间,更长的列排序时会消耗更多内存。
- 尽量避免使用TEXT/BLOB类型,查询时会使用临时表,导致严重的性能开销。
-
枚举类型(ENUM),把不重复的数据存储为一个预定义的集合。
有时可以使用ENUM代替常用的字符串类型。 ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。 ENUM在内部存储时,其实存的是整数。 尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。 排序是按照内部存储的整数
-
日期和时间类型,尽量使用timestamp,空间效率高于datetime。
用整数保存时间戳通常不方便处理。 如果需要存储微妙,可以使用bigint存储。 看到这里,这道真题是不是就比较容易回答了。
Python 中操作 Mysql 步骤
代码实现:
1. #首先安装包pymysql sudo pip install pymysql
2. #之后在程序中调用 from pymysql import *
3. ''' connection 对象 用于建立与数据库的连接 创建对象:调用 connect()方法 '''
4. conn = connect(参数列表)
5. ''' 参数列表:
6. host:连接 MySQL 主机,如果是本机则为”localhost“
7. port:连接 MySQL 主机端口,默认 3306
8. database:数据库名称
9. '''
SQL 语言不同于其他编程语言的最明显特征是处理代码的顺序。在大多数据库语言中,代码按编码顺序被处理。但在 SQL 语句中,第一个被处理的子句式 FROM,而不是第一出现的 SELECT。
SQL 查询处理的步骤序号:
- FROM <left_table>
- ON <join_condition>
- <join_type> JOIN <right_table>
- WHERE <where_condition>
- GROUP BY <group_by_list>
- WITH
- HAVING <having_condition>
- SELECT
- DISTINCT
- ORDER BY <order_by_list>
- <TOP_specification> <select_list>
以上每个步骤都会产生一个虚拟表,该虚拟表被用作下一个步骤的输入。这些虚拟表对调用者(客户端应用程序或者外部查询)不可用。只有最后一步生成的表才会会给调用者。如果没有在查询中指定某一个子句,将跳过相应的步骤。
逻辑查询处理阶段简介:
- FROM:对 FROM 子句中的前两个表执行笛卡尔积(交叉联接),生成虚拟表 VT1。
- ON:对 VT1 应用 ON 筛选器,只有那些使为真才被插入到 TV2。
- OUTER (JOIN):如果指定了 OUTER JOIN(相对于 CROSS JOIN 或 INNER JOIN),保留表中未找到匹配的行将作为外部行添加到 VT2,生成 TV3。如果 FROM 子句包含两个以上的表,则对上一个联接生成的结果表和下一个表重复执行步骤 1 到步骤 3,直到处理完所有的表位置。
- WHERE:对 TV3 应用 WHERE 筛选器,只有使为 true 的行才插入 TV4。
- GROUP BY:按 GROUP BY 子句中的列列表对 TV4 中的行进行分组,生成 TV5。CUTE|ROLLUP:把超组插入 VT5,生成 VT6。
- WITH: 对VT6应用ROLLUP或CUBE选项,生成虚拟表 7。
- HAVING:对 VT7 应用 HAVING 筛选器,只有使为 true 的组插入到 VT8。
- SELECT:处理 SELECT 列表,产生 VT9。
- DISTINCT:将重复的行从 VT9 中删除,产品 VT10。
- ORDER BY:将 VT10 中的行按 ORDER BY 子句中的列列表顺序,生成一个游标(VC11)。
- TOP:从 VC11 的开始处选择指定数量或比例的行,生成表 TV12,并返回给调用
说一下 Mysql 数据库存储的原理?
存储过程是一个可编程的函数,它在数据库中创建并保存。它可以有 SQL 语句和一些特殊的控制结构组成。当希望在不同的应用程序或平台上执行相同的函数,或者封装特定功能时,存储过程是非常有用的。数据库中的存储过程可以看做是对编程中面向对象方法的模拟。它允许控制数据的访问方式。存储过程通常有以下优点:
- 存储过程能实现较快的执行速度
- 存储过程允许标准组件是编程。
- 存储过程可以用流程控制语句编写,有很强的灵活性,可以完成复杂的判断和较复杂的运算。
- 存储过程可被作为一种安全机制来充分利用。
- 存储过程能够减少网络流量
你用的 Mysql 是哪个引擎,各引擎之间有什么区别?
mysql常用引擎包括:MYISAM、Innodb、Memory、MERGE
- MYISAM:全表锁,拥有较高的执行速度,不支持事务,不支持外键,并发性能差,占用空间相对较小,对事务完整性没有要求,以select、insert为主的应用基本上可以使用这引擎
- Innodb:行级锁,提供了具有提交、回滚和崩溃回复能力的事务安全,支持自动增长列,支持外键约束,并发能力强,占用空间是MYISAM的2.5倍,处理效率相对会差一些
- Memory:全表锁,存储在内存中,速度快,但会占用和数据量成正比的内存空间且数据在mysql重启时会丢失,默认使用HASH索引,检索效率非常高,但不适用于精确查找,主要用于那些内容变化不频繁的代码表
- MERGE:是一组MYISAM表的组合
主要 MyISAM 与 InnoDB 两个引擎,其主要区别如下:
- InnoDB 支持事务,MyISAM 不支持,这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而 MyISAM 就不可以了;
- MyISAM 适合查询以及插入为主的应用,InnoDB 适合频繁修改以及涉及到安全性较高的应用;
- InnoDB 支持外键,MyISAM 不支持;
- MyISAM 是5.5版本前的默认引擎,InnoDB 是5.5版本后的默认引擎;
- InnoDB 不支持 FULLTEXT 类型的索引;
- InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大, 因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的;
- InnoDB 中不保存表的行数,如 select count() from table 时,InnoDB需要扫描一遍整个表来计算有多少行,但是 MyISAM 只要简单的读出保存好的行数即可。注意的是,当 count()语句包含where 条件时,MyISAM 也需要扫描整个表;
- 对于自增长的字段,InnoDB 中必须包含只有该字段的索引,但是在 MyISAM 表中可以和其他字段一起建立联合索引;清空整个表时,InnoDB 是一行一行的删除,效率非常慢。MyISAM 则会重建表;
- InnoDB 支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like '%lee%'
事务的特性?
- 原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么均不执行。
- 一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序串行执行的结果相一致。
- 隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
- 持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
事务的并发问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
- 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
- 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那 么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
- 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
- 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读区别: 不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。
事务隔离级别
SQL 标准定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 SELECT @@tx_isolation; 命令来查看
这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server) 是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) ,但是你要知道的是InnoDB 存储引擎默认使用 REPEATTABLE-READ(可重读) 并不会有任何性能损失。 InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。
数据库索引?
数据库索引是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用 B_TREE。B_TREE 索引加速了数据访问,因为存储引擎不会再去扫描整张表得到需要的数据;相反,它从根节点开始,根节点保存了子节点的指针,存储引擎会根据指针快速寻找数据。
mysql 有4种不同的索引:
- 主键索引(PRIMARY) 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
- 唯一索引(UNIQUE) 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
- 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
- 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引
- 普通索引(INDEX)
- 可以通过 ALTER TABLE table_name ADD INDEX index_name (column); 创建普通索引
- 可以通过 ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3); 创建组合索引
- 全文索引(FULLTEXT)
- 可以通过 ALTER TABLE table_name ADD FULLTEXT (column); 创建全文索引
索引并非是越多越好,创建索引也需要耗费资源,一是增加了数据库的存储空间,二是在插入和删除时要花费较多的时间维护索引
MySQL 数据库索引的实现,索引的优缺点,索引的原理?
优点:
- 大大加快数据的检索速度;
- 创建唯一性索引,保证数据库表中每一行数据的唯一性;
- 加速表和表之间的连接;
- 在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间;
- 通过使用索引可以在查询的过程中使用优化隐藏器,提高系统的性能。
缺点:
- 索引需要占物理空间。
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。
实现原理:
MySQL 支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此 MySQL数据库支持多种索引类型,如 BTree 索引,B+Tree 索引,哈希索引,全文索引等等,
- 哈希索引:只有 memory(内存)存储引擎支持哈希索引,哈希索引用索引列的值计算该值的hashCode,然后在 hashCode 相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个 hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能。
- 全文索引:FULLTEXT(全文)索引,仅可用于 MyISAM,针对较大的数据,生成全文索引非常的消耗时间和空间。对于文本的大对象,或者较大的 CHAR 类型的数据,如果使用普通索引,那么匹配文本前几个字符还是可行的,但是想要匹配文本中间的几个单词,那么就要使用 LIKE %word%来匹配,这样需要很长的时间来处理,响应时间会大大增加,这种情况就可使用时 FULLTEXT 索引了,在生成 FULLTEXT 索引时,会为文本生成一份单词的清单,在索引时及根据这个单词的清单来索引。
创建索引的三种方式,删除索引
-
第一种方式:在执行CREATE TABLE时创建索引
CREATE TABLE user_index2 ( id INT auto_increment PRIMARY KEY, first_name VARCHAR (16), last_name VARCHAR (16), id_card VARCHAR (18), information text, KEY name (first_name, last_name), FULLTEXT KEY (information), UNIQUE KEY (id_card) );
-
第二种方式:使用ALTER TABLE命令去增加索引
ALTER TABLE table_name ADD INDEX index_name (column_list);
ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。
索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。
-
第三种方式:使用CREATE INDEX命令创建
CREATE INDEX index_name ON table_name (column_list);
CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)
-
删除索引
根据索引名删除普通索引、唯一索引、全文索引:
alter table 表名 drop KEY 索引名
alter table user_index drop KEY name; alter table user_index drop KEY id_card; alter table user_index drop KEY informan;
数据库的优化?
- 优化索引、SQL 语句、分析慢查询;
- 设计表的时候严格根据数据库的设计范式来设计数据库;
- 使用缓存,把经常访问到的数据而且不需要经常变化的数据放在缓存中,能节约磁盘 IO;
- 优化硬件;采用 SSD,使用磁盘队列技术(RAID0,RAID1,RDID5)等;
- 采用 MySQL 内部自带的表分区技术,把数据分成不同的文件,能够提高磁盘的读取效率;
- 垂直分表;把一些不经常读的数据放在一张表里,节约磁盘 I/O;
- 主从分离读写;采用主从复制把数据库的读操作和写入操作分离开来;
- 分库分表分机器(数据量特别大),主要的的原理就是数据路由;
- 选择合适的表引擎,参数上的优化;如果数据表需要事务处理,应该考虑使用 InnoDB,因为它完全符合 ACID 特性。如果不需要事务处理,使用默认存储引擎 MyISAM 是比较明智的
- 进行架构级别的缓存,静态化和分布式;
- 不采用全文索引;
- 采用更快的存储方式,例如 NoSQL 存储经常访问的数据。
谈一谈你对MySQL性能优化的理解
-
硬件及操作系统层面优化
从硬件层面来说,影响 Mysql 性能的因素有,CPU、可用内存大小、磁盘读写速度、网络带宽
从操作系层面来说,应用文件句柄数、操作系统网络的配置都会影响到 Mysql 性能。
这部分的优化一般由 DBA 或者运维工程师去完成。
在硬件基础资源的优化中,我们重点应该关注服务本身承载的体量,然后提出合理的指标要求,避免出现资源浪费!
-
架构设计层面的优化
MySQL 是一个磁盘 IO 访问量非常频繁的关系型数据库
在高并发和高性能的场景中,MySQL 数据库必然会承受巨大的并发压力,而此时,我们的优化方式可以分为几个部分。
- 搭建 Mysql 主从集群,单个 Mysql 服务容易单点故障,一旦服务器宕机,将会导致依赖 Mysql 数据库的应用全部无法响应。 主从集群或者主主集群可以保证服务的高可用性。
- 读写分离设计,在读多写少的场景中,通过读写分离的方案,可以避免读写冲突导致的性能影响
- 引入分库分表机制,通过分库可以降低单个服务器节点的 IO 压力,通过分表的方式可以降低单表数据量,从而提升 sql 查询的效率。
- 针对热点数据,可以引入更为高效的分布式数据库,比如 Redis、MongoDB等,他们可以很好的缓解 Mysql 的访问压力,同时还能提升数据检索性能。
-
MySQL 程序配置优化
MySQL 是一个经过互联网大厂验证过的生产级别的成熟数据库,对于 Mysql 数据库本身的优化,一般是通过 Mysql 中的配置文件 my.cnf 来完成的,比如。
- Mysql5.7 版本默认的最大连接数是 151 个,这个值可以在 my.cnf 中修改。
- binlog 日志,默认是不开启
- 缓存池 bufferpoll 的默认大小配置等。
由于这些配置一般都和用户安装的硬件环境以及使用场景有关系,因此这些配置官方只会提供一个默认值,具体情况还得由使用者来修改。
关于配置项的修改,需要关注两个方面。
- 配置的作用域,分为会话级别和全局
- 是否支持热加载
因此,针对这两个点,我们需要注意的是:
-
全局参数的设定对于已经存在的会话无法生效
-
会话参数的设定随着会话的销毁而失效
-
全局类的统一配置建议配置在默认配置文件中,否则重启服务会导致配置失效
-
SQL 优化又能分为三步曲
-
第一、慢 SQL 的定位和排查
我们可以通过慢查询日志和慢查询日志分析工具得到有问题的 SQL 列表。
-
第二、执行计划分析
针对慢 SQL,我们可以使用关键字 explain 来查看当前 sql 的执行计划。可以重点关注 type key rows filterd 等字段 ,从而定位该 SQL 执行慢的根本原因。再有的放矢的进行优化
-
第三、使用 show profile 工具
Show Profile 是 MySQL 提供的可以用来分析当前会话中,SQL 语句资源消耗情况的工具,可用于 SQL 调优的测量。在当前会话中,默认情况下处于 show profile是关闭状态,打开之后保存最近 15 次的运行结果
针对运行慢的 SQL,通过 profile 工具进行详细分析。可以得到 SQL 执行过程中所有的资源开销情况.
如 IO 开销,CPU 开销,内存开销等
-
优化数据库?提高数据库的性能?
-
对语句的优化
- 用程序中,保证在实现功能的基础上,尽量减少对数据库的访问次数;
- 通过搜索参数,尽量减少对表的访问行数,最小化结果集,从而减轻网络负担;
- 能够分开的操作尽量分开处理,提高每次的响应速度;
- 在数据窗口使用 SQL 时,尽量把使用的索引放在选择的首列;
- 算法的结构尽量简单;
-
在查询时,不要过多地使用通配符如 SELECT * FROM T1 语句,要用到几列就选择几列如:SELECT COL1,COL2 FROM T1;
- 在可能的情况下尽量限制尽量结果集行数如:SELECT TOP 300 COL1,COL2,COL3 FROM T1,因为某些情况下用户是不需要那么多的数据;
- 不要在应用中使用数据库游标,游标是非常有用的工具,但比使用常规的、面向集的 SQL 语句需要更大的开销;
- 按照特定顺序提取数据的查找。
-
避免使用不兼容的数据类型
例如 float 和 int、char 和 varchar、binary 和 varbinary 是不兼容的。
数据类型的不兼容可能使优化器无法执行一些本来可以进行的优化操作。
例如:
SELECT name FROM employee WHERE salary > 60000
在这条语句中,如 salary 字段是 money 型的,则优化器很难对其进行优化,因为 60000 是个整型
-
不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
-
避免使用!=或<>、IS NULL 或 IS NOT NULL、IN ,NOT IN 等这样的操作符,否则将导致引擎放弃使用索引而进行全表扫描
-
尽量使用数字型字段。
-
合理使用 EXISTS,NOT EXISTS 子句。很多时候可考虑用 exists 代替 in。
-
尽量避免在索引过的字符数据中,使用非打头字母搜索。
-
充分利用连接条件
-
消除对大型表行数据的顺序存取
-
避免困难的正规表达式
-
使用视图加速查询
-
能够用 BETWEEN 的就不要用 IN
-
能够用DISTINCT 的就不用 GROUP BY
-
充分利用索引
-
能用 UNION ALL 就不要用 UNION
-
尽量不要用 SELECT INTO 语句
-
必要时强制查询优化器使用某个索引
-
对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
-
应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描
-
对于多张大数据量(这里几百条就算大了)的表 JOIN,要先分页再 JOIN,否则逻辑读会很高,性能很差。
-
使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用。
-
尽量使用表变量来代替临时表。
-
避免频繁创建和删除临时表,以减少系统表资源的消耗。
-
尽量避免使用游标,因为游标的效率较差。
-
在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF。
-
尽量避免大事务操作,提高系统并发能力。
-
尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
-
分表分库,主从
-
虽然 UPDATE、DELETE 语句的写法基本固定,但是还是对 UPDATE 语句给点建议:
- 尽量不要修改主键字段。
- 当修改 VARCHAR 型字段时,尽量使用相同长度内容的值代替。
- 尽量最小化对于含有 UPDATE 触发器的表的 UPDATE 操作。Update 语句,如果只更改 1、2 个字段,不要 Update 全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志
- 避免 UPDATE 将要复制到其他数据库的列。
- 避免 UPDATE 建有很多索引的列。
- 避免 UPDATE 在 WHERE 子句条件中的列。
简单说一说drop、delete与truncate的区别
SQL中的drop、delete、truncate都表示删除,但是三者有一些差别
delete和truncate只删除表的数据不删除表的结构
速度,一般来说: drop> truncate >delete
delete语句是dml,这个操作会放到rollback segement中,事务提交之后才生效; 如果有相应的trigger,执行的时候将被触发. truncate,drop是ddl, 操作立即生效,原数据不放到rollback segment中,不能回滚. 操作不触发trigger.
什么是视图
视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,试图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。
什么是内联接、左外联接、右外联接?
内联接(Inner Join):匹配2张表中相关联的记录。
左外联接(Left Outer Join):除了匹配2张表中相关联的记录外,还会匹配左表中剩余的记录, 右表中未匹配到的字段用NULL表示。
右外联接(Right Outer Join):除了匹配2张表中相关联的记录外,还会匹配右表中剩余的记录, 左表中未匹配到的字段用NULL表示。在判定左表和右表时,要根据表名出现在Outer Join的左右 位置关系。
大表如何优化?
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
-
限定数据的范围
务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
-
读/写分离
经典的数据库拆分方案,主库负责写,从库负责读;
-
垂直分区
-
根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息, 可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
-
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大 家应该就更容易理解了。
垂直拆分的优点:可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点:主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行 Join来解决。此外,垂直分区会让事务变得更加复杂;
-
-
水平分区
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了 分布式的目的。 水平拆分可以支撑非常大的数据量。
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。
水平拆分能够支持非常大的数据量存储,应用端改造也少,但分片事务难以解决 ,跨节点Join性能较 差,逻辑复杂。《Java工程师修炼之道》的作者推荐尽量不要对数据进行分片,因为拆分会带来逻辑、 部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。
下面补充一下数据库分片的两种常见方案:
- 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
- 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
详细内容可以参考: MySQL大表优化方案: https://segmentfault.com/a/1190000006158186
Mysql 数据库如何分区、分表?
分区:就是把一张表的数据分成 N 个区块,在逻辑上看最终只是一张表,但底层是由 N 个物理区块组成的
分表:就是把一张表按一定的规则分解成 N 个具有独立存储空间的实体表。系统读写时需要根据定义好的规则得到对应的字表明,然后操作它
分表可以通过三种方式:Mysql 集群、自定义规则(根据一定的算法(如用 hash 的方式,也可以用求余(取模)的方式)让用户访问不同的表)和 merge 存储引擎。
分区有四类:
- RANGE 分区:基于属于一个给定连续区间的列值,把多行分配给分区。
- LIST 分区:类似于按 RANGE 分区,区别在于 LIST 分区是基于列值匹配一个离散值集合中的某个值来进行选择。
- HASH 分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含 MySQL 中有效的、产生非负整数值的任何表达式。
- KEY 分区:类似于按 HASH 分区,区别在于 KEY 分区只支持计算一列或多列,且 MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值。
什么时候考虑使用分区、分表?
- 一张表的查询速度已经慢到影响使用的时候
- sql 经过优化
- 数据量大
- 表中的数据是分段的
- 对数据的操作往往只涉及一部分数据,而不是所有的数据
分区解决的问题:
- 主要可以提升查询效率
什么时候考虑分表
- 一张表的查询速度已经慢到影响使用的时候
- sql 经过优化
- 数据量大
- 当频繁插入或者联合查询时,速度变慢
分表解决的问题:
- 分表后,单表的并发能力提高了,磁盘 I/O 性能也提高了,写操作效率提高了
- 查询一次的时间短了
- 数据分布在不同的文件,磁盘 I/O 性能提高
- 读写锁影响的数据量变小
- 插入数据库需要重新建立索引的数据减少
分库分表之后,id 主键如何处理?
因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。
生成全局 id 有下面这几种方式:
- UUID:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
- 数据库自增 id : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成 的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
- 利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
- Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
- 美团的Leaf分布式ID生成系统 :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、 Zookeeper等中间件。感觉还不错。美团技术团队的一篇文章:https://tech.meituan.com/2017/ 04/21/mt-leaf.html 。
mysql有关权限的表都有哪几个
MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容:
- user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。
- db权限表:记录各个帐号在各个数据库上的操作权限。
- table_priv权限表:记录数据表级的操作权限。
- columns_priv权限表:记录数据列级的操作权限。
- host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受 GRANT和REVOKE语句的影响。
Mysql 集群的优缺点?
优点:
- 99.999%的高可用性
- 快速的自动失效切换
- 灵活的分布式体系结构,没有单点故障
- 高吞吐量和低延迟
- 可扩展性强,支持在线扩容
缺点:
- 存在很多限制,比如:不支持外键
- 部署、管理、配置很复杂
- 占用磁盘空间大、内存大
- 备份和恢复不方便
- 重启的时候,数据节点将数据 load 到内存需要很长的时间
MySQL 集群方案
-
Repliaction 集群方案
特性:速度快、弱一致性、低价值、日志、新闻、帖子
-
PXC 集群方案(Percona XtraDB Cluster)
特性:速度慢、强一致性、高价值、订单、账户、财务
PXC 方案和 Replication 方案对比:
-
先看看 PXC 方案:很明显 PXC 方案在任何一个节点写入的数据都会同步到其他节点,数据双向同步的(在任何节点上都可以同时读写)。
-
Replication 集群方案:Replication 方案只能在 Master 数据库进行写操作,在 Slave 数据库进行读操作。如果在 Slave 数据库中写入数据,Master 数据库是不能知道的(单向同步的)。
-
PXC 数据的强一致性。PXC 采用同步复制,事务在所有集群节点要么同时提交,要么不提交。Replication 采用异步复制,无法保证数据的一致性。
-
下面看看 PXC 写入操作。当一个写入请求到达 PXC 集群中的一个 MySQL(node1 数据库) 数据库时,node1数据库会将该写入请求同步给集群中的其他所有数据库,等待所有数据库都成功提交事务后,node1 节点才会将写入成功的结果告诉给 node1 的客户端。PXC 的强一致性对保存高价值数据时特别重要。再看 Replication 集群写入操作:当一个写入请求到达 Master 数据库时,Master 数据库执行写入操作,然后 Master向客户端返回写入成功,同时异步的复制写入操作给 Slave 数据库,如果异步复制时出现问题,从数据库将无法执行写入操作,而客户端得到的是写入成功。这也是弱一致性的体现。
存储过程和函数的区别?
相同点:
-
存储过程和函数都是为了可重复的执行操作数据库的 sql 语句的集合。
-
存储过程和函数都是一次编译,就会被缓存起来,下次使用就直接命中已经编译好的 sql 语句,不需要重复使用。减少网络交互,减少网络访问流量。
不同点:
-
标识符不同,函数的标识符是 function,存储过程是 proceduce。
-
函数中有返回值,且必须有返回值,而过程没有返回值,但是可以通过设置参数类型(in,out)来实现多个参数或者返回值。
-
存储函数使用 select 调用,存储过程需要使用 call 调用。
-
select 语句可以在存储过程中调用,但是除了 select..into 之外的 select 语句都不能在函数中使用。
-
通过 in out 参数,过程相关函数更加灵活,可以返回多个结果。
Mysql 日志
-
错误日志:记录启动,运行或者停止 mysql 时出现的问题;
-
通用日志:记录建立的客户端连接和执行的语句;
-
二进制日志:记录所有更改数据的语句;
-
慢查询日志:记录所有执行时间超过 long_query_time 秒的查询或者不适用索引的查询)
通过使用--slow_query_log[={0|1}]选项来启用慢查询日志,所有执行时间超多 long_query_time 的语句都会被记录。
Mysql 数据库的操作?
-
修改表-修改字段,重命名版:
alter table 表名 change 原名 新名 类型及约束;
alter table students change birthday birth datetime not null;
-
修改表-修改字段,不重名版本:
alter table 表名 modify 列名 类型和约束;
alter table students modify birth date not null
-
全列插入:
insert into 表名 values(...)
insert into students values(0,"郭靖", 1,"内蒙","2017-6");
-
部分插入:
值的顺序与给出的列顺序对应
insert into students(name, birthday) values("黄蓉","2017-8");
-
修改:
update 表名 set 列 1=值 1,列 2=值 2, ... where
update students set gender=0, homwtown="古墓", where id = 5;
-
备份:
mysqldump -uroot -p 数据库名 > python.sql
-
恢复:
mysql -u root -p 数据库名 < python.sql
Mysql 数据库中怎么实现分页?
select * from table limit (start-1)*limit,limit; 其中 start 是页码,limit 是每页显示的条数。
提取数据库中倒数 10 条数据?
select top (10) * from table1 order by id desc。
MongoDB
Python 中调用 mongo 数据库的包叫什么?
Pymongo
MongoDB
MongoDB 是一个面向文档的数据库系统。使用 C++编写,不支持 SQL,但有自己功能强大的查询语法。
MongoDB 使用 BSON 作为数据存储和传输的格式。BSON 是一种类似 JSON 的二进制序列化文档,支持嵌套对象和数组。
MongoDB 很像 MySQL,document 对应 MySQL 的 row,collection 对应 MySQL 的 table
应用场景:
- 网站数据:mongo 非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性。
- 缓存:由于性能很高,mongo 也适合作为信息基础设施的缓存层。在系统重启之后,由 mongo 搭建的持久化缓存可以避免下层的数据源过载。
- 大尺寸、低价值的数据:使用传统的关系数据库存储一些数据时可能会比较贵,在此之前,很多程序员往往会选择传统的文件进行存储。
- 高伸缩性的场景:mongo 非常适合由数十或者数百台服务器组成的数据库。
- 用于对象及 JSON 数据的存储:mongo 的 BSON 数据格式非常适合文档格式化的存储及查询。
- 重要数据:mysql,一般数据:mongodb,临时数据:memcached
- 对于关系数据表而言,mongodb 是提供了一个更快速的视图 view;而对于 PHP 程序而言,mongodb 可以作为一个持久化的数组来使用,并且这个持久化的数组还可以支持排序、条件、限制等功能。
- 将 mongodb 代替 mysql 的部分功能,主要一个思考点就是:把 mongodb 当作 mysql的一个 view(视图),view 是将表数据整合成业务数据的关键。比如说对原始数据进行报表,那么就要先把原始数据统计后生成 view,在对 view 进行查询和报表。
不适合的场景:
-
高度事物性的系统:例如银行或会计系统。传统的关系型数据库目前还是更适用于需要大量原子性复杂事务的应用程序。
-
传统的商业智能应用:针对特定问题的 BI 数据库会对产生高度优化的查询方式。对于此类应用,数据仓库可能是更合适的选择。
-
需要 SQL 的问题
-
重要数据,关系数据
优点:
- 弱一致性(最终一致),更能保证用户的访问速度
- 文档结构的存储方式,能够更便捷的获取数
- 内置 GridFS,高效存储二进制大对象 (比如照片和视频)
- 支持复制集、主备、互为主备、自动分片等特性
- 动态查询
- 全索引支持,扩展到内部对象和内嵌数组
缺点:
- 不支持事务
- MongoDB 占用空间过大,维护工具不够成熟
MongoDB 成为优秀的 NoSQL 数据库的原因是什么?
- 面向文件的
- 高性能
- 高可用性
- 易扩展性
- 丰富的查询语言
- 可分片
- 对数据存储友好
分析器在 MongoDB 中的作用是什么?
MongoDB 中包括了一个可以显示数据库中每个操作性能特点的数据库分析器。通过这个分析器你可以找到比预期慢的查询(或写操作);利用这一信息,比如,可以确定是否需要添加索引。
怎么查看 MongoDB 正在使用的链接?
db._adminCommand("connPoolStats");
MySQL 与 MongoDB 本质之间最基本的差别是什么
差别在多方面,例如:数据的表示、查询、关系、事务、模式的设计和定义、速度和性能。
MongoDB 是由 C++语言编写的,是一个基于分布式文件存储的开源数据库系统。在高负载的情况下,添加更多的节点,可以保证服务器性能。
MongoDB 旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
MongoDB 是一个面向文档的数据库,目前由 10gen 开发并维护,它的功能丰富齐全,所以完全可以替代MySQL。
与 MySQL 等关系型数据库相比,MongoDB 的优点如下:
- 弱一致性,更能保证用户的访问速度。
- 文档结构的存储方式,能够更便捷的获取数据。
- 内置 GridFS,支持大容量的存储。
- 内置 Sharding。
- 第三方支持丰富。(这是与其他的 NoSQL 相比,MongoDB 也具有的优势)。
- 性能优越。
MongoDB 本身它还算比较年轻的一个产品,所以它的问题,就是成熟度肯定没有传统 MySQL那么成熟稳定。所以在使用的时候
第一,尽量使用稳定版,不要在线上使用开发版,这是一个大原则;
另外一点,备份很重要,MongoDB 如果出现一些异常情况,备份一定是要能跟上。除了通过传统的复制的方式来做备份,离线备份也还是要有,不管你是用什么方式,都要有一个完整的离线备份。往往最后出现了特殊情况,它能帮助到你;
另外,MongoDB 性能的一个关键点就是索引,索引是不是能有比较好的使用效率,索引是不是能够放在内存中,这样能够提升随机读写的性能。如果你的索引不能完全放在内存中,一旦出现随机读写比较高的时候,它就会频繁地进行磁盘交换,这个时候,MongoDB 的性能就会急剧下降,会出现波动。
另外,MongoDB 还有一个最大的缺点,就是它占用的空间很大,因为它属于典型空间换时间原则的类型。那么它的磁盘空间比普通数据库会浪费一些,而且到目前为止它还没有实现在线压缩功能,在 MongoDB 中频繁的进行数据增删改时,如果记录变了,例如数据大小发生了变化,这时候容易产生一些数据碎片,出现碎片引发的结果,一个是索引会出现性能问题,另外一个就是在一定的时间后,所占空间会莫明其妙地增大,所以要定期把数据库做修复,定期重新做索引,这样会提升 MongoDB 的稳定性和效率。在最新的版本里,它已经在实现在线压缩,估计应该在 2.0 版左右,应该能够实现在线压缩,可以在后台执行现在 repair DataBase 的一些操作。如果那样,就解决了目前困扰我们的大问题。
Redis
Redis持久化机制
Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出, 内存释放。
RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当 Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。 当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
Redis 基本类型、相关方法
Redis 支持五种数据类型:string(字符串)、hash(哈希)、list(列表 )、set(集合)及 zset(sorted set:有序集合)。
-
String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
常用命令: set,get,decr,incr,mget 等。
-
Hash
Hash 类型可以看成是一个 key/value 都是 String 的 Map 容器。在做单点登录的时候, 就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
常用命令:hget,hset,hgetall 等。
-
List
List 用于存储一个有序的字符串列表,常用的操作是向队列两端添加元素或者获得列表的某一片段。使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基 于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。
常用命令:lpush,rpush,lpop,rpop,lrange 等
-
Set
Set 可以理解为一组无序的字符集合,Set 中相同的元素是不会重复出现的,相同的元素只保留一个。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。 另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
常用命令:sadd,spop,smembers,sunion 等。
-
Sorted Set(有序集合)
有序集合是在集合的基础上为每一个元素关联一个分数,Redis 通过分数为集合中的成员进行排序。
常用命令:zadd,zrange,zrem,zcard 等。
Redis 中 list 底层实现有哪几种?有什么区别?
列表对象的编码可以是 ziplist 或者 linkedlist
ziplist 是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用 ziplist 存储。但当数据量过大时就 ziplist 就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是 O(N),即每次插入都会重新进行 realloc。如下图所示,对象结构中ptr所指向的就是一个 ziplist。整个 ziplist只需要 malloc 一次,它们在内存中是一块连续的区域。
linkedlist 是一种双向链表。它的结构比较简单,节点中存放 pre 和 next 两个指针,还有节点相关的信息。当每增加一个 node 的时候,就需要重新 malloc 一块内存。
更多详细内容请见:https://blog.csdn.net/caishenfans/article/details/44784131
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
-
缓存雪崩
我们可以简单的理解为:由于原有缓存失效,新缓存未到期间 (例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
解决办法: 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就是将缓 存失效时间分散开。
-
缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决办法: 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
- Bitmap: 典型的就是哈希表。缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。
- 布隆过滤器(推荐): 就是引入了k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。 Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突, 我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这 便是Bloom-Filter的基本思想。 Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
-
缓存预热
缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
- 直接写个缓存刷新页面,上线时手工操作下;
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
-
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6种策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
- 定时去清理过期的缓存;
- 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
-
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级, 并发送告警;
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
热点数据和冷数据是什么
热点数据,缓存才有价值
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存。
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。 再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
Redis 的并发竞争问题怎么解决?
方案一:可以使用独占锁的方式,类似操作系统的 mutex 机制,不过实现相对复杂,成本较高。
https://blog.csdn.net/black_ox/article/details/48972085
方案二:使用乐观锁的方式进行解决(成本较低,非阻塞,性能较高),本质上是假设不会进行冲突,使用 redis 的命令 watch 进行构造条件
方案三:利用 Redis 自带的 incr 命令,具体用法看这里http://doc.Redisfans.com/string/incr.html
方案四:这个是针对客户端来的,在代码里要对 Redis 操作的时候,针对同一 key 的资源,就先进行加锁
方案五:利用 Redis 的 setnx 实现内置的锁。
Redis 集群?
Redis 集群的类型:
主从复制
-
哨兵模式
-
Redis-Cluster 集群
-
Redis 集群的优点:
-
将数据自动切分(split)到多个节点的能力。
-
当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力。
-
-
Redis 集群搭建
Redis 集群是一个可以在多个 Redis 节点之间进行数据共享的设施(installation)。
Redis 集群不支持那些需要同时处理多个键的 Redis 命令, 因为执行这些命令需要
在多个 Redis 节点之间移动数据, 并且在高负载的情况下, 这些命令将降低 Redis 集群的性能, 并导致不可预测的行为。
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
Reds 集群的特性
Redis 集群数据共享
Redis 集群中的主从复制
Redis 集群的数据一致性
需要结合真实案例去分析,这里给大家推荐一个不错的博客。https://blog.csdn.net/yfkiss/article/details/38944179
Redis 的事务?
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
- redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
- 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
- 如果在一个事务中出现运行错误,那么正确的命令会被执行。
- MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
- EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 null 。
- 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
- WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
Redis 的使用场景有哪些?
- 取最新 N 个数据的操作
- 排行榜应用,取 TOP N 操作
- 需要精准设定过期时间的应用
- 计数器应用
- uniq 操作,获取某段时间所有数据排重值
- Pub/Sub 构建实时消息系统
- 构建队列系统
- 缓存
Redis 默认端口,默认过期时间,Value 最多可以容纳的数据长度?
默认端口:6379
默认过期时间:可以说永不过期,一般情况下,当配置中开启了超出最大内存限制就写磁盘的话,那么没有设置过期时间的 key 可能会被写到磁盘上。假如没设置,那么 REDIS 将使用LRU 机制,将内存中的老数据删除,并写入新数据。
Value 最多可以容纳的数据长度是:512M。
Redis 有多少个库?
Redis 一个实例下有 16 个。
Redis 为什么是单线程的
官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问
-
绝大部分请求是纯粹的内存操作(非常快速)
-
采用单线程,避免了不必要的上下文切换和竞争条件
-
非阻塞IO优点:
-
速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
-
支持丰富数据类型,支持string,list,set,sorted set,hash
-
支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发竞争key问题
同时有多个子系统去set一个key。这个时候要注意什么呢? 不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候, 这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。
- 如果对这个key操作,不要求顺序: 准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可
- 如果对这个key操作,要求顺序: 分布式锁+时间戳。 假设这会系统B先抢到锁,将key1设置为 {valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。
-
利用队列,将set方法变成串行访问也可以。redis遇到高并发,如果保证读写key的一致性对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内部已经帮你处理好并发的问题了。
-
单线程的redis为什么这么快
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 采用了非阻塞I/O多路复用机制
redis的过期策略以及内存淘汰机制
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除:用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。 在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样, redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置
maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
Redis 常见性能问题和解决方案?
- Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
- 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3…
为什么Redis的操作是原子性的,怎么保证原子性的?
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua的方式实现.
其他
怎样解决数据库高并发的问题?
解决数据库高并发的常见方案:
-
缓存式的 Web 应用程序架构:
在 Web 层和 DB(数据库)层之间加一层 cache 层,主要目的:减少数据库读取负担,提高数据读取速度。cache 存取的媒介是内存,可以考虑采用分布式的 cache 层,这样更容易破除内存容量的限制,同时增加了灵活性。
-
增加 Redis 缓存数据库:
-
增加数据库索引
-
页面静态化
效率最高、消耗最小的就是纯静态化的 html 页面,所以我们尽可能使我们的网站上的页面采用静态页面来实现,这个最简单的方法其实也是最有效的方法。用户可以直接获取页面,不用像 MVC结构走那么多流程,比较适用于页面信息大量被前台程序调用,但是更新频率很小的情况。
-
使用存储过程
处理一次请求需要多次访问数据库的操作,可以把操作整合到储存过程,这样只要一次数据库访问就可以了
-
MySQL 主从读写分离
当数据库的写压力增加,cache 层(如 Memcached)只能缓解数据库的读取压力。读写集中在一个数据库上让数据库不堪重负。使用主从复制技术(master-slave 模式)来达到读写分离,以提高读写性能和读库的可扩展性。读写分离就是只在主服务器上写,只在从服务器上读,基本原理是让主数据库处理事务性查询,而从数据库处理 select 查询,数据库复制被用于把事务性查询(增删改)导致的改变更新同步到集群中的从数据库。
MySQL 读写分离提升系统性能:
- 主从只负责各自的读和写,极大程度缓解 X 锁和 S 锁争用。
- slave 可以配置 MyISAM 引擎,提升查询性能以及节约系统开销。
- master 直接写是并发的,slave 通过主库发送来的 binlog 恢复数据是异步的。
- slave 可以单独设置一些参数来提升其读的性能。
- 增加冗余,提高可用性。
实现主从分离可以使用 MySQL 中间件如:Atlas
-
分表分库
在 cache 层的高速缓存,MySQL 的主从复制,读写分离的基础上,这时 MySQL 主库的写压力开始出现瓶颈,而数据量的持续猛增,由于 MyISAM 使用表锁,在高并发下会出现严重的锁问题,大量的高并发 MySQL 应用开始使用 InnoDB 引擎代替 MyISAM。采用 Master-Slave 复制模式的 MySQL 架构,只能对数据库的读进行扩展,而对数据的写操作还是集中在 Master 上。这时需要对数据库的吞吐能力进一步地扩展,以满足高并发访问与海量数据存储的需求。
对于访问极为频繁且数据量巨大的单表来说,首先要做的是减少单表的记录条数,以便减少数据查询所需的时间,提高数据库的吞吐,这就是所谓的分表【水平拆分】。在分表之前,首先需要选择适当的分表策略(尽量避免分出来的多表关联查询),使得数据能够较为均衡地分布到多张表中,并且不影响正常的查询。
分表能够解决单表数据量过大带来的查询效率下降的问题,但是却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问,当数据库 master 服务器无法承载写操作压力时,不管如何扩展 Slave 服务器都是没有意义的,对数据库进行拆分,从而提高数据库写入能力,即分库【垂直拆分】。
-
负载均衡集群
将大量的并发请求分担到多个处理节点。由于单个处理节点的故障不影响整个服务,负载均衡集群同时也实现了高可用性。
负载均衡将是大型网站解决高负荷访问和大量并发请求采用的终极解决办法。
NoSQL 和关系数据库的区别?
- SQL 数据存在特定结构的表中;而 NoSQL 则更加灵活和可扩展,存储方式可以是 JSON 文档、哈希表或者其他方式。
- 在 SQL 中,必须定义好表和字段结构后才能添加数据,例如定义表的主键(primary key),索引(index),触发器(trigger),存储过程(stored procedure)等。表结构可以在被定义之后更新,但是如果有比较大的结构变更的话就会变得比较复杂。在 NoSQL 中,数据可以在任何时候任何地方添加,不需要先定义表。
- SQL 中如果需要增加外部关联数据的话,规范化做法是在原表中增加一个外键,关联外部数据表。而在 NoSQL 中除了这种规范化的外部数据表做法以外,我们还能用如下的非规范化方式把外部数据直接放到原数据集中,以提高查询效率。缺点也比较明显,更新审核人数据的时候将会比较麻烦。
- SQL 中可以使用 JOIN 表链接方式将多个关系数据表中的数据用一条简单的查询语句查询出来。NoSQL 暂未提供类似 JOIN 的查询方式对多个数据集中的数据做查询。所以大部分 NoSQL 使用非规范化的数据存储方式存储数据。
- SQL 中不允许删除已经被使用的外部数据,而 NoSQL 中则没有这种强耦合的概念,可以随时删除任何数据。
- SQL 中如果多张表数据需要同批次被更新,即如果其中一张表更新失败的话其他表也不能更新成功。这种场景可以通过事务来控制,可以在所有命令完成后再统一提交事务。而 NoSQL 中没有事务这个概念,每一个数据集的操作都是原子级的。
- 在相同水平的系统设计的前提下,因为 NoSQL 中省略了 JOIN 查询的消耗,故理论上性能上是优于 SQL 的。
使用最多的数据库(Mysql,Mongodb,redis 等),对他们的理解?
MySQL 数据库:开源免费的关系型数据库,需要实现创建数据库、数据表和表的字段,表与表之间可以进行关联(一对多、多对多),是持久化存储。
Mongodb 数据库:是非关系型数据库,数据库的三元素是,数据库、集合、文档,可以进行持久化存储,也可作为内存数据库,存储数据不需要事先设定格式,数据以键值对的形式存储。
redis 数据库:非关系型数据库,使用前可以不用设置格式,以键值对的方式保存,文件格式相对自由,主要用于缓存数据库,也可以进行持久化存储。
MySQL 和 Redis 高可用性体现在哪些方面?
-
MySQL Replication 是 MySQL 官方提供的主从同步方案,用于将一个 MySQL 实例的数据,同步到另一个实例中。Replication 为保证数据安全做了重要的保证,也是现在运用最广的 MySQL容灾方案。Replication用两个或以上的实例搭建了 MySQL主从复制集群,提供单点写入,多点读取的服务,实现了读的 scale out。
-
Sentinel 是 Redis 官方为集群提供的高可用解决方案。在实际项目中可以使用 sentinel去做 Redis 自动故障转移,减少人工介入的工作量。另外 sentinel 也给客户端提供了监控消息的通知,这样客户端就可根据消息类型去判断服务器的状态,去做对应的适配操作。
下面是 Sentinel 主要功能列表:
- Monitoring:Sentinel 持续检查集群中的 master、slave 状态,判断是否存活。
- Notification:在发现某个 Redis 实例死的情况下,Sentinel 能通过 API 通知系统管理员或其他程序脚本。
- Automatic failover:如果一个 master 挂掉后,sentinel 立马启动故障转移,把某个 slave 提升为 master。其他的 slave 重新配置指向新 master。
- Configuration provider:对于客户端来说 sentinel 通知是有效可信赖的。客户端会连接 sentinel 去请求当前 master 的地址,一旦发生故障 sentinel 会提供新地址给客户端。
Redis 和 Mysql 如何保证数据一致性
一般情况下,Redis 用来实现应用和数据库之间读操作的缓存层,主要目的是减少数据库 IO,还可以提升数据的 IO 性能。
这是它的整体架构:
当应用程序需要去读取某个数据的时候,首先会先尝试去 Redis 里面加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到 Redis 里面。
在这样一个架构中,会出现一个问题,就是一份数据,同时保存在数据库和 Redis里面,当数据发生变化的时候,需要同时更新 Redis 和 Mysql,由于更新是有先后顺序的,并且它不像 Mysql 中的多表事务操作,可以满足 ACID 特性。所以就会出现数据一致性问题。
在这种情况下,能够选择的方法只有几种。
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis 中的数据不一致。
如果是先删除缓存,再更新数据库,理想情况是应用下次访问 Redis 的时候,发现 Redis 里面的数据是空的,就从数据库加载保存到 Redis 里面,那么数据是一致的。但是在极端情况下,由于删除 Redis 和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。
所以,如果需要在极端情况下仍然保证 Redis 和 Mysql 的数据一致性,就只能采用最终一致性方案。
比如基于 RocketMQ 的可靠性消息通信,来实现最终一致性
还可以直接通过 Canal 组件,监控 Mysql 中 binlog 的日志,把更新后的数据同步到 Redis 里面。
因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致性,那就不能使用这个方案来做。
Redis 和 MongoDB 的优缺点
MongoDB 和 Redis 都是 NoSQL,采用结构型数据存储。二者在使用场景中,存在一定的区别,这也主要由于二者在内存映射的处理过程,持久化的处理方法不同。MongoDB 建议集群部署,更多的考虑到集群方案,Redis 更偏重于进程顺序写入,虽然支持集群,也仅限于主-从模式。
Redis 优点:
- 读写性能优异
- 支持数据持久化,支持 AOF 和 RDB 两种持久化方式
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
- 数据结构丰富:数据结构丰富:支持 string、hash、set、sortedset、list 等数据结构。
缺点:
- Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP 才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了系统的可用性。
- Redis的主从复制采用全量复制,复制过程中主机会 fork出一个子进程对内存做一份快照,并将子进程的内存快照保存为文件发送给从机,这一过程需要确保主机有足够多的空余内存。若快照文件较大,对集群的服务能力会产生较大的影响,而且复制过程是在从机新加入集群或者从机和主机网络断开重连时都会进行,也就是网络波动都会造成主机和从机间的一次全量的数据复制,这对实际的系统运营造成了不小的麻烦。
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
MongoDB 优点:
- 弱一致性(最终一致),更能保证用户的访问速度
- 文档结构的存储方式,能够更便捷的获取数
- 内置 GridFS,高效存储二进制大对象 (比如照片和视频)
- 支持复制集、主备、互为主备、自动分片等特性
- 动态查询
- 全索引支持,扩展到内部对象和内嵌数组
缺点:
- 不支持事务
- MongoDB 占用空间过大
- 维护工具不够成熟
Memcached与Redis的区别都有哪些?
- 存储方式 Memecached把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis 有部份存在硬盘上,redis可以持久化其数据
- 数据支持类型 memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 ,提供list,set,zset,hash等数据结构的存储
- 使用底层模型不同。它们之间底层实现方式以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- value 值大小不同:Redis 最大可以达到 1gb;memcached 只有 1mb。
- redis的速度比memcached快很多
- Redis支持数据的备份,即master-slave模式的数据备份。
数据库负载均衡
负载均衡集群是由一组相互独立的计算机系统构成,通过常规网络或专用网络进行连接,由路由器衔接在一起,各节点相互协作、共同负载、均衡压力,对客户端来说,整个群集可以视为一台具有超高性能的独立服务器。
实现原理
实现数据库的负载均衡技术,首先要有一个可以控制连接数据库的控制端。在这里,它截断了数据库和程序的直接连接,由所有的程序来访问这个中间层,然后再由中间层来访问数据库。这样,我们就可以具体控制访问某个数据库了,然后还可以根据数据库的当前负载采取有效的均衡策略,来调整每次连接到哪个数据库。
实现多据库数据同步
对于负载均衡,最重要的就是所有服务器的数据都是实时同步的。这是一个集群所必需的,因为,如果数据不实时同步,那么用户从一台服务器读出的数据,就有别于从另一台服务器读出的数据,这是不能允许的。所以必须实现数据库的数据同步。这样,在查询的时候就可以有多个资源,实现均衡。比较常用的方法是 Moebius for SQL Server 集群,Moebius for SQL Server 集群采用将核心程序驻留在每个机器的数据库中的办法,这个核心程序称为 Moebius for SQL Server中间件,主要作用是监测数据库内数据的变化并将变化的数据同步到其他数据库中。数据同步完成后客户端才会得到响应,同步过程是并发完成的,所以同步到多个数据库和同步到一个数据库的时间基本相等;另外同步的过程是在事务的环境下完成的,保证了多份数据在任何时刻数据的一致性。正因为 Moebius 中间件宿主在数据库中的创新,让中间件不但能知道数据的变化,而且知道引起数据变化的 SQL 语句,根据 SQL 语句的类型智能的采取不同的数据同步的策略以保证数据同步成本的最小化。
数据条数很少,数据内容也不大,则直接同步数据。数据条数很少,但是里面包含大数据类型,比如文本,二进制数据等,则先对数据进行压缩然后再同步,从而减少网络带宽的占用和传输所用的时间。
数据条数很多,此时中间件会拿到造成数据变化的 SQL 语句, 然后对 SQL 语句进行解析,分析其执行计划和执行成本,并选择是同步数据还是同步 SQL 语句到其他的数据库中。此种情况应用在对表结构进行调整或者批量更改数据的时候非常有用。
优缺点
优点:
- 扩展性强:当系统要更高数据库处理速度时,只要简单地增加数据库服务器就可以得到扩展。
- 可维护性:当某节点发生故障时,系统会自动检测故障并转移故障节点的应用,保证数据库的持续工作。
- 安全性:因为数据会同步的多台服务器上,可以实现数据集的冗余,通过多份数据来保证安全性。另外它成功地将数据库放到了内网之中,更好地保护了数据库的安全性。
- 易用性:对应用来说完全透明,集群暴露出来的就是一个 IP。
缺点:
- 不能够按照 Web 服务器的处理能力分配负载。
- 负载均衡器(控制端)故障,会导致整个数据库系统瘫痪。
Sql 注入是如何产生的,如何防止?
程序开发过程中不注意规范书写 sql 语句和对特殊字符进行过滤,导致客户端可以通过全局变量
POST 和 GET 提交一些 sql 语句正常执行。产生 Sql 注入。下面是防止办法:
- 过滤掉一些常见的数据库操作关键字,或者通过系统函数来进行过滤。
- SQL 语句书写的时候尽量不要省略小引号(tab 键上面那个)和单引号
- 提高数据库命名技巧,对于一些重要的字段根据程序的特点命名,取不易被猜到的
- 对于常用的方法加以封装,避免直接暴漏 SQL 语句
- 打开 magic_quotes_gpc 来防止 SQL 注入
- 控制错误信息:关闭错误提示信息,将错误信息写到系统日志。
- 使用 mysqli 或 pdo 预处理。
数据库的设计?
第一范式:数据库表的每一列都是不可分割的原子数据项,即列不可拆分。
第二范式:建立在第一范式的基础上,要求数据库表中的每个实例或记录必须是可以唯一被区分的,即唯一标识。
第三范式:建立在第二范式的基础上,任何非主属性不依赖与其他非主属性,即引用主键。
BC范式:是指对于关系模式R,若 R为第一范式,且每个属性都不部分依赖于候选键也不传递依赖于候选键,则R称之为BC范式。
数据库的一些基本操作命令(列举一些常用命令即可)?
MySQL 的常见命令如下:
- create database name; 创建数据库
- use databasename; 选择数据库
- drop database name 直接删除数据库,不提醒
- show tables; 显示表
- describe tablename; 表的详细描述
- select 中加上 distinct 去除重复字段
- mysqladmin drop databasename 删除数据库前,有提示。
- select version(),current_date; 显示当前 mysql 版本和当前日期
MongoDB 的常见命令如下:
-
db.help(); Help 查看命令提示
-
use yourDB; 切换/创建数据库
-
show dbs; 查询所有数据库
-
db.dropDatabase(); 删除当前使用数据库
-
db.getName(); 查看当前使用的数据库
-
db.version(); 当前 db 版本
-
db.addUser("name"); 添加用户
db.addUser("userName", "pwd123", true);
-
show users; 显示当前所有用户
-
db.removeUser("userName"); 删除用户
-
db.yourColl.count(); 查询当前集合的数据条数
关系型数据库中,表和表之间有左连接,内连接,外连接,分别解释下他们的含义和区别?
内连接查询:查询的结果为两个表匹配到的数据。
右接查询:查询的结果为两个表匹配到的数据,右表特有的数据,对于左表中不存在的数据使用 null填充。
左连接查询:查询的结果为两个表匹配到的数据,左表特有的数据,对于右表中不存在的数据使用 null 填充。
八、shell与自动化运维
九、测试
禅道和 qc 的区别?
都是缺陷管理工具。
-
QC
作为缺陷管理工具,QC 在缺陷管理方面,做的相对完善。提 bug 页面:填写内容可以根据测试需求,不断修改添加新的字段;以我上一家公司为例,在提 bug 过程中,有一下几个必填项:
Bug 状态(new、fixed、closed 等)、发现人员、缺陷发现阶段(测试阶段、上现阶段等)、缺陷来源(测试人员给出的 bug 定位)、Bug 分类(功能、性能等问题)、测试阶段(单元测试、集成测试、系统测试等)、归属需求、缺陷回归次数、优先级、分配给,这些必填项再加上 bug 标题和操作描述、上传附件,使很多疑问都变得清晰。
缺陷查看页面:可以根据自己需要选择要呈现的字段,相对人性化可操作,每个显示的字段都可以进行筛选,使研发人员很快能定位到属于自己的 bug,再根据 bug 状态、优先级进行筛选,使未完结的 bug 能有序并无遗漏地完成修改;页面还有注释功能,研发人员能写出针对本问题的各种感想,方便完善而又人性化。
-
禅道(开源版)
禅道涉及面非常广,但是在缺陷管理这方面,与老牌的 QC 还是略逊一筹。提 bug 页面:页面是非常清晰整洁的 web 页面,但是需要填写的字段,并没有完全覆盖开发和测试人员的全部需求。页面字段:产品模块(对应 QC 中的项目)、所属项目(对应 QC 中的需求)、影响版本(bug 所属版本?)、当前指派(修改 bug 的人员)、bug 标题、重现步骤、相关需求(页面标注了这个字段,但是什么也没有显示,并且没有可填写的位置)、相关任务、类型/严重。
编写测试计划的目的是
- 使测试工作顺利进行
- 使项目参与人员沟通更舒畅
- 使测试工作更加系统化
测试人员在软件开发过程中的任务是什么
- 寻找 Bug;
- 避免软件开发过程中的缺陷;
- 衡量软件的品质;
- 关注用户的需求。
- 总的目标是:确保软件的质量。
您以往的工作中,一条软件缺陷(或者叫 Bug)记录都包含了哪些内容?如何提交高质量的软件缺陷(Bug)记录?
一条 Bug 记录最基本应包含:编号、Bug 所属模块、Bug 描述、Bug 级别、发现日期、发现人、修改日期、修改人、修改方法、回归结果等等;要有效的发现 Bug 需参考需求以及详细设计等前期文档设计出高效的测试用例,然后严格执行测试用例,对发现的问题要充分确认肯定,然后再向外发布如此才能提高提交 Bug 的质量。
简述黑盒测试和白盒测试的优缺点
黑盒测试的优点有:
- 比较简单,不需要了解程序内部的代码及实现;
- 与软件的内部实现无关;
- 从用户角度出发,能很容易的知道用户会用到哪些功能,会遇到哪些问题;
- 基于软件开发文档,所以也能知道软件实现了文档中的哪些功能;
- 在做软件自动化测试时较为方便。
黑盒测试的缺点有:
- 不可能覆盖所有的代码,覆盖率较低,大概只能达到总代码量的 30%;
- 自动化测试的复用性较低。
白盒测试的优点有:
- 帮助软件测试人员增大代码的覆盖率,提高代码的质量,发现代码中隐藏的问题。
白盒测试的缺点有:
- 程序运行会有很多不同的路径,不可能测试所有的运行路径;测试基于代码,只能测试开发人员做的对不对,而不能知道设计的正确与否,可能会漏掉一些功能需求;系统庞大时,测试开销会非常大。
简述常用的 Bug 管理或者用例管理工具,并且描述其中一个工作流程。
常用:testlink,QC,mantis,禅道,TAPD,JIRA 。
TAPD:产品创建(需求,计划,模块)-->项目创建(PM 排期、任务分解)-->研发(编码、单元测试等)-->测试(测试计划,用例,执行,bug,报告等)。
请列出你所知道的软件测试种类,至少 5 项。
单元测试,集成测试,系统测试,验收测试。
系统测试包含:功能测试,性能测试,压力测试,兼容性测试,健壮性测试,冒烟测试,文档测试。
Alpha 测试与 Beta 测试的区别是什么?
Alpha 主要是模拟用户的操作和用户的环境。
Beta 主要验证测试,准备进入发布阶段,Beta 测试是一种验收测试。
举例说明什么是Bug?一个bug report应包含什么关键字?
比如聊天中,点击发送按钮后,无法发送消息。
标题,模块,严重程度,bug 类型,版本号,可否重现,描述,附件,日志等等。
十、人工智能
数据挖掘
1G 的文件,每一行是一个词,词的大小不超过 16 字节,内存限制大小是 1M,返回频数最高的 100 个词。
使用生成器读取文件。每次读取 65536 行,一共进行 1500 次,当读取不到内容时关闭文件。每次读取,最终要得到 100 个频数最高的词。每 500 次,进行一次合并和统计,得到最多 50000 个词,对这 50000 个词提取其中频数最高的 100 个词。最终对最多 300 个筛选出来的词,进行合并和统计,提取最终频数最高的 100 个词。
筛选出 100 个高频词的步骤:
- 统计每个词出现的次数。维护一个 Key 为词,Value 为该词出现次数的 hash 表。每次读取一个词,如果该字串不在 hash 表中,那么加入该词,并且将 Value 值设为 1;如果该字串在 hash 表中,那么将该字串的计数加一即可。
- 据每个词的引用次数进行排序。冒泡、快排等等都可以。
一个大约有一万行的文本文件,每行一个词,要求统计出其中最频繁出现的前 10 个词,请给出思想和时间复杂度分析。
用 trie 树统计每个词出现的次数,时间复杂度是 O(n*le)(le 表示单词的平均长度)。然后是找出出现最频繁的前 10 个词,可以用堆来实现,时间复杂度是 O(n*lg10)。所以总的时间复杂度,是 O(n*le)与 O(n*lg10)中较大的哪一个。参考博客:http://www.mamicode.com/info-detail-1037262.html
怎么在海量数据中找出重复次数最多的一个?
算法思想:先做 hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然
后找出上一步求出的数据中重复次数最多的一个就是所求。
参考博客一:https://blog.csdn.net/u010601183/article/details/56481868
给 2 亿个不重复的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那 2 亿个数当中?
unsigned int 的取值范围是 0 到 2^32-1。我们可以申请连续的 2^32/8=512M 的内存,用每一个 bit 对应一个 unsigned int 数字。首先将 512M 内存都初始化为 0,然后每处理一个数字就将其对应的 bit 设置为 1。当需要查询时,直接找到对应 bit,看其值是 0 还是 1 即可。
淘宝主页的商品推送是怎么按照每人喜好不同来实现的?
- 根据商品的分类或其他基础属性进行推荐(相似性推荐),对于某一个商品来说,这是一种替代性的推荐方式,也即用户不想买它的时候,还可以有其他的选择。就比如说用户正在浏览一个斯伯丁的篮球,看完描述之后发现不是自己理想的款式,规格材质不太对,但是在这个单品下方,出现了一个同类型的耐磨材质的篮球,OK!那么这个用户可能就会把这个推荐的篮球带回家。这个例子中仅仅是依据商品的分类进行推荐,当然我们还可以根据实际情况加入商品的更多基础属性进行加权取值,算最后得分来进行推荐。
- 根据商品的被动销售级属性进行推荐(相关性推荐),根据商品最终在订单中出现的概率来判断商品间的相关性,目前还可以依赖于其他几个维度来参考一同做判断(被同时浏览的几率,被同时加入购物车的几率,被同时购买的几率),以下采取订单中是否通知购买来讲解算法模型,简单解释一下就是想买 A 的人还可能想买 C
- 通过记录用户行为数据,如搜索、浏览、咨询、加购、支付、收藏、评价、分享等等进行推荐;
- 基于用户的协同过滤,通过用户对不同类型的商品的喜好度进行评分,然后根据每类商品的喜好度评分构建一个多维向量,使用余弦公式有来评测用户之间喜好度的相似性,基于此将其他相似用户非常喜欢而该用户还没有了解的产品进行推荐。这部分推荐本质上是给用户推荐其他相似用户喜欢的内容,一句话概括:和你类似的人也喜欢这些商品。
推荐系统在项目中主要会遇到冷启动问题,探索和利用问题;安全问题。
机器学习
深度学习
十一、数据结构与算法
常用的数据结构
数组、堆栈、队列、链表、树、图、字典树、哈希表
-
数组
数组(Array)大概是最简单,也是最常用的数据结构了。其他数据结构,比如栈和队列都是由数组衍生出来的。
下图展示了 1 个数组,它有 4 个元素:
每一个数组元素的位置由数字编号,称为下标或者索引(index)。大多数编程语言的数组第一个元素的下 标是 0。
根据维度区分,有 2 种不同的数组:
- 一维数组(如上图所示)
- 多维数组(数组的元素为数组)
数组的基本操作
- Insert - 在某个索引处插入元素
- Get - 读取某个索引处的元素
- Delete - 删除某个索引处的元素
- Size - 获取数组的长度
-
栈
撤回,即 Ctrl+Z,是我们最常见的操作之一,大多数应用都会支持这个功能。你知道它是怎么实现的 吗?答案是这样的:把之前的应用状态(限制个数)保存到内存中,最近的状态放到第一个。这时,我们 需要栈(stack)来实现这个功能。
栈中的元素采用 LIFO (Last In First Out),即后进先出。
下图的栈有 3 个元素,3 在最上面,因此它会被第一个移除:
栈的基本操作
- Push — 在栈的最上方插入元素
- Pop — 返回栈最上方的元素,并将其删除
- isEmpty — 查询栈是否为空
- Top — 返回栈最上方的元素,并不删除
-
队列
队列(Queue)与栈类似,都是采用线性结构存储数据。它们的区别在于,栈采用 LIFO 方式,而队列采 用先进先出,即FIFO(First in First Out)。
下图展示了一个队列,1 是最上面的元素,它会被第一个移除:
队列的基本操作
-
Enqueue — 在队列末尾插入元素
-
Dequeue — 将队列第一个元素删除
-
isEmpty — 查询队列是否为空
-
Top — 返回队列的第一个元素
-
链表
链表(Linked List)也是线性结构,它与数组看起来非常像,但是它们的内存分配方式、内部结构和插入 删除操作方式都不一样。
链表是一系列节点组成的链,每一个节点保存了数据以及指向下一个节点的指针。
链表头指针指向第一 个节点,如果链表为空,则头指针为空或者为 null。
链表可以用来实现文件系统、哈希表和邻接表。
下图展示了一个链表,它有 3 个节点:
链表分为 2 种:
- 单向链表
- 双向链表
链表的基本操作
- InsertAtEnd — 在链表结尾插入元素
- InsertAtHead — 在链表开头插入元素
- Delete — 删除链表的指定元素
- DeleteAtHead — 删除链表第一个元素
- Search — 在链表中查询指定元素
- isEmpty — 查询链表是否为空
-
图
图(graph)由多个节点(vertex)构成,节点之间阔以互相连接组成一个网络。(x, y)表示一条边(edge), 它表示节点 x 与 y 相连。边可能会有权值(weight/cost)。
图分为两种:
- 无向图
- 有向图
在编程语言中,图有可能有以下两种形式表示:
- 邻接矩阵(Adjacency Matrix)
- 邻接表(Adjacency List)
遍历图有两周算法
- 广度优先搜索(Breadth First Search)
- 深度优先搜索(Depth First Search)
-
树
树(Tree)是一个分层的数据结构,由节点和连接节点的边组成。树是一种特殊的图,它与图最大的区别 是没有循环。
树被广泛应用在人工智能和一些复杂算法中,用来提供高效的存储结构。
下图是一个简单的树以及与树相关的术语:
树有很多分类:
- N 叉树(N-ary Tree)
- 平衡树(Balanced Tree)
- 二叉树(Binary Tree)
- 二叉查找树(Binary Search Tree)
- 平衡二叉树(AVL Tree)
- 红黑树(Red Black Tree)
- 2-3 树(2–3 Tree)
其中,二叉树和二叉查找树是最常用的树。
-
前缀树
前缀树(Prefix Trees 或者 Trie)与树类似,用于处理字符串相关的问题时非常高效。它可以实现快速检 索,常用于字典中的单词查询,搜索引擎的自动补全甚至 IP 路由。
下图展示了“top”, “thus”和“their”三个单词在前缀树中如何存储的:
单词是按照字母从上往下存储,“p”, “s”和“r”节点分别表示“top”, “thus”和“their”的单词结尾。
-
哈希表
哈希(Hash)将某个对象变换为唯一标识符,该标识符通常用一个短的随机字母和数字组成的字符串来代 表。哈希可以用来实现各种数据结构,其中最常用的就是哈希表(hash table)。 哈希表通常由数组实现。
哈希表的性能取决于 3 个指标:
- 哈希函数
- 哈希表的大小
- 哈希冲突处理方式
下图展示了有数组实现的哈希表,数组的下标即为哈希值,由哈希函数计算,作为哈希表的键(key), 而数组中保存的数据即为值(value):
算法的特征?
- 有穷性: 一个算法必须保证执行有限步骤之后结束;
- 确切性: 算法的每一步骤必须有确切的定义;
- 输入:一个算法有 0 个或多个输入,以刻画运算对象的初始情况,所谓 0 个输入是指算法本身给出了初始条件;
- 输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
- 可行性: 算法原则上能够精确地运行,而且人们用笔和纸做有限次数运算后即可完成。
冒泡排序的思想?
冒泡思想:通过无序区中相邻记录的关键字间的比较和位置的交换,使关键字最小的记录像气泡一样逐渐向上漂至水面。整个算法是从最下面的记录开始,对每两个相邻的关键字进行比较,把关键字较小的记录放到关键字较大的记录的上面,经过一趟排序后,关键字最小的记录到达最上面,接着再在剩下的记录中找关键字次小的记录,把它放在第二个位置上,依次类推,一直到所有记录有序为止
复杂度:时间复杂度为 O(n2),空间复杂度为 O(1)
1. def bubble_sort(alist):
2. for j in range(len(alist)-1,0,-1):
3. # j 表示每次遍历需要比较的次数,是逐渐减小的
4. for i in range(j):
5. if alist[i] > alist[i+1]:
6. alist[i], alist[i+1] = alist[i+1], alist[i]
7.
8. li = [54,26,93,17,77,31,44,55,20]
9. bubble_sort(li)
10.print(li)
快速排序的思想?
快排的基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
复杂度:快速排序是不稳定的排序算法,最坏的时间复杂度是 O(n2), 最好的时间复杂度是(nlogn), 空间复杂度为 O(logn)
13. def quick_sort(alist, start, end):
14. """快速排序"""
15. # 递归的退出条件
16. if start >= end:
17. return
18. # 设定起始元素为要寻找位置的基准元素
19. mid = alist[start]
20. # low 为序列左边的由左向右移动的游标
21. low = start
22. # high 为序列右边的由右向左移动的游标
23. high = end
24. while low < high:
25. # 如果 low 与 high 未重合,high 指向的元素不比基准元素小,则 high 向左移动
26. while low < high and alist[high] >= mid:
27. high -= 1
28. # 将 high 指向的元素放到 low 的位置上
29. alist[low] = alist[high]
30. # 如果 low 与 high 未重合,low 指向的元素比基准元素小,则 low 向右移动
31. while low < high and alist[low] < mid:
32. low += 1
33. # 将 low 指向的元素放到 high 的位置上
34. alist[high] = alist[low]
35. # 退出循环后,low 与 high 重合,此时所指位置为基准元素的正确位置
36. # 将基准元素放到该位置
37. alist[low] = mid
38. # 对基准元素左边的子序列进行快速排序
39. quick_sort(alist, start, low-1)
40. # 对基准元素右边的子序列进行快速排序
41. quick_sort(alist, low+1, end)
42. alist = [54,26,93,17,77,31,44,55,20]
43. quick_sort(alist,0,len(alist)-1)
44. print(alist)
如何判断单向链表中是否有环?
首先遍历链表,寻找是否有相同地址,借此判断链表中是否有环。如果程序进入死循环,则需要一块空间来存储指针,遍历新指针时将其和储存的旧指针比对,若有相同指针,则该链表有环,否则将这个新指针存下来后继续往下读取,直到遇见 NULL,这说明这个链表无环。
基础的数据结构有哪些?
基本的算法有: 排序算法(冒泡排序,插入排序,快速排序,归并排序),查找(二分查找),搜索((DFS) 深度优先搜索,(BFS)广度优先搜索),(Dijkstra 算法),动态规划算法,分类(朴素贝叶斯分类算 法等)。对于其他基本数据结构, 栈, 队列,树,都有一些基本的操作。
评价算法的好坏一般有两种: 时间复杂度和空间复杂度。
时间复杂度:同样的输入规模(问题规模)花费多少时间。
空间复杂度:同样的输入规模花费多少空间(主要是内存)。
以上两点越小越好。
稳定性:不会因为输入的不同而导致不稳定的情况发生。
算法的思路是否简单:越简单越容易实现的越好。
哪种数据结构可以实现递归?
栈可以实现,递归需要保存正在计算的上下文, 等待当前计算完成后弹出,再继续计算, 只有栈先进后出的特性才能实现。
斐波那契数列
斐波那契数列:简单地说,起始两项为 0 和 1,此后的项分别为它的前两项之和。
1. def fibo(num):
2. numList = [0,1]
3. for i in range(num - 2):
4. numList.append(numList[-2] + numList[-1])
5. return numList
二叉树如何求两个叶节点的最近公共祖先?
原理:二叉搜索树是排序过的 ,位于左子树的结点都比父结点小,位于右子树的结点都比父结点大,我们只需从根节点开始和两个输入的结点进行比较,如果当前节点的值比两个结点的值都大,那么最低的公共祖先结点一定
在该结点的左子树中,下一步开遍历当前结点的左子树。如果当前节点的值比两个结点的值都小,那么最低的公共祖先结点一定在该结点的右子树中,下一步开遍历当前结点的右子树。这样从上到下找到第一个在两个输入结点的值之间的结点。
1. class TreeNode(object):
2. def __init__(self, left=None, right=None, data=None):
3. self.data = data
4. self.left = left
5. self.right = right
6. def getCommonAncestor(root, node1, node2):
7. while root:
8. if root.data > node1.data and root.data > node2.data:
9. root = root.left
10. elif root.data < node1.data and root.data < node2.data:
11. root = root.right
12. else:
13. return root
14. return None
两个字符串,如何求公共字符串?
1. def getLCString(str1, str2):
2. maxlen = len(str1) if len(str1) < len(str2) else len(str2)
3. example = str1 if len(str1) < len(str2) else str2
4. other = str2 if str1 == example else str1
5. for i in range(maxlen):
6. for j in range(maxlen, 0, -1):
7. if other.find(example[i:j]) != -1:
8. return example[i:j]
找出二叉树中最远结点的距离?
计算一个二叉树的最大距离有两个情况。
情况 A: 路径经过左子树的最深节点,通过根节点,再到右子树的最深节点。
情况 B: 路径不穿过根节点,而是左子树或右子树的最大距离路径,取其大者。
只需要计算这两个情况的路径距离,并取其大者,就是该二叉树的最大距离。
写一个二叉树
1. class TreeNode(object):
2. def __init__(self, left=None, right=None, data=None):
3. self.data = data
4. self.left = left
5. self.right = right
6. def preorder(root): #前序遍历
7. if root is None:
8. return
9. else:
10. print(root.data)
11. preorder(root.left)
12. preorder(root.right)
13.
14. def inorder(root): #中序遍历
15. if root is None:
16. return
17. else:
18. inorder(root.left)
19. print(root.data)
20. inorder(root.right)
21.
22. def postorder(root): # 后序遍历
23. if root is None:
24. return
25. postorder(root.left)
26. postorder(root.right)
27. print(root.data)
写一个二分查找
1. def binary_search(alist,item):
2. n = len(alist)
3. if n == 0:
4. return False
5. mid = n // 2
6. if alist[mid] == item:
7. return True
8. elif alist[mid] > item:
9. return binary_search(alist[:mid],item)
10. else:
11. return binary_search(alist[mid+1:],item)
12.
13. if __name__ == '__main__':
14. li = [3,6,8,10,40,60]
15. print(binary_search(li,60))
set 用 in 时间复杂度是多少,为什么?
O(1),因为 set 是键值相同的一个数据结构,键做了 hash 处理。
深度优先遍历和广度优先遍历的区别?
二叉树的深度优先遍历的非递归的通用做法是采用栈,广度优先遍历的非递归的通用做法是采用队列。
- 深度优先遍历:对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。要特别注意的是,二叉树的深度优先遍历比较特殊,可以细分为先序遍历、中序遍历、后序遍历。具体说明如下:
- 先序遍历:对任一子树,先访问根,然后遍历其左子树,最后遍历其右子树。
- 中序遍历:对任一子树,先遍历其左子树,然后访问根,最后遍历其右子树。
- 后序遍历:对任一子树,先遍历其左子树,然后遍历其右子树,最后访问根。
- 广度优先遍历:又叫层次遍历,从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有结点可以访问为止。
深度优先搜素算法:不全部保留结点,占用空间少;有回溯操作(即有入栈、出栈操作),运行速度慢。
广度优先搜索算法:保留全部结点,占用空间大; 无回溯操作(即无入栈、出栈操作),运行速度快。
通常,深度优先搜索法不全部保留结点,扩展完的结点从数据库中弹出删去,这样,一般在数据库中存储的结点数就是深度值,因此它占用空间较少。所以,当搜索树的结点较多,用其它方法易产生内存溢出时,深度优先搜索不失为一种有效的求解方法。
广度优先搜索算法,一般需存储产生的所有结点,占用的存储空间要比深度优先搜索大得多,因此,程序设计中,必须考虑溢出和节省内存空间的问题。但广度优先搜索法一般无回溯操作,即入栈和出栈的操作,所以运行速度比深度优先搜索要快些。
列表中有 n 个正整数范围在[0,1000],请编程对列表中的数据进行排序
1. def quick_sort(lista,start,stop):
2. if start >= stop:
3. return
4. low = start
5. high = stop
6. mid = lista[start]
7. while low < high:
8. while low < high and lista[high] >= mid:
9. high -= 1
10. lista[low] = lista[high]
11. while low < high and mid > lista[low]:
12. low += 1
13. lista[high] = lista[low]
14. lista[low] = mid
15. quick_sort(lista,start,low-1)
16. quick_sort(lista,low+1,stop)
17. return lista
18. lista = [0,1000]
19. print(quick_sort(lista,0,len(lista)-1))
面向对象编程中有组合和继承的方法实现新的类,假设我们手头只有“栈”类,请用“组合”的方式使用“栈”(LIFO)来实现“队列”(FIFO),完成以下代码?
1. Class queue(stack):
2. def __init__(self):
3. stack1 = stack() # 进来的元素都放在里面
4. stack2 = stack() # 元素都从这里出去
5. def push(self, element):
6. self.stack1.push(element)
7. def pop(self):
8. if self.stack2.empty():#如果没有元素, 就把负责放入元素的栈中元素全部放进来
9. while not self.stack1.empty():
10. self.stack2.push(self.stack1.top())
11. self.stack1.pop()
12. ret = self.stack2.top() # 有元素后就可以弹出了
13. self.stack2.pop()
14. return ret
15. def top(self):
16. if (self.stack2.empty()):
17. while not self.stack1.empty():
18. self.stack2.push(self.stack1.top())
19. self.stack1.pop()
20. return self.stack2.top()
写程序把一个单向链表顺序倒过来(尽可能写出更多的实现方法,标出所写方法的空间和时间复杂度)
1. class ListNode:
2. def __init__(self,x):
3. self.val=x
4. self.next=None
5.
6. def nonrecurse(head): #循环的方法反转链表
7. if head is None or head.next is None:
8. return head
9. pre=None
10. cur=head
11. h=head
12. while cur:
13. h=cur
14. tmp=cur.next
15. cur.next=pre
16. pre=cur
17. cur=tmp
18. return h
19.
20. class ListNode:
21. def __init__(self,x):
22. self.val=x
23. self.next=None
24. def recurse(head,newhead): #递归,head 为原链表的头结点,newhead 为反转后链表的头结点
25. if head is None:
26. return
27. if head.next is None:
28. newhead=head
29. else :
30. newhead=recurse(head.next,newhead)
31. head.next.next=head
32. head.next=None
33. return newhead
有一个长度为 n 的数组 a,里面的元素都是整数,现有一个整数 B,写程序判断 数组 a 中是否有两个元素的和等于 B(尽可能写出更多的实现方法,标出所写方法的空间和时间复杂度)
1. def func(arr, d):
2. count = list()
3. for index, i in enumerate(arr):
4. for j in arr[index+1:]:
5. if i + j == d:
6. count.append((i, j))
7. return coun
桶排序(最快最简单的排序)
桶排序的基本思想是将一个数据表分割成许多 buckets,然后每个 bucket 各自排序,或用不同的排序算法,或者递归的使用 bucket sort 算法。也是典型的 divide-and-conquer 分而治之的策略。它是一个分布式的排序,介于 MSD 基数排序和 LSD 基数排序之间。
1. def bucketSort(nums):
2. # 选择一个最大的数
3. max_num = max(nums)
4. # 创建一个元素全是 0 的列表, 当做桶
5. bucket = [0]*(max_num+1)
6. # 把所有元素放入桶中, 即把对应元素个数加一
7. for i in nums:
8. bucket[i] += 1
9. # 存储排序好的元素
10. sort_nums = []
11. # 取出桶中的元素
12. for j in range(len(bucket)):
13. if bucket[j] != 0:
14. for y in range(bucket[j]):
15. sort_nums.append(j)
16. return sort_nums
17.
18. nums = [5,6,3,2,1,65,2,0,8,0]
19. print bucketSort(nums)
- 桶排序是稳定的
- 桶排序是常见排序里最快的一种, 大多数情况下比快排还要快
- 桶排序非常快,但是同时也非常耗空间,基本上是最耗空间的一种排序算法
青蛙跳台阶问题
一只青蛙要跳上 n 层高的台阶,一次能跳一级,也可以跳两级,请问这只青蛙有多少种跳上这个 n层高台阶的方法?
-
方法 1:递归
设青蛙跳上 n 级台阶有 f(n)种方法,把这 n 种方法分为两大类,第一种最后一次跳了一级台阶,这类方法共有 f(n-1)种,第二种最后一次跳了两级台阶,这种方法共有 f(n-2)种,则得出递推公式f(n)=f(n-1)+f(n-2),显然,f(1)=1,f(2)=2,递推公式如下:
这种方法虽然代码简单,但效率低,会超出时间上限
1. class Solution: 2. # @param {integer} n 3. # @return {integer} 4. def climbStairs(self, n): 5. if n==1: 6. return 1 7. elif n==2: 8. return 2 9. else: 10. return self.climbStairs(n-1)+self.climbStairs(n-2)
-
方法 2: 用循环来代替递归
这种方法的原理仍然基于上面的公式,但是用循环代替了递归,比上面的代码效率上有较大的提升,可以 AC。
1. class Solution: 2. # @param {integer} n 3. # @return {integer} 4. def climbStairs(self, n): 5. if n==1 or n==2: 6. return n 7. a=1;b=2;c=3 8. for i in range(3,n+1): 9. c=a+b;a=b;b=c 10. return c
-
方法3:建立简单数学模型,利用组合数公式
设青蛙跳上这 n 级台阶一共跳了 z 次,其中有 x 次是一次跳了两级,y 次是一次跳了一级,则有z=x+y ,2x+y=n,对一个固定的 x,利用组合可求出跳上这 n 级台阶的方法共有种方法又因为 x 在区间[0,n/2]内,所以我们只需要遍历这个区间内所有的整数,求出每个 x 对应的组合数累加到最后的结果即可。
1. class Solution: 2. # @param {integer} n 3. # @return {integer} 4. def climbStairs(self, n): 5. def fact(n): 6. result=1 7. for i in range(1,n+1): 8. result*=i 9. return result 10. total=0 11. for i in range(n/2+1): 12. total+=fact(i+n-2*i)/fact(i)/fact(n-2*i) 13. return total