日期: 2021 年 5 月 24 日

国内中高级的Android技术人才仍然稀缺?

前言

看到一篇文章中提到“*近几年国内的初级Android程序员已经很多了,但是中高级的Android技术人才仍然稀缺“,这的确不假,从我在百度所进行的一些面试来看,找一个适合的高级Android工程师的确不容易,一般需要进行大量的面试才能挑选出一个比较满意的。为什么中高级Android程序员不多呢?这是一个问题,我不好回答,但是我想写一篇文章来描述下Android的学习路线,期望可以帮助更多的Android程序员提升自己。由于我也是从一个菜鸟过来的,所以我会结合我的个人经历以及我对Android学习过程的认识来写这篇文章,这会让这篇文章更加真实,而并非纸上谈兵。

我的工作经历

我也是从一个Android菜鸟过来的。其实这句话放在任何人的身上都是适用的,即大家都是一步步过来的,因此作为初学者也不必因为技术差而郁闷,同理,高手也不要看不起一些所谓的菜鸟,因为这不公平,技术只有在同等的工作年限上才具有一定的可比性,也许你眼中的菜鸟只是个工作半年的新手,而你已经工作5年,可想而知,这根本不具有可比性,搞不好别人5年后可以达到比你更高的技术高度。
我是硕士研究生毕业,我已经工作3年零3个月了,职位上从*开始的腾讯初级工程师变成了现在的百度Android资深工程师。*开始我并不是做Android的,先是做了半年的C++,接着做了3个月的Web前端,然后公司内部转岗做Android到至今,纯Android工作年限的话其实是2.5年。但是我认为我的Android工作经验(注:工作年限不等同于工作经验)不止2.5年,我投入大量的业余时间在Android开发上,并且我习惯去思考问题、总结问题并将其融会贯通,因此我的Android技术在短时间内有了很大的提升。
在Android学习过程中,初学者踩过的坑我也都踩过,我也深深地知道大家在学习过程中*棘手的问题是什么。举个例子,在3年前,我想在SlidingMenu中放入一个ListView,发现二者总是不能很好地一起工作,这肯定是由于滑动冲突的问题,我也知道,但是不知道怎么解决。我就去学校图书馆翻遍了所有的Android书籍,无果。大家肯定都知道原因,为什么我无法从书中查找到问题的答案,因为入门书不讲滑动冲突,所谓的高级编程书也不讲。还有一个问题,我曾经以为view.post(runnable)可以让runnable的run方法在线程中执行,所以我就在run方法里面去做耗时的操作,现在想想我当时是多菜啊,因此我曾经也是菜鸟。
直到若干年后的某一天,我终于琢磨透了滑动冲突的事,然后我就把解决问题的思想写在了CSDN博客上,但是好像看得人并不多,很多人并不能体会我的用心,后来我博客的访问量越来越大,这才慢慢地得到了一些人的关注。后来有一天我有了写书的契机,我想到了我*开始学习Android时所踩过的坑,想到滑动冲突对我的困扰,为了更好地传播我的技术经验,为了让更多的人少踩一些坑,为了让更多地人成为Android高级工程师,我毅然决定将Android开发中*重要的、*疑难的、*容易困扰大家的、成为高级工程师所必备的知识点和盘托出,这就是《Android开发艺术探索》存在的原因以及意义。书的反响怎么样呢?从目前读者的评价来看,内容基本无差评,我收到了很多读者的肯定以及感谢,这说明很多人能够理解我的用心。
说了那么多,言归正传,下面说下Android学习路线的话题,本文打算从4个阶段来对Android的学习过程做一个全面的分析,分别为Android初级、中级、高级以及资深工程师,具体请看下面的分析。同理,本篇学习路线仍然只针对Android应用开发,不针对Rom开发和逆向工程等。这里虚拟一位“小明”的角色,在这里小明就是Android初学者的代表。
初级工程师

小明之前完全没接触过Android开发,是个应届生,他的待遇是13k,然后小明以校招的身份进入了百度,然后小明需要怎么学习才能成为初级工程师呢?这个时候,小明对编程基础、数据结构、C语言都有一定基础,Java语法什么的也都掌握的比较好,Android才有java语言,无奈的是小明并不会搞Android。
小明首先需要购买一本Android入门的书籍,为了更快地学习Android,小明业余时间也都用来一边看书一边照着书中的例子敲代码,结果2周时间小明就把这本书学了一遍。看完这本书后,小明对Android的历史、结构、代码规范等都有了一个大概的了解,并且,小明已经可以写出一些简单的Activity了。这个时候在小明眼里,Android开发很简单很好玩,通过在xml中摆放一些按钮文本框什么的就可以做一些界面了。
小明开始跟着他的技术导师做需求,一些简单的小需求小明自然是不在话下了。突然有一天来了一个需求,该需求要求小明在Activity中为一个button加一个动画效果,小明慌了:“完全没接触过,书上也没有讲,怎么办呢?”小明冷静了下,打开了百度搜索,输入“Android 动画”,打开前几个链接,小明恍然大悟,照着网上的例子把需求给实现了。后来导师告诉他:“学好Android,官方文档是必须看的,既全面又权威”。然后小明如获至宝,花了一年时间把上面的guide和training都看了一遍,并且他还动手抄了几个小例子。
有一天,小明又需要做一个动画相关的需求,这可难不倒小明,它熟练地打开了www.baidu.com,输入“Android 动画”,突然他楞了一下:”总不能每次写动画都要百度一下吧!“,于是他在CSDN开了一个博客,把动画相关的知识点都写上去,为的是后面再写动画相关的代码就不用百度去搜了,事实如何呢?后面再写动画相关的代码,小明的确不用再去百度搜了,因为通过写一篇动画博客,他把动画相关的细节都已经记住了,这样他就可以不用再去参考任何文档了,后来小明还学会了把一些琐碎的不方便放在博客上的东西写到了印象笔记上面,什么时候忘了10秒钟以内都可以快速找回来,而不是花10分钟去再次搜索一遍。
这里总结一下,Android入门的时候,需要有一本入门书,好好学习书中的内容,同时花一年时间把Android官方文档中的training和guide看一遍,同时通过写博客和记笔记的方式来做总结,建议让自己的每篇博客都有价值些。通过一年时间的学习,相信每个人都可以达到中级工程师的水平。
技术要求:

  • 基本知识点
    比如四大组件如何使用、如何创建Service、如何进行布局、简单的自定义View、动画等常见技术
  • 书籍推荐
    《*行代码 Android》、《疯狂Android》
    中级工程师
    小明经过一年的努力学习终于成为Android中级工程师了,月薪变成了17k。到了中级工程师,已经可以在公司里干很多体力活了,但是一些很重要的任务小明还不能一个人承担起来,这个时候小明需要学习的内容就很多了,如下所示:
  • AIDL:熟悉AIDL,理解其工作原理,懂transact和onTransact的区别;
  • Binder:从Java层大概理解Binder的工作原理,懂Parcel对象的使用;
  • 多进程:熟练掌握多进程的运行机制,懂Messenger、Socket等;
  • 事件分发:弹性滑动、滑动冲突等;
  • 玩转View:View的绘制原理、各种自定义View;
  • 动画系列:熟悉View动画和属性动画的不同点,懂属性动画的工作原理;
  • 懂性能优化、熟悉mat等工具
  • 懂点常见的设计模式
    学习方法
    阅读进阶书籍,阅读Android源码,阅读官方文档并尝试自己写相关的技术文章,需要有一定技术深度和自我思考。在这个阶段的学习过程中,有2个点是比较困扰大家的,一个是阅读源码,另一个是自定义View以及滑动冲突。
    如何阅读源码呢?这是个头疼的问题,但是源码必须要读。阅读源码的时候不要深入代码细节不可自拔,要关注代码的流程并尽量挖掘出对应用层开发有用的结论。另外仔细阅读源码中对一个类或者方法的注释,在看不懂源码时,源码中的注释可以帮你更好地了解源码中的工作原理,这个过程虽然艰苦,但是别无他法。
    如何玩转自定义View呢?我的建议是不要通过学习自定义view而学习自定义view。为什么这么说呢?因为自定义view的种类太多了,各式各样的绚丽的自定义效果,如何学的玩呢!我们要透过现象看本质,更多地去关注自定义view所需的知识点,这里做如下总结:
  • 搞懂view的滑动原理
  • 搞懂如何实现弹性滑动
  • 搞懂view的滑动冲突
  • 搞懂view的measure、layout和draw
  • 然后再学习几个已有的自定义view的例子
  • *后就可以搞定自定义view了,所谓万变不离其宗
    大概再需要1-2年时间,即可达到高级工程师的技术水平。我个人认为通过《Android开发艺术探索》和《Android群英传》可以缩短这个过程为0.5-1年。注意,达到高级工程师的技术水平不代表就可以立刻成为高级工程师(受机遇、是否跳槽的影响),但是技术达到了,成为高级工程师只是很简单的事。
    技术要求:
  • 稍微深入的知识点
    AIDL、Messenger、Binder、多进程、动画、滑动冲突、自定义View、消息队列等
  • 书籍推荐
    《Android开发艺术探索》、《Android群英传》
    高级工程师
    小明成为了梦寐以求的高级工程师,月薪达到了20k,还拿到了一丢丢股票。这个时候小明的Android水平已经不错了,但是小明的目标是资深工程师,小明听说资深工程师月薪可以达到30k+。
    为了成为Android资深工程师,需要学习的东西就更多了,并且有些并不是那么具体了,如下所示:
  • 继续加深理解”稍微深入的知识点“中所定义的内容
  • 了解系统核心机制:
  1. 了解SystemServer的启动过程
  2. 了解主线程的消息循环模型
  3. 了解AMS和PMS的工作原理
  4. 能够回答问题”一个应用存在多少个Window?“
  5. 了解四大组件的大概工作流程
  • 基本知识点的细节
  1. Activity的启动模式以及异常情况下不同Activity的表现
  2. Service的onBind和onReBind的关联
  3. onServiceDisconnected(ComponentName className)和binderDied()的区别
  4. AsyncTask在不同版本上的表现细节
  5. 线程池的细节和参数配置
  6. 熟悉设计模式,有架构意识

web基础教程:随笔

web基础教程:随笔

一、用自己的语言描述get、post、Accept、Referer、User-Agent、host、cookie、X_Forwarded_for、Location各请求头的含义

1. GET

http请求方法,从浏览器获取一个资源
2. POST

提交数据、账号密码等,加密传输
3. Accept

支持的语言程序、接收的文件类型等等…
4. Referer

起过渡作用,从一个页面转到另一个页面
5. User-Agent

显示浏览器的指纹信息
6. host

主机
7. cookie

记录并保存你去过哪些地方,可以用于分析用户的喜好推荐广告
8. X_Forwarded_for

识别http代理、负载均衡方式连接到web服务器的客户端ip地址(可修改ip地址),
9. Location

用于重定向响应中的重定向目标
二、常用 http 支持的方法有那些,同时对Head、options、put、get、post用自己的语言进行描述

1. Head

检查服务器上的资源,判断页面服务是否存在
2. options

判断并显示浏览器所支持的方法

3. put

向服务器上传资源,开启这个服务容易被攻击
4. get

向浏览器获取数据
5. post

向浏览器提交数据,加密传输
三、cookie头里面的secure与HttpOnly项分别代表什么含义

1. secure

仅在https请求中提交cookie。
2. HttpOnly

不会被钓鱼网站盗走cookie,安全级别高,
四、写出安全渗透里面常用编码有那些

Unicode编码、HTML编码、Base64编码、十六进制编码
五、burp大概有那些功能模块

Proxy(代理):默认端口8080,开启代理可以截获并修改web应用的数据包
Spider(抓取):抓取web提交的数据资源
Scanner(扫描器):扫描web程序的漏洞
Intruder(入侵):漏洞利用,web程序模糊测试,暴力破解等
Repeater(中继器):重放模拟数据包的请求与响应的过程
Sequenecer:检查web程序会话令牌的随机性并执行各种测试
Decoder(解码);解码和编码
六、静态 动态语言区别

1. http

静态语言,不存在漏洞,访问速度快,服务端和客户端代码一致(如html)
2. php

动态语言,可连接数据库实时更新,服务端和客户端代码不一致(如: asp,php,aspx,jsp)
七、常见的脚本语言有那些

如PHP, VBScript和Perl ;
八、常见的数据库有那些

mysql 、SQL Server、Oracle、Sybase、DB2
九、常见的数据库与脚本语言搭配

asp+access
asp+mssql
php+mysql
aspx+mssql
aspx+oracle
jsp+oracle
jsp+mssql
十、系统、脚本语言、中间件如何组合

Windows2003/2008/2012+asp、aspx、php+iis6.0/7.0+7.5
Apache+Windows/Linux+PHP Windows/Linux+Tomcat+JSP
十一、渗透测试过程中如何查看对方操作系统是什么系统或版本

1、工具(RASS、天镜、NMAP、X-SCAN)

2、第三方平台(seo.chinaz.com)

3、通过ping观看TTL值

C:\Users\陈婷>ping 127.0.0.1

正在 Ping 127.0.0.1 具有 32 字节的数据:
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
4、网页文件大小写

windows不区分大小写
Linux区分大小写

Python中格式化字符串更酷的方式

Python中格式化字符串更酷的方式
在 Python 中,大家都习惯使用 %s 或 format 来格式化字符串,在 Python 3.6 中,有了一个新的选择 f-string。

使用对比
我们先来看下 Python 中已经存在的这几种格式化字符串的使用比较。

# %s
username = ‘tom’
action = ‘payment’
message = ‘User %s has logged in and did an action %s.’ % (username, action)
print(message)

# format
username = ‘tom’
action = ‘payment’
message = ‘User {} has logged in and did an action {}.’.format(username, action)
print(message)

# f-string
username = ‘tom’
action = ‘payment’
message = f’User {user} has logged in and did an action {action}.’
print(message)

f”{2 * 3}”
# 6

comedian = {‘name’: ‘Tom’, ‘age’: 20}
f”The comedian is {comedian[‘name’]}, aged {comedian[‘age’]}.”
# ‘The comedian is Tom, aged 20.’
相比于常见的字符串格式符 %s 或 format 方法,f-strings 直接在占位符中插入变量显得更加方便,也更好理解。

方便的转换器
f-string 是当前*佳的拼接字符串的形式,拥有更强大的功能,我们再来看一下 f-string 的结构。

f ‘ <text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> … ‘
其中 ‘!s’ 调用表达式上的 str(),’!r’ 调用表达式上的 repr(),’!a’ 调用表达式上的 ascii()。大家可以看看下面的例子。

class Person:

def __init__(self, name, nickname):
self.name = name
self.nickname = nickame

def __str__(self):
return self.name

def __repr__(self):
return self.nickname

person = Person(‘王大锤’, ‘Wang Gangdan’)
print(f'{person!s}’)
print(f'{person!r}’)
print(f'{person.name!a}’)
print(f'{person.nickname!a}’)
性能
f-string 除了提供强大的格式化功能之外,还是这三种格式化方式中性能*高的实现。

>>> import timeit
>>> timeit.timeit(“””name = “Eric”
… age = 74
… ‘%s is %s.’ % (name, age)”””, number = 10000)
0.003324444866599663
>>> timeit.timeit(“””name = “Eric”
… age = 74
… ‘{} is {}.’.format(name, age)”””, number = 10000)
0.004242089427570761
>>> timeit.timeit(“””name = “Eric”
… age = 74
… f'{name} is {age}.'”””, number = 10000)
0.0024820892040722242
坦白的说,f-string 就是字符串 format 方法一个语法糖,但它进一步简化了格式化字符串的操作并带来了性能上的提升。使用 Python 3.6+ 的同学,使用 f-string 来代替你的 format 函数,以获得更强大的功能和更高的性能。

一个小例子助你彻底理解协程

一个小例子助你彻底理解协程

一个小例子助你彻底理解协程
协程,可能是Python中*让初学者困惑的知识点之一,它也是Python中实现并发编程的一种重要方式。Python中可以使用多线程和多进程来实现并发,这两种方式相对来说是大家比较熟悉的。事实上,还有一种实现并发的方式叫做异步编程,而协程就是实现异步编程的必要方式。

所谓协程,可以简单的理解为多个相互协作的子程序。在同一个线程中,当一个子程序阻塞时,我们可以让程序马上从一个子程序切换到另一个子程序,从而避免CPU因程序阻塞而闲置,这样就可以提升CPU的利用率,相当于用一种协作的方式加速了程序的执行。所以,我们可以言简意赅的说:协程实现了协作式并发。

接下来用一个小例子帮助大家理解什么是协作式并发,先看看下面的代码。

import time

def display(num):
time.sleep(1)
print(num)

for num in range(10):
display(num)
上面这段代码相信大家很容看懂,程序会输出0到9的数字,每隔1秒中输出一个数字,因此整个程序的执行需要大约10秒时间。值得注意的是,因为没有使用多线程或多进程,程序中只有一个执行单元,而time.sleep(1)的休眠操作会让整个线程停滞1秒钟,对于上面的代码来说,在这段时间里面CPU是完全闲置的没有做什么事情。

我们再来看看使用协程会发生什么事情。从Python 3.5开始,使用协程实现协作式编发有了更为便捷的语法,我们可以使用async来定义异步函数,可以使用await让一个阻塞的子程序将CPU让给与它协作的子程序。在Python 3.7中,asyanc和await成为了正式的关键字,让开发者有一种喜大普奔的感觉。我们先看看如何定义一个异步函数。

import asyncio

async def display(num):
await asyncio.sleep(1)
print(num)
接下来敲黑板说重点。异步函数不同于普通函数,调用普通函数会得到返回值,而调用异步函数会得到一个协程对象。我们需要将协程对象放到一个事件循环中才能达到与其他协程对象协作的效果,因为事件循环会负责处理子程序切换的操作,简单的说就是让阻塞的子程序让出CPU给可以执行的子程序。

我们先通过下面的列表生成式来代码10个协程对象,跟刚才在循环中调用display函数的道理一致。

coroutines = [display(num) for num in range(10)]
通过下面的代码可以获取事件循环并将协程对象放入事件循环中。

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coroutines))
loop.close()
执行上面的代码会发现,10个分别会阻塞1秒钟的协程总共只阻塞了约1秒种的时间,这就说明协程对象一旦阻塞会将CPU让出而不是让CPU处于闲置状态,这样就大大的提升了CPU的利用率。而且我们还会注意到,0到9的数字并不是按照我们创建协程对象的顺序打印出来的,这正是我们想要的结果啊;另外,多次执行该程序会发现每次输出的结果都不太一样,这正是并发程序本身执行顺序不确定性造成的结果。

上面的例子来自于著名的“花书”(《Python高级并发编程》),为了让大家对协程的体会更加深刻,对原书的代码做了小的改动,这个例子虽然简单,但是它已经让你体会到了协作式并发的魅力。在商业项目中,如果需要使用协作式并发,还可以将系统默认的事件循环替换为uvloop提供的事件循环,这样会获得更好的性能,因为uvloop是基于著名的跨平台异步I/O库libuv实现的。另外,如果要做基于HTTP的网络编程,三方库aiohttp是不错的选择,它基于asyncio实现了异步的HTTP服务器和客户端。

使用协程快速获得一个代理池

使用协程快速获得一个代理池

使用协程快速获得一个代理池
前言
在执行 I/O 密集型任务的时候,程序会因为等待 I/O 而阻塞。比如我们使用 requests 库来进行网络爬虫请求的话,如果网站响应速度过慢,程序会一直等待网站响应,*终导致其爬取效率十分低下。本文以爬取 IP 代理池为例,演示 Python 中如何利用异步协程来加速网络爬虫。

协程
协程(Coroutine),又称微线程,纤程,协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存,在调度回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合。

协程本质上是个单进程,协程相对于多进程来说,无需进程间上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。

我们可以使用协程来实现异步操作,比如在网络爬虫场景下,在发出一个请求之后,需要等待一定的时间才能得到响应。其实在这个等待过程中,程序可以干许多其他的事情,等到响应返回之后再切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。

Python 中的协程
从 Python 3.4 开始,Python 中加入了协程的概念,这个版本的协程是通过生成器对象来实现的,在 Python 3.5 中增加了 asyncio 库和 async、await 关键字,使得协程的实现更加方便。

asyncio 库
首先我们先来看一个不使用协程的程序,代码如下:

import time


def job(t):
print(‘Start job {}’.format(t))
time.sleep(t) # 等待 t 秒
print(‘Job {0} takes {0}s’.format(t))


def main():
[job(t) for t in range(1, 3)]


start = time.time()
main()
print(“total time: {}”.format(time.time() – start))
运行结果:

Start job 1
Job 1 takes 1s
Start job 2
Job 2 takes 2s
total time: 3.001577138900757
从运行结果可以看出,我们的 job 是按顺序执行的。必须执行完 job 1 才能开始执行 job 2, job 1 需要 1 秒的执行时间,job 2 需要 2 秒的执行时间,所以总时间是 3 秒多。

如果我们使用协程的方式,job 1 在等待 time.sleep(t) 执行结束的时候(可以看做是等待一个网页的下载成功),是可以切换到 job 2 执行的。

我们再来看一下使用协程改造后的代码:

import time
import asyncio


async def job(t): # 使用 async 关键字将一个函数定义为协程
print(‘Start job {}’.format(t))
await asyncio.sleep(t) # 等待 t 秒, 期间切换执行其他任务
print(‘Job {0} takes {0}s’.format(t))


async def main(loop): # 使用 async 关键字将一个函数定义为协程
tasks = [loop.create_task(job(t)) for t in range(1, 3)] # 创建任务, 不立即执行
await asyncio.wait(tasks) # 执行并等待所有任务完成


start = time.time()
loop = asyncio.get_event_loop() # 建立 loop
loop.run_until_complete(main(loop)) # 执行 loop
loop.close() # 关闭 loop
print(“total time: {}”.format(time.time() – start))
运行结果:

Start job 1
Start job 2
Job 1 takes 1s
Job 2 takes 2s
total time: 2.0033459663391113
从运行结果可以看出,我们没有等待 job 1 执行结束再开始执行 job 2,而是 job 1 触发 await 的时候切换到了 job 2 。 这时 job 1 和 job 2 同时在执行 await asyncio.sleep(t),所以*终程序的执行时间取决于执行时间*长的那个 job,也就是 job 2 的执行时间:2 秒。

aiohttp 库
在对 asyncio 库做了简单了解之后,我们来看一下如何通过协程来改造我们的爬虫程序。

安装 aiohttp 库:

pip install aiohttp
我们先来看一下使用 reqeusts 库实现一个网页的爬取:

import time

import requests

def fetch(url):
r = requests.get(url)
return r.url


def main():
results = [fetch(‘http://www.baidu.com’) for _ in range(2)]
print(results)


start = time.time()
main()
print(“total time: {}”.format(time.time() – start))
运行结果:

[‘http://www.baidu.com/’, ‘http://www.baidu.com/’]
total time: 1.5445010662078857
使用 requests 库,访问两次 http://www.baidu.com,共耗时 1.5 秒

我们用 aiohttp 库来改造上面的代码:

import time
import asyncio

import aiohttp


async def fetch(session, url):
response = await session.get(url) # await 等待网络 IO 并切换协程
return str(response.url)


async def main(loop):
async with aiohttp.ClientSession() as session:
tasks = [
loop.create_task(fetch(session, ‘http://www.baidu.com’))
for _ in range(2)
]
done, pending = await asyncio.wait(tasks) # 执行并等待所有任务完成
results = [r.result() for r in done] # 获取所有返回结果
print(results)


start = time.time()
loop = asyncio.get_event_loop() # 建立 事件循环
loop.run_until_complete(main(loop)) # 在 事件循环 中执行协程
loop.close() # 关闭 事件循环
print(“total time: {}”.format(time.time() – start))
运行结果:

[‘http://www.baidu.com’, ‘http://www.baidu.com’]
total time: 0.10848307609558105
使用 aiohttp 的代码执行时间较使用 reqeusts 的代码有大幅的提升。

上例中,我们使用官方推荐的方式创建 session,并通过 session 执行 get 操作。aiohttp 官方建议一个 application 中共享使用一个 session,不要为每个请求都创建 session。

使用 asyncio 和 aiohttp 快速获得一个代理池
通过爬虫解析免费的代理发布网站页面,来生成代理池。

import os
import re
import time
import asyncio

import aiohttp

HEADERS = {
‘User-Agent’:
‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15’
}

OUTPUT_FILE = ‘proxies.txt’ # 代理池输出文件
SITES = [‘http://www.live-socks.net’, ‘http://www.proxyserverlist24.top’] # 代理发布网站
CHECK_URL = ‘http://www.baidu.com’
LOCAL_PROXY = None # 在本地发起请求时的代理


# http get 协程
async def fetch(session, url, proxy=None):
proxy_headers = HEADERS if proxy else None
try:
async with session.get(
url, headers=HEADERS, proxy=proxy,
proxy_headers=proxy_headers,
timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 200:
return await response.text()
else:
return ”
except:
return ”


# 从代理发布网站获取代理发布页面链接
async def get_page_links(loop, session):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in SITES] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 解析出 html 页面中的代理发布链接
def parse(html):
return re.findall(r'<h3[\s\S]*?<a.*?(http.*?\.html).*?</a>’, html)

results = map(parse, htmls) # 逐个解析 html 页面

return [y for x in results for y in x]


# 从代理发布页面获取代理 IP
async def get_proxies(loop, session, page_links):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in page_links] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 解析出 html 页面中的代理 IP
def parse(html):
return re.findall(r’\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}’, html)

results = map(parse, htmls) # 逐个解析 html 页面

return list(set([y for x in results for y in x]))


# 验证代理 IP
async def check_proxy(session, proxy):
html = await fetch(session, CHECK_URL, proxy=proxy)

# 如果返回通过代理 IP 访问的页面,则说明代理 IP 有效
return proxy if html else ”


# 通过协程批量验证代理 IP,每次同时发起 200 个验证请求
async def check_proxies(loop, session, proxies):
checked_proxies = []
for i in range(0, len(proxies), 200):
_proxies = [proxy.strip() if proxy.strip().startswith(‘http://’)
else ‘http://’ + proxy.strip() for proxy in proxies[i:i + 200]]
tasks = [loop.create_task(check_proxy(session, proxy))
for proxy in _proxies]
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
checked = [f.result() for f in done] # 获取所有返回结果
checked_proxies += [p for p in checked if p] # 获取不为空的返回值,即验证成功的代理 IP

return checked_proxies


# 将代理 IP 逐行保存到文件
def save_proxies(proxies):
# 创建新文件,如果文件已存在,则清空文件内容
with open(OUTPUT_FILE, ‘w’) as f:
f.write(”)

# 通过追加写模式,逐行写入文件
with open(OUTPUT_FILE, ‘a’) as f:
for proxy in proxies:
f.write(proxy + ‘\n’)


async def main(loop):
async with aiohttp.ClientSession() as session:
page_links = await get_page_links(loop, session) # 获得代理发布页面链接
# 从代理发布页面获得代理 IP
proxies = await get_proxies(loop, session, page_links)
print(‘total proxy: {}’.format(len(proxies))) # 解析出的代理 IP 总量
proxies = await check_proxies(loop, session, proxies) # 验证代理 IP

print(‘total checked proxy: {}’.format(len(proxies))) # 验证后的代理 IP 总量
save_proxies(proxies) # 保存代理 IP 到文件


start = time.time()
loop = asyncio.get_event_loop() # 建立 事件循环
loop.run_until_complete(main(loop)) # 在 事件循环 中执行协程
loop.close() # 关闭 事件循环
total_time = time.time() – start
print(f’total time: {total_time}’)
运行结果:

total proxy: 15675
total checked proxy: 4503
total time: 487.2807550430298
更加高效的爬虫
在爬虫程序中,通常有网络请求任务、页面解析任务、数据清洗任务和数据入库任务。

网络请求任务、数据入库任务属于 IO 密集型任务,在 Python 中通常使用多线程模型来提高这类任务的性能,现在还可以通过 aiohttp,Motor(MongoDB 的异步 Python 驱动)等异步框架将性能进一步提升。

页面解析任务、数据清洗任务这类 CPU 密集型的任务我们该如何来提高性能?在 Python 中针对 CPU 密集型任务可以通过 multiprocessing 模块来提升性能,通过 multiprocessing 模块可以使程序运行在多核 CPU 中,增加 CPU 的利用率以提升计算性能。

给代理池爬虫示例增加多核计算支持:

import os
import re
import time
import asyncio
from multiprocessing import Pool

import aiohttp

HEADERS = {
‘User-Agent’:
‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15’
}

OUTPUT_FILE = ‘proxies.txt’ # 代理池输出文件
SITES = [‘http://www.live-socks.net’, ‘http://www.proxyserverlist24.top’] # 代理发布网站
CHECK_URL = ‘http://www.baidu.com’
LOCAL_PROXY = ‘http://127.0.0.1:1087′ # •在本地发起请求时的代理


# http get 协程
async def fetch(session, url, proxy=None):
proxy_headers = HEADERS if proxy else None
try:
async with session.get(
url, headers=HEADERS, proxy=proxy,
proxy_headers=proxy_headers,
timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 200:
return await response.text()
else:
return ”
except:
return ”

# 解析出 html 页面中的代理发布链接


def parse_page_link(html):
return re.findall(r'<h3[\s\S]*?<a.*?(http.*?\.html).*?</a>’, html)

# 从代理发布网站获取代理发布页面链接


async def get_page_links(loop, session):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in SITES] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 利用多核 CPU 的计算能力提升页面解析性能
with Pool(processes=os.cpu_count() * 2) as pool:
results = pool.map(parse_page_link, htmls)

return [y for x in results for y in x]

# 解析出 html 页面中的代理 IP


def parse_proxy(html):
return re.findall(r’\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}’, html)

# 从代理发布页面获取代理 IP


async def get_proxies(loop, session, page_links):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in page_links] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 利用多核 CPU 的计算能力提升页面解析性能
with Pool(processes=os.cpu_count() * 2) as pool:
results = pool.map(parse_proxy, htmls)

return list(set([y for x in results for y in x]))


# 验证代理 IP
async def check_proxy(session, proxy):
html = await fetch(session, CHECK_URL, proxy=proxy)

# 如果返回通过代理 IP 访问的页面,则说明代理 IP 有效
return proxy if html else ”


# 通过协程批量验证代理 IP,每次同时发起 200 个验证请求
async def check_proxies(loop, session, proxies):
checked_proxies = []
for i in range(0, len(proxies), 200):
_proxies = [proxy.strip() if proxy.strip().startswith(‘http://’)
else ‘http://’ + proxy.strip() for proxy in proxies[i:i + 200]]
tasks = [loop.create_task(check_proxy(session, proxy))
for proxy in _proxies]
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
checked = [f.result() for f in done] # 获取所有返回结果
checked_proxies += [p for p in checked if p] # 获取不为空的返回值,即验证成功的代理 IP

return checked_proxies


# 将代理 IP 逐行保存到文件
def save_proxies(proxies):
# 创建新文件,如果文件已存在,则清空文件内容
with open(OUTPUT_FILE, ‘w’) as f:
f.write(”)

# 通过追加写模式,逐行写入文件
with open(OUTPUT_FILE, ‘a’) as f:
for proxy in proxies:
f.write(proxy + ‘\n’)


async def main(loop):
async with aiohttp.ClientSession() as session:
page_links = await get_page_links(loop, session) # 获得代理发布页面链接
# 从代理发布页面获得代理 IP
proxies = await get_proxies(loop, session, page_links)
print(‘total proxy: {}’.format(len(proxies))) # 解析出的代理 IP 总量
proxies = await check_proxies(loop, session, proxies) # 验证代理 IP

print(‘total checked proxy: {}’.format(len(proxies))) # 验证后的代理 IP 总量
save_proxies(proxies) # 保存代理 IP 到文件


start = time.time()
loop = asyncio.get_event_loop() # 建立 事件循环
loop.run_until_complete(main(loop)) # 在 事件循环 中执行协程
loop.close() # 关闭 事件循环
total_time = time.time() – start
print(f’total time: {total_time}’)
进程间的调度及上下文切换是非常消耗资源的。上面例子中解析任务比较简单,解析量也非常少,增加多核计算支持后,性能几乎没有提升还有可能降低。在实际爬虫项目中需要根据实际情况来衡量和选择。

用 pprint 代替 print 更友好的打印调试信息

用 pprint 代替 print 更友好的打印调试信息

用 pprint 代替 print 更友好的打印调试信息
pprint 是 “pretty printer” 的简写,“pretty” 的含义是 “漂亮的、美观的”,因此 pprint 的含义便是:漂亮的打印。

这是个相当简单却有用的模块,主要用于打印复杂的数据结构对象,例如多层嵌套的列表、元组和字典等。

先看看 print() 打印的一个例子:

mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]

print(mylist)

[‘Beautiful is better than ugly.’, ‘Explicit is better than implicit.’, ‘Simple is better than complex.’, ‘Complex is better than complicated.’]
这是一个简单的例子,全部打印在一行里。如果对象中的元素是多层嵌套的内容(例如复杂的 字典 数据),再打印出那肯定是一团糟的,不好阅读。

使用 pprint 模块的 pprint() 替代 print(),可以解决如下痛点:

设置合适的行宽度,作适当的换行
设置打印的缩进、层级,进行格式化打印
判断对象中是否有无限循环,并优化打印内容
基本使用

pprint(object, stream=None, indent=1, width=80, depth=None, *,compact=False)
默认的行宽度参数为 80,当打印的字符小于 80 时,pprint() 基本上等同于内置函数 print(),当字符超出时,它会作美化,进行格式化输出。

import pprint

mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]

pprint.pprint(mylist)

# 超出80字符,打印的元素是换行的
[‘Beautiful is better than ugly.’,
‘Explicit is better than implicit.’,
‘Simple is better than complex.’,
‘Complex is better than complicated.’]
设置缩进

pprint.pprint(mylist, indent=4)

[ ‘Beautiful is better than ugly.’,
‘Explicit is better than implicit.’,
‘Simple is better than complex.’,
‘Complex is better than complicated.’]
设置打印行宽

mydict = {‘students’: [{‘name’:’Tom’, ‘age’: 18},{‘name’:’Jerry’, ‘age’: 19}]}

pprint.pprint(mydict)

# 正常打印
{‘students’: [{‘age’: 18, ‘name’: ‘Tom’}, {‘age’: 19, ‘name’: ‘Jerry’}]}

pprint.pprint(mydict, width=20)

# 行宽为 20
{‘students’: [{‘age’: 18,
‘name’: ‘Tom’},
{‘age’: 19,
‘name’: ‘Jerry’}]}

pprint.pprint(mydict, width=70)

# 行宽为 70
{‘students’: [{‘age’: 18, ‘name’: ‘Tom’},
{‘age’: 19, ‘name’: ‘Jerry’}]}
设置打印层级

newlist = [1, [2, [3, [4, [5]]]]]

pprint.pprint(newlist, depth=3)

# 超出的层级会用 … 表示
[1, [2, [3, […]]]]
用 pprint 替换 print
import pprint
print = pprint.pprint

mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]

print(mylist)

[‘Beautiful is better than ugly.’,
‘Explicit is better than implicit.’,
‘Simple is better than complex.’,
‘Complex is better than complicated.’]

zookeeper的Leader选举机制详解

分布式开发必须知道的Zookeeper知识及其的Leader选举机制(ZAB原子广播协议)

ZooKeeper是Hadoop下的一个子项目,它是一个针对大型分布式系统的可靠协调系统,提供的功能包括:配置维护、名字服务、分布式同步、组服务等; 它的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

ZooKeeper系统架构

下图就是Zookeeper的架构图:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

从上面的架构图中,我们需要了解的主要的信息有:

①ZooKeeper分为服务器端(Server)和客户端(Client),客户端可以连接到整个ZooKeeper服务的任意服务器上(Leader除外)。

②ZooKeeper 启动时,将从实例中选举一个Leader,Leader 负责处理数据更新等操作,一个更新操作成功的标志是当且仅当大多数Server在内存中成功修改数据(Quorom机制)。每个Server 在内存中存储了一份数据。

③Zookeeper是可以集群复制的,集群间通过Zab协议(Zookeeper Atomic Broadcast)来保持数据的一致性;

④Zab协议包含两个阶段:Leader Election阶段和Atomic Brodcast阶段。群中将选举出一个Leader,其他的机器则称为Follower,所有的写操作都被传送给Leader,并通过Brodcast将所有的更新告诉给Follower。 当Leader被选举出来,且大多数服务器完成了和leader的状态同步后,Leadder Election 的过程就结束了,就将会进入到Atomic Brodcast的过程。Atomic Brodcast同步Leader和Follower之间的信息,保证Leader和Follower具有形同的系统状态。


Quorom机制简介

在分布式系统中,冗余数据是保证可靠性的手段,因此冗余数据的一致性维护就非常重要。一般而言,一个写操作必须要对所有的冗余数据都更新完成了,才能称为成功结束。比如一份数据在5台设备上有冗余,因为不知道读数据会落在哪一台设备上,那么一次写操作,必须5台设备都更新完成,写操作才能返回。

对于写操作比较频繁的系统,这个操作的瓶颈非常大。Quorum算法可以让写操作只要写完3台就返回。剩下的由系统内部缓慢同步完成。而读操作,则需要也至少读3台,才能保证至少可以读到一个*新的数据。


Zookeeper中的四种角色

①Leader:领导者,负责进行投票的发起和决议,更新系统状态。

②Learner:学习者

③Follower(Learner的子类):跟随者,用于接受客户端请求并向客户端返回结结果,在选主过程中参与投票,Follower可以接收Client请求,如果是写请求将转发给Leader来更新系统状态。

④Observer:观察者,可以接收客户端连接,将写请求转发给Leader节点,但是不参与投票过程,只是同步Leader状态,因为Follower增多会导致投票阶段延迟增大,影响性能。Observer的目的是为了扩展系统,提高读取数据。


为什么Zookeeper中的Server数目一般为基数?

我们知道在Zookeeper中 Leader 选举算法采用了Quorom算法。该算法的核心思想是当多数Server写成功,则任务数据写成功。假设有3个Server,则*多允许一个Server挂掉;如果有4个Server,则同样*多允许一个Server挂掉。既然3个或者4个Server,同样*多允许1个Server挂掉,那么它们的可靠性是一样的,所以选择奇数个ZooKeeper Server即可,这里选择3个Server。


Zookeeper用于Leader选举的算法

①基于UDP的LeaderElection

②基于UDP的FastLeaderElection

③基于UDP和认证的FastLeaderElection

④基于TCP的FastLeaderElection(默认值)


FastLeaderElection机制

接下来要说的就是Zookeeper的Leader选举机制核心算法FastLeaderElection类。FastLeaderElection实现了Election接口,其需要实现接口中定义的lookForLeader(核心的选举算法入口)方法和shutdown方法FastLeaderElection选举算法是标准的Fast Paxos算法实现,可解决LeaderElection选举算法收敛速度慢的问题。

术语介绍

sid(myid)

每个Zookeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个Zookeeper集群唯一的ID(整数)。例如某Zookeeper集群包含三台服务器,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其ID与hostname必须一一对应,如下所示。在该配置文件中,server.后面的数据即为myid(Leader选举时用的sid或者leader)。

server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888

zxid

类似于RDBMS中的事务ID,用于标识一次更新操作的Proposal ID。为了保证顺序性,该zkid必须单调递增。因此Zookeeper使用一个64位的数来表示,高32位是Leader的epoch,从1开始,每次选出新的Leader,epoch加一。低32位为该epoch内的序号,每次epoch变化,都将低32位的序号重置。这样保证了zxid的全局递增性。


Zookeeper节点的四种状态

截图为Zookeeper定义的四种服务器节点状态:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • LOOKING: 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
  • FOLLOWING: 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。
  • LEADING: 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳。
  • OBSERVING: 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。

FastLeaderElection内部类

FastLeaderElection的内部类的情况如下图:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • Notification:表示收到的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的id、zxid、选举周期等信息。
  • ToSend:表示发送给其他服务器的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的id、zxid、选举周期等信息。
  • Messenger:包含了WorkerReceiverWorkerSender两个内部类。WorkerReceiver实现了Runnable接口,是选票接收器。WorkerSender也实现了Runnable接口,为选票发送器

Notification(收到的投票信息)

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • leader:被推选的leader的id。
  • zxid:被推选的leader的事务id。
  • electionEpoch:推选者的选举周期。
  • state:推选者的状态。
  • sid:推选者的id。
  • peerEpoch:被推选者的选举周期。

ToSend(发送的投票信息)

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • leader:被推选的leader的id。
  • zxid:被推选的leader的事务id。
  • electionEpoch:推选者的选举周期。
  • state:推选者的状态。
  • sid:推选者的id。
  • peerEpoch:被推选者的选举周期。

WorkerSender(选票发送器)

WorkerSender也实现了Runnable接口,为选票发送器,其会不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中。

  • 获取选票

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • 发送选票

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 


WorkerReceiver(选票接收器)

WorkerReceiver实现了Runnable接口,是选票接收器。其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息中。先会从QuorumCnxManager中的pollRecvQueue队列中取出其他服务器发来的选举消息,消息封装在Message数据结构中。然后判断消息中的服务器id是否包含在可以投票的服务器集合中,若不是,则会将本服务器的内部投票发送给该服务器,其流程如下:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

若包含该服务器,则根据消息(Message)解析出投票服务器的投票信息并将其封装为Notification,然后判断当前服务器是否为LOOKING,若为LOOKING,则直接将Notification放入FastLeaderElection的recvqueue。然后判断投票服务器是否为LOOKING状态,并且其选举周期小于当前服务器的逻辑时钟,则将本(当前)服务器的内部投票发送给该服务器,否则,直接忽略掉该投票。其流程如下:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

若本服务器的状态不为LOOKING,则会根据投票服务器中解析的version信息来构造ToSend消息,放入sendqueue,等待发送,起流程如下:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 


核心函数分析

sendNotifications函数

其会遍历所有的参与者投票集合,然后将自己的选票信息发送至上述所有的投票者集合,其并非同步发送,而是将ToSend消息放置于sendqueue中,之后由WorkerSender进行发送。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

totalOrderPredicate函数

该函数将接收的投票与自身投票进行PK,查看是否消息中包含的服务器id是否更优,其按照epoch、zxid、id的优先级进行PK。

  • 判断消息里的epoch是不是比当前的大,如果大则消息中id对应的服务器就是leader。
  • 如果epoch相等则判断zxid,如果消息里的zxid大,则消息中id对应的服务器就是leader。
  • 如果前面两个都相等那就比较服务器id,如果大,则其就是leader。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

termPredicate函数

该函数用于判断Leader选举是否结束,即是否有一半以上的服务器选出了相同的Leader,其过程是将收到的选票与当前选票进行对比,选票相同的放入同一个集合,之后判断选票相同的集合是否超过了半数。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

checkLeader函数

该函数检查是否已经完成了Leader的选举,此时Leader的状态应该是LEADING状态。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

lookForLeader函数

该函数就是leader选举的核心方法,代码行数有点多,这里仅分析其中的一部分。

  • 更新逻辑时钟、更新选票、发送选票

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • 获取投票数据、连接服务器

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • 选举Leader

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • LEADING状态处理

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

以上就是关于zookeeper的所有基本知识与Leader选举机制的讲解。

Mysql中SQL语句不使用索引的情况

MySQL查询不使用索引汇总

众所周知,增加索引是提高查询速度的有效途径,但是很多时候,即使增加了索引,查询仍然不使用索引,这种情况严重影响性能,这里就简单总结几条MySQL不使用索引的情况

如果MySQL估计使用索引比全表扫描更慢,则不使用索引。例如,如果列key均匀分布在1和100之间,下面的查询使用索引就不是很好:select * from table_name where key>1 and key<90;

如果使用MEMORY/HEAP表,并且where条件中不使用“=”进行索引列,那么不会用到索引,head表只有在“=”的条件下才会使用索引

用or分隔开的条件,如果or前的条件中的列有索引,而后面的列没有索引,那么涉及到的索引都不会被用到,例如:select * from table_name where key1='a' or key2='b';如果在key1上有索引而在key2上没有索引,则该查询也不会走索引

复合索引,如果索引列不是复合索引的*部分,则不使用索引(即不符合*左前缀),例如,复合索引为(key1,key2),则查询select * from table_name where key2='b';将不会使用索引

如果like是以‘%’开始的,则该列上的索引不会被使用。例如select * from table_name where key1 like '%a';该查询即使key1上存在索引,也不会被使用

如果列为字符串,则where条件中必须将字符常量值加引号,否则即使该列上存在索引,也不会被使用。例如,select * from table_name where key1=1;如果key1列保存的是字符串,即使key1上有索引,也不会被使用。

从上面可以看出,即使我们建立了索引,也不一定会被使用,那么我们如何知道我们索引的使用情况呢??在MySQL中,有Handler_read_keyHandler_read_rnd_key两个变量,如果Handler_read_key值很高而Handler_read_rnd_key的值很低,则表明索引经常不被使用,应该重新考虑建立索引。可以通过:show status like 'Handler_read%'来查看着连个参数的值。

关于如何正确创建Mysql的索引,请参考怎样正确创建MySQL索引的方法详解;众所周知,数据表索引可以提高数据的检索效率,也可以降低数据库的IO成本,并且索引还可以降低数据库的排序成本;但索引并不是时时都会生效的,比如以下几种情况,将导致索引失效:

1.如果条件中有or,即使其中有条件带索引也不会使用索引(这也是为什么SQL语句中尽量少用or的原因)

 

%title插图%num

注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引。

2.对于多列索引,不是使用的*部分,则不会使用索引。

3.like查询是以%开头时不会使用索引。

%title插图%num

 

4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。

%title插图%num

 

5.如果 mysql 估计使用全表扫描要比使用索引快,则不使用索引。

6.在索引列上做运算,也会导致不使用索引。

select * from a where b+1 >10;

此外,查看索引的使用情况

?

1 show status like 'Handler_read%';

大家可以注意:

handler_read_key:这个值越高越好,越高表示使用索引查询到的次数。

handler_read_rnd_next:这个值越高,说明查询越低效。

iOS app生命周期方法执行探究

使用一个新创建的没有后台运行的app测试:
[LogInfo] 2018-11-06 10:49:25.444
File:AppDelegate Line:30
Function:-[AppDelegate applicationWillResignActive:]
applicationWillResignActive
[LogInfo] 2018-11-06 10:49:26.030
File:AppDelegate Line:38
Function:-[AppDelegate applicationDidEnterBackground:]
applicationDidEnterBackground
[LogInfo] 2018-11-06 10:50:27.345
File:AppDelegate Line:22
Function:-[AppDelegate application:didFinishLaunchingWithOptions:]
didFinishLaunchingWithOptions
[LogInfo] 2018-11-06 10:50:27.588
File:AppDelegate Line:50
Function:-[AppDelegate applicationDidBecomeActive:]
applicationDidBecomeActive
[LogInfo] 2018-11-06 10:51:08.119
File:AppDelegate Line:30
Function:-[AppDelegate applicationWillResignActive:]
applicationWillResignActive
[LogInfo] 2018-11-06 10:51:09.153
File:AppDelegate Line:38
Function:-[AppDelegate applicationDidEnterBackground:]
applicationDidEnterBackground
[LogInfo] 2018-11-06 10:51:09.160
File:AppDelegate Line:55
Function:-[AppDelegate applicationWillTerminate:]
applicationWillTerminate

测试步骤:

切后台,杀应用
应用前台杀应用
可以看到步骤2调用了applicationWillTerminate,步骤1则不会。

但我们在一个支持后台运行的应用中进行同样的测试则发现,applicationWillTerminate总是会被调用;当切后台的时候applicationDidEnterBackground也会正常触发。

从这样的结果来看,苹果的注释有些让人难以理解:

(void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

(void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

很明显并非只有我们关心这一点:
https://stackoverflow.com/questions/29416375/when-will-applicationwillterminate-be-called
很值得一看。

总结:applicationWillTerminate不总是被执行,如果要保证自己的任务,需要使用beginBackgroundTaskWithExpirationHandler进行相关处理。而其具体的行为需要看你的应用是否支持后台运行(*好用实际应用的真机测试结果来支持你的观点,然后做好逻辑处理!)。

Java并发编程之锁机制之LockSupport工具

LockSupport类

了解线程的阻塞和唤醒,我们需要查看LockSupport类。具体代码如下:

  1. public class LockSupport {
  2. private LockSupport() {} // Cannot be instantiated.
  3. private static void setBlocker(Thread t, Object arg) {
  4. U.putObject(t, PARKBLOCKER, arg);
  5. }
  6. public static void unpark(Thread thread) {
  7. if (thread != null)
  8. U.unpark(thread);
  9. }
  10. public static void park(Object blocker) {
  11. Thread t = Thread.currentThread();
  12. setBlocker(t, blocker);
  13. U.park(false, 0L);
  14. setBlocker(t, null);
  15. }
  16. public static void parkNanos(Object blocker, long nanos) {
  17. if (nanos > 0) {
  18. Thread t = Thread.currentThread();
  19. setBlocker(t, blocker);
  20. U.park(false, nanos);
  21. setBlocker(t, null);
  22. }
  23. }
  24. public static void parkUntil(Object blocker, long deadline) {
  25. Thread t = Thread.currentThread();
  26. setBlocker(t, blocker);
  27. U.park(true, deadline);
  28. setBlocker(t, null);
  29. }
  30. public static Object getBlocker(Thread t) {
  31. if (t == null)
  32. throw new NullPointerException();
  33. return U.getObjectVolatile(t, PARKBLOCKER);
  34. }
  35. public static void park() {
  36. U.park(false, 0L);
  37. }
  38. public static void parkNanos(long nanos) {
  39. if (nanos > 0)
  40. U.park(false, nanos);
  41. }
  42. public static void parkUntil(long deadline) {
  43. U.park(true, deadline);
  44. }
  45. //省略部分代码
  46. private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
  47. private static final long PARKBLOCKER;
  48. private static final long SECONDARY;
  49. static {
  50. try {
  51. PARKBLOCKER = U.objectFieldOffset
  52. (Thread.class.getDeclaredField(“parkBlocker”));
  53. SECONDARY = U.objectFieldOffset
  54. (Thread.class.getDeclaredField(“threadLocalRandomSecondarySeed”));
  55. } catch (ReflectiveOperationException e) {
  56. throw new Error(e);
  57. }
  58. }
  59. }
  60. 复制代码

从上面的代码中,我们可以知道LockSupport中的对外提供的方法都是静态方法。这些方法提供了*基本的线程阻塞和唤醒功能,在LockSupport类中定义了一组以park开头的方法用来阻塞当前线程。以及unPark(Thread thread)方法来唤醒一个被阻塞的线程。关于park开头的方法具体描述如下表所示:

 

park.png

 

 

其中park(Object blocker)parkNanos(Object blocker, long nanos)parkUntil(Object blocker, long deadline)三个方法是Java 6中新增加的方法。其中参数blocker是用来标识当前线程等待的对象(下文简称为阻塞对象),该对象主要用于问题排查和系统监控

由于在Java 5之前,当线程阻塞时(使用synchronized关键字)在一个对象上时,通过线程dump能够查看到该线程的阻塞对象。方便问题定位,而Java 5退出的Lock等并发工具却遗漏了这一点,致使在线程dump时无法提供阻塞对象的信息。因此,在Java 6中,LockSupport新增了含有阻塞对象的park方法。用以替代原有的park方法。

LockSupport中的blocker

可能有很多读者对Blocker的原理有点好奇,既然线程都被阻塞了,是通过什么办法将阻塞对象设置到线程中去的呢? 不急不急,我们继续查看含有阻塞对象(Object blocker)的park方法。 我们发现内部都调用了setBlocker(Thread t, Object arg)方法。具体代码如下所示:

  1. private static void setBlocker(Thread t, Object arg) {
  2. U.putObject(t, PARKBLOCKER, arg);
  3. }
  4. 复制代码

其中 U为sun.misc.包下的Unsafe类。而其中的PARKBLOCKER是在静态代码块中进行赋值的,也就是如下代码:

  1. private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
  2. static {
  3. try {
  4. PARKBLOCKER = U.objectFieldOffset
  5. (Thread.class.getDeclaredField(“parkBlocker”));
  6. //省略部分代码
  7. } catch (ReflectiveOperationException e) {
  8. throw new Error(e);
  9. }
  10. }
  11. 复制代码

Thread.class.getDeclaredField("parkBlocker")方法其实很好理解,就是获取线程中的parkBlocker字段。如果有则返回其对应的Field字段,如果没有则抛出NoSuchFieldException异常。那么关于Unsafe中的objectFieldOffset(Field f)方法怎么理解呢?

在描述该方法之前,需要给大家讲一个知识点。在JVM中,可以自由选择如何实现Java对象的"布局",也就Java对象的各个部分分别放在内存那个地方,JVM是可以感知和决定的。 在sun.misc.Unsafe中提供了objectFieldOffset()方法用于获取某个字段相对 Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java 对象的某个字段。

有可能大家理解起来比较困难,这里给大家画了一个图,帮助大家理解,具体如下图所示:

blocker.png

 

 

在上图中,我们创建了两个Thread对象,其中Thread对象1在内存中分配的地址为0x10000-0x10100,Thread对象2在内存中分配的地址为0x11000-0x11100,其中parkBlocker对应内存偏移量为2(这里我们假设相对于其对象的“起始位置”的偏移量为2)。那么通过objectFieldOffset(Field f)就能获取该字段的偏移量。需要注意的是某字段在其类中的内存偏移量总是相同的,也就是对于Thread对象1与Thread对象2,parkBlocker字段在其对象所在的内存偏移量始终是相同的。

那么我们再回到setBlocker(Thread t, Object arg)方法,当我们获取到parkBlocker字段在其对象内存偏移量后, 接着会调用U.putObject(t, PARKBLOCKER, arg);,该方法有三个参数,*个参数是操作对象,第二个参数是内存偏移量,第三个参数是实际存储值。该方法理解起来也很简单,就是操作某个对象中某个内存地址下的数据。那么结合我们上面所讲的。该方法的实际操作结果如下图所示:

blocker_set.png

 

 

到现在,我们就应该懂了,尽管当前线程已经阻塞,但是我们还是能直接操控线程中实际存储该字段的内存区域来达到我们想要的结果。

LockSupport底层代码实现

通过阅读源代码我们可以发现,LockSupport中关于线程的阻塞和唤醒,主要调用的是sun.misc.Unsafe 中的park(boolean isAbsolute, long time)unpark(Object thread)方法,也就是如下代码:

  1. private static final jdk.internal.misc.Unsafe theInternalUnsafe =
  2. jdk.internal.misc.Unsafe.getUnsafe();
  3. public void park(boolean isAbsolute, long time) {
  4. theInternalUnsafe.park(isAbsolute, time);
  5. }
  6. public void unpark(Object thread) {
  7. theInternalUnsafe.unpark(thread);
  8. }
  9. 复制代码

查看sun.misc.包下的Unsafe.java文件我们可以看出,内部其实调用的是jdk.internal.misc.Unsafe中的方法。继续查看jdk.internal.misc.中的Unsafe.java中对应的方法:

  1. @HotSpotIntrinsicCandidate
  2. public native void unpark(Object thread);
  3. @HotSpotIntrinsicCandidate
  4. public native void park(boolean isAbsolute, long time);
  5. 复制代码

通过查看方法,我们可以得出*终调用的是JVM中的方法,也就是会调用hotspot.share.parims包下的unsafe.cpp中的方法。继续跟踪。

  1. UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
  2. //省略部分代码
  3. thread->parker()->park(isAbsolute != 0, time);
  4. //省略部分代码
  5. } UNSAFE_END
  6. UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread)) {
  7. Parker* p = NULL;
  8. //省略部分代码
  9. if (p != NULL) {
  10. HOTSPOT_THREAD_UNPARK((uintptr_t) p);
  11. p->unpark();
  12. }
  13. } UNSAFE_END
  14. 复制代码

通过观察代码我们发现,线程的阻塞和唤醒其实是与hotspot.share.runtime中的Parker类相关。我们继续查看:

  1. class Parker : public os::PlatformParker {
  2. private:
  3. volatile int _counter ;//该变量非常重要,下文我们会具体描述
  4. //省略部分代码
  5. protected:
  6. ~Parker() { ShouldNotReachHere(); }
  7. public:
  8. // For simplicity of interface with Java, all forms of park (indefinite,
  9. // relative, and absolute) are multiplexed into one call.
  10. void park(bool isAbsolute, jlong time);
  11. void unpark();
  12. //省略部分代码
  13. }
  14. 复制代码

在上述代码中,volatile int _counter该字段的值非常重要,一定要注意其用volatile修饰(在下文中会具体描述,接着当我们通过SourceInsight工具(推荐大家阅读代码时,使用该工具)点击其park与unpark方法时,我们会得到如下界面:

 

parker.png

 

 

从图中红色矩形中我们可也看出,针对线程的阻塞和唤醒,不同操作系统有着不同的实现。众所周知Java是跨平台的。针对不同的平台,做出不同的处理。也是非常理解的。因为作者对windows与solaris操作系统不是特别了解。所以这里我选择对Linux下的平台下进行分析。也就是选择hotspot.os.posix包下的os_posix.cpp文件进行分析。

Linux下的park实现

为了方便大家理解Linux下的阻塞实现,在实际代码中我省略了一些不重要的代码,具体如下图所示:

  1. void Parker::park(bool isAbsolute, jlong time) {
  2. //(1)如果_counter的值大于0,那么直接返回
  3. if (Atomic::xchg(0, &_counter) > 0) return;
  4. //获取当前线程
  5. Thread* thread = Thread::current();
  6. JavaThread *jt = (JavaThread *)thread;
  7. //(2)如果当前线程已经中断,直接返回。
  8. if (Thread::is_interrupted(thread, false)) {
  9. return;
  10. }
  11. //(3)判断时间,如果时间小于0,或者在*对时间情况下,时间为0直接返回
  12. struct timespec absTime;
  13. if (time < 0 || (isAbsolute && time == 0)) { // don’t wait at all
  14. return;
  15. }
  16. //如果时间大于0,判断阻塞超时时间或阻塞截止日期,同时将时间赋值给absTime
  17. if (time > 0) {
  18. to_abstime(&absTime, time, isAbsolute);
  19. }
  20. //(4)如果当前线程已经中断,或者申请互斥锁失败,则直接返回
  21. if (Thread::is_interrupted(thread, false) ||
  22. pthread_mutex_trylock(_mutex) != 0) {
  23. return;
  24. }
  25. //(5)如果是时间等于0,那么就直接阻塞线程,
  26. if (time == 0) {
  27. _cur_index = REL_INDEX; // arbitrary choice when not timed
  28. status = pthread_cond_wait(&_cond[_cur_index], _mutex);
  29. assert_status(status == 0, status, “cond_timedwait”);
  30. }
  31. //(6)根据absTime之前计算的时间,阻塞线程相应时间
  32. else {
  33. _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
  34. status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
  35. assert_status(status == 0 || status == ETIMEDOUT,
  36. status, “cond_timedwait”);
  37. }
  38. //省略部分代码
  39. //(7)当线程阻塞超时,或者到达截止日期时,直接唤醒线程
  40. _counter = 0;
  41. status = pthread_mutex_unlock(_mutex);
  42. //省略部分代码
  43. }
  44. 复制代码

从整个代码来看其实关于Linux下的park方法分为以下七个步骤:

  • (1)调用Atomic::xchg方法,将_counter的值赋值为0,其方法的返回值为之前_counter的值,如果返回值大于0(因为有其他线程操作过_counter的值,也就是其他线程调用过unPark方法),那么就直接返回。
  • (2)如果当前线程已经中断,直接返回。也就是说如果当前线程已经中断了,那么调用park()方法来阻塞线程就会无效。
  • (3) 判断其设置的时间是否合理,如果合理,判断阻塞超时时间阻塞截止日期,同时将时间赋值给absTime
  • (4) 在实际对线程进行阻塞前,再一次判断如果当前线程已经中断,或者申请互斥锁失败,则直接返回
  • (5) 如果是时间等于0(时间为0,表示一直阻塞线程,除非调用unPark方法唤醒),那么就直接阻塞线程,
  • (6)根据absTime之前计算的时间,并调用pthread_cond_timedwait方法阻塞线程相应的时间。
  • (7) 当线程阻塞相应时间后,通过pthread_mutex_unlock方法直接唤醒线程,同时将_counter赋值为0。

因为关于Linux的阻塞涉及到其内部函数,这里将用到的函数都进行了声明。大家可以根据下表所介绍的方法进行理解。具体方法如下表所示:

linux方法.png

 

 

Linux下的unpark实现

在了解了Linux的park实现后,再来理解Linux的唤醒实现就非常简单了,查看相应方法:

  1. void Parker::unpark() {
  2. int status = pthread_mutex_lock(_mutex);
  3. assert_status(status == 0, status, “invariant”);
  4. const int s = _counter;
  5. //将_counter的值赋值为1
  6. _counter = 1;
  7. // must capture correct index before unlocking
  8. int index = _cur_index;
  9. status = pthread_mutex_unlock(_mutex);
  10. assert_status(status == 0, status, “invariant”);
  11. //省略部分代码
  12. }
  13. 复制代码

其实从代码整体逻辑来讲,*终唤醒其线程的方法为pthread_mutex_unlock(_mutex)(关于该函数的作用,我已经在上表进行介绍了。大家可以参照Linux下的park实现中的图表进行理解)。同时将_counter的值赋值为1, 那么结合我们上文所讲的park(将线程进行阻塞)方法,那么我们可以得知整个线程的唤醒与阻塞,在Linux系统下,其实是受到Parker类中的_counter的值的影响的

LockSupport的使用

现在我们基本了解了LockSupport的基本原理。现在我们来看看它的基本使用吧。在例子中,为了方便大家顺便弄清blocker的作用,这里我调用了带blocker的park方法。具体代码如下所示:

  1. class LockSupportDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread a = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. LockSupport.park(“线程a的blocker数据”);
  7. System.out.println(“我是被线程b唤醒后的操作”);
  8. }
  9. });
  10. a.start();
  11. //让当前主线程睡眠1秒,保证线程a在线程b之前执行
  12. Thread.sleep(1000);
  13. Thread b = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. String before = (String) LockSupport.getBlocker(a);
  17. System.out.println(“阻塞时从线程a中获取的blocker——>” + before);
  18. LockSupport.unpark(a);
  19. //这里睡眠是,保证线程a已经被唤醒了
  20. try {
  21. Thread.sleep(1000);
  22. String after = (String) LockSupport.getBlocker(a);
  23. System.out.println(“唤醒时从线程a中获取的blocker——>” + after);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. });
  29. b.start();
  30. }
  31. }
  32. 复制代码

代码中,创建了两个线程,线程a与线程b(线程a优先运行与线程b),在线程a中,通过调用LockSupport.park("线程a的blocker数据");给线程a设置了一个String类型的blocker,当线程a运行的时候,直接将线程a阻塞。在线程b中,先会获取线程a中的blocker,打印输出后。再通过LockSupport.unpark(a);唤醒线程a。当唤醒线程a后。*后输出并打印线程a中的blocker。 实际代码运行结果如下:

  1. 阻塞时从线程a中获取的blocker——>线程a的blocker数据
  2. 我是被线程b唤醒后的操作
  3. 唤醒时从线程a中获取的blocker——>null
  4. 复制代码

从结果中,我们可以看出,线程a被阻塞时,后续就不会再进行操作了。当线程a被线程b唤醒后。之前设置的blocker也变为null了。同时如果在线程a中park语句后还有额外的操作。那么会继续运行。关于为毛之前的blocker之前变为null,具体原因如下:

  1. public static void park(Object blocker) {
  2. Thread t = Thread.currentThread();
  3. setBlocker(t, blocker);
  4. U.park(false, 0L);//当线程被阻塞时,会阻塞在这里
  5. setBlocker(t, null);//线程被唤醒时,会将blocer置为null
  6. }
  7. 复制代码

通过上述例子,我们完全知道了blocker可以在线程阻塞的时候,获取数据。也就证明了当我们对线程进行问题排查和系统监控的时候blocker的有着非常重要的作用。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速