100.python面试之常见题

python最新常见面试题系列

垃圾回收机制

python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略

引用计数

Python语言默认采用的垃圾收集机制是『引用计数法 Reference Counting』,该算法最早George E. Collins在1960的时候首次提出,50年后的今天,该算法依然被很多编程语言使用,『引用计数法』的原理是:每个对象维护一个ob_ref字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数ob_ref加1,每当该对象的引用失效时计数ob_ref减1,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。它的缺点是需要额外的空间维护引用计数,这个问题是其次的,不过最主要的问题是它不能解决对象的“循环引用”,因此,也有很多语言比如Java并没有采用该算法做来垃圾的收集机制。

什么是循环引用?A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数虽然都为1,但显然应该被回收,例子:

1
2
3
4
5
6
a = { } #对象A的引用计数为 1
b = { } #对象B的引用计数为 1
a['b'] = b #B的引用计数增1
b['a'] = a #A的引用计数增1
del a #A的引用减 1,最后A对象的引用为 1
del b #B的引用减 1, 最后B对象的引用为 1

recycle-refrence.jpg

在这个例子中程序执行完del语句后,A、B对象已经没有任何引用指向这两个对象,但是这两个对象各包含一个对方对象的引用,虽然最后两个对象都无法通过其它变量来引用这两个对象了,这对GC来说就是两个非活动对象或者说是垃圾对象,但是他们的引用计数并没有减少到零。因此如果是使用引用计数法来管理这两对象的话,他们并不会被回收,它会一直驻留在内存中,就会造成了内存泄漏(内存空间在使用完毕后未释放)。为了解决对象的循环引用问题,Python引入了标记-清除和分代回收两种GC机制。

标记清除

『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

mark-sweepg

在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象

装饰器

其主要作用就是在不改变原有函数代码的前提下,给该函数添加新的功能

装饰器的执行时间是加载模块时立即执行,这意味着只要加载了带装饰器的模块,即使不运行被装饰的函数,装饰器也运行了

应用场景

用户验证、输出格式化、异常捕获、日志管理、统计函数运行时间等

闭包

在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包。

普通装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 扩展功能
def extend(func):
def method(*args, **kwargs):
# 原有函数前扩展其功能
print('我可以编写代码')
# 执行原有函数
return func(*args, **kwargs)
return method

# 电脑原有功能
@extend
def computer(name, something):
print(f'我叫{name}可以{something}')
return '我是computer原有代码的返回结果'

# 执行computer
res = computer('银河1号', '播放音乐')
print(res)

运行结果:

1
2
3
我可以编写代码
我叫银河1号可以播放音乐
我是computer原有代码的返回结果

含参装饰器

带有参数的装饰器就是使用装饰器装饰函数的时候可以传入指定参数,语法格式: @装饰器(参数,…)

函数装饰器

解决方案:在装饰器外面再包裹上一个函数,让最外面的函数接收参数,返回的是装饰器,因为@符号后面必须是装饰器实例。

使用带有参数的装饰器,其实是在装饰器外面又包裹了一个函数,使用该函数接收参数,返回是装饰器,因为 @ 符号需要配合装饰器实例使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorator(admin):
def logging(func):
def inner(*args, **kwargs):
# 接收错误代码和错误信息
res = func(*args, **kwargs)
print("错误信息已写入日志:【%s】。操作人:%s" % (res, admin))
return inner
return logging

@decorator('张三')
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

运行结果:

1
错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】。操作人:张三

多个装饰器

多个装饰器的装饰过程是: 离函数最近的装饰器先装饰,然后外面的装饰器再进行装饰,由内到外的装饰过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def make_div(func):
"""添加div标签"""
def func_inner():
# 接收函数返回值
res = func()
# 格式化结果
return "<div>%s</div>" % res
return func_inner

def make_p(func):
"""添加p标签"""
def func_inner():
# 接收函数返回值
res = func()
# 格式化结果
return "<p>%s</p>" % res
return func_inner

@make_div
@make_p
def show():
"""带参数的函数"""
return '我就是简单一句话,怎么了?'

print(show())

运行结果:

1
<div><p>我就是简单一句话,怎么了?</p></div>

类装饰器

  • 想要让类的实例对象能够像函数一样进行调用,需要在类里面使用call方法,把类的实例变成可调用对象(callable)
  • 类装饰器装饰函数功能在call方法里面进行添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Logging(object):
def __init__(self, func):
# 接收被装饰的函数,设为私有防止外部调用
self.__func = func

def __call__(self, *args, **kwargs):
print('装饰前的操作')
res = self.__func(*args, **kwargs)
print("错误信息已写入日志:【%s】" % res)


@Logging
def show(err, msg):
"""带参数的函数"""
return '错误代码:%s;错误提示:%s' % (err, msg)

show('DBERROR', '数据库出错了')

运行结果:

1
2
装饰前的操作
错误信息已写入日志:【错误代码:DBERROR;错误提示:数据库出错了】

GIL锁

GIL 是python的全局解释器锁,同一进程中假如有多个线程运行,一个线程在运行python程序的时候会霸占python解释器(加了一把锁即GIL),使该进程内的其他线程无法运行,等该线程运行完后其他线程才能运行。如果线程运行过程中遇到耗时操作,则解释器锁解开,使其他线程运行。所以在多线程中,线程的运行仍是有先后顺序的,并不是同时进行。

多进程中因为每个进程都能被系统分配资源,相当于每个进程有了一个python解释器,所以多进程可以实现多个进程的同时运行,缺点是进程系统资源开销大

with语句

一个类只要实现了__enter__()和__exit__()这个两个方法,通过该类创建的对象我们就称之为上下文管理器。

常规对文件进行IO操作我们使用的是open方法,后面为了简便和防止文件未关闭出现了更简单的用法,with open。

Python提供了 with 语句的这种写法,既简单又安全,并且 with 语句执行完成以后自动调用关闭文件操作,即使出现异常也会自动调用关闭文件操作

类实现上下文管理器

定义一个File类,实现 __enter__() 和 __exit__()方法,然后使用 with 语句来完成操作文件, 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class File(object):
"""文件操作类"""
def __init__(self, filepath, mode):
self.filepath = filepath
self.mode = mode

def __enter__(self):
"""打开文件"""
self.file = open(self.filepath, self.mode)
print("打开文件")
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
"""关闭文件"""
print("关闭文件")
self.file.close()


if __name__ == '__main__':
with File('log.log', 'r') as file:
file.write("家啊")

装饰器实现上下文管理器

Python 还提供了一个 @contextmanager 的装饰器,更进一步简化了上下文管理器的实现方式。通过 yield 将函数分割成两部分,yield 上面的语句在 __enter__ 方法中执行,yield 下面的语句在 __exit__ 方法中执行,紧跟在 yield 后面的参数是函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from contextlib import contextmanager

@contextmanager
def file_handle(filepath, mode):
"""文件操作"""
try:
file = open(filepath, mode)
yield file
except Exception as e:
print(e)
finally:
print('能执行到这里吗')
file.close()


if __name__ == '__main__':
with file_handle('log.log', 'r') as f:
f.write("写入东西")

异常捕获

try/except…else

try/except 语句还有一个可选的 else 子句,如果使用这个子句,那么必须放在所有的 except 子句之后。

else 子句将在 try 子句没有发生任何异常的时候执行。

即:没有异常执行完try执行else,有异常直接执行except。(但是try里面没有异常的代码部分也会被执行)

img

try-finally 语句

try-finally 语句无论是否发生异常都将执行最后的代码。

函数中如果在异常中使用return语句,无论有无异常最后只会使用finally中的return语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def func():
try:
a = 99
num = 8 / 0
# print("我是try")
return "try"
except Exception as e:
# print(e)
return "error"
else:
# print("我是else")
return 'else'
finally:
# print("我是finally")
return "我是finally"

print(func())

执行结果:

1
我是finally

静态方法只是名义上归类管理,实际上在静态方法里面访问不了类或者实例的任何属性。 一般不需要传参数self。
类方法只能访问类变量,不能访问实例变量。需要有self参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A(object):
# 构造函数
def __init__(self, title1):
self.title = title1
print(self.title)

# 实例函数
def foo(self, title2):
print(title2)

# 静态函数
@staticmethod
def static_foo():
print("静态方法")

# 类函数
@classmethod
def cls_foo(cls):
cls.foo(a,"类函数调用实例函数")
print("类方法")
a=A('我是示例标题')
a.cls_foo()
A.cls_foo()

单例模式

应用场景:日志logger插入,计时器、权限校验、网站计数器,windows资源管理器,回收站,线程池,数据库连接池等资源池。
什么情况下需要单例模式:

1.每个实例都会占用资源,而且每个实例初始化都会影响性能;

2.当有同步需求的时候,如日志文件的控制,确保只有一个实例。

1
2
3
4
5
6
7
8
9
10
class Singleton:
def __new__(cls, *args, **kwargs):
print("我是new")
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self, name):
print("我是初始化方法", name)
s = Singleton('ll')

init和new方法

_new__方法用于创建对象并返回对象,当返回对象时会自动调用__init__方法进行初始化。__new__方法是静态方法,而__init__是实例方法。

1、__new__至少要有一个参数cls,代表当前类,此参数在实例化时由Python解释器自动识别

2、__new__必须要有返回值,返回实例化出来的实例,这点在自己实现__new__时要特别注意,可以return父类(通过super(当前类名, cls))__new__出来的实例,或者直接是object的__new__出来的实例

3、__init__有一个参数self,就是这个__new__返回的实例,__init__在__new__的基础上可以完成一些其它初始化的动作,__init__不需要返回值

4、如果__new__创建的是当前类的实例,会自动调用__init__函数,通过return语句里面调用的__new__函数的第一个参数是cls来保证是当前类实例,如果是其他类的类名,;那么实际创建返回的就是其他类的实例,其实就不会调用当前类的__init__函数,也不会调用其他类的__init__函数。

1
2
3
4
5
6
7
8
9
class Person:
def __new__(cls, *args, **kwargs):
print("我是new方法,cls的ID为:%s;__new__方法为:%s" % (id(cls), super().__new__(cls)))
return super().__new__(cls)

def __init__(self, name="dd"):
print("我是init方法,接受的参数为:%s;self为:%s" % (name, self))
s = Person('刘德华')
print("类内存id为:%s" % id(Person))

运行结果:

1
2
3
我是new方法,cls的ID为:39409184;__new__方法为:<__main__.Person object at 0x00000000024CA520>
我是init方法,接受的参数为:刘德华;self为:<__main__.Person object at 0x00000000024CA520>
类内存id为:39409184

面向对象

Python是一门面向对象的语言。面向对象都有三大特性:封装、继承、多态。

1、封装

隐藏对象的属性和实现细节,仅对外提供公共访问方式。在python中用双下划线开头的方式将属性设置成私有的 。

好处:1. 将变化隔离;2. 便于使用;3. 提高复用性;4. 提高安全性。

2、继承

继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类。即一个派生类继承基类的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计:一个Dog类型的对象派生自Animal类,这是模拟”是一个(is-a)”关系 。

python中类的继承分为:单继承和多继承

1
2
3
4
5
6
7
class ParentClass1: #定义父类

class ParentClass2: #定义父类

class SubClass1(ParentClass1): #单继承,基类是ParentClass1,派生类是SubClass

class SubClass2(ParentClass1,ParentClass2): #python支持多继承,用逗号分隔开多个继承的类

3、多态

一种事物的多种体现形式,函数的重写其实就是多态的一种体现 。Python中,多态指的是父类的引用指向子类的对象 。

实现多态的步骤:

1、定义新的子类

2、重写对应的父类方法

3、使用子类的方法直接处理,不调用父类的方法

多态的好处:

(1)增加了程序的灵活性

(2)增加了程序可扩展性

web项目的性能优化

该题目网上有很多方法,我不想截图网上的长串文字,看的头疼,按我自己的理解说几点

前端优化:

1、减少http请求、例如制作精灵图

2、html和CSS放在页面上部,javascript放在页面下面,因为js加载比HTML和Css加载慢,所以要优先加载html和css,以防页面显示不全,性能差,也影响用户体验差

后端优化:

1、缓存存储读写次数高,变化少的数据,比如网站首页的信息、商品的信息等。应用程序读取数据时,一般是先从缓存中读取,如果读取不到或数据已失效,再访问磁盘数据库,并将数据再次写入缓存。

2、异步方式,如果有耗时操作,可以采用异步,比如celery

3、代码优化,避免循环和判断次数太多,如果多个if else判断,优先判断最有可能先发生的情况

数据库优化:

1、如有条件,数据可以存放于redis,读取速度快

2、建立索引、外键等

3、分库分表,读写分离

提高python运行效率的方法

1、使用生成器,因为可以节约大量内存

2、循环代码优化,避免过多重复代码的执行

3、核心模块用Cython PyPy等,提高效率

4、多进程、多线程、协程

5、多个if elif条件判断,可以把最有可能先发生的条件放到前面写,这样可以减少程序判断的次数,提高效率

mysql和redis区别

redis: 内存型非关系数据库,数据保存在内存中,速度快

mysql:关系型数据库,数据保存在磁盘中,检索的话,会有一定的Io操作,访问速度相对慢

常见状态码

200 OK

请求正常处理完毕

204 No Content

请求成功处理,没有实体的主体返回

206 Partial Content

GET范围请求已成功处理

301 Moved Permanently

永久重定向,资源已永久分配新URI

302 Found

临时重定向,资源已临时分配新URI

303 See Other

临时重定向,期望使用GET定向获取

304 Not Modified

发送的附带条件请求未满足

307 Temporary Redirect

临时重定向,POST不会变成GET

400 Bad Request

请求报文语法错误或参数错误

401 Unauthorized

需要通过HTTP认证,或认证失败

403 Forbidden

请求资源被拒绝

404 Not Found

无法找到请求资源(服务器无理由拒绝)

500 Internal Server Error

服务器故障或Web应用故障

503 Service Unavailable

服务器超负载或停机维护

flask和django区别

形象类比
  如果Django类似于精装修的房子,自带豪华家具、非常齐全功能强大的家电,什么都有了,拎包入住即可,十分方便。而Flask类似于毛坯房,自己想把房子装修成什么样自己找材料,买家具自己装。材料和家具种类非常丰富,并且都是现成免费的,直接拿过去用即可。

  • 体量上的区别
      Flask:小巧、灵活,让程序员自己决定定制哪些功能,非常适用于小型网站。
      对于普通的工人来说将毛坯房装修为城市综合体还是很麻烦的,使用Flask来开发大型网站也一样,开发的难度较大,代码架构需要自己设计,开发成本取决于开发者的能力和经验。
      Django:大而全,功能极其强大,是Python web框架的先驱,用户多,第三方库极其丰富。
      非常适合企业级网站的开发,但是对于小型的微服务来说,总有“杀鸡焉有宰牛刀”的感觉,体量较大,非常臃肿,定制化程度没有Flask高,也没有Flask那么灵活。

php和python的区别

PHP和Python的区别之一:定义不同。

  PHP是一种通用开源脚本语言,语法混合了C、Java、Perl以及PHP自创的语法,因此利于学习,使用广泛,主要适用于Web开发领域。PHP还可以执行编译后代码,编译可以达到加密和优化代码运行,使代码运行更快。

  Python是一种面向对象的解释型计算机程序设计语言,语法简洁清晰,特色之一是强制用空白符(white space)作为语句缩进。Python具有丰富和强大的库,常被昵称为胶水语言,能够把用其他语言制作的各种模块(尤其是C/C++)很轻松地联结在一起。

  PHP和Python的区别之二:优点和不足。

  PHP的优点是容易上手(学习曲线短而平)、支持所有主流的Web服务器、提供了广泛的数据库支持、提供大量的可用扩展和源代码、适用于几乎每一种操作系统和平台;缺点是不适合开发桌面应用程序、全局配置参数会改变语言语义,给部署和可移植性带来了复杂性、错误处理机制历来很差劲、被认为不如其他编程语言来得安全可靠。

  Python的优点是简单易学、语法易读有条理、可在多个系统和平台上运行、提供了快速原型和动态语义功能、易于构建应用程序、面向对象编程驱动型、通过认真实施程序包和模块,获得可重用性;不足是在处理多处理器/多核心工作方面其实不是很好、缺少商业支持机构、运行速度不如Java等语言。

cookie和session的区别

1,session 在服务器端,cookie 在客户端(浏览器)

2、session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效,存储Session时,键与Cookie中的sessionid相同,值是开发人员设置的键值对信息,进行了base64编码,过期时间由开发人员设置

3、cookie安全性比session差

常见异常

IOError:输入输出异常

AttributeError:试图访问一个对象没有的属性

ImportError:无法引入模块或包,基本是路径问题

IndentationError:语法错误,代码没有正确的对齐

IndexError:下标索引超出序列边界

KeyError:试图访问你字典里不存在的键

SyntaxError:Python代码逻辑语法出错,不能执行

NameError:使用一个还未赋予对象的变量

解决GIL问题的方案:

1.使用其它语言,例如C,Java

2.使用其它解释器,如java的解释器jython

3.使用多进程

解决方法1:

GIL既然是针对线程的锁,那我们如果直接使用Python进行多进程编程,就可以绕过GIL了,Python中有对应的模块,名字叫multiprocesssing。但是,对于进程来说,进程间的通信又需要我们手动实现,大大增加了编程的难度及复杂性。

解决方法2:

更换一个解释器执行程序就可以了,比如 “jython” (用JAVA写的python解释器)

解决方法3:

使用python语言的特性:胶水.

在子线程部分我们不用python语言来写。我们用其他语言来写,比如C,我们让子线程部分用c来写,就ok。(实质上也相当于那部分代码绕过了cython解释器)。

协程

协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。

所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:

1
2
3
4
5
6
7
8
9
def A():
print('1')
print('2')
print('3')

def B():
print('x')
print('y')
print('z')

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

1
2
3
4
5
6
1
2
x
y
3
z

但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。

但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

来看例子:

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'

def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点:

“子程序就是协程的一种特例。”

PEP8编码规范

1.行缩进:tab(4个空格)

  • 隐式行连接缩进

    • 1、对齐
    • 2、层级缩进
    • 3、\
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 对齐缩进
    # 对齐缩进
    foo = dict(name="MuSen", age=18,
    gender="男", height="180")


    # 层级缩进(为了区别应当再缩进四格)
    def fun_add(
    a, b=200,
    c=1000, d=2000):
    return a, b, c, d


    # 行连线 \
    with open("txt1.txt") as f1,\
    open("txt2.txt") as f2:
    f1.read()
    f2.read()

2. 单行字符限制

  • 所有行限制的最大字符数为79个
  • 没有结构化限制的大块文本(文档字符或者注释),每行最大字符数限制在72

3. 空行

  • 顶级函数和类的定义之间有两行空行
  • 类内部的函数定义之间有一个空行

4. 源文件编码方式

  • Python核心发布中的的代码应该始终UTF-8(Python2中默认是ASCII编码)
  • Python3中不应该有编码声明

5. 注释

1、与代码组矛盾的注释比没有注释还糟,代码有更新,更新对应的的注释!

2、如果注释很短,结尾句号可以省略。块注释一般由完整句子的一个或多个段落组成,并且每句话结束有个句号。在句尾结束的时候应该使用两个空格

3、在非英语国家的Python程序员,请使用英文写注释,除非你120%的确信你的代码不会被其他语言的人阅读—-忽略

  • 行内注释

    • 行内注释和代码至少要有两个空格分离
    • 注释由#和一个空格开始,有节制的使用
    1
    2
    a = 6
    print(a) # 打印a
  • 块注释

    • 块注释通常使用与跟随他们的某些(或全部)代码,并锁紧到与代码相同的级别。
    • 块注释的每一行开头使用一个#和一个空格(除非块注释内部缩进文本)
    • 块注释的段落通过只有一个#的空行分割。
    1
    2
    3
    4
    5
    def add_num(a, b, c):
    # 此函数的功能式返回三个数字和
    #
    # 简单明了
    return a + b + c

6. 文档注释PEP 257描述写出的文档相关的约定

  • 文档注释应当使用:三个双引号 “”xxxx “””来包裹
  • 要为所有的公共的模块、函数、类便携文档说明
  • 非公共的方法没有必要添加文档注释,但应该有一个描述方法具体作用的注释,这个注释应该在def那一行之后
1
2
3
4
5
6
7
8
9
10
def add_num(a, b, c):
'''
这是一个计算的方法

:param a: int
:param b: int
:param c: int
:return:
'''
return a + b + c
  • 单行文档注释:”””数值””” 引号文字同一行
  • 多行文档注释:多行文档字符串由一个摘要组成,就像一行文档字符串,后跟一个空行,后面是更详细的描述,多行文档说明使用结尾三引号独立 一行
  • 提取文档的注释:对象的__doc__属性
1
2
3
import requests

print(requests.__doc__)

7. 模块和包相关规范

  • 位置:导入文娱文件的顶部,在文档注释之后,在模块全局变量之前

  • 导入顺序

    • 1、标准库导入
    • 2、相关的第三方库导入
    • 3、特定本地应用库导入
  • 模块的内置属性(名字前后双下划线)

    • 例如:all*,author,version,应该放在模块的文档注释之后,任意import,from 语句之前
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    """
    第一先文档注释
    第二__all__双下滑先
    第三 import
    第四 全局变量
    """
    __all__ = {}

    import requests

    a = 5


    # 推荐
    import os
    import requests
    from subprocess import Popen, PIPE

    # 不推荐
    from requests import get
    import requests,os
    from requests import * # 中毒禁忌

8. 命名规范

  • 变量命名
    • 永远不要用字母”l””(小写的L)”O”(大写的O)作为单字符变量
    • 在某些字体里面无法和数字0和1区分
  • 函数命名
    • 函数名应该小写,用下滑线分割
    • 大小写混合仅在为了兼容已存在的代码的风格使用,保持向后兼容性
  • 类命名
    • 类名一般使用首字母大写的约定
    • 在接口被文档化并且要被用于调用的情况下么可以使用函数的命名风格代替
    • 对于内置的变量命名有一个单独的约定:大部分内置变量是单个单词(或者两个单词连接在一起),首字符大写的命名只用于异常名或者内部变量
  • 类里面函数和参数
    • 始终要将self作为实例方法的第一个参数
    • 始终要将cls作为类静态方法的第一个参数
    • 如果函数的参数名和已有的关键词冲突,在最后单一下划线比缩写或者随意拼写更好,class_比cla更好
  • 包和模块
    • 模块的命名要短
    • 使用小写
    • 避免使用特殊字符
    • 尽量保持模块名简单,以无需分开单词命名(不推荐使用两单词之间下划线分开)
  • 常量
    • 通常定义与模块级别并且所有的字母都是大写、单词用下划线分开

9.项目结构价绍

  • readme:对项目的整体介绍,同时也是一份使用手册,需要时常维护更新,通常为README.rst/README.md
  • LICENSE:简述该项目许可说明和授权
  • setup.py:通过setup把核心代码打包发布
  • sample:存放项目核心代码
  • requirements.txt:存放该项目所有依赖的第三方库
  • docs:包的参考文档
  • tests:所有的代码测试都存在于该目录下
  • makefile:用于项目的命令管理(开源项目比较广泛)根据项目需求添加其他文件和目录

乐观锁和悲观锁

悲观锁, 就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,乐观锁适用于多读的应用类型,这样可以提高吞吐量

r、r+、rb、rb+文件打开模式区别

img

get和post区别

1、GET请求是通过URL直接请求数据,数据信息可以在URL中直接看到,比如浏览器访问;而POST请求是放在请求头中的,我们是无法直接看到的;

2、GET提交有数据大小的限制,一般是不超过1024个字节,而这种说法也不完全准确,HTTP协议并没有设定URL字节长度的上限,而是浏览器做了些处理,所以长度依据浏览器的不同有所不同;POST请求在HTTP协议中也没有做说明,一般来说是没有设置限制的,但是实际上浏览器也有默认值。总体来说,少量的数据使用GET,大量的数据使用POST。

3、GET请求因为数据参数是暴露在URL中的,所以安全性比较低,比如密码是不能暴露的,就不能使用GET请求;POST请求中,请求参数信息是放在请求头的,所以安全性较高,可以使用。在实际中,涉及到登录操作的时候,尽量使用HTTPS请求,安全性更好。

可变数据类型

当该数据类型的对应变量的值发生了改变,那么它对应的内存地址不会发生改变,对于这种数据类型,就称可变数据类型。

可变类型有Set(集合)、List(列表)、Dictionary(字典)。

不可变数据类型

当该数据类型的对应变量的值发生了改变,那么它对应的内存地址也会发生改变,对于这种数据类型,就称不可变数据类型。

python不可变类型有Number(数字)、String(字符串)、Tuple(元组);

copy和deepcopy区别

1、复制不可变数据类型,不管copy还是deepcopy,都是同一个地址当浅复制的值是不可变对象(数值,字符串,元组)时和=“赋值”的情况一样,对象的id值与浅复制原来的值相同。

img

2、复制的值是可变对象(列表和字典)

浅拷贝copy有两种情况:

第一种情况:复制的 对象中无 复杂 子对象,原来值的改变并不会影响浅复制的值,同时浅复制的值改变也并不会影响原来的值。原来值的id值与浅复制原来的值不同。

第二种情况:复制的对象中有 复杂 子对象 (例如列表中的一个子元素是一个列表), 改变原来的值 中的复杂子对象的值 ,会影响浅复制的值。

深拷贝deepcopy:完全复制独立,包括内层列表和字典

img

img

多线程、多进程

进程:

1、操作系统进行资源分配和调度的基本单位,多个进程之间相互独立

2、稳定性好,如果一个进程崩溃,不影响其他进程,但是进程消耗资源大,开启的进程数量有限制

线程:

1、CPU进行资源分配和调度的基本单位,线程是进程的一部分,是比进程更小的能独立运行的基本单位,一个进程下的多个线程可以共享该进程的所有资源

2、如果IO操作密集,则可以多线程运行效率高,缺点是如果一个线程崩溃,都会造成进程的崩溃

应用:

IO密集的用多线程,在用户输入,sleep 时候,可以切换到其他线程执行,减少等待的时间

CPU密集的用多进程,因为假如IO操作少,用多线程的话,因为线程共享一个全局解释器锁,当前运行的线程会霸占GIL,其他线程没有GIL,就不能充分利用多核CPU的优势

cookie和session的区别

1,session 在服务器端,cookie 在客户端(浏览器)

2、session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效,存储Session时,键与Cookie中的sessionid相同,值是开发人员设置的键值对信息,进行了base64编码,过期时间由开发人员设置

3、cookie安全性比session差

mysql引擎和区别

InnoDB:支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。

MyISAM:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低,也可以使用。

MEMORY:所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。

Innodb引擎

Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别,关于数据库事务与其隔离级别的内容请见数据库事务与其隔离级别这篇文章。该引擎还提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统,MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECT COUNT(*) FROM TABLE时需要扫描全表。当需要使用数据库事务时,该引擎当然是首选。由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。

  1. 拥有MyISAM没有的事务处理操作
    事务处理:提交,回归,崩溃处理
  2. 支持外键操作
  3. 支持行级锁。由于锁粒度小,写操作不会锁定全表,并发较高的情况下,效率提升。(缺点:如果执行全表扫描,由于行级锁不能确定扫描的范围,必须锁定全表
  4. InnoDB是为处理巨大数据量的最大性能设计。它的CPU效率可能是任何其他基于磁盘的关系型数据库引擎锁不能匹敌的
  5. 本身是基于MySQL后台的完整数据库系统MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上
  6. InnoDB被用在众多需要高性能的大型数据库站点上
  7. InnoDB不创建目录,使用InnoDB时,MySQL将在MySQL数据目录下创建一个名为ibdata1的10MB大小的自动扩展数据文件,以及两个名为ib_logfile0和ib_logfile1的5MB大小的日志文件

总结:支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。

MyIASM引擎

MyIASM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。不过和Innodb不同,MyIASM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyIASM也是很好的选择。

基于ISAM存储引擎,并对其进行了扩展。是mysql的默认数据库引擎

  1. 不支持事务处理,行级锁,外键,但是是唯一支持表压缩全文索引的存储引擎。插入和更改表的效率较为低下(默认是锁定整张表),但是区别于 InnoDB,MyIASM中存储了表的行数,所以SELECT*时只需要直接读取已经存好的值而不需要全表扫描。
  2. MyISAM更强调快速的读取(适合不关心事务处理)
  3. 当删除和更新插入操作混合使用时,动态尺寸的行产生更少的碎片(通过合并相邻被删除的块,以及如果下一个块被删除,就扩展到下一个块)
  4. 每个表的最大索引说为64(可以通过重新编译修改),每个索引的最大列数为16,键最大长度1000字节
  5. 支持表压缩,所有数字键值以高字节优先被存储以允许一个更高的索引压缩
  6. 数据文件和索引文件可以存储在不同目录
  7. 使用MyISAM引擎创建数据库,将产生3个文件。文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义、数据文件的扩展名为.MYD(MYData)、索引文件的扩展名时.MYI(MYIndex)

E.G:大多数虚拟主机提供商和INTERNET平台提供商只允许使用MYISAM格式。MyISAM格式的一个重要缺陷就是不能在表损坏后恢复数据
优点:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用。

两种引擎的选择

大尺寸的数据集趋向于选择InnoDB引擎,因为它支持事务处理和故障恢复。数据库的大小决定了故障恢复的时间长短,InnoDB可以利用事务日志进行数据恢复,这会比较快。主键查询在InnoDB引擎下也会相当快,不过需要注意的是如果主键太长也会导致性能问题,关于这个问题我会在下文中讲到。大批的INSERT语句(在每个INSERT语句中写入多行,批量插入)在MyISAM下会快一些,但是UPDATE语句在InnoDB下则会更快一些,尤其是在并发量大的时候。

如果要提供提交、回滚、崩溃恢复能力的事物安全(ACID兼容)能力,并要求实现并发控制,InnoDB是一个好的选择
如果数据表主要用来插入和查询记录,则MyISAM引擎能提供较高的处理效率

mysql的内连接、左连接、右连接区别

内连接关键字:inner join;左连接:left join;右连接:right join。
内连接是把匹配的关联数据显示出来;左连接是左边的表全部显示出来,右边的表显示出符合条件的数据;右连接正好相反。

mysql索引是怎么实现的

索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找数据。
具体来说mysql中的索引,不同的数据引擎实现有所不同,但目前主流的数据库引擎的索引都是B+树实现的,B+树的搜索效率,可以到达二分法的性能,找到数据区域之后就找到了完整的数据结构了,所以索引的性能也是更好的。

怎么验证mysql的索引是否满足需求
使用explain查看sql是如何执行查询语句的,从而分析你的索引是否满足需求
explain语法:explain select * from table where type=1

数据库的事务隔离

数据库的事务隔离是在MYSQL.ini配置文件里添加的,在文件的最后添加:transaction-isolation = REPEATABLE-READ,可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。
1.未提交读:最低隔离级别,事务未提交前,就可以被其他事务读取(会出现幻读、脏读、不可重复读)
2.提交读:一个事务提交之后才能被其他事务读取到(会造成幻读,不可重复读)
3.可重复读:默认级别,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致的,禁止读取到别的事务未提交的数据(会造成幻读)
4.序列化:代价最高最可靠的隔离界别,该隔离级别能防止脏读、不可重复读、幻读

​ 1.脏读:表示一个事务读取到另一个事务未提交的数据
​ 2.不可重复读:在一个事务内,多次读取同一数据,两次读取之间,有另一个事务对数据进行了修改,导致两次数据读取不一致
​ 3.幻读:例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样。

mysql常用引擎

1.InnoDB 引擎:InnoDB 引擎提供了对数据库 acid 事务的支持,并且还提供了行级锁和外键的约束,它的设计的目标就是处理大数据容量的数据库系统。MySQL 运行的时候,InnoDB 会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎是不支持全文搜索,同时启动也比较的慢,它是不会保存表的行数的,所以当进行 select count() from table 指令的时候,需要进行扫描全表。由于锁的粒度小,写操作是不会锁定全表的,所以在并发度较高的场景下使用会提升效率的。
2.MyIASM 引擎:MySQL 的默认引擎,但不提供事务的支持,也不支持行级锁和外键。因此当执行插入和更新语句时,即执行写操作的时候需要锁定这个表,所以会导致效率会降低。不过和 InnoDB 不同的是,MyIASM 引擎是保存了表的行数,于是当进行 select count() from table 语句时,可以直接的读取已经保存的值而不需要进行扫描全表。所以,如果表的读操作远远多于写操作时,并且不需要事务的支持的,可以将 MyIASM 作为数据库引擎的首选。

mysql的行锁和表锁

InnoDB支持表锁和行锁,默认为行锁。MySAM只支持表锁。
1.表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突概率最高,并发量最低。
2.行级锁:开销大,加锁慢,会出现死锁。锁粒度小,发生锁冲突概率小,并发量最高。

乐观锁和悲观锁

1.乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
2.悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据时都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。
数据库的乐观锁需要自己实现,在表里添加一个version字段,每次修改成功值加1,这样每次修改的时候先比对一下,自己拥有的version和数据库现在的version是否一致,如果不一致就不修改,这样就实现了乐观锁。

mysql问题排查手段

1.使用show processlist命令查看当前所有连接信息
2.使用explain命令查询SQL语句执行计划
3.开启慢查询日志,查看慢查询的sql

索引

索引就一种特殊的查询表,数据库的搜索引擎可以利用它加速对数据的检索。它很类似与现实生活中书的目录,不需要查询整本书内容就可以找到想要的数据。索引可以是唯一的,创建索引允许指定单个列或者是多个列。缺点是它减慢了数据录入的速度,同时也增加了数据库的尺寸大小。

索引分类和区别

索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

普通索引(由关键字KEY或INDEX定义的索引)的唯一任务是加快对数据的访问速度。

普通索引允许被索引的数据列包含重复的值。如果能确定某个数据列将只包含彼此各不相同的值,在为这个数据列创建索引的时候就应该用关键字UNIQUE把它定义为一个唯一索引。也就是说,唯一索引可以保证数据记录的唯一性。

主键,是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字 PRIMARY KEY 来创建。

索引可以覆盖多个数据列,如像INDEX(columnA, columnB)索引,这就是联合索引。

索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件。

索引的目的

  • 快速访问数据表中的特定信息,提高检索速度
  • 创建唯一性索引,保证数据库表中每一行数据的唯一性。
  • 加速表和表之间的连接
  • 使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间

索引负面影响

创建索引和维护索引需要耗费时间,这个时间随着数据量的增加而增加;索引需要占用物理空间,不光是表需要占用数据空间,每个索引也需要占用物理空间;当对表进行增、删、改、的时候索引也要动态维护,这样就降低了数据的维护速度。

索引建立原则

  • 在最频繁使用的、用以缩小查询范围的字段上建立索引。
  • 在频繁使用的、需要排序的字段上建立索引

什么情况下不适合建立索引

  • 对于查询中很少涉及的列或者重复值比较多的列,不宜建立索引。
  • 对于一些特殊的数据类型,不宜建立索引,比如文本字段(text)等

如何通俗地理解三个范式?

答:第一范式:1NF是对属性的原子性约束,要求属性具有原子性,不可再分解;

第二范式:2NF是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;

第三范式:3NF是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。。

范式化设计优缺点:

优点:

可以尽量得减少数据冗余,使得更新快,体积小

缺点:

对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引优化

反范式化:

优点:可以减少表得关联,可以更好得进行索引优化

缺点:数据冗余以及数据异常,数据得修改需要更多的成本

什么是基本表?什么是视图?

答:基本表是本身独立存在的表,在 SQL 中一个关系就对应一个表。 视图是从一个或几个基本表导出的表。视图本身不独立存储在数据库中,是一个虚表

视图的优点?

答:(1) 视图能够简化用户的操作 (2) 视图使用户能以多种角度看待同一数据; (3) 视图为数据库提供了一定程度的逻辑独立性; (4) 视图能够对机密数据提供安全保护。

SQL语句优化有哪些方法?(选择几条)

(1)Where子句中:where表之间的连接必须写在其他Where条件之前,那些可以过滤掉最大数量记录的条件必须写在Where子句的末尾.HAVING最后。

(2)用EXISTS替代IN、用NOT EXISTS替代NOT IN。

(3) 避免在索引列上使用计算

(4)避免在索引列上使用IS NULL和IS NOT NULL

(5)对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。  

(6)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描

(7)应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描

如何进行SQL优化?

(1)选择正确的存储引擎

以 MySQL为例,包括有两个存储引擎 MyISAM 和 InnoDB,每个引擎都有利有弊。

MyISAM 适合于一些需要大量查询的应用,但其对于有大量写操作并不是很好。甚至你只是需要update一个字段,整个表都会被锁起来,而别的进程,就算是读进程都无法操作直到读操作完成。另外,MyISAM 对于 SELECT COUNT(*) 这类的计算是超快无比的。

InnoDB 的趋势会是一个非常复杂的存储引擎,对于一些小的应用,它会比 MyISAM 还慢。但是它支持“行锁” ,于是在写操作比较多的时候,会更优秀。并且,他还支持更多的高级应用,比如:事务。

(2)优化字段的数据类型

记住一个原则,越小的列会越快。如果一个表只会有几列罢了(比如说字典表,配置表),那么,我们就没有理由使用 INT 来做主键,使用 MEDIUMINT, SMALLINT 或是更小的 TINYINT 会更经济一些。如果你不需要记录时间,使用 DATE 要比 DATETIME 好得多。当然,你也需要留够足够的扩展空间。

(3)为搜索字段添加索引

索引并不一定就是给主键或是唯一的字段。如果在你的表中,有某个字段你总要会经常用来做搜索,那么最好是为其建立索引,除非你要搜索的字段是大的文本字段,那应该建立全文索引。

(4)避免使用Select 从数据库里读出越多的数据,那么查询就会变得越慢。并且,如果你的数据库服务器和WEB服务器是两台独立的服务器的话,这还会增加网络传输的负载。即使你要查询数据表的所有字段,也尽量不要用通配符,善用内置提供的字段排除定义也许能给带来更多的便利。

(5)使用 ENUM 而不是 VARCHAR

ENUM 类型是非常快和紧凑的。在实际上,其保存的是 TINYINT,但其外表上显示为字符串。这样一来,用这个字段来做一些选项列表变得相当的完美。例如,性别、民族、部门和状态之类的这些字段的取值是有限而且固定的,那么,你应该使用 ENUM 而不是 VARCHAR。

(6)尽可能的使用 NOT NULL

除非你有一个很特别的原因去使用 NULL 值,你应该总是让你的字段保持 NOT NULL。 NULL其实需要额外的空间,并且,在你进行比较的时候,你的程序会更复杂。 当然,这里并不是说你就不能使用NULL了,现实情况是很复杂的,依然会有些情况下,你需要使用NULL值。

(7)固定长度的表会更快

如果表中的所有字段都是“固定长度”的,整个表会被认为是 “static” 或 “fixed-length”。 例如,表中没有如下类型的字段: VARCHAR,TEXT,BLOB。只要你包括了其中一个这些字段,那么这个表就不是“固定长度静态表”了,这样,MySQL 引擎会用另一种方法来处理。

固定长度的表会提高性能,因为MySQL搜寻得会更快一些,因为这些固定的长度是很容易计算下一个数据的偏移量的,所以读取的自然也会很快。而如果字段不是定长的,那么,每一次要找下一条的话,需要程序找到主键。并且,固定长度的表也更容易被缓存和重建。不过,唯一的副作用是,固定长度的字段会浪费一些空间,因为定长的字段无论你用不用,他都是要分配那么多的空间。

‘相关子查询’与‘非相关子查询’有什么区别?

答:子查询:嵌套在其他查询中的查询称之。

子查询又称内部,而包含子查询的语句称之外部查询(又称主查询)。

所有的子查询可以分为两类,即相关子查询和非相关子查询

(1)非相关子查询是独立于外部查询的子查询,子查询总共执行一次,执行完毕后将值传递给外部查询。

(2)相关子查询的执行依赖于外部查询的数据,外部查询执行一行,子查询就执行一次。

故非相关子查询比相关子查询效率高

MySQL数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?

a. 设计良好的数据库结构,允许部分数据冗余,尽量避免join查询,提高效率。

b. 选择合适的表字段数据类型和存储引擎,适当的添加索引。

c. mysql库主从读写分离。

d. 找规律分表,减少单表中的数据量提高查询速度。

e。添加缓存机制,比如memcached,apc等。

f. 不经常改动的页面,生成静态页面。

g. 书写高效率的SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE.

char和varchar的区别?

答:是一种固定长度的类型,varchar则是一种可变长度的类型,它们的区别是:

char(M)类型的数据列里,每个值都占用M个字节,如果某个长度小于M,MySQL就会在它的右边用空格字符补足.(在检索操作中那些填补出来的空格字符将被去掉)在varchar(M)类型的数据列里,每个值只占用刚好够用的字节再加上一个用来记录其长度的字节(即总长度为L+1字节).

varchar的适用场景:

  • 字符串列的最大长度比平均长度大很多
  • 字符串很少被更新,容易产生存储碎片
  • 使用多字节字符集存储字符串

Char的场景:

存储具有近似得长度(md5值,身份证,手机号),长度比较短小得字符串(因为varchar需要额外空间记录字符串长度),更适合经常更新得字符串,更新时不会出现页分裂得情况,避免出现存储碎片,获得更好的io性能

主键、外键和索引的区别

定义:

主键–唯一标识一条记录,不能有重复的,不允许为空

外键–表的外键是另一表的主键, 外键可以有重复的, 可以是空值

索引–该字段没有重复值,但可以有一个空值

作用:

主键–用来保证数据完整性

外键–用来和其他表建立联系用的

索引–是提高查询排序的速度

个数:

主键–主键只能有一个

外键–一个表可以有多个外键

索引–一个表可以有多个唯一索引

MySQL外连接、内连接与自连接的区别

先说什么是交叉连接: 交叉连接又叫笛卡尔积,它是指不使用任何条件,直接将一个表的所有记录和另一个表中的所有记录一一匹配。

内连接 则是只有条件的交叉连接,根据某个条件筛选出符合条件的记录,不符合条件的记录不会出现在结果集中,即内连接只连接匹配的行。

外连接 其结果集中不仅包含符合连接条件的行,而且还会包括左表、右表或两个表中的所有数据行,这三种情况依次称之为左外连接,右外连接,和全外连接。

左外连接,也称左连接,左表为主表,左表中的所有记录都会出现在结果集中,对于那些在右表中并没有匹配的记录,仍然要显示,右边对应的那些字段值以NULL来填充。

右外连接,也称右连接,右表为主表,右表中的所有记录都会出现在结果集中。左连接和右连接可以互换,MySQL目前还不支持全外连接。

Myql中的事务回滚机制

事务是用户定义的一个数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的工作单位,事务回滚是指将该事务已经完成的对数据库的更新操作撤销。

要同时修改数据库中两个不同表时,如果它们不是一个事务的话,当第一个表修改完,可能第二个表修改过程中出现了异常而没能修改,此时就只有第二个表依旧是未修改之前的状态,而第一个表已经被修改完毕。而当你把它们设定为一个事务的时候,当第一个表修改完,第二表修改出现异常而没能修改,第一个表和第二个表都要回到未修改的状态,这就是所谓的事务回滚

SQL语言包括哪几部分?每部分都有哪些操作关键字?

答:SQL语言包括数据定义(DDL)、数据操纵(DML),数据控制(DCL)和数据查询(DQL)四个部分。

数据定义:Create Table,Alter Table,Drop Table, Craete/Drop Index等

数据操纵:Select ,insert,update,delete,

数据控制:grant,revoke

数据查询:select

什么是事务

事务(transaction)是作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功,则认为事务成功,即使只有一个操作失败,事务也不成功。如果所有操作完成,事务则提交,其修改将作用于所有其他数据库进程。如果一个操作失败,则事务将回滚,该事务所有操作的影响都将取消。ACID 四大特性,原子性、隔离性、一致性、持久性。

事务特性:

(1)原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。

(2)一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态

(3)隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,

(4) 持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。

什么是锁?

数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。

加锁是实现数据库并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。

基本锁类型:锁包括行级锁和表级锁

什么叫视图?

答:视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,视图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。

游标是什么?

游标:是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。

什么是存储过程?

答:存储过程是一个预编译的SQL语句,优点是允许模块化的设计,就是说只需创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比单纯SQL语句执行要快。可以用一个命令对象来调用存储过程。

完整性约束包括哪些?

答:数据完整性(Data Integrity)是指数据的精确(Accuracy)和可靠性(Reliability)。

分为以下四类:

\1) 实体完整性:规定表的每一行在表中是惟一的实体。

\2) 域完整性:是指表中的列必须满足某种特定的数据类型约束,其中约束又包括取值范围、精度等规定。

\3) 参照完整性:是指两个表的主关键字和外关键字的数据应一致,保证了表之间的数据的一致性,防止了数据丢失或无意义的数据在数据库中扩散。

\4) 用户定义的完整性:不同的关系数据库系统根据其应用环境的不同,往往还需要一些特殊的约束条件。用户定义的完整性即是针对某个特定关系数据库的约束条件,它反映某一具体应用必须满足的语义要求。

与表有关的约束:包括列约束(NOT NULL(非空约束))和表约束(PRIMARY KEY、foreign key、check、UNIQUE) 。

xxs攻击

XSS是跨站脚本攻击,首先是利用跨站脚本漏洞以一个特权模式去执行攻击者构造的脚本,然后利用不安全的Activex控件执行恶意的行为。

使用htmlspecialchars()函数对提交的内容进行过滤,使字符串里面的特殊符号实体化。

高并发下接口幂等性解决方案

一、幂等性概念
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。

二、幂等性场景
1、查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;

2、删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;

3、唯一索引:防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);

4、token机制:防止页面重复提交。

原理上通过session token来实现的(也可以通过redis来实现)。当客户端请求页面时,服务器会生成一个随机数Token,并且将Token放置到session当中,然后将Token发给客户端(一般通过构造hidden表单)。
下次客户端提交请求时,Token会随着表单一起提交到服务器端。

服务器端第一次验证相同过后,会将session中的Token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的Token没变,但服务器端session中Token已经改变了。

5、悲观锁
获取数据的时候加锁获取。select * from table_xxx where id=’xxx’ for update; 注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;

6、乐观锁——乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:
\1. 通过版本号实现update table_xxx set name=#name#,version=version+1 where version=#version#如下图(来自网上);
\2. 通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;

7、分布式锁

如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);

8、select + insert
并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法;

9、状态机幂等
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助

10、对外提供接口的api如何保证幂等
如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号;source+seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求) 。
重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。

三、总结
幂等与你是不是分布式高并发还有JavaEE都没有关系。关键是你的操作是不是幂等的。一个幂等的操作典型如:把编号为5的记录的A字段设置为0这种操作不管执行多少次都是幂等的。一个非幂等的操作典型如:把编号为5的记录的A字段增加1这种操作显然就不是幂等的。要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。譬如说需求是:当用户点击赞同时,将答案的赞同数量+1。改为:当用户点击赞同时,确保答案赞同表中存在一条记录,用户、答案。赞同数量由答案赞同表统计出来。总之幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好。

最左匹配原则

最左匹配原则就是指在联合索引中,如果你的 SQL 语句中用到了联合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个联合索引去进行匹配。例如某表现有索引(a,b,c),现在你有如下语句:

1
2
3
4
5
6
7
8
9
10
11
select * from t where a=1 and b=1 and c =1;     #这样可以利用到定义的索引(a,b,c),用上a,b,c

select * from t where a=1 and b=1; #这样可以利用到定义的索引(a,b,c),用上a,b

select * from t where b=1 and a=1; #这样可以利用到定义的索引(a,b,c),用上a,c(mysql有查询优化器)

select * from t where a=1; #这样也可以利用到定义的索引(a,b,c),用上a

select * from t where b=1 and c=1; #这样不可以利用到定义的索引(a,b,c)

select * from t where a=1 and c=1; #这样可以利用到定义的索引(a,b,c),但只用上a索引,b,c索引用不到

也就是说通过最左匹配原则你可以定义一个联合索引,但是使得多中查询条件都可以用到该索引。
值得注意的是,当遇到范围查询(>、<、between、like)就会停止匹配。也就是:

1
select * from t where a=1 and b>1 and c =1; #这样a,b可以用到(a,b,c),c索引用不到 

这条语句只有 a,b 会用到索引,c 都不能用到索引。这个原因可以从联合索引的结构来解释。

但是如果是建立(a,c,b)联合索引,则a,b,c都可以使用索引,因为优化器会自动改写为最优查询语句

1
2
3
select * from t where a=1 and b >1 and c=1;  #如果是建立(a,c,b)联合索引,则a,b,c都可以使用索引
#优化器改写为
select * from t where a=1 and c=1 and b >1;

这也是最左前缀原理的一部分,索引index1:(a,b,c),只会走a、a,b、a,b,c 三种类型的查询,其实这里说的有一点问题,a,c也走,但是只走a字段索引,不会走c字段。

另外还有一个特殊情况说明下,select * from table where a = ‘1’ and b > ‘2’ and c=’3’ 这种类型的也只会有 a与b 走索引,c不会走。

像select * from table where a = ‘1’ and b > ‘2’ and c=’3’ 这种类型的sql语句,在a、b走完索引后,c肯定是无序了,所以c就没法走索引,数据库会觉得还不如全表扫描c字段来的快。

以index (a,b,c)为例建立这样的索引相当于建立了索引a、ab、abc三个索引。一个索引顶三个索引当然是好事,毕竟每多一个索引,都会增加写操作的开销和磁盘空间的开销。

最左匹配原则的原理

最左匹配原则都是针对联合索引来说的,所以我们可以从联合索引的原理来了解最左匹配原则。

我们都知道索引的底层是一颗 B+ 树,那么联合索引当然还是一颗 B+ 树,只不过联合索引的健值数量不是一个,而是多个。构建一颗 B+ 树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建 B+ 树。
例子:假如创建一个(a,b,c)的联合索引,那么它的索引树是这样的:
img

该图就是一个形如(a,b,c)联合索引的 b+ 树,其中的非叶子节点存储的是第一个关键字的索引 a,而叶子节点存储的是三个关键字的数据。这里可以看出 a 是有序的,而 b,c 都是无序的。但是当在 a 相同的时候,b 是有序的,b 相同的时候,c 又是有序的。
通过对联合索引的结构的了解,那么就可以很好的了解为什么最左匹配原则中如果遇到范围查询就会停止了。以 select * from t where a=5 and b>0 and c =1; #这样a,b可以用到(a,b,c),c不可以 为例子,当查询到 b 的值以后(这是一个范围值),c 是无序的。所以就不能根据联合索引来确定到低该取哪一行。

总结

  • 在 InnoDB 中联合索引只有先确定了前一个(左侧的值)后,才能确定下一个值。如果有范围查询的话,那么联合索引中使用范围查询的字段后的索引在该条 SQL 中都不会起作用。
  • 值得注意的是,in= 都可以乱序,比如有索引(a,b,c),语句 select * from t where c =1 and a=1 and b=1,这样的语句也可以用到最左匹配,因为 MySQL 中有一个优化器,他会分析 SQL 语句,将其优化成索引可以匹配的形式,即 select * from t where a =1 and a=1 and c=1

为什么要使用联合索引

1、减少开销。建一个联合索引(col1,col2,col3),实际相当于建了(col1),(col1,col2),(col1,col2,col3)三个索引。每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销!

2、覆盖索引。对联合索引(col1,col2,col3),如果有如下的sql: select col1,col2,col3 from test where col1=1 and col2=2。那么MySQL可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io操作。减少io操作,特别的随机io其实是dba主要的优化策略。所以,在真正的实际应用中,覆盖索引是主要的提升性能的优化手段之一。

3、效率高。索引列越多,通过索引筛选出的数据越少。有1000W条数据的表,有如下sql:select from table where col1=1 and col2=2 and col3=3,假设假设每个条件可以筛选出10%的数据,如果只有单值索引,那么通过该索引能筛选出1000W10%=100w条数据,然后再回表从100w条数据中找到符合col2=2 and col3= 3的数据,然后再排序,再分页;如果是联合索引,通过索引筛选出1000w10% 10% *10%=1w,效率提升可想而知!

使用索引优化查询问题:
1、创建单列索引还是多列索引?
如果查询语句中的where、order by、group 涉及多个字段,一般需要创建多列索引,比如:

select * from user where nick_name = ‘ligoudan’ and job = ‘dog’;
2、多列索引的顺序如何选择?
一般情况下,把选择性高德字段放在前面,比如:
查询sql:

select * from user where age = ‘20’ and name = ‘zh’ order by nick_name;
这时候如果建索引的话,首字段应该是age,因为age定位到的数据更少,选择性更高。
但是务必注意一点,满足了某个查询场景就可能导致另外一个查询场景更慢。
3、避免使用范围查询
很多情况下,范围查询都可能导致无法使用索引。
4、尽量避免查询不需要的数据

explain select * from user where job like ‘%ligoudan%’;
explain select job from user where job like ‘%ligoudan%’;
同样的查询,不同的返回值,第二个就可以使用覆盖索引,第一个只能全表遍历了。
5、查询的数据类型要正确

explain select * from user where create_date >= now();
explain select * from user where create_date >= ‘2020-05-01 00:00:00’;
第一条语句就可以使用create_date的索引,第二个就不可以。

sql注入

SQL注入产生的原因:程序开发过程中不注意规范书写sql语句和对特殊字符进行过滤,导致客户端可以通过全局变量POST和GET提交一些sql语句正常执行。

防止SQL注入的方式:

  • 开启配置文件中的magic_quotes_gpc 和 magic_quotes_runtime设置
  • 执行sql语句时使用addslashes进行sql语句转换
  • Sql语句书写尽量不要省略双引号和单引号。
  • 过滤掉sql语句中的一些关键词:update、insert、delete、select、 * 。
  • 提高数据库表和字段的命名技巧,对一些重要的字段根据程序的特点命名,取不易被猜到的。
  • Php配置文件中设置register_globals为off,关闭全局变量注册
  • 控制错误信息,不要在浏览器上输出错误信息,将错误信息写到日志文件中。

MySQL索引-B+树

索引是一种数据结构,用于帮助我们在大量数据中快速定位到我们想要查找的数据。
索引最形象的比喻就是图书的目录了。注意这里的大量,数据量大了索引才显得有意义,如果我想要在 [1,2,3,4] 中找到 4 这个数据,直接对全数据检索也很快,没有必要费力气建索引再去查找。

索引在 MySQL 数据库中分三类:

  • B+ 树索引
  • Hash 索引
  • 全文索引

我们今天要介绍的是工作开发中最常接触到的 InnoDB 存储引擎中的 B+ 树索引。要介绍 B+ 树索引,就不得不提二叉查找树,平衡二叉树和 B 树这三种数据结构。B+ 树就是从他们仨演化来的。

二叉查找树

首先,让我们先看一张图:

image-20240312174107039

从图中可以看到,我们为 user 表(用户信息表)建立了一个二叉查找树的索引。

图中的圆为二叉查找树的节点,节点中存储了键(key)和数据(data)。键对应 user 表中的 id,数据对应 user 表中的行数据。

二叉查找树的特点就是任何节点的左子节点的键值都小于当前节点的键值,右子节点的键值都大于当前节点的键值。顶端的节点我们称为根节点,没有子节点的节点我们称之为叶节点。

如果我们需要查找 id=12 的用户信息,利用我们创建的二叉查找树索引,查找流程如下:

  • 将根节点作为当前节点,把 12 与当前节点的键值 10 比较,12 大于 10,接下来我们把当前节点>的右子节点作为当前节点。
  • 继续把 12 和当前节点的键值 13 比较,发现 12 小于 13,把当前节点的左子节点作为当前节点。
  • 把 12 和当前节点的键值 12 对比,12 等于 12,满足条件,我们从当前节点中取出 data,即 id=12,name=xm。

利用二叉查找树我们只需要 3 次即可找到匹配的数据。如果在表中一条条的查找的话,我们需要 6 次才能找到。

平衡二叉树

上面我们讲解了利用二叉查找树可以快速的找到数据。但是,如果上面的二叉查找树是这样的构造:

这个时候可以看到我们的二叉查找树变成了一个链表。如果我们需要查找 id=17 的用户信息,我们需要查找 7 次,也就相当于全表扫描了。

导致这个现象的原因其实是二叉查找树变得不平衡了,也就是高度太高了,从而导致查找效率的不稳定。

为了解决这个问题,我们需要保证二叉查找树一直保持平衡,就需要用到平衡二叉树了。

平衡二叉树又称 AVL 树,在满足二叉查找树特性的基础上,要求每个节点的左右子树的高度差不能超过 1。

下面是平衡二叉树和非平衡二叉树的对比:

由平衡二叉树的构造我们可以发现第一张图中的二叉树其实就是一棵平衡二叉树。

平衡二叉树保证了树的构造是平衡的,当我们插入或删除数据导致不满足平衡二叉树不平衡时,平衡二叉树会进行调整树上的节点来保持平衡。具体的调整方式这里就不介绍了。

平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。

B 树

因为内存的易失性。一般情况下,我们都会选择将 user 表中的数据和索引存储在磁盘这种外围设备中。

但是和内存相比,从磁盘中读取数据的速度会慢上百倍千倍甚至万倍,所以,我们应当尽量减少从磁盘中读取数据的次数。

另外,从磁盘中读取数据时,都是按照磁盘块来读取的,并不是一条一条的读。

如果我们能把尽量多的数据放进磁盘块中,那一次磁盘读取操作就会读取更多数据,那我们查找数据的时间也会大幅度降低。

如果我们用树这种数据结构作为索引的数据结构,那我们每查找一次数据就需要从磁盘中读取一个节点,也就是我们说的一个磁盘块。

我们都知道平衡二叉树可是每个节点只存储一个键值和数据的。那说明什么?说明每个磁盘块仅仅存储一个键值和数据!那如果我们要存储海量的数据呢?

可以想象到二叉树的节点将会非常多,高度也会极其高,我们查找数据时也会进行很多次磁盘 IO,我们查找数据的效率将会极低!

为了解决平衡二叉树的这个弊端,我们应该寻找一种单个节点可以存储多个键值和数据的平衡树。也就是我们接下来要说的 B 树。

B 树(Balance Tree)即为平衡树的意思,下图即是一棵 B 树:

图中的 p 节点为指向子节点的指针,二叉查找树和平衡二叉树其实也有,因为图的美观性,被省略了。

图中的每个节点称为页,页就是我们上面说的磁盘块,在 MySQL 中数据读取的基本单位都是页,所以我们这里叫做页更符合 MySQL 中索引的底层数据结构。

从上图可以看出,B 树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的 B 树为 3 阶 B 树,高度也会很低。

基于这个特性,B 树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。

假如我们要查找 id=28 的用户信息,那么我们在上图 B 树中查找的流程如下:

  • 先找到根节点也就是页 1,判断 28 在键值 17 和 35 之间,那么我们根据页 1 中的指针 p2 找到页 3。
  • 将 28 和页 3 中的键值相比较,28 在 26 和 30 之间,我们根据页 3 中的指针 p2 找到页 8。
  • 将 28 和页 8 中的键值相比较,发现有匹配的键值 28,键值 28 对应的用户信息为(28,bv)。

B+ 树

B+ 树是对 B 树的进一步优化。让我们先来看下 B+ 树的结构图:

根据上图我们来看下 B+ 树和 B 树有什么不同:

①B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。

之所以这么做是因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16KB。

如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。

另外,B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。

一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。

②因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。

那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而 B 树因为数据分散在各个节点,要实现这一点是很不容易的。

有心的读者可能还发现上图 B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。

其实上面的 B 树我们也可以对各个节点加上链表。这些不是它们之前的区别,是因为在 MySQL 的 InnoDB 存储引擎中,索引就是这样存储的。

也就是说上图中的 B+ 树索引就是 InnoDB 中 B+ 树索引真正的实现方式,准确的说应该是聚集索引(聚集索引和非聚集索引下面会讲到)。

通过上图可以看到,在 InnoDB 中,我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。

MyISAM 中的 B+ 树索引实现与 InnoDB 中的略有不同。在 MyISAM 中,B+ 树索引的叶子节点并不存储数据,而是存储数据的文件地址。

聚集索引 VS 非聚集索引

在上节介绍 B+ 树索引的时候,我们提到了图中的索引其实是聚集索引的实现方式。

那什么是聚集索引呢?在 MySQL 中,B+ 树索引按照存储方式的不同分为聚集索引和非聚集索引。

这里我们着重介绍 InnoDB 中的聚集索引和非聚集索引:

①****聚集索引(聚簇索引):以 InnoDB 作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。

这是因为 InnoDB 是把数据存放在 B+ 树中的,而 B+ 树的键值就是主键,在 B+ 树的叶子节点中,存储了表中所有的数据。

这种以主键作为 B+ 树索引的键值而构建的 B+ 树索引,我们称之为聚集索引。

②****非聚集索引(非聚簇索引):以主键以外的列值作为键值构建的 B+ 树索引,我们称之为非聚集索引。

非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。

明白了聚集索引和非聚集索引的定义,我们应该明白这样一句话:数据即索引,索引即数据。

利用聚集索引和非聚集索引查找数据

前面我们讲解 B+ 树索引的时候并没有去说怎么在 B+ 树中进行数据的查找,主要就是因为还没有引出聚集索引和非聚集索引的概念。

下面我们通过讲解如何通过聚集索引以及非聚集索引查找数据表中数据的方式介绍一下 B+ 树索引查找数据方法。

利用聚集索引查找数据

还是这张 B+ 树索引图,现在我们应该知道这就是聚集索引,表中的数据存储在其中。

现在假设我们要查找 id>=18 并且 id<40 的用户数据。对应的 sql 语句为:

MySQL

1select * from user where id>=18 and id <40

其中 id 为主键,具体的查找过程如下:

①一般根节点都是常驻内存的,也就是说页 1 已经在内存中了,此时不需要到磁盘中读取数据,直接从内存中读取即可。

从内存中读取到页 1,要查找这个 id>=18 and id <40 或者范围值,我们首先需要找到 id=18 的键值。

从页 1 中我们可以找到键值 18,此时我们需要根据指针 p2,定位到页 3。

②要从页 3 中查找数据,我们就需要拿着 p2 指针去磁盘中进行读取页 3。

从磁盘中读取页 3 后将页 3 放入内存中,然后进行查找,我们可以找到键值 18,然后再拿到页 3 中的指针 p1,定位到页 8。

③同样的页 8 页不在内存中,我们需要再去磁盘中将页 8 读取到内存中。

将页 8 读取到内存中后。因为页中的数据是链表进行连接的,而且键值是按照顺序存放的,此时可以根据二分查找法定位到键值 18。

此时因为已经到数据页了,此时我们已经找到一条满足条件的数据了,就是键值 18 对应的数据。

因为是范围查找,而且此时所有的数据又都存在叶子节点,并且是有序排列的,那么我们就可以对页 8 中的键值依次进行遍历查找并匹配满足条件的数据。

我们可以一直找到键值为 22 的数据,然后页 8 中就没有数据了,此时我们需要拿着页 8 中的 p 指针去读取页 9 中的数据。

④因为页 9 不在内存中,就又会加载页 9 到内存中,并通过和页 8 中一样的方式进行数据的查找,直到将页 12 加载到内存中,发现 41 大于 40,此时不满足条件。那么查找到此终止。

最终我们找到满足条件的所有数据,总共 12 条记录:

(18,kl), (19,kl), (22,hj), (24,io), (25,vg) , (29,jk), (31,jk) , (33,rt) , (34,ty) , (35,yu) , (37,rt) , (39,rt) 。

下面看下具体的查找流程图

利用非聚集索引查找数据

读者看到这张图的时候可能会蒙,这是啥东西啊?怎么都是数字。如果有这种感觉,请仔细看下图中红字的解释。

什么?还看不懂?那我再来解释下吧。首先,这个非聚集索引表示的是用户幸运数字的索引(为什么是幸运数字?一时兴起想起来的:-)),此时表结构是这样的。

img

在叶子节点中,不再存储所有的数据了,存储的是键值和主键。对于叶子节点中的 x-y,比如 1-1。左边的 1 表示的是索引的键值,右边的 1 表示的是主键值。

如果我们要找到幸运数字为 33 的用户信息,对应的 sql 语句为:

MySQL

1select * from user where luckNum=33

查找的流程跟聚集索引一样,这里就不详细介绍了。我们最终会找到主键值 47,找到主键后我们需要再到聚集索引中查找具体对应的数据信息,此时又回到了聚集索引的查找流程。

下面看下具体的查找流程图:

在 MyISAM 中,聚集索引和非聚集索引的叶子节点都会存储数据的文件地址。

总结

本篇文章从二叉查找树,详细说明了为什么 MySQL 用 B+ 树作为数据的索引,以及在 InnoDB 中数据库如何通过 B+ 树索引来存储数据以及查找数据。

我们一定要记住这句话:数据即索引,索引即数据。


并发和并行

并发和并行最开始都是操作系统中的概念,表示的是CPU执行多个任务的方式

并发

并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

我们现在使用的windows操作系统,是可以”同时”做很多件事儿的。比如我们可以一边看电影,一边聊QQ;一边听歌,一边打游戏。

但是,这所谓的”同时”,在操作系统底层可能并不是真正的意义上的”同时”。

实际上,对于单CPU的计算机来说,在CPU中,同一时间是只能干一件事儿的。为了看起来像是“同时干多件事”,Windows这种操作系统是把CPU的时间划分成长短基本相同的时间区间,即”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个应用使用。

这样,给用户的感觉是他在同时的进行听歌和打游戏,实际上,在操作系统中,CPU是在游戏进程和音乐播放器进程之间来回切换执行的。

操作系统时间片的使用是有规则的:某个作业在时间片结束之前,整个任务还没有完成,那么该作业就被暂停下来,放弃CPU,等待下一轮循环再继续做。此时CPU又分配给另一个作业去使用。

我们把目光聚焦在CPU的执行上,把这个过程放大的话,CPU就好像是一个电话亭。多个用户并不是同一时间在使用这个电话亭中的电话的,而是轮流使用的。

由于计算机的处理速度很快,只要时间片的间隔取得适当,那么一个用户作业从用完分配给它的一个时间片到获得下一个CPU时间片,中间有所”停顿”,但用户察觉不出来。

所以,在单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术,并发完成的。

就想前面提到的操作系统的时间片分时调度。打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。

并行

并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。

并发并行总结

并发是指在一段时间内宏观上多个程序同时运行。

并行指的是同一个时刻,多个任务确实真的在同时运行。

举例:

我们两个人在吃午饭。你在吃饭的整个过程中,吃了米饭、吃了蔬菜、吃了牛肉。吃米饭、吃蔬菜、吃牛肉这三件事其实就是并发执行的。对于你来说,整个过程中看似是同时完成的的。但其实你是在吃不同的东西之间来回切换的。

还是我们两个人吃午饭。在吃饭过程中,你吃了米饭、蔬菜、牛肉。我也吃了米饭、蔬菜和牛肉。我们两个人之间的吃饭就是并行的。两个人之间可以在同一时间点一起吃牛肉,或者一个吃牛肉,一个吃蔬菜。之间是互不影响的。

并发和并行的区别

并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的、

只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

简述Django的orm

ORM,全拼Object-Relation Mapping,意为对象-关系映射

实现了数据模型与数据库的解耦,通过简单的配置就可以轻松更换数据库,而不需要修改代码只需要面向对象编程,orm操作本质上会根据对接的数据库引擎,翻译成对应的sql语句,所有使用Django开发的项目无需关心程序底层使用的是MySQL、Oracle、sqlite….,如果数据库迁移,只需要更换Django的数据库引擎即可

img

三次握手四次挥手

三次握手
当面试官问你为什么需要有三次握手、三次握手的作用、讲讲三次三次握手的时候,我想很多人会这样回答:

  首先很多人会先讲下握手的过程:

  1、第一次握手:客户端给服务器发送一个 SYN 报文。

  2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。

  3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。

  4、服务器收到 ACK 报文之后,三次握手建立完成。

  作用是为了确认双方的接收与发送能力是否正常。

  这里我顺便解释一下为啥只有三次握手才能确认双方的接受与发送能力是否正常,而两次却不可以:

  第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

  因此,需要三次握手才能确认双方的接收与发送能力是否正常。

  这样回答其实也是可以的,但我觉得,这个过程的我们应该要描述的更详细一点,因为三次握手的过程中,双方是由很多状态的改变的,而这些状态,也是面试官可能会问的点。所以我觉得在回答三次握手的时候,我们应该要描述的详细一点,而且描述的详细一点意味着可以扯久一点。加分的描述我觉得应该是这样:

  刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后
  1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Send 状态。

  2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。

  3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。

  4、服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。

三次握手的作用
三次握手的作用也是有好多的,多记住几个,保证不亏。例如:
1、确认双方的接受能力、发送能力是否正常。
2、指定自己的初始化序列号,为后面的可靠传送做准备。
3、如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成到。

  单单这样还不足以应付三次握手,面试官可能还会问一些其他的问题,例如:

1、(ISN)是固定的吗?
三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。

  如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

2、什么是半连接队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

  这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, …

3、三次握手过程中可以携带数据吗
很多人可能会认为三次握手都不能携带数据,其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。
而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。

四次挥手
四次挥手也一样,千万不要对方一个 FIN 报文,我方一个 ACK 报文,再我方一个 FIN 报文,我方一个 ACK 报文。然后结束,最好是说的详细一点,例如想下面这样就差不多了,要把每个阶段的状态记好。

  刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:

  1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。

  2、第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。

  3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

  4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态

  5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。


  这里特别需要主要的就是TIME_WAIT这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。

  至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。

  这里我给出每个状态所包含的含义,有兴趣的可以看看。

LISTEN - 侦听来自远方TCP端口的连接请求;
SYN-SENT -在发送连接请求后等待匹配的连接请求;
SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认;
ESTABLISHED- 代表一个打开的连接,数据可以传送给用户;
FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
FIN-WAIT-2 - 从远程TCP等待连接中断请求;
CLOSE-WAIT - 等待从本地用户发来的连接中断请求;
CLOSING -等待远程TCP对连接中断的确认;
LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;
TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认;
CLOSED - 没有任何连接状态;

原文链接:https://blog.csdn.net/dreamispossible/article/details/91345391

http相关

跨域、同源策略

同源策略需要同时满足以下三点要求:

1)协议相同

2)域名相同

3)端口相同

http://www.test.com与https://www.test.com 不同源——协议不同

http://www.test.com与http://www.admin.com 不同源——域名不同

http://www.test.com与http://www.test.com:8081 不同源——端口不同

只要不满足其中任意一个要求,就不符合同源策略,就会出现“跨域”

前端

vue

vue事件循环机制

JS 里的一种任务分类方式分为: 同步任务异步任务

虽然 JS 是单线程的,但是浏览器的内核却是多线程的,在浏览器的内核中不同的异步操作由不同的浏览器内核模块调度执行,异步任务操作会将相关回调添加到任务队列中

而不同的异步操作添加到任务队列的时机也不同,比如onclick, setTimeout, ajax 处理的方式都不同

这些异步操作是由浏览器内核来执行的,浏览器内核上包含 3 种 webAPI,分别是 DOM Binding(DOM绑定)network(网络请求)timer(定时器)模块

按照这种分类方式:JS 的执行机制是

  • 首先判断 js 代码是同步还是异步,不停的检查调用栈中是否有任务需要执行,如果没有,就检查任务队列,从中弹出一个任务,放入栈中,如此往复循环,要是同步就进入主进程,异步就进入事件表
  • 异步任务在事件表中注册函数,当满足触发条件后,被推入事件队列
  • **同步任务进入主线程后一直执行,直到主线程空闲时,才会去事件队列中查看是否有可执行的异步任务,**如果有就推入主进程中

以上三步循环执行,这就是事件循环(event loop),它是连接任务队列和控制调用栈的

vuex作用

①vuex是一个状态管理的插件,可以解决不同组件之间的数据共享和数据持久化。

②vue中的多个组件之间的通讯,不同组件的行为需要变更同一状态。虽然我们经常会采用父子组件直接引用或者通过事件来变更和同 步状态的多份拷贝,这些模式比较脆弱,通常会导致无法维护代码,此时就可以用vuex.

axios+vue实现登入拦截

①路由拦截,需要在定义路由的时候添加一个字段requireAuth,用于判断该路由的访问是否需要登入,如果用户已经登入,则可以跳 转到路由,否则就进入到登入页面,登入成功后跳转到目标路由。

②定义完路由之后,我们通过vue-router提供的钩子函数beforeEach()对路由进行判断,代码如下:

1
2
3
4
5
6
7
8
router.beforeEach((to,from,next) => {
if(to.meta.requireAuth){ //判断该路由是否需要登入权限
if(!store.state.token){ //通过vuex state获取当前的token是否存在
router.push('/login');//不存在则跳转到登入页面
}
next();//进行下一个钩子
}
});

③如果当token失效了,但是token依然保存在本地。这时候你去访问需要登入权限的路由时,实际上需要让用户重新登入,这时候就 需要结合http拦截器+后端接口返回的http状态码来判断,axios拦截器分为两种,见下代码:

1
2
3
4
5
6
7
8
9
10
11
axios.interceptors.request.use(
(config) => {
// 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的 header都加上token,不用每次请求都手动添加了
if(store.state.token){
config.headers.token = store.state.token;
}
return config;
},
(error) => {
return Promise.reject(error);
});

http response拦截器

1
2
3
4
5
6
7
8
9
axios.interceptors.response.use(
(response) => { //用来判断响应状态
return response;
//return Promise.resolve(response);
},
(error) => {
return Promise.reject(error);
}
)

什么是webpack

①打包:可以把多个JavaScript文件打包成一个文件,减少服务器压力和下载宽带

②转换:把扩展语言转换成为普通的JavaScript,让浏览器顺利运行。

③优化:肩负起了优化和提升性能的责任

hash模式

vue-router默认是hash模式,使用URL的hash来模拟一个完整的URL,于是当URL改变时,页面不会重新加载

computed和watch的原理

1.1通过watch的方法,监听被改变的变量,然后再watch的那个变量命名的函数中去对我们要修改的值进行重新的赋值,或者是触发一 次更新。watch的执行类似于emit与on这种触发方式,通过vue的watch实例监听值来自动触发一个函数的执行。

1.2computed监听变量,通过return一个新的变量的方式来更新一个变量的数据,computed函数的执行最快,在HTML渲染开始就已经 执行了

1.3应用场景

①watch的触发消耗大,每次数据的改变就要触发一次函数的执行,这不太友好。

②computed在改变一个变量时,和data对象里的数据属性是同一类的。返回的值直接就修改掉了原来的值,最大的优点在于简洁、代 码少当多次调用computed属性是,调用的其实是缓存。

子父组件传值

父组件向子组件传值

父组件给子组件传值,组件中通过props属性传递数据。Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个 property。

一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data 中的值一样。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>vue组件</title>
<style type="text/css">
div {
padding: 10px;
}

/* 全局组件使用的样式 */
.liyuan_css {
background-color: coral;
}

.lishimin_css {
background-color: forestgreen;
}

.liyuanji_css {
background-color: royalblue;
}
</style>
</head>
<body>
<div id="app">
<!-- 将组件名以标签形式添加到html中,调用组件中的内容 -->
<liyuan></liyuan>

</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
// 创建局部组件
let Liyuanji = {
// data属性指定绑定的数据内容,可以在当前的组件中进行使用
data: function() {
return {
name: "李元吉"
}
},
// 在子组件中通过props属性定义接受值的名称
props: ['message'],
// template指定组件内显示的html内容;// 在全局组件中调用子组件时,通过attribute指定子组件中pos接受父组件中的哪个值
template: `<div class="liyuanji_css">我是局部组件【{{name}}】,有人发来消息说:{{message}}</div>`,
// 注册局部组件
components: {
'lishimin': Lishimin
}
}
// 定义一个全局组件,使用Vue.component注册
Vue.component(
'liyuan', {
// data属性指定绑定的数据内容,可以在当前的组件中进行使用
data: function() {
return {
name: "李渊"
}
},
// template指定组件内显示的html内容,并可以在该组件内使用注册过的局部组件
template: `<div class="liyuan_css">我是全局组件【{{name}}】<liyuanji message="李世民要杀你们,快跑!"></liyuanji></div>`,
// 注册局部组件
components: {
'lishimin': Lishimin,
'liyuanji': Liyuanji
}
}
)
// 实例化Vue
new Vue({
el: "#app"
})
</script>
</body>
</html>


子组件向父组件传值

子组件给父组件传值,通过$emit将数据传递个父组件

直接使用$emit传值

子组件中直接定义点击传值

1
<button @click="$emit('notice', '父王快救我吗!')">救我</button>

父组件也直接接收赋值

1
v-on:notice="warning = $event"
使用方法间接传值

子组件定义一个方法用来向父组件传值,其本质还是利用了$emit

1
2
3
4
5
6
7
// 组件方法
methods:{
// 传递数据给父组件方法
send_value(message){
this.$emit('notice', message)
}
}

然后在模板中使用该方法

1
<button @click="send_value('父王,不会有任何人受到伤害的!')">穿给父王</button>

父组件接收也定义一个方法

1
2
3
4
5
methods:{
receive_method(value){
this.warning = value
}
}

模板中使用该方法接收子组件传过来的值

1
<lishimin v-on:notice="receive_method"></lishimin>

算法

质数

正向查质数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义一个列表存放4以内的所有质数
arr = []
# 比如我们取4以内的最大质数
num = 4
# 外循环取出每个数字
for i in range(2, num+1):
# 内循环取出比当前数字小的所有数字,依次与当前数字进行取余操作
for j in range(2, i):
# 进行取余操作,如果余数为0则代表当前数字可以被其他数字整除,即不为质数,直接终止当前内循环,进行下个数字的比较
if i%j == 0:
break
else:
# 当前内循环走完依然没有被break掉,证明当前数字不能被整除,我们将其放在数组中
arr.append(i)
print(arr)
print(max(arr))

# 输出结果:
# [2, 3]
# 3
# 即:4以内的最大质数为3

反向查质数

上面的代码我们是否可以优化一下?我们发现正向的话需要取出所有的质数列表,如果我们反向呢?那第一个质数不就是最大的质数了吗?上面的代码我们只需要改变一小部分既可实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 比如我们取4以内的最大质数
num = 4
# 外循环取出每个数字(反向:4,3,2)
for i in range(num, 1, -1):
# 内循环取出比当前数字小的所有数字,依次与当前数字进行取余操作
for j in range(2, i):
# 进行取余操作,如果余数为0则代表当前数字可以被其他数字整除,即不为质数,直接终止当前内循环,进行下个数字的比较
if i%j == 0:
break
else:
# 当前内循环走完依然没有被break掉,证明当前数字不能被整除,即当前是数字就是质数,另外我们是倒序查找的,所以肯定是最大的质数,也许往后循环再判断了,直接break掉。
print(i)
break

# 输出结果:
# 3

# 即:4以内的最大质数为3

排序

冒泡排序

1
2
3
4
5
6
7
8
lis = [9, 2, 7, 5, 4]
#外层循环,控制依次每个数字比较的轮次,这个次数用数字的数量和数字数量减去1都是一样的,因为最后依次不用比较了,都跟最后一个数比较过了,所以我们这里少循环一次
for item in range(len(lis)-1):
#5个数字比较4次即可得出最大值
for i in range(len(lis)-1):
if lis[i] > lis[i+1]:
lis[i], lis[i+1] = lis[i+1], lis[i]
print(lis)

优化后的代码

1
2
3
4
5
6
7
8
lis = [9, 2, 7, 5, 4]
#外层循环,控制依次每个数字比较的轮次,这个次数用数字的数量和数字数量减去1都是一样的,因为最后依次不用比较了,都跟最后一个数比较过了,所以我们这里少循环一次
for item in range(len(lis)-1):
#5个数字比较4次即可得出最大值
for i in range(len(lis)-1-item):
if lis[i] > lis[i+1]:
lis[i], lis[i+1] = lis[i+1], lis[i]
print(lis)

斐波那契数

递归(慢)

1
2
3
4
5
# 递归
def func1(n):
if n==0 or n == 1:
return 1
return func1(n-2) + func1(n-1)

数组(中等)

1
2
3
4
5
6
def func2(n):
lis = [1, 1]
for i in range(2, n+1):
lis.append(lis[-2]+lis[-1])
return lis[n]
print('func2:', func2(200000))

变量方式(最优)

1
2
3
4
5
6
7
8
9
10
11
def func3(n):
a = 1
b = 1
c = 0
for i in range(2, n+1):
c = a+b
a = b
b = c
return c
print('func3:', func3(200000))

二分查找

普通二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# num为用户要查找的数字
num =8
# lis为用户查找的范围有序list
lis = [1, 3, 4, 6, 8, 9]
# 初始左边界为0
left = 0
# 初始右边界为最后列表最后一个元素的索引
right = len(lis)-1
# 左边界小于或者等于右边界意味着查找中
while left <= right:
# 取列表中间元素的索引(索引只能为整数)
middle = (left + right) // 2
# 用户查找的值小于中间值,意味着用户要查找的值在左半边,右边边界就可以缩小到中间元素索引的前一位
if num < lis[middle]:
right = middle - 1
# 用户查找的值大于中间值,意味着用户要查找的值在右半边,左边边界就可以缩小到中间元素索引的后一位
if num > lis[middle]:
left = middle + 1
# 用户查找的值等于中间值,意味着找到了该数字
if num == lis[middle]:
print('找到了', middle)
break
else:
print('未找到')

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 查找数字
# num 要查找的数字
# lis 查找的范围
# left 查找的左边界
# right 查找的右边界
def func(num, lis, left, right):
# 左边界小于或者等于右边界意味着查找中
if left <= right:
# 取列表中间元素的索引(索引只能为整数)
middle = (left + right) // 2
# 用户查找的值小于中间值,意味着用户要查找的值在左半边,右边边界就可以缩小到中间元素索引的前一位
if num < lis[middle]:
right = middle - 1
return func(num, lis, left, right) #此处return必须添加,否则数据返回不到上一层,最终结果就会拿不到
# 用户查找的值大于中间值,意味着用户要查找的值在右半边,左边边界就可以缩小到中间元素索引的后一位
if num > lis[middle]:
left = middle + 1
return func(num, lis, left, right) #此处return必须添加,否则数据返回不到上一层,最终结果就会拿不到
# 用户查找的值等于中间值,意味着找到了该数字
if num == lis[middle]:
return middle
else:
return -1
lis = [1, 3, 4, 6, 8, 9]
ret = func(9, lis, 0, len(lis)-1)
print(ret)

切片二分查找法(优点:代码少;缺点:获取不到查找结果的索引)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查找数字
# n 要查找的数字
# lis 查找的范围
def func(lis, n):
# 此处我们定义一个索引列表,长度为lis最后一个元素值大小,并将每项都赋值为0
indexList = [0 for item in range(lis[-1]+1)]
# 循环将要查找到范围写入到索引列表index,范围元素对应的索引列表元素值为1
for i in lis:
indexList[i] = 1
# 如果要查找的数字在范围内,并且其作为索引对应的值为1,代表勋在
if n < len(indexList) and indexList[n] == 1:
return 1
else:
return -1
lis = [1, 3, 4, 6, 8, 9]
ret = func(lis, 15)
print(ret)

列表取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查找数字
# n 要查找的数字
# lis 查找的范围
def func(lis, n):
# 此处我们定义一个索引列表,长度为lis最后一个元素值大小,并将每项都赋值为0
indexList = [0 for item in range(lis[-1]+1)]
# 循环将要查找到范围写入到索引列表index,范围元素对应的索引列表元素值为1
for i in lis:
indexList[i] = 1
# 如果要查找的数字在范围内,并且其作为索引对应的值为1,代表勋在
if n < len(indexList) and indexList[n] == 1:
return 1
else:
return -1
lis = [1, 3, 4, 6, 8, 9]
ret = func(lis, 15)
print(ret)

运维

docker

什么是docker

  Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。

什么是dockfile

  Dockfile是一个用于编写docker镜像生成过程的文件,其有特定的语法。在一个文件夹中,如果有一个名字为Dockfile的文件,其内容满足语法要求,在这个文件夹路径下执行命令:docker build –tag name:tag .,就可以按照描述构建一个镜像了。name是镜像的名称,tag是镜像的版本或者是标签号,不写就是lastest。注意后面有一个空格和点。

什么是docker-compose

  假如,你有一个java镜像,一个mysql镜像,一个nginx镜像。如果没有docker-compose,那么每次启动的时候,你需要敲各个容器的启动参数,环境变量,容器命名,指定不同容器的链接参数等等一系列的操作,相当繁琐。而用了docker-composer之后,你就可以把这些命令一次性写在docker-composer.yml文件中,以后每次启动这一整个环境(含3个容器)的时候,你只要敲一个docker-composer up命令就ok了。

Dockerfile中最常见的指令是什么

FROM:指定基础镜像
LABEL:功能是为镜像指定标签
RUN:运行指定的命令
CMD:容器启动时要运行的命令

Dockerfile中的命令COPY和ADD命令有什么区别

COPY与ADD的区别COPY的SRC只能是本地文件,其他用法一致

docker常用命令

作用命令描述、示例
拉取或者更新指定镜像docker pull 镜像名
将镜像推送至远程仓库docker push
删除容器docker rm
删除镜像docker rmi
搜索镜像docker search 镜像名
运行镜像docker run 镜像名(镜像id前三位)
列出所有镜像docker images或docker image ls
列出所有容器(存活的)docker ps
查看运行过的镜像记录(挂掉的)docker ps -a
查看运行过的镜像记录仅显示记录iddocker ps -aq
导出镜像docker save
导入镜像docker loaddocker load < /tmp/flask-centos.tar.gz
批量删除所有容器运行记录docker rm docker ps -aqdocker save tonyu/flask-hello > /tmp/flask-centos.tar.gz
交互式运行容器docker run:运行容器命令docker run -it –rm ubuntu bash 命令参数: -it: -i代表交互式操作;-t代表终端。 –rm:容器退出后将其删除。也可不指定参数,手动docker rm,使用–rm可以避免浪费空间。 ubuntu:指定的容器镜像 bash:指定用交互式的shell, 因此需要bash命令(可省略)
查看容器日志docker logs 容器id
终止容器运行docker stop 容器id
重新运行容器docker start 容器id
提交容器为自定义镜像docker commit 容器id 新镜像名docker commit d68 centos-vim
进入镜像docker exec 容器id

容器与主机之间的数据拷贝命令

docker cp 命令用于容器与主机之间的数据拷贝
主机到容器:
docker cp /www 96f7f14e99ab:/www/
容器到主机:
docker cp 96f7f14e99ab:/www /tmp/

启动nginx容器(随机端口映射),并挂载本地文件目录到容器html的命令

docker run -d -P –name nginx2 -v /home/nginx:/usr/share/nginx/html nginx

当用户在浏览器当中输入一个网站,说说计算机对dns解释结果哪些流程?注:本机跟本地DNS还没有缓存

1.用户输入网址到浏览器;
2.浏览器发出DNS请求信息;
3.计算机首先查询本机HOST文件,不存在,继续下一步;
4.计算机按照本地DNS的顺序,向区域dns服务器查询IP结果;区域dns服务器查询不到时会从根域开
始,按照DNS层次结构向下搜索,直至对于信息具有权威性;
5.将返回dns结果给本地dns和本机,本机和本地dns并缓存本结果,直到TTL过期,才再次查询此结
果;
6.返回IP结果给浏览器;并给本地的DNS一份结果;
7.浏览器根据IP信息,获取页面;

redis

redis是做什么的

1.Redis主要是用于做缓存,数据的持久化的作用,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
2.Redis不仅仅支持key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
3.Redis支持数据的备份,即master-slave模式的数据备份。
4.性能极高 读写速度非常快– Redis能读的速度是110000次/s,写的速度是81000次/s

Redis应用场景

1、热点数据的缓存。
2、限时业务的运用,利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
3、计数器相关问题,所以可以运用于高并发的秒杀活动、分布式序列号的生成。
4、排行榜相关问题,进行热点数据的排序。

Redis的缺点

1.缓存和数据库双写一致性问题
2.缓存雪崩问题(小企业一般不会遇见)
3.缓存击穿问题
4.缓存的并发竞争问题
Redis为什么是单线程的
因为CPU不是redis的瓶颈,redis的瓶颈最有可能是机器内存或网络带宽,而且单线程容易实现,所以就采用单线程。

Redis和数据库双写一致性问题

采取正确更新策略,强调一致性要求的数据时,一定不能放缓存。
必须先更新数据库,再删缓存。
新增,更改,删除数据库操作时同步更新redis,可以使用事务机制来保证数据的一致性
其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

什么是缓存穿透,怎么解决

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库查询,造成缓存穿透。
解决方案:最简单粗暴的方式,如果一个查询返回的数据为空(不管数据存不存在还是系统故障),我们就把这个空的结果进行缓存,但它的过期时间会很短,最长不超过5分钟

缓存雪崩

  • 缓存穿透:key中对应的缓存数据不存在,导致去请求数据库,造成数据库的压力倍增的情况

  • 缓存击穿:redis过期后的一瞬间,有大量用户请求同一个缓存数据,导致这些请求都去请求数据库,造成数据库压力倍增的情,针对一个key而言

  • 缓存雪崩:缓存服务器宕机或者大量缓存集中某个时间段失效,导致请求全部去到数据库,造成数据库压力倍增的情况,这个是针对多个key而言

一、缓存穿透的解决方案

常用方法可以采用布隆过滤器方法进行数据拦截,其次可以还有一种解决思路,就是如果请求的数据为空,将空值也进行缓存,就不会发生穿透情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
class getPrizeList {
/**
* redis实例
* @var \Redis
*/
private $redis;

/**
* @var string
*/
private $redis_key = '|prize_list';

/**
* 过期时间
* @var int
*/
private $expire = 30;

/**
* getPrizeList constructor.
* @param $redis
*/
public function __construct($redis)
{
$this->redis = $redis;
}

/**
* @return array|bool|string
*/
public function fetch()
{
$result = $this->redis->get($this->redis_key);
if(!isset($result)) {
//此处应该进行数据库查询...
//如果查询结果不存在,给其默认空数组进行缓存
$result = [];
$this->redis->set($this->redis_key, $result, $this->expire);
}

return $result;
}
}

二、缓存击穿解决办法

使用互斥锁(mutex key),就是一个key过期时,多个请求过来允许其中一个请求去操作数据库,其他请求等待第一个请求成功返回结果后再请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
class getPrizeList {
/**
* redis实例
* @var \Redis
*/
private $redis;

/**
* @var string
*/
private $redis_key = '|prize_list';

/**
* @var string
*/
private $setnx_key = '|prize_list_setnx';

/**
* 过期时间
* @var int
*/
private $expire = 30;

/**
* getPrizeList constructor.
* @param $redis
*/
public function __construct($redis)
{
$this->redis = $redis;
}

/**
* @return array|bool|string
*/
public function fetch()
{
$result = $this->redis->get($this->redis_key);
if(!isset($result)) {
if($this->redis->setnx($this->setnx_key, 1, $this->expire)) {
//此处应该进行数据库查询...
//$result = 数据库查询结果;
$this->redis->set($this->redis_key, $result, $this->expire);
$this->redis->del($this->setnx_key); //删除互斥锁
} else {
//其他请求每等待10毫秒重新请求一次
sleep(10);
self::fetch();
}
}

return $result;
}
}

三、缓存雪崩的解决办法

这种情况是因为多个key同时过期导致的数据库压力,一种方法可以在key过期时间基础上增加时间随机数,让过期时间分散开,减少缓存时间过期的重复率

另一种方法就是加锁排队,这种有点像上面缓存击穿的解决方式,但是这种请求量太大,比如5000个请求过来,4999个都需要等待,这必然是指标不治本,不仅用户体验性差,分布式环境下就更加复杂,因此在高并发场景下很少使用

最好的解决方法,是使用缓存标记,判断该标记是否过期,过期则去请求数据库,而缓存数据的过期时间要设置的比缓存标记的长,这样当一个请求去操作数据库的时候,其他请求拿的是上一次缓存数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
class getPrizeList {
/**
* redis实例
* @var \Redis
*/
private $redis;

/**
* @var string
*/
private $redis_key = '|prize_list';

/**
* 缓存标记key
* @var string
*/
private $cash_key = '|prize_list_cash';

/**
* 过期时间
* @var int
*/
private $expire = 30;

/**
* getPrizeList constructor.
* @param $redis
*/
public function __construct($redis)
{
$this->redis = $redis;
}

/**
* @return array|bool|string
*/
public function fetch()
{
$cash_result = $this->redis->get($this->cash_key);
$result = $this->redis->get($this->redis_key);
if(!$cash_result) {
$this->redis->set($this->cash_key, 1, $this->expire);
//此处应该进行数据库查询...
//$result = 数据库查询结果, 并且设置的时间要比cash_key长,这里设置为2倍;
$this->redis->set($this->redis_key, $result, $this->expire * 2);
$this->redis->del($this->cash_key); //删除互斥锁
}

return $result;
}
}

Redis持久化策略

redis的数据都保存在内存中,如果断电或宕机,则内存数据将擦除,导致数据丢失。
1.RDB模式
特点:
是redis中默认的持久化策略
定期持久化,保存的是redis中的内存数据快照,持久化文件占用空间较小
可能导致内存数据丢失
命令:
前提:需要在redis的客户端中执行
save命令,立即持久化,会导致其他操作陷入阻塞
bgsave命令,开启后台运行,以异步的方式进行持久化,不会造成其他操作的阻塞
2.AOF模式
特点:
1.默认关闭,如果需要开启则需要修改配置文件
2.可以实现数据的实时持久化,记录的是用户的操作过程
3.只要开启了AOF模式,则持久化方式以AOF为主
总结:
1.如果用户允许少量数据丢失,则可选择RDB模式,效率更高
2.如果不允许数据丢失,则选用AOF模式
3.可以两种方式都选,需要搭建主从结构,主机选RDB,从机选AOF,可以保证业务允许

Redis常见的性能问题有哪些?该如何解决?

1.主服务器写内存快照,会阻塞主线程的工作,当快照较大时对性能影响是非常大的,会间断性暂停服务,所有主服务器最好不要写内存快照。
2.redis主从赋值的性能问题,为了主从复制的速度和连接的稳定性,主从库最好在同一个局域网内。

Reids集群哨兵模式的特性?

监控:每个哨兵会不断监控master和slave是否在正常工作
提醒:如果哨兵监控的redis出了问题,哨兵可以通知管理员和其它应用程序
故障转移:如果master未按预期工作,哨兵可以选举出新的master继续工作
配置程序:客户端需要先连接哨兵,哨兵会告知客户当前master节点的地址

redis哨兵模式选举机制

当redis集群的主节点故障时,Sentinel集群将从剩余的从节点中选举一个新的主节点,有以下步骤:

  1. 故障节点主观下线
  2. 故障节点客观下线
  3. Sentinel集群选举Leader
  4. Sentinel Leader决定新主节点

选举过程

1、主观下线

Sentinel集群的每一个Sentinel节点会定时对redis集群的所有节点发心跳包检测节点是否正常。如果一个节点在down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该redis节点被该Sentinel节点主观下线。

2、客观下线

当节点被一个Sentinel节点记为主观下线时,并不意味着该节点肯定故障了,还需要Sentinel集群的其他Sentinel节点共同判断为主观下线才行。

该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过quorum数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线。

如果客观下线的redis节点是从节点或者是Sentinel节点,则操作到此为止,没有后续的操作了;如果客观下线的redis节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点。

3、Sentinel集群选举Leader

如果需要从redis集群选举一个节点为主节点,首先需要从Sentinel集群中选举一个Sentinel节点作为Leader。

每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。

如果一个Sentinel节点获得的选举票数达到Leader最低票数(quorumSentinel节点数/2+1的最大值),则该Sentinel节点选举为Leader;否则重新进行选举。

在这里插入图片描述

4、Sentinel Leader决定新主节点

当Sentinel集群选举出Sentinel Leader后,由Sentinel Leader从redis从节点中选择一个redis节点作为主节点:

  1. 过滤故障的节点
  2. 选择优先级slave-priority最大的从节点作为主节点,如不存在则继续
  3. 选择复制偏移量(数据写入量的字节,记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续
  4. 选择runid(redis每次启动的时候生成随机的runid作为redis的标识)最小的从节点作为主节点

img

为什么Sentinel集群至少3节点

一个Sentinel节选举成为Leader的最低票数为quorumSentinel节点数/2+1的最大值,如果Sentinel集群只有2个Sentinel节点,则

1
2
3
4
5
6
Sentinel节点数/2 + 1
= 2/2 + 1
= 2


123

即Leader最低票数至少为2,当该Sentinel集群中由一个Sentinel节点故障后,仅剩的一个Sentinel节点是永远无法成为Leader。

也可以由此公式可以推导出,Sentinel集群允许1个Sentinel节点故障则需要3个节点的集群;允许2个节点故障则需要5个节点集群。

nginx

为什么要用Nginx?

跨平台、配置简单、反向代理、高并发连接:处理2-3万并发连接数,官方监测能支持5万并发,内存消耗小:开启10个nginx才占150M内存 ,nginx处理静态文件好,耗费内存少,
而且Nginx内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。
使用Nginx的话还能:
节省宽带:支持GZIP压缩,可以添加浏览器本地缓存
稳定性高:宕机的概率非常小
接收用户请求是异步的

Nginx应用场景

http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。
虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。
反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。
nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。

Nginx代理负载均衡的调度算法有哪些?具体实现时的现象是什么?

1.轮询(默认):每个请求按时间顺序逐一分配到不同的后端,如果后台某台服务器宕机,自动剔除故障系统,使用户访问不受影响,这种方式简便,成本低,但是可靠性低,负载均衡不均衡,适用于图片服务器集群和纯静态页面服务器集群。
2.weight(权重):weight的值越大分配到访问概率越高,主要用于后端每台服务器性能不均衡的情况下,或者仅仅为在主从的情况下设置不同的权值,达到合理有效的利用主机资源。
3.IP_HASH(访问IP):每个请求按访问的哈希结果分配,使来自同一个IP的访问固定一台后端服务器,并且可以有效解决动态网页存在的session的共享问题。
4.FAIR(第三方):比weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能的进行均衡负载,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。nginx本身不支持fair,如果需要这种调度算法,则需要安装upstream_fair模块。
5.URL_HASH(第三方):按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,可以进一步提高后端缓存服务器的效率。这种调度算法需要安装nginx的hash软件

Nginx负载均衡

什么是Nginx负载均衡

在服务器集群中,**Nginx** 起到一个代理服务器的角色(即反向代理),为了避免单独一个服务器压力过大,将来自用户的请求转发给不同的服务器。

Nginx负载均衡策略

负载均衡用于从 “upstream” 模块定义的后端服务器列表中选取一台服务器接受用户的请求。一个最基本的 upstream 模块是这样的,模块内的 server 是服务器列表:

1
2
3
4
5
6
7
#动态服务器组
upstream dynamic_zuoyu {
server localhost:8080; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082; #tomcat 8.5
server localhost:8083; #tomcat 9.0
}

在 upstream 模块配置完成后,要让指定的访问反向代理到服务器列表:

1
2
3
4
5
#其他页面反向代理到tomcat容器
location ~ .*$ {
index index.jsp index.html;
proxy_pass http://dynamic_zuoyu;
}

这就是最基本的负载均衡实例,但这不足以满足实际需求;目前 Nginx 服务器的 upstream 模块支持 6 种方式的分配。

负载均衡策略

轮询

最基本的配置方法,上面的例子就是轮询的方式,它是 upstream 模块默认的负载均衡默认策略。每个请求会按时间顺序逐一分配到不同的后端服务器。有如下参数:

fail_timeout与max_fails结合使用
max_fails设置在 fail_timeout 参数设置的时间内最大失败次数,如果在这个时间内,所有针对该服务器的请求都失败了,那么认为该服务器会被认为是停机了。
fail_time服务器会被认为停机的时间长度,默认为 10s。
backup标记该服务器为备用服务器。当主服务器停止时,请求会被发送到它这里。
down标记服务器永久停机了。

注意:

  1. 在轮询中,如果服务器 down 掉了,会自动剔除该服务器。
  2. 缺省配置就是轮询策略。
  3. 此策略适合服务器配置相当,无状态且短平快的服务使用。

weight

权重方式,在轮询策略的基础上指定轮询的几率。例子如下:

1
2
3
4
5
6
7
#动态服务器组
upstream dynamic_zuoyu {
server localhost:8080 weight=2; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082 backup; #tomcat 8.5
server localhost:8083 max_fails=3 fail_timeout=20s; #tomcat 9.0
}

在该例子中,weight 参数用于指定轮询几率,weight 的默认值为1 ;weight 的数值与访问比率成正比,比如 Tomcat 7.0 被访问的几率为其他服务器的两倍。

注意:

  1. 权重越高分配到需要处理的请求越多。
  2. 此策略可以与 least_conn 和 ip_hash 结合使用。
  3. 此策略比较适合服务器的硬件配置差别比较大的情况。

ip_hash

指定负载均衡器按照基于客户端 IP 的分配方式,这个方法确保了相同的客户端的请求一直发送到相同的服务器,以保证 session 会话。这样每个访客都固定访问一个后端服务器,可以解决 session 不能跨服务器的问题。

1
2
3
4
5
6
7
8
#动态服务器组
upstream dynamic_zuoyu {
ip_hash; #保证每个访客固定访问一个后端服务器
server localhost:8080 weight=2; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082; #tomcat 8.5
server localhost:8083 max_fails=3 fail_timeout=20s; #tomcat 9.0
}

注意:

  1. 在 nginx 版本 1.3.1 之前,不能在 ip_hash 中使用权重(weight)。
  2. ip_hash 不能与 backup 同时使用。
  3. 此策略适合有状态服务,比如 session。
  4. 当有服务器需要剔除,必须手动 down 掉。

least_conn

把请求转发给连接数较少的后端服务器。轮询算法是把请求平均的转发给各个后端,使它们的负载大致相同;但是,有些请求占用的时间很长,会导致其所在的后端负载较高。

这种情况下,least_conn 这种方式就可以达到更好的负载均衡效果。

1
2
3
4
5
6
7
8
#动态服务器组
upstream dynamic_zuoyu {
least_conn; #把请求转发给连接数较少的后端服务器
server localhost:8080 weight=2; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082 backup; #tomcat 8.5
server localhost:8083 max_fails=3 fail_timeout=20s; #tomcat 9.0
}

注意:

  1. 此负载均衡策略适合请求处理时间长短不一造成服务器过载的情况。

fair

fair 属于第三方的负载均衡策略,其实现需要安装第三方插件。fair 按照服务器端的响应时间来分配请求,响应时间短的优先分配。

1
2
3
4
5
6
7
8
#动态服务器组
upstream dynamic_zuoyu {
server localhost:8080; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082; #tomcat 8.5
server localhost:8083; #tomcat 9.0
fair; #实现响应时间短的优先分配
}

url_hash

url_hash 也属于第三方的负载均衡策略,其实现需要安装第三方插件。

url_hash 按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器,要配合缓存命中来使用。同一个资源多次请求,可能会到达不同的服务器上,导致不必要的多次下载,缓存命中率不高,以及一些资源时间的浪费。

而使用 url_hash,可以使得同一个 url(也就是同一个资源请求)会到达同一台服务器,一旦缓存住了资源,再此收到请求,就可以从缓存中读取。

1
2
3
4
5
6
7
8
#动态服务器组
upstream dynamic_zuoyu {
hash $request_uri; #实现每个url定向到同一个后端服务器
server localhost:8080; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082; #tomcat 8.5
server localhost:8083; #tomcat 9.0
}

总结

  1. 轮询 (默认):每一个来自网络中的请求,轮流分配给内部的服务器,从 1 到 N 然后重新开始。此种负载均衡算法适合服务器组内部的服务器都具有相同的配置并且平均服务请求相对均衡的情况。
  2. 加权轮询(weight):根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。例如:服务器 A 的权值被设计成 1,B 的权值是 3,C 的权值是
    6,则服务器 A、B、C 将分别接受到 10%、30%、60% 的服务请求。此种均衡算法能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。
  3. ip-hash(ip_hash):我们都知道,每个请求的客户端都有相应的 ip 地址,该均衡策略中,nginx 将会根据相应的 hash 函数,对每个请求的 ip 作为关键字,得到的 hash 值将会决定将请求分发给相应 Server 进行处理。
  4. 最少连接数(least_conn):最少连接,也就是说 nginx 会判断后端集群服务器中哪个 Server 当前的 Active Connection 数是最少的,那么对于每个新进来的请求,nginx 将该其分发给对应的 Server。

Nginx的作用

这个问题是入门级知识点,讨论Nginx的用处。我觉得只要几个重要的点都回答到位就可以了,可以考虑这样的一个回答:Nginx是一个高性能web服务器和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。不仅可以实现负载均衡,还可以做接口限流,缓存等功能。

使用Nginx的优势点

  • Nginx由于使用了epoll和kqueue网路I/O模型,在实际生产环境能够支撑3万左右并发连接。
  • Nginx内存消耗低。
  • Nginx跨平台,而且配置相对来说难度较低。
  • Nginx内置健康检查功能,如果负载均衡其中一个服务器宕机了,则接受到的请求会发送给其他服务器去处理。
  • 支持Gzip压缩,可以添加浏览器本地缓存的Header头。
  • Nginx支持热部署,可以在不间断服务的情况下平滑进行配置的更改。
  • Nginx异步接收用户请求,减轻了Web服务器的压力。

Nginx如何实现高并发

这个问题出来可能懂一点Nginx的朋友们都是浮现出5个字:异步非阻塞。实际上Nginx就是异步非阻塞,使用了epoll模型并对底层代码进行大幅度优化。之前其实有讲过Nginx是采用1个master进程,多个worker进程的模式,每次接收到一个请求,master会将请求按照一定策略分发给一个worker进程去进行处理请求。worker进程数一般设置为和CPU核心数一致,异步非阻塞模式就会使得worker线程在等待请求callback的空闲时间可以接收处理新的请求,当接收到旧请求的callback时再回去继续处理该请求,这样就完成了少数几个worker进程却实现了高并发的问题。

Nginx为何不使用多线程?

众所周知,没创建一个新的线程,都需要为其分配cpu和内存。当然,创建进程也是一样,但是由于线程过多会导致内存消耗过多。所以Nginx采用单线程异步处理用户请求,这样不需要不断地为新的线程分配cpu和内存,减轻服务器内存消耗,所以使得Nginx性能方面更为高效。

Nginx如何处理请求?

Nginx启动后,首先进行配置文件的解析,解析成功会得到虚拟服务器的ip和端口号,在主进程master进程中创建socket,对addrreuse选项进行设置,并将socket绑定到对应的ip地址和端口并进行监听。然后创建子进程worker进程,当客户端和Nginx进行三次握手,则可以创建成功与Nginx的连接。当有新的请求进入时,空闲的worker进程会竞争,当某一个worker进程竞争成功,则会得到这个已经成功建立连接的socket,然后创建ngx_connection_t结构体,接下来设置读写事件处理函数并添加读写事件用来与客户端进行数据交换。当请求结束Nginx或者客户端主动关闭连接,此时一个请求处理完毕。

为什么要做动静分离?

在日常开发中,前端请求静态文件比如图片资源是不需要经过后端服务器的,但是调用API这些类型的就需要后端进行处理请求,所以为了提高对资源文件的响应速度,我们应该使用动静分离的策略去做架构。我们可以将静态文件放到Nginx中,将动态资源的请求转发到后端服务器去进行进一步的处理。

Nginx负载均衡的几种常用方式?

轮询方式:默认情况下Nginx使用轮询的方式实现负载均衡,每个新的请求按照时间顺序逐一分配到不同的后端服务器去进行处理,如果后端服务器宕机,则Nginx的健康检查功能会将这个后端服务器剔除。但是轮询方式是显而易见的:可靠性低而且负载分配不平衡,所以轮询方式更适用于图片服务器或者静态资源服务器。

  • weight:可以对不同的后端服务器设置不同的权重比例,这样可以改变不同后端服务器处理请求的比例。可以给性能更优的后端服务器配置更高的权重。
  • ip_hash:这种方式会根据请求的ip地址的hash结果分配后端服务器来处理请求,这样每个用户发起的请求固定只会由同一个后端服务器处理,这样可以解决session问题。
  • fail:这种方式有点类似于轮询方式,主要是根据后端服务器的响应时间来分配请求,响应时间短的后端服务器优先分配请求。
  • url_hash:这种方式是按照请求url的hash结果来将不同请求分配到不同服务器,使用这种方式每个url的请求都会由同一个后端服务器进行处理,后端服务器为缓存时效率会更高。

Session不同步如何处理?

上面其实提过了解决方案,负载均衡方式使用ip_hash方式,如果用户已经访问过某个后端器,则再次访问时会将这个请求的ip地址进行哈希算法转换,自动定位到该服务器。当然也可以通过redis缓存用户session,一样可以处理session不同步的问题。

Nginx常用优化配置

  1. 调整worker_processes指定Nginx需要创建的worker进程数量,刚才有提到worker进程数一般设置为和CPU核心数一致。
  2. 调整worker_connections设置Nginx最多可以同时服务的客户端数。结合worker_processes配置可以获得每秒可以服务的最大客户端数。
  3. 启动gzip压缩,可以对文件大小进行压缩,减少了客户端http的传输带宽,可以大幅度提高页面的加载速度。
  4. 启用缓存,如果请求静态资源,启用缓存是可以大幅度提升性能的。关于启用缓存可以观看Nginx缓存这篇文章:Nginx缓存原理及机制

Nginx正向代理

正向代理也是大家最常接触的到的代理模式,那究竟什么是正向代理呢?我们都知道Google在国内是无法正常访问的,但是某些时候我们由于技术问题需要去访问Google时,我们会先找到一个可以访问Google的代理服务器,我们将请求发送到代理服务器,代理服务器去访问Google,然后将访问到的数据返回给我们,这样的过程就是正向代理。正向代理最大的特点是客户端需要明确知道要访问的服务器地址,Google服务器只清楚请求来自哪个代理服务器,而不清楚来自哪个具体的客户端,正向代理可以隐藏真实客户端的具体信息。
file

客户端必须设置正向代理服务器,而且需要知道正向代理服务器的IP地址以及代理程序的端口。一句话来概括就是正向代理代理的是客户端,是一个位于客户端和Google服务器之间的服务器,为了从Google服务器取得数据,客户端向代理服务器发送一个请求并指定目标(Google服务器),然后代理向原始服务器转交请求并将获得的数据返回给客户端。总结正向代理的几个作用:

  • 访问国外无法访问的网站做缓存,加速访问资源
  • 对客户端访问授权,上网进行认证代理
  • 可以记录用户访问记录(上网行为管理),对外隐藏用户信息

Nginx反向代理

多个客户端给服务器发送的请求,Nginx服务器接收到请求以后,按照一定的规则转发到不同的服务器进行业务逻辑处理,也就是我们刚才讲到的负载均衡的策略。此时请求来源于哪个客户端是确定的,但是请求由哪台服务器处理的并不明确,Nginx扮演的就是一个反向代理角色。可以这样来理解,反向代理对外都是透明的,访问者并不知道自己访问的是一个代理。反向代理代理的是服务端,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息。总结下反向代理的两个作用:

  • 保证内网的安全,通常将反向代理作为公网访问地址,Web服务器是内网
  • 负载均衡,通过反向代理服务器来优化网站的负载

Nginx中正向代理与反向代理的区别

file

  1. 在正向代理中,隐藏了请求来源的客户端信息;
  2. 在反向代理中,隐藏了请求具体处理的服务端信息;

nginx反向代理配置

反向代理:

我们将请求发送到服务器,然后服务器对我们的请求进行转发,我们只需要和代理服务器进行通信就好,偷个图:

img

正向代理:

对于目标服务器来讲,感受不到真实的客户端,与它通信的是代理客户端,如科学谷歌的软件就是一个正向代理,偷个图:

img

举个正向代理的例子,**我(客户端)没有绿码出不了门,但是朋友(代理)有,我(客户端)朋友(代理)去超市买瓶水,而对于超市(服务器)来讲,他们感知不到我(客户端)**的存在,这就是正向代理。

举个反向代理例子,**我(客户端)朋友(代理)去给我买瓶水,并没有说去哪里买,反正朋友(代理)买回来了,对于我(客户端)来讲,我(客户端)感知不到超市(服务器)**的存在,这就是反向代理。

简单概括下就是,服务器代理被称为反向代理,客户端代理被称为正向代理。

nignx如何配置?

nignx下载

http://nginx.org/en/download.html

文件目录

img

其他目录也没研究,跟本次的跨域也不搭边(我还没学会~)

启动服务

在这个目录下,打开cmd命令行,输入nginx,你也可以双击nginx.exe,但显得不直观,访问Localhost:80端口,就可以看到下方的界面,

img

不想要的话,可以自行修改,进入配置文件目录,

img

优化配置代码

删除注释和无关代码后的文件长这样:

img

  1. 第一个location中的root和index字段也可以删掉,毕竟和我们这次跨域没啥关系~
  2. 红框中的error_page和它下面的location也可以删掉,理由和上面一样

删除上述两项后的代码如下,

img

启动服务

先来启动一个node服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require('http');

http.createServer(function (request, response) {
response.writeHead(200, {
'Content-Type': 'application/json;charset=utf-8'
});
// 这个例子中要回传的data
let data = {
name: 'nginx proxy'
};

data = JSON.stringify(data);

response.end(data);

}).listen(8887);

console.log('server1 is listen at 8887 port');复制代码

错误示范

启动服务后,我们直接进行访问,模拟跨域场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>
<h1>nginx反向代理</h1>
<script>
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8887');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
console.log(xhr)
}
}
}
</script>
</div>复制代码

果不其然报了跨域的错误:

img

开始配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {

}
}
}
复制代码

我们在location里面加上两个字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:8887;
add_header Access-Control-Allow-Origin *;
}
}
}复制代码
  1. proxy_pass,代表要代理的服务器端口
  2. add_header,了解过CORS的朋友应该知道,这个是配置响应头
  3. listen,代表监听的端口

现在修改下请求的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div>
<h1>nginx反向代理</h1>
<script>
var xhr = new XMLHttpRequest();
// 在这里把原来的localhost:8887修改成localhost:80
xhr.open('GET', 'http://localhost:80');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
console.log(xhr)
}
}
}
</script>
</div>复制代码

修改完毕后重启服务,这里提供两种方式,不过都得在文件目录下新开一个cmd命令行,

  1. 直接重启。输入nginx -s reload复制代码
  2. 先关闭再开启。输入nginx -s stop nginx复制代码

img

如果是想对服务器上的api文件夹下发请求的话,那就只需要修改配置文件中的这个字段就行,

1
2
3
4
location /api {
// 你的代码,如
// proxy_pass 你的路径
// add_header Access-Control-Allow-Origin *;}复制代码

搭建Nginx正向代理服务

需求背景:

前段时间公司因为业务需求需要部署一个正向代理,需要内网服务通过正向代理访问到外网移动端厂商域名通道等效果,之前一直用nginx做四层或者七层的反向代理,正向代理还是第一次配置,配置的过程也遇到些小坑,今天就分享出来。

安装环境准备:

​ nginx本身是不支持https协议请求转发,为了让nginx能达到这一效果需要借助第三方模块ngx_http_proxy_connect_module。首先下载这一模块:https://github.com/chobits/ngx_http_proxy_connect_module到服务器,然后准备nginx环境

1
2
3
4
5
6
7
yum -y install pcre-devel zlib-devel gcc gcc+c++ make openssl-devel pcre-devel  zlib-devel patch   
tar xf nginx-1.6.0.tar.gz
unzip /root/ngx_http_proxy_connect_module-master.zip
cd /root/nginx-1.6.0/
patch -p1 < /root/ngx_http_proxy_connect_module-master/proxy_connect.patch
./configure --add-module=/root/ngx_http_proxy_connect_module-master/ngx_http_proxy_connect_module
make && make install

编译安装成功后,配置nginx正向代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#user  nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;

server {
listen 88; #监听端口
resolver 183.60.82.98; #dns解析地址
server_name _;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass https://$host$request_uri; #设定http代理服务器的协议和地址
proxy_set_header HOST $host;
proxy_buffers 256 4k;
proxy_max_temp_file_size 0k;
proxy_connect_timeout 30;
proxy_send_timeout 60;
proxy_read_timeout 60;
proxy_next_upstream error timeout invalid_header http_502;
#root html;
#index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

}



server {
resolver 8.8.8.8; #dns解析地址
listen 89; #代理监听端口
proxy_connect;
proxy_connect_allow 443 563;
location / {
proxy_pass https://$host$request_uri; #设定https代理服务器的协议和地址
proxy_set_header HOST $host;
proxy_buffers 256 4k;
proxy_max_temp_file_size 0k;
proxy_connect_timeout 30;
proxy_send_timeout 60;
proxy_read_timeout 60;
proxy_next_upstream error timeout invalid_header http_502;

}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

}

}  

当配置文件配置好之后保存即可,重启nginx,进行测试:

去内网服务器里curl,可以在环境变量里添加代理:

1
2
3
4
5
vim  /etc/profile
...
...
#export https_proxy=正向代理IP:端口
export https_proxy=192.168.3.17:89

  img

另一种 方式不用加环境变量,临时代理

1
2
#curl -i  --proxy 代理IP:端口      要访问域名
curl -i --proxy 192.168.3.17:89 www.baidu.com

  

ElasticSearch

聚合查询

概要

Elasticsearch的聚合查询,跟数据库的聚合查询效果是同样的,咱们能够将两者拿来对比学习,如求和、求平均值、求最大最小等等。java

基础概念

bucket

数据分组,一些数据按照某个字段进行bucket划分,这个字段值相同的数据放到一个bucket中。能够理解成Java中的Map<String, List>结构,相似于Mysql中的group by后的查询结果。mysql

metric:

对一个数据分组执行的统计,好比计算最大值,最小值,平均值等
相似于Mysql中的max(),min(),avg()函数的值,都是在group by后使用的。sql

案例

咱们仍是以英文儿歌为案例背景,回顾一下索引结构:数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
PUT /music
{
"mappings": {
"children": {
"properties": {
"id": {
"type": "keyword"
},
"author_first_name": {
"type": "text",
"analyzer": "english"
},
"author_last_name": {
"type": "text",
"analyzer": "english"
},
"author": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"type": "text",
"analyzer": "english",
"fielddata": true
},
"tags": {
"type": "text",
"analyzer": "english"
},
"length": {
"type": "long"
},
"likes": {
"type": "long"
},
"isRelease": {
"type": "boolean"
},
"releaseDate": {
"type": "date"
}
}
}
}
}

统计目前收录的哪一种语言的歌曲最多

1
2
3
4
5
6
7
8
9
10
11
GET /music/children/_search
{
"size": 0,
"aggs": {
"song_qty_by_language": {
"terms": {
"field": "language"
}
}
}
}

语法解释:微信

  • size:0 表示只要统计后的结果,原始数据不展示
  • aggs:固定语法 ,聚合分析都要声明aggs
  • song_qty_by_language:聚合的名称,能够随便写,建议规范命名
  • terms:按什么字段进行分组
  • field:具体的字段名称

响应结果以下:架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 5,
"max_score": 0,
"hits": []
},
"aggregations": {
"song_qty_by_language": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "english",
"doc_count": 5
}
]
}
}
}

语法解释:并发

  • hits: 因为请求时设置了size:0,hits就是空的
  • aggregations:聚合查询的结果
  • song_qty_by_language:请求时声明的名称
  • buckets:根据指定字段查询后获得的数据分组集合,[]内的是每个数据分组,其中key为每一个bucket的对应指定字段的值,doc_count为统计的数量。

默认按doc_count降序排序。app

按语种统计每种歌曲的平均时长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /music/children/_search
{
"size": 0,
"aggs": {
"lang": {
"terms": {
"field": "language"
},
"aggs": {
"length_avg": {
"avg": {
"field": "length"
}
}
}
}
}
}

这里演示的是两层aggs聚合查询,先按语种统计,获得数据分组,再在数据分组里算平均时长。分布式

多个aggs嵌套语法也是如此,注意一下aggs代码块的位置便可。ide

统计最长时长、最短时长等的歌曲

最经常使用的统计:count,avg,max,min,sum,语法含义与mysql相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
GET /music/children/_search
{
"size": 0,
"aggs": {
"color": {
"terms": {
"field": "language"
},
"aggs": {
"length_avg": {
"avg": {
"field": "length"
}
},
"length_max": {
"max": {
"field": "length"
}
},
"length_min": {
"min": {
"field": "length"
}
},
"length_sum": {
"sum": {
"field": "length"
}
}
}
}
}
}

按时长分段统计歌曲平均时长

以30秒为一段,看各段区间的平均值。

histogram语法位置跟terms同样,做范围分区,搭配interval参数一块儿使用
interval:30表示分的区间段为[0,30),[30,60),[60,90),[90,120)

段的闭合关系是左开右闭,若是数据在某段区间内没有,也会返回空的区间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /music/children/_search
{
"size": 0,
"aggs": {
"sales_price_range": {
"histogram": {
"field": "length",
"interval": 30
},
"aggs": {
"length_avg": {
"avg": {
"field": "length"
}
}
}
}
}
}

这种数据的结果能够用来生成柱状图或折线图。

按上架日期分段统计新歌数量

按月统计

date histogram与histogram语法相似,搭配date interval指定区间间隔
extended_bounds表示最大的时间范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /music/children/_search
{
"size": 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "releaseDate",
"interval": "month",
"format": "yyyy-MM-dd",
"min_doc_count": 0,
"extended_bounds": {
"min": "2019-10-01",
"max": "2019-12-31"
}
}
}
}
}

interval的值能够天、周、月、季度、年等。咱们能够延伸一下,好比统计今年每一个季度的新发布歌曲的点赞数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
GET /music/children/_search
{
"size": 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "releaseDate",
"interval": "quarter",
"format": "yyyy-MM-dd",
"min_doc_count": 0,
"extended_bounds": {
"min": "2019-01-01",
"max": "2019-12-31"
}
},
"aggs": {
"lang_qty": {
"terms": {
"field": "language"
},
"aggs": {
"like_sum": {
"sum": {
"field": "likes"
}
}
}
},
"total" :{
"sum": {
"field": "likes"
}
}
}
}
}
}

带上过滤条件

聚合查询能够和query搭配使用,至关于mysql中where与group by联合使用

查询条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /music/children/_search
{
"size": 0,
"query": {
"match": {
"language": "english"
}
},
"aggs": {
"sales": {
"terms": {
"field": "language"
}
}
}
}
过滤条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /music/children/_search
{
"size": 0,
"query": {
"constant_score": {
"filter": {
"term": {
"language": "english"
}
}
}
},
"aggs": {
"sales": {
"terms": {
"field": "language"
}
}
}
}

global bucket查询

global:就是global bucket,会将全部的数据归入聚合scope,不受前面的query或filter影响。

global bucket适用于同时统计指定条件的数据与所有数据的对比,如咱们创造的场景:指定做者的歌与所有歌曲的点赞数量对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GET /music/children/_search
{
"size": 0,
"query": {
"match": {
"author": "Jean Ritchie"
}
},
"aggs": {
"likes": {
"sum": {
"field": "likes"
}
},
"all": {
"global": {},
"aggs": {
"all_likes": {
"sum": {
"field": "likes"
}
}
}
}
}
}

统计近2月,近1月的点赞数

aggs.filter针对是聚合里的数据

bucket filter:对不一样的bucket下的aggs,进行filter

相似于mysql的中having语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
GET /music/children/_search
{
"size": 0,
"aggs": {
"recent_60d": {
"filter": {
"range": {
"releaseDate": {
"gte": "now-60d"
}
}
},
"aggs": {
"recent_60d_likes_sum": {
"sum": {
"field": "likes"
}
}
}
},
"recent_30d": {
"filter": {
"range": {
"releaseDate": {
"gte": "now-30d"
}
}
},
"aggs": {
"recent_30d_likes_sum": {
"avg": {
"field": "likes"
}
}
}
}
}
}

统计排序

默认按doc_count降序排序,排序规则能够改,order里面能够指定aggs的别名,如length_avg,相似于mysql的order by cnt asc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /music/children/_search
{
"size": 0,
"aggs": {
"group_by_lang": {
"terms": {
"field": "language",
"order": {
"length_avg": "desc"
}
},
"aggs": {
"length_avg": {
"avg": {
"field": "length"
}
}
}
}
}
}