开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

指令式编程 VS 声明式编程 指令式编程 VS 声明式编程

发表于 2017-12-26

指令式编程 VS 声明式编程

概括地说,我们可以有两种编写代码的方式:指令式和声明式。

我们可以定义如下:

  • 指令式编程:告诉机器该如何做,并得到自己想要的结果。
  • 声明式编程:告诉机器您想得到什么,让机器自己计算该如何做。

指令式编程和声明式编程的例子

举一个简单的例子,假设我们想让数组中的每个值变为原来的2倍。

指令式编程的代码可以如下:

1
2
3
4
5
6
7
8
复制代码var numbers = [1,2,3,4,5]
var doubled = []

for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push(newNumber)
}
console.log(doubled) //=> [2,4,6,8,10]

我们遍历整个数组,取出每个元素,乘以2,然后把新值放到新数组中,直到完成。

一种声明式的编程方式可以使用Array.map,如下:

1
2
3
4
5
6
复制代码var numbers = [1,2,3,4,5]

var doubled = numbers.map(function(n) {
return n * 2
})
console.log(doubled) //=> [2,4,6,8,10]

map根据旧数组返回新的数组,在这个例子中,通过把旧数组中的元素传入到map (function(n) { return n*2 }中,返回一个新数组,新数组的每个值都是相对应的旧数组的值的2倍。

map函数的作用是抽象出遍历数组的过程,让我们更关注于我们想得到什么。注意,我们传入到map中的函数是纯净的。不能有任何副作用(改变其他额外的状态),它只是拿到一个数,并把它变成2倍。

对于数组,这里还有其他一些常见的声明式抽象函数。比如说,为了对数组中所有的元素求和,我们可以这么做:

1
2
3
4
5
6
7
复制代码var numbers = [1,2,3,4,5]
var total = 0

for(var i = 0; i < numbers.length; i++) {
total += numbers[i]
}
console.log(total) //=> 15

或者我们可以使用声明式函数reduce:

1
2
3
4
5
6
复制代码var numbers = [1,2,3,4,5]

var total = numbers.reduce(function(sum, n) {
return sum + n
}, 0);
console.log(total) //=> 15

reduce利用给定的函数将数组遍历计算出一个值。它将这个函数应用到数组中的每个元素。在每次调用中,第一个参数(例子中的sum)是前一个元素调用函数后得出的结果,第二个参数(n)是当前元素。所以,在这个例子中,每一步,都把当前数组元素n加到sum中,这样,最后我们就能得到整个数组的和。

同样,reduce为我们抽象了循环遍历和状态管理方面的事情,给了我们遍历数组计算出一个值的通用方法。我们需要做的就是明确我们想要什么。

很奇怪?

我保证,如果您之前没见过map或者reduce,您一定会感觉到奇怪。作为程序员,我们总是很习惯指定如何让事情发生,“遍历数组”,“如果怎么样然后怎么样”,“用新值更新这个变量”。既然我们已经知道如何告诉机器该怎么做,为什么还要学习这个看起来有点奇怪的抽象呢?

在许多情况下,指令式代码是好的。当我们编写业务逻辑时,我们通常不得不编写大部分必需的代码,因为在我们的业务逻辑中不存在更通用的抽象。

但是如果我们花时间去学习(或构建!)声明式的抽象方法,在编写代码时,我们就可以使用一些强大的快捷方式。首先,我们通常会写得越来越少,这是一个速见成效的胜利。然后,我们也可以在一个更高的层次思考问题,站在云端思考我们想要发生什么,而不是陷在泥里思考如何使它发生。

SQL

您可能没有意识到,但是在SQL中,您已经使用过声明式编程了。

您可以把SQL看作一种用于处理数据集的声明式查询语言。您是否用SQL写过整个应用?可能没有。但是对于处理相关联的数据集,它会非常强大。

做一个查询:

1
2
3
复制代码SELECT * from dogs
INNER JOIN owners
WHERE dogs.owner_id = owners.id

想象一下您用指令式编程来写这段逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码//dogs = [{name: 'Fido', owner_id: 1}, {...}, ... ]
//owners = [{id: 1, name: 'Bob'}, {...}, ...]

var dogsWithOwners = []
var dog, owner

for(var di=0; di < dogs.length; di++) {
dog = dogs[di]

for(var oi=0; oi < owners.length; oi++) {
owner = owners[oi]
if (owner && dog.owner_id == owner.id) {
dogsWithOwners.push({
dog: dog,
owner: owner
})
}
}}
}

我并不是说SQL很容易理解,或者当您第一次看到时就很轻松地明白它,但是相比于那段复杂的代码,它已经简洁很多了。

但是它不仅更简短而且更容易阅读,SQL还给了我们许多其他好处。因为我们已经抽象了具体的实现方法,我们可以只关注我们想要什么,然后让数据库优化具体实现步骤。

如果我们没有使用它,我们自己的代码将会很慢,因为我们必须要为列表中的每只dog遍历整个owners数组。

但是在SQL代码的例子中,我们可以让数据库来自己实现如何返回给我们正确的结果。如果使用索引有意义(假设我们已经建立了),数据库就会这么做,这会提升很大的性能。如果此次查询在一秒前就执行过,就可以直接从缓存中读取。通过放手让计算机自己决定实现方式,我们只需要稍微改变一下认知,就可以得到巨大的好处。

d3.js

另外一个能体现出声明式编程好处的地方在用户界面、图表和动画。

编写用户界面是一件很困难的事。因为我们有用户交互,想要做好用户交互,我们通常会有很多的状态管理和指令式代码,其实这些都可以被抽象出来,但通常并没有。

一个很好的声明式抽象的例子是d3.js。通过使用JavaScript和SVG(大部分),d3这个库可以帮我们创建交互式的,带动画的可视化数据。

第一次(第五次,甚至第十次)您看到或尝试写d3代码,您可能都会头痛。就像SQL一样,d3几乎封装了所有您可能用到的处理可视化数据的方法,让您只关注您想得到什么。

这里有一个例子(我建议大家看一下这个例子,了解下上下文)。这个d3图表是根据data数组中的每个对象画一个圆。为了展示发生了什么,我们每秒钟加一个圆。

有趣的代码是:

1
2
3
4
5
6
7
8
9
10
11
复制代码//var data = [{x: 5, y: 10}, {x: 20, y: 5}]

var circles = svg.selectAll('circle')
.data(data)

circles.enter().append('circle')
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.y })
.attr('r', 0)
.transition().duration(500)
.attr('r', 5)

没有必要弄清这里究竟发生了什么(不管怎样,你都需要一段时间才能清醒过来),但要点是这样的:

首先我们选取所有的circlesvg(初始的时候没有)。然后给它们绑定一些数据。

D3持续追踪哪个数据点被绑定到图表中哪个圆上。所以开始的时候我们有两个数据点,但是没有圆;然后我们可以使用.enter()方法来获取已经“进入”的数据点。对于这些点,我们想对应地将一个圆加入到图表中,以数据的x和y为圆心,初始半径为0,但是0.5s后渐变到半径为5。

这为什么有趣?

再来看一下代码,并思考我们是否描述了我们想要什么样的可视化图表,或者是否怎样来画?您会发现,几乎没有怎样画的代码出现。我们只是描述了我们想要什么:

我想要把这个数据画成圆,圆心由数据指定。并且如果有新的圆,就加进来,且半径要有动画。

这很神奇,我们没有写一个循环,也没有写状态管理。编写图表通常是困难和麻烦的,但是d3已经为我们做了大部分封装,我们只需要明确我们想要什么即可。

现在d3.js易懂了吗?并没有,它肯定要花一段时间来学习。并且您大部分要学习的是放弃指定事情如何发生,而是要学习如何明确您想要什么。

刚开始的时候,这很困难,但是经过几个小时后,神奇的事情发生了——您会越来越有效率。通过封装具体实现方式,d3.js真正地让您关注您想看到什么,并且这也是当您实现可视化时只需要关注的。它把您从繁琐的细节中解放出来,让您以一个更高的水平思考问题,打开了创造的可能性。

最后

声明式编程允许我们描述我们想要的,让底层软件/计算机等来处理具体如何实现。

正如所见,很多时候,这可以让我们在写代码方面得到提高,不仅是更少的代码行数,或者性能,而且高抽象的编码方式使我们能更关注于我们想要什么,这正是我们作为问题解决者真正该关心的。

问题是,过去我们已经习惯于指令式编程。它使我们感觉舒服和自然,甚至强大(能够控制它是怎么发生的),不肯将具体实现方式交给我们看不到或者不理解的程序。

有时候,编写具体实现方式是可以的。如果我们需要微调代码来提高性能,我们可能需要更细节地指明如何实现。或者对于业务代码,本身就没有什么可以抽象封装的地方,我们就得编写指令式代码。

但是,我们可以(并且应该)经常寻找声明性的方法来编写代码,如果找不到,我们应该构建它们。这在开始的时候会很困难吗?是的,几乎可以肯定!但正如我们看到的SQL和d3.js,长期收益是巨大的!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

译:Linux 文件系统介绍

发表于 2017-12-22


本文旨在对Linux文件系统概念进行深层次的讨论。本文既不准备对某个特定类型的文件系统(比如ext4)进行基础性的描述,也不打算作为一个讲解文件系统命令的教程。

每台通用的计算机都需要把各种类型的数据存储在硬盘驱动器(HDD)或者一些同样功能的设备上,比如USB。存储在这些设备上有几个原因,首先,RAM会在计算机电源关闭时丢失内容,虽然也有非易失性类型的内存,可以在电源关闭后维持数据存储不丢失(如flash内存也就是闪存使用的USB和固态硬盘),但flash内存要比一些标准的、挥发性的内存比如DDR3和其他类似的类型昂贵的多。

数据需要存储在硬盘驱动器上的第二个原因是,即使是标准的RAM也要比磁盘空间更昂贵。RAM和磁盘成本都在迅速下降,但如果按每字节的成本来算的话还是RAM更高。我们就基于16GB RAM和2TB硬盘的成本,快速计算其每个字节的成本,结果显示 RAM比硬盘驱动器的价格高约71倍。目前,RAM的典型成本大约每字节0.0000000043743750。

更加直截了当说的话,在计算机的早期,一种内存是基于CRT屏幕上的点的,每一比特大约1美元,这是非常非常昂贵的!

定义

你也许会听到人们经常以不同的或者混淆的方式谈论文件系统这个词。这个词本身可能有多重含义,你可能需要从讨论或文档的语境中辨别真正的意思。

我将尝试根据我在不同情况下使用它的方式来定义“文件系统”的各种含义。注意,在试图遵循标准的“official”含义时,我的意图是根据它的各种用法定义术语。在本文后面的小节中,将更详细地讨论这些含义。

  1. 整个Linux目录结构从顶部(/)根目录开始。
  2. 各种特定类型的数据存储格式,如EXT3、EXT4、BTRFS、XFS等。Linux支持近100种类型的文件系统,包括一些非常古老的文件系统,以及一些最新的文件系统。每个文件系统类型都使用自己的元数据结构来定义如何存储和访问数据。
  3. 一个分区或被格式化为特定类型文件系统的逻辑卷,可以被挂载到Linux文件系统上的指定挂载点上。

基本的文件系统功能

磁盘存储是必需的,它带来了一些有趣且不可避免的细节。显然,文件系统的设计目的是为数据的非易失性存储提供空间,这是它的最根本的功能。但是它还有许多其他重要的功能满足不同的需求。

所有文件系统都需要提供一个命名空间(namespace)——即一个命名和组织的方法。它定义了如何命名文件,具体来说是文件名的长度和可用于文件名的字符的子集,这些字符可以从全部字符集中获取。它还定义了磁盘上数据的逻辑结构,例如使用目录来组织文件,而不是将它们集中在一个单一的、巨大的文件集中。

一旦定义了名称空间,就需要一个元数据结构来为该名称空间提供逻辑基础。其中元数据包括支持分层目录结构所需的数据结构;用于确定磁盘上哪些块空间已经被使用和哪些可用的结构;允许维护文件和目录名称的结构;文件相关的信息,比如它们的大小和时间,比如创建时间、修改时间或最后访问时间等等;以及属于文件的数据在磁盘上面的位置。还有一些其他元数据用于存储关于磁盘划分的高级信息,如逻辑卷和分区。这个更高级别的元数据和它所代表的结构包含了描述存储在驱动器或分区上的文件系统的信息,这些元数据独立于上面提到的一般文件系统元数据。

文件系统还需要API接口为系统函数调用提供访问,这些系统函数调用操作文件和目录等文件系统对象。APIs提供诸如创建、移动和删除文件之类的接口。它还提供了一些算法来确定文件放置在文件系统上的位置。这些算法还有确定速度或最小化磁盘碎片等作用。

现代文件系统还提供了一个安全模式,它是一个为文件和目录定义访问权限的方案。Linux文件系统安全模式有助于确保用户只能访问他们自己的文件,而不是其他人或操作系统本身的文件。

最后的构建块是实现所有这些功能所需的软件。为了改提高系统和程序员效率,Linux使用了一种two-part的软件实现方式。

图1:Linux two-part 文件系统软件实现方式
这两部分实现的第一部分是Linux虚拟文件系统。这个虚拟文件系统为内核和开发人员提供了访问所有类型文件系统的一组命令。虚拟文件系统软件调用特定的设备驱动程序来连接到各种类型的文件系统。文件系统特定的设备驱动程序是实现的第二部分。设备驱动程序将文件系统命令的标准集根据特定分区或逻辑卷上的文件系统类型做转换和解释。

目录结构

作为一个通常很有条理的处女座,我喜欢把东西放在那些小而有组织的地方,而不是一个大的桶。使用目录可以帮助我存储和定位我想找的文件。目录也被称为文件夹,因为它们可以被看作实际生活中办公桌上保存文件的文件夹。

在Linux和许多其他操作系统中,目录可以以树状的层次结构来构造。Linux目录结构在Linux文件系统层次标准(FHS)中得到了很好的定义和记录。在访问它们时引用这些目录是通过使用由前斜杠(/)连接的顺序较深的目录名称(/)来实现的,例如/var/log和/var/spool/mail。这些被称为路径。

下表提供了一个非常简短的标准、众所周知的和定义在顶层上的Linux目录及其用途的列表。

表1:Linux文件系统层次结构的顶层
表1中显示的目录及其子目录及其子目录的子目录,其中背景色为蓝色的目录被认为是根文件系统中不能缺少的组成部分。也就是说,它们不能作为单独的文件系统创建,并且在启动时安装。这是因为它们(特别是它们的内容)必须在引导时出现,以便系统正确引导。

/media和/mnt目录是根文件系统的一部分,但它们不应该包含任何数据。相反,它们只是临时的挂载点。
其余的目录,在表1中没有背景颜色的目录不需要在引导序列中出现,但是会在以后安装,在启动序列中准备主机来执行有用的工作。

可以通过参考官方的Linux文件系统层次标准(FHS)web页面,以了解这些目录及其许多子目录的详细信息。维基百科对FHS的描述也很好。应尽可能密切地遵循这一标准,以确保业务和职能的一致性。不管主机上使用的文件系统类型是什么,这个分层目录结构都是相同的。

Linux 统一目录结构

在一些非linux PC操作系统中,如果有多个物理硬盘或多个分区,每个磁盘或分区都被分配一个驱动器号。想定位到文件或程序所在的硬盘的位置,驱动器号是必需的,比如C:或D:。然后,以命令的形式发出驱动器字母D:例如,要更改到D:驱动器,然后使用cd命令更改到正确的目录来定位所需的文件。每个硬盘都有自己独立的和完整的目录树。

Linux文件系统将所有物理硬盘和分区统一到一个目录结构中。这一切都是从顶部(/)目录开始的。所有其他目录及其子目录都位于Linux根目录下。这意味着只有一个单独的目录树来搜索文件和程序。

它们之所以能工作,都是因为文件系统,如/home、/tmp、/var、/opt、/usr可以被创建在单独的物理硬盘上一个不同的分区,或一个不同的逻辑卷/(根)文件系统,然后安装在一个挂载点(目录)作为根文件系统树的一部分。即使是可移动的驱动器,如闪存盘或外部USB或ESATA硬盘驱动器也将安装到根文件系统中,并成为该目录树的一个不可分割的部分。

在从一个版本的Linux发行版升级到另一个版本,或者从一个发行版切换成到另一个发行版时,文件系统有一个很好的理由可以做到这一点。一般来说,除了Fedora的dnf升级之类的升级工具之外,偶尔重新格式化包含操作系统的硬盘驱动器是明智的,因为在升级过程中,硬盘驱动器会清除任何随时间积累的东西。如果/home是根文件系统的一部分,它将被重新格式化,然后必须从备份中恢复。通过将/home格式化为一个单独的文件系统,那么在根文件系统格式化时它将识别成一个单独的文件系统,并且可以跳过当前步骤。这也适用于数据库、电子邮件收件箱、网站和其他可变用户和系统数据存储的目录/var。

维护Linux目录树的某些部分作为单独的文件系统还有其他原因。例如,很久以前,当我还没有意识到围绕着所有需要的Linux目录都作为/(root)文件系统的一部分的潜在问题时,我曾用大量非常大的文件填充了我的主目录。由于/home目录和/tmp目录都不是独立的文件系统,而只是根文件系统的子目录,所以整个根文件系统都被填满了。操作系统没有空间创建临时文件或扩展现有的数据文件。起初,应用程序开始抱怨没有空间保存文件,然后操作系统本身开始变得非常奇怪。引导到单用户模式,并清除我的主目录中的问题文件,这让我可以重新开始。然后,我使用一个相当标准的多文件系统设置重新安装了Linux,并能够防止完全的系统崩溃再次发生。

我曾经还遇到过一个情况,Linux主机继续运行,但是阻止用户使用GUI桌面登录。我能够使用一个虚拟控制台本地使用命令行接口(CLI),并远程使用SSH。问题是,/tmp文件系统已经填满了,而GUI桌面所需的一些临时文件在登录时无法创建。由于CLI登录不需要在/tmp中创建文件,因此缺少空间并没有阻止我使用CLI进行登录。在这种情况下,/tmp目录是一个单独的文件系统,在卷组中有大量可用的空间,/tmp逻辑卷是其中的一部分。我只是将/tmp 逻辑卷扩展到一个够大的容量(其实就是LVM扩容),以适应我对该主机所需要的临时文件空间数量的新需求,并解决了问题。请注意,此解决方案不需要重新启动,并且当/tmp文件系统被放大后,用户可以登录到桌面。

逻辑卷扩展也可以参考我之前总结的一篇文章,简洁明了:LVM动态扩展

另一种情况发生在我在一家大型科技公司做实验室管理员的时候。我们的一个开发人员在错误的位置(/var)安装了应用程序(我个人认为不能说是装在错误的位置,只能说装的位置的可用空间不合适)。应用程序崩溃是因为/var文件系统已经满了,而存储在/var/log/上的日志文件由于缺少空间,不能添加新的消息。但是,由于关键的/(root)和/tmp文件系统没有填充,系统仍然保持运行。删除违规应用程序并将其重新安装到/opt文件系统中解决了这个问题。(其实通过LVM动态扩容也是可以解决这个问题,要么扩展空间大小,要么换大空间的文件系统)

文件系统类型

Linux支持读取大约100个分区类型,它只可以在其中的几个而不是所有的分区上创建或写文件。但是,在同一个根文件系统上的不同类型的挂载文件系统是可以做到的,也是非常常见的。在此上下文中,我们讨论的是在硬盘或逻辑卷的分区上存储和管理用户数据所需的结构和元数据。这里提供了Linux fdisk命令识别的文件系统分区类型的完整列表,这样您就可以了解Linux与许多类型的系统之间的高度兼容性。

1
复制代码0 Empty 24 NEC DOS 81 Minix / old Lin bf Solaris 1 FAT12 27 Hidden NTFS Win 82 Linux swap / So c1 DRDOS/sec (FAT- 2 XENIX root 39 Plan 9 83 Linux c4 DRDOS/sec (FAT- 3 XENIX usr 3c PartitionMagic 84 OS/2 hidden or c6 DRDOS/sec (FAT- 4 FAT16 <32M 40 Venix 80286 85 Linux extended c7 Syrinx 5 Extended 41 PPC PReP Boot 86 NTFS volume set da Non-FS data 6 FAT16 42 SFS 87 NTFS volume set db CP/M / CTOS / . 7 HPFS/NTFS/exFAT 4d QNX4.x 88 Linux plaintext de Dell Utility 8 AIX 4e QNX4.x 2nd part 8e Linux LVM df BootIt 9 AIX bootable 4f QNX4.x 3rd part 93 Amoeba e1 DOS access a OS/2 Boot Manag 50 OnTrack DM 94 Amoeba BBT e3 DOS R/O b W95 FAT32 51 OnTrack DM6 Aux 9f BSD/OS e4 SpeedStor c W95 FAT32 (LBA) 52 CP/M a0 IBM Thinkpad hi ea Rufus alignment e W95 FAT16 (LBA) 53 OnTrack DM6 Aux a5 FreeBSD eb BeOS fs f W95 Ext'd (LBA) 54 OnTrackDM6 a6 OpenBSD ee GPT 10 OPUS 55 EZ-Drive a7 NeXTSTEP ef EFI (FAT-12/16/ 11 Hidden FAT12 56 Golden Bow a8 Darwin UFS f0 Linux/PA-RISC b 12 Compaq diagnost 5c Priam Edisk a9 NetBSD f1 SpeedStor 14 Hidden FAT16 <3 61 SpeedStor ab Darwin boot f4 SpeedStor 16 Hidden FAT16 63 GNU HURD or Sys af HFS / HFS+ f2 DOS secondary 17 Hidden HPFS/NTF 64 Novell Netware b7 BSDI fs fb VMware VMFS 18 AST SmartSleep 65 Novell Netware b8 BSDI swap fc VMware VMKCORE 1b Hidden W95 FAT3 70 DiskSecure Mult bb Boot Wizard hid fd Linux raid auto 1c Hidden W95 FAT3 75 PC/IX bc Acronis FAT32 L fe LANstep 1e Hidden W95 FAT1 80 Old Minix be Solaris boot ff BBT

拥有支持读取这么多分区类型的能力的主要目的是允许兼容性和与其他计算机系统的文件系统的某些互操作性。使用Fedora创建新文件系统时可用的选项如下所示。

  • btrfs
  • cramfs
  • ext2
  • ext3
  • ext4
  • fat
  • gfs2
  • hfsplus
  • minix
  • msdos
  • ntfs
  • reiserfs
  • vfat
  • xfs

其他发行版支持创建不同的文件系统类型。例如,CentOS 6只支持创建上面列表中粗体显示的文件系统。

挂载

在Linux中,to mount一词指的是早期的计算机中,当一个磁带或可移动的磁盘包需要在适当的驱动器上进行物理安装时。在物理上放置磁盘之后,磁盘包上的文件系统将由操作系统逻辑上挂载,以使操作系统、应用程序和用户能够访问这些内容。

挂载点仅仅是一个目录,就像任何其他的目录一样,它是作为根文件系统的一部分创建的。例如,home文件系统安装在目录/home上。文件系统可以安装在其他非根文件系统上的挂载点上,但这并不常见。
Linux根文件系统安装在根目录上(/)非常早的引导序列。其他文件系统会被安装在后面,由Linux启动程序,无论是在SystemV下的rc,还是在新的Linux版本中的systemd,在启动过程中挂载文件系统是由/ etc/fstab配置文件管理的。一个容易记住的方法是fstab表示“文件系统表”,它是要挂载的文件系统的列表,还有它们指定的挂载点,以及特定文件系统可能需要的任何选项。

文件系统安装在现有的目录(挂载点)上,使用mount命令。一般来说,任何被用作挂载点的目录都应该是空的,并且没有包含其中的任何其他文件。Linux不会阻止用户将一个文件系统安装到一个已经存在的文件系统上,或者在一个包含文件的目录上。如果你在现有的目录或文件系统上安装了一个文件系统,那么原始的内容将会被隐藏,只有新挂载的文件系统的内容才会可见。

结论

我希望本文能够把围绕“文件系统”这个术语的一些可能的混淆给理清楚。我花了很长时间才真正理解并理解Linux文件系统的复杂性、优雅和功能。

如果你有问题,请把它们加到下面的评论中,我会尽量回答。

下个月

另一个重要的概念是,对于Linux,一切都是文件。这个概念为用户和系统管理员提供了一些有趣且重要的实际应用程序。我之所以提到这一点,是因为您可能想在我下个月的/dev目录下的文章前阅读我的一切皆文件的文章。

原文地址:opensource.com/life/16/10/…

“一切皆文件”这篇文章我也进行了翻译

翻译地址:www.tony-yin.top/2017/12/21/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里开源的分布式分库分表中间件之MyCat从入门到放弃

发表于 2017-12-21

1.非分片字段查询

Mycat中的路由结果是通过分片字段和分片方法来确定的。例如下图中的一个Mycat分库方案:

  • 根据 tt_waybill 表的 id 字段来进行分片
  • 分片方法为 id 值取 3 的模,根据模值确定在DB1,DB2,DB3中的某个分片

如果查询条件中有 id 字段的情况还好,查询将会落到某个具体的分片。例如:

mysql>select * from tt_waybill where id = 12330;

此时Mycat会计算路由结果

12330 % 3 = 0 –> DB1

并将该请求路由到DB1上去执行。 如果查询条件中没有 分片字段 条件,例如:

mysql>select * from tt_waybill where waybill_no =88661;

此时Mycat无法计算路由,便发送到所有节点上执行:

DB1 –> select * from tt_waybill where waybill_no =88661; DB2 –> select * from tt_waybill where waybill_no =88661; DB3 –> select * from tt_waybill where waybill_no =88661;

如果该分片字段选择度高,也是业务常用的查询维度,一般只有一个或极少数个DB节点命中(返回结果集)。示例中只有3个DB节点,而实际应用中的DB节点数远超过这个,假如有50个,那么前端的一个查询,落到MySQL数据库上则变成50个查询,会极大消耗Mycat和MySQL数据库资源。

如果设计使用Mycat时有非分片字段查询,请考虑放弃!

2.分页排序

先看一下Mycat是如何处理分页操作的,假如有如下Mycat分库方案: 一张表有30份数据分布在3个分片DB上,具体数据分布如下

DB1:[0,1,2,3,4,10,11,12,13,14] DB2:[5,6,7,8,9,16,17,18,19] DB3:[20,21,22,23,24,25,26,27,28,29]

(这个示例的场景中没有查询条件,所以都是全分片查询,也就没有假定该表的分片字段和分片方法)

当应用执行如下分页查询时

mysql>select * from table limit 2;

Mycat将该SQL请求分发到各个DB节点去执行,并接收各个DB节点的返回结果

DB1: [0,1] DB2: [5,6] DB3: [20,21]

但Mycat向应用返回的结果集取决于哪个DB节点最先返回结果给Mycat。如果Mycat最先收到DB1节点的结果集,那么Mycat返回给应用端的结果集为 [0,1],如果Mycat最先收到DB2节点的结果集,那么返回给应用端的结果集为 [5,6]。也就是说,相同情况下,同一个SQL,在Mycat上执行时会有不同的返回结果。

在Mycat中执行分页操作时必须显示加上排序条件才能保证结果的正确性,下面看一下Mycat对排序分页的处理逻辑。 假如在前面的分页查询中加上了排序条件(假如表数据的列名为id)

mysql>select * from table order by id limit 2;

Mycat的处理逻辑如下图:

在有排序呢条件的情况下,Mycat接收到各个DB节点的返回结果后,对其进行最小堆运算,计算出所有结果集中最小的两条记录 [0,1] 返回给应用。

但是,当排序分页中有 偏移量 (offset)时,处理逻辑又有不同。假如应用的查询SQL如下:

mysql>select * from table order by id limit 5,2;

如果按照上述排序分页逻辑来处理,那么处理结果如下图:

Mycat将各个DB节点返回的数据 [10,11], [16,17], [20,21] 经过最小堆计算后返回给应用的结果集是 [10,11]。可是,对于应用而言,该表的所有数据明明是 0-29 这30个数据的集合, limit 5,2 操作返回的结果集应该是 [5,6],如果返回 [10,11] 则是错误的处理逻辑。

所以Mycat在处理 有偏移量的排序分页 时是另外一套逻辑——改写SQL 。如下图:

Mycat在下发有 limit m,n 的SQL语句时会对其进行改写,改写成 limit 0, m+n 来保证查询结果的逻辑正确性。所以,Mycat发送到后端DB上的SQL语句是

mysql>select * from table order by id limit 0,7;

各个DB返回给Mycat的结果集是

DB1: [0,1,2,3,4,10,11] DB2: [5,6,7,8,9,16,17] DB3: [20,21,22,23,24,25,26]

经过最小堆计算后得到最小序列 [0,1,2,3,4,5,6] ,然后返回偏移量为5的两个结果为 [5,6] 。

虽然Mycat返回了正确的结果,但是仔细推敲发现这类操作的处理逻辑是及其消耗(浪费)资源的。应用需要的结果集为2条,Mycat中需要处理的结果数为21条。也就是说,对于有 t 个DB节点的全分片 limit m, n 操作,Mycat需要处理的数据量为 (m+n)*t 个。比如实际应用中有50个DB节点,要执行limit 1000,10操作,则Mycat处理的数据量为 50500 条,返回结果集为10,当偏移量更大时,内存和CPU资源的消耗则是数十倍增加。

如果设计使用Mycat时有分页排序,请考虑放弃!

3.任意表JOIN

先看一下在单库中JOIN中的场景。假设在某单库中有 player 和 team 两张表,player 表中的 team_id 字段与 team 表中的 id 字段相关联。操作场景如下图:

JOIN操作的SQL如下

mysql>select p_name,t_name from player p, team t where p.no = 3 and p.team_id = t.id;

此时能查询出结果

p_name t_name
Wade Heat

如果将这两个表的数据分库后,相关联的数据可能分布在不同的DB节点上,如下图:

这个SQL在各个单独的分片DB中都查不出结果,也就是说Mycat不能查询出正确的结果集。

设计使用Mycat时如果要进行表JOIN操作,要确保两个表的关联字段具有相同的数据分布,否则请考虑放弃!

4.分布式事务

Mycat并没有根据二阶段提交协议实现 XA事务,而是只保证 prepare 阶段数据一致性的 弱XA事务 ,实现过程如下:

应用开启事务后Mycat标识该连接为非自动提交,比如前端执行

mysql>begin;

Mycat不会立即把命令发送到DB节点上,等后续下发SQL时,Mycat从连接池获取非自动提交的连接去执行。

Mycat会等待各个节点的返回结果,如果都执行成功,Mycat给该连接标识为 Prepare Ready 状态,如果有一个节点执行失败,则标识为 Rollback 状态。

执行完成后Mycat等待前端发送 commit 或 rollback 命令。发送 commit 命令时,Mycat检测当前连接是否为 Prepare Ready 状态,若是,则将 commit 命令发送到各个DB节点。

但是,这一阶段是无法保证一致性的,如果一个DB节点在 commit 时故障,而其他DB节点 commit 成功,Mycat会一直等待故障DB节点返回结果。Mycat只有收到所有DB节点的成功执行结果才会向前端返回 执行成功 的包,此时Mycat只能一直 waiting 直至TIMEOUT,导致事务一致性被破坏。

设计使用Mycat时如果有分布式事务,得先看是否得保证事务得强一致性,否则请考虑放弃!

更多分享,敬请关注

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

构建基于Spark的推荐引擎(Python) 构建基于Spa

发表于 2017-12-20

构建基于Spark的推荐引擎(Python)

推荐引擎背后的想法是预测人们可能喜好的物品并通过探寻物品之间的联系来辅助这个过程

在学习Spark机器学习这本书时,书上用scala完成,自己不熟悉遂用pyshark完成,更深入的理解了spark对协同过滤的实现

在这里我们的推荐模型选用协同过滤这种类型,使用Spark的MLlib中推荐模型库中基于矩阵分解(matrix factorization)的实现。

协同过滤(Collaborative Filtering)

协同过滤简单来说是利用某兴趣相投、拥有共同经验之群体的喜好来推荐用户感兴趣的信息,个人通过合作的机制给予信息相当程度的回应(如评分)并记录下来以达到过滤的目的进而帮助别人筛选信息,回应不一定局限于特别感兴趣的,特别不感兴趣信息的纪录也相当重要。

很简单的例子来介绍就是日常我们生活中经常找电影会通过向和自己品味类似的朋友要求推荐,这就是协同过滤的思想

基于用户的协同过滤推荐机制的基本原理

基于用户或物品的方法的得分取决于若干用户或是物品之间依据相似度所构成的集合(即邻居),故它们也常被称为最邻近模型。

矩阵分解

这里我们要处理的数据是用户提供的自身偏好数据,即用户对物品的打分数据。

这些数据可以被转换成用户为行,物品为列的二维矩阵,即评分矩阵A(m*n)表示m个用户对n个物品的打分情况

UI i1 i2 i3
u1 3.0 3.0 ?
u2 ? 2.0 4.0
u3 ? 5.0 ?

这个矩阵A很多元素都是空的,我们称其为缺失值(missing value)。

协同过滤提出了一种支持不完整评分矩阵的矩阵分解方法,不用对评分矩阵进行估值填充。

在推荐系统中,我们希望得到用户对所有物品的打分情况,如果用户没有对一个物品打分,那么就需要预测用户是否会对该物品打分,以及会打多少分。这就是所谓的矩阵补全(矩阵分解)

对于这个矩阵分解的方式就是找出两个低维度的矩阵,使得他们的乘积是原始的矩阵。

因此这也是一种降维技术。要找到和‘用户-物品’矩阵近似的k维矩阵,最终要求得出表示用户的m x k维矩阵,和一个表示物品的k x n维矩阵。这两个矩阵也称作因子矩阵。

对于k的理解为对于每个产品,这里已电影为例,可以从k个角度进行评价,即k个特征值

由于是对‘用户-物品’矩阵直接建模,用这些模型进行预测也相对直接,要计算给定用户对某个物品的预计评级,就从用户因子矩阵和物品因子矩阵分别选取对应的行与列,然后计算两者的点积。

假设对于用户A,该用户对一部电影的综合评分和电影的特征值存在一定的线性关系,

即电影的综合评分=(a1d1+a2d2+a3d3+a4d4)

其中a1-4为用户A的特征值,d1-4为之前所说的电影的特征值

最小二乘法实现协同

最小二乘法(Alternating Least Squares, ALS)是一种求解矩阵分解问题的最优化方法。它功能强大、效果理想而且被证明相对容易并行化。这使得它很适合如Spark这样的平台。

使用用户特征矩阵$ U(m*k) $ 中的第i个用户的特征向量$ u_i $ ,

和产品特征矩阵$ V(n*k) $第j个物品的特征向量$ v_i $来预测打分矩阵$ A(m*n) $中的$ a_{ij} $,

得出矩阵分解模型的损失函数如下

? \large C = \sum\limits_{(i,j)\in R}[(a_{ij} - u_iv_j^T)^2+\lambda(u_i^2+v_j^2)] ?


通常的优化方法分为两种:交叉最小二乘法(alternative least squares)和随机梯度下降法(stochastic gradient descent)。Spark使用的是交叉最小二乘法(ALS)来最优化损失函数。
算法的思想就是:我们先随机生成然后固定它求解,再固定求解,这样交替进行下去,直到取得最优解$ min(C) $

使用PySpark实现

我们这里的数据集是Movielens 100k数据集,包含了多个用户对多部电影的10万次评级数据

下载地址

读取评级数据集,该数据包括用户ID,影片ID,星级和时间戳等字段,使用/t分隔

通过sc.textFile()读取数据为RDD,通过分隔符取前3个属性分别为用户ID,影片ID,星级

1
2
3
复制代码rawData = sc.textFile('/home/null/hadoop/data/ml-100k/u.data')
rawData.first()
type(rawData)
1
复制代码pyspark.rdd.RDD
1
2
复制代码rawRatings = rawData.map(lambda line: line.split("\t")[:3])
rawRatings.first()
1
复制代码['196', '242', '3']
1
2
复制代码# 导入spark中的mllib的推荐库
import pyspark.mllib.recommendation as rd

生成Rating类的RDD数据

1
2
3
复制代码# 由于ALS模型需要由Rating记录构成的RDD作为参数,因此这里用rd.Rating方法封装数据
ratings = rawRatings.map(lambda line: rd.Rating(int(line[0]), int(line[1]), float(line[2])))
ratings.first()
1
复制代码Rating(user=196, product=242, rating=3.0)

训练ALS模型

  • rank: 对应ALS模型中的因子个数,即矩阵分解出的两个矩阵的新的行/列数,即$ A \approx UV^T , k << m,n $m,n中的k
  • iterations: 对应运行时的最大迭代次数
  • lambda: 控制模型的正则化过程,从而控制模型的过拟合情况。
1
2
3
复制代码# 训练ALS模型
model = rd.ALS.train(ratings, 50, 10, 0.01)
model
1
复制代码<pyspark.mllib.recommendation.MatrixFactorizationModel at 0x7f53cc29c710>
1
2
3
复制代码# 对用户789预测其对电影123的评级
predictedRating = model.predict(789,123)
predictedRating
1
复制代码3.1740832151065774
1
2
3
复制代码# 获取对用户789的前10推荐
topKRecs = model.recommendProducts(789,10)
topKRecs
1
2
3
4
5
6
7
8
9
10
复制代码[Rating(user=789, product=429, rating=6.302989890089658),
Rating(user=789, product=496, rating=5.782039583864358),
Rating(user=789, product=651, rating=5.665266358968961),
Rating(user=789, product=250, rating=5.551256887914674),
Rating(user=789, product=64, rating=5.5336980239740186),
Rating(user=789, product=603, rating=5.468600343790217),
Rating(user=789, product=317, rating=5.442052952711695),
Rating(user=789, product=480, rating=5.414042111530209),
Rating(user=789, product=180, rating=5.413309515550101),
Rating(user=789, product=443, rating=5.400024900653429)]

检查推荐内容

这里首先将电影的数据读入,讲数据处理为电影ID到标题的映射

然后获取某个用户评级前10的影片同推荐这个用户的前10影片进行比较

1
2
3
复制代码#检查推荐内容
movies = sc.textFile('/home/null/hadoop/data/ml-100k/u.item')
movies.first()
1
复制代码'1|Toy Story (1995)|01-Jan-1995||http://us.imdb.com/M/title-exact?Toy%20Story%20(1995)|0|0|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0'
1
2
3
复制代码titles_data= movies.map(lambda line: line.split("|")[:2]).collect()
titles = dict(titles_data)
titles
1
2
复制代码moviesForUser = ratings.keyBy(lambda rating: rating.user).lookup(789)
type(moviesForUser)
1
复制代码list
1
2
复制代码moviesForUser = sorted(moviesForUser,key=lambda r: r.rating, reverse=True)[0:10]
moviesForUser
1
2
3
4
5
6
7
8
9
10
复制代码[Rating(user=789, product=127, rating=5.0),
Rating(user=789, product=475, rating=5.0),
Rating(user=789, product=9, rating=5.0),
Rating(user=789, product=50, rating=5.0),
Rating(user=789, product=150, rating=5.0),
Rating(user=789, product=276, rating=5.0),
Rating(user=789, product=129, rating=5.0),
Rating(user=789, product=100, rating=5.0),
Rating(user=789, product=741, rating=5.0),
Rating(user=789, product=1012, rating=4.0)]
1
复制代码[(titles[str(r.product)], r.rating) for r in moviesForUser]
1
2
3
4
5
6
7
8
9
10
复制代码[('Godfather, The (1972)', 5.0),
('Trainspotting (1996)', 5.0),
('Dead Man Walking (1995)', 5.0),
('Star Wars (1977)', 5.0),
('Swingers (1996)', 5.0),
('Leaving Las Vegas (1995)', 5.0),
('Bound (1996)', 5.0),
('Fargo (1996)', 5.0),
('Last Supper, The (1995)', 5.0),
('Private Parts (1997)', 4.0)]
1
复制代码[(titles[str(r.product)], r.rating) for r in topKRecs]
1
2
3
4
5
6
7
8
9
10
复制代码[('Day the Earth Stood Still, The (1951)', 6.302989890089658),
("It's a Wonderful Life (1946)", 5.782039583864358),
('Glory (1989)', 5.665266358968961),
('Fifth Element, The (1997)', 5.551256887914674),
('Shawshank Redemption, The (1994)', 5.5336980239740186),
('Rear Window (1954)', 5.468600343790217),
('In the Name of the Father (1993)', 5.442052952711695),
('North by Northwest (1959)', 5.414042111530209),
('Apocalypse Now (1979)', 5.413309515550101),
('Birds, The (1963)', 5.400024900653429)]

推荐模型效果的评估

均方差(Mean Squared Error,MSE)

定义为各平方误差的和与总数目的商,其中平方误差是指预测到的评级与真实评级的差值平方

直接度量模型的预测目标变量的好坏

均方根误差(Root Mean Squared Error,RMSE)

对MSE取其平方根,即预计评级和实际评级的差值的标准差

1
2
3
4
复制代码# evaluation metric
usersProducts = ratings.map(lambda r:(r.user, r.product))
predictions = model.predictAll(usersProducts).map(lambda r: ((r.user, r.product),r.rating))
predictions.first()
1
复制代码((316, 1084), 4.006135662882842)
1
2
复制代码ratingsAndPredictions = ratings.map(lambda r: ((r.user,r.product), r.rating)).join(predictions)
ratingsAndPredictions.first()
1
复制代码((186, 302), (3.0, 2.7544572973050236))
1
2
3
4
复制代码# 使用MLlib内置的评估函数计算MSE,RMSE
from pyspark.mllib.evaluation import RegressionMetrics
predictionsAndTrue = ratingsAndPredictions.map(lambda line: (line[1][0],line[1][3]))
predictionsAndTrue.first()
1
复制代码(3.0, 2.7544572973050236)
1
2
3
复制代码# MSE
regressionMetrics = RegressionMetrics(predictionsAndTrue)
regressionMetrics.meanSquaredError
1
复制代码0.08509832708963357
1
2
复制代码# RMSE
regressionMetrics.rootMeanSquaredError
1
复制代码0.2917161755707653

参考:

  • 深入理解Spark ML:基于ALS矩阵分解的协同过滤算法与源码分析
  • 如何解释spark mllib中ALS算法的原理?——知乎
  • Maching Learning With Spark——人民邮电出版社

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

彻底搞懂Java内存泄露

发表于 2017-12-20

简书 编程之乐
转载请注明原创出处,谢谢!

Java内存回收方式

Java判断对象是否可以回收使用的而是可达性分析算法。

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

Paste_Image.png

在Java语言里,可作为GC Roots对象的包括如下几种:

1
2
3
4
复制代码a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象

摘自《深入理解Java虚拟机》

使用leakcanary检测泄漏

关于LeakCanary使用参考以下文章:

  1. LeakCanary: 让内存泄露无所遁形
  2. LeakCanary 中文使用说明

LeakCanary的内存泄露提示一般会包含三个部分:
第一部分(LeakSingle类的sInstance变量)
引用第二部分(LeakSingle类的mContext变量),
导致第三部分(MainActivity类的实例instance)泄露.

Paste_Image.png

leakcanary使用注意

即使是空的Activity,如果检测泄露时候遇到了如下这样的泄露,注意,把refWatcher.watct()放在onDestroy里面即可解决,或者忽略这样的提示。
由于文章已写很多,下面的就不再修改,忽略这种错误即可。

1
2
3
4
5
6
7
复制代码* com.less.demo.TestActivity has leaked:
* GC ROOT static android.app.ActivityThread.sCurrentActivityThread
* references android.app.ActivityThread.mActivities
* references android.util.ArrayMap.mArray
* references array java.lang.Object[].[1]
* references android.app.ActivityThread$ActivityClientRecord.activity
* leaks com.less.demo.TestActivity instance
1
2
3
4
5
复制代码protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}

leakcanary和代码示例说明内存泄露

案例一(静态成员引起的内存泄露)

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class App extends Application {
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
App application = (App) context.getApplicationContext();
return application.refWatcher;
}
}

测试内部类持有外部类引用,内部类是静态的(GC-ROOT,将一直连着这个外部类实例),静态的会和Application一个生命周期,这会导致一直持有外部类引用(内部类隐含了一个成员变量$0), 即使外部类制空= null,也无法释放。

OutterClass

1
2
3
4
5
6
7
8
复制代码public class OutterClass {
private String name;
class Inner{
public void list(){
System.out.println("outter name is " + name);
}
}
}

TestActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class TestActivity extends Activity {
// 静态的内部类实例
private static OutterClass.Inner innerClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
OutterClass outterClass = new OutterClass();
innerClass = outterClass.new Inner();
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(outterClass);// 监控的对象
outterClass = null;
}

Paste_Image.png

案例二(单例模式引起的内存泄露)

DownloadManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public class DownloadManager {
private static DownloadManager instance;
private Task task ;
public static DownloadManager getInstance(){
if (instance == null) {
instance = new DownloadManager();
}
return instance;
}
public Task newTask(){
this.task = new Task();
return task;
}
}

Task

1
2
3
4
5
6
7
复制代码public class Task {
private Call call;
public Call newCall(){
this.call = new Call();
return call;
}
}

Call

1
2
3
4
5
复制代码public class Call {
public void execute(){
System.out.println("=========> execute call");
}
}

TestActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class TestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
RefWatcher refWatcher = App.getRefWatcher(this);
Task task = DownloadManager.getInstance().newTask();
Call call = task.newCall();
call.execute();
refWatcher.watch(call);// 监控的对象
call = null; // 无法回收,DownloadManager是静态单例,引用task,task引用了call,即使call置为空,也无法回收,切断GC_ROOT 联系即可避免内存泄露,即置task为空。
}
}

Paste_Image.png

部分日志打印如下:当前的GC_ROOT是DownloadManager的instance实例。

1
2
3
4
5
6
复制代码 In com.leakcanary.demo:1.0:1.
* com.less.demo.Call has leaked:
* GC ROOT static com.less.demo.DownloadManager.instance
* references com.less.demo.DownloadManager.task
* references com.less.demo.Task.call
* leaks com.less.demo.Call instance

关于上面两种方式导致的内存泄露问题,这里再举两个案例说明以加强理解。

案例三(静态变量导致的内存泄露)

1
2
3
4
5
6
7
8
9
10
复制代码public class TestActivity extends Activity {
private static Context sContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
sContext = this;
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);// 监控的对象
}

Paste_Image.png

打印日志如下:

1
2
3
4
复制代码 In com.leakcanary.demo:1.0:1.
com.less.demo.TestActivity has leaked:
GC ROOT static com.less.demo.TestActivity.sContext
leaks com.less.demo.TestActivity instance

从这段日志可以分析出:声明static后,sContext的生命周期将和Application一样长,Activity即使退出到桌面,Application依然存在->sContext依然存在,GC此时想回收Activity却发现Activity仍然被sContext(GC-ROOT连接着),导致死活回收不了,内存泄露。

上面的代码改造一下,如下。

1
2
3
4
5
6
7
8
9
10
11
复制代码public class TestActivity extends Activity {
private static View sView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
sView = new View(this);
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}
}

Paste_Image.png
日志如下

1
2
3
4
5
复制代码In com.leakcanary.demo:1.0:1.
com.less.demo.TestActivity has leaked:
GC ROOT static com.less.demo.TestActivity.sView
references android.view.View.mContext
leaks com.less.demo.TestActivity instance

案例四(单例模式导致的内存泄露)
DownloadManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码public class DownloadManager {
private static DownloadManager instance;
private List<DownloadListener> mListeners = new ArrayList<>();
public interface DownloadListener {
void done();
}
public static DownloadManager getInstance(){
if (instance == null) {
instance = new DownloadManager();
}
return instance;
}
public void register(DownloadListener downloadListener){
if (!mListeners.contains(downloadListener)) {
mListeners.add(downloadListener);
}
}
public void unregister(DownloadListener downloadListener){
if (mListeners.contains(downloadListener)) {
mListeners.remove(downloadListener);
}
}
}

TestActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class TestActivity extends Activity implements DownloadManager.DownloadListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
DownloadManager.getInstance().register(this);
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 忘记 unregister
// DownloadManager.getInstance().unregister(this);
}
@Override
public void done() {
System.out.println("done!");
}
}

Paste_Image.png

1
2
3
4
5
6
7
复制代码 In com.leakcanary.demo:1.0:1.
* com.less.demo.TestActivity has leaked:
* GC ROOT static com.less.demo.DownloadManager.instance
* references com.less.demo.DownloadManager.mListeners
* references java.util.ArrayList.array
* references array java.lang.Object[].[0]
* leaks com.less.demo.TestActivity instance

错误写法一定导致内存泄露吗?

答案是:否定的。
如下案例,有限时间内是可以挽救内存泄露发生的,所以控制好生命周期,其他情况:如单例对象(静态变量)的生命周期比其持有的sContext
的生命周期更长时 ->内存泄露,更短时->躲过内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public class TestActivity extends Activity {
private static Context sContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
sContext = this;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
sContext = null;
}
},1000);// 分别测试1s和12s,前者不会内存泄露,后者一定泄露。所以如果赶在GC之前切断GC_ROOT是可以避免内存泄露的。
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}
}

Handler 引起的内存泄露详细分析

handler导致的内存泄露可能我们大多数都犯过.
注意以下代码中的注释,非静态内部类虽然持有外部类引用,但是持有并不代表一定泄露,而是看gc时谁的命长。经过测试 情况(1)始终没有内存泄露。

为什么会这样, 很早阅读Handler源码时候记得这几个货都是互相引用来引用去的,Message有个target字段, message.target = handler;
handler.post(message);又把这个message推入了MessageQueue中,而MessageQueue是在一个Looper线程中不断轮询处理消息,而有时候message还是个老不死,能够重复利用。如果当Activity退出时候,还有消息未处理或正在处理,由于message引用handler,handler又引用Activity,此时将引发内存泄露。

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
复制代码public class TestActivity extends Activity {
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
System.out.println("===== handle message ====");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
// (1) 不会导致内存泄露
handler.sendEmptyMessageDelayed(0x123,0);
// (2) 会导致内存泄露,非静态内部类(包括匿名内部类,比如这个 Handler 匿名内部类)会引用外部类对象 this(比如 Activity)
// 当它使用了 postDelayed 的时候,如果 Activity 已经 finish 了,而这个 handler 仍然引用着这个 Activity 就会致使内存泄漏
// 因为这个 handler 会在一段时间内继续被 main Looper 持有,导致引用仍然存在.
handler.sendEmptyMessageDelayed(0x123, 12000);
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}
}

Paste_Image.png

1
2
3
4
5
6
7
复制代码com.less.demo.TestActivity has leaked:
* GC ROOT android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper.mH
* references com.android.internal.view.IInputConnectionWrapper$MyHandler.mQueue
* references android.os.MessageQueue.mMessages
* references android.os.Message.target , matching exclusion field android.os.Message#target
* references com.less.demo.TestActivity$1.this$0 (anonymous subclass of android.os.Handler)
* leaks com.less.demo.TestActivity instance

知道了原理后,即使写出易于内存泄露的代码也能够避免内存泄露啦。
上述代码只需在onDestroy()函数中调用mHandler.removeCallbacksAndMessages(null);
在Activity退出的时候的移除消息队列中所有消息和所有的Runnable。

内部类引起的内存泄露

内部类种类大致如下:

  1. 非静态内部类(成员内部类)
  2. 静态内部类(嵌套内部类)
  3. 局部内部类(定义在方法内或者作用域内的类,好似局部变量,所以不能有访问控制符和static等修饰)
  4. 匿名内部类(没有名字,仅使用一次new个对象即扔掉类的定义)

为什么非静态内部类持有外部类引用,静态内部类不持有外部引用。

这个问题非常简单,就像 static的方法只能调用static的东西,非static可以调用非static和static的一样。static–> 针对class, 非static->针对 对象,我是这么简单理解的。看图:

Paste_Image.png

匿名内部类
将局部内部类的使用再深入一步,假如只创建某个局部内部类的一个对象,就不必命名了。

匿名内部类的类型可以是如下几种方式。

  1. 接口匿名内部类
  2. 抽象类匿名内部类
  3. 类匿名内部类

匿名内部类总结:

  1. 其实主要就是类定义一次就失效了,主要使用的是这个类(不知道名字)的实例。根据内部类的特性,能够实现回调和闭包。
  2. JavaScript和Python的回调传递的是fuction,Java传递的是object。
    Java中常常用到回调,而回调的本质就是传递一个对象,JavaScript或其他语言则是传递一个函数(如Python,或者C,使用函数指针的方式),由于传递一个对象可以携带其他的一些信息,所以Java认为传递一个对象比传递一个函数要灵活的多(当然java也可以用Method反射对象传递函数)。参考《Java核心技术》

非静态内部类导致内存泄露(前提dog的命长)
下面的案例将导致内存泄露
其中private static Dog dog ; 导致Dog的命比TestActivity长,这就糟糕了,但是注意,如果改为private Dog dog ; 即使Dog是非静态内部类,也不会导致内存泄露,要死也是Dog先死,毕竟Dog是TestActivity的家庭成员,开挂也得看主人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class TestActivity extends Activity {
private static Dog dog ;
class Dog {
public void say(){
System.out.println("I am lovely dog!");
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
dog = new Dog();
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}
}

Paste_Image.png

1
2
3
4
5
复制代码In com.leakcanary.demo:1.0:1.
* com.less.demo.TestActivity has leaked:
* GC ROOT static com.less.demo.TestActivity.dog
* references com.less.demo.TestActivity$Dog.this$0
* leaks com.less.demo.TestActivity instance

哪些内部类或者回调函数是否持有外部类对象?
一个反射案例说明一切

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
78
79
80
81
82
83
复制代码/**
* 作者: limitless
* 描述: 一个案例测试所有类型内部类对外部类对象的持有情况
* 网站: https://github.com/wangli0
*/
public class Main {
/* 持有外部类引用 */
private IAppListener mAppListener = new IAppListener() {
private String name;
@Override
public void done() {
System.out.println("匿名内部类对象作为成员变量");
}
};
/* 未持有 */
private static IAppListener sAppListener = new IAppListener() {
private String name;
@Override
public void done() {
System.out.println("匿名内部类对象作为static成员变量");
}
};
public static void main(String args[]) {
Main main = new Main();
main.test1();
main.test2();
main.test3();// test3 《=》test4
main.test4();
main.test5();
main.test6();
}
class Dog {
private String name;
}
/* 持有外部类引用 */
public void test1(){
Dog dog = new Dog();
getAllFieldName(dog.getClass());
}
static class Cat {
private String name;
}
/* 未持有 */
private void test2() {
Cat cat = new Cat();
getAllFieldName(cat.getClass());
}
/* 持有外部类引用 */
private void test3() {
class Monkey{
String name;
}
Monkey monkey = new Monkey();
getAllFieldName(monkey.getClass());
}
/* 持有外部类引用 */
private void test4() {
// 常用作事件回调的地方(有可能引起内存泄露)
IAppListener iAppListener = new IAppListener() {
private String name;
@Override
public void done() {
System.out.println("匿名内部类");
}
};
getAllFieldName(iAppListener.getClass());
}
/* 持有外部类引用 */
private void test5() {
getAllFieldName(mAppListener.getClass());
}
/* 未持有 */
private void test6() {
getAllFieldName(sAppListener.getClass());
}
private void getAllFieldName(Class<?> clazz) {
System.out.println("className: ======> " + clazz.getSimpleName());
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
}
}

Paste_Image.png

上述结果足够说明,即使是方法内的回调对象也是持有外部类引用的,那么虽然作用域是局部的,也有存在内存泄露的可能。我多次强调 持有某对象 不代表一定泄露,看的是谁命长。回调在Android开发过程中几乎处处可见,如果持有就会内存泄露的话,那几乎就没法玩了。
一般情况下,我们常常设置某个方法内的xx.execute(new Listener(){xx});是不会导致内存泄露的,这个方法执行完,局部作用域就失效了。但是如果在execute(listener);过程中,某个单例模式的对象
突然引用了这个listener对象,那么就会导致内存泄露。

下面用实例证明我的想法
TestActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class TestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
Task task = new Task();
task.execute(new ICallback() {
@Override
public void done() {
System.out.println("下载完成!");
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}
}

Task

1
2
3
4
5
复制代码public class Task {
public void execute(ICallback iCallback) {
DownloadManager.getInstance().execute(iCallback);
}
}

DownloadManager

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class DownloadManager {
public static DownloadManager instance;
private ICallback mICallback;
public static DownloadManager getInstance(){
if (instance == null) {
instance = new DownloadManager();
}
return instance;
}
public void execute(ICallback iCallback) {
this.mICallback = iCallback;// 反例,千万不要这么做,将导致内存泄露,如果注释掉这行,内存泄露不会发生
iCallback.done();
}

Paste_Image.png

这足以证明我的想法是正确的,Callback的不巧当使用同样会导致内存泄露 的发送。

总结

  1. 如果某些单例需要使用到Context对象,推荐使用Application的context,不要使用Activity的context,否则容易导致内存泄露。单例对象的生命周期和Application一致,这样Application和单例对象就一起销毁。
  2. 优先使用静态内部类而不是非静态的,因为非静态内部类持有外部类引用可能导致垃圾回收失败。如果你的静态内部类需要宿主Activity的引用来执行某些东西,你要将这个引用封装在一个WeakReference中,避免意外导致Activity泄露,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收 只被弱引用关联 的对象,只被 说明这个对象本身已经没有用处了。
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
复制代码public class TestActivity extends Activity {
private MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);

}
static class MyHandler extends Handler {
private WeakReference<Activity> mWeakReference;
public MyHandler(Activity activity){
mWeakReference = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Toast.makeText(mWeakReference.get(), "xxxx", Toast.LENGTH_LONG).show();
Log.d("xx", mWeakReference.get().getPackageName());
}
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(this);
refWatcher.watch(this);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

大数据查询——HBase读写设计与实践

发表于 2017-12-20

背景介绍

本项目主要解决 check 和 opinion2 张历史数据表(历史数据是指当业务发生过程中的完整中间流程和结果数据)的在线查询。原实现基于 Oracle 提供存储查询服务,随着数据量的不断增加,在写入和读取过程中面临性能问题,且历史数据仅供业务查询参考,并不影响实际流程,从系统结构上来说,放在业务链条上游比较重。本项目将其置于下游数据处理 Hadoop 分布式平台来实现此需求。下面列一些具体的需求指标:

  1. 数据量:目前 check 表的累计数据量为 5000w+ 行,11GB;opinion 表的累计数据量为 3 亿 +,约 100GB。每日增量约为每张表 50 万 + 行,只做 insert,不做 update。
  2. 查询要求:check 表的主键为 id(Oracle 全局 id),查询键为 check_id,一个 check_id 对应多条记录,所以需返回对应记录的 list; opinion 表的主键也是 id,查询键是 bussiness_no 和 buss_type,同理返回 list。单笔查询返回 List 大小约 50 条以下,查询频率为 100 笔 / 天左右,查询响应时间 2s。

技术选型

从数据量及查询要求来看,分布式平台上具备大数据量存储,且提供实时查询能力的组件首选 HBase。根据需求做了初步的调研和评估后,大致确定 HBase 作为主要存储组件。将需求拆解为写入和读取 HBase 两部分。

读取 HBase 相对来说方案比较确定,基本根据需求设计 RowKey,然后根据 HBase 提供的丰富 API(get,scan 等)来读取数据,满足性能要求即可。

写入 HBase 的方法大致有以下几种:

  1. Java 调用 HBase 原生 API,HTable.add(List(Put))。
  2. MapReduce 作业,使用 TableOutputFormat 作为输出。
  3. Bulk Load,先将数据按照 HBase 的内部数据格式生成持久化的 HFile 文件,然后复制到合适的位置并通知 RegionServer ,即完成海量数据的入库。其中生成 Hfile 这一步可以选择 MapReduce 或 Spark。

本文采用第 3 种方式,Spark + Bulk Load 写入 HBase。该方法相对其他 2 种方式有以下优势:

  1. BulkLoad 不会写 WAL,也不会产生 flush 以及 split。
  2. 如果我们大量调用 PUT 接口插入数据,可能会导致大量的 GC 操作。除了影响性能之外,严重时甚至可能会对 HBase 节点的稳定性造成影响,采用 BulkLoad 无此顾虑。
  3. 过程中没有大量的接口调用消耗性能。
  4. 可以利用 Spark 强大的计算能力。

图示如下:

设计

环境信息

1
2
3
4
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__Hadoop 2.5-2.7
HBase 0.98.6
Spark 2.0.0-2.1.1
Sqoop 1.4.6__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

表设计

本段的重点在于讨论 HBase 表的设计,其中 RowKey 是最重要的部分。为了方便说明问题,我们先来看看数据格式。以下以 check 举例,opinion 同理。

check 表(原表字段有 18 个,为方便描述,本文截选 5 个字段示意)

)

如上图所示,主键为 id,32 位字母和数字随机组成,业务查询字段 check_id 为不定长字段(不超过 32 位),字母和数字组成,同一 check_id 可能对应多条记录,其他为相关业务字段。众所周知,HBase 是基于 RowKey 提供查询,且要求 RowKey 是唯一的。RowKey 的设计主要考虑的是数据将怎样被访问。初步来看,我们有 2 种设计方法。

  1. 拆成 2 张表,一张表 id 作为 RowKey,列为 check 表对应的各列;另一张表为索引表,RowKey 为 check_id,每一列对应一个 id。查询时,先找到 check_id 对应的 id list,然后根据 id 找到对应的记录。均为 HBase 的 get 操作。
  2. 将本需求可看成是一个范围查询,而不是单条查询。将 check_id 作为 RowKey 的前缀,后面跟 id。查询时设置 Scan 的 startRow 和 stopRow,找到对应的记录 list。

第一种方法优点是表结构简单,RowKey 容易设计,缺点为 1)数据写入时,一行原始数据需要写入到 2 张表,且索引表写入前需要先扫描该 RowKey 是否存在,如果存在,则加入一列,否则新建一行,2)读取的时候,即便是采用 List, 也至少需要读取 2 次表。第二种设计方法,RowKey 设计较为复杂,但是写入和读取都是一次性的。综合考虑,我们采用第二种设计方法。

RowKey 设计

热点问题

HBase 中的行是以 RowKey 的字典序排序的,其热点问题通常发生在大量的客户端直接访问集群的一个或极少数节点。默认情况下,在开始建表时,表只会有一个 region,并随着 region 增大而拆分成更多的 region,这些 region 才能分布在多个 regionserver 上从而使负载均分。对于我们的业务需求,存量数据已经较大,因此有必要在一开始就将 HBase 的负载均摊到每个 regionserver,即做 pre-split。常见的防治热点的方法为加盐,hash 散列,自增部分(如时间戳)翻转等。

RowKey 设计

Step1:确定预分区数目,创建 HBase Table

不同的业务场景及数据特点确定数目的方式不一样,我个人认为应该综合考虑数据量大小和集群大小等因素。比如 check 表大小约为 11G,测试集群大小为 10 台机器,hbase.hregion.max.filesize=3G(当 region 的大小超过这个数时,将拆分为 2 个),所以初始化时尽量使得一个 region 的大小为 1~2G(不会一上来就 split),region 数据分到 11G/2G=6 个,但为了充分利用集群资源,本文中 check 表划分为 10 个分区。如果数据量为
100G,且不断增长,集群情况不变,则 region 数目增大到 100G/2G=50 个左右较合适。Hbase check 表建表语句如下:

1
2
3
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__create 'tinawang:check',
{ NAME => 'f', COMPRESSION => 'SNAPPY',DATA_BLOCK_ENCODING => 'FAST_DIFF',BLOOMFILTER=>'ROW'},
{SPLITS => [ '1','2','3', '4','5','6','7','8','9']}__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

其中,Column Family =‘f’,越短越好。

COMPRESSION => ‘SNAPPY’,HBase 支持 3 种压缩 LZO, GZIP and Snappy。GZIP 压缩率高,但是耗 CPU。后两者差不多,Snappy 稍微胜出一点,cpu 消耗的比 GZIP 少。一般在 IO 和 CPU 均衡下,选择 Snappy。

DATA_BLOCK_ENCODING => ‘FAST_DIFF’,本案例中 RowKey 较为接近,通过以下命令查看 key 长度相对 value 较长。

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__./hbase org.apache.hadoop.hbase.io.hfile.HFile -m -f /apps/hbase/data/data/tinawang/check/a661f0f95598662a53b3d8b1ae469fdf/f/a5fefc880f87492d908672e1634f2eed_SeqId_2___Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

)

Step2:RowKey 组成

Salt

让数据均衡的分布到各个 Region 上,结合 pre-split,我们对查询键即 check 表的 check_id 求 hashcode 值,然后 modulus(numRegions) 作为前缀,注意补齐数据。

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__StringUtils.leftPad(Integer.toString(Math.abs(check_id.hashCode() % numRegion)),1,’0’) __Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

说明:如果数据量达上百 G 以上,则 numRegions 自然到 2 位数,则 salt 也为 2 位。

Hash 散列

因为 check_id 本身是不定长的字符数字串,为使数据散列化,方便 RowKey 查询和比较,我们对 check_id 采用 SHA1 散列化,并使之 32 位定长化。

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__MD5Hash.getMD5AsHex(Bytes.toBytes(check_id))__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

唯一性

以上 salt+hash 作为 RowKey 前缀,加上 check 表的主键 id 来保障 RowKey 唯一性。综上,check 表的 RowKey 设计如下:(check_id=A208849559)

为增强可读性,中间还可以加上自定义的分割符,如’+’,’|’等。

7+7c9498b4a83974da56b252122b9752bf+56B63AB98C2E00B4E053C501380709AD
以上设计能保证对每次查询而言,其 salt+hash 前缀值是确定的,并且落在同一个 region 中。需要说明的是 HBase 中 check 表的各列同数据源 Oracle 中 check 表的各列存储。

WEB 查询设计

RowKey 设计与查询息息相关,查询方式决定 RowKey 设计,反之基于以上 RowKey 设计,查询时通过设置 Scan 的 [startRow,stopRow], 即可完成扫描。以查询 check_id=A208849559 为例,根据 RowKey 的设计原则,对其进行 salt+hash 计算,得前缀。

1
2
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__startRow = 7+7c9498b4a83974da56b252122b9752bf
stopRow = 7+7c9498b4a83974da56b252122b9752bg__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

代码实现关键流程

Spark write to HBase

Step0: prepare work

因为是从上游系统承接的业务数据,存量数据采用 sqoop 抽到 hdfs;增量数据每日以文件的形式从 ftp 站点获取。因为业务数据字段中包含一些换行符,且 sqoop1.4.6 目前只支持单字节,所以本文选择’0x01’作为列分隔符,’0x10’作为行分隔符。

Step1: Spark read hdfs text file

SparkContext.textfile() 默认行分隔符为”\n”,此处我们用“0x10”,需要在 Configuration 中配置。应用配置,我们调用 newAPIHadoopFile 方法来读取 hdfs 文件,返回 JavaPairRDD,其中 LongWritable 和 Text 分别为 Hadoop 中的 Long 类型和 String 类型(所有 Hadoop 数据类型和 java 的数据类型都很相像,除了它们是针对网络序列化而做的特殊优化)。我们需要的数据文件放在 pairRDD
的 value 中,即 Text 指代。为后续处理方便,可将 JavaPairRDD转换为 JavaRDD< String >。

Step2: Transfer and sort RDD

① 将 avaRDD< String>转换成 JavaPairRDD<tuple2,String>,其中参数依次表示为,RowKey,col,value。做这样转换是因为 HBase 的基本原理是基于 RowKey 排序的,并且当采用 bulk load 方式将数据写入多个预分区(region)时,要求 Spark 各 partition 的数据是有序的,RowKey,column family(cf),col name 均需要有序。在本案例中因为只有一个列簇,所以将
RowKey 和 col name 组织出来为 Tuple2格式的 key。请注意原本数据库中的一行记录(n 个字段),此时会被拆成 n 行。

② 基于 JavaPairRDD<tuple2,String>进行 RowKey,col 的二次排序。如果不做排序,会报以下异常:

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__java.io.IOException: Added a key notlexically larger than previous key__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

③ 将数据组织成 HFile 要求的 JavaPairRDDhfileRDD。

Step3:create hfile and bulk load to HBase

①主要调用 saveAsNewAPIHadoopFile 方法:

1
2
3
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__hfileRdd.saveAsNewAPIHadoopFile(hfilePath,ImmutableBytesWritable.class,
KeyValue.class,HFileOutputFormat2.class,config);
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

② hfilebulk load to HBase

1
2
3
4
5
6
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__final Job job = Job.getInstance();
job.setMapOutputKeyClass(ImmutableBytesWritable.class);
job.setMapOutputValueClass(KeyValue.class);
HFileOutputFormat2.configureIncrementalLoad(job,htable);
LoadIncrementalHFiles bulkLoader = newLoadIncrementalHFiles(config);
bulkLoader.doBulkLoad(newPath(hfilePath),htable);__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

注:如果集群开启了 kerberos,step4 需要放置在 ugi.doAs()方法中,在进行如下验证后实现

1
2
3
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(keyUser,keytabPath);
UserGroupInformation.setLoginUser(ugi);
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

访问 HBase 集群的 60010 端口 web,可以看到 region 分布情况。

Read from HBase

本文基于 spring boot 框架来开发 web 端访问 HBase 内数据。

use connection pool(使用连接池)

创建连接是一个比较重的操作,在实际 HBase 工程中,我们引入连接池来共享 zk 连接,meta 信息缓存,region server 和 master 的连接。

1
2
3
4
5
6
7
8
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__HConnection connection = HConnectionManager.createConnection(config);
HTableInterface table = connection.getTable("table1");
try {
// Use the table as needed, for a single operation and a single thread
} finally {
table.close();
}
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

也可以通过以下方法,覆盖默认线程池。

1
2
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__HConnection createConnection(org.apache.hadoop.conf.Configuration conf,ExecutorService pool);
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

process query

Step1: 根据查询条件,确定 RowKey 前缀

根据 3.3 RowKey 设计介绍,HBase 的写和读都遵循该设计规则。此处我们采用相同的方法,将 web 调用方传入的查询条件,转化成对应的 RowKey 前缀。例如,查询 check 表传递过来的 check_id=A208849559,生成前缀 7+7c9498b4a83974da56b252122b9752bf。

Step2:确定 scan 范围

A208849559 对应的查询结果数据即在 RowKey 前缀为 7+7c9498b4a83974da56b252122b9752bf 对应的 RowKey 及 value 中。

1
2
3
4
5
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__scan.setStartRow(Bytes.toBytes(rowkey_pre)); //scan, 7+7c9498b4a83974da56b252122b9752bf
byte[] stopRow = Bytes.toBytes(rowkey_pre);
stopRow[stopRow.length-1]++;
scan.setStopRow(stopRow);// 7+7c9498b4a83974da56b252122b9752bg
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

Step3:查询结果组成返回对象

遍历 ResultScanner 对象,将每一行对应的数据封装成 table entity,组成 list 返回。

测试

从原始数据中随机抓取 1000 个 check_id,用于模拟测试,连续发起 3 次请求数为 2000(200 个线程并发,循环 10 次),平均响应时间为 51ms,错误率为 0。

)

)

如上图,经历 N 次累计测试后,各个 region 上的 Requests 数较为接近,符合负载均衡设计之初。

踩坑记录

1、kerberos 认证问题

如果集群开启了安全认证,那么在进行 Spark 提交作业以及访问 HBase 时,均需要进行 kerberos 认证。

本文采用 yarn cluster 模式,像提交普通作业一样,可能会报以下错误。

1
2
3
4
5
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__ERROR StartApp: job failure,
java.lang.NullPointerException
at com.tinawang.spark.hbase.utils.HbaseKerberos.<init>(HbaseKerberos.java:18)
at com.tinawang.spark.hbase.job.SparkWriteHbaseJob.run(SparkWriteHbaseJob.java:60)
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

定位到 HbaseKerberos.java:18,代码如下:

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__this.keytabPath = (Thread.currentThread().getContextClassLoader().getResource(prop.getProperty("hbase.keytab"))).getPath();__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

这是因为 executor 在进行 HBase 连接时,需要重新认证,通过 –keytab 上传的 tina.keytab 并未被 HBase 认证程序块获取到,所以认证的 keytab 文件需要另外通过 –files 上传。示意如下

1
2
3
4
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__--keytab /path/tina.keytab \
--principal tina@GNUHPC.ORG \
--files "/path/tina.keytab.hbase"
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

其中 tina.keytab.hbase 是将 tina.keytab 复制并重命名而得。因为 Spark 不允许同一个文件重复上传。

2、序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__org.apache.spark.SparkException: Task not serializable
at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)
at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner?clean(ClosureCleaner.scala:288)
at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)
at org.apache.spark.SparkContext.clean(SparkContext.scala:2101)
at org.apache.spark.rdd.RDD?anonfun$map$1.apply(RDD.scala:370)
at org.apache.spark.rdd.RDD?anonfun$map$1.apply(RDD.scala:369)
...
org.apache.spark.deploy.yarn.ApplicationMaster?anon$2.run(ApplicationMaster.scala:637)
Caused by: java.io.NotSerializableException: org.apache.spark.api.java.JavaSparkContext
Serialization stack:
- object not serializable (class: org.apache.spark.api.java.JavaSparkContext, value: org.apache.spark.api.java.JavaSparkContext@24a16d8c)
- field (class: com.tinawang.spark.hbase.processor.SparkReadFileRDD, name: sc, type: class org.apache.spark.api.java.JavaSparkContext)
...
__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

解决方法一:

如果 sc 作为类的成员变量,在方法中被引用,则加 transient 关键字,使其不被序列化。

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__private transient JavaSparkContext sc;__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

解决方法二:

将 sc 作为方法参数传递,同时使涉及 RDD 操作的类 implements Serializable。 代码中采用第二种方法。详见代码。

3、批量请求测试

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__Exception in thread "http-nio-8091-Acceptor-0" java.lang.NoClassDefFoundError: org/apache/tomcat/util/ExceptionUtils__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

或者

1
复制代码__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__Exception in thread "http-nio-8091-exec-34" java.lang.NoClassDefFoundError: ch/qos/logback/classic/spi/ThrowableProxy__Wed Dec 20 2017 10:35:48 GMT+0800 (CST)____Wed Dec 20 2017 10:35:48 GMT+0800 (CST)__

查看下面 issue 以及一次排查问题的过程,可能是 open file 超过限制。

github.com/spring-proj…

mp.weixin.qq.com/s/34GVlaYDO…

使用 ulimit-a 查看每个用户默认打开的文件数为 1024。

在系统文件 /etc/security/limits.conf 中修改这个数量限制,在文件中加入以下内容, 即可解决问题。

  • soft nofile 65536
  • hard nofile 65536

作者介绍

汪婷,中国民生银行大数据开发工程师,专注于 Spark 大规模数据处理和 Hbase 系统设计。

参考文献

hbase.apache.org/book.html#p…
www.opencore.com/blog/2016/1…
hbasefly.com/2016/03/23/…
github.com/spring-proj…
mp.weixin.qq.com/s/34GVlaYDO…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SOAP与WSDL详解

发表于 2017-12-20

SOAP是我们Web Service中很常见的一个协议,SOAP确定了一种通过XML实现跨语言、跨机器传输调用的协议,WSDL更像是所提供服务的一个规范、一个文档,本篇文章介绍梳理一下他们的规则与逻辑,更好的认识一下SOAP协议及WSDL描述文件。

SOAP简单对象访问协议

SOAP(Simple Object Access Protocol)简单对象访问协议是交换数据的一种规范,在Web Service中,交换带结构信息。可基于HTTP等协议,使用XML格式传输,抽象于语言实现、平台和硬件。即多语言包括PHP、Java、.Net均可支持。

优点是跨语言,非常适合异步通信和针对松耦合的C/S,缺点是必须做很多运行时检查。

相关概念

  • SOAP封装(envelop),定义了一个框架,描述消息中的内容是什么,是谁发送的,谁应当接受并处理。
  • SOAP编码规则(encoding rules),定义了一种序列化的机制,表示应用程序需要使用的数据类型的实例。
  • SOAP RPC表示(RPC representation),定义了一个协定,用于表示远程过程调用和应答。
  • SOAP绑定(binding),定义了SOAP使用哪种协议交换信息。使用HTTP/TCP/UDP协议都可以。

基本结构

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码<?xml version="1.0"?>
<soap:Envelope
    xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
    soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
 
    <soap:Header>
      ...
      ...
    </soap:Header>
 
    <soap:Body>
          ...
          ...
          <soap:Fault>
            ...
            ...
          </soap:Fault>
    </soap:Body>
</soap:Envelope>

一条SOAP消息就是一个普通的XML文档,Envelope元素与Body元素(包含调用和响应信息)必须存在,Header元素(包含头部信息)和Fault元素(提供有关在处理此消息所发生的错误的信息)可以作为可选存在

SOAP Envelope元素

SOAP消息的根元素,可把XML文档定义为SOAP消息

命名空间

xmlns:SOAP命名空间,固定不变。

SOAP在默认命名空间中定义了3个属性:actor,mustUnderstand,encodingStyle。这些被定义在SOAP头部的属性可定义容器如何对SOAP消息进行处理。

  • mustUnderstand属性——用于标识标题项对其进行处理的接受者来说是强制的还是可选的。(0可选1强制)soap:mustUnderstand="0/1"
  • SOAP的actor属性可用于将Header元素寻址到一个特定的端点 soap:actor="URI"
  • SOAP的encodingStyle属性用于定义在文档中使用的数据类型。此属性可出现在任何SOAP元素中,并会被应用到元素的内容及元素的所有子元素上。SOAP消息没有默认的编码方式。soap:encodingstyle="URI"

SOAP Header元素

可选的SOAP Header元素可包含有关SOAP消息的应用程序专用信息。如果Header元素被提供,则它必须是Envelope元素的第一个子元素

1
2
3
4
5
复制代码<soap:Header>
   <m:Trans xmlns:m="http://www.w3schools.com/transaction/"
    soap:mustUnderstand="1">234 #表示处理此头部的接受者必须认可此元素,假如此元素接受者无法认可此元素,则在处理此头部时必须失效
   </m:Trans>
</soap:Heaser>

SOAP Body元素

必须的SOAP Body元素可包含打算传送到消息最终端点的实际SOAP消息。SOAP Body元素的直接子元素可以使合格的命名空间

SOAP Fault元素

用于存留SOAP消息的错误和状态消息,可选的SOAP Fault元素用于指示错误消息。如果已提供了Fault元素,则它必须是Body元素的子元素,在一条SOAP消息中,Fault元素只能出现一次。

SOAP Fault子元素:

  • 供识别障碍的代码
  • 可供人阅读的有关障碍的说明
  • 有关是谁引发故障的信息
  • 存留涉及Body元素的应用程序的专用错误信息

faultcode值描述:

  • versionMismatch SOAP Envelope的无效命名空间被发现
  • mustUnderstand Header元素的一个直接子元素(mustUnderstand=”1′)无法被理解
  • Client 消息被不正确的构成,或包含不正确的信息
  • Server 服务器有问题,因此无法处理进行下去

WSDL网络服务描述语言

WSDL(Web Services Description Language)网络服务描述语言,WSDL 是一种使用 XML 编写的文档。这种文档可描述某个 Web Service。

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码<definitions>
    <types>
       definition of types........
    </types>
    <message>
       definition of a message....
    </message>
    <portType>
       definition of a port.......
    </portType>
    <binding>
       definition of a binding....
    </binding>
</definitions>

一个WSDL文档通常包含7个重要的元素,即types、import、message、portType、operation、binding、service元素。这些元素嵌套在definitions元素中,definitions是WSDL文档的根元素。

特定实例剖析

以盛付通的一个接口为例,介绍一下整个wsdl描述文件,网址如下http://cardpay.shengpay.com/api-acquire-channel/services/receiveOrderService?wsdl

Types

数据类型定义的容器,它使用某种类型系统(一般地使用XML Schema中的类型系统)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码<xs:element name="receB2COrderRequest" type="tns:ReceB2COrderRequest"/>  
<xs:element name="receB2COrderResponse" type="tns:ReceB2COrderResponse"/>
 
<xs:complexType name="ReceB2COrderRequest">
    <xs:sequence>
      <xs:element minOccurs="0" name="buyerContact" type="xs:string"/>  
      .......
    </xs:sequence>
</xs:complexType>  
 
<xs:complexType name="receiveB2COrder">
        <xs:sequence>
          <xs:element minOccurs="0" name="arg0" type="tns:ReceB2COrderRequest"/>
        </xs:sequence>
</xs:complexType>

Message

通信消息的数据结构的抽象类型化定义。使用Types所定义的类型来定义整个消息的数据结构。

1
2
3
复制代码<wsdl:message name="receiveB2COrder"> 
    <wsdl:part element="tns:receiveB2COrder" name="parameters"/>
  </wsdl:message>

Operation & PortType

Operation 对服务中所支持的操作的抽象描述,一般单个Operation描述了一个访问入口的请求/响应消息对。
PortType 对于某个访问入口点类型所支持的操作的抽象集合,这些操作可以由一个或多个服务访问点来支持。

1
2
3
4
5
6
7
复制代码<wsdl:portType name="ReceiveOrderAPI"> 
    <wsdl:operation name="receiveB2COrder">
      <wsdl:input message="tns:receiveB2COrder" name="receiveB2COrder"/>  
      <wsdl:output message="tns:receiveB2COrderResponse" name="receiveB2COrderResponse"/>  
      <wsdl:fault message="tns:MasAPIException" name="MasAPIException"/>
    </wsdl:operation>
  </wsdl:portType>

Binding

特定端口类型的具体协议和数据格式规范的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码<wsdl:binding name="ReceiveOrderAPIExplorterServiceSoapBinding" type="tns:ReceiveOrderAPI"> 
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>  
    <wsdl:operation name="receiveB2COrder">
      <soap:operation soapAction="" style="document"/>  
      <wsdl:input name="receiveB2COrder">
        <soap:body use="literal"/>
      </wsdl:input>  
      <wsdl:output name="receiveB2COrderResponse">
        <soap:body use="literal"/>
      </wsdl:output>  
      <wsdl:fault name="MasAPIException">
        <soap:fault name="MasAPIException" use="literal"/>
      </wsdl:fault>
    </wsdl:operation>
  </wsdl:binding>

Port&Service

Port 定义为协议/数据格式绑定与具体Web访问地址组合的单个服务访问点。
Service 相关服务访问点的集合。

1
2
3
4
5
复制代码<wsdl:service name="ReceiveOrderAPIExplorterService"> 
    <wsdl:port binding="tns:ReceiveOrderAPIExplorterServiceSoapBinding" name="ReceiveOrderAPIExplorterPort">
      <soap:address location="http://cardpay.shengpay.com/api-acquire-channel/services/receiveOrderService"/>
    </wsdl:port>
  </wsdl:service>

PHP操作示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码//soap版本为1.1,不缓存wsdl文件
$options = array(
    'trace'=>true,
    'cache_wsdl'=>WSDL_CACHE_NONE,
    'soap_version'=> SOAP_1_1
);
//上送参数
$request = array();//do something
 
//准备请求
$soapClient = new SoapClient($url, $options);
try {
    $response = $soapClient->__soapCall($function, array(array('arg0'=>$request)));
    if (is_object($response)) {
        $responseArray = get_object_vars($response);
        return $responseArray;
    }
} catch (SOAPFault $e) {
    //do something
} catch(Exception $e) {
    //do something
}

小结

附wsdl示例全文:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
复制代码<?xml version="1.0" encoding="utf-8"?>
 
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.sdo.com/mas/api/receive/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="ReceiveOrderAPIExplorterService" targetNamespace="http://www.sdo.com/mas/api/receive/">  
  <wsdl:types>
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="unqualified" elementFormDefault="unqualified" targetNamespace="http://www.sdo.com/mas/api/receive/">  
      <xs:element name="receB2COrderRequest" type="tns:ReceB2COrderRequest"/>  
      <xs:element name="receB2COrderResponse" type="tns:ReceB2COrderResponse"/>  
      <xs:complexType name="ReceB2COrderRequest">
        <xs:sequence>
          <xs:element minOccurs="0" name="buyerContact" type="xs:string"/>  
          <xs:element minOccurs="0" name="buyerId" type="xs:string"/>  
          <xs:element minOccurs="0" name="buyerIp" type="xs:string"/>  
          <xs:element minOccurs="0" name="buyerName" type="xs:string"/>  
          <xs:element minOccurs="0" name="cardPayInfo" type="xs:string"/>  
          <xs:element minOccurs="0" name="cardValue" type="xs:string"/>  
          <xs:element minOccurs="0" name="currency" type="xs:string"/>  
          <xs:element minOccurs="0" name="depositId" type="xs:string"/>  
          <xs:element minOccurs="0" name="depositIdType" type="xs:string"/>  
          <xs:element minOccurs="0" name="expireTime" type="xs:string"/>  
          <xs:element minOccurs="0" name="extension" type="tns:extension"/>  
          <xs:element minOccurs="0" name="header" type="tns:header"/>  
          <xs:element minOccurs="0" name="instCode" type="xs:string"/>  
          <xs:element minOccurs="0" name="language" type="xs:string"/>  
          <xs:element minOccurs="0" name="notifyUrl" type="xs:string"/>  
          <xs:element minOccurs="0" name="orderAmount" type="xs:string"/>  
          <xs:element minOccurs="0" name="orderNo" type="xs:string"/>  
          <xs:element minOccurs="0" name="orderTime" type="xs:string"/>  
          <xs:element minOccurs="0" name="pageUrl" type="xs:string"/>  
          <xs:element minOccurs="0" name="payChannel" type="xs:string"/>  
          <xs:element minOccurs="0" name="payType" type="xs:string"/>  
          <xs:element minOccurs="0" name="payeeId" type="xs:string"/>  
          <xs:element minOccurs="0" name="payerAuthTicket" type="xs:string"/>  
          <xs:element minOccurs="0" name="payerId" type="xs:string"/>  
          <xs:element minOccurs="0" name="payerMobileNo" type="xs:string"/>  
          <xs:element minOccurs="0" name="productDesc" type="xs:string"/>  
          <xs:element minOccurs="0" name="productId" type="xs:string"/>  
          <xs:element minOccurs="0" name="productName" type="xs:string"/>  
          <xs:element minOccurs="0" name="productNum" type="xs:string"/>  
          <xs:element minOccurs="0" name="productUrl" type="xs:string"/>  
          <xs:element minOccurs="0" name="sellerId" type="xs:string"/>  
          <xs:element minOccurs="0" name="signature" type="tns:signature"/>  
          <xs:element minOccurs="0" name="terminalType" type="xs:string"/>  
          <xs:element minOccurs="0" name="unitPrice" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:complexType name="extension">
        <xs:sequence>
          <xs:element minOccurs="0" name="ext1" type="xs:string"/>  
          <xs:element minOccurs="0" name="ext2" type="xs:string"/>  
          <xs:element minOccurs="0" name="ext3" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:complexType name="header">
        <xs:sequence>
          <xs:element minOccurs="0" name="charset" type="xs:string"/>  
          <xs:element minOccurs="0" name="sendTime" type="xs:string"/>  
          <xs:element minOccurs="0" name="sender" type="tns:sender"/>  
          <xs:element minOccurs="0" name="service" type="tns:service"/>  
          <xs:element minOccurs="0" name="traceNo" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:complexType name="sender">
        <xs:sequence>
          <xs:element minOccurs="0" name="senderId" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:complexType name="service">
        <xs:sequence>
          <xs:element minOccurs="0" name="serviceCode" type="xs:string"/>  
          <xs:element minOccurs="0" name="version" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:complexType name="signature">
        <xs:sequence>
          <xs:element minOccurs="0" name="signMsg" type="xs:string"/>  
          <xs:element minOccurs="0" name="signType" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:complexType name="ReceB2COrderResponse">
        <xs:sequence>
          <xs:element minOccurs="0" name="customerLogoUrl" type="xs:string"/>  
          <xs:element minOccurs="0" name="customerName" type="xs:string"/>  
          <xs:element minOccurs="0" name="customerNo" type="xs:string"/>  
          <xs:element minOccurs="0" name="extension" type="tns:extension"/>  
          <xs:element minOccurs="0" name="header" type="tns:header"/>  
          <xs:element minOccurs="0" name="orderAmount" type="xs:string"/>  
          <xs:element minOccurs="0" name="orderNo" type="xs:string"/>  
          <xs:element minOccurs="0" name="orderType" type="xs:string"/>  
          <xs:element minOccurs="0" name="returnInfo" type="tns:returnInfo"/>  
          <xs:element minOccurs="0" name="sessionId" type="xs:string"/>  
          <xs:element minOccurs="0" name="signature" type="tns:signature"/>  
          <xs:element minOccurs="0" name="tokenId" type="xs:string"/>  
          <xs:element minOccurs="0" name="transNo" type="xs:string"/>  
          <xs:element minOccurs="0" name="transStatus" type="xs:string"/>  
          <xs:element minOccurs="0" name="transTime" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:complexType name="returnInfo">
        <xs:sequence>
          <xs:element minOccurs="0" name="errorCode" type="xs:string"/>  
          <xs:element minOccurs="0" name="errorMsg" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:element name="MasAPIException" type="tns:MasAPIException"/>  
      <xs:complexType name="MasAPIException">
        <xs:sequence/>
      </xs:complexType>  
      <xs:element name="receiveB2COrder" type="tns:receiveB2COrder"/>  
      <xs:complexType name="receiveB2COrder">
        <xs:sequence>
          <xs:element minOccurs="0" name="arg0" type="tns:ReceB2COrderRequest"/>
        </xs:sequence>
      </xs:complexType>  
      <xs:element name="receiveB2COrderResponse" type="tns:receiveB2COrderResponse"/>  
      <xs:complexType name="receiveB2COrderResponse">
        <xs:sequence>
          <xs:element minOccurs="0" name="return" type="tns:ReceB2COrderResponse"/>
        </xs:sequence>
      </xs:complexType>
    </xs:schema>
  </wsdl:types>  
  <wsdl:message name="receiveB2COrder">
    <wsdl:part element="tns:receiveB2COrder" name="parameters"/>
  </wsdl:message>  
  <wsdl:message name="receiveB2COrderResponse">
    <wsdl:part element="tns:receiveB2COrderResponse" name="parameters"/>
  </wsdl:message>  
  <wsdl:message name="MasAPIException">
    <wsdl:part element="tns:MasAPIException" name="MasAPIException"/>
  </wsdl:message>  
  <wsdl:portType name="ReceiveOrderAPI">
    <wsdl:operation name="receiveB2COrder">
      <wsdl:input message="tns:receiveB2COrder" name="receiveB2COrder"/>  
      <wsdl:output message="tns:receiveB2COrderResponse" name="receiveB2COrderResponse"/>  
      <wsdl:fault message="tns:MasAPIException" name="MasAPIException"/>
    </wsdl:operation>
  </wsdl:portType>  
  <wsdl:binding name="ReceiveOrderAPIExplorterServiceSoapBinding" type="tns:ReceiveOrderAPI">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>  
    <wsdl:operation name="receiveB2COrder">
      <soap:operation soapAction="" style="document"/>  
      <wsdl:input name="receiveB2COrder">
        <soap:body use="literal"/>
      </wsdl:input>  
      <wsdl:output name="receiveB2COrderResponse">
        <soap:body use="literal"/>
      </wsdl:output>  
      <wsdl:fault name="MasAPIException">
        <soap:fault name="MasAPIException" use="literal"/>
      </wsdl:fault>
    </wsdl:operation>
  </wsdl:binding>  
  <wsdl:service name="ReceiveOrderAPIExplorterService">
    <wsdl:port binding="tns:ReceiveOrderAPIExplorterServiceSoapBinding" name="ReceiveOrderAPIExplorterPort">
      <soap:address location="http://cardpay.shengpay.com/api-acquire-channel/services/receiveOrderService"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

相关文章

  • PHP操作SOAP详解
  • JavaFX实现Hosts管理工具
  • 逆向分析某app并使用Java与PHP语言实现RC4加解密
  • 解决Windows磁盘爆满却不知如何清理问题

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

scrapy 爬妹子图 Step1 Step2 Step3

发表于 2017-12-19

前一篇写了基础的爬虫,这篇我们就来爬图片保存下来。环境如上一篇scrapy 爬电影 抓取数据

Step1

首先还是实体类写好我们需要的字段
注释中ImagesPipeline后面会解释

1
2
3
4
5
6
7
复制代码from scrapy import Item, Field
class PaimgItem(Item):
# 这两个字段 image_urls images是ImagesPipeline存放需要的
image_urls = Field()
images = Field()

name = Field()

Step2

项目结构不变,依然首先来写我们的Spider

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
复制代码import scrapy
from scrapy.spiders import CrawlSpider
from PaImg.items import PaimgItem


class MeizituSpider(CrawlSpider):
name = "meizitu"
host = 'http://www.meizitu.com/'
start_urls = ['http://www.meizitu.com/a/sexy.html']

headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
}

def parse(self, response):
nextPage = response.xpath(u'//div[@id="wp_page_numbers"]//a[text()="下一页"]/@href').extract_first()
nextPage = self.host + nextPage

for p in response.xpath('//li[@class="wp-item"]//a/@href').extract():
# scrapy.Request再次请求获取详情html由parse_item解析
yield scrapy.Request(p, callback=self.parse_item)

yield scrapy.Request(nextPage, callback=self.parse)

def parse_item(self, response):
item = PaimgItem()
# 详情中的图片列表
item['image_urls'] = response.xpath("//div[@id='picture']//img/@src").extract()
item['name'] = response.xpath("//div[@id='picture']//img/@alt").extract()[0].split(u',')[0]
return item

上一篇如果你认真看了写了,这一点代码相信你一眼就看懂了。如果不懂先看一下上一篇吧scrapy 爬电影 抓取数据

Step3

上面我们将详情页的图片列表地址存储保存了下来。我们需要一个专门下载每张图片的类。
也就是Step1中提到的ImagesPipeline。它是用来处理下载图片的一个Pipeline关于Pipline可以查看Pipeline。我们创建一个PaimgPipeline类继承自ImagesPipeline

1
复制代码class PaimgPipeline(ImagesPipeline):

我们需要复写四个方法:get_media_requests(进行图片下载请求处理) item_completed(图片下载完成的通知) file_path(下载原始图片的保存路径) thumb_path(缩略图保存路径)
完整代码:

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
复制代码headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
"Referer": "https://www.meizitu.com/", # 加入referer 为下载的域名网站
}


class PaimgPipeline(ImagesPipeline):
# 在工作流程中可以看到,管道会得到图片的URL并从项目中下载。
# # 为了这么做,你需要重写 get_media_requests() 方法,并对各个图片URL返回一个Request:
def get_media_requests(self, item, info):
for image_url in item['image_urls']:
# 这里把item传过去,因为后面需要用item里面的name作为文件名
yield Request(image_url, meta={'item': item}, headers=headers)

def item_completed(self, results, item, info):
image_paths = [x['path'] for ok, x in results if ok]
if not image_paths:
raise DropItem("Item contains no images")
return item

def file_path(self, request, response=None, info=None):
item = request.meta['item']
image_guid = request.url.split('/')[-1] # 倒数第一个元素
filenames = "full/%s/%s" % (item['name'], image_guid)
# print(filename)
return filenames

def thumb_path(self, request, thumb_id, response=None, info=None):
item = request.meta['item']
image_guid = request.url.split('/')[-1] # 倒数第一个元素
# thumb_id就是setting文件中定义的big small
filenames = "thumbil/%s/%s/%s" % (thumb_id, item['name'], image_guid)
return filenames

Step4

编写随机的UserAgent和IP代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码import random
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
'''
这个类主要用于产生随机UserAgent
'''
class RandomUserAgent(UserAgentMiddleware):

def __init__(self, agents):
self.agents = agents

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.getlist('USER_AGENTS')) # 返回的是本类的实例cls ==RandomUserAgent

def process_request(self, request, spider):
request.headers.setdefault('User-Agent', random.choice(self.agents))
1
2
3
4
5
6
7
8
9
10
11
复制代码import random
from PaImg.settings import IPPOOL

class ProxiesMiddleware(object):
def __init__(self, ip=''):
self.ip = ip

def process_request(self, request, spider):
thisip = random.choice(IPPOOL)
print("this is ip:" + thisip["ipaddr"])
request.meta["proxy"] = "http://" + thisip["ipaddr"]

Step5

下来我们就需要配置一下settings.py了。

  • 首先我们先把刚刚写的PaimgPipeline配置到settings中
1
2
3
复制代码ITEM_PIPELINES = {
'PaImg.pipelines.PaimgPipeline': 300,
}
  • 设置上面编写的RandomUserAgent及ProxiesMiddleware
1
2
3
4
5
6
7
8
复制代码DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'PaImg.RandomUserAgent.RandomUserAgent': 100,

# 代理IP可能会失效 需要重新ping代理ip地址
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware' : None,
'PaImg.ProxiesMiddleware.ProxiesMiddleware' : 100
}
  • 设置USER_AGENTS,也就是上面我们自己编写RandomUserAgent需要的数据
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
复制代码USER_AGENTS = [
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 LBBROWSER",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SV1; QQDownload 732; .NET4.0C; .NET4.0E; 360SE)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
"Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; zh-cn) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
"Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10"
]
  • 设置IPPOOL,也就是上面我们自己编写ProxiesMiddleware需要的数据( 代理IP可能会失效 需要重新ping代理ip地址)
1
2
3
4
5
6
7
8
复制代码IPPOOL=[
{"ipaddr":"211.142.141.210:8998"},
{"ipaddr":"58.19.15.218:808"},
{"ipaddr":"117.90.1.141:9000"},
{"ipaddr":"125.117.115.180:9000"},
{"ipaddr":"211.142.141.210:8998"},
{"ipaddr":"163.125.251.242:8118"}
]
  • 设置上面略缩图的大小,有两种方式:
1
2
3
4
复制代码IMAGES_THUMBS = {
'small': (50, 50),
'big': (270, 270),
}
  • 设置图片(宽最小与高最小)过滤。以防止我们会下载一些广告图或其他无用图
1
2
复制代码IMAGES_MIN_HEIGHT = 110
IMAGES_MIN_WIDTH = 110
  • 设置本地保存路径
1
复制代码IMAGES_STORE = '/Users/cuiyang/Pictures/meizitu'
  • 还有我们的延迟下载
1
复制代码DOWNLOAD_DELAY = 2

基本上这些就已经够了,我们还可以再优化一下配置,也是比较通用的一些配置

Tip

  • 减少超时时间以提高爬取速度
1
复制代码DOWNLOAD_TIMEOUT = 15
  • 不对失败的HTTP请求进行重试
1
复制代码RETRY_ENABLED = False
  • 禁用cookie
1
复制代码COOKIES_ENABLED = False
  • 通用爬取经常抓取大量的 “index” 页面; AjaxCrawlMiddleware能帮助您正确地爬取。 由于有些性能问题,且对于特定爬虫没有什么意义,该中间默认关闭。
1
复制代码AJAXCRAWL_ENABLED = False

最后附上源码地址:源码地址

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

scrapy 爬电影 抓取数据 序 Step1 Step2

发表于 2017-12-19

前段时间学python学完了基础的知识当然是要来点实际的东西玩玩了。爬虫,这个对于python再适合不过,今天就先来爬一个电影网站,下一篇我们来爬美女图片,这篇就做为一个爬虫基础练练手。将他有的资源信息爬下来保存成一个csv文件。

序

环境 mac python3.6.1 pycharm

Step1

默认scrapy的环境是安装好的。我们在终端里输入scrapy startproject 工程名新建一个爬虫项目,scrapy会为我们初始化一个基本结构如下图:

image.png

其中Id97Index.py是我们编写逻辑的文件,也是我们自己建的。除此之外都会在新建项目时生成。
Step2
=====

在items.py中创建我们的实体类:

image.png

分别为“封面”、“电影名”、“评分”、“类型”
该实体类会在后面提交数据时scrapy进行写入需要用到的,总之呢。你需要存什么数据就写对应字段,后面保存文件后你就明白了
Step3
=====

现在可以开始写我们爬虫的逻辑了
如Step1我们在spiders文件夹下新建一个Index97Index.py再新建类Id97Movie继承CrawlSpider如下:

image.png

其中name为我们启动项目的名字
host主要为后面做一些拼接
allowed_domains主域名
start_urls需要请求爬的初始urls
image.png

image.png

从源码可以看到name和start_urls是必要的参数,并且一开始的请求是循环start_urls,所以一定不能忘记,名字也不能定义。

Step4

覆写parse方法

image.png

image.png

  • 根据chrome中查看到每个item内容都在红框中这个标签内,我们可以右键选择copy xpath(xpath知识可以google两分钟就会)进行xpath选取
  • 获取下一页url,同样找到下一页的xpath。这里我是把最下面的”上一页,下一页 页码”都拿来。因为下一页这个标签总在倒数第二个就可以使用pages[-2]获取下一页的url
  • for循环处理每个列表列的内容使用parse_item方法处理
  • 最后yield Request(nextPageUrl, callback=self.parse)再次请求下一页的内容,依然使用parse方法处理这样循环直到最后一页
    parse_item代码:
    image.png

Step5

设置settings.py我们需要一个存放路径及格式

image.png

Step6

到该项目的目录下在终端执行scrapy crawl name(step3中所说的name='id97'),也可将命令直接放在python文件中进行执行

image.png

至此这个爬虫就基本结束了。settings.py中还有一些优化配置网上还是比较多的。可以自行配置试试看
源代码: 源码地址

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

中小型研发团队架构实践:搜索服务器Solr

发表于 2017-12-18

一、Solr 是什么

Apache Solr 是一个开源的搜索服务器,Solr 使用 Java 语言开发,主要基于 HTTP 和 Apache Lucene 实现。 Apache Lucene 是一个高效的、基于 Java 的全文检索库。

二、为什么要用 Solr

  • 在公司后台历史订单查询的应用中,模糊查询的实现方式为 LIKE ‘%something%’,性能很差。
  • 基于关键字的日志内容需要快速检索。
  • 其他数据库模糊查询的优化方案。

三、Solr 的特性

  • 具备高级全文搜索的能力
  • 高容量
  • 基于标准的开放接口(XML、JSON、HTTP):Document 通过 HTTP 利用 XML 加到一个搜索集合中,查询该集合时也是通过 HTTP 收到一个 XML/JSON 响应来实现
  • 提供功能全面的管理界面,使你能够容易地控制你的 Solr 实例
  • 易监控
  • 高稳定性和容错性
  • 易配置,且不失灵活和适配性
  • 准实时索引,确保你能够实时看到更新后的内容
  • 可扩展插件架构:新功能能够以插件的形式非常方便地添加到 Solr 服务器上

四、Solr 怎样工作

4.1、Web 管理 UI

URL 为:http://139.198.13.12:7000/solr/admin.html。请注意:Solr5.5 的,一定要加 admin.html,如果不加的话,则按回车后将返回 404(表示找不到页面)。

4.2、Solr 服务端的安装与配置

4.2.1、安装 Solr 服务:安装的版本号是 5.5.4。

4.2.2、建立 Core

要使用 Solr,需要建立类似于数据库实例的 Core。每个 Core 对应一个文件夹,此文件夹建立在 Solr Home 路径下,且其名字要和 Core 的名字一致:

4.2.3、配置 Core

以 Demo 中使用于 Solr 服务器上的 PolicyCore 为例,修改以下 3 个配置文件:

solrconfig.xml、managed-schema 是从位于【{Solr Home 路径}/configsets/basic_configs/conf】路径下的同名配置文件拷贝而来,而 data-config.xml 来自:对 Solr 服务端安装文件 solr-5.5.4.tgz 解压后,得到 solr-5.5.4 的文件夹名,然后把位于【solr-5.5.4/example/example-DIH/solr/db/conf】路径下的 db-data-config.xml 文件拷贝到【{Solr
Home 路径}/configsets/basic_configs/conf】路径下,并重命名为 data-config.xml。

在 solrconfig.xml 配置文件中增加如下内容:

1
2
3
4
5
6
7
8
9
复制代码__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__<lib dir="../contrib/extraction/lib" regex=".*\.jar" />
<lib dir="../dist/" regex="solr-cell-\d.*\.jar" />
<lib dir="../contrib/clustering/lib/" regex=".*\.jar" />
<lib dir="../dist/" regex="solr-clustering-\d.*\.jar" />
<lib dir="../contrib/langid/lib/" regex=".*\.jar" />
<lib dir="../dist/" regex="solr-langid-\d.*\.jar" />
<lib dir="../contrib/velocity/lib" regex=".*\.jar" />
<lib dir="../dist/" regex="solr-velocity-\d.*\.jar" />
<lib dir="../dist/" regex="solr-dataimporthandler-\d.*\.jar" />__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__

以上内容加在【5.5.4】节点之后、【${solr.data.dir:}】节点之前。

1
2
3
4
5
复制代码__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__<requestHandler name="/dataimport" class="solr.DataImportHandler">
<lst name="defaults">
<str name="config">data-config.xml</str>
</lst>
</requestHandler>__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__

以上内容加的位置请见如下图所示:

对 managed-schema 文件进行修改:以下内容加在节点内:

1
2
3
4
复制代码__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__<fieldType name="textPolicy_ik" class="solr.TextField">
<analyzer type="index" useSmart="false" class="org.wltea.analyzer.lucene.IKAnalyzer" />
<analyzer type="query" useSmart="true" class="org.wltea.analyzer.lucene.IKAnalyzer" />
</fieldType>__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__

注释掉以下配置:

1
复制代码__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__

然后在其下增加如下配置:

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
复制代码__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__<field name="PolicyID" type="string" indexed="true" stored="true" required="true" multiValued="false" />
<field name="PolicyGroupID" type="long" indexed="true" stored="true" />
<field name="PolicyOperatorID" type="long" indexed="true" stored="true" />
<field name="PolicyOperatorName" type="textPolicy_ik" indexed="true" stored="true" omitNorms="true" />
<field name="PolicyCode" type="textPolicy_ik" indexed="true" stored="true" omitNorms="true" />
<field name="PolicyName" type="textPolicy_ik" indexed="true" stored="true" omitNorms="true" />
<field name="PolicyType" type="string" indexed="true" stored="true" />
<field name="TicketType" type="int" indexed="true" stored="true" />
<field name="FlightType" type="int" indexed="true" stored="true" />
<field name="DepartureDate" type="tdate" indexed="true" stored="true" default="NOW+8HOUR" />
<field name="ArrivalDate" type="tdate" indexed="true" stored="true" default="NOW+8HOUR" />
<field name="ReturnDepartureDate" type="tdate" indexed="true" stored="true" default="NOW+8HOUR" />
<field name="ReturnArrivalDate" type="tdate" indexed="true" stored="true" default="NOW+8HOUR" />
<field name="DepartureCityCodes" type="textPolicy_ik" indexed="true" stored="true" omitNorms="true" />
<field name="TransitCityCodes" type="textPolicy_ik" indexed="true" stored="true" omitNorms="true" />
<field name="ArrivalCityCodes" type="textPolicy_ik" indexed="true" stored="true" omitNorms="true" />
<field name="OutTicketType" type="int" indexed="true" stored="true" />
<field name="OutTicketStart" type="tdate" indexed="true" stored="true" default="NOW+8HOUR" />
<field name="OutTicketEnd" type="tdate" indexed="true" stored="true" default="NOW+8HOUR" />
<field name="OutTicketPreDays" type="int" indexed="true" stored="true" />
<field name="Remark" type="textPolicy_ik" indexed="true" stored="true" omitNorms="true" />
<field name="Status" type="int" indexed="true" stored="true" />
<field name="SolrUpdatedTime" type="tdate" indexed="true" stored="true" default="NOW+8HOUR" />

<uniqueKey>PolicyID</uniqueKey>__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__

属性说明:

  • name:表示域名。
  • type:表示域的类型,必须匹配类型,不然会报错。如果需要分词,那么就传分词器名如 textPolicy_ik;另外,日期建议传 tdate,因为可以加快范围查找速度。
  • indexed:是否要做索引。
  • stored:是否要存储。
  • required:是否必填。
  • multiValued:是否有多个值。如果设置为多值,里面的值就采用数组的方式来存储。

对 data-config.xml 文件进行修改:先注释掉默认有的 dataConfig,然后在被注释内容的后面增加如下配置内容:

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
复制代码__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__<dataConfig>
<dataSource driver="com.microsoft.sqlserver.jdbc.SQLServerDriver" url="jdbc:sqlserver://{SQLServer 服务器 IP 地址}:{端口号,如果端口号是默认的 1433,则可不写};DatabaseName=SolrDB" user="sa" password="{登录 SQL Server 的密码}"/>
<document name="Info">
<entity name="Policy" dataSource="SolrDB" transformer="ClobTransformer" pk="PolicyID"
query="SELECT [PolicyID], [PolicyGroupID], [PolicyOperatorID], [PolicyOperatorName], [PolicyCode], [PolicyName], [PolicyType], [TicketType], [FlightType], DATEADD(HOUR, 8, CAST([DepartureDate] AS DATETIME)) [DepartureDate], DATEADD(HOUR, 8, CAST([ArrivalDate] AS DATETIME)) [ArrivalDate], DATEADD(HOUR, 8, CAST([ReturnDepartureDate] AS DATETIME)) [ReturnDepartureDate], DATEADD(HOUR, 8, CAST([ReturnArrivalDate] AS DATETIME)) [ReturnArrivalDate], [DepartureCityCodes], [TransitCityCodes], [ArrivalCityCodes], [OutTicketType], [OutTicketStart], [OutTicketEnd], [OutTicketPreDays], [Remark], [Status], DATEADD(HOUR, 8, CAST([SolrUpdatedTime] AS DATETIME)) [SolrUpdatedTime] FROM [Policy]"
deltaImportQuery="SELECT [PolicyID], [PolicyGroupID], [PolicyOperatorID], [PolicyOperatorName], [PolicyCode], [PolicyName], [PolicyType], [TicketType], [FlightType], DATEADD(HOUR, 8, CAST([DepartureDate] AS DATETIME)) [DepartureDate], DATEADD(HOUR, 8, CAST([ArrivalDate] AS DATETIME)) [ArrivalDate], DATEADD(HOUR, 8, CAST([ReturnDepartureDate] AS DATETIME)) [ReturnDepartureDate], DATEADD(HOUR, 8, CAST([ReturnArrivalDate] AS DATETIME)) [ReturnArrivalDate], [DepartureCityCodes], [TransitCityCodes], [ArrivalCityCodes], [OutTicketType], [OutTicketStart], [OutTicketEnd], [OutTicketPreDays], [Remark], [Status], DATEADD(HOUR, 8, CAST([SolrUpdatedTime] AS DATETIME)) [SolrUpdatedTime] FROM [Policy] WHERE PolicyID = '${dataimporter.delta.PolicyID}'"
deltaQuery="SELECT [PolicyID] FROM [Policy] WHERE [SolrUpdatedTime] > '${dataimporter.last_index_time}'">
<field column="PolicyID" name="PolicyID"/>
<field column="PolicyGroupID" name="PolicyGroupID"/>
<field column="PolicyOperatorID" name="PolicyOperatorID"/>
<field column="PolicyOperatorName" name="PolicyOperatorName"/>
<field column="PolicyCode" name="PolicyCode"/>
<field column="PolicyName" name="PolicyName"/>
<field column="PolicyType" name="PolicyType"/>
<field column="TicketType" name="TicketType"/>
<field column="FlightType" name="FlightType"/>
<field column="DepartureDate" name="DepartureDate"/>
<field column="ArrivalDate" name="ArrivalDate"/>
<field column="ReturnDepartureDate" name="ReturnDepartureDate"/>
<field column="ReturnArrivalDate" name="ReturnArrivalDate"/>
<field column="DepartureCityCodes" name="DepartureCityCodes"/>
<field column="TransitCityCodes" name="TransitCityCodes"/>
<field column="ArrivalCityCodes" name="ArrivalCityCodes"/>
<field column="OutTicketType" name="OutTicketType"/>
<field column="OutTicketStart" name="OutTicketStart"/>
<field column="OutTicketEnd" name="OutTicketEnd"/>
<field column="OutTicketPreDays" name="OutTicketPreDays"/>
<field column="Remark" name="Remark"/>
<field column="Status" name="Status"/>
<field column="SolrUpdatedTime" name="SolrUpdatedTime"/>
</entity>
</document>
</dataConfig>__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__

属性说明:

  • query:查询数据库表中符合的记录数据。
  • deltaImportQuery:表示次查询。次查询是获取以上步骤的 ID,然后把其全部数据获取,根据获取的数据,对索引库进行更新操作,可能是删除、添加或修改。此查询只对增量导入起作用,可以返回多个字段的值,一般情况下,都是返回所有字段的列。
  • deltaQuery:查询出需要增量索引的数据,所有经过修改的记录的 ID,可能是修改操作、添加操作或删除操作产生的。此查询只对增量导入起作用,而且只能返回 ID 值。

4.3、为 SolrDB 数据库的 Policy 表增加字段和触发器

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
复制代码__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__USE [SolrDB]
GO

CREATE TRIGGER [dbo].[TR_Solr_UPDATE_Policy] ON [dbo].[Policy]
FOR UPDATE, INSERT
AS
BEGIN
IF UPDATE(PolicyID)
OR UPDATE(PolicyGroupID)
OR UPDATE(PolicyOperatorID)
OR UPDATE(PolicyOperatorName)
OR UPDATE(PolicyCode)
OR UPDATE(PolicyName)
OR UPDATE(PolicyType)
OR UPDATE(TicketType)
OR UPDATE(FlightType)
OR UPDATE(DepartureDate) OR UPDATE(ArrivalDate)
OR UPDATE(ReturnDepartureDate) OR UPDATE(ReturnArrivalDate)
OR UPDATE(DepartureCityCodes)
OR UPDATE(TransitCityCodes)
OR UPDATE(ArrivalCityCodes)
OR UPDATE(OutTicketType)
OR UPDATE(OutTicketStart) OR UPDATE(OutTicketEnd)
OR UPDATE(OutTicketPreDays)
OR UPDATE(Remark)
OR UPDATE(Status)
BEGIN
UPDATE dbo.Policy
SET SolrUpdatedTime = GETDATE()
FROM dbo.Policy p, inserted i
WHERE p.PolicyID = i.PolicyID
END
END
GO__Mon Dec 18 2017 11:11:46 GMT+0800 (CST)____Mon Dec 18 2017 11:11:46 GMT+0800 (CST)__

4.4、SolrNet

SolrNet 是 Solr 的开源.NET 客户端之一。

4.5、定时从数据库中全量、增量数据导入到 Solr

Solr 自身提供有定时增量导入功能,但经测试 apache-solr-dataimportscheduler1.0 版本在 Solr5.5 上已经不能使用,除非修改 apache-solr-dataimportscheduler 的源码。于是,我们采用了如下方式:

首先,开发 Job 任务调度 RESTful 服务,这种方式不仅可以实现定时增量数据导入,也能够实现定时全量数据导入。

然后,在自主研发的【Job 集中式管理平台】中把相关内容都配置好,如下图所示。

这样,我们的 JobServer 就会定时地以 HTTP GET 或 HTTP POST 或 HTTP HEAD 方式请求全量 / 增量导入链接,从而实现了定时全量、增量数据导入功能。另外,如果你想要知道如何利用 SolrNet 实现全量导入、增量导入,请分别参考 Demo 代码中的 FullDataImport() 和 DeltaDataImport() 这两个示例。

4.6、准实时数据导入、删除以及查询

用 SolrNet 的 CURD API 实现,示例请见 Demo 的 Add()、Delete() 和 Query()。准实时数据导入较定时增量数据导入更近于实时,在实际应用中如通过消息队列对数据库和 Solr 同时更新,则更好。

五、Demo 下载及更多资料

  • SolrDemo 下载地址:github.com/das2017/Sol…
  • Solr 官网:lucene.apache.org/solr/
  • Lucene 官网:lucene.apache.org/
  • SolrNet 官网:github.com/mausch/Solr…

本系列文章涉及内容清单如下,其中有感兴趣的,欢迎关注:

  • 开篇:中小型研发团队架构实践三要点
  • 缓存 Redis:Redis快速入门及应用
  • 消息队列 RabbitMQ:如何用好消息队列RabbitMQ?
  • 集中式日志 ELK:中小型研发团队架构实践之集中式日志ELK
  • 任务调度 Job:中小型研发团队架构实践之任务调度Job
  • 应用监控 Metrics:应用监控怎么做?
  • 微服务框架 MSA:这是你心心念念的.NET栈的微服务架构实践
  • 搜索利器 Solr
  • 分布式协调器 ZooKeeper
  • 小工具:
  • Dapper.NET/EmitMapper/AutoMapper/Autofac/NuGet
  • 发布工具 Jenkins
  • 总体架构设计:电商如何做企业总体架构?
  • 单个项目架构设计
  • 统一应用分层:如何规范公司所有应用分层?
  • 调试工具 WinDbg
  • 单点登录
  • 企业支付网关
  • 结篇

作者介绍

杨丽,拥有多年互联网应用系统研发经验,曾就职于古大集团,现任职中青易游的系统架构师,主要负责公司研发中心业务系统的架构设计以及新技术积累和培训。现阶段主要关注开源软件、软件架构、微服务以及大数据。

张辉清,10 多年的 IT 老兵,先后担任携程架构师、古大集团首席架构、中青易游 CTO 等职务,主导过两家公司的技术架构升级改造工作。现关注架构与工程效率,技术与业务的匹配与融合,技术价值与创新。

感谢雨多田光对本文的审校。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…903904905…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%