Python爬虫学前普及

爬虫篇 | Python爬虫学前普及

【摘要】*近整理一个爬虫系列方面的文章,不管大家的基础如何,我从头开始整一个爬虫系列方面的文章,让大家循序渐进的学习爬虫,小白也没有学习障碍,那么今天,环球网校的小编就来针对Python讲讲爬虫学前普及,希望今天的文章对您可以有所帮助。

恩,准备进入正题了!*近一段时间没有怎么更新公众号,主要就是在做爬虫教程的一些准备工作,看看爬虫需要用到那些技术,然后做个计划出来,确定一下学习课程中缝,这不今天就先列出一些玩爬虫需要的准备工作!

Python爬虫这门技术你可以做得很简单,你也可以玩得很深入.打比方用简单的爬虫方式爬取1000万条数据可能需要一周时间,但如果你的爬虫玩得比较厉害,你可以采用分布式爬虫技术1天就能完成了1000万条数据。虽然都是爬虫,但这就是菜鸟与大牛的区别!这就和太*拳似的,易学难精!

这里面的技术点挺多的!现在来简单聊聊爬虫需要涉及的知识点。

网页知识

html,js,css,xpath这些知识,虽然简单,但一定需要了解。你得知道这些网页是如何构成的,然后才能去分解他们.

HTTP知识

一般爬虫你需要模拟浏览器的操作,才能去获取网页的信息

如果有些网站需要登录,才能获取更多的资料,你得去登录,你得把登录的账号密码进行提交

有些网站登录后需要保存cookie信息才能继续获取更多资料

正则表达式

有了正则表达式才能更好的分割网页信息,获取我们想要的数据,所以正则表达式也是需要了解的.

一些重要的爬虫库

url,url2,requests

beautiulSoup4,re,lxml

数据库

爬取到的数据我们得有个地方来保存,可以使用文件,也可以使用数据库,这里我会使用mysql,还有更适合爬虫的MongoDB数据库,以及分布式要用到的redis 数据库

爬虫框架

PySpider和Scrapy 这两个爬虫框架是非常NB的,简单的爬虫可以使用urllib与urllib2以及正则表达式就能完成,但高级的爬虫还得用这两个框架。这两个框架需要另行安装。后面一起学习.

反爬虫

有时候你的网站数据想禁止别人爬取,可以做一些反爬虫处理操作。打比方百度上就无法去查找淘宝上的数据,这样就避开了搜索引擎的竞争,淘宝就可以搞自己的一套竞价排名

分布式爬虫

使用多个redis实例来缓存各台主机上爬取的数据。

爬虫要学的东西还是挺多的,想把爬虫玩得666,基本就是这些知识点吧!好了,上面的东西我也只是粗略整理,笔误在所难免,后面我们会一起来学习爬虫知识吧!可以为您带来帮助。以上的内容都可以为您的python学习之路带来便利,小编祝您学习之路顺利。

Python 优化提速的 8 个小技巧

Python 优化提速的 8 个小技巧

%title插图%num

Python 是一种脚本语言,相比 C/C++ 这样的编译语言,在效率和性能方面存在一些不足。但是,有很多时候,Python 的效率并没有想象中的那么夸张。本文对一些 Python 代码加速运行的技巧进行整理。

0. 代码优化原则

本文会介绍不少的 Python 代码加速运行的技巧。在深入代码优化细节之前,需要了解一些代码优化基本原则。

*个基本原则是不要过早优化。很多人一开始写代码就奔着性能优化的目标,“让正确的程序更快要比让快速的程序正确容易得多”。因此,优化的前提是代码能正常工作。过早地进行优化可能会忽视对总体性能指标的把握,在得到全局结果前不要主次颠倒。

第二个基本原则是权衡优化的代价。优化是有代价的,想解决所有性能的问题是几乎不可能的。通常面临的选择是时间换空间或空间换时间。另外,开发代价也需要考虑。

第三个原则是不要优化那些无关紧要的部分。如果对代码的每一部分都去优化,这些修改会使代码难以阅读和理解。如果你的代码运行速度很慢,首先要找到代码运行慢的位置,通常是内部循环,专注于运行慢的地方进行优化。在其他地方,一点时间上的损失没有什么影响。

1. 避免全局变量

  1. # 不推荐写法。代码耗时:26.8
  2. import math
  3. size = 10000
  4. for x in range(size):
  5.     for y in range(size):
  6.         z = math.sqrt(x) + math.sqrt(y)

许多程序员刚开始会用 Python 语言写一些简单的脚本,当编写脚本时,通常习惯了直接将其写为全局变量,例如上面的代码。但是,由于全局变量和局部变量实现方式不同,定义在全局范围内的代码运行速度会比定义在函数中的慢不少。通过将脚本语句放入到函数中,通常可带来 15% – 30% 的速度提升。

  1. # 推荐写法。代码耗时:20.6
  2. import math
  3. def main():  # 定义到函数中,以减少全部变量使用
  4.     size = 10000
  5.     for x in range(size):
  6.         for y in range(size):
  7.             z = math.sqrt(x) + math.sqrt(y)
  8. main()

2. 避免.

2.1 避免模块和函数属性访问

  1. # 不推荐写法。代码耗时:14.5
  2. import math
  3. def computeSqrt(size: int):
  4.     result = []
  5.     for i in range(size):
  6.         result.append(math.sqrt(i))
  7.     return result
  8. def main():
  9.     size = 10000
  10.     for _ in range(size):
  11.         result = computeSqrt(size)
  12. main()

每次使用.(属性访问操作符时)会触发特定的方法,如__getattribute__()__getattr__(),这些方法会进行字典操作,因此会带来额外的时间开销。通过from import语句,可以消除属性访问。

  1. # *次优化写法。代码耗时:10.9
  2. from math import sqrt
  3. def computeSqrt(size: int):
  4.     result = []
  5.     for i in range(size):
  6.         result.append(sqrt(i))  # 避免math.sqrt的使用
  7.     return result
  8. def main():
  9.     size = 10000
  10.     for _ in range(size):
  11.         result = computeSqrt(size)
  12. main()

在第 1 节中我们讲到,局部变量的查找会比全局变量更快,因此对于频繁访问的变量sqrt,通过将其改为局部变量可以加速运行。

  1. # 第二次优化写法。代码耗时:9.9
  2. import math
  3. def computeSqrt(size: int):
  4.     result = []
  5.     sqrt = math.sqrt  # 赋值给局部变量
  6.     for i in range(size):
  7.         result.append(sqrt(i))  # 避免math.sqrt的使用
  8.     return result
  9. def main():
  10.     size = 10000
  11.     for _ in range(size):
  12.         result = computeSqrt(size)
  13. main()

除了math.sqrt外,computeSqrt函数中还有.的存在,那就是调用listappend方法。通过将该方法赋值给一个局部变量,可以彻底消除computeSqrt函数中for循环内部的.使用。

  1. # 推荐写法。代码耗时:7.9
  2. import math
  3. def computeSqrt(size: int):
  4.     result = []
  5.     append = result.append
  6.     sqrt = math.sqrt    # 赋值给局部变量
  7.     for i in range(size):
  8.         append(sqrt(i))  # 避免 result.append 和 math.sqrt 的使用
  9.     return result
  10. def main():
  11.     size = 10000
  12.     for _ in range(size):
  13.         result = computeSqrt(size)
  14. main()

2.2 避免类内属性访问

  1. # 不推荐写法。代码耗时:10.4
  2. import math
  3. from typing import List
  4. class DemoClass:
  5.     def __init__(self, value: int):
  6.         self._value = value
  7.     def computeSqrt(self, size: int) -> List[float]:
  8.         result = []
  9.         append = result.append
  10.         sqrt = math.sqrt
  11.         for _ in range(size):
  12.             append(sqrt(self._value))
  13.         return result
  14. def main():
  15.     size = 10000
  16.     for _ in range(size):
  17.         demo_instance = DemoClass(size)
  18.         result = demo_instance.computeSqrt(size)
  19. main()

避免.的原则也适用于类内属性,访问self._value的速度会比访问一个局部变量更慢一些。通过将需要频繁访问的类内属性赋值给一个局部变量,可以提升代码运行速度。

  1. # 推荐写法。代码耗时:8.0
  2. import math
  3. from typing import List
  4. class DemoClass:
  5.     def __init__(self, value: int):
  6.         self._value = value
  7.     def computeSqrt(self, size: int) -> List[float]:
  8.         result = []
  9.         append = result.append
  10.         sqrt = math.sqrt
  11.         value = self._value
  12.         for _ in range(size):
  13.             append(sqrt(value))  # 避免 self._value 的使用
  14.         return result
  15. def main():
  16.     size = 10000
  17.     for _ in range(size):
  18.         demo_instance = DemoClass(size)
  19.         demo_instance.computeSqrt(size)
  20. main()

3. 避免不必要的抽象

  1. # 不推荐写法,代码耗时:0.55
  2. class DemoClass:
  3.     def __init__(self, value: int):
  4.         self.value = value
  5.     @property
  6.     def value(self) -> int:
  7.         return self._value
  8.     @value.setter
  9.     def value(self, x: int):
  10.         self._value = x
  11. def main():
  12.     size = 1000000
  13.     for i in range(size):
  14.         demo_instance = DemoClass(size)
  15.         value = demo_instance.value
  16.         demo_instance.value = i
  17. main()

任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装代码时,都会让代码变慢。大部分情况下,需要重新进行审视使用属性访问器的定义是否有必要,使用getter/setter函数对属性进行访问通常是 C/C++ 程序员遗留下来的代码风格。如果真的没有必要,就使用简单属性。

  1. # 推荐写法,代码耗时:0.33
  2. class DemoClass:
  3.     def __init__(self, value: int):
  4.         self.value = value  # 避免不必要的属性访问器
  5. def main():
  6.     size = 1000000
  7.     for i in range(size):
  8.         demo_instance = DemoClass(size)
  9.         value = demo_instance.value
  10.         demo_instance.value = i
  11. main()

4. 避免数据复制

4.1 避免无意义的数据复制

  1. # 不推荐写法,代码耗时:6.5
  2. def main():
  3.     size = 10000
  4.     for _ in range(size):
  5.         value = range(size)
  6.         value_list = [x for x in value]
  7.         square_list = [x * x for x in value_list]
  8. main()

上面的代码中value_list完全没有必要,这会创建不必要的数据结构或复制。

  1. # 推荐写法,代码耗时:4.8
  2. def main():
  3.     size = 10000
  4.     for _ in range(size):
  5.         value = range(size)
  6.         square_list = [x * x for x in value]  # 避免无意义的复制
  7. main()

另外一种情况是对 Python 的数据共享机制过于偏执,并没有很好地理解或信任 Python 的内存模型,滥用 copy.deepcopy()之类的函数。通常在这些代码中是可以去掉复制操作的。

4.2 交换值时不使用中间变量

  1. # 不推荐写法,代码耗时:0.07
  2. def main():
  3.     size = 1000000
  4.     for _ in range(size):
  5.         a = 3
  6.         b = 5
  7.         temp = a
  8.         a = b
  9.         b = temp
  10. main()

上面的代码在交换值时创建了一个临时变量temp,如果不借助中间变量,代码更为简洁、且运行速度更快。

  1. # 推荐写法,代码耗时:0.06
  2. def main():
  3.     size = 1000000
  4.     for _ in range(size):
  5.         a = 3
  6.         b = 5
  7.         a, b = b, a  # 不借助中间变量
  8. main()

4.3 字符串拼接用join而不是+

  1. # 不推荐写法,代码耗时:2.6
  2. import string
  3. from typing import List
  4. def concatString(string_list: List[str]) -> str:
  5.     result = 
  6.     for str_i in string_list:
  7.         result += str_i
  8.     return result
  9. def main():
  10.     string_list = list(string.ascii_letters * 100)
  11.     for _ in range(10000):
  12.         result = concatString(string_list)
  13. main()

当使用a + b拼接字符串时,由于 Python 中字符串是不可变对象,其会申请一块内存空间,将ab分别复制到该新申请的内存空间中。因此,如果要拼接 n 个字符串,会产生 n-1 个中间结果,每产生一个中间结果都需要申请和复制一次内存,严重影响运行效率。而使用join()拼接字符串时,会首先计算出需要申请的总的内存空间,然后一次性地申请所需内存,并将每个字符串元素复制到该内存中去。

  1. # 推荐写法,代码耗时:0.3
  2. import string
  3. from typing import List
  4. def concatString(string_list: List[str]) -> str:
  5.     return .join(string_list)  # 使用 join 而不是 +
  6. def main():
  7.     string_list = list(string.ascii_letters * 100)
  8.     for _ in range(10000):
  9.         result = concatString(string_list)
  10. main()

5. 利用if条件的短路特性

  1. # 不推荐写法,代码耗时:0.05
  2. from typing import List
  3. def concatString(string_list: List[str]) -> str:
  4.     abbreviations = {‘cf.’‘e.g.’‘ex.’‘etc.’‘flg.’‘i.e.’‘Mr.’‘vs.’}
  5.     abbr_count = 0
  6.     result = 
  7.     for str_i in string_list:
  8.         if str_i in abbreviations:
  9.             result += str_i
  10.     return result
  11. def main():
  12.     for _ in range(10000):
  13.         string_list = [‘Mr.’‘Hat’‘is’‘Chasing’‘the’‘black’‘cat’‘.’]
  14.         result = concatString(string_list)
  15. main()

if 条件的短路特性是指对if a and b这样的语句, 当aFalse时将直接返回,不再计算b;对于if a or b这样的语句,当aTrue时将直接返回,不再计算b。因此, 为了节约运行时间,对于or语句,应该将值为True可能性比较高的变量写在or前,而and应该推后。

  1. # 推荐写法,代码耗时:0.03
  2. from typing import List
  3. def concatString(string_list: List[str]) -> str:
  4.     abbreviations = {‘cf.’‘e.g.’‘ex.’‘etc.’‘flg.’‘i.e.’‘Mr.’‘vs.’}
  5.     abbr_count = 0
  6.     result = 
  7.     for str_i in string_list:
  8.         if str_i[-1] == ‘.’ and str_i in abbreviations:  # 利用 if 条件的短路特性
  9.             result += str_i
  10.     return result
  11. def main():
  12.     for _ in range(10000):
  13.         string_list = [‘Mr.’‘Hat’‘is’‘Chasing’‘the’‘black’‘cat’‘.’]
  14.         result = concatString(string_list)
  15. main()

6. 循环优化

6.1 用for循环代替while循环

  1. # 不推荐写法。代码耗时:6.7
  2. def computeSum(size: int) -> int:
  3.     sum_ = 0
  4.     i = 0
  5.     while i < size:
  6.         sum_ += i
  7.         i += 1
  8.     return sum_
  9. def main():
  10.     size = 10000
  11.     for _ in range(size):
  12.         sum_ = computeSum(size)
  13. main()

Python 的for循环比while循环快不少。

  1. # 推荐写法。代码耗时:4.3
  2. def computeSum(size: int) -> int:
  3.     sum_ = 0
  4.     for i in range(size):  # for 循环代替 while 循环
  5.         sum_ += i
  6.     return sum_
  7. def main():
  8.     size = 10000
  9.     for _ in range(size):
  10.         sum_ = computeSum(size)
  11. main()

6.2 使用隐式for循环代替显式for循环

针对上面的例子,更进一步可以用隐式for循环来替代显式for循环

  1. # 推荐写法。代码耗时:1.7
  2. def computeSum(size: int) -> int:
  3.     return sum(range(size))  # 隐式 for 循环代替显式 for 循环
  4. def main():
  5.     size = 10000
  6.     for _ in range(size):
  7.         sum = computeSum(size)
  8. main()

6.3 减少内层for循环的计算

  1. # 不推荐写法。代码耗时:12.8
  2. import math
  3. def main():
  4.     size = 10000
  5.     sqrt = math.sqrt
  6.     for x in range(size):
  7.         for y in range(size):
  8.             z = sqrt(x) + sqrt(y)
  9. main()

上面的代码中sqrt(x)位于内侧for循环, 每次训练过程中都会重新计算一次,增加了时间开销。

  1. # 推荐写法。代码耗时:7.0
  2. import math
  3. def main():
  4.     size = 10000
  5.     sqrt = math.sqrt
  6.     for x in range(size):
  7.         sqrt_x = sqrt(x)  # 减少内层 for 循环的计算
  8.         for y in range(size):
  9.             z = sqrt_x + sqrt(y)
  10. main()

7. 使用numba.jit

我们沿用上面介绍过的例子,在此基础上使用numba.jitnumba可以将 Python 函数 JIT 编译为机器码执行,大大提高代码运行速度。关于numba的更多信息见下面的主页:http://numba.pydata.org/numba.pydata.org

  1. # 推荐写法。代码耗时:0.62
  2. import numba
  3. @numba.jit
  4. def computeSum(size: float) -> int:
  5.     sum = 0
  6.     for i in range(size):
  7.         sum += i
  8.     return sum
  9. def main():
  10.     size = 10000
  11.     for _ in range(size):
  12.         sum = computeSum(size)
  13. main()

8. 选择合适的数据结构

Python 内置的数据结构如strtuplelistsetdict底层都是 C 实现的,速度非常快,自己实现新的数据结构想在性能上达到内置的速度几乎是不可能的。

list类似于 C++ 中的std::vector,是一种动态数组。其会预分配一定内存空间,当预分配的内存空间用完,又继续向其中添加元素时,会申请一块更大的内存空间,然后将原有的所有元素都复制过去,之后销毁之前的内存空间,再插入新元素。

删除元素时操作类似,当已使用内存空间比预分配内存空间的一半还少时,会另外申请一块小内存,做一次元素复制,之后销毁原有大内存空间。

因此,如果有频繁的新增、删除操作,新增、删除的元素数量又很多时,list的效率不高。此时,应该考虑使用collections.dequecollections.deque是双端队列,同时具备栈和队列的特性,能够在两端进行 O(1) 复杂度的插入和删除操作。

list的查找操作也非常耗时。当需要在list频繁查找某些元素,或频繁有序访问这些元素时,可以使用bisect维护list对象有序并在其中进行二分查找,提升查找的效率。

另外一个常见需求是查找*小值或*大值,此时可以使用heapq模块将list转化为一个堆,使得获取*小值的时间复杂度是 O(1)。

下面的网页给出了常用的 Python 数据结构的各项操作的时间复杂度:https://wiki.python.org/moin/TimeComplexity

参考资料

  • David Beazley & Brian K. Jones. Python Cookbook, Third edition. O’Reilly Media, ISBN: 9781449340377, 2013.
  • 张颖 & 赖勇浩. 编写高质量代码:改善Python程序的91个建议. 机械工业出版社, ISBN: 9787111467045, 2014.

MySQL原理解析:MySQL的索引结构为什么使用B+树?

前言

在MySQL中,无论是Innodb还是MyIsam,都使用了B+树作索引结构(这里不考虑hash等其他索引)。本文将从*普通的二叉查找树开始,逐步说明各种树解决的问题以及面临的新问题,从而说明MySQL为什么选择B+树作为索引结构。

一、二叉查找树(BST):不平衡

二叉查找树(BST,Binary Search Tree),也叫二叉排序树,在二叉树的基础上需要满足:任意节点的左子树上所有节点值不大于根节点的值,任意节点的右子树上所有节点值不小于根节点的值。如下是一颗BST

 

MySQL原理解析:MySQL的索引结构为什么使用B+树?

 

 

当需要快速查找时,将数据存储在BST是一种常见的选择,因为此时查询时间取决于树高,平均时间复杂度是O(lgn)。然而,BST可能长歪而变得不平衡,如下图所示,此时BST退化为链表,时间复杂度退化为O(n)。

为了解决这个问题,引入了平衡二叉树。

 

MySQL原理解析:MySQL的索引结构为什么使用B+树?

 

 

二、平衡二叉树(AVL):旋转耗时

AVL树是严格的平衡二叉树,所有节点的左右子树高度差不能超过1;AVL树查找、插入和删除在平均和*坏情况下都是O(lgn)。

AVL实现平衡的关键在于旋转操作:插入和删除可能破坏二叉树的平衡,此时需要通过一次或多次树旋转来重新平衡这个树。当插入数据时,*多只需要1次旋转(单旋转或双旋转);但是当删除数据时,会导致树失衡,AVL需要维护从被删除节点到根节点这条路径上所有节点的平衡,旋转的量级为O(lgn)。

由于旋转的耗时,AVL树在删除数据时效率很低;在删除操作较多时,维护平衡所需的代价可能高于其带来的好处,因此AVL实际使用并不广泛。

三、红黑树:树太高

与AVL树相比,红黑树并不追求严格的平衡,而是大致的平衡:只是确保从根到叶子的*长的可能路径不多于*短的可能路径的两倍长。从实现来看,红黑树*大的特点是每个节点都属于两种颜色(红色或黑色)之一,且节点颜色的划分需要满足特定的规则(具体规则略)。红黑树示例如下:

 

MySQL原理解析:MySQL的索引结构为什么使用B+树?

 

 

与AVL树相比,红黑树的查询效率会有所下降,这是因为树的平衡性变差,高度更高。但红黑树的删除效率大大提高了,因为红黑树同时引入了颜色,当插入或删除数据时,只需要进行O(1)次数的旋转以及变色就能保证基本的平衡,不需要像AVL树进行O(lgn)次数的旋转。总的来说,红黑树的统计性能高于AVL。

因此,在实际应用中,AVL树的使用相对较少,而红黑树的使用非常广泛。例如,Java中的TreeMap使用红黑树存储排序键值对;Java8中的HashMap使用链表+红黑树解决哈希冲突问题(当冲突节点较少时,使用链表,当冲突节点较多时,使用红黑树)。

对于数据在内存中的情况(如上述的TreeMap和HashMap),红黑树的表现是非常优异的。但是对于数据在磁盘等辅助存储设备中的情况(如MySQL等数据库),红黑树并不擅长,因为红黑树长得还是太高了。当数据在磁盘中时,磁盘IO会成为*大的性能瓶颈,设计的目标应该是尽量减少IO次数;而树的高度越高,增删改查所需要的IO次数也越多,会严重影响性能。

四、B树:为磁盘而生

B树也称B-树(其中-不是减号),是为磁盘等辅存设备设计的多路平衡查找树,与二叉树相比,B树的每个非叶节点可以有多个子树。因此,当总节点数量相同时,B树的高度远远小于AVL树和红黑树(B树是一颗“矮胖子”),磁盘IO次数大大减少。

定义B树*重要的概念是阶数(Order),对于一颗m阶B树,需要满足以下条件:

  • 每个节点*多包含 m 个子节点。
  • 如果根节点包含子节点,则至少包含 2 个子节点;除根节点外,每个非叶节点至少包含 m/2 个子节点。
  • 拥有 k 个子节点的非叶节点将包含 k – 1 条记录。
  • 所有叶节点都在同一层中。

可以看出,B树的定义,主要是对非叶结点的子节点数量和记录数量的限制。

下图是一个3阶B树的例子:

 

MySQL原理解析:MySQL的索引结构为什么使用B+树?

 

 

B树的优势除了树高小,还有对访问局部性原理的利用。所谓局部性原理,是指当一个数据被使用时,其附近的数据有较大概率在短时间内被使用。B树将键相近的数据存储在同一个节点,当访问其中某个数据时,数据库会将该整个节点读到缓存中;当它临近的数据紧接着被访问时,可以直接在缓存中读取,无需进行磁盘IO;换句话说,B树的缓存命中率更高。

B树在数据库中有一些应用,如mongodb的索引使用了B树结构。但是在很多数据库应用中,使用了是B树的变种B+树。

五、B+树

B+树也是多路平衡查找树,其与B树的区别主要在于:

  • B树中每个节点(包括叶节点和非叶节点)都存储真实的数据,B+树中只有叶子节点存储真实的数据,非叶节点只存储键。在MySQL中,这里所说的真实数据,可能是行的全部数据(如Innodb的聚簇索引),也可能只是行的主键(如Innodb的辅助索引),或者是行所在的地址(如MyIsam的非聚簇索引)。
  • B树中一条记录只会出现一次,不会重复出现,而B+树的键则可能重复重现——一定会在叶节点出现,也可能在非叶节点重复出现。
  • B+树的叶节点之间通过双向链表链接。
  • B树中的非叶节点,记录数比子节点个数少1;而B+树中记录数与子节点个数相同。

由此,B+树与B树相比,有以下优势:

  • 更少的IO次数:B+树的非叶节点只包含键,而不包含真实数据,因此每个节点存储的记录个数比B数多很多(即阶m更大),因此B+树的高度更低,访问时所需要的IO次数更少。此外,由于每个节点存储的记录数更多,所以对访问局部性原理的利用更好,缓存命中率更高。
  • 更适于范围查询:在B树中进行范围查询时,首先找到要查找的下限,然后对B树进行中序遍历,直到找到查找的上限;而B+树的范围查询,只需要对链表进行遍历即可。
  • 更稳定的查询效率:B树的查询时间复杂度在1到树高之间(分别对应记录在根节点和叶节点),而B+树的查询复杂度则稳定为树高,因为所有数据都在叶节点。

B+树也存在劣势:由于键会重复出现,因此会占用更多的空间。但是与带来的性能优势相比,空间劣势往往可以接受,因此B+树的在数据库中的使用比B树更加广泛。

六、感受B+树的威力

前面说到,B树/B+树与红黑树等二叉树相比,*大的优势在于树高更小。实际上,对于Innodb的B+索引来说,树的高度一般在2-4层。下面来进行一些具体的估算。

树的高度是由阶数决定的,阶数越大树越矮;而阶数的大小又取决于每个节点可以存储多少条记录。Innodb中每个节点使用一个页(page),页的大小为16KB,其中元数据只占大约128字节左右(包括文件管理头信息、页面头信息等等),大多数空间都用来存储数据。

  • 对于非叶节点,记录只包含索引的键和指向下一层节点的指针。假设每个非叶节点页面存储1000条记录,则每条记录大约占用16字节;当索引是整型或较短的字符串时,这个假设是合理的。延伸一下,我们经常听到建议说索引列长度不应过大,原因就在这里:索引列太长,每个节点包含的记录数太少,会导致树太高,索引的效果会大打折扣,而且索引还会浪费更多的空间。
  • 对于叶节点,记录包含了索引的键和值(值可能是行的主键、一行完整数据等,具体见前文),数据量更大。这里假设每个叶节点页面存储100条记录(实际上,当索引为聚簇索引时,这个数字可能不足100;当索引为辅助索引时,这个数字可能远大于100;可以根据实际情况进行估算)。

对于一颗3层B+树,*层(根节点)有1个页面,可以存储1000条记录;第二层有1000个页面,可以存储10001000条记录;第三层(叶节点)有10001000个页面,每个页面可以存储100条记录,因此可以存储10001000100条记录,即1亿条。而对于二叉树,存储1亿条记录则需要26层左右。

七、总结

*后,总结一下各种树解决的问题以及面临的新问题:

  1. 二叉查找树(BST):解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表;
  2. 平衡二叉树(AVL):通过旋转解决了平衡的问题,但是旋转操作效率太低;
  3. 红黑树:通过舍弃严格的平衡和引入红黑节点,解决了AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多;
  4. B树:通过将二叉树改为多路平衡查找树,解决了树过高的问题;
  5. B+树:在B树的基础上,将非叶节点改造为不存储数据的纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效。

超全!我常用的70个数据分析网址

超全!我常用的70个数据分析网址

 

今天给大家分享的这篇文章,更像是一份数据分析常用网站字典,一共70个,可视化、词频词云、PPT模板等等面面俱到,值得收藏!

  数据可视化工具

百度ECharts http://echarts.baidu.com/
Cytoscape http://www.cytoscape.org/
图表秀 http://www.tubiaoxiu.com/
数据观 http://shujuguan.cn/
微博足迹可视化 http://vis.pku.edu.cn/weibova/weibogeo_footprint/index.html
BDP个人版 https://me.bdp.cn/home.html
魔镜 http://www.moojnn.com/
图表秀 https://www.tubiaoxiu.com
文图 https://www.wentu.io
百度图说 http://tushuo.baidu.com
infogr.am https://infogr.am/
Infographic https://venngage.com
visually https://visual.ly
Piktochart https://piktochart.com
slides https://slides.com
声享 https://ppt.baomitu.com
AntV https://antv.alipay.com/index.html

 词频分析工具、词云

Rost http://www.cncrk.com/downinfo/54638.html
图悦 http://www.picdata.cn/
语义分析系统 http://ictclas.nlpir.org/nlpir/
Tagul https://tagul.com/
腾讯文智 http://nlp.qq.com/semantic.cgi
Tagxedo词云 http://www.tagxedo.com/

 舆情分析工具

清博舆情系统 http://yuqing.gsdata.cn/
云相 http://www.weidata.cn/

  PPT模板工具

我图网 http://so.ooopic.com/
51PPT模板 http://www.51pptmoban.com/ppt/
无忧PPT http://www.51ppt.com.cn/
第1PPT http://www.1ppt.com/
站长之家 http://sc.chinaz.com/ppt/
设计师网址导航 http://www.userinterface.com.cn/
HiPPTer –  汇总PPT设计的酷站&神器 http://www.hippter.com
微软的官方网站——OFFICEPlus: http://www.officeplus.cn/
国货精品WPS公司下的稻壳儿 http://www.docer.com/
演界网 http://www.yanj.cn/

 互联网趋势分析工具

微博指数 http://data.weibo.com/index
百度指数 http://index.baidu.com/
好搜指数 http://index.so.com/#index
搜狗指数 http://zhishu.sogou.com/
百度预测 http://trends.baidu.com/

  在线调查工具

腾讯问卷调查 http://wj.qq.com/
麦客 http://www.mikecrm.com/
ICTR http://cn2.ictr.cn/
问道网 http://www.askform.cn/
问卷星 http://www.sojump.com/
调查派 http://www.diaochapai.com/
问卷网 http://www.wenjuan.com/
SurveyMonkey https://zh.surveymonkey.com/

  网站分析监测工具

H5传播分析工具 http://chuanbo.datastory.com.cn/
百度统计 http://tongji.baidu.com/web/welcome/login
腾讯云分析 http://mta.qq.com/
51.la http://www.51.la/

  社交媒体监测工具

孔明社会化媒体管理 http://www.kmsocial.cn/
企业微博管理中心 http://e.weibo.com/
知乎用户深度分析 http://www.kanzhihu.com/useranalysis

 其他数据网站

数据分析网 http://www.afenxi.com
媒体微博排行榜 http://v6.bang.weibo.com/xmt
友盟 http://www.umeng.com/
中国新闻地图 http://vis.360.cn/open/cnnews/
中国票房榜 http://www.cbooo.cn/
收视率排行 http://www.tvtv.hk/archives/category/tv
农业大数据云平台 http://www.dataagri.com/agriculture/gis.action
房价指数 http://industry.fang.com/data/datacenter.aspx
中国统计局 http://data.stats.gov.cn/
中国主要城市拥堵排名 http://report.amap.com/traffic/
中国综合社会调查 http://www.chinagss.org/
中国P2P网贷指数 http://www.p2p001.com/wdzs/wdzs_p2pline.html
Alexa http://www.alexa.com/
易车汽车指数 http://index.bitauto.com/
旅游预测 http://trends.baidu.com/tour/

万水千山总是情,点个 ???? 行不行。

  1. 推荐阅读:
  2. 入门: *全的零基础学Python的问题  | 零基础学了8个月的Python  | 实战项目 |学Python就是这条捷径
  3. 干货:爬取豆瓣短评,电影《后来的我们》 | 38年NBA*佳球员分析 |   从万众期待到口碑扑街!唐探3令人失望  | 笑看新倚天屠龙记 | 灯谜答题王 |用Python做个海量小姐姐素描图 |
  4. 趣味:弹球游戏  | 九宫格  | 漂亮的花 | 两百行Python《天天酷跑》游戏!
  5. AI: 会做诗的机器人 | 给图片上色 | 预测收入 | 碟中谍这么火,我用机器学习做个迷你推荐系统电影

年度爆款文案

Synchronized锁在Spring事务管理下,为啥还线程不安全

开启10000个线程,每个线程给员工表的money字段【初始值是0】加1,没有使用悲观锁和乐观锁,但是在业务层方法上加了synchronized关键字,问题是代码执行完毕后数据库中的money 字段不是10000,而是小于10000 问题出在哪里?

Service层代码:

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

SQL代码(没有加悲观/乐观锁):

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

用1000个线程跑代码:

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

简单来说:多线程跑一个使用synchronized关键字修饰的方法,方法内操作的是数据库,按正常逻辑应该*终的值是1000,但经过多次测试,结果是低于1000。这是为什么呢?

一、我的思考

既然测试出来的结果是低于1000,那说明这段代码不是线程安全的。不是线程安全的,那问题出现在哪呢?众所周知,synchronized方法能够保证所修饰的代码块、方法保证有序性、原子性、可见性。

讲道理,以上的代码跑起来,问题中Service层的increaseMoney()是有序的、原子的、可见的,所以断定跟synchronized应该没关系。

(参考我之前写过的synchronize锁笔记:Java锁机制了解一下)

既然Java层面上找不到原因,那分析一下数据库层面的吧(因为方法内操作的是数据库)。在increaseMoney()方法前加了@Transcational注解,说明这个方法是带有事务的。事务能保证同组的SQL要么同时成功,要么同时失败。讲道理,如果没有报错的话,应该每个线程都对money值进行+1。从理论上来说,结果应该是1000的才对。

(参考我之前写过的Spring事务:一文带你看懂Spring事务!)

根据上面的分析,我怀疑是提问者没测试好(hhhh,逃),于是我也跑去测试了一下,发现是以提问者的方式来使用是真的有问题

首先贴一下我的测试代码:

@RestController
public class EmployeeController {
 @Autowired
 private EmployeeService employeeService;
 @RequestMapping("/add")
 public void addEmployee() {
 for (int i = 0; i < 1000; i++) {
 new Thread(() -> employeeService.addEmployee()).start();
 }
 }
}
@Service
public class EmployeeService {
 @Autowired
 private EmployeeRepository employeeRepository;
 @Transactional
 public synchronized void addEmployee() {
 // 查出ID为8的记录,然后每次将年龄增加一
 Employee employee = employeeRepository.getOne(8);
 System.out.println(employee);
 Integer age = employee.getAge();
 employee.setAge(age + 1);
 employeeRepository.save(employee);
 }
}

简单地打印了每次拿到的employee值,并且拿到了SQL执行的顺序,如下(贴出小部分):

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

从打印的情况我们可以得出:多线程情况下并没有串行执行addEmployee()方法。这就导致对同一个值做重复的修改,所以*终的数值比1000要少。

二、图解出现的原因

发现并不是同步执行的,于是我就怀疑synchronized关键字和Spring肯定有点冲突。于是根据这两个关键字搜了一下,找到了问题所在。

我们知道Spring事务的底层是Spring AOP,而Spring AOP的底层是动态代理技术。跟大家一起回顾一下动态代理:

 public static void main(String[] args) {
 // 目标对象
 Object target ;
 Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
 @Override
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 // 但凡带有@Transcational注解的方法都会被拦截
 // 1... 开启事务
 method.invoke(target);
 // 2... 提交事务
 return null;
 }
 
 });
 }

(详细请参考我之前写过的动态代理:给女朋友讲解什么是代理模式)

实际上Spring做的处理跟以上的思路是一样的,我们可以看一下TransactionAspectSupport类中invokeWithinTransaction():

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

调用方法开启事务,调用方法提交事务

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

在多线程环境下,就可能会出现:方法执行完了(synchronized代码块执行完了),事务还没提交,别的线程可以进入被synchronized修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是*新的,所以就出现了这个问题。

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

三、解决问题

从上面我们可以发现,问题所在是因为@Transcational注解和synchronized一起使用了,加锁的范围没有包括到整个事务。所以我们可以这样做:

新建一个名叫SynchronizedService类,让其去调用addEmployee()方法,整个代码如下:

@RestController
public class EmployeeController {
 @Autowired
 private SynchronizedService synchronizedService ;
 @RequestMapping("/add")
 public void addEmployee() {
 for (int i = 0; i < 1000; i++) {
 new Thread(() -> synchronizedService.synchronizedAddEmployee()).start();
 }
 }
}
// 新建的Service类
@Service
public class SynchronizedService {
 @Autowired
 private EmployeeService employeeService ;
	
 // 同步
 public synchronized void synchronizedAddEmployee() {
 employeeService.addEmployee();
 }
}
@Service
public class EmployeeService {
 @Autowired
 private EmployeeRepository employeeRepository;
 
 @Transactional
 public void addEmployee() {
 // 查出ID为8的记录,然后每次将年龄增加一
 Employee employee = employeeRepository.getOne(8);
 System.out.println(Thread.currentThread().getName() + employee);
 Integer age = employee.getAge();
 employee.setAge(age + 1);
 employeeRepository.save(employee);
 }
}

我们将synchronized锁的范围包含到整个Spring事务上,这就不会出现线程安全的问题了。在测试的时候,我们可以发现1000个线程跑起来比之前要慢得多,当然我们的数据是正确的:

搞不懂,Synchronized锁在Spring事务管理下,为啥还线程不安全?

 

*后

可以发现的是,虽然说Spring事务用起来我们是非常方便的,但如果不了解一些Spring事务的细节,很多时候出现Bug了就百思不得其解。还是得继续加油努力呀~~~

 

filter、interceptor、aspect应如何选择

目录

  1. 前言
  2. Filter过滤器
  3. Interceptor拦截器
  4. Aspect切片
  5. 总结

前言

小伙伴们应该听说过过滤器、拦截器、切面,印象上都能够起到截断拦截的作用,在做一些业务需求时,不知道如何选择,今天就来介绍一下他们之间的区别。

Filter过滤器

过滤器可以拦截到方法的请求和响应(ServletRequest request, ServletResponse response),并对请求响应做出过滤操作。

过滤器依赖于servlet容器。在实现上,基于函数回调,它可以对几乎所有请求进行过滤,一个过滤器实例只能在容器初始化时调用一次。

使用过滤器的目的是用来做一些过滤操作,获取我们想要获取的数据,比如:在过滤器中修改字符编码;在过滤器中修改HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等。

话不多说,先上代码

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

再定义两个Controller,一个UserController,一个OrderController

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

虽然Filter过滤器和Controller请求都已经定义了,但现在过滤器是不起作用的。需要把Filter配置一下,有两个方案

*个方案在Filter上面加上@Component

@Component
public class TimeFilter implements Filter

第二个方案配置化注册过滤器

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

第二个方案的特点就是可以细化到过滤哪些规则的URL

我们来启动应用时,过滤器被初始化了,init函数被回调

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

请求http://localhost:9000/order/1

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

看看控制台的日志输出

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

请求http://localhost:9000/user/1

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

控制台日志输出

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

停止应用后,控制台输出

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

Filter随web应用的启动而启动,只初始化一次,随web应用的停止而销毁。

1.启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;

2.每一次请求时都只调用方法doFilter()进行处理

3.停止服务器时调用destroy()方法,销毁实例。

我们再来看看doFilter方法

doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

从参数我们看到,filter里面是能够获取到请求的参数和响应的数据;但此方法是无法知道是哪一个Controller类中的哪个方法被执行。

还有一点需要注意的是,filter中是没法使用注入的bean的,也就是无法使用@Autowired

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

上面代码注入的值为null。这是为什么呢

其实Spring中,web应用启动的顺序是:listener->filter->servlet,先初始化listener,然后再来就filter的初始化,再接着才到我们的dispathServlet的初始化,因此,当我们需要在filter里注入一个注解的bean时,就会注入失败,因为filter初始化时,注解的bean还没初始化,没法注入。

如果一定你要使用,需要做一些处理,可以私信老顾哦

Interceptor拦截器

依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上,基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在一个方法前,调用一个方法,或者在方法后,调用一个方法。

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

在WebMvcConfigurationSupport配置一下

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

执行结果

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

我们发现拦截器中可以获取到Controller对象

preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

object handler就是controller方法对象

HandlerMethod handlerMethod = (HandlerMethod)handler;
handlerMethod.getBean().getClass().getName(); //获取类名
handlerMethod.getMethod().getName(); //获取方法名

但我们发现获取不到方法的参数值,这个是为什么呢?在DispatcherServlet类中,方法

doDispatch(HttpServletRequest request, HttpServletResponse response)

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

applyPreHandle这个方法执行,就是执行的拦截器的preHandler方法,但这个过程中,controller方法没有从request中获取请求参数,组装方法参数;而是在ha.handle这个方法的时候,才会组装参数

虽然没法得到方法的参数,但是可以获得IOC的bean哦。

再说明一点的是postHandler方法

postHandler方法的执行,当controller内部有异常,posthandler方法是不会执行的。

afterCompletion方法,不管controller内部是否有异常,都会执行此方法;此方法还会有个Exception ex这个参数;如果有异常,ex会有异常值;没有异常 此值为null

注意点如果controller内部有异常,但异常被@ControllerAdvice 异常统一捕获的话,ex也会为null

Aspect切片

AOP操作可以对操作进行横向的拦截,*大的优势在于他可以获取执行方法的参数,对方法进行统一的处理。常见使用日志,事务,请求参数安全验证

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

上面的代码中,我们是可以获取方法的参数的

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

虽然切面aop可以拿到方法参数,但拿不到response,request对象。

总结

我们这里来总结一下过滤器、拦截器、Aspect,看看区别

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

如果三者方式同时采用,那他们的执行顺序是什么呢?

filter -> interceptor -> ControllerAdvice -> aspect -> controller

返回值顺序,或异常返回顺序

controller -> aspect -> controllerAdvice -> Interceptor -> Filter

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

用一个图描述一下执行顺序

阿里二面:filter、interceptor、aspect应如何选择?很多人中招

 

nginx配置调优

一、一般来说nginx 配置文件中对优化比较有作用的为以下几项:

1. worker_processes 8;

nginx 进程数,建议按照cpu 数目来指定,一般为它的倍数 (如,2个四核的cpu计为8)。

2. worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;

为每个进程分配cpu,上例中将8 个进程分配到8 个cpu,当然可以写多个,或者将一
个进程分配到多个cpu。

3. worker_rlimit_nofile 65535;

这个指令是指当一个nginx 进程打开的*多文件描述符数目,理论值应该是*多打开文
件数(ulimit -n)与nginx 进程数相除,但是nginx 分配请求并不是那么均匀,所以*好与ulimit -n 的值保持一致。

现在在linux 2.6内核下开启文件打开数为65535,worker_rlimit_nofile就相应应该填写65535。

这是因为nginx调度时分配请求到进程并不是那么的均衡,所以假如填写10240,总并发量达到3-4万时就有进程可能超过10240了,这时会返回502错误。

查看linux系统文件描述符的方法:

[root@web001 ~]# sysctl -a | grep fs.file

fs.file-max = 789972

fs.file-nr = 510 0 789972

4. use epoll;

使用epoll 的I/O 模型

(

补充说明:

与apache相类,nginx针对不同的操作系统,有不同的事件模型

A)标准事件模型
Select、poll属于标准事件模型,如果当前系统不存在更有效的方法,nginx会选择select或poll
B)高效事件模型
Kqueue:使用于 FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 和 MacOS X. 使用双处理器的MacOS X系统使用kqueue可能会造成内核崩溃。
Epoll: 使用于Linux内核2.6版本及以后的系统。

 

/dev/poll:使用于 Solaris 7 11/99+, HP/UX 11.22+ (eventport), IRIX 6.5.15+ 和 Tru64 UNIX 5.1A+。

Eventport:使用于 Solaris 10. 为了防止出现内核崩溃的问题, 有必要安装安全补丁。

)

5. worker_connections 65535;

每个进程允许的*多连接数, 理论上每台nginx 服务器的*大连接数为worker_processes*worker_connections。

6. keepalive_timeout 60;

keepalive 超时时间。

7. client_header_buffer_size 4k;

客户端请求头部的缓冲区大小,这个可以根据你的系统分页大小来设置,一般一个请求头的大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。

分页大小可以用命令getconf PAGESIZE 取得。

[root@web001 ~]# getconf PAGESIZE

4096

但也有client_header_buffer_size超过4k的情况,但是client_header_buffer_size该值必须设置为“系统分页大小”的整倍数。

8. open_file_cache max=65535 inactive=60s;

这个将为打开文件指定缓存,默认是没有启用的,max 指定缓存数量,建议和打开文件数一致,inactive 是指经过多长时间文件没被请求后删除缓存。

9. open_file_cache_valid 80s;

这个是指多长时间检查一次缓存的有效信息。

10. open_file_cache_min_uses 1;

open_file_cache 指令中的inactive 参数时间内文件的*少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在inactive 时间内一次没被使用,它将被移除。

 

二、关于内核参数的优化:

net.ipv4.tcp_max_tw_buckets = 6000

timewait 的数量,默认是180000。

net.ipv4.ip_local_port_range = 1024 65000

允许系统打开的端口范围。

net.ipv4.tcp_tw_recycle = 1

启用timewait 快速回收。

net.ipv4.tcp_tw_reuse = 1

开启重用。允许将TIME-WAIT sockets 重新用于新的TCP 连接。

net.ipv4.tcp_syncookies = 1

开启SYN Cookies,当出现SYN 等待队列溢出时,启用cookies 来处理。

net.core.somaxconn = 262144

web 应用中listen 函数的backlog 默认会给我们内核参数的net.core.somaxconn 限制到128,而nginx 定义的NGX_LISTEN_BACKLOG 默认为511,所以有必要调整这个值。

net.core.netdev_max_backlog = 262144

每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的*大数目。

net.ipv4.tcp_max_orphans = 262144

系统中*多有多少个TCP 套接字不被关联到任何一个用户文件句柄上。如果超过这个数字,孤儿连接将即刻被复位并打印出警告信息。这个限制仅仅是为了防止简单的DoS 攻击,不能过分依靠它或者人为地减小这个值,更应该增加这个值(如果增加了内存之后)。

net.ipv4.tcp_max_syn_backlog = 262144

记录的那些尚未收到客户端确认信息的连接请求的*大值。对于有128M 内存的系统而言,缺省值是1024,小内存的系统则是128。

net.ipv4.tcp_timestamps = 0

时间戳可以避免序列号的卷绕。一个1Gbps 的链路肯定会遇到以前用过的序列号。时间戳能够让内核接受这种“异常”的数据包。这里需要将其关掉。

net.ipv4.tcp_synack_retries = 1

为了打开对端的连接,内核需要发送一个SYN 并附带一个回应前面一个SYN 的ACK。也就是所谓三次握手中的第二次握手。这个设置决定了内核放弃连接之前发送SYN+ACK 包的数量。

net.ipv4.tcp_syn_retries = 1

在内核放弃建立连接之前发送SYN 包的数量。

net.ipv4.tcp_fin_timeout = 1

如 果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2 状态的时间。对端可以出错并永远不关闭连接,甚至意外当机。缺省值是60 秒。2.2 内核的通常值是180 秒,3你可以按这个设置,但要记住的是,即使你的机器是一个轻载的WEB 服务器,也有因为大量的死套接字而内存溢出的风险,FIN- WAIT-2 的危险性比FIN-WAIT-1 要小,因为它*多只能吃掉1.5K 内存,但是它们的生存期长些。

net.ipv4.tcp_keepalive_time = 30

当keepalive 起用的时候,TCP 发送keepalive 消息的频度。缺省是2 小时。

 

三、下面贴一个完整的内核优化设置:

vi /etc/sysctl.conf CentOS5.5中可以将所有内容清空直接替换为如下内容:

net.ipv4.ip_forward = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.default.accept_source_route = 0
kernel.sysrq = 0
kernel.core_uses_pid = 1
net.ipv4.tcp_syncookies = 1
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 68719476736
kernel.shmall = 4294967296
net.ipv4.tcp_max_tw_buckets = 6000
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 16384 4194304
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.netdev_max_backlog = 262144
net.core.somaxconn = 262144
net.ipv4.tcp_max_orphans = 3276800
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_fin_timeout = 1
net.ipv4.tcp_keepalive_time = 30
net.ipv4.ip_local_port_range = 1024 65000

使配置立即生效可使用如下命令:
/sbin/sysctl -p

四、下面是关于系统连接数的优化

linux 默认值 open files 和 max user processes 为 1024

#ulimit -n

1024

#ulimit Cu

1024

问题描述: 说明 server 只允许同时打开 1024 个文件,处理 1024 个用户进程

使用ulimit -a 可以查看当前系统的所有限制值,使用ulimit -n 可以查看当前的*大打开文件数。

新装的linux 默认只有1024 ,当作负载较大的服务器时,很容易遇到error: too many open files 。因此,需要将其改大。

解决方法:

使用 ulimit Cn 65535 可即时修改,但重启后就无效了。(注ulimit -SHn 65535 等效 ulimit -n 65535 ,-S 指soft ,-H 指hard)

有如下三种修改方式:

1. 在/etc/rc.local 中增加一行 ulimit -SHn 65535
2. 在/etc/profile 中增加一行 ulimit -SHn 65535
3. 在/etc/security/limits.conf *后增加:

* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535

具体使用哪种,在 CentOS 中使用第1 种方式无效果,使用第3 种方式有效果,而在Debian 中使用第2 种有效果

# ulimit -n

65535

# ulimit -u

65535

备注:ulimit 命令本身就有分软硬设置,加-H 就是硬,加-S 就是软默认显示的是软限制

soft 限制指的是当前系统生效的设置值。 hard 限制值可以被普通用户降低。但是不能增加。 soft 限制不能设置的比 hard 限制更高。 只有 root 用户才能够增加 hard 限制值。

 

五、下面是一个简单的nginx 配置文件:

user www www;
worker_processes 8;
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000
01000000;
error_log /www/log/nginx_error.log crit;
pid /usr/local/nginx/nginx.pid;
worker_rlimit_nofile 204800;
events
{
use epoll;
worker_connections 204800;
}
http
{
include mime.types;
default_type application/octet-stream;
charset utf-8;
server_names_hash_bucket_size 128;
client_header_buffer_size 2k;
large_client_header_buffers 4 4k;
client_max_body_size 8m;
sendfile on;
tcp_nopush on;
keepalive_timeout 60;
fastcgi_cache_path /usr/local/nginx/fastcgi_cache levels=1:2
keys_zone=TEST:10m
inactive=5m;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 4k;
fastcgi_buffers 8 4k;
fastcgi_busy_buffers_size 8k;
fastcgi_temp_file_write_size 8k;
fastcgi_cache TEST;
fastcgi_cache_valid 200 302 1h;
fastcgi_cache_valid 301 1d;
fastcgi_cache_valid any 1m;
fastcgi_cache_min_uses 1;
fastcgi_cache_use_stale error timeout invalid_header http_500;
open_file_cache max=204800 inactive=20s;
open_file_cache_min_uses 1;
open_file_cache_valid 30s;
tcp_nodelay on;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
gzip_vary on;
server
{
listen 8080;
server_name backup.aiju.com;
index index.php index.htm;
root /www/html/;
location /status
{
stub_status on;
}
location ~ .*/.(php|php5)?$
{
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fcgi.conf;
}
location ~ .*/.(gif|jpg|jpeg|png|bmp|swf|js|css)$
{
expires 30d;
}
log_format access ‘$remote_addr — $remote_user [$time_local] “$request” ‘
‘$status $body_bytes_sent “$http_referer” ‘
‘”$http_user_agent” $http_x_forwarded_for’;
access_log /www/log/access.log access;
}
}

六、关于FastCGI 的几个指令:

fastcgi_cache_path /usr/local/nginx/fastcgi_cache levels=1:2 keys_zone=TEST:10minactive=5m;

这个指令为FastCGI 缓存指定一个路径,目录结构等级,关键字区域存储时间和非活动删除时间。

fastcgi_connect_timeout 300;

指定连接到后端FastCGI 的超时时间。

fastcgi_send_timeout 300;

向FastCGI 传送请求的超时时间,这个值是指已经完成两次握手后向FastCGI 传送请求的超时时间。

fastcgi_read_timeout 300;

接收FastCGI 应答的超时时间,这个值是指已经完成两次握手后接收FastCGI 应答的超时时间。

fastcgi_buffer_size 4k;

指定读取FastCGI 应答*部分需要用多大的缓冲区,一般*部分应答不会超过1k,由于页面大小为4k,所以这里设置为4k。

fastcgi_buffers 8 4k;

指定本地需要用多少和多大的缓冲区来缓冲FastCGI 的应答。

fastcgi_busy_buffers_size 8k;

这个指令我也不知道是做什么用,只知道默认值是fastcgi_buffers 的两倍。

fastcgi_temp_file_write_size 8k;

在写入fastcgi_temp_path 时将用多大的数据块,默认值是fastcgi_buffers 的两倍。

fastcgi_cache TEST

开启FastCGI 缓存并且为其制定一个名称。个人感觉开启缓存非常有用,可以有效降低CPU 负载,并且防止502 错误。

fastcgi_cache_valid 200 302 1h;
fastcgi_cache_valid 301 1d;
fastcgi_cache_valid any 1m;

为指定的应答代码指定缓存时间,如上例中将200,302 应答缓存一小时,301 应答缓存1 天,其他为1 分钟。

fastcgi_cache_min_uses 1;

缓存在fastcgi_cache_path 指令inactive 参数值时间内的*少使用次数,如上例,如果在5 分钟内某文件1 次也没有被使用,那么这个文件将被移除。

fastcgi_cache_use_stale error timeout invalid_header http_500;

不知道这个参数的作用,猜想应该是让nginx 知道哪些类型的缓存是没用的。以上为nginx 中FastCGI 相关参数,另外,FastCGI 自身也有一些配置需要进行优化,如果你使用php-fpm 来管理FastCGI,可以修改配置文件中的以下值:

<value name=”max_children”>60</value>

同时处理的并发请求数,即它将开启*多60 个子线程来处理并发连接。

<value name=”rlimit_files”>102400</value>

*多打开文件数。

<value name=”max_requests”>204800</value>

每个进程在重置之前能够执行的*多请求数。

为什么要使用MQ消息中间件以及选型

一、为什么要使用MQ消息中间件?

一个用消息队列的人,不知道为啥用,有点尴尬。没有复习这点,很容易被问蒙,然后就开始胡扯了。

回答:这个问题,咱只答三个*主要的应用场景,不可否认还有其他的,但是只答三个主要的,即以下六个字:

解耦、异步、削峰

1、解耦

传统模式:

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

传统模式的缺点:

系统间耦合性太强,如上图所示,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!

中间件模式:

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

中间件模式的的优点:

将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。

2、异步

传统模式:

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

传统模式的缺点:

一些非必要的业务逻辑以同步的方式运行,太耗费时间。

中间件模式:

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

中间件模式的的优点:

将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

3、削峰

传统模式

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

传统模式的缺点:

并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常

中间件模式:

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

中间件模式的的优点:

系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。

二、使用了消息队列会有什么缺点?

分析:一个使用了MQ的项目,如果连这个问题都没有考虑过,就把MQ引进去了,那就给自己的项目带来了风险。

我们引入一个技术,要对这个技术的弊端有充分的认识,才能做好预防。要记住,不要给公司挖坑!

回答:回答也很容易,从以下两个个角度来答

系统可用性降低:

你想啊,本来其他系统只要运行好好的,那你的系统就是正常的。

现在你非要加个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性降低

系统复杂性增加:

要多考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输。

因此,需要考虑的东西更多,系统复杂性增大。

但是,我们该用还是要用的。

三、消息队列如何选型?

先说一下,我只会ActiveMQ,RabbitMQ,RocketMQ,Kafka,对什么ZeroMQ等其他MQ没啥理解,因此只能基于这四种MQ给出回答。

分析:既然在项目中用了MQ,肯定事先要对业界流行的MQ进行调研,如果连每种MQ的优缺点都没了解清楚,就拍脑袋依据喜好,用了某种MQ,还是给项目挖坑。

如果面试官问:”你为什么用这种MQ?。”

你直接回答”领导决定的。”

这种回答就很LOW了。

还是那句话,不要给公司挖坑。

我们可以看出,RabbitMQ版本发布比ActiveMq频繁很多。至于RocketMQ和kafka就不带大家看了,总之也比ActiveMQ活跃的多。详情,可自行查阅。

再来一个性能对比表

当面试官要你介绍一下MQ时,该怎么回答?

 

综合上面的材料得出以下两点:

1、中小型软件公司,建议选RabbitMQ.

一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。

正所谓,成也萧何,败也萧何!他的弊端也在这里,虽然RabbitMQ是开源的,然而国内有几个能定制化开发erlang的程序员呢?

所幸,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。

不考虑rocketmq和kafka的原因是,一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。

不考虑rocketmq的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。

2、大型软件公司,根据具体使用在rocketMq和kafka之间二选一

一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。

针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。

至于kafka,根据业务场景选择,如果有日志采集功能,肯定是首选kafka了。具体该选哪个,看使用场景。

四、如何保证消息队列是高可用的?

在第二点说过了,引入消息队列后,系统的可用性下降。

在生产中,没人使用单机模式的消息队列。

因此,作为一个合格的程序员,应该对消息队列的高可用有很深刻的了解。

如果面试官问:”你们的消息中间件如何保证高可用的?”

如果你的回答只是表明自己只会订阅和发布消息,面试官就会怀疑你是不是只是自己搭着玩,压根没在生产用过。

因此,请做一个爱思考,会思考,懂思考的程序员。

回答:这问题,其实要对消息队列的集群模式要有深刻了解,才好回答。

以rcoketMQ为例,他的集群就有多master 模式、多master多slave异步复制模式、多 master多slave同步双写模式。

多master多slave模式部署架构图(网上找的,偷个懒,懒得画):

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

其实博主*眼看到这个图,就觉得和kafka好像,只是NameServer集群,在kafka中是用zookeeper代替,都是用来保存和发现master和slave用的。

通信过程如下:

Producer 与 NameServer集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Broker Master 建立长连接,且定时向 Broker 发送心跳。

Producer 只能将消息发送到 Broker master,但是 Consumer 则不一样,它同时和提供 Topic 服务的 Master 和 Slave建立长连接,既可以从 Broker Master 订阅消息,也可以从 Broker Slave 订阅消息。

那么kafka呢,为了对比说明直接上kafka的拓补架构图(也是找的,懒得画)

当面试官要你介绍一下MQ时,该怎么回答?

 

 

如上图所示,一个典型的Kafka集群中包含若干Producer(可以是web前端产生的Page View,或者是服务器日志,系统CPU、Memory等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。

Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。

Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。

至于rabbitMQ,也有普通集群和镜像集群模式,自行去了解,比较简单,两小时即懂。

要求,在回答高可用的问题时,应该能逻辑清晰的画出自己的MQ集群架构或清晰的叙述出来。

五、如何保证消息不被重复消费?

这个问题其实换一种问法就是,如何保证消息队列的幂等性?

这个问题可以认为是消息队列领域的基本问题。换句话来说,是在考察你的设计能力,这个问题的回答可以根据具体的业务场景来答,没有固定的答案。

回答:先来说一下为什么会造成重复消费?

其实无论是那种消息队列,造成重复消费原因其实都是类似的。

正常情况下,消费者在消费消息时候,消费完毕后,会发送一个确认信息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。只是不同的消息队列发送的确认信息形式不同

例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,kafka实际上有个offset的概念

简单说一下(如果还不懂,出门找一个kafka入门到精通教程),就是每一个消息都有一个offset,kafka消费过消息后,需要提交offset,让消息队列知道自己已经消费过了。

那造成重复消费的原因?

就是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。

如何解决?这个问题针对业务场景来答分以下几点

1、比如,你拿到这个消息做数据库的insert操作。

那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

2、再比如,你拿到这个消息做redis的set的操作

那就容易了,不用解决。因为你无论set几次结果都是一样的,set操作本来就算幂等操作。

3、如果上面两种情况还不行,上大招。

准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

六、如何保证消费的可靠性传输?

分析:我们在使用消息队列的过程中,应该做到消息不能多消费,也不能少消费。如果无法做到可靠性传输,可能给公司带来千万级别的财产损失。

同样的,如果可靠性传输在使用过程中,没有考虑到,这不是给公司挖坑么,你可以拍拍屁股走了,公司损失的钱,谁承担。

还是那句话,认真对待每一个项目,不要给公司挖坑

回答:其实这个可靠性传输,每种MQ都要从三个角度来分析:生产者弄丢数据、消息队列弄丢数据、消费者弄丢数据

RabbitMQ

1、生产者丢数据

从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。

transaction机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。

然而缺点就是吞吐量下降了。因此,按照博主的经验,生产上用confirm模式的居多。

一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始)

一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID)

这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

处理Ack和Nack的代码如下所示(说好不上代码的,偷偷上了):

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

2、消息队列丢数据

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。

这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。

这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步

1、将queue的持久化标识durable设置为true,则代表是一个持久的队列

2、发送消息的时候将deliveryMode=2

这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据

3、消费者丢数据

消费者丢数据一般是因为采用了自动确认消息模式。

这种模式下,消费者会自动确认收到信息。这时rahbitMQ会立即将消息删除,这种情况下如果消费者出现异常而没能处理该消息,就会丢失该消息。

至于解决方案,采用手动确认消息即可。

kafka

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

Producer在发布消息到某个Partition时,先通过ZooKeeper找到该Partition的Leader

然后无论该Topic的Replication Factor为多少(也即该Partition有多少个Replica),Producer只将该消息发送到该Partition的Leader。

Leader会将该消息写入其本地Log。每个Follower都从Leader中pull数据。

针对上述情况,得出如下分析

1、生产者丢数据

在kafka生产中,基本都有一个leader和多个follwer。follwer会去同步leader的信息。

因此,为了避免生产者丢数据,做如下两点配置

*个配置要在producer端设置acks=all。这个配置保证了,follwer同步完成后,才认为消息发送成功。

在producer端设置retries=MAX,一旦写入失败,这无限重试

2、消息队列丢数据

针对消息队列丢数据的情况,无外乎就是,数据还没同步,leader就挂了,这时zookpeer会将其他的follwer切换为leader,那数据就丢失了。

针对这种情况,应该做两个配置。

replication.factor参数,这个值必须大于1,即要求每个partition必须有至少2个副本

min.insync.replicas参数,这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系

这两个配置加上上面生产者的配置联合起来用,基本可确保kafka不丢数据

3、消费者丢数据

这种情况一般是自动提交了offset,然后你处理程序过程中挂了。kafka以为你处理好了。

再强调一次offset是干嘛的

offset:指的是kafka的topic中的每个消费组消费的下标。

简单的来说就是一条消息对应一个offset下标,每次消费数据的时候如果提交offset,那么下次消费就会从提交的offset加一那里开始消费。

比如一个topic中有100条数据,我消费了50条并且提交了,那么此时的kafka服务端记录提交的offset就是49(offset从0开始),那么下次消费的时候offset就从50开始消费。

解决方案也很简单,改成手动提交即可。

七、如何保证消息的顺序性?

分析:其实并非所有的公司都有这种业务需求,但是还是对这个问题要有所复习。

回答:针对这个问题,通过某种算法,将需要保持先后顺序的消息放到同一个消息队列中(kafka中就是partition,rabbitMq中就是queue)。然后只用一个消费者去消费该队列。

有的人会问:那如果为了吞吐量,有多个消费者去消费怎么办?

这个问题,没有固定回答的套路。比如我们有一个微博的操作,发微博、写评论、删除微博,这三个异步操作。如果是这样一个业务场景,那只要重试就行。

比如你一个消费者先执行了写评论的操作,但是这时候,微博都还没发,写评论一定是失败的,等一段时间。等另一个消费者,先执行写评论的操作后,再执行,就可以成功。

总之,针对这个问题,我的观点是保证入队有序就行,出队以后的顺序交给消费者自己去保证,没有固定套路。

 

当面试官要你介绍一下MQ时,该怎么回答?

 

 

八、总结

写到这里,希望读者把本文提出的这几个问题,经过深刻的准备后,一般来说,能囊括大部分的消息队列的知识点。

如果面试官不问这几个问题怎么办,简单,自己把几个问题讲清楚,突出以下自己考虑的全面性。

*后,其实我不太提倡这样突击复习,希望大家打好基本功,做一个爱思考,懂思考,会思考的程序员。

TCP粘包、拆包以及粘包、拆包产生原因

一、TCP粘包、拆包图解

阿里Java一面:熟悉TCP粘包、拆包?说说粘包、拆包产生原因

 

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:

  1. 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
  2. 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
  3. 服务端分两次读取到了数据包,*次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
  4. 服务端分两次读取到了数据包,*次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。

特别要注意的是,如果TCP的接受滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包。

二、 粘包、拆包产生原因

产生原因主要有这3种:

  • 滑动窗口
  • MSS/MTU限制
  • Nagle算法

1、滑动窗口

TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。

现在来看一下滑动窗口是如何造成粘包、拆包的?

粘包:假设发送方的每256 bytes表示一个完整的报文,接收方由于数据处理不及时,这256个字节的数据都会被缓存到SO_RCVBUF(接收缓存区)中。如果接收方的SO_RCVBUF中缓存了多个报文,那么对于接收方而言,这就是粘包。

拆包:考虑另外一种情况,假设接收方的窗口只剩了128,意味着发送方*多还可以发送128字节,而由于发送方的数据大小是256字节,因此只能发送前128字节,等到接收方ack后,才能发送剩余字节。这就造成了拆包。

2、MSS和MTU分片

MSS: 是Maximum Segement Size缩写,表示TCP报文中data部分的*大长度,是TCP协议在OSI五层网络模型中传输层对一次可以发送的*大数据的限制。

MTU: *大传输单元是Maxitum Transmission Unit的简写,是OSI五层网络模型中链路层(datalink layer)对一次可以发送的*大数据的限制。

当需要传输的数据大于MSS或者MTU时,数据会被拆分成多个包进行传输。由于MSS是根据MTU计算出来的,因此当发送的数据满足MSS时,必然满足MTU。

为了更好的理解,我们先介绍一下在5层网络模型中应用通过TCP发送数据的流程:

阿里Java一面:熟悉TCP粘包、拆包?说说粘包、拆包产生原因

 

对于应用层来说,只关心发送的数据DATA,将数据写入socket在内核中的发送缓冲区SO_SNDBUF即返回,操作系统会将SO_SNDBUF中的数据取出来进行发送。传输层会在DATA前面加上TCP Header,构成一个完整的TCP报文。

当数据到达网络层(network layer)时,网络层会在TCP报文的基础上再添加一个IP Header,也就是将自己的网络地址加入到报文中。到数据链路层时,还会加上Datalink Header和CRC。

当到达物理层时,会将SMAC(Source Machine,数据发送方的MAC地址),DMAC(Destination Machine,数据接受方的MAC地址 )和Type域加入。

可以发现数据在发送前,每一层都会在上一层的基础上增加一些内容,下图演示了MSS、MTU在这个过程中的作用。

阿里Java一面:熟悉TCP粘包、拆包?说说粘包、拆包产生原因

 

MTU是以太网传输数据方面的限制,每个以太网帧都有*小的大小64bytes*大不能超过1518bytes。刨去以太网帧的帧头 (DMAC目的MAC地址48bit=6Bytes+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾 CRC校验部分4Bytes(这个部分有时候大家也把它叫做FCS),那么剩下承载上层协议的地方也就是Data域*大就只能有1500Bytes这个值 我们就把它称之为MTU。

由于MTU限制了一次*多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCP Header和Ip Header,因此刨去这两个部分,就是TCP协议一次可以发送的实际应用数据的*大大小,也就是MSS。

MSS长度=MTU长度-IP Header-TCP Header

TCP Header的长度是20字节,IPv4中IP Header长度是20字节,IPV6中IP Header长度是40字节,因此:在IPV4中,以太网MSS可以达到1460byte;在IPV6中,以太网MSS可以达到1440byte。

需要注意的是MSS表示的一次可以发送的DATA的*大长度,而不是DATA的真实长度。发送方发送数据时,当SO_SNDBUF中的数据量大于MSS时,操作系统会将数据进行拆分,使得每一部分都小于MSS,这就是拆包,然后每一部分都加上TCP Header,构成多个完整的TCP报文进行发送,当然经过网络层和数据链路层的时候,还会分别加上相应的内容。

需要注意: 默认情况下,与外部通信的网卡的MTU大小是1500个字节。而本地回环地址的MTU大小为65535,这是因为本地测试时数据不需要走网卡,所以不受到1500的限制。

3、 Nagle算法

TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),同时,对方接收到数据,也需要发送ACK表示确认。

即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据。这种情况转变成了4000%的消耗,这样的情况对于重负载的网络来是无法接受的。

为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。

Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

Nagle算法的基本定义是任意时刻,*多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

Nagle算法的规则:

  1. 如果SO_SNDBUF(发送缓冲区)中的数据长度达到MSS,则允许发送;
  2. 如果该SO_SNDBUF中含有FIN,表示请求关闭连接,则先将SO_SNDBUF中的剩余数据发送,再关闭;
  3. 设置了TCP_NODELAY=true选项,则允许发送。TCP_NODELAY是取消TCP的确认延迟机制,相当于禁用了Nagle 算法。
  4. 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
  5. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

elasticsearch持久化变更

如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。

在 动态更新索引,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。

即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。

Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。通过 translog ,整个流程看起来是下面这样:

  1. 一个文档被索引之后,就会被添加到内存缓冲区,并且 追加到了 translog ,正如 图 21 “新的文档被添加到内存缓冲区并且被追加到了事务日志” 描述的一样。

    图 21. 新的文档被添加到内存缓冲区并且被追加到了事务日志%title插图%num

  2.  
  3. 刷新(refresh)使分片处于 图 22 “刷新(refresh)完成后, 缓存被清空但是事务日志不会” 描述的状态,分片每秒被刷新(refresh)一次:
    • 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync 操作。
    • 这个段被打开,使其可被搜索。
    • 内存缓冲区被清空。

    图 22. 刷新(refresh)完成后, 缓存被清空但是事务日志不会%title插图%num

  4.  
  5. 这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志(见 图 23 “事务日志不断积累文档” )。

    图 23. 事务日志不断积累文档%title插图%num

  6.  
  7. 每隔一段时间—​例如 translog 变得越来越大—​索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行(见 图 24 “在刷新(flush)之后,段被全量提交,并且事务日志被清空” ):
    • 所有在内存缓冲区的文档都被写入一个新的段。
    • 缓冲区被清空。
    • 一个提交点被写入硬盘。
    • 文件系统缓存通过 fsync 被刷新(flush)。
    • 老的 translog 被删除。

translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用*后一个提交点去恢复已知的段,并且会重放 translog 中所有在*后一次提交后发生的变更操作。

translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何*近的变更。这意味着它总是能够实时地获取到文档的*新版本。

图 24. 在刷新(flush)之后,段被全量提交,并且事务日志被清空

%title插图%num

flush API编辑

这个执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush 。 分片每30分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。请查看 translog 文档 来设置,它可以用来控制这些阈值:

flush API 可以被用来执行一个手工的刷新(flush):

POST /blogs/_flush 

POST /_flush?wait_for_ongoing
  刷新(flush) blogs 索引。
  刷新(flush)所有的索引并且并且等待所有刷新在返回前完成。

你很少需要自己手动执行 flush 操作;通常情况下,自动刷新就足够了。

这就是说,在重启节点或关闭索引之前执行 flush 有益于你的索引。当 Elasticsearch 尝试恢复或重新打开一个索引, 它需要重放 translog 中所有的操作,所以如果日志越短,恢复越快。

Translog 有多安全?

translog 的目的是保证操作不会丢失。这引出了这个问题: Translog 有多安全?

在文件被 fsync 到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被 fsync 刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。*终, 基本上,这意味着在整个请求被 fsync 到主分片和复制分片的translog之前,你的客户端不会得到一个 200 OK 响应。

在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。

但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次 fsync 。

这个行为可以通过设置 durability 参数为 async 来启用:

PUT /my_index/_settings
{
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s"
}

这个选项可以针对索引单独设置,并且可以动态进行修改。如果你决定使用异步 translog 的话,你需要 保证 在发生crash时,丢失掉 sync_interval 时间段的数据也无所谓。请在决定前知晓这个特性。

如果你不确定这个行为的后果,*好是使用默认的参数( "index.translog.durability": "request" )来避免数据丢失。