[
  {
    "path": "Flink/10-Flink集群的高可用(搭建篇补充).md",
    "content": "Flink的HA搭建并不复杂，本质来说就是配置2个jobmanager。\n本文作为Flink集群部署的补充篇。\n> 这篇文章来自网络，向作者尼小摩致敬，\n\n## 概述\n\nJobManager 协调每个 Flink 部署。它负责调度和资源管理。\n默认情况下，每个 Flink 集群只有一个 JobManager 实例。 这会产生单点故障(SPOF)：如果 JobManager 崩溃，则无法提交新作业并且导致运行中的作业运行失败。\n使用 JobManager 高可用性模式，可以避免这个问题，从而消除 SPOF。您可以为Standalone和 YARN 集群配置高可用性。\n\n\n## Standalone集群高可用性\n\n针对 Standalone 集群的 JobManager 高可用性的一般概念是，任何时候都有一个 主 JobManager 和多个备 JobManagers，以便在主节点失败时有备 JobManagers 来接管集群。这保证了没有单点故障，一旦备 JobManager 接管集群，作业就可以正常运行。主备  JobManager 实例之间没有明显的区别。每个 JobManager 都可以充当主备节点。\n例如，请考虑以下三个 JobManager 实例的设置:\n\n![2278c5ee57d47a73498685c4c728c432](10-Flink集群的高可用(搭建篇补充).resources/6278EDED-A65A-4539-A16D-7BCD9FE77864.png)\n\n### 配置\n\n要启用 JobManager 高可用性，您必须将高可用性模式设置为 zookeeper，配置 zookeeper quorum 将所有 JobManager 主机及其 web UI 端口写入配置文件。\nFlink利用 ZooKeeper 在所有正在运行的 JobManager 实例之间进行分布式协调。 ZooKeeper 是独立于 Flink 的服务，通过 Leader 选举和轻量级一致状态存储提供高可靠的分布式协调。\n\n### Masters文件 (masters服务器)\n\n要启动HA集群，请在以下位置配置Master文件\n\n\n* conf/masters:masters文件：masters文件包含启动 jobmanager 的所有主机和 web 用户界面绑定的端口。\n\n```\n jobManagerAddress1:webUIPort1\n    [...]\n jobManagerAddressX:webUIPortX\n```\n\n默认情况下，job manager选一个随机端口作为进程随机通信端口。您可以通过 high-availability.jobmanager.port 键修改此设置。此配置接受单个端口（例如50010），范围（50000-50025）或两者的组合（50010,50011,50020-50025,50050-50075）。\n\n### 配置文件（flink-conf.yaml）\n\n要启动HA集群，请将以下配置键添加到 conf/flink-conf.yaml:\n\n* 高可用性模式（必需）：在 conf/flink-conf.yaml 中，必须将高可用性模式设置为zookeeper，以打开高可用模式。或者将此选项设置为工厂类的 FQN，Flink 通过创建 HighAvailabilityServices 实例使用。\n\n```\n high-availability: zookeeper\n```\n\n* Zookeeper quorum（必需）： ZooKeeper quorum 是 ZooKeeper 服务器的复制组，它提供分布式协调服务。\n\n```\nhigh-availability.zookeeper.quorum:address1:2181[,...],addressX:2181\n```\n每个 addressX:port 都是一个 ZooKeeper 服务器的ip及其端口，Flink 可以在指定的地址和端口访问zookeeper。\n\n* ZooKeeper root （推荐）： ZooKeeper 根节点，在该节点下放置所有集群节点。\n\n```\n  high-availability.zookeeper.path.root: /flink\n```\n\n* ZooKeeper cluster-id（推荐）： ZooKeeper的cluster-id节点，在该节点下放置集群的所有相关数据。\n\n```\n high-availability.cluster-id: /default_ns # important: customize per cluster\n```\n\n**重要：** 在运行 YARN 或其他群集管理器中运行时，不要手动设置此值。在这些情况下，将根据应用程序 ID 自动生成 cluster-id。 手动设置 cluster-id 会覆盖 YARN 中的自动生成的 ID。反过来，使用 -z CLI 选项指定 cluster-id 会覆盖手动配置。如果在裸机上运行多个 Flink HA 集群，则必须为每个集群手动配置单独的 cluster-id。\n\n\n* 存储目录（必需）： JobManager 元数据保存在文件系统 storageDir 中，在 ZooKeeper 中仅保存了指向此状态的指针。\n\n```\nhigh-availability.storageDir: hdfs:///flink/recovery\n```\n\n该storageDir 中保存了 JobManager 恢复状态所需的所有元数据。\n配置 master 文件和 ZooKeeper quorum 之后，您可以使用提供的集群启动脚本。它们将启动 HA 群集。请注意，启动 Flink HA 集群前，必须启动 Zookeeper 集群，并确保为要启动的每个 HA 群集配置单独的 ZooKeeper 根路径。\n\n**示例：具有2个 JobManager 的 Standalone 集群**\n\n1. 在conf/flink-conf.yaml 中配置高可用模式和 ZooKeeper quorum：\n\n```\nhigh-availability: zookeeper\nhigh-availability.zookeeper.quorum: localhost:2181\nhigh-availability.zookeeper.path.root: /flink\nhigh-availability.cluster-id: /cluster_one\nhigh-availability.storageDir:    hdfs:///flink/recovery\n```\n2. 在 conf/master 中配置 master:\n\n```\n    localhost:8081\n    localhost:8082\n```\n3. 在 conf/zoo.cfg 中配置 ZooKeeper 服务（目前，每台机器只能运行一个 ZooKeeper 进程）\n\n```\n    server.0=localhost:2888:3888\n```\n\n4. 启动 ZooKeeper quorum：\n\n```\n    $ bin/start-zookeeper-quorum.sh\n    Starting zookeeper daemon on host localhost.\n```\n\n5. 启动 Flink HA 集群：\n\n```\n    $ bin/start-cluster.sh\n    Starting HA cluster with 2 masters and 1 peers     in ZooKeeper quorum.\n    Starting jobmanager daemon on host localhost.\n    Starting jobmanager daemon on host localhost.\n    Starting taskmanager daemon on host localhost.\n```\n\n6. 停止 Zookeeper quorum 和集群：\n\n```\n    $ bin/stop-cluster.sh\n    Stopping taskmanager daemon (pid: 7647) on localhost.\n    Stopping jobmanager daemon (pid: 7495) on host localhost.\n    Stopping jobmanager daemon (pid: 7349) on host localhost.\n    $ bin/stop-zookeeper-quorum.sh\n    Stopping zookeeper daemon (pid: 7101) on host localhost.\n```\n\n## YARN 集群的高可用性\n\n在运行高可用性 YARN 集群时，我们不会运行多个 JobManager (ApplicationMaster) 实例，而只运行一个，该JobManager实例失败时，YARN会将其重新启动。Yarn的具体行为取决于您使用的 YARN 版本。\n\n\n### 配置\n\nApplication Master最大重试次数（yarn-site.xml）\n在YARN 配置文件 yarn-site.xml 中，需要配置 application master 的最大重试次数：\n\n```\n<property>\n  <name>yarn.resourcemanager.am.max-attempts</name>\n  <value>4</value>\n  <description>\n    The maximum number of application master execution attempts.\n  </description>\n</property>\n```\n\n当前 YARN 版本的默认值是2(表示允许单个JobManager失败两次)。\n\nApplication Attempts（flink-conf.yaml）：\n除了HA配置(参考上文)之外，您还必须配置最大重试次数 conf/flink-conf.yaml:\n\n```\nyarn.application-attempts: 10\n```\n\n这意味着在如果程序启动失败，YARN会再重试9次（9 次重试 + 1次启动）。如果 YARN 操作需要，如果启动10次作业还失败，yarn才会将该任务的状态置为失败。如果抢占，节点硬件故障或重启，NodeManager 重新同步等操作需要，YARN继续尝试启动应用。 这些重启不计入 yarn.application-attempts 个数中。重要的是要注意 yarn.resourcemanager.am.max-attempts 为yarn中程序重启上限。因此， Flink 中设置的程序尝试次数不能超过 YARN 的集群设置。\n\n### 示例：高可用的YARN Session\n\n1.配置 HA 模式和 ZooKeeper 集群在 conf/flink-conf.yaml 中：\n```\n    high-availability: zookeeper\n    high-availability.zookeeper.quorum: localhost:2181\n    high-availability.storageDir: hdfs:///flink/recovery\n    high-availability.zookeeper.path.root: /flink\n    yarn.application-attempts: 10\n\n```\n2. 配置 ZooKeeper 服务在 conf/zoo.cfg 中(目前每台机器只能运行一个 ZooKeeper 进程)：\n```\n    server.0=localhost:2888:3888\n\n```\n3. 启动 ZooKeeper 集群：\n```\n    $ bin/start-zookeeper-quorum.sh\n    Starting zookeeper daemon on host localhost.\n\n```\n4. 启动 HA 集群：\n```\n    $ bin / yarn-session.sh -n 2\n```\n### 配置 Zookeeper 安全性\n\n如果 ZooKeeper 使用 Kerberos 以安全模式运行，flink-conf.yaml 根据需要覆盖以下配置：\n```\nzookeeper.sasl.service-name: zookeeper \n# 默认设置是 “zookeeper” 。如果 ZooKeeper 集群配置了\n# 不同的服务名称，那么可以在这里提供。\n\nzookeeper.sasl.login-context-name: Client  \n# 默认设置是 “Client”。该值配置需要匹配\n# \"security.kerberos.login.contexts\"中的其中一个值。\n```\n\n有关 Kerberos 安全性的 Flink 配置的更多信息，请参阅 此处。您还可以在 此处 找到关于 Flink 内部如何设置基于 kerberos 的安全性的详细信息。\n\n\n### Bootstrap ZooKeeper\n\n如果您没有正在运行的ZooKeeper，则可以使用Flink程序附带的脚本。\n这是一个 ZooKeeper 配置模板 conf/zoo.cfg。您可以为主机配置为使用 server.X 条目运行 ZooKeeper，其中 X 是每个服务器的唯一IP:\n```\nserver.X=addressX:peerPort:leaderPort\n[...]\nserver.Y=addressY:peerPort:leaderPort\n```\n该脚本 bin/start-zookeeper-quorum.sh 将在每个配置的主机上启动 ZooKeeper 服务器。 Flink wrapper 会启动 ZooKeeper 服务，该 wraper 从 conf/zoo.cfg 中读取配置，并设置一些必需的配置项。在生产设置中，建议您使用自己安装的 ZooKeeper。\n\n"
  },
  {
    "path": "Flink/11-时间戳和水印.md",
    "content": "本文作者为阿里巴巴高级技术专家:金竹，原文发表在云栖社区。\n\n地址为:https://yq.aliyun.com/articles/666056?spm=a2c4e.11155435.0.0.106e1b10snGqMd\n\n## 实际问题（乱序）\n\n在介绍Watermark相关内容之前我们先抛出一个具体的问题，在实际的流式计算中数据到来的顺序对计算结果的正确性有至关重要的影响，比如：某数据源中的某些数据由于某种原因(如：网络原因，外部存储自身原因)会有5秒的延时，也就是在实际时间的第1秒产生的数据有可能在第5秒中产生的数据之后到来(比如到Window处理节点).选具体某个delay的元素来说，假设在一个5秒的Tumble窗口(详见Window介绍章节)，有一个EventTime是 11秒的数据，在第16秒时候到来了。图示第11秒的数据，在16秒到来了，如下图：\n\n\n![e2c42f1a11d5155df6b157797157c233](11-时间戳和水印.resources/D3CD1BC9-F864-4784-AE75-6C9CC19903CC.png)\n\n那么对于一个Count聚合的Tumble(5s)的window，上面的情况如何处理才能window2=4，window3=2 呢？Apache Flink的时间类型\n开篇我们描述的问题是一个很常见的TimeWindow中数据乱序的问题，乱序是相对于事件产生时间和到达Apache Flink 实际处理算子的顺序而言的，关于时间在Apache Flink中有如下三种时间类型，如下图：\n\n![cd581a1f6e2089fbd7b58ca3cae79197](11-时间戳和水印.resources/4A6A9E37-7B5E-4602-9EAB-8DE359F849B4.png)\n\n\n那么对于一个Count聚合的Tumble(5s)的window，上面的情况如何处理才能window2=4，window3=2 呢？\n\n## Apache Flink的时间类型\n\n开篇我们描述的问题是一个很常见的TimeWindow中数据乱序的问题，乱序是相对于事件产生时间和到达Apache Flink 实际处理算子的顺序而言的，关于时间在Apache Flink中有如下三种时间类型，如下图：\n\n![093a38b1c2f74dcd6aa7e08e98ba5bb6](11-时间戳和水印.resources/9A88F330-80C2-4E30-8F73-146F315DDE78.png)\n\n\n* ProcessingTime \n\n是数据流入到具体某个算子时候相应的系统时间。ProcessingTime 有最好的性能和最低的延迟。但在分布式计算环境中ProcessingTime具有不确定性，相同数据流多次运行有可能产生不同的计算结果。\n\n* IngestionTime\n\nIngestionTime是数据进入Apache Flink框架的时间，是在Source Operator中设置的。与ProcessingTime相比可以提供更可预测的结果，因为IngestionTime的时间戳比较稳定(在源处只记录一次)，同一数据在流经不同窗口操作时将使用相同的时间戳，而对于ProcessingTime同一数据在流经不同窗口算子会有不同的处理时间戳。\n\n* EventTime\n\nEventTime是事件在设备上产生时候携带的。在进入Apache Flink框架之前EventTime通常要嵌入到记录中，并且EventTime也可以从记录中提取出来。在实际的网上购物订单等业务场景中，大多会使用EventTime来进行数据计算。\n\n开篇描述的问题和本篇要介绍的Watermark所涉及的时间类型均是指EventTime类型。\n\n## 什么是Watermark\n\nWatermark是Apache Flink为了处理EventTime 窗口计算提出的一种机制,本质上也是一种时间戳，由Apache Flink Source或者自定义的Watermark生成器按照需求Punctuated或者Periodic两种方式生成的一种系统Event，与普通数据流Event一样流转到对应的下游算子，接收到Watermark Event的算子以此不断调整自己管理的EventTime clock。 Apache Flink 框架保证Watermark单调递增，算子接收到一个Watermark时候，框架知道不会再有任何小于该Watermark的时间戳的数据元素到来了，所以Watermark可以看做是告诉Apache Flink框架数据流已经处理到什么位置(时间维度)的方式。 Watermark的产生和Apache Flink内部处理逻辑如下图所示: \n\n\n![2aa4bad9c654141d2c4f3a31e0ccc45c](11-时间戳和水印.resources/4F4C947D-0C11-4A4B-B21F-90F7857B46D4.png)\n\n\n\n## Watermark的产生方式\n\n目前Apache Flink 有两种生产Watermark的方式，如下：\n\n* Punctuated - 数据流中每一个递增的EventTime都会产生一个Watermark。 \n\n在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力，所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成。\n\n* Periodic - 周期性的（一定时间间隔或者达到一定的记录条数）产生一个Watermark。在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark，否则在极端情况下会有很大的延时。\n\n所以Watermark的生成方式需要根据业务场景的不同进行不同的选择。\n\n## Watermark的接口定义\n\n对应Apache Flink Watermark两种不同的生成方式，我们了解一下对应的接口定义，如下：\n\n* Periodic Watermarks - AssignerWithPeriodicWatermarks\n\n```\n/**\n* Returns the current watermark. This method is periodically called by the\n* system to retrieve the current watermark. The method may return {@code null} to\n* indicate that no new Watermark is available.\n*\n* &lt;p&gt;The returned watermark will be emitted only if it is non-null and itsTimestamp\n* is larger than that of the previously emitted watermark (to preserve the contract of\n* ascending watermarks). If the current watermark is still\n* identical to the previous one, no progress in EventTime has happened since\n* the previous call to this method. If a null value is returned, or theTimestamp\n* of the returned watermark is smaller than that of the last emitted one, then no\n* new watermark will be generated.\n*\n* &lt;p&gt;The interval in which this method is called and Watermarks are generated\n* depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.\n*\n* @see org.Apache.flink.streaming.api.watermark.Watermark\n* @see ExecutionConfig#getAutoWatermarkInterval()\n*\n* @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.\n*/\n@Nullable\nWatermark getCurrentWatermark();\n```\n\n* Punctuated Watermarks - AssignerWithPunctuatedWatermarks \n\n```\npublic interface AssignerWithPunctuatedWatermarks&lt;T&gt; extendsTimestampAssigner&lt;T&gt; {\n\n/**\n* Asks this implementation if it wants to emit a watermark. This method is called right after\n* the {@link #extractTimestamp(Object, long)} method.\n*\n* &lt;p&gt;The returned watermark will be emitted only if it is non-null and itsTimestamp\n* is larger than that of the previously emitted watermark (to preserve the contract of\n* ascending watermarks). If a null value is returned, or theTimestamp of the returned\n* watermark is smaller than that of the last emitted one, then no new watermark will\n* be generated.\n*\n* &lt;p&gt;For an example how to use this method, see the documentation of\n* {@link AssignerWithPunctuatedWatermarks this class}.\n*\n* @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.\n*/\n@Nullable\nWatermark checkAndGetNextWatermark(T lastElement, long extractedTimestamp);\n}\n```\n\nAssignerWithPunctuatedWatermarks 继承了TimestampAssigner接口 -TimestampAssigner\n\n\n```\npublic interfaceTimestampAssigner&lt;T&gt; extends Function {\n\n/**\n* Assigns aTimestamp to an element, in milliseconds since the Epoch.\n*\n* &lt;p&gt;The method is passed the previously assignedTimestamp of the element.\n* That previousTimestamp may have been assigned from a previous assigner,\n* by ingestionTime. If the element did not carry aTimestamp before, this value is\n* {@code Long.MIN_VALUE}.\n*\n* @param element The element that theTimestamp is wil be assigned to.\n* @param previousElementTimestamp The previous internalTimestamp of the element,\n* or a negative value, if noTimestamp has been assigned, yet.\n* @return The newTimestamp.\n*/\nlong extractTimestamp(T element, long previousElementTimestamp);\n}\n```\n\n从接口定义可以看出，Watermark可以在Event(Element)中提取EventTime，进而定义一定的计算逻辑产生Watermark的时间戳。\n\n## Watermark解决如上问题\n\n从上面的Watermark生成接口和Apache Flink内部对Periodic Watermark的实现来看，Watermark的时间戳可以和Event中的EventTime 一致，也可以自己定义任何合理的逻辑使得Watermark的时间戳不等于Event中的EventTime，Event中的EventTime自产生那一刻起就不可以改变了，不受Apache Flink框架控制，而Watermark的产生是在Apache Flink的Source节点或实现的Watermark生成器计算产生(如上Apache Flink内置的 Periodic Watermark实现), Apache Flink内部对单流或多流的场景有统一的Watermark处理。\n\n回过头来我们在看看Watermark机制如何解决上面的问题，上面的问题在于如何将迟来的EventTime 位11的元素正确处理。要解决这个问题我们还需要先了解一下EventTime window是如何触发的？ EventTime window 计算条件是当Window计算的Timer时间戳 小于等于 当前系统的Watermak的时间戳时候进行计算。 \n\n* 当Watermark的时间戳等于Event中携带的EventTime时候，上面场景（Watermark=EventTime)的计算结果如下：\n\n![0c37afd412d169744cbfaefc19b2d8b5](11-时间戳和水印.resources/4C5D9E8A-E171-40A8-AD04-FAC454F6A273.png)\n\n\n 上面对应的DDL(Alibaba 企业版的Flink分支)定义如下：\n \n \n```\nCREATE TABLE source(\n...,\nEvent_timeTimeStamp,\nWATERMARK wk1 FOR Event_time as withOffset(Event_time, 0) \n) with (\n...\n);\n```\n\n* 如果想正确处理迟来的数据可以定义Watermark生成策略为 Watermark = EventTime -5s， 如下：\n\n![1a9b254b6b2cdb6f6535fd1555cfcaf6](11-时间戳和水印.resources/A401E45B-4739-4CC9-96BD-67C02F33E3E0.png)\n\n\n\n上面对应的DDL(Alibaba 内部的DDL语法，目前正在和社区讨论)定义如下： \n\n```\nCREATE TABLE source(\n...,\nEvent_timeTimeStamp,\nWATERMARK wk1 FOR Event_time as withOffset(Event_time, 5000) \n) with (\n...\n);\n```\n\n上面正确处理的根源是我们采取了 延迟触发 window 计算 的方式正确处理了 Late Event. 与此同时，我们发现window的延时触发计算，也导致了下游的LATENCY变大，本例子中下游得到window的结果就延迟了5s.\n\n## 多流的Watermark处理\n\n在实际的流计算中往往一个job中会处理多个Source的数据，对Source的数据进行GroupBy分组，那么来自不同Source的相同key值会shuffle到同一个处理节点，并携带各自的Watermark，Apache Flink内部要保证Watermark要保持单调递增，多个Source的Watermark汇聚到一起时候可能不是单调自增的，这样的情况Apache Flink内部是如何处理的呢？如下图所示：\n\n![37acdf161b102c8158f00a904e61b26f](11-时间戳和水印.resources/187EBCD3-0C1D-46AA-845A-ECB1E40737FC.png)\n\n\nApache Flink内部实现每一个边上只能有一个递增的Watermark， 当出现多流携带Eventtime汇聚到一起(GroupBy or Union)时候，Apache Flink会选择所有流入的Eventtime中最小的一个向下游流出。从而保证watermark的单调递增和保证数据的完整性.如下图: \n\n![fb198b7cb4d4bb5268ce1a51f05dfbc7](11-时间戳和水印.resources/057D9392-3A73-4A4C-93AF-EC5AB849B3A0.png)\n\n\n本节以一个流计算常见的乱序问题介绍了Apache Flink如何利用Watermark机制来处理乱序问题. 本篇内容在一定程度上也体现了EventTime Window中的Trigger机制依赖了Watermark(后续Window篇章会介绍)。Watermark机制是流计算中处理乱序，正确处理Late Event的核心手段。\n\n"
  },
  {
    "path": "Flink/12-Broadcast广播变量.md",
    "content": "## 广播变量简介\n\n在Flink中，同一个算子可能存在若干个不同的并行实例，计算过程可能不在同一个Slot中进行，不同算子之间更是如此，因此不同算子的计算数据之间不能像Java数组之间一样互相访问，而广播变量Broadcast便是解决这种情况的。\n\n我们可以把广播变量理解为是一个公共的共享变量，我们可以把一个dataset 数据集广播出去，然后不同的task在节点上都能够获取到，这个数据在每个节点上只会存在一份\n\n\n\n## 用法\n\n```\n1：初始化数据\n  DataSet<Integer> num = env.fromElements(1, 2, 3)\n  2：广播数据\n  .withBroadcastSet(toBroadcast, \"num\");\n  3：获取数据\n  Collection<Integer> broadcastSet = getRuntimeContext().getBroadcastVariable(\"num\");\n  \n  注意：\n  1：广播出去的变量存在于每个节点的内存中，所以这个数据集不能太大。因为广播出去的数据，会常驻内存，除非程序执行结束\n  2：广播变量在初始化广播出去以后不支持修改，这样才能保证每个节点的数据都是一致的。\n\n```\n\n## 注意事项\n\n### 使用广播状态，task 之间不会相互通信\n\n只有广播的一边可以修改广播状态的内容。用户必须保证所有 operator 并发实例上对广播状态的     修改行为都是一致的。或者说，如果不同的并发实例拥有不同的广播状态内容，将导致不一致的结果。\n### 广播状态中事件的顺序在各个并发实例中可能不尽相同\n\n广播流的元素保证了将所有元素（最终）都发给下游所有的并发实例，但是元素的到达的顺序可能在并发实例之间并不相同。因此，对广播状态的修改不能依赖于输入数据的顺序。\n\n### 所有operator task都会快照下他们的广播状态\n在checkpoint时，所有的 task 都会 checkpoint 下他们的广播状态，随着并发度的增加，checkpoint 的大小也会随之增加\n### 广播变量存在内存中\n\n广播出去的变量存在于每个节点的内存中，所以这个数据集不能太大，百兆左右可以接受，Gb不能接受\n\n\n## 案例\n\n```\npublic class BroadCastTest {\n\n    public static void main(String[] args) throws Exception{\n        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n        //1.封装一个DataSet\n        DataSet<Integer> broadcast = env.fromElements(1, 2, 3);\n        DataSet<String> data = env.fromElements(\"a\", \"b\");\n        data.map(new RichMapFunction<String, String>() {\n            private List list = new ArrayList();\n            @Override\n            public void open(Configuration parameters) throws Exception {\n                // 3. 获取广播的DataSet数据 作为一个Collection\n                Collection<Integer> broadcastSet = getRuntimeContext().getBroadcastVariable(\"number\");\n                list.addAll(broadcastSet);\n            }\n\n            @Override\n            public String map(String value) throws Exception {\n                return value + \": \"+ list;\n            }\n        }).withBroadcastSet(broadcast, \"number\") \n            // 2. 广播的broadcast\n          .printToErr();//打印到err方便查看\n    }\n}\n```\n\n输出结果：\n\n```\na: [1, 2, 3]\nb: [1, 2, 3]\n```"
  },
  {
    "path": "Flink/13-Flink-Kafka-Connector.md",
    "content": "\n## 简介\n\nFlink-kafka-connector用来做什么？\n\nKafka中的partition机制和Flink的并行度机制结合，实现数据恢复\nKafka可以作为Flink的source和sink\n任务失败，通过设置kafka的offset来恢复应用\n\n### kafka简单介绍\n\n\n关于kafka，我们会有专题文章介绍，这里简单介绍几个必须知道的概念。\n\n**1.生产者（Producer）**\n    顾名思义，生产者就是生产消息的组件，它的主要工作就是源源不断地生产出消息，然后发送给消息队列。生产者可以向消息队列发送各种类型的消息，如狭义的字符串消息，也可以发送二进制消息。生产者是消息队列的数据源，只有通过生产者持续不断地向消息队列发送消息，消息队列才能不断处理消息。\n**2.消费者（Consumer）**\n    所谓消费者，指的是不断消费（获取）消息的组件，它获取消息的来源就是消息队列（即Kafka本身）。换句话说，生产者不断向消息队列发送消息，而消费者则不断从消息队列中获取消息。\n**3.主题（Topic）**\n    主题是Kafka中一个极为重要的概念。首先，主题是一个逻辑上的概念，它用于从逻辑上来归类与存储消息本身。多个生产者可以向一个Topic发送消息，同时也可以有多个消费者消费一个Topic中的消息。Topic还有分区和副本的概念。Topic与消息这两个概念之间密切相关，Kafka中的每一条消息都归属于某一个Topic，而一个Topic下面可以有任意数量的消息。\n\n### kafka简单操作\n\n启动zk：nohup bin/zookeeper-server-start.sh config/zookeeper.properties  &\n\n启动server: nohup bin/kafka-server-start.sh config/server.properties &\n\n创建一个topic：bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test     \n\n查看topic：bin/kafka-topics.sh --list --zookeeper localhost:2181\n\n发送数据：bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test\n\n启动一个消费者：bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning\n\n\n删除topic： bin/kafka-topics.sh --delete --zookeeper localhost:2181  --topic topn\n\n\n## Flink消费Kafka注意事项\n\n* setStartFromGroupOffsets()【默认消费策略】\n\n    默认读取上次保存的offset信息\n    如果是应用第一次启动，读取不到上次的offset信息，则会根据这个参数auto.offset.reset的值来进行消费数据\n\n\n* setStartFromEarliest()\n从最早的数据开始进行消费，忽略存储的offset信息\n\n\n* setStartFromLatest()\n从最新的数据进行消费，忽略存储的offset信息\n\n\n* setStartFromSpecificOffsets(Map<KafkaTopicPartition, Long>)\n从指定位置进行消费\n\n* 当checkpoint机制开启的时候，KafkaConsumer会定期把kafka的offset信息还有其他operator的状态信息一块保存起来。当job失败重启的时候，Flink会从最近一次的checkpoint中进行恢复数据，重新消费kafka中的数据。\n\n\n* 为了能够使用支持容错的kafka Consumer，需要开启checkpoint\nenv.enableCheckpointing(5000); // 每5s checkpoint一次\n\n## 搭建Kafka单机环境\n\n我本地安装了一个kafka_2.11-2.1.0版本的kafka\n\n![00a9a6e5a8ef9db0e1806cae88f833fb](13-Flink-Kafka-Connector.resources/0174F674-797A-4C03-A020-944EA4D00C00.png)\n\n启动Zookeeper和kafka server:\n```\n启动zk：nohup bin/zookeeper-server-start.sh config/zookeeper.properties  &\n\n启动server: nohup bin/kafka-server-start.sh config/server.properties &\n```\n创建一个topic:\n```\nbin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test\n```\n\n![4ca97fc9c79240ec7c25251b0ae78331](13-Flink-Kafka-Connector.resources/BCED174D-B20F-4CEA-9DF0-D5796CF3E5CD.png)\n\n\n\n## 实战案例\n\n> 所有代码，我放在了我的公众号，回复**Flink**可以下载\n\n*  海量【**java和大数据的面试题+视频资料**】整理在公众号，关注后可以下载~\n*  更多大数据技术欢迎和作者一起探讨~\n\n![](https://user-gold-cdn.xitu.io/2019/2/23/1691a0d20e61eb0d?w=300&h=390&f=png&s=14824)\n\n### Kafka作为Flink Sink\n\n首先pom依赖：\n```\n<dependency>\n            <groupId>org.apache.flink</groupId>\n            <artifactId>flink-connector-kafka_2.11</artifactId>\n            <version>1.7.0</version>\n        </dependency>\n```\n\n向kafka写入数据：\n\n```\npublic class KafkaProducer {\n\n\n    public static void main(String[] args) throws Exception{\n\n        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n\n        DataStreamSource<String> text = env.addSource(new MyNoParalleSource()).setParallelism(1);\n\n        Properties properties = new Properties();\n        properties.setProperty(\"bootstrap.servers\", \"localhost:9092\");\n        //new FlinkKafkaProducer(\"topn\",new KeyedSerializationSchemaWrapper(new SimpleStringSchema()),properties,FlinkKafkaProducer.Semantic.EXACTLY_ONCE);\n\t    FlinkKafkaProducer<String> producer = new FlinkKafkaProducer(\"test\",new SimpleStringSchema(),properties);\n/*\n        //event-timestamp事件的发生时间\n        producer.setWriteTimestampToKafka(true);\n*/\n        text.addSink(producer);\n        env.execute();\n    }\n}//\n```\n大家这里特别注意，我们实现了一个并行度为1的`MyNoParalleSource`来生产数据，代码如下：\n\n```\n//使用并行度为1的source\npublic class MyNoParalleSource implements SourceFunction<String> {//1\n\n    //private long count = 1L;\n    private boolean isRunning = true;\n    \n    /**\n     * 主要的方法\n     * 启动一个source\n     * 大部分情况下，都需要在这个run方法中实现一个循环，这样就可以循环产生数据了\n     *\n     * @param ctx\n     * @throws Exception\n     */\n    @Override\n    public void run(SourceContext<String> ctx) throws Exception {\n        while(isRunning){\n            //图书的排行榜\n            List<String> books = new ArrayList<>();\n            books.add(\"Pyhton从入门到放弃\");//10\n            books.add(\"Java从入门到放弃\");//8\n            books.add(\"Php从入门到放弃\");//5\n            books.add(\"C++从入门到放弃\");//3\n            books.add(\"Scala从入门到放弃\");//0-4\n            int i = new Random().nextInt(5);\n            ctx.collect(books.get(i));\n\n            //每2秒产生一条数据\n            Thread.sleep(2000);\n        }\n    }\n    //取消一个cancel的时候会调用的方法\n    @Override\n    public void cancel() {\n        isRunning = false;\n    }\n}\n\n```\n\n代码实现了一个发送器，来发送书名<Pyhton从入门到放弃><Java从入门到放弃>等...\n\n然后右键运行我们的程序，控制台输出如下：\n\n![b4819de0dcc717fc2286fbf1e834c31c](13-Flink-Kafka-Connector.resources/5241B014-3ECE-42BD-811F-63C1A95DDCD3.png)\n\n开始源源不断的生产数据了。\n\n然后我们用命令去查看一下 kafka `test`这个topic：\n\n```\nbin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning\n```\n\n输出如下：\n\n![6e94e74c501b60a856d8330a3fa9ceca](13-Flink-Kafka-Connector.resources/89A29311-55D2-4DD0-9B5C-8C1BCDDA2C73.png)\n\n\n### Kafka作为Flink Source\n\n直接上代码：\n\n```\npublic class KafkaConsumer {\n\n    public static void main(String[] args) throws Exception{\n\n        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n        Properties properties = new Properties();\n        properties.setProperty(\"bootstrap.servers\", \"localhost:9092\");\n\n        FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>(\"test\", new SimpleStringSchema(), properties);\n        //从最早开始消费\n        consumer.setStartFromEarliest();\n        DataStream<String> stream = env\n                .addSource(consumer);\n        stream.print();\n        //stream.map();\n        env.execute();\n\n    }\n}//\n```\n\n控制台输出如下：\n\n![40873b1f64d5b99b13508d1c45d1d27d](13-Flink-Kafka-Connector.resources/04A6ACBD-88E2-417D-8DAC-0E93546BA144.png)\n\n将我们之前发往kafka的消息全部打印出来了。\n\n"
  },
  {
    "path": "Flink/14-Flink-Table-&-SQL.md",
    "content": "## 简介\n\nApache Flink具有两个关系API - 表API和SQL - 用于统一流和批处理。Table API是Scala和Java的语言集成查询API，允许以非常直观的方式组合来自关系运算符的查询，Table API和SQL接口彼此紧密集成，以及Flink的DataStream和DataSet API。您可以轻松地在基于API构建的所有API和库之间切换。例如，您可以使用CEP库从DataStream中提取模式，然后使用Table API分析模式，或者可以在预处理上运行Gelly图算法之前使用SQL查询扫描，过滤和聚合批处理表数据。\n\n## Flink SQL的编程模型\n\n\n### 创建一个TableEnvironment\nTableEnvironment是Table API和SQL集成的核心概念，它主要负责:\n　　1、在内部目录中注册一个Table\n　　2、注册一个外部目录\n　　3、执行SQL查询\n　　4、注册一个用户自定义函数(标量、表及聚合)\n　　5、将DataStream或者DataSet转换成Table\n　　6、持有ExecutionEnvironment或者StreamExecutionEnvironment的引用\n一个Table总是会绑定到一个指定的TableEnvironment中，相同的查询不同的TableEnvironment是无法通过join、union合并在一起。\nTableEnvironment有一个在内部通过表名组织起来的表目录，Table API或者SQL查询可以访问注册在目录中的表，并通过名称来引用它们。\n\n### 在目录中注册表\nTableEnvironment允许通过各种源来注册一个表:\n\n　　1、一个已存在的Table对象，通常是Table API或者SQL查询的结果\n         Table projTable = tableEnv.scan(\"X\").select(...);\n\n　　2、TableSource，可以访问外部数据如文件、数据库或者消息系统\n         TableSource csvSource = new CsvTableSource(\"/path/to/file\", ...);\n\n　　3、DataStream或者DataSet程序中的DataStream或者DataSet\n         //将DataSet转换为Table\n         Table table= tableEnv.fromDataSet(tableset);\n\n### 注册TableSink\t\n\n注册TableSink可用于将 Table API或SQL查询的结果发送到外部存储系统，例如数据库，键值存储，消息队列或文件系统（在不同的编码中，例如，CSV，Apache [Parquet] ，Avro，ORC]，......）:\n　　\n```\nTableSink csvSink = new CsvTableSink(\"/path/to/file\", ...); \n　　\n```\n```\n　　2、 String[] fieldNames = {\"a\", \"b\", \"c\"}; \n                TypeInformation[] fieldTypes = {Types.INT, Types.STRING, Types.LONG}; \n                tableEnv.registerTableSink(\"CsvSinkTable\", fieldNames, fieldTypes, csvSink);\n```\n\n## 实战案例一\n\n基于Flink SQL的WordCount:\n\n```\npublic class WordCountSQL {\n\n    public static void main(String[] args) throws Exception{\n\n        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n        BatchTableEnvironment tEnv = TableEnvironment.getTableEnvironment(env);\n\n        List list  =  new ArrayList();\n        String wordsStr = \"Hello Flink Hello TOM\";\n        String[] words = wordsStr.split(\"\\\\W+\");\n        for(String word : words){\n            WC wc = new WC(word, 1);\n            list.add(wc);\n        }\n        DataSet<WC> input = env.fromCollection(list);\n        tEnv.registerDataSet(\"WordCount\", input, \"word, frequency\");\n        Table table = tEnv.sqlQuery(\n                \"SELECT word, SUM(frequency) as frequency FROM WordCount GROUP BY word\");\n        DataSet<WC> result = tEnv.toDataSet(table, WC.class);\n        result.print();\n    }//main\n\n    public static class WC {\n        public String word;//hello\n        public long frequency;//1\n\n        // public constructor to make it a Flink POJO\n        public WC() {}\n\n        public WC(String word, long frequency) {\n            this.word = word;\n            this.frequency = frequency;\n        }\n\n        @Override\n        public String toString() {\n            return \"WC \" + word + \" \" + frequency;\n        }\n    }\n\n}\n```\n输出如下：\n\n```\nWC TOM 1\nWC Hello 2\nWC Flink 1\n```\n\n## 实战案例二\n\n本例稍微复杂，首先读取一个文件中的内容进行统计，并写入到另外一个文件中：\n\n```\npublic class SQLTest {\n\n\tpublic static void main(String[] args) throws Exception{\n\n\t\tExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n\t\tBatchTableEnvironment tableEnv = BatchTableEnvironment.getTableEnvironment(env);\n\t\tenv.setParallelism(1);\n\n\t\tDataSource<String> input = env.readTextFile(\"test.txt\");\n\t\tinput.print();\n\t\t//转换成dataset\n\t\tDataSet<Orders> topInput = input.map(new MapFunction<String, Orders>() {\n\t\t\t@Override\n\t\t\tpublic Orders map(String s) throws Exception {\n\t\t\t\tString[] splits = s.split(\" \");\n\t\t\t\treturn new Orders(Integer.valueOf(splits[0]), String.valueOf(splits[1]),String.valueOf(splits[2]), Double.valueOf(splits[3]));\n\t\t\t}\n\t\t});\n\t\t//将DataSet转换为Table\n\t\tTable order = tableEnv.fromDataSet(topInput);\n\t\t//orders表名\n\t\ttableEnv.registerTable(\"Orders\",order);\n\n\t\tTable tapiResult = tableEnv.scan(\"Orders\").select(\"name\");\n\t\ttapiResult.printSchema();\n\n\t\tTable sqlQuery = tableEnv.sqlQuery(\"select name, sum(price) as total from Orders group by name order by total desc\");\n\n\t\t//转换回dataset\n\t\tDataSet<Result> result = tableEnv.toDataSet(sqlQuery, Result.class);\n\n\t\t//将dataset map成tuple输出\n\t\t/*result.map(new MapFunction<Result, Tuple2<String,Double>>() {\n\t\t\t@Override\n\t\t\tpublic Tuple2<String, Double> map(Result result) throws Exception {\n\t\t\t\tString name = result.name;\n\t\t\t\tDouble total = result.total;\n\t\t\t\treturn Tuple2.of(name,total);\n\t\t\t}\n\t\t}).print();*/\n\n\n\t\tTableSink sink = new CsvTableSink(\"SQLTEST.txt\", \"|\");\n\t\t//writeToSink\n\n\t\t/*sqlQuery.writeToSink(sink);\n\t\tenv.execute();*/\n\n\t\tString[] fieldNames = {\"name\", \"total\"};\n\t\tTypeInformation[] fieldTypes = {Types.STRING, Types.DOUBLE};\n\t\ttableEnv.registerTableSink(\"SQLTEST\", fieldNames, fieldTypes, sink);\n\t\tsqlQuery.insertInto(\"SQLTEST\");\n\t\tenv.execute();\n\t}\n\n\t/**\n\t * 源数据的映射类\n\t */\n\tpublic static class Orders {\n\t\t/**\n\t\t * 序号，姓名，书名，价格\n\t\t */\n\t\tpublic Integer id;\n\t\tpublic String name;\n\t\tpublic String book;\n\t\tpublic Double price;\n\n\t\tpublic Orders() {\n\t\t\tsuper();\n\t\t}\n\t\tpublic Orders(Integer id, String name, String book, Double price) {\n\t\t\tthis.id = id;\n\t\t\tthis.name = name;\n\t\t\tthis.book = book;\n\t\t\tthis.price = price;\n\t\t}\n\t}\n\t/**\n\t * 统计结果对应的类\n\t */\n\tpublic static class Result {\n\t\tpublic String name;\n\t\tpublic Double total;\n\n\t\tpublic Result() {}\n\t}\n\t}//\n```\n\n以上所有代码，大家在公众号回复`Flink`即可下载，可以直接本地运行，方便大家调试"
  },
  {
    "path": "Flink/15-Flink实战项目之实时热销排行.md",
    "content": "\n## 需求\n某个图书网站，希望看到双十一秒杀期间实时的热销排行榜单。我们可以将“实时热门商品”翻译成程序员更好理解的需求:每隔5秒钟输出最近一小时内点击量最多的前 N 个商品/图书.\n\n\n## 需求分解\n\n将这个需求进行分解我们大概要做这么几件事情：\n\n* 告诉 Flink 框架基于时间做窗口，我们这里用processingTime，不用自带时间戳\n* 过滤出图书点击行为数据\n* 按一小时的窗口大小，每5秒钟统计一次，做滑动窗口聚合（Sliding Window）\n* 聚合，输出窗口中点击量前N名的商品\n\n\n\n## 代码实现\n\n### 向Kafka发消息模拟购买事件\n\n```\npublic class KafkaProducer {\n\n\n    public static void main(String[] args) throws Exception{\n\n        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n\n        DataStreamSource<String> text = env.addSource(new MyNoParalleSource()).setParallelism(1);\n\n        Properties properties = new Properties();\n        properties.setProperty(\"bootstrap.servers\", \"127.0.0.1:9092\");\n        //new FlinkKafkaProducer(\"topn\",new KeyedSerializationSchemaWrapper(new SimpleStringSchema()),properties,FlinkKafkaProducer.Semantic.EXACTLY_ONCE);\n\t    FlinkKafkaProducer<String> producer = new FlinkKafkaProducer(\"topn\",new SimpleStringSchema(),properties);\n/*\n        //event-timestamp事件的发生时间\n        producer.setWriteTimestampToKafka(true);\n*/\n        text.addSink(producer);\n        env.execute();\n    }\n}//\n```\n其中的：`MyNoParalleSource` 是作者自己实现的一个并行度为1的发送器，用来向kafka发送数据：\n\n```\npublic class MyNoParalleSource implements SourceFunction<String> {//1\n\n    //private long count = 1L;\n    private boolean isRunning = true;\n\n    /**\n     * 主要的方法\n     * 启动一个source\n     * 大部分情况下，都需要在这个run方法中实现一个循环，这样就可以循环产生数据了\n     *\n     * @param ctx\n     * @throws Exception\n     */\n    @Override\n    public void run(SourceContext<String> ctx) throws Exception {\n        while(isRunning){\n            //图书的排行榜\n            List<String> books = new ArrayList<>();\n            books.add(\"Pyhton从入门到放弃\");//10\n            books.add(\"Java从入门到放弃\");//8\n            books.add(\"Php从入门到放弃\");//5\n            books.add(\"C++从入门到放弃\");//3\n            books.add(\"Scala从入门到放弃\");//0-4\n            int i = new Random().nextInt(5);\n            ctx.collect(books.get(i));\n\n            //每1秒产生一条数据\n            Thread.sleep(1000);\n        }\n    }\n    //取消一个cancel的时候会调用的方法\n    @Override\n    public void cancel() {\n        isRunning = false;\n    }\n}\n\n```\n\n可见，我们每过1秒向Kafka的topn这个topic随机发送一本书的名字用来模拟购买行为。\n\n整体实现代码如下：\n\n```\npublic class TopN {\n\n\tpublic static void main(String[] args) throws Exception{\n\n\t\t/**\n\t\t *\n\t\t *  书1 书2 书3\n\t\t *  （书1,1） (书2，1) （书3,1）\n\t\t *\n\t\t *\n\t\t */\n\t\t//每隔5秒钟 计算过去1小时 的 Top 3 商品\n\t\tStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n\n\t\tenv.setParallelism(1);\n\n\t\tenv.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime); //以processtime作为时间语义\n\n\n\t\tProperties properties = new Properties();\n\t\tproperties.setProperty(\"bootstrap.servers\", \"127.0.0.1:9092\");\n\t\tFlinkKafkaConsumer<String> input = new FlinkKafkaConsumer<>(\"topn\", new SimpleStringSchema(), properties);\n\n\t\t//从最早开始消费 位点\n\t\tinput.setStartFromEarliest();\n\n\n\t\tDataStream<String> stream = env\n\t\t\t\t.addSource(input);\n\n\t\tDataStream<Tuple2<String, Integer>> ds = stream\n\t\t\t\t.flatMap(new LineSplitter()); //将输入语句split成一个一个单词并初始化count值为1的Tuple2<String, Integer>类型\n\n\n\t\tDataStream<Tuple2<String, Integer>> wcount = ds\n\t\t\t\t.keyBy(0)\n\t\t\t\t.window(SlidingProcessingTimeWindows.of(Time.seconds(600),Time.seconds(5)))\n\t\t\t\t//key之后的元素进入一个总时间长度为600s,每5s向后滑动一次的滑动窗口\n\t\t\t\t.sum(1);// 将相同的key的元素第二个count值相加\n\n\t\twcount\n\t\t\t\t.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(5)))//(shu1, xx) (shu2,xx)....\n\t\t\t\t//所有key元素进入一个5s长的窗口（选5秒是因为上游窗口每5s计算一轮数据，topN窗口一次计算只统计一个窗口时间内的变化）\n\t\t\t\t.process(new TopNAllFunction(3))\n\t\t\t\t.print();\n//redis sink  redis -> 接口\n\n\t\tenv.execute();\n\t}//\n\n\n\n\n\n\tprivate static final class LineSplitter implements\n\t\t\tFlatMapFunction<String, Tuple2<String, Integer>> {\n\n\t\tpublic void flatMap(String value, Collector<Tuple2<String, Integer>> out) {\n\t\t\t// normalize and split the line\n\t\t\t//String[] tokens = value.toLowerCase().split(\"\\\\W+\");\n\n\t\t\t// emit the pairs\n\t\t\t/*for (String token : tokens) {\n\t\t\t\tif (token.length() > 0) {\n\t\t\t\t\tout.collect(new Tuple2<String, Integer>(token, 1));\n\t\t\t\t}\n\t\t\t}*/\n\n\t\t\t//（书1,1） (书2，1) （书3,1）\n\t\t\tout.collect(new Tuple2<String, Integer>(value, 1));\n\t\t}\n\t}\n\n\tprivate static class TopNAllFunction\n\t\t\textends\n\t\t\tProcessAllWindowFunction<Tuple2<String, Integer>, String, TimeWindow> {\n\n\t\tprivate int topSize = 3;\n\n\t\tpublic TopNAllFunction(int topSize) {\n\n\t\t\tthis.topSize = topSize;\n\t\t}\n\n\t\tpublic void process(\n\n\t\t\t\tProcessAllWindowFunction<Tuple2<String, Integer>, String, TimeWindow>.Context arg0,\n\t\t\t\tIterable<Tuple2<String, Integer>> input,\n\t\t\t\tCollector<String> out) throws Exception {\n\n\t\t\tTreeMap<Integer, Tuple2<String, Integer>> treemap = new TreeMap<Integer, Tuple2<String, Integer>>(\n\t\t\t\t\tnew Comparator<Integer>() {\n\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic int compare(Integer y, Integer x) {\n\t\t\t\t\t\t\treturn (x < y) ? -1 : 1;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}); //treemap按照key降序排列，相同count值不覆盖\n\n\t\t\tfor (Tuple2<String, Integer> element : input) {\n\t\t\t\ttreemap.put(element.f1, element);\n\t\t\t\tif (treemap.size() > topSize) { //只保留前面TopN个元素\n\t\t\t\t\ttreemap.pollLastEntry();\n\t\t\t\t}\n\t\t\t}\n\n\n\t\t\tfor (Map.Entry<Integer, Tuple2<String, Integer>> entry : treemap\n\t\t\t\t\t.entrySet()) {\n\t\t\t\tout.collect(\"=================\\n热销图书列表:\\n\"+ new Timestamp(System.currentTimeMillis()) +  treemap.toString() + \"\\n===============\\n\");\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\n}//\n```\n\n查看输出：\n```\n=================\n热销图书列表:\n2019-03-05 22:32:40.004{8=(Java从入门到放弃,8), 7=(C++从入门到放弃,7), 5=(Php从入门到放弃,5)}\n===============\n=================\n热销图书列表:\n2019-03-05 22:32:45.004{8=(Java从入门到放弃,8), 7=(C++从入门到放弃,7), 5=(Php从入门到放弃,5)}\n===============\n\n```"
  },
  {
    "path": "Flink/16-Flink-Redis-Sink.md",
    "content": "## 简介\n\n流式计算中，我们经常有一些场景是消费Kafka数据，进行处理，然后存储到其他的数据库或者缓存或者重新发送回其他的消息队列中。\n本文讲述一个简单的Redis作为Sink的案例。\n后续，我们会补充完善，比如落入Hbase，Kafka，Mysql等。\n\n\n## 关于Redis Sink\n\nFlink提供了封装好的写入Redis的包给我们用，首先我们要新增一个依赖：\n```\n<dependency>\n    <groupId>org.apache.flink</groupId>\n    <artifactId>flink-connector-redis_2.10</artifactId>\n    <version>1.1.5</version>\n</dependency>\n\n```\n\n\n然后我们实现一个自己的RedisSinkExample：\n\n```\n//指定Redis set\npublic static final class RedisSinkExample implements RedisMapper<Tuple2<String,Integer>> {\npublic RedisCommandDescription getCommandDescription() {\n    return new RedisCommandDescription(RedisCommand.SET, null);\n}\n\npublic String getKeyFromData(Tuple2<String, Integer> data) {\n    return data.f0;\n}\n\npublic String getValueFromData(Tuple2<String, Integer> data) {\n    return data.f1.toString();\n}\n}\n```\n\n我们用最简单的单机Redis的SET命令进行演示。\n\n完整的代码如下，实现一个读取Kafka的消息，然后进行WordCount，并把结果更新到redis中：\n\n```\n\npublic class RedisSinkTest {\n\npublic static void main(String[] args) throws Exception{\n\nStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\nenv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);\nenv.enableCheckpointing(2000);\nenv.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);\n\n//连接kafka\nProperties properties = new Properties();\nproperties.setProperty(\"bootstrap.servers\", \"127.0.0.1:9092\");\n\nFlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>(\"test\", new SimpleStringSchema(), properties);\nconsumer.setStartFromEarliest();\nDataStream<String> stream = env.addSource(consumer);\nDataStream<Tuple2<String, Integer>> counts = stream.flatMap(new LineSplitter()).keyBy(0).sum(1);\n\n//实例化FlinkJedisPoolConfig 配置redis\nFlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder().setHost(\"127.0.0.1\").setPort(\"6379\").build();\n//实例化RedisSink，并通过flink的addSink的方式将flink计算的结果插入到redis\n\ncounts.addSink(new RedisSink<>(conf,new RedisSinkExample()));\nenv.execute(\"WordCount From Kafka To Redis\");\n\n}//\npublic static final class LineSplitter implements FlatMapFunction<String, Tuple2<String, Integer>> {\n\n@Override\npublic void flatMap(String value, Collector<Tuple2<String, Integer>> out) {\n    String[] tokens = value.toLowerCase().split(\"\\\\W+\");\n    for (String token : tokens) {\n        if (token.length() > 0) {\n            out.collect(new Tuple2<String, Integer>(token, 1));\n        }\n    }\n}\n}\n//指定Redis set\npublic static final class RedisSinkExample implements RedisMapper<Tuple2<String,Integer>> {\npublic RedisCommandDescription getCommandDescription() {\n    return new RedisCommandDescription(RedisCommand.SET, null);\n}\n\npublic String getKeyFromData(Tuple2<String, Integer> data) {\n    return data.f0;\n}\n\npublic String getValueFromData(Tuple2<String, Integer> data) {\n    return data.f1.toString();\n}\n}\n\n}//\n\n```\n预告，后续更新写入Hbase和Mysql案例代码。\n"
  },
  {
    "path": "Flink/17-Flink消费Kafka写入Mysql.md",
    "content": "\n\n本文介绍消费Kafka的消息实时写入Mysql\n\n1. maven新增依赖：\n\n```\n<dependency>\n    <groupId>mysql</groupId>\n    <artifactId>mysql-connector-java</artifactId>\n    <version>5.1.39</version>\n</dependency>\n\n```\n\n\n2.重写RichSinkFunction,实现一个Mysql Sink\n\n```\npublic class MysqlSink extends\n    RichSinkFunction<Tuple3<Integer, String, Integer>> {\nprivate Connection connection;\nprivate PreparedStatement preparedStatement;\nString username = \"\";\nString password = \"\";\nString drivername = \"\";   //配置改成自己的配置\nString dburl = \"\";\n\n@Override\npublic void invoke(Tuple3<Integer, String, Integer> value) throws Exception {\n    Class.forName(drivername);\n    connection = DriverManager.getConnection(dburl, username, password);\n    String sql = \"replace into table(id,num,price) values(?,?,?)\"; //假设mysql 有3列 id,num,price\n    preparedStatement = connection.prepareStatement(sql);\n    preparedStatement.setInt(1, value.f0);\n    preparedStatement.setString(2, value.f1);\n    preparedStatement.setInt(3, value.f2);\n    preparedStatement.executeUpdate();\n    if (preparedStatement != null) {\n        preparedStatement.close();\n    }\n    if (connection != null) {\n        connection.close();\n    }\n}\n}\n```\n\n3. Flink主类\n\n```\npublic class MysqlSinkTest {\n\npublic static void main(String[] args) throws Exception {\nStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\nProperties properties = new Properties();\nproperties.setProperty(\"bootstrap.servers\", \"localhost:9092\");\n\n// 1,abc,100  类似这样的数据，当然也可以是很复杂的json数据，去做解析\nFlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>(\"test\", new SimpleStringSchema(), properties);\nenv.getConfig().disableSysoutLogging();  //设置此可以屏蔽掉日记打印情况\nenv.getConfig().setRestartStrategy(\n        RestartStrategies.fixedDelayRestart(5, 5000));\nenv.enableCheckpointing(2000);\nDataStream<String> stream = env\n        .addSource(consumer);\n\nDataStream<Tuple3<Integer, String, Integer>> sourceStream = stream.filter((FilterFunction<String>) value -> StringUtils.isNotBlank(value))\n                                                                        .map((MapFunction<String, Tuple3<Integer, String, Integer>>) value -> {\n    String[] args1 = value.split(\",\");\n    return new Tuple3<Integer, String, Integer>(Integer\n            .valueOf(args1[0]), args1[1],Integer\n            .valueOf(args1[2]));\n});\n\nsourceStream.addSink(new MysqlSink());\nenv.execute(\"data to mysql start\");\n}\n}\n\n```\n"
  },
  {
    "path": "Flink/6-Flink重启策略.md",
    "content": "\n## 概述\n\n* Flink支持不同的重启策略，以在故障发生时控制作业如何重启\n* 集群在启动时会伴随一个默认的重启策略，在没有定义具体重启策略时会使用该默认策略。 \n* 如果在工作提交时指定了一个重启策略，该策略会覆盖集群的默认策略默认的重启策略可以通过 Flink 的配置文件 flink-conf.yaml 指定。配置参数 restart-strategy 定义了哪个策略被使用。\n* 常用的重启：\n\n    1.策略固定间隔 (Fixed delay)\n    2.失败率 (Failure rate)\n    3.无重启 (No restart)\n\n* 如果没有启用 checkpointing，则使用无重启 (no restart) 策略。如果启用了 checkpointing，但没有配置重启策略，则使用固定间隔 (fixed-delay) 策略\n* 重启策略可以在flink-conf.yaml中配置，表示全局的配置。也可以在应用代码中动态指定，会覆盖全局配置\n\n\n## 固定间隔\n\n第一种：全局配置 flink-conf.yaml\n```\n\trestart-strategy: fixed-delay \n\trestart-strategy.fixed-delay.attempts: 3 \n\trestart-strategy.fixed-delay.delay: 10 s\n```\n第二种：应用代码设置：\n\t\n    ```\n    env.setRestartStrategy(RestartStrategies.fixedDelayRestart( 3,// 尝试重启的次数 \n        Time.of(10, TimeUnit.SECONDS) // 间隔 ));\n    ```\n\n## 失败率\n\n* 失败率重启策略在Job失败后会重启，但是超过失败率后，Job会最终被认定失败。在两个连续的重启尝试之间，重启策略会等待一个固定的时间\n\n**下面配置是5分钟内若失败了3次则认为该job失败，重试间隔为10s**\n\n第一种：全局配置 flink-conf.yaml\n```\n    restart-strategy: failure-rate  \n\trestart-strategy.failure-rate.max-failures-per-interval: 3  \n\trestart-strategy.failure-rate.failure-rate-interval: 5 min  \n\trestart-strategy.failure-rate.delay: 10 s\n```\n    \n第二种：应用代码设置\n\n```\n   env.setRestartStrategy(RestartStrategies.failureRateRestart(  3,//一个时间段内的最大失败次数  \nTime.of(5, TimeUnit.MINUTES), // 衡量失败次数的是时间段  Time.of(10, TimeUnit.SECONDS) // 间隔  ));\n```\n\n## 无重启策略\n\n第一种：全局配置 flink-conf.yaml\n\n```\n\trestart-strategy: none\n```\n\n第二种：应用代码设置\n```\nExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); \tenv.setRestartStrategy(RestartStrategies.noRestart());\n\n```\n\n\n## 实际代码演示\n\n```\npublic class RestartTest {\n\n    public static void main(String[] args) {\n        //获取flink的运行环境\n        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n\n        // 每隔1000 ms进行启动一个检查点【设置checkpoint的周期】\n        env.enableCheckpointing(1000);\n\n        // 间隔10秒 重启3次\n        env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3,Time.seconds(10)));\n\n        //5分钟内若失败了3次则认为该job失败，重试间隔为10s\n        env.setRestartStrategy(RestartStrategies.failureRateRestart(3,Time.of(5,TimeUnit.MINUTES),Time.of(10,TimeUnit.SECONDS)));\n\n        //不重试\n        env.setRestartStrategy(RestartStrategies.noRestart());\n    }//\n\n}\n```\n\n"
  },
  {
    "path": "Flink/7-Flink的分布式缓存.md",
    "content": "\n## 分布式缓存\n\nFlink提供了一个分布式缓存，类似于hadoop，可以使用户在并行函数中很方便的读取本地文件，并把它放在taskmanager节点中，防止task重复拉取。\n此缓存的工作机制如下：程序注册一个文件或者目录(本地或者远程文件系统，例如hdfs或者s3)，通过ExecutionEnvironment注册缓存文件并为它起一个名称。\n当程序执行，Flink自动将文件或者目录复制到所有taskmanager节点的本地文件系统，仅会执行一次。用户可以通过这个指定的名称查找文件或者目录，然后从taskmanager节点的本地文件系统访问它。\n\n\n## 示例\n\n在ExecutionEnvironment中注册一个文件：\n\n```\n//获取运行环境\nExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n\n\n//1：注册一个文件,可以使用hdfs上的文件 也可以是本地文件进行测试\nenv.registerCachedFile(\"/Users/wangzhiwu/WorkSpace/quickstart/text\",\"a.txt\");\n\n```\n\n在用户函数中访问缓存文件或者目录(这里是一个map函数)。这个函数必须继承RichFunction,因为它需要使用RuntimeContext读取数据:\n\n```\nDataSet<String> result = data.map(new RichMapFunction<String, String>() {\n            private ArrayList<String> dataList = new ArrayList<String>();\n\n            @Override\n            public void open(Configuration parameters) throws Exception {\n                super.open(parameters);\n                //2：使用文件\n                File myFile = getRuntimeContext().getDistributedCache().getFile(\"a.txt\");\n                List<String> lines = FileUtils.readLines(myFile);\n                for (String line : lines) {\n                    this.dataList.add(line);\n                    System.err.println(\"分布式缓存为:\" + line);\n                }\n            }\n\n            @Override\n            public String map(String value) throws Exception {\n                //在这里就可以使用dataList\n                System.err.println(\"使用datalist：\" + dataList + \"------------\" +value);\n                //业务逻辑\n                return dataList +\"：\" +  value;\n            }\n        });\n\n        result.printToErr();\n    }\n```\n\n完整代码如下,仔细看注释：\n\n```\n\npublic class DisCacheTest {\n\n    public static void main(String[] args) throws Exception{\n\n        //获取运行环境\n        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n\n        //1：注册一个文件,可以使用hdfs上的文件 也可以是本地文件进行测试\n      //text 中有4个单词:hello flink hello FLINK env.registerCachedFile(\"/Users/wangzhiwu/WorkSpace/quickstart/text\",\"a.txt\");\n\n        DataSource<String> data = env.fromElements(\"a\", \"b\", \"c\", \"d\");\n\n        DataSet<String> result = data.map(new RichMapFunction<String, String>() {\n            private ArrayList<String> dataList = new ArrayList<String>();\n\n            @Override\n            public void open(Configuration parameters) throws Exception {\n                super.open(parameters);\n                //2：使用文件\n                File myFile = getRuntimeContext().getDistributedCache().getFile(\"a.txt\");\n                List<String> lines = FileUtils.readLines(myFile);\n                for (String line : lines) {\n                    this.dataList.add(line);\n                    System.err.println(\"分布式缓存为:\" + line);\n                }\n            }\n\n            @Override\n            public String map(String value) throws Exception {\n                //在这里就可以使用dataList\n                System.err.println(\"使用datalist：\" + dataList + \"------------\" +value);\n                //业务逻辑\n                return dataList +\"：\" +  value;\n            }\n        });\n\n        result.printToErr();\n    }\n}//\n\n```\n\n输出结果如下：\n```\n[hello, flink, hello, FLINK]：a\n[hello, flink, hello, FLINK]：b\n[hello, flink, hello, FLINK]：c\n[hello, flink, hello, FLINK]：d\n```\n"
  },
  {
    "path": "Flink/8-Flink中的窗口.md",
    "content": "\n## 窗口\n\n### 窗口类型\n1. flink支持两种划分窗口的方式（time和count）    如果根据时间划分窗口，那么它就是一个time-window    如果根据数据划分窗口，那么它就是一个count-window\n\n2. flink支持窗口的两个重要属性（size和interval）    \n\n* 如果size=interval,那么就会形成tumbling-window(无重叠数据)    \n* 如果size>interval,那么就会形成sliding-window(有重叠数据)    \n* 如果size<interval,那么这种窗口将会丢失数据。比如每5秒钟，统计过去3秒的通过路口汽车的数据，将会漏掉2秒钟的数据。\n\n3. 通过组合可以得出四种基本窗口：\n\n* `time-tumbling-window` 无重叠数据的时间窗口，设置方式举例：timeWindow(Time.seconds(5)) \n   \n* `time-sliding-window`  有重叠数据的时间窗口，设置方式举例：timeWindow(Time.seconds(5), Time.seconds(3)) \n\n\n* `count-tumbling-window`无重叠数据的数量窗口，设置方式举例：countWindow(5)    \n\n\n\n* `count-sliding-window` 有重叠数据的数量窗口，设置方式举例：countWindow(5,3)\n \n4. flink支持在stream上的通过key去区分多个窗口\n\n      \n### 窗口的实现方式 \n\n上一张经典图：\n\n![1ee4f402b32040ef6ed9dcc44a212078](8-Flink中的窗口.resources/CD34BCB4-E2D4-41AD-B8E7-B6EFDB0BAB82.png)\n\n\n* Tumbling Time Window\n\n假如我们需要统计每一分钟中用户购买的商品的总数，需要将用户的行为事件按每一分钟进行切分，这种切分被成为翻滚时间窗口（Tumbling Time Window）。翻滚窗口能将数据流切分成不重叠的窗口，每一个事件只能属于一个窗口。\n\n```\n// 用户id和购买数量 stream\nval counts: DataStream[(Int, Int)] = ...\nval tumblingCnts: DataStream[(Int, Int)] = counts\n  // 用userId分组\n  .keyBy(0) \n  // 1分钟的翻滚窗口宽度\n  .timeWindow(Time.minutes(1))\n  // 计算购买数量\n  .sum(1) \n```\n\n* Sliding Time Window\n\n我们可以每30秒计算一次最近一分钟用户购买的商品总数。这种窗口我们称为滑动时间窗口（Sliding Time Window）。在滑窗中，一个元素可以对应多个窗口。通过使用 DataStream API，我们可以这样实现：\n\n```\nval slidingCnts: DataStream[(Int, Int)] = buyCnts\n  .keyBy(0) \n  .timeWindow(Time.minutes(1), Time.seconds(30))\n  .sum(1)\n```\n\n* Tumbling Count Window\n\n当我们想要每100个用户购买行为事件统计购买总数，那么每当窗口中填满100个元素了，就会对窗口进行计算，这种窗口我们称之为翻滚计数窗口（Tumbling Count Window），上图所示窗口大小为3个。通过使用 DataStream API，我们可以这样实现：\n\n```\n// Stream of (userId, buyCnts)\nval buyCnts: DataStream[(Int, Int)] = ...\n\nval tumblingCnts: DataStream[(Int, Int)] = buyCnts\n  // key stream by sensorId\n  .keyBy(0)\n  // tumbling count window of 100 elements size\n  .countWindow(100)\n  // compute the buyCnt sum \n  .sum(1)\n```\n\n* Session Window\n\n在这种用户交互事件流中，我们首先想到的是将事件聚合到会话窗口中（一段用户持续活跃的周期），由非活跃的间隙分隔开。如上图所示，就是需要计算每个用户在活跃期间总共购买的商品数量，如果用户30秒没有活动则视为会话断开（假设raw data stream是单个用户的购买行为流）。Session Window 的示例代码如下：\n\n\n```\n// Stream of (userId, buyCnts)\nval buyCnts: DataStream[(Int, Int)] = ...\n    \nval sessionCnts: DataStream[(Int, Int)] = vehicleCnts\n    .keyBy(0)\n    // session window based on a 30 seconds session gap interval \n    .window(ProcessingTimeSessionWindows.withGap(Time.seconds(30)))\n    .sum(1)\n```\n\n一般而言，window 是在无限的流上定义了一个有限的元素集合。这个集合可以是基于时间的，元素个数的，时间和个数结合的，会话间隙的，或者是自定义的。Flink 的 DataStream API 提供了简洁的算子来满足常用的窗口操作，同时提供了通用的窗口机制来允许用户自己定义窗口分配逻辑。\n\n"
  },
  {
    "path": "Flink/9-Flink中的Time.md",
    "content": "## 时间\n\n### 时间类型\n\n* Flink中的时间与现实世界中的时间是不一致的，在flink中被划分为**事件时间，摄入时间，处理时间**三种。\n\n* 如果以EventTime为基准来定义时间窗口将形成EventTimeWindow,要求消息本身就应该携带EventTime\n\n* 如果以IngesingtTime为基准来定义时间窗口将形成IngestingTimeWindow,以source的systemTime为准。\n\n* 如果以ProcessingTime基准来定义时间窗口将形成ProcessingTimeWindow，以operator的systemTime为准。\n\n\n![54544878430fd9d3047b60740a0161f8](9-Flink中的Time.resources/895DEF14-E8B4-4A1C-9D96-66D88517DDA6.png)\n\n\n### 时间详解\n\n**Processing Time**\n\nProcessing Time 是指事件被处理时机器的系统时间。\n\n\n当流程序在 Processing Time 上运行时，所有基于时间的操作(如时间窗口)将使用当时机器的系统时间。每小时 Processing Time 窗口将包括在系统时钟指示整个小时之间到达特定操作的所有事件。\n\n例如，如果应用程序在上午 9:15 开始运行，则第一个每小时 Processing Time 窗口将包括在上午 9:15 到上午 10:00 之间处理的事件，下一个窗口将包括在上午 10:00 到 11:00 之间处理的事件。\n\nProcessing Time 是最简单的 “Time” 概念，不需要流和机器之间的协调，它提供了最好的性能和最低的延迟。但是，在分布式和异步的环境下，Processing Time 不能提供确定性，因为它容易受到事件到达系统的速度（例如从消息队列）、事件在系统内操作流动的速度以及中断的影响。\n\n**Event Time**\n\nEvent Time 是事件发生的时间，一般就是数据本身携带的时间。这个时间通常是在事件到达 Flink 之前就确定的，并且可以从每个事件中获取到事件时间戳。在 Event Time 中，时间取决于数据，而跟其他没什么关系。Event Time 程序必须指定如何生成 Event Time 水印，这是表示 Event Time 进度的机制。\n\n完美的说，无论事件什么时候到达或者其怎么排序，最后处理 Event Time 将产生完全一致和确定的结果。但是，除非事件按照已知顺序（按照事件的时间）到达，否则处理 Event Time 时将会因为要等待一些无序事件而产生一些延迟。由于只能等待一段有限的时间，因此就难以保证处理 Event Time 将产生完全一致和确定的结果。\n\n假设所有数据都已到达， Event Time 操作将按照预期运行，即使在处理无序事件、延迟事件、重新处理历史数据时也会产生正确且一致的结果。 例如，每小时事件时间窗口将包含带有落入该小时的事件时间戳的所有记录，无论它们到达的顺序如何。\n\n请注意，有时当 Event Time 程序实时处理实时数据时，它们将使用一些 Processing Time 操作，以确保它们及时进行。\n\n\n**Ingestion Time**\n\nIngestion Time 是事件进入 Flink 的时间。 在源操作处，每个事件将源的当前时间作为时间戳，并且基于时间的操作（如时间窗口）会利用这个时间戳。\n\nIngestion Time 在概念上位于 Event Time 和 Processing Time 之间。 与 Processing Time 相比，它稍微贵一些，但结果更可预测。因为 Ingestion Time 使用稳定的时间戳（在源处分配一次），所以对事件的不同窗口操作将引用相同的时间戳，而在 Processing Time 中，每个窗口操作符可以将事件分配给不同的窗口（基于机器系统时间和到达延迟）。\n\n与 Event Time 相比，Ingestion Time 程序无法处理任何无序事件或延迟数据，但程序不必指定如何生成水印。\n\n在 Flink 中，Ingestion Time 与 Event Time 非常相似，但 Ingestion Time 具有自动分配时间戳和自动生成水印功能。\n\n\n"
  },
  {
    "path": "Flink/Flink从入门到放弃(入门篇1)-Flink是什么？.md",
    "content": "\n\n> 本文是例行介绍，熟悉的直接跳过 - 鲁迅\n\n> 鲁迅: ...\n\n# 大纲\n\n**入门篇：**\n![8bf609799f0f1265e71fa5bd0d498c45](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/1.png)\n\n**放弃篇：**\n![563c79efb6518f991c732f1d95f97a62](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/A44BE2B6-FBC9-4143-9743-F097B9C0FDD6.png)\n\n\n\n\n## Flink是什么\n\n## 一句话概括\nApache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台，提供支持流处理和批处理两种类型应用的功能。\n\n## 前身\nApache Flink 的前身是柏林理工大学一个研究性项目， 在 2014 被 Apache 孵化器所接受，然后迅速地成为了Apache Software Foundation的顶级项目之一。\n\n## 特点\n\n现有的开源计算方案，会把流处理和批处理作为两种不同的应用类型：流处理一般需要支持低延迟、Exactly-once保证，而批处理需要支持高吞吐、高效处理。\nFlink是完全支持流处理，也就是说作为流处理看待时输入数据流是无界的；批处理被作为一种特殊的流处理，只是它的输入数据流被定义为有界的。\n\n\n## Flink组件栈\n\n![102b82e4ca65fa679cee53c017d830aa](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/6F963775-B91B-447F-959E-38B4029BE56D.png)\n\n### Deployment层\t\n主要涉及了Flink的部署模式，Flink支持多种部署模式：本地、集群（Standalone/YARN）、云（GCE/EC2）\n\n![cd3ae86f9ae0764f77af85696114d79b](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/F7406066-68CA-4BE7-9743-7FD65A0D722C.png)\n\n### Runtime层 \n\nRuntime层提供了支持Flink计算的全部核心实现，比如：支持分布式Stream处理、JobGraph到ExecutionGraph的映射、调度等等，为上层API层提供基础服务\n\n### API层\n\nAPI层主要实现了面向无界Stream的流处理和面向Batch的批处理API，其中面向流处理对应DataStream API，面向批处理对应DataSet API \n\n### Libaries层 \n\n* 在API层之上构建的满足特定应用的实现计算框架，也分别对应于面向流处理和面向批处理两类\n\n* 面向流处理支持：CEP（复杂事件处理）、基于SQL-like的操作（基于Table的关系操作）\n\n* 面向批处理支持：FlinkML（机器学习库）、Gelly（图处理）\n\n\n## Flink的优势\n\n* 支持高吞吐、低延迟、高性能的流处理\n* 支持高度灵活的窗口（Window）操作\n* 支持有状态计算的Exactly-once语义\n* 提供DataStream API和DataSet API\n\n![d698791fc0eee2b74bfb9af430206705](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/3DE5BD22-BFE2-49C4-8DA8-C42EAD1948FB.png)\n\n![f49a3e84af366184696c6c6800d84a50](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/0E6F6341-5EB0-40FD-9953-70C3F0904043.png)\n\n\n\n## Flink基本编程模型\n\n> * Flink程序的基础构建模块是流(streams) 与 转换(transformations)\n> * 每一个数据流起始于一个或多个 source，并终止于一个或多个 sink\n\n\n下面是一个由Flink程序映射为Streaming Dataflow的示意图:\n\n![1cee64d1b99673231aa5315d579d5182](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/656C0986-42A7-4E76-B3CA-C0372395E451.png)\n\n并行数据流示意图:\n![b51a95236221451ab1958f8aefc5af62](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/E6A4AF88-12D9-413A-A318-06A86ABDC1AF.png)\n\n\n## Flink基本架构\n\n> * Flink是基于Master-Slave风格的架构\n> * Flink集群启动时，会启动一个JobManager进程、至少一个TaskManager进程\n\n![7c947040b492ea28cd48252a0f1427a7](Flink从入门到放弃(入门篇1)-Flink是什么？.resources/866EF50B-A9ED-461A-AC13-78BEBBDCCFC9.png)\n\n### JobManager\n\n* Flink系统的协调者，它负责接收Flink Job，调度组成Job的多个Task的执行\n\n* 收集Job的状态信息，并管理Flink集群中从节点TaskManager\n\n### TaskManager\n\n* 实际负责执行计算的Worker，在其上执行Flink Job的一组Task\n* TaskManager负责管理其所在节点上的资源信息，如内存、磁盘、网络，在启动的时候将资源的状态向JobManager汇报\n\n### Client\n\n* 用户提交一个Flink程序时，会首先创建一个Client，该Client首先会对用户提交的Flink程序进行预处理，并提交到Flink集群\n\n* Client会将用户提交的Flink程序组装一个JobGraph， 并且是以JobGraph的形式提交的\n\n\n## 最后\n\n本文是例行介绍，熟悉的直接跳过。\n"
  },
  {
    "path": "Flink/Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.md",
    "content": "## 本地安装单机版本Flink\n\n一般来说，线上都是集群模式，那么单机模式方便我们测试和学习。\n\n### 环境要求\n\n本地机器上需要有 Java 8 和 maven 环境，推荐在linux或者mac上开发Flink应用：\n\n如果有 Java 8 环境，运行下面的命令会输出如下版本信息：\n\n![510a825bb50a810b2f950590f894c5a9](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/34F234C6-C9D6-46AB-A864-652BE177B4CA.png)\n\n如果有 maven 环境，运行下面的命令会输出如下版本信息：\n\n![d466ed9d3338048745899c7db8150c5a](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/1A1D2049-1042-43E1-BE0B-6D9FAA8224BE.png)\n\n\n\n开发工具推荐使用 ItelliJ IDEA。\n\n#### 第一种方式\n\n来这里[https://flink.apache.org/](https://flink.apache.org/)\n\n看这里：\n![50d9244cad5d21c4fffca5c1af808977](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/E0A8FC57-9184-4BE8-8D20-BDD91C3C44FD.png)\n\n>注意：\n```\nAn Apache Hadoop installation is not required to use Apache Flink. For users that use Flink without any Hadoop components, we recommend the release without bundled Hadoop libraries.\n```\n\n这是啥意思？\n这个意思就是说Flink可以不依赖Hadoop环境，如果说单机玩的话，下载一个`only`版本就行了。\n\n\n#### 第二种方式(不推荐)\n\n```\ngit clone https://github.com/apache/flink.git \ncd flink\nmvn clean package -DskipTests  \n```\n然后进入编译好的Flink中去执行 `bin/start-cluster.sh`\n\n### 其他乱七八糟的安装办法\n\n比如 Mac用户可以用`brew install apache-flink` ,前提是安装过 `brew`这个mac下的工具.\n\n## 启动Flink\n\n我们先到Flink的目录下来：\n如下：\n```\n$ flink-1.7.1 pwd\n/Users/wangzhiwu/Downloads/flink-1.7.1\n```\n![19d1b2fb4d6b1fb3bff67522a239f2ca](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/BE68C066-BD15-4FAF-B649-82D9B26F255D.png)\n\n执行命令：\n\n![a596b49f69bd4fcd8058bfc7800d5cc0](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/C88AEAF7-42B7-4AD1-A793-3E89EBE751E2.png)\n\n接着就可以进入 web 页面(http://localhost:8081/) 查看\n\n![cd63db57b32a8862c5e0d2e8152f83ec](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/DAEECBBB-0FB7-4D4E-B338-B3181C23B6CB.png)\n\n恭喜你，一个单机版的flink就跑起来了。\n\n\n## 构建一个应用\n\n当然了，我们可以用maven，一顿new，new出来一个过程，这里我们将使用 Flink Maven Archetype 来创建我们的项目结构和一些初始的默认依赖。在你的工作目录下，运行如下命令来创建项目：\n\n```\nmvn archetype:generate \\\n    -DarchetypeGroupId=org.apache.flink \\\n    -DarchetypeArtifactId=flink-quickstart-java \\\n    -DarchetypeVersion=1.7.2 \\\n    -DgroupId=flink-project \\\n    -DartifactId=flink-project \\\n    -Dversion=0.1 \\\n    -Dpackage=myflink \\\n    -DinteractiveMode=false\n```\n这样一个工程就构建好了。\n\n还有一个更加牛逼的办法，看这里：\n\n```\ncurl https://flink.apache.org/q/quickstart.sh | bash\n```\n直接在命令行执行上面的命令，结果如下图：\n\n![92b875adb611a4ce657c7535c19fd8b1](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/A78DC26C-BD00-44A9-9481-FE67B9BAE9CF.png)\n\n同样可以构建一个Flink工程，而且自带一些demo。\n\n原理是什么？点一下它看看就明白了。\n[https://flink.apache.org/q/quickstart.sh](https://flink.apache.org/q/quickstart.sh)\n\n\n## 编写一个入门级的WordCount\n\n```public class WordCount {\n\n\t//\n\t//\tProgram\n\t//\n\n\tpublic static void main(String[] args) throws Exception {\n\n\t\t// set up the execution environment\n\t\tfinal ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n\n\t\t// get input data\n\t\tDataSet<String> text = env.fromElements(\n\t\t\t\t\"To be, or not to be,--that is the question:--\",\n\t\t\t\t\"Whether 'tis nobler in the mind to suffer\",\n\t\t\t\t\"The slings and arrows of outrageous fortune\",\n\t\t\t\t\"Or to take arms against a sea of troubles,\"\n\t\t\t\t);\n\n\t\tDataSet<Tuple2<String, Integer>> counts =\n\t\t\t\t// split up the lines in pairs (2-tuples) containing: (word,1)\n\t\t\t\ttext.flatMap(new LineSplitter())\n\t\t\t\t// group by the tuple field \"0\" and sum up tuple field \"1\"\n\t\t\t\t.groupBy(0) //(i,1) (am,1) (chinese,1)\n\t\t\t\t.sum(1);\n\n\t\t// execute and print result\n\t\tcounts.print();\n\n\t}\n\n\t//\n\t// \tUser Functions\n\t//\n\n\t/**\n\t * Implements the string tokenizer that splits sentences into words as a user-defined\n\t * FlatMapFunction. The function takes a line (String) and splits it into\n\t * multiple pairs in the form of \"(word,1)\" (Tuple2&lt;String, Integer&gt;).\n\t */\n\tpublic static final class LineSplitter implements FlatMapFunction<String, Tuple2<String, Integer>> {\n\n\t\t@Override\n\t\tpublic void flatMap(String value, Collector<Tuple2<String, Integer>> out) {\n\t\t\t// normalize and split the line\n\t\t\tString[] tokens = value.toLowerCase().split(\"\\\\W+\");\n\n\t\t\t// emit the pairs\n\t\t\tfor (String token : tokens) {\n\t\t\t\tif (token.length() > 0) {\n\t\t\t\t\tout.collect(new Tuple2<String, Integer>(token, 1));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n类似的例子，官方也有提供的，可以在这里下载：\n[WordCount官方推荐](https://github.com/apache/flink/blob/master/flink-examples/flink-examples-batch/src/main/java/org/apache/flink/examples/java/wordcount/WordCount.java)\n\n### 运行\n\n本地右键运行：\n![f642c8bf60959ab7c9544bff44882121](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/8F0D8EF2-5C0B-4067-AA87-31D7A0DC16C7.png)\n\n提交到本地单机Flink上\n\n* 进入工程目录，使用以下命令打包\n```\nmvn clean package -Dmaven.test.skip=true\n```\n然后，进入 flink 安装目录 bin 下执行以下命令提交程序：\n```\nflink run -c org.myorg.laowang.WordCount /Users/wangzhiwu/WorkSpace/quickstart/target/quickstart-0.1.jar\n```\n分别制定main方法和jar包的地址。\n\n在刚才的控制台中，可以看到：\n![ffabc914659360af98a6a359c704281a](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/EB619900-BBDE-4E32-9089-0DC867FF9220.png)\n我们刚才提交过的程序。\n\nflink的log目录下有我们提交过的任务的日志：\n![857dd4a1b3017866327e73acd46b86f5](Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.resources/620369FB-ABCA-4184-AA90-C7FEDB114B07.png)\n\n\n\n## 总结\n\n一次简单的flink之旅就完成了。\n"
  },
  {
    "path": "Flink/Flink从入门到放弃(入门篇3)-DataSetAPI.md",
    "content": "\n## 编程结构\n\n```\npublic class SocketTextStreamWordCount {\n\n\tpublic static void main(String[] args) throws Exception {\n\t\tif (args.length != 2){\nSystem.err.println(\"USAGE:\\nSocketTextStreamWordCount <hostname> <port>\");\n\t\t\treturn;\n\t\t}\n\t\tString hostName = args[0];\n\t\tInteger port = Integer.parseInt(args[1]);\n\t\tfinal StreamExecutionEnvironment env = StreamExecutionEnvironment\n\t\t\t\t.getExecutionEnvironment();\n\t\tDataStream<String> text = env.socketTextStream(hostName, port);\n\n\t\tDataStream<Tuple2<String, Integer>> counts \n\t\ttext.flatMap(new LineSplitter())\n\t\t\t\t.keyBy(0)\n\t\t\t\t.sum(1);\n\t\tcounts.print();\n\t\tenv.execute(\"Java WordCount from SocketTextStream Example\");\n\t}\n```\n上面的`SocketTextStreamWordCount`是一个典型的Flink程序，他由一下及格部分构成：\n* 获得一个execution environment，\n* 加载/创建初始数据，\n* 指定此数据的转换，\n* 指定放置计算结果的位置，\n* 触发程序执行\n\n\n\n## DataSet API\n分类：\n\n* Source: 数据源创建初始数据集，例如来自文件或Java集合\n* Transformation: 数据转换将一个或多个DataSet转换为新的DataSet\n* Sink: 将计算结果存储或返回\n\n### DataSet Sources\n\n#### 基于文件的\n\n* `readTextFile(path)/ TextInputFormat`- 按行读取文件并将其作为字符串返回。\n\n* `readTextFileWithValue(path)/ TextValueInputFormat`- 按行读取文件并将它们作为StringValues返回。StringValues是可变字符串。\n\n* `readCsvFile(path)/ CsvInputFormat`- 解析逗号（或其他字符）分隔字段的文件。返回元组或POJO的DataSet。支持基本java类型及其Value对应作为字段类型。\n\n* `readFileOfPrimitives(path, Class)/ PrimitiveInputFormat`- 解析新行（或其他字符序列）分隔的原始数据类型（如String或）的文件Integer。\n\n* `readFileOfPrimitives(path, delimiter, Class)/ PrimitiveInputFormat`- 解析新行（或其他字符序列）分隔的原始数据类型的文件，例如String或Integer使用给定的分隔符。\n\n* `readSequenceFile(Key, Value, path)/ SequenceFileInputFormat`- 创建一个JobConf并从类型为SequenceFileInputFormat，Key class和Value类的指定路径中读取文件，并将它们作为Tuple2 <Key，Value>返回。\n\n#### 基于集合\n\n* `fromCollection(Collection)` - 从Java Java.util.Collection创建数据集。集合中的所有数据元必须属于同一类型。\n\n* `fromCollection(Iterator, Class)` - 从迭代器创建数据集。该类指定迭代器返回的数据元的数据类型。\n\n* `fromElements(T ...)` - 根据给定的对象序列创建数据集。所有对象必须属于同一类型。\n\n* `fromParallelCollection(SplittableIterator, Class) `- 并行地从迭代器创建数据集。该类指定迭代器返回的数据元的数据类型。\n\n* `generateSequence(from, to)` - 并行生成给定间隔中的数字序列。\n\n#### 通用方法\n\n* `readFile(inputFormat, path)/ FileInputFormat`- 接受文件输入格式。\n\n* `createInput(inputFormat)/ InputFormat`- 接受通用输入格式。\n\n#### 代码示例\n\n```\nExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n\n// 从本地文件系统读\nDataSet<String> localLines = env.readTextFile(\"file:///path/to/my/textfile\");\n\n// 读取HDFS文件\nDataSet<String> hdfsLines = env.readTextFile(\"hdfs://nnHost:nnPort/path/to/my/textfile\");\n\n// 读取CSV文件\nDataSet<Tuple3<Integer, String, Double>> csvInput = env.readCsvFile(\"hdfs:///the/CSV/file\").types(Integer.class, String.class, Double.class);\n\n// 读取CSV文件中的部分\nDataSet<Tuple2<String, Double>> csvInput = env.readCsvFile(\"hdfs:///the/CSV/file\").includeFields(\"10010\").types(String.class, Double.class);\n\n// 读取CSV映射为一个java类\nDataSet<Person>> csvInput = env.readCsvFile(\"hdfs:///the/CSV/file\").pojoType(Person.class, \"name\", \"age\", \"zipcode\");\n\n// 读取一个指定位置序列化好的文件\nDataSet<Tuple2<IntWritable, Text>> tuples =\n env.readSequenceFile(IntWritable.class, Text.class, \"hdfs://nnHost:nnPort/path/to/file\");\n\n// 从输入字符创建\nDataSet<String> value = env.fromElements(\"Foo\", \"bar\", \"foobar\", \"fubar\");\n\n// 创建一个数字序列\nDataSet<Long> numbers = env.generateSequence(1, 10000000);\n\n// 从关系型数据库读取\nDataSet<Tuple2<String, Integer> dbData =\nenv.createInput(JDBCInputFormat.buildJDBCInputFormat()                    .setDrivername(\"org.apache.derby.jdbc.EmbeddedDriver\")                   .setDBUrl(\"jdbc:derby:memory:persons\")\n.setQuery(\"select name, age from persons\")\n.setRowTypeInfo(new RowTypeInfo(BasicTypeInfo.STRING_TYPE_INFO, BasicTypeInfo.INT_TYPE_INFO))\n.finish());\n```\n\n### DataSet Transformation\n\n> 详细可以参考官网:https://flink.sojb.cn/dev/batch/dataset_transformations.html#filter\n\n* Map\t\n\n\n采用一个数据元并生成一个数据元。\n```\ndata.map(new MapFunction<String, Integer>() {\n  public Integer map(String value) { return Integer.parseInt(value); }\n});\n```\n* FlatMap\t\n\n采用一个数据元并生成零个，一个或多个数据元。\n```\ndata.flatMap(new FlatMapFunction<String, String>() {\n  public void flatMap(String value, Collector<String> out) {\n    for (String s : value.split(\" \")) {\n      out.collect(s);\n    }\n  }\n});\n```\n\n* MapPartition\n\n\n在单个函数调用中转换并行分区。该函数将分区作为Iterable流来获取，并且可以生成任意数量的结果值。每个分区中的数据元数量取决于并行度和先前的 算子操作。\n```\ndata.mapPartition(new MapPartitionFunction<String, Long>() {\n  public void mapPartition(Iterable<String> values, Collector<Long> out) {\n    long c = 0;\n    for (String s : values) {\n      c++;\n    }\n    out.collect(c);\n  }\n});\n```\n* Filter\n\n计算每个数据元的布尔函数，并保存函数返回true的数据元。\n重要信息：系统假定该函数不会修改应用谓词的数据元。违反此假设可能会导致错误的结果。\n```\ndata.filter(new FilterFunction<Integer>() {\n  public boolean filter(Integer value) { return value > 1000; }\n});\n```\n* Reduce\t\n\n通过将两个数据元重复组合成一个数据元，将一组数据元组合成一个数据元。Reduce可以应用于完整数据集或分组数据集。\n```\ndata.reduce(new ReduceFunction<Integer> {\n  public Integer reduce(Integer a, Integer b) { return a + b; }\n});\n```\n如果将reduce应用于分组数据集，则可以通过提供CombineHintto 来指定运行时执行reduce的组合阶段的方式 setCombineHint。在大多数情况下，基于散列的策略应该更快，特别是如果不同键的数量与输入数据元的数量相比较小（例如1/10）。\n\n* ReduceGroup\t\n\n\n将一组数据元组合成一个或多个数据元。ReduceGroup可以应用于完整数据集或分组数据集。\n\n```\ndata.reduceGroup(new GroupReduceFunction<Integer, Integer> {\n  public void reduce(Iterable<Integer> values, Collector<Integer> out) {\n    int prefixSum = 0;\n    for (Integer i : values) {\n      prefixSum += i;\n      out.collect(prefixSum);\n    }\n  }\n});\n```\n\n* Aggregate\t\n\n\n将一组值聚合为单个值。聚合函数可以被认为是内置的reduce函数。聚合可以应用于完整数据集或分组数据集。\n\n```\nDataset<Tuple3<Integer, String, Double>> input = // [...]\nDataSet<Tuple3<Integer, String, Double>> output = input.aggregate(SUM, 0).and(MIN, 2);\n```\n\n您还可以使用简写语法进行最小，最大和总和聚合。\n\n```\nDataset<Tuple3<Integer, String, Double>> input = // [...]\nDataSet<Tuple3<Integer, String, Double>> output = input.sum(0).andMin(2);\n```\n* Distinct\t\n\n返回数据集的不同数据元。它相对于数据元的所有字段或字段子集从输入DataSet中删除重复条目。\n```\ndata.distinct();\n```\n使用reduce函数实现Distinct。您可以通过提供CombineHintto 来指定运行时执行reduce的组合阶段的方式 setCombineHint。在大多数情况下，基于散列的策略应该更快，特别是如果不同键的数量与输入数据元的数量相比较小（例如1/10）。\n\n* Join\t\n\n通过创建在其键上相等的所有数据元对来连接两个数据集。可选地使用JoinFunction将数据元对转换为单个数据元，或使用FlatJoinFunction将数据元对转换为任意多个（包括无）数据元。请参阅键部分以了解如何定义连接键。\n\n```\nresult = input1.join(input2)\n               .where(0)       // key of the first input (tuple field 0)\n               .equalTo(1);    // key of the second input (tuple field 1)\n```\n\n您可以通过Join Hints指定运行时执行连接的方式。提示描述了通过分区或广播进行连接，以及它是使用基于排序还是基于散列的算法。\n如果未指定提示，系统将尝试估算输入大小，并根据这些估计选择最佳策略。\n\n```\n// This executes a join by broadcasting the first data set\n// using a hash table for the broadcast data\nresult = input1.join(input2, JoinHint.BROADCAST_HASH_FIRST)\n               .where(0).equalTo(1);\n```\n\n请注意，连接转换仅适用于等连接。其他连接类型需要使用OuterJoin或CoGroup表示。\n\n* OuterJoin\t\n\n在两个数据集上执行左，右或全外连接。外连接类似于常规（内部）连接，并创建在其键上相等的所有数据元对。此外，如果在另一侧没有找到匹配的Keys，则保存“外部”侧（左侧，右侧或两者都满）的记录。匹配数据元对（或一个数据元和null另一个输入的值）被赋予JoinFunction以将数据元对转换为单个数据元，或者转换为FlatJoinFunction以将数据元对转换为任意多个（包括无）数据元。请参阅键部分以了解如何定义连接键。\n\n```\ninput1.leftOuterJoin(input2) // rightOuterJoin or fullOuterJoin for right or full outer joins\n      .where(0)              // key of the first input (tuple field 0)\n      .equalTo(1)            // key of the second input (tuple field 1)\n      .with(new JoinFunction<String, String, String>() {\n          public String join(String v1, String v2) {\n             // NOTE:\n             // - v2 might be null for leftOuterJoin\n             // - v1 might be null for rightOuterJoin\n             // - v1 OR v2 might be null for fullOuterJoin\n          }\n      });\n\n```\n\n* CoGroup\t\n\n\nreduce 算子操作的二维变体。将一个或多个字段上的每个输入分组，然后关联组。每对组调用转换函数。\n\n```\ndata1.coGroup(data2)\n     .where(0)\n     .equalTo(1)\n     .with(new CoGroupFunction<String, String, String>() {\n         public void coGroup(Iterable<String> in1, Iterable<String> in2, Collector<String> out) {\n           out.collect(...);\n         }\n      });\n```\n\n* Cross\t\n\n\n构建两个输入的笛卡尔积（交叉乘积），创建所有数据元对。可选择使用CrossFunction将数据元对转换为单个数据元\n\n```\nDataSet<Integer> data1 = // [...]\nDataSet<String> data2 = // [...]\nDataSet<Tuple2<Integer, String>> result = data1.cross(data2);\n```\n\n注：交叉是一个潜在的非常计算密集型 算子操作它甚至可以挑战大的计算集群！建议使用crossWithTiny（）和crossWithHuge（）来提示系统的DataSet大小。\n\n* Union\t\n\n\n生成两个数据集的并集。\n\n```\nDataSet<String> data1 = // [...]\nDataSet<String> data2 = // [...]\nDataSet<String> result = data1.union(data2);\n```\n\n* Rebalance\t\n\n\n均匀地Rebalance 数据集的并行分区以消除数据偏差。只有类似Map的转换可能会遵循Rebalance 转换。\n\n```\nDataSet<String> in = // [...]\nDataSet<String> result = in.rebalance()\n                           .map(new Mapper());\n                           \n```\n\n* Hash-Partition\n\n\n散列分区给定键上的数据集。键可以指定为位置键，表达键和键选择器函数。\n\n```\nDataSet<Tuple2<String,Integer>> in = // [...]\nDataSet<Integer> result = in.partitionByHash(0)\n                            .mapPartition(new PartitionMapper());\n```\n\n* Range-Partition\n\n\nRange-Partition给定键上的数据集。键可以指定为位置键，表达键和键选择器函数。\n\n```\nDataSet<Tuple2<String,Integer>> in = // [...]\nDataSet<Integer> result = in.partitionByRange(0)\n                            .mapPartition(new PartitionMapper());\n```\n\n* Custom Partitioning\n\n\n手动指定数据分区。 \n注意：此方法仅适用于单个字段键。\n\n```\nDataSet<Tuple2<String,Integer>> in = // [...]\nDataSet<Integer> result = in.partitionCustom(Partitioner<K> partitioner, key)\n```\n\n* Sort Partition\t\n\n\n本地按指定顺序对指定字段上的数据集的所有分区进行排序。可以将字段指定为元组位置或字段表达式。通过链接sortPartition（）调用来完成对多个字段的排序。\n\n```\nDataSet<Tuple2<String,Integer>> in = // [...]\nDataSet<Integer> result = in.sortPartition(1, Order.ASCENDING)\n                            .mapPartition(new PartitionMapper());\n```\n\n* First-n\t\n\n\n返回数据集的前n个（任意）数据元。First-n可以应用于常规数据集，分组数据集或分组排序数据集。分组键可以指定为键选择器函数或字段位置键。\n\n```\nDataSet<Tuple2<String,Integer>> in = // [...]\n// regular data set\nDataSet<Tuple2<String,Integer>> result1 = in.first(3);\n// grouped data set\nDataSet<Tuple2<String,Integer>> result2 = in.groupBy(0)                                     .first(3);\n// grouped-sorted data set\nDataSet<Tuple2<String,Integer>> result3 = in.groupBy(0)                                     .sortGroup(1, Order.ASCENDING)                     .first(3);\n\n```\n\n### DataSet Sink\n\n数据接收器使用DataSet用于存储或返回。使用OutputFormat描述数据接收器算子操作 。Flink带有各种内置输出格式，这些格式封装在DataSet上的算子操作中：\n\n* writeAsText()/ TextOutputFormat- 按字符串顺序写入数据元。通过调用每个数据元的toString（）方法获得字符串。\n* writeAsFormattedText()/ TextOutputFormat- 按字符串顺序写数据元。通过为每个数据元调用用户定义的format（）方法来获取字符串。\n* writeAsCsv(...)/ CsvOutputFormat- 将元组写为逗号分隔值文件。行和字段分隔符是可配置的。每个字段的值来自对象的toString（）方法。\n* print()/ printToErr()/ print(String msg)/ printToErr(String msg)- 在标准输出/标准错误流上打印每个数据元的toString（）值。可选地，可以提供前缀（msg），其前缀为输出。这有助于区分不同的打印调用。如果并行度大于1，则输出也将与生成输出的任务的标识符一起添加。\n* write()/ FileOutputFormat- 自定义文件输出的方法和基类。支持自定义对象到字节的转换。\n* output()/ OutputFormat- 大多数通用输出方法，用于非基于文件的数据接收器（例如将结果存储在数据库中）。\n\n可以将DataSet输入到多个 算子操作。程序可以编写或打印数据集，同时对它们执行其他转换。\n\n示例：\n\n```\n// text data\nDataSet<String> textData = // [...]\n\n// write DataSet to a file on the local file system\ntextData.writeAsText(\"file:///my/result/on/localFS\");\n\n// write DataSet to a file on a HDFS with a namenode running at nnHost:nnPort\ntextData.writeAsText(\"hdfs://nnHost:nnPort/my/result/on/localFS\");\n\n// write DataSet to a file and overwrite the file if it exists\ntextData.writeAsText(\"file:///my/result/on/localFS\", WriteMode.OVERWRITE);\n\n// tuples as lines with pipe as the separator \"a|b|c\"\nDataSet<Tuple3<String, Integer, Double>> values = // [...]\nvalues.writeAsCsv(\"file:///path/to/the/result/file\", \"\\n\", \"|\");\n\n// this writes tuples in the text formatting \"(a, b, c)\", rather than as CSV lines\nvalues.writeAsText(\"file:///path/to/the/result/file\");\n\n// this writes values as strings using a user-defined TextFormatter object\nvalues.writeAsFormattedText(\"file:///path/to/the/result/file\",\n    new TextFormatter<Tuple2<Integer, Integer>>() {\n        public String format (Tuple2<Integer, Integer> value) {\n            return value.f1 + \" - \" + value.f0;\n        }\n    });\n```\n\n使用自定义输出格式：\n\n```\nDataSet<Tuple3<String, Integer, Double>> myResult = [...]\n\n// write Tuple DataSet to a relational database\nmyResult.output(\n    // build and configure OutputFormat\n    JDBCOutputFormat.buildJDBCOutputFormat()\n                    .setDrivername(\"org.apache.derby.jdbc.EmbeddedDriver\")\n                    .setDBUrl(\"jdbc:derby:memory:persons\")\n                    .setQuery(\"insert into persons (name, age, height) values (?,?,?)\")\n                    .finish()\n    );\n```\n\n## 序列化器\n\n* Flink自带了针对诸如int，long，String等标准类型的序列化器\n\n\n* 针对Flink无法实现序列化的数据类型，我们可以交给Avro和Kryo\n\n\n* 使用方法：ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();\n\n```\n使用avro序列化：env.getConfig().enableForceAvro();\n使用kryo序列化：env.getConfig().enableForceKryo();\n使用自定义序列化：env.getConfig().addDefaultKryoSerializer(Class<?> type, Class<? extends Serializer<?>> serializerClass)\n\n```\n\n## 数据类型\n\n* Java Tuple 和 Scala case class\n\n* Java POJOs：java实体类\n\n* Primitive Types\n  默认支持java和scala基本数据类型\n\n* General Class Types\n  默认支持大多数java和scala class\n\n* Hadoop Writables\n  支持hadoop中实现了org.apache.hadoop.Writable的数据类型\n\n\n* Special Types\n  例如scala中的Either Option 和Try\n\n"
  },
  {
    "path": "Flink/Flink从入门到放弃(入门篇4)-DataStreamAPI.md",
    "content": "\nDataStream算子将一个或多个DataStream转换为新DataStream。程序可以将多个转换组合成复杂的数据流拓扑。\nDataStreamAPI和DataSetAPI主要的区别在于Transformation部分。\n## DataStream Transformation\n\n### map\n* DataStream→DataStream\n用一个数据元生成一个数据元。一个map函数，它将输入流的值加倍：\n```\nDataStream<Integer> dataStream = //...\ndataStream.map(new MapFunction<Integer, Integer>() {\n    @Override\n    public Integer map(Integer value) throws Exception {\n        return 2 * value;\n    }\n});\n```\n\n\n### FlatMap\n\n* DataStream→DataStream\n\n采用一个数据元并生成零个，一个或多个数据元。将句子分割为单词的flatmap函数：\n\n```\ndataStream.flatMap(new FlatMapFunction<String, String>() {\n    @Override\n    public void flatMap(String value, Collector<String> out)\n        throws Exception {\n        for(String word: value.split(\" \")){\n            out.collect(word);\n        }\n    }\n});\n```\n\n### Filter\n* DataStream→DataStream\t\n计算每个数据元的布尔函数，并保存函数返回true的数据元。过滤掉零值的过滤器：\n\n```\ndataStream.filter(new FilterFunction<Integer>() {\n    @Override\n    public boolean filter(Integer value) throws Exception {\n        return value != 0;\n    }\n});\n```\n\n### KeyBy\n* DataStream→KeyedStream\n\n逻辑上将流分区为不相交的分区。具有相同Keys的所有记录都分配给同一分区。在内部，keyBy（）是使用散列分区实现的。指定键有不同的方法。\n\n此转换返回KeyedStream，其中包括使用被Keys化状态所需的KeyedStream。\n\n```\ndataStream.keyBy(\"someKey\") // Key by field \"someKey\"\ndataStream.keyBy(0) // Key by the first element of a Tuple\n```\n\n🌺注意：\n\n如果出现以下情况，则类型不能成为key：\n\n* 它是POJO类型但不覆盖hashCode（）方法并依赖于Object.hashCode（）实现\n\n* 任何类型的数组\n\n### Reduce\nKeyedStream→DataStream\n\n将当前数据元与最后一个Reduce的值组合并发出新值。 \n例如：reduce函数，用于创建部分和的流：\n\n```\nkeyedStream.reduce(new ReduceFunction<Integer>() {\n    @Override\n    public Integer reduce(Integer value1, Integer value2)\n    throws Exception {\n        return value1 + value2;\n    }\n});   \n```\n### Fold\nKeyedStream→DataStream\n\n具有初始值的被Keys化数据流上的“滚动”折叠。将当前数据元与最后折叠的值组合并发出新值。 \n\n折叠函数，当应用于序列（1,2,3,4,5）时，发出序列“start-1”，“start-1-2”，“start-1-2-3”,. ..\n\n```\nDataStream<String> result =\n  keyedStream.fold(\"start\", new FoldFunction<Integer, String>() {\n    @Override\n    public String fold(String current, Integer value) {\n        return current + \"-\" + value;\n    }\n  });\n```\n\n### 聚合\n* KeyedStream→DataStream\n\n在被Keys化数据流上滚动聚合。min和minBy之间的差异是min返回最小值，而minBy返回该字段中具有最小值的数据元(max和maxBy相同)。\n\n```\nkeyedStream.sum(0);\nkeyedStream.sum(\"key\");\nkeyedStream.min(0);\nkeyedStream.min(\"key\");\nkeyedStream.max(0);\nkeyedStream.max(\"key\");\nkeyedStream.minBy(0);\nkeyedStream.minBy(\"key\");\nkeyedStream.maxBy(0);\nkeyedStream.maxBy(\"key\");\n```\n\n### Window函数\n\n关于Flink的窗口概念，我们会在后面有详细介绍。\n\n* Window\nKeyedStream→WindowedStream\n\n可以在已经分区的KeyedStream上定义Windows。Windows根据某些特征（例如，在最后5秒内到达的数据）对每个Keys中的数据进行分组。\n\n```\ndataStream.keyBy(0)\n.window(TumblingEventTimeWindows\n.of(Time.seconds(5))); // Last 5 seconds of data\n    \n```\n* Window Apply\nWindowedStream→DataStream \nAllWindowedStream→DataStream\n\n将一般函数应用于整个窗口。下面是一个手动求和窗口数据元的函数。\n\n注意：如果您正在使用windowAll转换，则需要使用AllWindowFunction。\n\n```\nwindowedStream.apply (new WindowFunction<Tuple2<String,Integer>, Integer, Tuple, Window>() {\n    public void apply (Tuple tuple,\n            Window window,\n            Iterable<Tuple2<String, Integer>> values,\n            Collector<Integer> out) throws Exception {\n        int sum = 0;\n        for (value t: values) {\n            sum += t.f1;\n        }\n        out.collect (new Integer(sum));\n    }\n});\n\n// applying an AllWindowFunction on non-keyed window stream\nallWindowedStream.apply (new AllWindowFunction<Tuple2<String,Integer>, Integer, Window>() {\n    public void apply (Window window,\n            Iterable<Tuple2<String, Integer>> values,\n            Collector<Integer> out) throws Exception {\n        int sum = 0;\n        for (value t: values) {\n            sum += t.f1;\n        }\n        out.collect (new Integer(sum));\n    }\n});\n    \n```\n\n* Window Reduce\nWindowedStream→DataStream\n\n将reduce函数应用于窗口并返回reduce后的值。\n\n```\nwindowedStream.reduce (new ReduceFunction<Tuple2<String,Integer>>() {\n    public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {\n        return new Tuple2<String,Integer>(value1.f0, value1.f1 + value2.f1);\n    }\n});\n```\n\n* 提取时间戳\n>关于Time我们在后面有专门的章节进行介绍\n\n\nDataStream→DataStream\n\n从记录中提取时间戳，以便使用使用事件时间语义的窗口。\n\n```\nstream.assignTimestamps (new TimeStampExtractor() {...});\n```\n\n### Partition 分区\n\n* 自定义分区\nDataStream→DataStream\t\n使用用户定义的分区程序为每个数据元选择目标任务。\n\n```\ndataStream.partitionCustom(partitioner, \"someKey\");\ndataStream.partitionCustom(partitioner, 0);\n```\n\n* 随机分区\nDataStream→DataStream\t\n根据均匀分布随机分配数据元。\n```\ndataStream.shuffle();\n```     \n* Rebalance （循环分区）\nDataStream→DataStream\t\n分区数据元循环，每个分区创建相等的负载。在存在数据倾斜时用于性能优化。\n```\ndataStream.rebalance();\n```\n\n* rescale\nDataStream→DataStream\n\n如果上游 算子操作具有并行性2并且下游算子操作具有并行性6，则一个上游 算子操作将分配元件到三个下游算子操作，而另一个上游算子操作将分配到其他三个下游 算子操作。另一方面，如果下游算子操作具有并行性2而上游 算子操作具有并行性6，则三个上游 算子操作将分配到一个下游算子操作，而其他三个上游算子操作将分配到另一个下游算子操作。\n\n在不同并行度不是彼此的倍数的情况下，一个或多个下游 算子操作将具有来自上游 算子操作的不同数量的输入。\n\n请参阅此图以获取上例中连接模式的可视化：\n\n![5bd63a6c99ad06ba3d96d03be3cb25ff.svg+xml](evernotecid://DF961740-2AB0-48AB-AAE7-53BB9D286C7A/appyinxiangcom/12131181/ENResource/p1410)\n```\ndataStream.rescale();\n```\n\n* 广播\nDataStream→DataStream\t\n向每个分区广播数据元。\n```\ndataStream.broadcast();\n```      "
  },
  {
    "path": "Flink/Flink集群部署.md",
    "content": "\n## 部署方式\n\n\n一般来讲有三种方式：\n\n* Local\n* Standalone\n* Flink On Yarn/Mesos/K8s…\n\n## 单机模式\n\n参考上一篇**Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用**\n\n## Standalone模式部署\n\n我们基于CentOS7虚拟机搭建一个3个节点的集群：\n\n角色分配：\n```\nMaster: 192.168.246.134\nSlave: 192.168.246.135\nSlave: 192.168.246.136\n\n```\n```\n192.168.246.134 jobmanager\n192.168.246.135 taskmanager\n192.168.246.136 taskmanager\n```\n假设三台机器都存在：\n用户root 密码为123\n\n```\n192.168.246.134 master\n192.168.246.135 slave1\n192.168.246.136 slave2\n\n```\n三台机器首先要做ssh免登，具体方法很简单，可以百度。\n\n下载一个包到本地：\n![5cd146f0daa759be76f96435a36f5ecb](Flink集群部署.resources/551E446E-EC51-4389-B180-04C4B0BD425F.png)\n这里我选择了1.7.2版本+Hadoop2.8+Scala2.11版本\n然后，分发\n```\nscp flink-1.7.2-bin-hadoop28-scala_2.11.tgz root@192.168.246.13X:~\nscp jdk-8u11-linux-x64.tar.gz root@192.168.246.13X:~\n注意：X代表4、5、6，分发到3台机器\n\n修改解压后目录属主：\nChown -R  root:root flink/\nChown -R root:root jdk8/\n\nexport JAVA_HOME=/root/jdk8\nexport JRE_HOME=${JAVA_HOME}/jre\nexport CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib\nexport PATH=${JAVA_HOME}/bin:$PATH\n```\n\n分别修改master和slave的flink-conf.yaml文件\n```\nVim  flink/conf/flink-conf.yaml\n\n##配置master节点ip\njobmanager.rpc.address: 192.168.1.100\n\n##配置slave节点可用内存，单位MB\ntaskmanager.heap.mb: 25600\n\n##配置每个节点的可用slot，1 核CPU对应 1 slot\n##the number of available CPUs per machine \ntaskmanager.numberOfTaskSlots: 30\n\n##默认并行度 1 slot资源\nparallelism.default: 1\n\n修改slave节点配置文件slaves：\n192.168.246.135\n192.168.246.136\n```\n\n启动集群：\n```\n##在master节点上执行此脚本，就可以启动集群，前提要保证master节点到slaver节点可以免密登录，\n##因为它的启动过程是：先在master节点启动jobmanager进程，然后ssh到各slaver节点启动taskmanager进程\n./bin/start-cluster.sh\n停止集群：\n./bin/stop-cluster.sh\n```\n\n## Flink on yarn集群部署\n\n### Yarn的简介：\n![b1afbe55f1f2480530635cb86b6798e8](Flink集群部署.resources/4655360C-0DCA-41C9-B5B5-AF1293920C66.png)\n\n* ResourceManager\nResourceManager 负责整个集群的资源管理和分配，是一个全局的资源管理系统。 NodeManager 以心跳的方式向 ResourceManager 汇报资源使用情况（目前主要是 CPU 和内存的使用情况）。RM 只接受 NM 的资源回报信息，对于具体的资源处理则交给 NM 自己处理。\n* NodeManager\nNodeManager 是每个节点上的资源和任务管理器，它是管理这台机器的代理，负责该节点程序的运行，以及该节点资源的管理和监控。YARN 集群每个节点都运行一个NodeManager。\nNodeManager 定时向 ResourceManager 汇报本节点资源（CPU、内存）的使用情况和Container 的运行状态。当 ResourceManager 宕机时 NodeManager 自动连接 RM 备用节点。\nNodeManager 接收并处理来自 ApplicationMaster 的 Container 启动、停止等各种请求。\n* ApplicationMaster\n负责与 RM 调度器协商以获取资源（用 Container 表示）。 \n将得到的任务进一步分配给内部的任务(资源的二次分配)。 \n与 NM 通信以启动/停止任务。 \n监控所有任务运行状态，并在任务运行失败时重新为任务申请资源以重启任务\n\n### Flink on yarn 集群启动步骤\n\n* 步骤1　用户向YARN中提交应用程序，其中包括ApplicationMaster程序、启动ApplicationMaster的命令、用户程序等。\n* 步骤2　ResourceManager为该应用程序分配第一个Container，并与对应的Node-Manager通信，要求它在这个Container中启动应用程序的ApplicationMaster。\n* 步骤3　ApplicationMaster首先向ResourceManager注册，这样用户可以直接通过ResourceManager查看应用程序的运行状态，然后它将为各个任务申请资源，并监控它的运行状态，直到运行结束，即重复步骤4~7。\n* 步骤4　ApplicationMaster采用轮询的方式通过RPC协议向ResourceManager申请和领取资源。\n* 步骤5　一旦ApplicationMaster申请到资源后，便与对应的NodeManager通信，要求它启动任务。\n* 步骤6　NodeManager为任务设置好运行环境（包括环境变量、JAR包、二进制程序等）后，将任务启动命令写到一个脚本中，并通过运行该脚本启动任务。\n* 步骤7　各个任务通过某个RPC协议向ApplicationMaster汇报自己的状态和进度，以让ApplicationMaster随时掌握各个任务的运行状态，从而可以在任务失败时重新启动任务。  在应用程序运行过程中，用户可随时通过RPC向ApplicationMaster查询应用程序的当前运行状态。\n* 步骤8　应用程序运行完成后，ApplicationMaster向ResourceManager注销并关闭自己\n\n\n### on yarn 集群部署\n\n设置Hadoop环境变量：\n```\n[root@hadoop2 flink-1.7.2]# vi /etc/profile\nexport HADOOP_CONF_DIR=这里是你自己的hadoop路径\n```\n\nbin/yarn-session.sh -h 查看使用方法:\n\n\n![4f6f481ebaa405fec89a32f848555352](Flink集群部署.resources/A4B3DD87-69AF-4CB0-ADA2-F925F9639564.png)\n\n在启动的是可以指定TaskManager的个数以及内存(默认是1G)，也可以指定JobManager的内存，但是JobManager的个数只能是一个\n\n我们开启动一个YARN session：\n```\n./bin/yarn-session.sh -n 4 -tm 8192 -s 8\n\n```\n\n上面命令启动了4个TaskManager，每个TaskManager内存为8G且占用了8个核(是每个TaskManager，默认是1个核)。在启动YARN session的时候会加载conf/flink-config.yaml配置文件，我们可以根据自己的需求去修改里面的相关参数.\n\nYARN session启动之后就可以使用bin/flink来启动提交作业:\n\n例如：\n```\n./bin/flink run -c com.demo.wangzhiwu.WordCount $DEMO_DIR/target/flink-demo-1.0.SNAPSHOT.jar --port 9000\n```\n\nflink run的用法如下：\n```\n用法: run [OPTIONS] <jar-file> <arguments>  \n  \"run\" 操作参数:  \n     -c,--class <classname>           如果没有在jar包中指定入口类，则需要在这里通过这个参数指定  \n\n     -m,--jobmanager <host:port>      指定需要连接的jobmanager(主节点)地址  \n                                      使用这个参数可以指定一个不同于配置文件中的jobmanager  \n     -p,--parallelism <parallelism>   指定程序的并行度。可以覆盖配置文件中的默认值。  \n```\n\n使用run 命令向yarn集群提交一个job。客户端可以确定jobmanager的地址。当然，你也可以通过-m参数指定jobmanager。jobmanager的地址在yarn控制台上可以看到。\n\n值得注意的是：\n\n上面的YARN session是在Hadoop YARN环境下启动一个Flink cluster集群，里面的资源是可以共享给其他的Flink作业。我们还可以在YARN上启动一个Flink作业。这里我们还是使用./bin/flink，但是不需要事先启动YARN session：\n```\n./bin/flink run -m yarn-cluster -yn 2 ./examples/batch/WordCount.jar      \\\n          --input hdfs://user/hadoop/input.txt                            \\\n          --output hdfs://user/hadoop/output.txt\n```\n\n上面的命令同样会启动一个类似于YARN session启动的页面。其中的-yn是指TaskManager的个数，必须要指定。\n\n#### 后台运行 yarn session\n\n如果你不希望flink yarn client一直运行，也可以启动一个后台运行的yarn session。使用这个参数：-d 或者 --detached\n在这种情况下，flink yarn client将会只提交任务到集群然后关闭自己。注意：在这种情况下，无法使用flink停止yarn session。\n必须使用yarn工具来停止yarn session\n\n```\nyarn application -kill <applicationId> \n```\n\n#### flink on yarn的故障恢复\n\nflink 的 yarn 客户端通过下面的配置参数来控制容器的故障恢复。这些参数可以通过conf/flink-conf.yaml 或者在启动yarn session的时候通过-D参数来指定。\n\n* yarn.reallocate-failed：这个参数控制了flink是否应该重新分配失败的taskmanager容器。默认是true。\n* yarn.maximum-failed-containers：applicationMaster可以接受的容器最大失败次数，达到这个参数，就会认为yarn session失败。默认这个次数和初始化请求的taskmanager数量相等(-n 参数指定的)。\n* yarn.application-attempts：applicationMaster重试的次数。如果这个值被设置为1(默认就是1)，当application master失败的时候，yarn session也会失败。设置一个比较大的值的话，yarn会尝试重启applicationMaster。\n\n#### 日志文件查看\n\n在某种情况下，flink yarn session 部署失败是由于它自身的原因，用户必须依赖于yarn的日志来进行分析。最有用的就是yarn log aggregation 。启动它，用户必须在yarn-site.xml文件中设置yarn.log-aggregation-enable 属性为true。一旦启用了，用户可以通过下面的命令来查看一个失败的yarn session的所有详细日志。\n\n```\nyarn logs -applicationId <application ID>  \n```\n\n完。"
  },
  {
    "path": "Flink漫谈系列/Apache-Flink-漫谈系列(02)-Watermark.md",
    "content": "## 实际问题（乱序）\n\n在介绍Watermark相关内容之前我们先抛出一个具体的问题，在实际的流式计算中数据到来的顺序对计算结果的正确性有至关重要的影响，比如：某数据源中的某些数据由于某种原因(如：网络原因，外部存储自身原因)会有5秒的延时，也就是在实际时间的第1秒产生的数据有可能在第5秒中产生的数据之后到来(比如到Window处理节点).选具体某个delay的元素来说，假设在一个5秒的Tumble窗口(详见Window介绍章节)，有一个EventTime是 11秒的数据，在第16秒时候到来了。图示第11秒的数据，在16秒到来了，如下图：\n\n![77ad6ed47946b1ceff52f9b031da91aa](Apache-Flink-漫谈系列(02)-Watermark.resources/E6EAF710-D569-4A11-A65D-687482D7B246.png)\n\n那么对于一个Count聚合的Tumble(5s)的window，上面的情况如何处理才能window2=4，window3=2 呢？\n\n## Apache Flink的时间类型\n\n开篇我们描述的问题是一个很常见的TimeWindow中数据乱序的问题，乱序是相对于事件产生时间和到达Apache Flink 实际处理算子的顺序而言的，关于时间在Apache Flink中有如下三种时间类型，如下图：\n\n\n![1a8fc0f8081b795a23ac7c1c3385c8e1](Apache-Flink-漫谈系列(02)-Watermark.resources/B28386CD-F1C2-4DED-AC1C-8983B7BB3C81.png)\n\n**ProcessingTime**\n\n是数据流入到具体某个算子时候相应的系统时间。ProcessingTime 有最好的性能和最低的延迟。但在分布式计算环境中ProcessingTime具有不确定性，相同数据流多次运行有可能产生不同的计算结果。\n\n**IngestionTime**\n\nIngestionTime是数据进入Apache Flink框架的时间，是在Source Operator中设置的。与ProcessingTime相比可以提供更可预测的结果，因为IngestionTime的时间戳比较稳定(在源处只记录一次)，同一数据在流经不同窗口操作时将使用相同的时间戳，而对于ProcessingTime同一数据在流经不同窗口算子会有不同的处理时间戳。\n\n**EventTime**\n\nEventTime是事件在设备上产生时候携带的。在进入Apache Flink框架之前EventTime通常要嵌入到记录中，并且EventTime也可以从记录中提取出来。在实际的网上购物订单等业务场景中，大多会使用EventTime来进行数据计算。\n\n开篇描述的问题和本篇要介绍的Watermark所涉及的时间类型均是指EventTime类型。\n\n## 什么是Watermark\n\nWatermark是Apache Flink为了处理EventTime 窗口计算提出的一种机制,本质上也是一种时间戳，由Apache Flink Source或者自定义的Watermark生成器按照需求Punctuated或者Periodic两种方式生成的一种系统Event，与普通数据流Event一样流转到对应的下游算子，接收到Watermark Event的算子以此不断调整自己管理的EventTime clock。 Apache Flink 框架保证Watermark单调递增，算子接收到一个Watermark时候，框架知道不会再有任何小于该Watermark的时间戳的数据元素到来了，所以Watermark可以看做是告诉Apache Flink框架数据流已经处理到什么位置(时间维度)的方式。 Watermark的产生和Apache Flink内部处理逻辑如下图所示: \n\n![53c8aaff92cc32438a4dfc8c7ae6a55f](Apache-Flink-漫谈系列(02)-Watermark.resources/32C5E28E-7C48-4E69-A180-FF78815EF75E.png)\n\n## Watermark的产生方式\n目前Apache Flink 有两种生产Watermark的方式，如下：\n\n* Punctuated - 数据流中每一个递增的EventTime都会产生一个Watermark。 \n在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力，所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成。\n\n* Periodic - 周期性的（一定时间间隔或者达到一定的记录条数）产生一个Watermark。在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark，否则在极端情况下会有很大的延时。\n\n所以Watermark的生成方式需要根据业务场景的不同进行不同的选择。\n\n## Watermark的接口定义\n对应Apache Flink Watermark两种不同的生成方式，我们了解一下对应的接口定义，如下：\n\n* Periodic Watermarks - AssignerWithPeriodicWatermarks\n\n```\n/**\n * Returns the current watermark. This method is periodically called by the\n * system to retrieve the current watermark. The method may return {@code null} to\n * indicate that no new Watermark is available.\n *\n * &lt;p&gt;The returned watermark will be emitted only if it is non-null and itsTimestamp\n * is larger than that of the previously emitted watermark (to preserve the contract of\n * ascending watermarks). If the current watermark is still\n * identical to the previous one, no progress in EventTime has happened since\n * the previous call to this method. If a null value is returned, or theTimestamp\n * of the returned watermark is smaller than that of the last emitted one, then no\n * new watermark will be generated.\n *\n * &lt;p&gt;The interval in which this method is called and Watermarks are generated\n * depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.\n *\n * @see org.Apache.flink.streaming.api.watermark.Watermark\n * @see ExecutionConfig#getAutoWatermarkInterval()\n *\n * @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.\n */\n @Nullable\n Watermark getCurrentWatermark();\n```\n\n* Punctuated Watermarks -AssignerWithPunctuatedWatermarks \n\n```\npublic interface AssignerWithPunctuatedWatermarks&lt;T&gt; extendsTimestampAssigner&lt;T&gt; {\n\n/**\n * Asks this implementation if it wants to emit a watermark. This method is called right after\n * the {@link #extractTimestamp(Object, long)} method.\n *\n * &lt;p&gt;The returned watermark will be emitted only if it is non-null and itsTimestamp\n * is larger than that of the previously emitted watermark (to preserve the contract of\n * ascending watermarks). If a null value is returned, or theTimestamp of the returned\n * watermark is smaller than that of the last emitted one, then no new watermark will\n * be generated.\n *\n * &lt;p&gt;For an example how to use this method, see the documentation of\n * {@link AssignerWithPunctuatedWatermarks this class}.\n *\n * @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.\n */\n @Nullable\nWatermark checkAndGetNextWatermark(T lastElement, long extractedTimestamp);\n}\n```\n\n* AssignerWithPunctuatedWatermarks 继承了TimestampAssigner接口 -TimestampAssigner\n\n```\npublic interfaceTimestampAssigner&lt;T&gt; extends Function {\n\n/**\n * Assigns aTimestamp to an element, in milliseconds since the Epoch.\n *\n * &lt;p&gt;The method is passed the previously assignedTimestamp of the element.\n * That previousTimestamp may have been assigned from a previous assigner,\n * by ingestionTime. If the element did not carry aTimestamp before, this value is\n * {@code Long.MIN_VALUE}.\n *\n * @param element The element that theTimestamp is wil be assigned to.\n * @param previousElementTimestamp The previous internalTimestamp of the element,\n *                                 or a negative value, if noTimestamp has been assigned, yet.\n * @return The newTimestamp.\n */\nlong extractTimestamp(T element, long previousElementTimestamp);\n}\n```\n\n从接口定义可以看出，Watermark可以在Event(Element)中提取EventTime，进而定义一定的计算逻辑产生Watermark的时间戳。\n\n## Watermark解决如上问题\n\n从上面的Watermark生成接口和Apache Flink内部对Periodic Watermark的实现来看，Watermark的时间戳可以和Event中的EventTime 一致，也可以自己定义任何合理的逻辑使得Watermark的时间戳不等于Event中的EventTime，Event中的EventTime自产生那一刻起就不可以改变了，不受Apache Flink框架控制，而Watermark的产生是在Apache Flink的Source节点或实现的Watermark生成器计算产生(如上Apache Flink内置的 Periodic Watermark实现), Apache Flink内部对单流或多流的场景有统一的Watermark处理。\n\n回过头来我们在看看Watermark机制如何解决上面的问题，上面的问题在于如何将迟来的EventTime 位11的元素正确处理。要解决这个问题我们还需要先了解一下EventTime window是如何触发的？ EventTime window 计算条件是当Window计算的Timer时间戳 小于等于 当前系统的Watermak的时间戳时候进行计算。 \n\n* 当Watermark的时间戳等于Event中携带的EventTime时候，上面场景（Watermark=EventTime)的计算结果如下：\n\n![4d4f83e9f06a2665797495ea6ad66a6a](Apache-Flink-漫谈系列(02)-Watermark.resources/12F12191-492B-45E4-987E-8744D4235822.png)\n\n上面对应的DDL(Alibaba 企业版的Flink分支)定义如下：\n\n```\nCREATE TABLE source(\n  ...,\n  Event_timeTimeStamp,\n  WATERMARK wk1 FOR Event_time as withOffset(Event_time, 0) \n) with (\n  ...\n);\n```\n\n* 如果想正确处理迟来的数据可以定义Watermark生成策略为 Watermark = EventTime -5s， 如下：\n\n![918b32429bface38d9548080c1e6be67](Apache-Flink-漫谈系列(02)-Watermark.resources/EB72E25B-15BA-4651-BB26-177F1A71E692.png)\n\n上面对应的DDL(Alibaba 内部的DDL语法，目前正在和社区讨论)定义如下： \n\n```\nCREATE TABLE source(\n  ...,\n  Event_timeTimeStamp,\n  WATERMARK wk1 FOR Event_time as withOffset(Event_time, 5000) \n) with (\n  ...\n);\n```\n\n上面正确处理的根源是我们采取了 延迟触发 window 计算 的方式正确处理了 Late Event. 与此同时，我们发现window的延时触发计算，也导致了下游的LATENCY变大，本例子中下游得到window的结果就延迟了5s.\n\n\n\n## 多流的Watermark处理\n\n在实际的流计算中往往一个job中会处理多个Source的数据，对Source的数据进行GroupBy分组，那么来自不同Source的相同key值会shuffle到同一个处理节点，并携带各自的Watermark，Apache Flink内部要保证Watermark要保持单调递增，多个Source的Watermark汇聚到一起时候可能不是单调自增的，这样的情况Apache Flink内部是如何处理的呢？如下图所示：\n\n![228534a1e3534fce5402e4be413d5067](Apache-Flink-漫谈系列(02)-Watermark.resources/C7873B00-89E2-4CE4-BB5E-584A1CF4767A.png)\n\nApache Flink内部实现每一个边上只能有一个递增的Watermark， 当出现多流携带Eventtime汇聚到一起(GroupBy or Union)时候，Apache Flink会选择所有流入的Eventtime中最小的一个向下游流出。从而保证watermark的单调递增和保证数据的完整性.如下图:\n\n![32909a105423763289c08c573c3bca56](Apache-Flink-漫谈系列(02)-Watermark.resources/8F75B306-8255-4D8D-BE79-BD2FC98113B8.png)\n\n\n## 小结\n\n本节以一个流计算常见的乱序问题介绍了Apache Flink如何利用Watermark机制来处理乱序问题. 本篇内容在一定程度上也体现了EventTime Window中的Trigger机制依赖了Watermark(后续Window篇章会介绍)。Watermark机制是流计算中处理乱序，正确处理Late Event的核心手段。"
  },
  {
    "path": "Flink漫谈系列/Apache-Flink-漫谈系列(03)-State.md",
    "content": "## 实际问题\n\n在流计算场景中，数据会源源不断的流入Apache Flink系统，每条数据进入Apache Flink系统都会触发计算。如果我们想进行一个Count聚合计算，那么每次触发计算是将历史上所有流入的数据重新新计算一次，还是每次计算都是在上一次计算结果之上进行增量计算呢？答案是肯定的，Apache Flink是基于上一次的计算结果进行增量计算的。那么问题来了: \"上一次的计算结果保存在哪里，保存在内存可以吗？\"，答案是否定的，如果保存在内存，在由于网络，硬件等原因造成某个计算节点失败的情况下，上一次计算结果会丢失，在节点恢复的时候，就需要将历史上所有数据（可能十几天，上百天的数据）重新计算一次，所以为了避免这种灾难性的问题发生，Apache Flink 会利用State存储计算结果。本篇将会为大家介绍Apache Flink State的相关内容。\n\n## 什么是State\n\n这个问题似乎有些\"弱智\"？不管问题的答案是否显而易见，但我还是想简单说一下在Apache Flink里面什么是State？State是指流计算过程中计算节点的中间计算结果或元数据属性，比如 在aggregation过程中要在state中记录中间聚合结果，比如 Apache Kafka 作为数据源时候，我们也要记录已经读取记录的offset，这些State数据在计算过程中会进行持久化(插入或更新)。所以Apache Flink中的State就是与时间相关的，Apache Flink任务的内部数据（计算数据和元数据属性）的快照。\n\n## 为什么需要State\n\n\n与批计算相比，State是流计算特有的，批计算没有failover机制，要么成功，要么重新计算。流计算在 大多数场景 下是增量计算，数据逐条处理（大多数场景)，每次计算是在上一次计算结果之上进行处理的，这样的机制势必要将上一次的计算结果进行存储（生产模式要持久化），另外由于 机器，网络，脏数据等原因导致的程序错误，在重启job时候需要从成功的检查点(checkpoint，后面篇章会专门介绍)进行state的恢复。增量计算，Failover这些机制都需要state的支撑。\n\n## State 实现\n\nApache Flink内部有四种state的存储实现，具体如下：\n\n* 基于内存的HeapStateBackend - 在debug模式使用，不 建议在生产模式下应用；\n* 基于HDFS的FsStateBackend - 分布式文件持久化，每次读写都产生网络IO，整体性能不佳；\n* 基于RocksDB的RocksDBStateBackend - 本地文件+异步HDFS持久化；\n* 还有一个是基于Niagara(Alibaba内部实现)NiagaraStateBackend - 分布式持久化- 在Alibaba生产环境应用；\n\n## State 持久化逻辑\n\nApache Flink版本选择用RocksDB+HDFS的方式进行State的存储，State存储分两个阶段，首先本地存储到RocksDB，然后异步的同步到远程的HDFS。 这样而设计既消除了HeapStateBackend的局限（内存大小，机器坏掉丢失等），也减少了纯分布式存储的网络IO开销。\n\n![c6e2f3f39188c48255c79834ab872c5e](Apache-Flink-漫谈系列(03)-State.resources/11AD7C2A-A1DD-4238-8226-AED47EF6F446.png)\n\n## State 分类\nApache Flink 内部按照算子和数据分组角度将State划分为如下两类：\n\n* KeyedState - 这里面的key是我们在SQL语句中对应的GroupBy/PartitioneBy里面的字段，key的值就是groupby/PartitionBy字段组成的Row的字节数组，每一个key都有一个属于自己的State，key与key之间的State是不可见的；\n\n* OperatorState - Apache Flink内部的Source Connector的实现中就会用OperatorState来记录source数据读取的offset。 \n\n## State 扩容重新分配\n\nApache Flink是一个大规模并行分布式系统，允许大规模的有状态流处理。 为了可伸缩性，Apache Flink作业在逻辑上被分解成operator graph，并且每个operator的执行被物理地分解成多个并行运算符实例。 从概念上讲，Apache Flink中的每个并行运算符实例都是一个独立的任务，可以在自己的机器上调度到网络连接的其他机器运行。\n\nApache Flink的DAG图中只有边相连的节点🈶网络通信，也就是整个DAG在垂直方向有网络IO，在水平方向如下图的stateful节点之间没有网络通信，这种模型也保证了每个operator实例维护一份自己的state，并且保存在本地磁盘（远程异步同步）。通过这种设计，任务的所有状态数据都是本地的，并且状态访问不需要任务之间的网络通信。 避免这种流量对于像Apache Flink这样的大规模并行分布式系统的可扩展性至关重要。\n\n如上我们知道Apache Flink中State有OperatorState和KeyedState，那么在进行扩容时候（增加并发）State如何分配呢？比如：外部Source有5个partition，在Apache Flink上面由Srouce的1个并发扩容到2个并发，中间Stateful Operation 节点由2个并发并扩容的3个并发，如下图所示:\n\n![9c48ca9908816902cd317a26e1fffa26](Apache-Flink-漫谈系列(03)-State.resources/60A3963C-B15F-456F-9B37-69C022B6491D.png)\n\n在Apache Flink中对不同类型的State有不同的扩容方法，接下来我们分别介绍。\n\n\n## OperatorState对扩容的处理\n\n我们选取Apache Flink中某个具体Connector实现实例进行介绍，以MetaQ为例，MetaQ以topic方式订阅数据，每个topic会有N>0个分区，以上图为例，加上我们订阅的MetaQ的topic有5个分区，那么当我们source由1个并发调整为2个并发时候，State是怎么恢复的呢？\nstate 恢复的方式与Source中OperatorState的存储结构有必然关系，我们先看MetaQSource的实现是如何存储State的。首先MetaQSource 实现了ListCheckpointed<T extends Serializable>，其中的T是Tuple2<InputSplit,Long>，我们在看ListCheckpointed接口的内部定义如下：\n\n```\npublic interface ListCheckpointed<T extends Serializable>; {\nList<T> snapshotState(long var1, long var3) throws Exception;\n\nvoid restoreState(List&lt;T&gt; var1) throws Exception;\n}\n```\n我们发现 snapshotState方法的返回值是一个List<T>,T是Tuple2<InputSplit,Long>，也就是snapshotState方法返回List<Tuple2<InputSplit,Long>>,这个类型说明state的存储是一个包含partiton和offset信息的列表，InputSplit代表一个分区，Long代表当前partition读取的offset。InputSplit有一个方法如下：\n\n```\npublic interface InputSplit extends Serializable {\n    int getSplitNumber();\n}\n```\n\n也就是说，InputSplit我们可以理解为是一个Partition索引，有了这个数据结构我们在看看上面图所示的case是如何工作的？当Source的并行度是1的时候，所有打partition数据都在同一个线程中读取，所有partition的state也在同一个state中维护，State存储信息格式如下：\n\n![67cf069e8320cfeb0cf3fba127ed2cf9](Apache-Flink-漫谈系列(03)-State.resources/736E827E-E4C1-4412-8F0C-F51AECB68329.png)\n\n如果我们现在将并发调整为2，那么我们5个分区的State将会在2个独立的任务（线程）中进行维护，在内部实现中我们有如下算法进行分配每个Task所处理和维护partition的State信息，如下：\n\n```\nList<Integer> assignedPartitions = new LinkedList<>();\nfor (int i = 0; i < partitions; i++) {\n        if (i % consumerCount == consumerIndex) {\n                assignedPartitions.add(i);\n        }\n}\n```\n\n这个求mod的算法，决定了每个并发所处理和维护partition的State信息，针对我们当前的case具体的存储情况如下：\n\n\n![b9840bd73df13cabe4252f6b0bd224a0](Apache-Flink-漫谈系列(03)-State.resources/436E7A11-0256-4373-A3D4-ED5A4363B0E3.png)\n\n\n那么到现在我们发现上面扩容后State得以很好的分配得益于OperatorState采用了List<T>的数据结构的设计。另外大家注意一个问题，相信大家已经发现上面分配partition的算法有一个限制，那就是Source的扩容（并发数）是否可以超过Source物理存储的partition数量呢？答案是否定的，不能。目前Apache Flink的做法是提前报错，即使不报错也是资源的浪费，因为超过partition数量的并发永远分配不到待管理的partition。\n\n## KeyedState对扩容的处理\n对于KeyedState最容易想到的是hash(key) mod parallelism(operator) 方式分配state，就和OperatorState一样，这种分配方式大多数情况是恢复的state不是本地已有的state，需要一次网络拷贝，这种效率比较低，OperatorState采用这种简单的方式进行处理是因为OperatorState的state一般都比较小，网络拉取的成本很小，对于KeyedState往往很大，我们会有更好的选择，在Apache Flink中采用的是Key-Groups方式进行分配。\n\n## 什么是Key-Groups\nKey-Groups 是Apache Flink中对keyed state按照key进行分组的方式，每个key-group中会包含N>0个key，一个key-group是State分配的原子单位。在Apache Flink中关于Key-Group的对象是 KeyGroupRange, 如下：\n\n```\npublic class KeyGroupRange implements KeyGroupsList, Serializable {\n        ...\n        ...\n        private final int startKeyGroup;\n        private final int endKeyGroup;\n        ...\n        ...\n}\n```\n\nKeyGroupRange两个重要的属性就是 startKeyGroup和endKeyGroup，定义了startKeyGroup和endKeyGroup属性后Operator上面的Key-Group的个数也就确定了。\n\n## 什么决定Key-Groups的个数\nkey-group的数量在job启动前必须是确定的且运行中不能改变。由于key-group是state分配的原子单位，而每个operator并行实例至少包含一个key-group，因此operator的最大并行度不能超过设定的key-group的个数，那么在Apache Flink的内部实现上key-group的数量就是最大并行度的值。\n \nGroupRange.of(0, maxParallelism)如何决定key属于哪个Key-Group\n确定好GroupRange之后，如何决定每个Key属于哪个Key-Group呢？我们采取的是取mod的方式，在KeyGroupRangeAssignment中的assignToKeyGroup方法会将key划分到指定的key-group中，如下：\n\n```\npublic static int assignToKeyGroup(Object key, int maxParallelism) {\n  return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);\n}\n\npublic static int computeKeyGroupForKeyHash(int keyHash, int maxParallelism) {\n  return HashPartitioner.INSTANCE.partition(keyHash, maxParallelism);\n}\n\n@Override\npublic int partition(T key, int numPartitions) {\n  return MathUtils.murmurHash(Objects.hashCode(key)) % numPartitions;\n}\n```\n\n如上实现我们了解到分配Key到指定的key-group的逻辑是利用key的hashCode和maxParallelism进行取余操作来分配的。如下图当parallelism=2,maxParallelism=10的情况下流上key与key-group的对应关系如下图所示：\n\n![fbb3b4b83d4ed020c55ff476a3f2f8f7](Apache-Flink-漫谈系列(03)-State.resources/90D10775-4713-43B6-B7FE-F77334D29212.png)\n\n如上图key(a)的hashCode是97，与最大并发10取余后是7，被分配到了KG-7中，流上每个event都会分配到KG-0至KG-9其中一个Key-Group中。\n每个Operator实例如何获取Key-Groups\n 了解了Key-Groups概念和如何分配每个Key到指定的Key-Groups之后，我们看看如何计算每个Operator实例所处理的Key-Groups。 在KeyGroupRangeAssignment的computeKeyGroupRangeForOperatorIndex方法描述了分配算法：\n \n \n```\npublic static KeyGroupRange computeKeyGroupRangeForOperatorIndex(\n  int maxParallelism,\n  int parallelism,\n  int operatorIndex) {\n    GroupRange splitRange = GroupRange.of(0, maxParallelism).getSplitRange(parallelism, operatorIndex);\n    int startGroup = splitRange.getStartGroup();\n    int endGroup = splitRange.getEndGroup();\nreturn new KeyGroupRange(startGroup, endGroup - 1);\n}\n\npublic GroupRange getSplitRange(int numSplits, int splitIndex) {\n    ...\n    final int numGroupsPerSplit = getNumGroups() / numSplits;\n    final int numFatSplits = getNumGroups() % numSplits;\n\n    int startGroupForThisSplit;\n    int endGroupForThisSplit;\n    if (splitIndex &lt; numFatSplits) {\n        startGroupForThisSplit = getStartGroup() + splitIndex * (numGroupsPerSplit + 1);\n        endGroupForThisSplit =   startGroupForThisSplit + numGroupsPerSplit + 1;\n    } else {\n        startGroupForThisSplit = getStartGroup() + splitIndex * numGroupsPerSplit + numFatSplits;\n        endGroupForThisSplit =  startGroupForThisSplit + numGroupsPerSplit;\n    }\n    if (startGroupForThisSplit &gt;= endGroupForThisSplit) {\n            return GroupRange.emptyGroupRange();\n    } else {\n            return new GroupRange(startGroupForThisSplit, endGroupForThisSplit);\n    }\n}\n```\n\n上面代码的核心逻辑是先计算每个Operator实例至少分配的Key-Group个数，将不能整除的部分N个，平均分给前N个实例。最终每个Operator实例管理的Key-Groups会在GroupRange中表示，本质是一个区间值；下面我们就上图的case，说明一下如何进行分配以及扩容后如何重新分配。\n假设上面的Stateful Operation节点的最大并行度maxParallelism的值是10，也就是我们一共有10个Key-Group，当我们并发是2的时候和并发是3的时候分配的情况如下图：\n\n![163e2e62bc6c6e513d7c7fb10cda954f](Apache-Flink-漫谈系列(03)-State.resources/FBDBA73F-4927-4834-8284-4893707EA6FB.png)\n\n\n如上算法我们发现在进行扩容时候，大部分state还是落到本地的，如Task0只有KG-4被分出去，其他的还是保持在本地。同时我们也发现，一个job如果修改了maxParallelism的值那么会直接影响到Key-Groups的数量和key的分配，也会打乱所有的Key-Group的分配，目前在Apache Flink系统中统一将maxParallelism的默认值调整到4096，最大程度的避免无法扩容的情况发生。\n\n## 小结\n\n本篇简单介绍了Apache Flink中State的概念，并重点介绍了OperatorState和KeyedState在扩容时候的处理方式。Apache Flink State是支撑Apache Flink中failover，增量计算，Window等重要机制和功能的核心设施。后续介绍failover，增量计算，Window等相关篇章中也会涉及State的利用，当涉及到本篇没有覆盖的内容时候再补充介绍。"
  },
  {
    "path": "Flink漫谈系列/Apache-Flink漫谈系列(1)-概述.md",
    "content": "![ac7b60f2d0c6bba23165c6e218902a41](Apache-Flink漫谈系列(1)-概述.resources/49D66A77-779B-468F-9BCB-6846609484DA.png)\n\n摘要：Apache Flink 的命脉 \"命脉\" 即生命与血脉，常喻极为重要的事物。系列的首篇，首篇的首段不聊Apache Flink的历史，不聊Apache Flink的架构，不聊Apache Flink的功能特性，我们用一句话聊聊什么是 Apache Flink 的命脉？我的答案是：Apache Flink 是以\"批是流的特例\"的认知进行系统设计的。         \n\n                     \n\n\"命脉\" 即生命与血脉，常喻极为重要的事物。系列的首篇，首篇的首段不聊Apache Flink的历史，不聊Apache Flink的架构，不聊Apache Flink的功能特性，我们用一句话聊聊什么是 Apache Flink 的命脉？我的答案是：Apache Flink 是以\"批是流的特例\"的认知进行系统设计的。\n\n\n\n\n我们经常听说 \"天下武功，唯快不破\"，大概意思是说 \"任何一种武功的招数都是有拆招的，唯有速度快，快到对手根本来不及反应，你就将对手KO了，对手没有机会拆招，所以唯快不破\"。 那么这与Apache Flink有什么关系呢？Apache Flink是Native Streaming(纯流式)计算引擎，在实时计算场景最关心的就是\"快\",也就是 \"低延时\"。\n\n\n\n就目前最热的两种流计算引擎Apache Spark和Apache Flink而言，谁最终会成为No1呢？单从 \"低延时\" 的角度看，Spark是Micro Batching(微批式)模式，最低延迟Spark能达到0.5~2秒左右，Flink是Native Streaming(纯流式)模式，最低延时能达到微秒。很显然是相对较晚出道的 Apache Flink 后来者居上。 那么为什么Apache Flink能做到如此之 \"快\"呢？根本原因是Apache Flink 设计之初就认为 \"批是流的特例\"，整个系统是Native Streaming设计，每来一条数据都能够触发计算。相对于需要靠时间来积攒数据Micro Batching模式来说，在架构上就已经占据了绝对优势。\n\n\n\n那么为什么关于流计算会有两种计算模式呢？归其根本是因为对流计算的认知不同，是\"流是批的特例\" 和 \"批是流的特例\" 两种不同认知产物。\n\n\n\nMicro Batching 模式\n\n\nMicro-Batching 计算模式认为 \"流是批的特例\"， 流计算就是将连续不断的批进行持续计算，如果批足够小那么就有足够小的延时，在一定程度上满足了99%的实时计算场景。那么那1%为啥做不到呢？这就是架构的魅力，在Micro-Batching模式的架构实现上就有一个自然流数据流入系统进行攒批的过程，这在一定程度上就增加了延时。具体如下示意图：\n\n![cbac823e35fa901338428e1b2c490bf9](Apache-Flink漫谈系列(1)-概述.resources/3BF00033-D856-49C2-A301-E6DB65B22EAE.png)\n\n很显然Micro-Batching模式有其天生的低延时瓶颈，但任何事物的存在都有两面性，在大数据计算的发展历史上，最初Hadoop上的MapReduce就是优秀的批模式计算框架，Micro-Batching在设计和实现上可以借鉴很多成熟实践。\n\n\n\nNative Streaming 模式\n\n\nNative Streaming 计算模式认为 \"\"批是流的特\", 这个认知更贴切流的概念，比如一些监控类的消息流，数据库操作的binlog，实时的支付交易信息等等自然流数据都是一条，一条的流入。Native Streaming 计算模式每条数据的到来都进行计算，这种计算模式显得更自然，并且延时性能达到更低。具体如下示意图：\n\n![73b0b324304bfc075c8025bd5d09848f](Apache-Flink漫谈系列(1)-概述.resources/68B36353-346D-4E32-9AD8-8AE91F3FE461.png)\n\n很明显Native Streaming模式占据了流计算领域 \"低延时\" 的核心竞争力，当然Native Streaming模式的实现框架是一个历史先河，第一个实现\nNative Streaming模式的流计算框架是第一个吃螃蟹的人，需要面临更多的挑战，后续章节我们会慢慢介绍。当然Native Streaming模式的框架实现上面很容易实现Micro-Batching和Batching模式的计算，Apache Flink就是Native Streaming计算模式的流批统一的计算引擎。\n\n\n\nApache Flink 按不同的需求支持Local，Cluster，Cloud三种部署模式，同时Apache Flink在部署上能够与其他成熟的生态产品进行完美集成，如 Cluster模式下可以利用YARN(Yet Another Resource Negotiator）/Mesos集成进行资源管理，在Cloud部署模式下可以与GCE(Google Compute Engine), EC2(Elastic Compute Cloud)进行集成。\n\n\n\nLocal 模式\n\n\n该模式下Apache Flink 整体运行在Single JVM中，在开发学习中使用，同时也可以安装到很多端类设备上。参考\n\n\n\nCluster模式\n\n\n该模式是典型的投产的集群模式，Apache Flink 既可以Standalone的方式进行部署，也可以与其他资源管理系统进行集成部署，比如与YARN进行集成。Standalone Cluster 参考 YARN Cluster 参考\n这种部署模式是典型的Master/Slave模式，我们以Standalone Cluster模式为例示意如下：\n\n![2d187cc7509ddf0c8c3f937821e708ea](Apache-Flink漫谈系列(1)-概述.resources/79ACC2B0-BF5E-4DC4-99DE-9C4DF6007A3F.png)\n\n其中JM(JobManager)是Master，TM(TaskManager)是Slave，这种Master/Slave模式有一个典型的问题就是SPOF(single point of failure), SPOF如何解决呢？Apache Flink 又提供了HA(High Availability)方案，也就是提供多个Master，在任何时候总有一个JM服役，N(N>=1)个JM候选,进而解决SPOF问题，示意如下：\n\n\n![b4efa7e407cd155208b16ab325ac06bf](Apache-Flink漫谈系列(1)-概述.resources/5F99E16C-1172-4E31-989B-DA7C0800D476.png)\n\n在实际的生产环境我们都会配置HA方案，目前Alibaba内部使用的也是基于YARN Cluster的HA方案。\n\n\n\nCloud 模式\n\n\n该模式主要是与成熟的云产品进行集成，Apache Flink官网介绍了Google的GCE 参考，Amazon的EC2 参考，在Alibaba我们也可以将Apache Flink部署到Alibaba的ECS(Elastic Compute Service)。\n\n\n\n什么是容错\n\n\n容错(Fault Tolerance) 是指容忍故障，在故障发生时能够自动检测出来并使系统能够自动回复正常运行。当出现某些指定的网络故障、硬件故障、软件错误时，系统仍能执行规定的一组程序，或者说程序不会因系统中的故障而中止，并且执行结果也不会因系统故障而引起计算差错。\n\n\n\n容错的处理模式\n\n\n在一个分布式系统中由于单个进程或者节点宕机都有可能导致整个Job失败，那么容错机制除了要保证在遇到非预期情况系统能够\"运行\"外，还要求能\"正确运行\",也就是数据能按预期的处理方式进行处理，保证计算结果的正确性。计算结果的正确性取决于系统对每一条计算数据处理机制，一般有如下三种处理机制：\n\n\n\nAt Most Once：最多消费一次，这种处理机制会存在数据丢失的可能。\n\nAt Least Once：最少消费一次，这种处理机制数据不会丢失，但是有可能重复消费。\n\nExactly Once：精确一次，无论何种情况下，数据都只会消费一次，这种机制是对数据准确性的最高要求，在金融支付，银行账务等领域必须采用这种模式。\n\n\n\nApache Flink的容错机制\n\n\nApache Flink的Job会涉及到3个部分，外部数据源(External Input), Flink内部数据处理(Flink Data Flow)和外部输出(External Output)。如下示意图:\n\n![7614e8dee7b01008061f1d3622a4d18f](Apache-Flink漫谈系列(1)-概述.resources/C810C9BC-45F5-4ADA-8408-7FF57C0C31DB.png)\n\n目前Apache Flink 支持两种数据容错机制：\n\n\n\n* At Least Once\n* Exactly Once\n\n\n\n其中 Exactly Once 是最严格的容错机制，该模式要求每条数据必须处理且仅处理一次。那么对于这种严格容错机制，一个完整的Flink Job容错要做到 End-to-End 的 容错必须结合三个部分进行联合处理，根据上图我们考虑三个场景：\n\n\n\n系统内部容错\n\n\nApache Flink利用Checkpointing机制来处理容错，Checkpointing的理论基础 Stephan 在 Lightweight Asynchronous Snapshots for Distributed Dataflows 进行了细节描述，该机制源于有K. MANI CHANDY和LESLIE LAMPORT 发表的 Determining-Global-States-of-a-Distributed-System Paper。Apache Flink 基于Checkpointing机制对Flink Data Flow实现了At Least Once 和 Exactly Once 两种容错处理模式。\n\n\n\nApache Flink Checkpointing的内部实现会利用 Barriers，StateBackend等后续章节会详细介绍的技术来将数据的处理进行Marker。Apache Flink会利用Barrier将整个流进行标记切分，如下示意图：\n\n![728f489db662f1d46e7050a526d9e19e](Apache-Flink漫谈系列(1)-概述.resources/72919AF2-F467-4306-BE2B-042FA56E4DD4.png)\n\n这样Apache Flink的每个Operator都会记录当前成功处理的Checkpoint，如果发生错误，就会从上一个成功的Checkpoint开始继续处理后续数据。比如 Soruce Operator会将读取外部数据源的Position实时的记录到Checkpoint中，失败时候会从Checkpoint中读取成功的position继续精准的消费数据。每个算子会在Checkpoint中记录自己恢复时候必须的数据，比如流的原始数据和中间计算结果等信息，在恢复的时候从Checkpoint中读取并持续处理流数据。\n\n\n\n外部Source容错\n\n\nApache Flink 要做到 End-to-End 的 Exactly Once 需要外部Source的支持，比如上面我们说过 Apache Flink的Checkpointing机制会在Source节点记录读取的Position，那就需要外部数据提供读取的Position和支持根据Position进行数据读取。\n\n\n\n外部Sink容错\n\n\nApache Flink 要做到 End-to-End 的 Exactly Once 相对比较困难，如上场景三所述，当Sink Operator节点宕机，重新恢复时候根据Apache Flink 内部系统容错 exactly once的保证,系统会回滚到上次成功的Checkpoin继续写入，但是上次成功Checkpoint之后当前Checkpoint未完成之前已经把一部分新数据写入到kafka了. Apache Flink自上次成功的Checkpoint继续写入kafka，就造成了kafka再次接收到一份同样的来自Sink Operator的数据,进而破坏了End-to-End 的 Exactly Once 语义(重复写入就变成了At Least Once了)，如果要解决这一问题，Apache Flink 利用Two phase commit(两阶段提交)的方式来进行处理。本质上是Sink Operator 需要感知整体Checkpoint的完成，并在整体Checkpoint完成时候将计算结果写入Kafka。\n\n\n\n批与流是两种不同的数据处理模式，如Apache Storm只支持流模式的数据处理，Apache Spark只支持批(Micro Batching)模式的数据处理。那么Apache Flink 是如何做到既支持流处理模式也支持批处理模式呢？\n\n\n\n统一的数据传输层\n\n\n开篇我们就介绍Apache Flink 的 \"命脉\"是以\"批是流的特例\"为导向来进行引擎的设计的，系统设计成为 \"Native Streaming\"的模式进行数据处理。那么Apache FLink将批模式执行的任务看做是流式处理任务的特殊情况，只是在数据上批是有界的(有限数量的元素)。\n\n\n\nApache Flink 在网络传输层面有两种数据传输模式：\n\n\n* PIPELINED模式 - 即一条数据被处理完成以后，立刻传输到下一个节点进行处理。\n* BATCH 模式 - 即一条数据被处理完成后，并不会立刻传输到下一个节点进行处理，而是写入到缓存区，如果缓存写满就持久化到本地硬盘上，最后当所有数据都被处理完成后，才将数据传输到下一个节点进行处理。\n\n\n\n对于批任务而言同样可以利用PIPELINED模式，比如我要做count统计，利用PIPELINED模式能拿到更好的执行性能。只有在特殊情况，比如SortMergeJoin，这时候我们需要全局数据排序，才需要BATCH模式。大部分情况流与批可用统一的传输策略，只有特殊情况，才将批看做是流的一个特例继续特殊处理。\n\n\n\n统一任务调度层\n\n\nApache Flink 在任务调度上流与批共享统一的资源和任务调度机制（后续章节会详细介绍）。\n\n\n\n统一的用户API层\n\n\nApache Flink 在DataStremAPI和DataSetAPI基础上，为用户提供了流批统一的上层TableAPI和SQL，在语法和语义上流批进行高度统一。(其中DataStremAPI和DataSetAPI对流和批进行了分别抽象，这一点并不优雅，在Alibaba内部对其进行了统一抽象）。\n\n\n\n求同存异\n\n\nApache Flink 是流批统一的计算引擎，并不意味着流与批的任务都走统一的code path，在对底层的具体算子的实现也是有各自的处理的，在具体功能上面会根据不同的特性区别处理。比如 批没有Checkpoint机制，流上不能做SortMergeJoin。\n\n\n\n组件栈\n\n\n我们上面内容已经介绍了很多Apache Flink的各种组件，下面我们整体概览一下全貌，如下：\n\n![d919b72d93c2dcfd7b6e452b9f3e8a42](Apache-Flink漫谈系列(1)-概述.resources/624AD4C0-351D-42C0-ACEA-E30851223B5F.png)\n\n\n\nTableAPI和SQL都建立在DataSetAPI和DataStreamAPI的基础之上，那么TableAPI和SQL是如何转换为DataStream和DataSet的呢？\n\n\n\nTableAPI&SQL到DataStrem&DataSet的架构\n\n\nTableAPI&SQL最终会经过Calcite优化之后转换为DataStream和DataSet，具体转换示意如下：\n\n![b491eca0a60b6bd617e2a48124795e6d](Apache-Flink漫谈系列(1)-概述.resources/DC074C98-3D57-4997-AAF6-896BEF272F84.png)\n\n对于流任务最终会转换成DataStream，对于批任务最终会转换成DataSet。\n\n\n\nANSI-SQL的支持\n\n\nApache Flink 之所以利用ANSI-SQL作为用户统一的开发语言，是因为SQL有着非常明显的优点，如下：\n\n![e2f124f35ef0ba1c14ae9cc7b995b7ef](Apache-Flink漫谈系列(1)-概述.resources/7C067E43-57A2-4A83-BE9C-3C769DDFF6C6.png)\n\n\nDeclarative - 用户只需要表达我想要什么，不用关心如何计算。\n\nOptimized - 查询优化器可以为用户的 SQL 生成最优的执行计划，获取最好的查询性能。\n\nUnderstandable - SQL语言被不同领域的人所熟知，用SQL 作为跨团队的开发语言可以很大地提高效率。\n\nStable - SQL 是一个拥有几十年历史的语言，是一个非常稳定的语言，很少有变动。\n\nUnify - Apache Flink在引擎上对流与批进行统一，同时又利用ANSI-SQL在语法和语义层面进行统一。\n\n\n\n无限扩展的优化机制\n\n\nApache Flink 利用Apache Calcite对SQL进行解析和优化，Apache Calcite采用Calcite是开源的一套查询引擎，实现了两套Planner：\n\n\n\nHepPlanner - 是RBO(Rule Base Optimize)模式，基于规则的优化。\n\nVolcanoPlanner - 是CBO(Cost Base Optimize)模式，基于成本的优化。\n\n\n\nFlink SQL会利用Calcite解析优化之后，最终转换为底层的DataStrem和Dataset。上图中 Batch rules和Stream rules可以根据优化需要无限添加优化规则。\n\n\n\nApache Flink 优秀的架构就像一座摩天大厦的地基一样为Apache Flink 持久的生命力打下了良好的基础，为打造Apache Flink丰富的功能生态留下无限的空间。\n\n\n\n类库\n\n\nCEP - 复杂事件处理类库，核心是一个状态机，广泛应用于事件驱动的监控预警类业务场景。\n\nML - 机器学习类库，机器学习主要是识别数据中的关系、趋势和模式，一般应用在预测类业务场景。\n\nGELLY - 图计算类库，图计算更多的是考虑边和点的概念，一般被用来解决网状关系的业务场景。\n\n\n\n算子\n\n\nApache Flink 提供了丰富的功能算子，对于数据流的处理来讲，可以分为单流处理(一个数据源)和多流处理(多个数据源)。\n\n\n\n多流操作\n\n\n如上通过UION和JOIN我们可以将多流最终变成单流，Apache Flink 在单流上提供了更多的操作算子。\n\n\n\n单流操作\n\n\n将多流变成单流之后，我们按数据输入输出的不同归类如下：\n\n\n\n类型\t输入\t输出\tTable/SQL算子\tDataStream/DataSet算子\nScalar Function\t1\t1\tBuilt-in & UDF,\tMap\nTable Function\t1\tN(N>=0)\tBuilt-in & UDTF\tFlatMap\nAggregate Function\tN(N>=0)\t1\tBuilt-in & UDAF\tReduce\n\n\n如上表格对单流上面操作做简单归类，除此之外还可以做 过滤，排序，窗口等操作，我们后续章节会逐一介绍。\n\n\n\n存在的问题\n\n\nApache Flink 目前的架构还存在很大的优化空间，比如前面提到的DataStreamAPI和DataSetAPI其实是流与批在API层面不统一的体现，同时看具体实现会发现DataStreamAPI会生成Transformation tree然后生成StreamGraph，最后生成JobGraph，底层对应StreamTask，但DataSetAPI会形成Operator tree，flink-optimize模块会对Batch Plan进行优化，形成Optimized Plan 后形成JobGraph,最后形成BatchTask。具体示意如下：\n\n![bb52accfabda8668b66b21ddc0e6380c](Apache-Flink漫谈系列(1)-概述.resources/1DEA43A8-381D-451C-9B99-9106B2B058B5.png)\n\n这种情况其实 DataStreamAPI到Runtime 和 DataSetAPI到Runtime的实现上并没有得到最大程度的统一和复用。在这一点上面Aalibab 企业版的Flink在架构和实现上都进行了进一步优化。\n\n\n\n组件栈\n\n\nAlibaba 对Apache Flink进行了大量的架构优化，如下架构是一直努力的方向，大部分功能还在持续开发中，具体如下：\n\n\n![41be7de757381783db0856e2d9ca0832](Apache-Flink漫谈系列(1)-概述.resources/928F452D-B175-4740-A139-8186EEDAC99C.png)\n\n如上架构我们发现较大的变化是：\n\n\n\nQP/QE/QO - 我们增加了QP/QE/QO层，在这一层进行统一的流和批的查询优化和底层算子的转换。\n\nDAG API - 我们在Runtime层面统一抽象API接口，在API层对流与批进行统一。\n\n\n\nTableAPI&SQL到Runtime的架构\n\n\nApache Flink执行层是流批统一的设计，在API和算子设计上面我们尽量达到流批的共享，在TableAPI和SQL层无论是流任务还是批任务最终都转换为统一的底层实现。这个层面最核心的变化是批最终也会生成StreamGraph，执行层运行Stream Task，如下：\n\n![153476e3b50765b18f6516827c2a28f5](Apache-Flink漫谈系列(1)-概述.resources/61F69F73-CEE1-43F4-BB88-177A115FC62E.png)\n\n本篇概要的介绍了\"批是流的特例\"这一设计观点是Apache Flink的\"命脉\"，它决定了Apache Flink的运行模式是纯流式的，这在实时计算场景的\"低延迟\"需求上，相对于Micro Batching模式占据了架构的绝对优势，同时概要的向大家介绍了Apache Flink的部署模式，容错处理，引擎的统一性和Apache Flink的架构，最后和大家分享了Apache Flink的优化架构。\n\n本篇没有对具体技术进行详细展开，大家只要对Apache Flink有初步感知，头脑中知道Alibaba对Apache Flink进行了架构优化，增加了众多功能就可以了，至于Apache Flink的具体技术细节和实现原理，以及Alibaba对Apache Flink做了哪些架构优化和增加了哪些功能后续章节会展开介绍！"
  },
  {
    "path": "Flink漫谈系列/我的Markdown笔记/Apache-Flink-漫谈系列(03)-State.md",
    "content": "## 实际问题\n\n在流计算场景中，数据会源源不断的流入Apache Flink系统，每条数据进入Apache Flink系统都会触发计算。如果我们想进行一个Count聚合计算，那么每次触发计算是将历史上所有流入的数据重新新计算一次，还是每次计算都是在上一次计算结果之上进行增量计算呢？答案是肯定的，Apache Flink是基于上一次的计算结果进行增量计算的。那么问题来了: \"上一次的计算结果保存在哪里，保存在内存可以吗？\"，答案是否定的，如果保存在内存，在由于网络，硬件等原因造成某个计算节点失败的情况下，上一次计算结果会丢失，在节点恢复的时候，就需要将历史上所有数据（可能十几天，上百天的数据）重新计算一次，所以为了避免这种灾难性的问题发生，Apache Flink 会利用State存储计算结果。本篇将会为大家介绍Apache Flink State的相关内容。\n\n## 什么是State\n\n这个问题似乎有些\"弱智\"？不管问题的答案是否显而易见，但我还是想简单说一下在Apache Flink里面什么是State？State是指流计算过程中计算节点的中间计算结果或元数据属性，比如 在aggregation过程中要在state中记录中间聚合结果，比如 Apache Kafka 作为数据源时候，我们也要记录已经读取记录的offset，这些State数据在计算过程中会进行持久化(插入或更新)。所以Apache Flink中的State就是与时间相关的，Apache Flink任务的内部数据（计算数据和元数据属性）的快照。\n\n## 为什么需要State\n\n与批计算相比，State是流计算特有的，批计算没有failover机制，要么成功，要么重新计算。流计算在 大多数场景 下是增量计算，数据逐条处理（大多数场景)，每次计算是在上一次计算结果之上进行处理的，这样的机制势必要将上一次的计算结果进行存储（生产模式要持久化），另外由于 机器，网络，脏数据等原因导致的程序错误，在重启job时候需要从成功的检查点(checkpoint，后面篇章会专门介绍)进行state的恢复。增量计算，Failover这些机制都需要state的支撑。\n\n## State 实现\n\nApache Flink内部有四种state的存储实现，具体如下：\n\n* 基于内存的HeapStateBackend - 在debug模式使用，不 建议在生产模式下应用；\n* 基于HDFS的FsStateBackend - 分布式文件持久化，每次读写都产生网络IO，整体性能不佳；\n* 基于RocksDB的RocksDBStateBackend - 本地文件+异步HDFS持久化；\n* 还有一个是基于Niagara(Alibaba内部实现)NiagaraStateBackend - 分布式持久化- 在Alibaba生产环境应用；\n\n## State 持久化逻辑\n\nApache Flink版本选择用RocksDB+HDFS的方式进行State的存储，State存储分两个阶段，首先本地存储到RocksDB，然后异步的同步到远程的HDFS。 这样而设计既消除了HeapStateBackend的局限（内存大小，机器坏掉丢失等），也减少了纯分布式存储的网络IO开销。\n\n![c6e2f3f39188c48255c79834ab872c5e](Apache-Flink-漫谈系列(03)-State.resources/11AD7C2A-A1DD-4238-8226-AED47EF6F446.png)\n\n## State 分类\nApache Flink 内部按照算子和数据分组角度将State划分为如下两类：\n\n* KeyedState - 这里面的key是我们在SQL语句中对应的GroupBy/PartitioneBy里面的字段，key的值就是groupby/PartitionBy字段组成的Row的字节数组，每一个key都有一个属于自己的State，key与key之间的State是不可见的；\n\n* OperatorState - Apache Flink内部的Source Connector的实现中就会用OperatorState来记录source数据读取的offset。 \n\n## State 扩容重新分配\n\nApache Flink是一个大规模并行分布式系统，允许大规模的有状态流处理。 为了可伸缩性，Apache Flink作业在逻辑上被分解成operator graph，并且每个operator的执行被物理地分解成多个并行运算符实例。 从概念上讲，Apache Flink中的每个并行运算符实例都是一个独立的任务，可以在自己的机器上调度到网络连接的其他机器运行。\n\nApache Flink的DAG图中只有边相连的节点🈶网络通信，也就是整个DAG在垂直方向有网络IO，在水平方向如下图的stateful节点之间没有网络通信，这种模型也保证了每个operator实例维护一份自己的state，并且保存在本地磁盘（远程异步同步）。通过这种设计，任务的所有状态数据都是本地的，并且状态访问不需要任务之间的网络通信。 避免这种流量对于像Apache Flink这样的大规模并行分布式系统的可扩展性至关重要。\n\n如上我们知道Apache Flink中State有OperatorState和KeyedState，那么在进行扩容时候（增加并发）State如何分配呢？比如：外部Source有5个partition，在Apache Flink上面由Srouce的1个并发扩容到2个并发，中间Stateful Operation 节点由2个并发并扩容的3个并发，如下图所示:\n\n![9c48ca9908816902cd317a26e1fffa26](Apache-Flink-漫谈系列(03)-State.resources/60A3963C-B15F-456F-9B37-69C022B6491D.png)\n\n在Apache Flink中对不同类型的State有不同的扩容方法，接下来我们分别介绍。\n\n\n## OperatorState对扩容的处理\n\n我们选取Apache Flink中某个具体Connector实现实例进行介绍，以MetaQ为例，MetaQ以topic方式订阅数据，每个topic会有N>0个分区，以上图为例，加上我们订阅的MetaQ的topic有5个分区，那么当我们source由1个并发调整为2个并发时候，State是怎么恢复的呢？\nstate 恢复的方式与Source中OperatorState的存储结构有必然关系，我们先看MetaQSource的实现是如何存储State的。首先MetaQSource 实现了ListCheckpointed<T extends Serializable>，其中的T是Tuple2<InputSplit,Long>，我们在看ListCheckpointed接口的内部定义如下：\n\n```\npublic interface ListCheckpointed<T extends Serializable>; {\nList<T> snapshotState(long var1, long var3) throws Exception;\n\nvoid restoreState(List&lt;T&gt; var1) throws Exception;\n}\n```\n我们发现 snapshotState方法的返回值是一个List<T>,T是Tuple2<InputSplit,Long>，也就是snapshotState方法返回List<Tuple2<InputSplit,Long>>,这个类型说明state的存储是一个包含partiton和offset信息的列表，InputSplit代表一个分区，Long代表当前partition读取的offset。InputSplit有一个方法如下：\n\n```\npublic interface InputSplit extends Serializable {\n    int getSplitNumber();\n}\n```\n\n也就是说，InputSplit我们可以理解为是一个Partition索引，有了这个数据结构我们在看看上面图所示的case是如何工作的？当Source的并行度是1的时候，所有打partition数据都在同一个线程中读取，所有partition的state也在同一个state中维护，State存储信息格式如下：\n\n![67cf069e8320cfeb0cf3fba127ed2cf9](Apache-Flink-漫谈系列(03)-State.resources/736E827E-E4C1-4412-8F0C-F51AECB68329.png)\n\n如果我们现在将并发调整为2，那么我们5个分区的State将会在2个独立的任务（线程）中进行维护，在内部实现中我们有如下算法进行分配每个Task所处理和维护partition的State信息，如下：\n\n```\nList<Integer> assignedPartitions = new LinkedList<>();\nfor (int i = 0; i < partitions; i++) {\n        if (i % consumerCount == consumerIndex) {\n                assignedPartitions.add(i);\n        }\n}\n```\n\n这个求mod的算法，决定了每个并发所处理和维护partition的State信息，针对我们当前的case具体的存储情况如下：\n\n\n![b9840bd73df13cabe4252f6b0bd224a0](Apache-Flink-漫谈系列(03)-State.resources/436E7A11-0256-4373-A3D4-ED5A4363B0E3.png)\n\n\n那么到现在我们发现上面扩容后State得以很好的分配得益于OperatorState采用了List<T>的数据结构的设计。另外大家注意一个问题，相信大家已经发现上面分配partition的算法有一个限制，那就是Source的扩容（并发数）是否可以超过Source物理存储的partition数量呢？答案是否定的，不能。目前Apache Flink的做法是提前报错，即使不报错也是资源的浪费，因为超过partition数量的并发永远分配不到待管理的partition。\n\n## KeyedState对扩容的处理\n对于KeyedState最容易想到的是hash(key) mod parallelism(operator) 方式分配state，就和OperatorState一样，这种分配方式大多数情况是恢复的state不是本地已有的state，需要一次网络拷贝，这种效率比较低，OperatorState采用这种简单的方式进行处理是因为OperatorState的state一般都比较小，网络拉取的成本很小，对于KeyedState往往很大，我们会有更好的选择，在Apache Flink中采用的是Key-Groups方式进行分配。\n\n## 什么是Key-Groups\nKey-Groups 是Apache Flink中对keyed state按照key进行分组的方式，每个key-group中会包含N>0个key，一个key-group是State分配的原子单位。在Apache Flink中关于Key-Group的对象是 KeyGroupRange, 如下：\n\n```\npublic class KeyGroupRange implements KeyGroupsList, Serializable {\n        ...\n        ...\n        private final int startKeyGroup;\n        private final int endKeyGroup;\n        ...\n        ...\n}\n```\n\nKeyGroupRange两个重要的属性就是 startKeyGroup和endKeyGroup，定义了startKeyGroup和endKeyGroup属性后Operator上面的Key-Group的个数也就确定了。\n\n## 什么决定Key-Groups的个数\nkey-group的数量在job启动前必须是确定的且运行中不能改变。由于key-group是state分配的原子单位，而每个operator并行实例至少包含一个key-group，因此operator的最大并行度不能超过设定的key-group的个数，那么在Apache Flink的内部实现上key-group的数量就是最大并行度的值。\n \nGroupRange.of(0, maxParallelism)如何决定key属于哪个Key-Group\n确定好GroupRange之后，如何决定每个Key属于哪个Key-Group呢？我们采取的是取mod的方式，在KeyGroupRangeAssignment中的assignToKeyGroup方法会将key划分到指定的key-group中，如下：\n\n```\npublic static int assignToKeyGroup(Object key, int maxParallelism) {\n  return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);\n}\n\npublic static int computeKeyGroupForKeyHash(int keyHash, int maxParallelism) {\n  return HashPartitioner.INSTANCE.partition(keyHash, maxParallelism);\n}\n\n@Override\npublic int partition(T key, int numPartitions) {\n  return MathUtils.murmurHash(Objects.hashCode(key)) % numPartitions;\n}\n```\n\n如上实现我们了解到分配Key到指定的key-group的逻辑是利用key的hashCode和maxParallelism进行取余操作来分配的。如下图当parallelism=2,maxParallelism=10的情况下流上key与key-group的对应关系如下图所示：\n\n![fbb3b4b83d4ed020c55ff476a3f2f8f7](Apache-Flink-漫谈系列(03)-State.resources/90D10775-4713-43B6-B7FE-F77334D29212.png)\n\n如上图key(a)的hashCode是97，与最大并发10取余后是7，被分配到了KG-7中，流上每个event都会分配到KG-0至KG-9其中一个Key-Group中。\n每个Operator实例如何获取Key-Groups\n 了解了Key-Groups概念和如何分配每个Key到指定的Key-Groups之后，我们看看如何计算每个Operator实例所处理的Key-Groups。 在KeyGroupRangeAssignment的computeKeyGroupRangeForOperatorIndex方法描述了分配算法：\n \n \n```\npublic static KeyGroupRange computeKeyGroupRangeForOperatorIndex(\n  int maxParallelism,\n  int parallelism,\n  int operatorIndex) {\n    GroupRange splitRange = GroupRange.of(0, maxParallelism).getSplitRange(parallelism, operatorIndex);\n    int startGroup = splitRange.getStartGroup();\n    int endGroup = splitRange.getEndGroup();\nreturn new KeyGroupRange(startGroup, endGroup - 1);\n}\n\npublic GroupRange getSplitRange(int numSplits, int splitIndex) {\n    ...\n    final int numGroupsPerSplit = getNumGroups() / numSplits;\n    final int numFatSplits = getNumGroups() % numSplits;\n\n    int startGroupForThisSplit;\n    int endGroupForThisSplit;\n    if (splitIndex &lt; numFatSplits) {\n        startGroupForThisSplit = getStartGroup() + splitIndex * (numGroupsPerSplit + 1);\n        endGroupForThisSplit =   startGroupForThisSplit + numGroupsPerSplit + 1;\n    } else {\n        startGroupForThisSplit = getStartGroup() + splitIndex * numGroupsPerSplit + numFatSplits;\n        endGroupForThisSplit =  startGroupForThisSplit + numGroupsPerSplit;\n    }\n    if (startGroupForThisSplit &gt;= endGroupForThisSplit) {\n            return GroupRange.emptyGroupRange();\n    } else {\n            return new GroupRange(startGroupForThisSplit, endGroupForThisSplit);\n    }\n}\n```\n\n上面代码的核心逻辑是先计算每个Operator实例至少分配的Key-Group个数，将不能整除的部分N个，平均分给前N个实例。最终每个Operator实例管理的Key-Groups会在GroupRange中表示，本质是一个区间值；下面我们就上图的case，说明一下如何进行分配以及扩容后如何重新分配。\n假设上面的Stateful Operation节点的最大并行度maxParallelism的值是10，也就是我们一共有10个Key-Group，当我们并发是2的时候和并发是3的时候分配的情况如下图：\n\n![163e2e62bc6c6e513d7c7fb10cda954f](Apache-Flink-漫谈系列(03)-State.resources/FBDBA73F-4927-4834-8284-4893707EA6FB.png)\n\n\n如上算法我们发现在进行扩容时候，大部分state还是落到本地的，如Task0只有KG-4被分出去，其他的还是保持在本地。同时我们也发现，一个job如果修改了maxParallelism的值那么会直接影响到Key-Groups的数量和key的分配，也会打乱所有的Key-Group的分配，目前在Apache Flink系统中统一将maxParallelism的默认值调整到4096，最大程度的避免无法扩容的情况发生。\n\n## 小结\n\n本篇简单介绍了Apache Flink中State的概念，并重点介绍了OperatorState和KeyedState在扩容时候的处理方式。Apache Flink State是支撑Apache Flink中failover，增量计算，Window等重要机制和功能的核心设施。后续介绍failover，增量计算，Window等相关篇章中也会涉及State的利用，当涉及到本篇没有覆盖的内容时候再补充介绍。"
  },
  {
    "path": "Hadoop/Hadoop极简入门.md",
    "content": "\n其实Hadoop诞生至今已经十多年了，网络上也充斥着关于Hadoop相关知识的海量资源。但是，有时还是会使刚刚接触大数据领域的童鞋分不清hadoop、hdfs、Yarn和MapReduce等等技术词汇。\n\nHadoop是ASF(Apache软件基金会)开源的，根据Google开源的三篇大数据论文设计的，一个能够允许大量数据在计算机集群中，通过使用简单的编程模型进行分布式处理的框架。其设计的规模可从单一的服务器到数千台服务器，每一个均可提供局部运算和存储功能。Hadoop并不依赖昂贵的硬件以支持高可用性。Hadoop可以检测并处理应用层上的错误，并可以把错误转移到其他服务器上(让它错误，我在用别的服务器顶上就可以了)，所以Hadoop提供一个基于计算机集群的、高效性的服务。\n\n经过十年的发展，Hadoop这个名词的本身也在不断进化者，目前我们提到Hadoop大多是指大数据的生态圈，这个生态圈包括众多的软件技术(e.g.  HBase、Hive和Spark等等)。\n\n有如Spring框架有着最基础的几个模块Context、Bean和Core，其他的模块和项目都是基于这些基础模块构建。Hadoop与之一样，也有最基础的几个模块。\n\n**Common**: 支持其他模块的公用工具包。\n\n\n**HDFS**: 一个可高吞吐访问应用数据的分布式文件系统。\n\n**YARN**: 一个管理集群服务器资源和任务调度的框架。\n\n**MapReduce**: 基于Yarn对大数据集进行并行计算的系统。\n\n其它的，像HBase、Hive等等不过在这几个基础模块上的高级抽象。另外Hadoop也不是目前大数据的唯一解决方案，像Amazon的大数据技术方案等等。\n\nCommon\nCommon模块是Hadoop最为基础的模块，他为其他模块提供了像操作文件系统、I/O、序列化和远程方法调用等最为基础的实现。如果想深入的了解Hadoop的具体实现，可以阅读一下Common的源码。\n\n## HDFS\nHDFS是“Hadoop Distributed File System”的首字母缩写，是一种设计运行在一般硬件条件（不需要一定是服务器级别的设备，但更好的设备能发挥更大的作用）下的分布式文件系统. 他和现有的其他分布式文件系统(e.g. RAID)有很多相似的地方。和其他分布式文件系统的不同之处是HDFS设计为运行在低成本的硬件上(e.g. 普通的PC机)，且提供高可靠性的服务器. HDFS设计满足大数据量，大吞吐量的应用情况。\n\n为了更好的理解分布式文件系统，我们先从文件讲起。\n\n### 文件\n\n文件这个词，恐怕只要是现代人都不会陌生。但是在不同行业中，文件有着不同的意义。在计算机科学领域，文件是什么呢？文件是可以在目录中看的见的图标么？当然不是。文件在存储设备时，是个N长的字节序列。而在一个计算机使用者的角度而言，文件是对所有I/O设备的抽象。每个I/O设备都可以视为文件，包括磁盘、键盘和网络等。文件这个简单而精致的概念其内涵是十分丰富的，它向应用程序提供了一个统一的视角，来看待系统中可能含有的各式各样的I/O设备。\n\n### 文件系统\n\n那么一台计算机上肯定不止一个文件，成千上万的文件怎么管理呢？因此需要我们需要一种对文件进行管理的东西，即文件系统。文件系统是一种在计算机上存储和组织数据的方法，它使得对其访问和查找变得容易，文件系统使用文件和树形目录的抽象逻辑概念代替了硬盘和光盘等物理设备使用数据块的概念，用户使用文件系统来保存数据而不必关心数据实际保存在硬盘的地址为多少的数据块上，只需要记住这个文件的所属目录和文件名。在写入新数据之前，用户不必关心硬盘上的那个块地址没有被使用，硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成，用户只需要记住数据被写入到了哪个文件中即可。\n\n### 分布式文件系统\n\n相对于单机的文件系统而言，分布式文件系统(Distributed file system)。是一种允许文件通过网络在多台主机上分享的文件系统，可让多计算机上的多用户分享文件和存储空间。\n\n在这样的文件系统中，客户端并非直接访问底层的数据存储区块和磁盘。而是通过网络，基于单机文件系统并借由特定的通信协议的帮助，来实现对于文件系统的读写。\n\n分布式文件系统需要拥有的最基本的能力是通过畅通网络I/O来实现数据的复制与容错。也就是说，一方面一个文件是分为多个数据块分布在多个设备中。另一方面，数据块有多个副本分布在不同的设备上。即使有一小部分的设备出现离线和宕机等情况，整体来说文件系统仍然可以持续运作而不会有数据损失。\n\n注意:分布式文件系统和分布式数据存储的界线是模糊的，但一般来说，分布式文件系统是被设计用在局域网，比较强调的是传统文件系统概念的延伸，并通过软件方法来达成容错的目的。而分布式数据存储，则是泛指应用分布式运算技术的文件和数据库等提供数据存储服务的系统。\n\n### HDFS\n\nHDFS正是Hadoop中负责分布式文件系统的。HDFS采用master/slave架构。一个HDFS集群是由一个Namenode和一定数目的Datanodes组成。Namenode是一个中心服务器，负责管理文件系统的命名空间以及文件的访问控制。集群中的Datanode一般是一个设备上部署一个，负责管理它所在节点上的存储。HDFS暴露了文件系统的命名空间，用户能够以文件的形式在上面存储数据。实际上，一个文件会被分成一个或多个数据块，这些块存储在一组Datanode上。Namenode执行文件系统的命名空间操作，比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体Datanode设备的映射。Datanode负责处理文件系统客户端的读写请求。在Namenode的统一调度下进行数据块的创建、删除和复制。为了保证文件系统的高可靠，往往需要另一个Standby的Namenode在Actived Namenode出现问题后，立刻接管文件系统。\n\n网络上有很多关于hdfs的安装配置手册，本文就不再复述。只提供一个以前项目中应用过的部署架构仅供大家参考。\n\n![d5ca565f7d2f845320da67b0b90fb62e](Hadoop极简入门.resources/1558787234954.jpg)\n\n\n这个高可用的HDFS架构是由3台zookeeper设备、2台域名服务(DNS)和时间服务(NTP)设备、2台Namenode设备(如果必要Standby可以更多)、一个共享存储设备(NFS)和N个DataNode组成。\n\nZookeeper负责接受NameNode的心跳，当Actived namenode不向zookeeper报告心跳时，Standby Namenode的监控进程会收到这个消息，从而激活Standby NameNode并接管Active NameNode的工作。\n\nNFS负责为2个NameNode存储EditLog文件，(NameNode 在执行 HDFS 客户端提交的创建文件或者移动文件这样的写操作时，会首先把这些操作记录在 EditLog 文件之中，然后再更新内存中的文件系统镜像，最终再刷新到磁盘。 EditLog 只是在数据恢复的时候起作用。记录在 EditLog 之中的每一个操作又称为一个事务，每个事务有一个整数形式的事务 id 作为编号。EditLog 会被切割为很多段，每一段称为一个 Segment)当发生NameNode切换的情况时，Standby NameNode接管后，会根据EditLog中把未完成的写操作继续下去并开使向EditLog写入新的写操作记录。(此外，hadoop还提供了另一种QJM的EditLog方案)\n\nDNS&NTP分布负责整个系统的(包括客户端)域名服务和时间服务。这个在集群部署中是非常有必要的两个存在。首先说一下DNS的必要性，一、Hadoop是极力提倡用机器名作为在HDFS环境中的标识。二、当然可以在/etc/hosts文件中去标明机器名和IP的映射关系，可是请想想如果在一个数千台设备的集群中添加一个设备时，负责系统维护的伙伴会不会骂集群的设计者呢？其次是NTP的必要性，在刚刚开始接触Hadoop集群时我遇到的大概90%的问题是由于各个设备时间不一致导致的。各个设备的时间同步是数据一致性和管理一致性的一个基本保障。\n\n### MapReduce\nMapReduce是一个使用简单的软件框架，基于它写出来的应用程序能够运行在由上千个商用机器组成的大型集群上，并以一种可靠容错的方式并行处理上T级别的数据集。\n\n一个MapReduce 作业(job)通常会把输入的数据集切分为若干独立的数据块，由 map任务(task)以完全并行的方式处理它们。框架会对map的输出先进行排序， 然后把结果输入给reduce任务。通常作业的输入和输出都会被存储在文件系统中。 整个框架负责任务的调度和监控，以及重新执行已经失败的任务。\n\n通常，MapReduce框架和HDFS是运行在一相同的设备集群上的，也就是说，计算设备和存储设备通常在一起。这种配置允许框架在那些已经存好数据的设备上高效地调度任务，这可以使整个集群的网络带宽被非常高效地利用。\n\nMapReduce框架由一个单独的master JobTracker 和每个集群设备一个slave TaskTracker共同组成。master负责调度构成一个作业的所有任务，这些任务分布在不同的slave上，master监控它们的执行，重新执行已经失败的任务。而slave仅负责执行由master指派的任务。\n\n用户编写的MapReduce应用程序应该指明输入/输出的文件位置(路径)，并通过实现合适的接口或抽象类提供map和reduce函数。再加上其他作业的参数，就构成了作业配置(job configuration)。然后，job client提交作业(jar包/可执行程序等)和配置信息给JobTracker，后者负责分发这些软件和配置信息给slave、调度任务并监控它们的执行，同时提供状态和诊断信息给job-client。\n\n![cd1528eceb1199009fdb11b1e8432a5b](Hadoop极简入门.resources/1558787253396.jpg)\n\n\n\n在抽象的层面上MapReduce是由两个函数Map和Reduce组成的。简单来说，一个Map函数就是对一些独立元素组成的概念上的列表的每一个元素进行指定的操作。事实上，每个元素都是被独立操作的，而原始列表没有被更改，因为这里创建了一个新的列表来保存操作结果。这就是说，Map操作是可以高度并行的。而Reduce函数指的是对Map函数的结果（中间经过洗牌的过程，会把map的结果进行分组）分组后多个列表的元素进行适当的归并。\n\n注意:虽然Hadoop框架是用JavaTM实现的，但MapReduce应用程序则不一定要用 Java来写 。至少Scala是可以写的哟。\n\n附上Scala实现的计算词频的Scala源码\n\n    \n```\nimport java.io.IOException\nimport java.util.StringTokenizer\n\nimport org.apache.hadoop.conf.Configuration\nimport org.apache.hadoop.fs.Path\nimport org.apache.hadoop.io.{IntWritable, Text}\nimport org.apache.hadoop.mapreduce.lib.input.FileInputFormat\nimport org.apache.hadoop.mapreduce.lib.output.FileOutputFormat\nimport org.apache.hadoop.mapreduce.{Job, Mapper, Reducer}\n\nimport scala.collection.JavaConversions\n\nobject WordCount {\ndef main(args: Array[String]): Unit = {\nval job = new Job(new Configuration(), \"WordCount\")\njob.setJarByClass(classOf[WordMapper]);\njob.setMapperClass(classOf[WordMapper]);\njob.setCombinerClass(classOf[WordReducer]);\njob.setReducerClass(classOf[WordReducer]);\njob.setOutputKeyClass(classOf[Text]);\njob.setOutputValueClass(classOf[IntWritable]);\njob.setNumReduceTasks(1)\nFileInputFormat.addInputPath(job, new Path(args(0)));\nFileOutputFormat.setOutputPath(job, new Path(args(1)));\nSystem.exit(job.waitForCompletion(true) match { case true => 0\ncase false => 1\n});\n}\n}\n\nclass WordMapper extends Mapper[Object, Text, Text, IntWritable] {\nval one = new IntWritable(1)\n\n@throws[IOException]\n@throws[InterruptedException]\noverride def map(key: Object, value: Text, context: Mapper[Object, Text, Text, IntWritable]#Context) = {\nval stringTokenizer = new StringTokenizer(value.toString());\nwhile (stringTokenizer.hasMoreTokens()) {\ncontext.write(new Text(stringTokenizer.nextToken()), one);\n}\n}\n}\n\nclass WordReducer extends Reducer[Text, IntWritable, Text, IntWritable] {\n@throws[IOException]\n@throws[InterruptedException]\noverride def reduce(key: Text, values: java.lang.Iterable[IntWritable], context: Reducer[Text, IntWritable, Text, IntWritable]#Context) = {\nimport JavaConversions.iterableAsScalaIterable\ncontext.write(key, new IntWritable(values.map(x=>x.get()).reduce(_+_)));\n}\n}\n```\n\n### Yarn\nYARN(Yet Another Resource Negotiator)是Hadoop的设备资源管理器，它是一个通用资源管理系统，MapReduce和其他上层应用提供统一的资源管理和调度，它为集群在利用率、资源统一管理和数据共享等方面提供了巨大的帮助。\n\nYarn由ResourceManager、NodeManager、ApplicationMaster和Containe四个概念构成。\n\n![b8685cf68532773d51876a447537fdef](Hadoop极简入门.resources/1558787271436.jpg)\n\nResourceManager是一个全局的资源管理器，负责整个系统的资源管理和分配。它主要由两个组件构成:调度器(Scheduler)和应用程序管理器(Applications Manager)。调度器根据容量、队列等限制条件，将系统中的资源分配给各个正在运行的MapReduce程序。应用程序管理器负责管理整个系统中所有MapReduce程序，包括提交、与调度器协商资源以启动ApplicationMaster、监控ApplicationMaster运行状态并在失败时重新启动它等。\n\n用户提交的每个MapReduce程序均包含一个ApplicationMaster，主要功能包括：与ResourceManager调度器协商以获取资源(用Container表示)；将得到的任务进一步分配给内部的任务(资源的二次分配)；与NodeManager通信以启动/停止任务；监控所有任务运行状态，并在任务运行失败时重新为任务申请资源以重启任务。\n\nNodeManager是每个设备上的资源和任务管理器，一方面，它会定时地向ResourceManager汇报本设备上的资源使用情况和各个Container的运行状态；另一方面，它接收并处理来自ApplicationMaster的Container启动/停止等各种请求。\n\nContainer是YARN中的资源抽象，它封装了某个设备上的多维度资源，如内存、CPU、磁盘、网络等，当AM向RM申请资源时，RM为AM返回的资源便是用Container表示。\n\n## 结语\n本文走马观花的介绍了Hadoop相关内容。文章的主要目的是给大家一个对大数据的分布式解决方案的感官印象，为后面的大数据相关文章提供一个基础的理解。最后要强调的是，思考大数据方向的问题是一定要记住分布式的概念，因为你的数据并不在一个设备中甚至不再一个集群中，而且计算也是分布的。所以在设计大数据应用程序时，要花时间思考程序和算法在单机应用和分布式应用所产生的不同(e.g. 加权平均值)。\n\n"
  },
  {
    "path": "Hadoop/MapReduce编程模型和计算框架架构原理.md",
    "content": "Hadoop解决大规模数据分布式计算的方案是MapReduce。MapReduce既是一个编程模型，又是一个计算框架。也就是说，开发人员必须基于MapReduce编程模型进行编程开发，然后将程序通过MapReduce计算框架分发到Hadoop集群中运行。我们先看一下作为编程模型的MapReduce。\n\n### MapReduce编程模型\n\nMapReduce是一种非常简单又非常强大的编程模型。\n\n简单在于其编程模型只包含map和reduce两个过程，map的主要输入是一对<key , value>值，经过map计算后输出一对<key , value>值；然后将相同key合并，形成<key , value集合>；再将这个<key , value集合>输入reduce，经过计算输出零个或多个<key , value>对。\n\n但是MapReduce同时又是非常强大的，不管是关系代数运算（SQL计算），还是矩阵运算（图计算），大数据领域几乎所有的计算需求都可以通过MapReduce编程来实现。\n\n我们以WordCount程序为例。WordCount主要解决文本处理中的词频统计问题，就是统计文本中每一个单词出现的次数。如果只是统计一篇文章的词频，几十K到几M的数据，那么写一个程序，将数据读入内存，建一个Hash表记录每个词出现的次数就可以了，如下图。\n\n\n![1cf32673aae43e61a75847b066884057](MapReduce编程模型和计算框架架构原理.resources/9BC09734-6728-4F29-A798-AE7684F1C04E.png)\n\n\n但是如果想统计全世界互联网所有网页（数万亿计）的词频数（这正是google这样的搜索引擎典型需求），你不可能写一个程序把全世界的网页都读入内存，这时候就需要用MapReduce编程来解决。\n\nWordCount的MapReduce程序如下。\n\n```\npublic class WordCount {\n\npublic static class TokenizerMapper\nextends Mapper<Object, Text, Text, IntWritable>{\n\nprivate final static IntWritable one = new IntWritable(1);\nprivate Text word = new Text();\n\npublic void map(Object key, Text value, Context context\n) throws IOException, InterruptedException {\nStringTokenizer itr = new StringTokenizer(value.toString());\nwhile (itr.hasMoreTokens()) {\nword.set(itr.nextToken());\ncontext.write(word, one);\n}\n}\n}\n\npublic static class IntSumReducer\nextends Reducer<Text,IntWritable,Text,IntWritable> {\nprivate IntWritable result = new IntWritable();\n\npublic void reduce(Text key, Iterable<IntWritable> values,\nContext context\n) throws IOException, InterruptedException {\nint sum = 0;\nfor (IntWritable val : values) {\nsum += val.get();\n}\nresult.set(sum);\ncontext.write(key, result);\n}\n}\n}\n```\n其核心是一个map函数，一个reduce函数。\n\nmap函数的输入主要是一个<key , value>对，在这个例子里，value是要统计的所有文本中的一行数据，key在这里不重要，我们忽略。\n```\npublic void map(Object key, Text value, Context context)\n```\nmap函数的计算过程就是，将这行文本中的单词提取出来，针对每个单词输出一个<word , 1>这样的<key , value>对。\n\nMapReduce计算框架会将这些<word , 1>收集起来，将相同的word放在一起，形成<word , <1,1,1,1,1,1,1.....>>这样的<key , value集合>数据，然后将其输入给reduce函数。\n```\npublic void reduce(Text key, Iterable<IntWritable> values,Context context)\n```\n这里的reduce的输入参数values就是由很多个1组成的集合，而key就是具体的单词word。\n\nreduce函数的计算过程就是，将这个集合里的1求和，再将单词（word）和这个和（sum）组成一个<key , value>(<word , sum>)输出。每一个输出就是一个单词和它的词频统计总和。\n\n假设有两个block的文本数据需要进行词频统计，MapReduce计算过程如下图。\n\n![3ecd6347527aa0562dc545c6e8e1997c](MapReduce编程模型和计算框架架构原理.resources/80C38BAC-B7B6-413A-9504-44AF99C5FB27.png)\n\n### MapReduce计算过程\n\n\n一个map函数可以针对一部分数据进行运算，这样就可以将一个大数据切分成很多块（这也正是HDFS所做的），MapReduce计算框架为每个块分配一个map函数去计算，从而实现大数据的分布式计算。\n\n上面提到MapReduce编程模型将大数据计算过程切分为map和reduce两个阶段，在map阶段为每个数据块分配一个map计算任务，然后将所有map输出的key进行合并，相同的key及其对应的value发送给同一个reduce任务去处理。\n\n这个过程有两个关键问题需要处理\n\n* 如何为每个数据块分配一个map计算任务，代码是如何发送数据块所在服务器的，发送过去是如何启动的，启动以后又如何知道自己需要计算的数据在文件什么位置（数据块id是什么）\n\n* 处于不同服务器的map输出的<key , value> ，如何把相同的key聚合在一起发送给reduce任务\n\n* 这两个关键问题正好对应文章中“MapReduce计算过程”一图中两处“MapReduce框架处理”。\n\n\n![fe3a4f4e8603397638a92d67964c8b0e](MapReduce编程模型和计算框架架构原理.resources/0EAF1DCB-D641-4F33-A093-41B6AFE34EE2.png)\n\n\n我们先看下MapReduce是如何启动处理一个大数据计算应用作业的。\n\n#### MapReduce作业启动和运行机制\n\n我们以Hadoop1为例，MapReduce运行过程涉及以下几类关键进程：\n\n* 大数据应用进程：启动用户MapReduce程序的主入口，主要指定Map和Reduce类、输入输出文件路径等，并提交作业给Hadoop集群。\n\n* JobTracker进程：根据要处理的输入数据量启动相应数量的map和reduce进程任务，并管理整个作业生命周期的任务调度和监控。JobTracker进程在整个Hadoop集群全局唯一。\n\n* TaskTracker进程：负责启动和管理map进程以及reduce进程。因为需要每个数据块都有对应的map函数，TaskTracker进程通常和HDFS的DataNode进程启动在同一个服务器，也就是说，Hadoop集群中绝大多数服务器同时运行DataNode进程和TaskTacker进程。\n\n如下图所示。\n\n\n![56113971e19e97cdacc9f4b4f9993b76](MapReduce编程模型和计算框架架构原理.resources/011912DD-397C-4C64-B978-F932E333E232.png)\n\n\n具体作业启动和计算过程如下：\n\n* 应用进程将用户作业jar包存储在HDFS中，将来这些jar包会分发给Hadoop集群中的服务器执行MapReduce计算。\n* 应用程序提交job作业给JobTracker。\n* JobTacker根据作业调度策略创建JobInProcess树，每个作业都会有一个自己的JobInProcess树。\n* JobInProcess根据输入数据分片数目（通常情况就是数据块的数目）和设置的reduce数目创建相应数量的TaskInProcess。\n* TaskTracker进程和JobTracker进程进行定时通信。\n* 如果TaskTracker有空闲的计算资源（空闲CPU核），JobTracker就会给他分配任务。分配任务的时候会根据TaskTracker的服务器名字匹配在同一台机器上的数据块计算任务给它，使启动的计算任务正好处理本机上的数据。\n* TaskRunner收到任务后根据任务类型（map还是reduce），任务参数（作业jar包路径，输入数据文件路径，要处理的数据在文件中的起始位置和偏移量，数据块多个备份的DataNode主机名等）启动相应的map或者reduce进程。\n* map或者reduce程序启动后，检查本地是否有要执行任务的jar包文件，如果没有，就去HDFS上下载，然后加载map或者reduce代码开始执行。\n* 如果是map进程，从HDFS读取数据（通常要读取的数据块正好存储在本机）。如果是reduce进程，将结果数据写出到HDFS。\n\n通过以上过程，MapReduce可以将大数据作业计算任务分布在整个Hadoop集群中运行，每个map计算任务要处理的数据通常都能从本地磁盘上读取到。而用户要做的仅仅是编写一个map函数和一个reduce函数就可以了，根本不用关心这两个函数是如何被分布启动到集群上的，数据块又是如何分配给计算任务的。这一切都由MapReduce计算框架完成。\n\n#### MapReduce数据合并与连接机制\n在WordCount例子中，要统计相同单词在所有输入数据中出现的次数，而一个map只能处理一部分数据，一个热门单词几乎会出现在所有的map中，这些单词必须要合并到一起进行统计才能得到正确的结果。\n\n事实上，几乎所有的大数据计算场景都需要处理数据关联的问题，简单如WordCount只要对key进行合并就可以了，复杂如数据库的join操作，需要对两种类型（或者更多类型）的数据根据key进行连接。\n\nMapReduce计算框架处理数据合并与连接的操作就在map输出与reduce输入之间，这个过程有个专门的词汇来描述，叫做shuffle。\n\n#### MapReduce shuffle过程\n每个map任务的计算结果都会写入到本地文件系统，等map任务快要计算完成的时候，MapReduce计算框架会启动shuffle过程，在map端调用一个Partitioner接口，对map产生的每个<key , value>进行reduce分区选择，然后通过http通信发送给对应的reduce进程。这样不管map位于哪个服务器节点，相同的key一定会被发送给相同的reduce进程。reduce端对收到的<key , value>进行排序和合并，相同的key放在一起，组成一个<key , value集合>传递给reduce执行。\n\nMapReduce框架缺省的Partitioner用key的哈希值对reduce任务数量取模，相同的key一定会落在相同的reduce任务id上，实现上，这样的Partitioner代码只需要一行，如下所示。\n\n```\n/** Use {@link Object#hashCode()} to partition. */ \npublic int getPartition(K2 key, V2 value, int numReduceTasks) { \nreturn (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; \n}\n```\n\nshuffle是大数据计算过程中发生奇迹的地方，不管是MapReduce还是Spark，只要是大数据批处理计算，一定会有shuffle过程，让数据关联起来，数据的内在关系和价值才会呈现出来。不理解shuffle，就会在map和reduce编程中产生困惑，不知道该如何正确设计map的输出和reduce的输入。shuffle也是整个MapReduce过程中最难最消耗性能的地方，在MapReduce早期代码中，一半代码都是关于shuffle处理的。\n\n![e0eb09bb5187517b530feb90ab55f767](MapReduce编程模型和计算框架架构原理.resources/8DB07269-807E-435E-B65D-3A0A16E05D07.png)\n"
  },
  {
    "path": "JVM/HotSpot垃圾收集器.md",
    "content": "# HotSpot垃圾收集器\n\nHotSpot 虚拟机提供了多种垃圾收集器，每种收集器都有各自的特点，虽然我们要对各个收集器进行比较，但并非为了挑选出一个最好的收集器。我们选择的只是对具体应用最合适的收集器。\n\n## 新生代垃圾收集器\n\n### Serial 垃圾收集器（单线程）\n\n只开启**一条** GC 线程进行垃圾回收，并且在垃圾收集过程中停止一切用户线程\\(Stop The World\\)。\n\n一般客户端应用所需内存较小，不会创建太多对象，而且堆内存不大，因此垃圾收集器回收时间短，即使在这段时间停止一切用户线程，也不会感觉明显卡顿。因此 Serial 垃圾收集器**适合客户端**使用。\n\n由于 Serial 收集器只使用一条 GC 线程，避免了线程切换的开销，从而简单高效。 \n![3f94897d49934c0d10fcf641f2ea9398](HotSpot垃圾收集器.resources/9790224E-BF78-4E47-A73D-94EC5845A554.png)\n\n### ParNew 垃圾收集器（多线程）\n\n\nParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。\n\nParNew 追求“**低停顿时间**”,与 Serial 唯一区别就是使用了多线程进行垃圾收集，在多 CPU 环境下性能比 Serial 会有一定程度的提升；但**线程切换需要额外的开销**，因此在单 CPU 环境中表现不如 Serial。\n\n![03e2a048077b12ff86e8e71c51d0e2c8](HotSpot垃圾收集器.resources/B8252EDC-045D-4565-AAF7-64C729EDE03D.png)\n\n\n### Parallel Scavenge 垃圾收集器（多线程）\n\nParallel Scavenge 和 ParNew 一样，都是多线程、新生代垃圾收集器。但是两者有巨大的不同点：\n\n* Parallel Scavenge：追求 CPU 吞吐量，能够在较短时间内完成指定任务，因此适合没有交互的后台计算。\n* ParNew：追求降低用户停顿时间，适合交互式应用。\n\n吞吐量 = 运行用户代码时间 / \\(运行用户代码时间 + 垃圾收集时间\\)\n\n追求高吞吐量，可以通过减少 GC 执行实际工作的时间，然而，仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做，因为在此期间积累在堆中的对象数量很高。单个 GC 需要花更多的时间来完成，从而导致更高的暂停时间。而考虑到低暂停时间，最好频繁运行 GC 以便更快速完成，反过来又导致吞吐量下降。\n\n* 通过参数 -XX:GCTimeRadio 设置垃圾回收时间占总 CPU 时间的百分比。\n* 通过参数 -XX:MaxGCPauseMillis 设置垃圾处理过程最久停顿时间。\n* 通过命令 -XX:+UseAdaptiveSizePolicy 开启自适应策略。我们只要设置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio，收集器会自动调整新生代的大小、Eden 和 Survivor 的比例、对象进入老年代的年龄，以最大程度上接近我们设置的 MaxGCPauseMillis 或 GCTimeRadio。\n\n## 老年代垃圾收集器\n\n### Serial Old 垃圾收集器（单线程）\n\nSerial Old 收集器是 Serial 的老年代版本，都是单线程收集器，只启用一条 GC 线程，都适合客户端应用。它们唯一的区别就是：Serial Old 工作在老年代，使用“标记-整理”算法；Serial 工作在新生代，使用“复制”算法。\n\n### Parallel Old 垃圾收集器（多线程）\n\nParallel Old 收集器是 Parallel Scavenge 的老年代版本，追求 CPU 吞吐量。\n\n### CMS 垃圾收集器\n\nCMS\\(Concurrent Mark Sweep，并发标记清除\\)收集器是以获取最短回收停顿时间为目标的收集器（追求低停顿），它在垃圾收集时使得用户线程和 GC 线程并发执行，因此在垃圾收集过程中用户也不会感到明显的卡顿。\n\n* 初始标记：Stop The World，仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。\n* 并发标记：使用**多条**标记线程，与用户线程并发执行。此过程进行可达性分析，标记出所有废弃对象。速度很慢。\n* 重新标记：Stop The World，使用多条标记线程并发执行，将刚才并发标记过程中新出现的废弃对象标记出来。\n* 并发清除：只使用一条 GC 线程，与用户线程并发执行，清除刚才标记的对象。这个过程非常耗时。\n\n并发标记与并发清除过程耗时最长，且可以与用户线程一起工作，因此，**总体上说**，CMS 收集器的内存回收过程是与用户线程**一起并发执行**的。\n\n![a7a96aa9e9e60973b3a0cc3e92c6e2bb](HotSpot垃圾收集器.resources/1238D175-FF81-4F5A-8899-CCA09DCCD5D9.png)\n\nCMS 的缺点：\n\n* 吞吐量低\n* 无法处理浮动垃圾，导致频繁 Full GC\n* 使用“标记-清除”算法产生碎片空间\n\n对于产生碎片空间的问题，可以通过开启 -XX:+UseCMSCompactAtFullCollection，在每次 Full GC 完成后都会进行一次内存压缩整理，将零散在各处的对象整理到一块。设置参数 -XX:CMSFullGCsBeforeCompaction告诉 CMS，经过了 N 次 Full GC 之后再进行一次内存整理。\n\n## G1 通用垃圾收集器\n\nG1 是一款面向服务端应用的垃圾收集器，它没有新生代和老年代的概念，而是将堆划分为一块块独立的 Region。当要进行垃圾收集时，首先估计每个 Region 中垃圾的数量，每次都从垃圾回收价值最大的 Region 开始回收，因此可以获得最大的回收效率。\n\n从整体上看， G1 是基于“标记-整理”算法实现的收集器，从局部（两个 Region 之间）上看是基于“复制”算法实现的，这意味着运行期间不会产生内存空间碎片。\n\n这里抛个问题👇\n一个对象和它内部所引用的对象可能不在同一个 Region 中，那么当垃圾回收时，是否需要扫描整个堆内存才能完整地进行一次可达性分析？\n\n并不！每个 Region 都有一个 Remembered Set，用于记录本区域中所有对象引用的对象所在的区域，进行可达性分析时，只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。\n\n如果不计算维护 Remembered Set 的操作，G1 收集器的工作过程分为以下几个步骤：\n\n* 初始标记：Stop The World，仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。\n* 并发标记：使用**一条**标记线程与用户线程并发执行。此过程进行可达性分析，速度很慢。\n* 最终标记：Stop The World，使用多条标记线程并发执行。\n* 筛选回收：回收废弃对象，此时也要 Stop The World，并使用多条筛选回收线程并发执行。\n\n（完）\n"
  },
  {
    "path": "JVM/HotSpot虚拟机对象探秘.md",
    "content": "# HotSpot虚拟机对象探秘\n\n## 对象的内存布局\n\n在 HotSpot 虚拟机中，对象的内存布局分为以下 3 块区域：\n\n* 对象头（Header）\n* 实例数据（Instance Data）\n* 对齐填充（Padding）\n\n![c716b7dab73b395ce177f282ac257e9b](HotSpot虚拟机对象探秘.resources/0455EAC5-DBCC-4956-A843-DF363813D9E1.png)\n\n### 对象头\n\n对象头记录了对象在运行过程中所需要使用的一些数据：\n\n\n* 哈希码\n* GC 分代年龄\n* 锁状态标志\n* 线程持有的锁\n* 偏向线程 ID\n* 偏向时间戳\n\n对象头可能包含类型指针，通过该指针能确定对象属于哪个类。如果对象是一个数组，那么对象头还会包括数组长度。\n\n### 实例数据\n\n实例数据部分就是成员变量的值，其中包括父类成员变量和本类成员变量。\n\n### 对齐填充\n\n用于确保对象的总长度为 8 字节的整数倍。\n\nHotSpot VM 的自动内存管理系统要求对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数（1 倍或 2 倍），因此，当对象实例数据部分没有对齐时，就需要通过对齐填充来补全。\n\n> 对齐填充并不是必然存在，也没有特别的含义，它仅仅起着占位符的作用。\n\n## 对象的创建过程\n\n### 类加载检查\n\n虚拟机在解析`.class`文件时，若遇到一条 new 指令，首先它会去检查常量池中是否有这个类的符号引用，并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有，那么必须先执行相应的类加载过程。\n\n### 为新生对象分配内存\n\n对象所需内存的大小在类加载完成后便可完全确定，接下来从堆中划分一块对应大小的内存空间给新的对象。分配堆中内存有两种方式：\n\n- **指针碰撞**<br>\n如果 Java **堆中内存绝对规整**（说明采用的是“**复制算法**”或“**标记整理法**”），空闲内存和已使用内存中间放着一个指针作为分界点指示器，那么分配内存时只需要把指针向空闲内存挪动一段与对象大小一样的距离，这种分配方式称为“**指针碰撞**”。\n\n- **空闲列表**<br>\n如果 Java **堆中内存并不规整**，已使用的内存和空闲内存交错（说明采用的是**标记-清除法**，有碎片），此时没法简单进行指针碰撞， VM 必须维护一个列表，记录其中哪些内存块空闲可用。分配之时从空闲列表中找到一块足够大的内存空间划分给对象实例。这种方式称为“**空闲列表**”。\n\n### 初始化\n\n分配完内存后，为对象中的成员变量赋上初始值，设置对象头信息，调用对象的构造函数方法进行初始化。\n\n至此，整个对象的创建过程就完成了。\n\n## 对象的访问方式\n\n所有对象的存储空间都是在堆中分配的，但是这个对象的引用却是在堆栈中分配的。也就是说在建立一个对象时两个地方都分配内存，在堆中分配的内存实际建立这个对象，而在堆栈中分配的内存只是一个指向这个堆对象的指针（引用）而已。 那么根据引用存放的地址类型的不同，对象有不同的访问方式。\n\n### 句柄访问方式\n\n堆中需要有一块叫做“句柄池”的内存空间，句柄中包含了对象实例数据与类型数据各自的具体地址信息。\n\n引用类型的变量存放的是该对象的句柄地址（reference）。访问对象时，首先需要通过引用类型的变量找到该对象的句柄，然后根据句柄中对象的地址找到对象。\n\n![3955fd1345d45baf4d42746d78ad112c](HotSpot虚拟机对象探秘.resources/FE9CC6BA-3D79-4CBD-8392-ABDB40E678E4.jpg)\n\n### 直接指针访问方式\n\n引用类型的变量直接存放对象的地址，从而不需要句柄池，通过引用能够直接访问对象。但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。\n\n![d84bacb25c024fd9925cc5280722b810](HotSpot虚拟机对象探秘.resources/4CFF1522-9C12-4100-9F5F-6251AB79991F.jpg)\n\n\n需要说明的是，HotSpot 采用第二种方式，即直接指针方式来访问对象，只需要一次寻址操作，所以在性能上比句柄访问方式快一倍。但像上面所说，它需要**额外的策略**来存储对象在方法区中类信息的地址。\n"
  },
  {
    "path": "JVM/JVM 性能调优.md",
    "content": "# JVM 性能调优\n\n在高性能硬件上部署程序，目前主要有两种方式：  \n\n* 通过 64 位 JDK 来使用大内存；\n* 使用若干个 32 位虚拟机建立逻辑集群来利用硬件资源。\n\n## 使用64位JDK管理大内存\n\n堆内存变大后，虽然垃圾收集的频率减少了，但每次垃圾回收的时间变长。 如果堆内存为14 G，那么每次 Full GC 将长达数十秒。如果 Full GC 频繁发生，那么对于一个网站来说是无法忍受的。\n\n对于用户交互性强、对停顿时间敏感的系统，可以给 Java 虚拟机分配超大堆的前提是有把握把应用程序的 Full GC 频率控制得足够低，至少要低到不会影响用户使用。\n\n可能面临的问题：  \n\n\n* 内存回收导致的长时间停顿；\n* 现阶段，64位 JDK 的性能普遍比 32 位 JDK 低；\n* 需要保证程序足够稳定，因为这种应用要是产生堆溢出几乎就无法产生堆转储快照（因为要产生超过 10GB 的 Dump 文件），哪怕产生了快照也几乎无法进行分析；\n* 相同程序在 64 位 JDK 消耗的内存一般比 32 位 JDK 大，这是由于指针膨胀，以及数据类型对齐补白等因素导致的。\n\n## 使用32位JVM建立逻辑集群\n\n在一台物理机器上启动多个应用服务器进程，每个服务器进程分配不同端口， 然后在前端搭建一个负载均衡器，以反向代理的方式来分配访问请求。\n\n考虑到在一台物理机器上建立逻辑集群的目的仅仅是为了尽可能利用硬件资源，并不需要关心状态保留、热转移之类的高可用性能需求， 也不需要保证每个虚拟机进程有绝对的均衡负载，因此使用无 Session 复制的亲合式集群是一个不错的选择。 我们仅仅需要保障集群具备亲合性，也就是均衡器按一定的规则算法（一般根据 SessionID 分配） 将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可。\n\n可能遇到的问题：  \n\n\n* 尽量避免节点竞争全局资源，如磁盘竞争，各个节点如果同时访问某个磁盘文件的话，很可能导致 IO 异常；\n* 很难高效利用资源池，如连接池，一般都是在节点建立自己独立的连接池，这样有可能导致一些节点池满了而另外一些节点仍有较多空余；\n* 各个节点受到 32 位的内存限制；\n* 大量使用本地缓存的应用，在逻辑集群中会造成较大的内存浪费，因为每个逻辑节点都有一份缓存，这时候可以考虑把本地缓存改成集中式缓存。\n\n## 调优案例分析与实战\n\n### 场景描述\n\n一个小型系统，使用 32 位 JDK，4G 内存，测试期间发现服务端不定时抛出内存溢出异常。 加入 -XX:+HeapDumpOnOutOfMemoryError（添加这个参数后，堆内存溢出时就会输出异常日志）， 但再次发生内存溢出时，没有生成相关异常日志。\n\n### 分析\n\n在 32 位 JDK 上，1.6G 分配给堆，还有一部分分配给 JVM 的其他内存，直接内存最大也只能在剩余的 0.4G 空间中分出一部分， 如果使用了 NIO，JVM 会在 JVM 内存之外分配内存空间，那么就要小心“直接内存”不足时发生内存溢出异常了。\n\n### 直接内存的回收过程\n\n直接内存虽然不是 JVM 内存空间，但它的垃圾回收也由 JVM 负责。\n\n垃圾收集进行时，虚拟机虽然会对直接内存进行回收， 但是直接内存却不能像新生代、老年代那样，发现空间不足了就通知收集器进行垃圾回收， 它只能等老年代满了后 Full GC，然后“顺便”帮它清理掉内存的废弃对象。 否则只能一直等到抛出内存溢出异常时，先 catch 掉，再在 catch 块里大喊 “System.gc\\(\\)”。 要是虚拟机还是不听，那就只能眼睁睁看着堆中还有许多空闲内存，自己却不得不抛出内存溢出异常了。\n\n（完）"
  },
  {
    "path": "JVM/JVM内存结构.md",
    "content": "# JVM 内存结构\n\nJava 虚拟机的内存空间分为 5 个部分：\n\n* 程序计数器\n* Java 虚拟机栈\n* 本地方法栈\n* 堆\n* 方法区\n![9aa201f682e51b9c8e5b1afab765f7d9](JVM内存结构.resources/355273B7-9D0C-4CA0-B422-4A869D35AA26.jpg)\nJDK 1.8 同 JDK 1.7 比，最大的差别就是：元数据区取代了永久代。元空间的本质和永久代类似，都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于：元数据空间并不在虚拟机中，而是使用本地内存。\n\n## 程序计数器（PC 寄存器）\n\n### 程序计数器的定义\n\n程序计数器是一块较小的内存空间，是当前线程正在执行的那条字节码指令的地址。若当前线程正在执行的是一个本地方法，那么此时程序计数器为`Undefined`。\n\n\n### 程序计数器的作用\n\n* 字节码解释器通过改变程序计数器来依次读取指令，从而实现代码的流程控制。\n* 在多线程情况下，程序计数器记录的是当前线程执行的位置，从而当线程切换回来时，就知道上次线程执行到哪了。\n\n### 程序计数器的特点\n\n* 是一块较小的内存空间。\n* 线程私有，每条线程都有自己的程序计数器。\n* 生命周期：随着线程的创建而创建，随着线程的结束而销毁。\n* 是唯一一个不会出现`OutOfMemoryError`的内存区域。\n\n## Java 虚拟机栈（Java 栈）\n\n### Java 虚拟机栈的定义\n\nJava 虚拟机栈是描述 Java 方法运行过程的内存模型。\n\nJava 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域，用于存放该方法运行过程中的一些信息，如：\n\n* 局部变量表\n* 操作数栈\n* 动态链接\n* 方法出口信息\n* ......\n\n![875223b19a3ea457678d5a09acb950e0](JVM内存结构.resources/6F3902DB-275A-4FC6-8E3A-754DE6F987BA.jpg)\n\n### 压栈出栈过程\n\n当方法运行过程中需要创建局部变量时，就将局部变量的值存入栈帧中的局部变量表中。\n\nJava 虚拟机栈的栈顶的栈帧是当前正在执行的活动栈，也就是当前正在执行的方法，PC 寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操作数栈使用，当在这个栈帧中调用另一个方法，与之对应的栈帧又会被创建，新创建的栈帧压入栈顶，变为当前的活动栈帧。\n\n方法结束后，当前栈帧被移出，栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。如果没有返回值，那么新的活动栈帧中操作数栈的操作数没有变化。\n\n> 由于Java 虚拟机栈是与线程对应的，数据不是线程共享的，因此不用关心数据一致性问题，也不会存在同步锁的问题。\n\n### Java 虚拟机栈的特点\n\n* 局部变量表随着栈帧的创建而创建，它的大小在编译时确定，创建时只需分配事先规定的大小即可。在方法运行过程中，局部变量表的大小不会发生改变。\n* Java 虚拟机栈会出现两种异常：StackOverFlowError 和 OutOfMemoryError。\n* StackOverFlowError  若 Java 虚拟机栈的大小不允许动态扩展，那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时，抛出 StackOverFlowError 异常。\n* OutOfMemoryError  若允许动态扩展，那么当线程请求栈时内存用完了，无法再动态扩展时，抛出 OutOfMemoryError 异常。\n* Java 虚拟机栈也是线程私有，随着线程创建而创建，随着线程的结束而销毁。\n\n> 出现 StackOverFlowError 时，内存空间可能还有很多。\n\n## 本地方法栈（C 栈）\n\n### 本地方法栈的定义\n\n本地方法栈是为 JVM 运行 Native 方法准备的空间，由于很多 Native 方法都是用 C 语言实现的，所以它通常又叫 C 栈。它与 Java 虚拟机栈实现的功能类似，只不过本地方法栈是描述本地方法运行过程的内存模型。\n\n### 栈帧变化过程\n\n本地方法被执行时，在本地方法栈也会创建一块栈帧，用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。\n\n方法执行结束后，相应的栈帧也会出栈，并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。\n\n> 如果 Java 虚拟机本身不支持 Native 方法，或是本身不依赖于传统栈，那么可以不提供本地方法栈。如果支持本地方法栈，那么这个栈一般会在线程创建的时候按线程分配。\n\n## 堆\n\n### 堆的定义\n\n堆是用来存放对象的内存空间，几乎所有的对象都存储在堆中。\n\n### 堆的特点\n\n* 线程共享，整个 Java 虚拟机只有一个堆，所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个。\n* 在虚拟机启动时创建。\n* 是垃圾回收的主要场所。\n* 进一步可分为：新生代\\(Eden区  From Survior  To Survivor\\)、老年代。\n\n不同的区域存放不同生命周期的对象，这样可以根据不同的区域使用不同的垃圾回收算法，更具有针对性。\n\n堆的大小既可以固定也可以扩展，但对于主流的虚拟机，堆的大小是可扩展的，因此当线程请求分配内存，但堆已满，且内存已无法再扩展时，就抛出 OutOfMemoryError 异常。\n\n> Java 堆所使用的内存不需要保证是连续的。而由于堆是被所有线程共享的，所以对它的访问需要注意同步问题，方法和对应的属性都需要保证一致性。\n\n## 方法区\n\n### 方法区的定义\n\nJava 虚拟机规范中定义方法区是堆的一个逻辑部分。方法区存放以下信息：  \n\n\n* 已经被虚拟机加载的类信息\n* 常量\n* 静态变量\n* 即时编译器编译后的代码\n\n### 方法区的特点\n\n* 线程共享。  方法区是堆的一个逻辑部分，因此和堆一样，都是线程共享的。整个虚拟机中只有一个方法区。\n* 永久代。  方法区中的信息一般需要长期存在，而且它又是堆的逻辑分区，因此用堆的划分方法，把方法区称为“永久代”。\n* 内存回收效率低。  方法区中的信息一般需要长期存在，回收一遍之后可能只有少量信息无效。主要回收目标是：对常量池的回收；对类型的卸载。\n* Java 虚拟机规范对方法区的要求比较宽松。  和堆一样，允许固定大小，也允许动态扩展，还允许不实现垃圾回收。\n\n### 运行时常量池\n\n方法区中存放：类信息、常量、静态变量、即时编译器编译后的代码。常量就存放在运行时常量池中。\n\n当类被 Java 虚拟机加载后， .class 文件中的常量就存放在方法区的运行时常量池中。而且在运行期间，可以向常量池中添加新的常量。如 String 类的 intern\\(\\) 方法就能在运行期间向常量池中添加字符串常量。\n\n## 直接内存（堆外内存）\n\n直接内存是除 Java 虚拟机之外的内存，但也可能被 Java 使用。\n\n### 操作直接内存\n\n在 NIO 中引入了一种基于通道和缓冲的 IO 方式。它可以通过调用本地方法直接分配 Java 虚拟机之外的内存，然后通过一个存储在堆中的`DirectByteBuffer`对象直接操作该内存，而无须先将外部内存中的数据复制到堆中再进行操作，从而提高了数据操作的效率。\n\n直接内存的大小不受 Java 虚拟机控制，但既然是内存，当内存不足时就会抛出 OutOfMemoryError 异常。\n\n### 直接内存与堆内存比较\n\n* 直接内存申请空间耗费更高的性能\n* 直接内存读取 IO 的性能要优于普通的堆内存。\n* 直接内存作用链： 本地 IO -&gt; 直接内存 -&gt; 本地 IO\n* 堆内存作用链：本地 IO -&gt; 直接内存 -&gt; 非直接内存 -&gt; 直接内存 -&gt; 本地 IO\n\n> 服务器管理员在配置虚拟机参数时，会根据实际内存设置`-Xmx`等参数信息，但经常忽略直接内存，使得各个内存区域总和大于物理内存限制，从而导致动态扩展时出现`OutOfMemoryError`异常。\n"
  },
  {
    "path": "JVM/jvm系列(一)java类的加载机制.md",
    "content": "## 什么是类的加载\n类的加载指的是将类的.class文件中的二进制数据读入到内存中，将其放在运行时数据区的方法区内，然后在堆区创建一个 java.lang.Class对象，用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象， Class对象封装了类在方法区内的数据结构，并且向Java程序员提供了访问方法区内的数据结构的接口。\n\n![bc9171c9be2249422e1967bd71116e68](jvm系列(一)java类的加载机制.resources/39F977A1-EAA5-4870-9752-0DAD0C9B2955.jpg)\n\n类加载器并不需要等到某个类被“首次主动使用”时再加载它，JVM规范允许类加载器在预料某个类将要被使用时就预先加载它，如果在预先加载的过程中遇到了.class文件缺失或存在错误，类加载器必须在程序首次主动使用该类时才报告错误（LinkageError错误）如果这个类一直没有被程序主动使用，那么类加载器就不会报告错误\n\n* 加载.class文件的方式\n* 从本地系统中直接加载通过网络下载.class文件\n* 从zip，jar等归档文件中加载.class文件\n* 从专有数据库中提取.class文件将Java源文件动态编译为.class文件\n\n\n## 类的生命周期\n![04d9839801915f9aad3a9d8f9abee1e4](jvm系列(一)java类的加载机制.resources/B33959C2-DD9E-4FA5-8BDE-0CFC04DDDB11.jpg)\n其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中，加载、验证、准备和初始化这四个阶段发生的顺序是确定的，而解析阶段则不一定，它在某些情况下可以在初始化阶段之后开始，这是为了支持Java语言的运行时绑定（也成为动态绑定或晚期绑定）。另外注意这里的几个阶段是按顺序开始，而不是按顺序进行或完成，因为这些阶段通常都是互相交叉地混合进行的，通常在一个阶段执行的过程中调用或激活另一个阶段。\n#### 加载\n查找并加载类的二进制数据加载时类加载过程的第一个阶段，在加载阶段，虚拟机需要完成以下三件事情：\n\n* 通过一个类的全限定名来获取其定义的二进制字节流。\n* 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。\n* 在Java堆中生成一个代表这个类的 java.lang.Class对象，作为对方法区中这些数据的访问入口。\n\n\n相对于类加载的其他阶段而言，加载阶段（准确地说，是加载阶段获取类的二进制字节流的动作）是可控性最强的阶段，因为开发人员既可以使用系统提供的类加载器来完成加载，也可以自定义自己的类加载器来完成加载。\n加载阶段完成后，虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中，而且在Java堆中也创建一个 java.lang.Class类的对象，这样便可以通过该对象访问方法区中的这些数据。\n#### 连接\n验证：确保被加载的类的正确性\n验证是连接阶段的第一步，这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作：\n\n* 文件格式验证：验证字节流是否符合Class文件格式的规范；例如：是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。\n* 元数据验证：对字节码描述的信息进行语义分析（注意：对比javac编译阶段的语义分析），以保证其描述的信息符合Java语言规范的要求；例如：这个类是否有父类，除了 java.lang.Object之外。\n* 字节码验证：通过数据流和控制流分析，确定程序语义是合法的、符合逻辑的。\n* 符号引用验证：确保解析动作能正确执行。验证阶段是非常重要的，但不是必须的，它对程序运行期没有影响，如果所引用的类经过反复验证，那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施，以缩短虚拟机类加载的时间。\n\n准备：为类的 静态变量分配内存，并将其初始化为默认值\n准备阶段是正式为类变量分配内存并设置类变量初始值的阶段，这些内存都将在方法区中分配。对于该阶段有以下几点需要注意：\n\n* 1、这时候进行内存分配的仅包括类变量（static），而不包括实例变量，实例变量会在对象实例化时随着对象一块分配在Java堆中。\n* 2、这里所设置的初始值通常情况下是数据类型默认的零值（如0、0L、null、false等），而不是被在Java代码中被显式地赋予的值。假设一个类变量的定义为： publicstaticintvalue=3；\n\n那么变量value在准备阶段过后的初始值为0，而不是3，因为这时候尚未开始执行任何Java方法，而把value赋值为3的 publicstatic指令是在程序编译后，存放于类构造器 &lt;clinit&gt;（）方法之中的，所以把value赋值为3的动作将在初始化阶段才会执行。\n这里还需要注意如下几点：\n\n>对基本数据类型来说，对于类变量（static）和全局变量，如果不显式地对其赋值而直接使用，则系统会为其赋予默认的零值，而对于局部变量来说，在使用前必须显式地为其赋值，否则编译时不通过。对于同时被static和final修饰的常量，必须在声明的时候就为其显式地赋值，否则编译时不通过；而只被final修饰的常量则既可以在声明时显式地为其赋值，也可以在类初始化时显式地为其赋值，总之，在使用前必须为其显式地赋值，系统不会为其赋予默认零值。对于引用数据类型reference来说，如数组引用、对象引用等，如果没有对其进行显式地赋值而直接使用，系统都会为其赋予默认的零值，即null。如果在数组初始化时没有对数组中的各元素赋值，那么其中的元素将根据对应的数据类型而被赋予默认的零值。\n\n3、如果类字段的字段属性表中存在 ConstantValue属性，即同时被final和static修饰，那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。\n假设上面的类变量value被定义为： publicstaticfinalintvalue=3；\n\n编译时Javac将会为value生成ConstantValue属性，在准备阶段虚拟机就会根据 ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中\n\n#### 解析：把类中的符号引用转换为直接引用\n解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程，解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标，可以是任何字面量。\n直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。\n\n#### 初始化\n初始化，为类的静态变量赋予正确的初始值，JVM负责对类进行初始化，主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式：\n\n* ①声明类变量是指定初始值\n* ②使用静态代码块为类变量指定初始值\n\nJVM初始化步骤\n\n* 1、假如这个类还没有被加载和连接，则程序先加载并连接该类\n* 2、假如该类的直接父类还没有被初始化，则先初始化其直接父类\n* 3、假如类中有初始化语句，则系统依次执行这些初始化语句\n\n类初始化时机：只有当对类的主动使用的时候才会导致类的初始化，类的主动使用包括以下六种：\n\n* 创建类的实例，也就是new的方式\n* 访问某个类或接口的静态变量，或者对该静态变量赋值\n* 调用类的静态方法\n* 反射（如 Class.forName(“com.shengsiyuan.Test”)）\n* 初始化某个类的子类，则其父类也会被初始化\n* Java虚拟机启动时被标明为启动类的类（ JavaTest），直接使用 java.exe命令来运行某个主类\n\n#### 结束生命周期\n\n* 执行了 System.exit()方法\n* 程序正常执行结束\n* 程序在执行过程中遇到了异常或错误而异常终止\n* 由于操作系统出现错误而导致Java虚拟机进程终止\n\n\n## 3、类加载器\n寻找类加载器，先来一个小例子\n\n```\npackage com.neo.classloader;\npublic class ClassLoaderTest {\n     public static void main(String[] args) {\n        ClassLoader loader = Thread.currentThread().getContextClassLoader();\n        System.out.println(loader);\n        System.out.println(loader.getParent());\n        System.out.println(loader.getParent().getParent());\n    }\n}\n```\n运行后，输出结果：\n```\nsun.misc.Launcher$AppClassLoader@64fef26a\nsun.misc.Launcher$ExtClassLoader@1ddd40f3\nnull\n```\n从上面的结果可以看出，并没有获取到ExtClassLoader的父Loader，原因是Bootstrap Loader（引导类加载器）是用C语言实现的，找不到一个确定的返回父Loader的方式，于是就返回null。\n\n这几种类加载器的层次关系如下图所示：\n![485ac9f5734023befd01d86ce457a16e](jvm系列(一)java类的加载机制.resources/660D04B1-E9B5-4609-8CD6-6674BC130BA9.jpg)\n\n注意：这里父类加载器并不是通过继承关系来实现的，而是采用组合实现的。\n\n站在Java虚拟机的角度来讲，只存在两种不同的类加载器：启动类加载器：它使用C++实现（这里仅限于Hotspot，也就是JDK1.5之后默认的虚拟机，有很多其他的虚拟机是用Java语言实现的），是虚拟机自身的一部分；所有其他的类加载器：这些类加载器都由Java语言实现，独立于虚拟机之外，并且全部继承自抽象类java.lang.ClassLoader，这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。\n\n站在Java开发人员的角度来看，类加载器可以大致划分为以下三类：\n\n**启动类加载器：** Bootstrap ClassLoader，负责加载存放在JDK\\jre\\lib(JDK代表JDK的安装目录，下同)下，或被-Xbootclasspath参数指定的路径中的，并且能被虚拟机识别的类库（如rt.jar，所有的java.*开头的类均被Bootstrap ClassLoader加载）。启动类加载器是无法被Java程序直接引用的。\n\n**扩展类加载器：** Extension ClassLoader，该加载器由sun.misc.Launcher$ExtClassLoader实现，它负责加载DK\\jre\\lib\\ext目录中，或者由java.ext.dirs系统变量指定的路径中的所有类库（如javax.*开头的类），开发者可以直接使用扩展类加载器。\n\n**应用程序类加载器：** Application ClassLoader，该类加载器由sun.misc.Launcher$AppClassLoader来实现，它负责加载用户类路径（ClassPath）所指定的类，开发者可以直接使用该类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。\n\n\n应用程序都是由这三种类加载器互相配合进行加载的，如果有必要，我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件，因此如果编写了自己的ClassLoader，便可以做到如下几点：\n\n1）在执行非置信代码之前，自动验证数字签名。\n\n2）动态地创建符合用户特定需要的定制化构建类。\n\n3）从特定的场所取得java class，例如数据库中和网络中。\n\n**JVM类加载机制**\n\n•全盘负责，当一个类加载器负责加载某个Class时，该Class所依赖的和引用的其他Class也将由该类加载器负责载入，除非显示使用另外一个类加载器来载入\n\n•父类委托，先让父类加载器试图加载该类，只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类\n\n•缓存机制，缓存机制将会保证所有加载过的Class都会被缓存，当程序中需要使用某个Class时，类加载器先从缓存区寻找该Class，只有缓存区不存在，系统才会读取该类对应的二进制数据，并将其转换成Class对象，存入缓存区。这就是为什么修改了Class后，必须重启JVM，程序的修改才会生效\n\n## 4、类的加载\n\n类加载有三种方式：\n\n1、命令行启动应用时候由JVM初始化加载\n\n2、通过Class.forName()方法动态加载\n\n3、通过ClassLoader.loadClass()方法动态加载\n\n例子：\n```\npackage com.neo.classloader;\npublic class loaderTest { \n        public static void main(String[] args) throws ClassNotFoundException { \n                ClassLoader loader = HelloWorld.class.getClassLoader(); \n                System.out.println(loader); \n                //使用ClassLoader.loadClass()来加载类，不会执行初始化块 \n                loader.loadClass(\"Test2\"); \n                //使用Class.forName()来加载类，默认会执行初始化块 \n//                Class.forName(\"Test2\"); \n                //使用Class.forName()来加载类，并指定ClassLoader，初始化时不执行静态块 \n//                Class.forName(\"Test2\", false, loader); \n        } \n}\n```\ndemo 类\n```\npublic class Test2 { \n        static { \n                System.out.println(\"静态初始化块执行了！\"); \n        } \n}\n```\n分别切换加载方式，会有不同的输出结果。\n\nClass.forName()和ClassLoader.loadClass()区别\nClass.forName()：将类的.class文件加载到jvm中之外，还会对类进行解释，执行类中的static块；\nClassLoader.loadClass()：只干一件事情，就是将.class文件加载到jvm中，不会执行static中的内容,只有在newInstance才会去执行static块。\n注：\nClass.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数，创建类的对象。\n\n## 5、双亲委派模型\n\n双亲委派模型的工作流程是：如果一个类加载器收到了类加载的请求，它首先不会自己去尝试加载这个类，而是把请求委托给父加载器去完成，依次向上，因此，所有的类加载请求最终都应该被传递到顶层的启动类加载器中，只有当父加载器在它的搜索范围中没有找到所需的类时，即无法完成该加载，子加载器才会尝试自己去加载该类。\n\n双亲委派机制:\n1、当AppClassLoader加载一个class时，它首先不会自己去尝试加载这个类，而是把类加载请求委派给父类加载器ExtClassLoader去完成。\n\n2、当ExtClassLoader加载一个class时，它首先也不会自己去尝试加载这个类，而是把类加载请求委派给BootStrapClassLoader去完成。\n\n3、如果BootStrapClassLoader加载失败（例如在$JAVA_HOME/jre/lib里未查找到该class），会使用ExtClassLoader来尝试加载；\n\n4、若ExtClassLoader也加载失败，则会使用AppClassLoader来加载，如果AppClassLoader也加载失败，则会报出异常ClassNotFoundException。\n\n```\npublic Class<?> loadClass(String name)throws ClassNotFoundException {\n            return loadClass(name, false);\n    }\n    \n    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {\n            // 首先判断该类型是否已经被加载\n            Class c = findLoadedClass(name);\n            if (c == null) {\n                //如果没有被加载，就委托给父类加载或者委派给启动类加载器加载\n                try {\n                    if (parent != null) {\n                         //如果存在父类加载器，就委派给父类加载器加载\n                        c = parent.loadClass(name, false);\n                    } else {\n                    //如果不存在父类加载器，就检查是否是由启动类加载器加载的类，通过调用本地方法native Class findBootstrapClass(String name)\n                        c = findBootstrapClass0(name);\n                    }\n                } catch (ClassNotFoundException e) {\n                 // 如果父类加载器和启动类加载器都不能完成加载任务，才调用自身的加载功能\n                    c = findClass(name);\n                }\n            }\n            if (resolve) {\n                resolveClass(c);\n            }\n            return c;\n        }\n```\n\n双亲委派模型意义：\n\n-系统类防止内存中出现多份同样的字节码\n\n-保证Java程序安全稳定运行\n\n## 6、自定义类加载器\n\n通常情况下，我们都是直接使用系统类加载器。但是，有的时候，我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码，为保证安全性，这些字节码经过了加密处理，这时系统类加载器就无法对其进行加载，这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类，从上面对 loadClass 方法来分析来看，我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程：\n\n```\npackage com.neo.classloader;\n\nimport java.io.*;\n\n\npublic class MyClassLoader extends ClassLoader {\n\n    private String root;\n\n    protected Class<?> findClass(String name) throws ClassNotFoundException {\n        byte[] classData = loadClassData(name);\n        if (classData == null) {\n            throw new ClassNotFoundException();\n        } else {\n            return defineClass(name, classData, 0, classData.length);\n        }\n    }\n\n    private byte[] loadClassData(String className) {\n        String fileName = root + File.separatorChar\n                + className.replace('.', File.separatorChar) + \".class\";\n        try {\n            InputStream ins = new FileInputStream(fileName);\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            int bufferSize = 1024;\n            byte[] buffer = new byte[bufferSize];\n            int length = 0;\n            while ((length = ins.read(buffer)) != -1) {\n                baos.write(buffer, 0, length);\n            }\n            return baos.toByteArray();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public String getRoot() {\n        return root;\n    }\n\n    public void setRoot(String root) {\n        this.root = root;\n    }\n\n    public static void main(String[] args)  {\n\n        MyClassLoader classLoader = new MyClassLoader();\n        classLoader.setRoot(\"E:\\\\temp\");\n\n        Class<?> testClass = null;\n        try {\n            testClass = classLoader.loadClass(\"com.neo.classloader.Test2\");\n            Object object = testClass.newInstance();\n            System.out.println(object.getClass().getClassLoader());\n        } catch (ClassNotFoundException e) {\n            e.printStackTrace();\n        } catch (InstantiationException e) {\n            e.printStackTrace();\n        } catch (IllegalAccessException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n自定义类加载器的核心在于对字节码文件的获取，如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示，我并未对class文件进行加密，因此没有解密的过程。这里有几点需要注意：\n\n1、这里传递的文件名需要是类的全限定性名称，即com.paddx.test.classloading.Test格式的，因为 defineClass 方法是按这种格式进行处理的。\n\n2、最好不要重写loadClass方法，因为这样容易破坏双亲委托模式。\n\n3、这类Test 类本身可以被 AppClassLoader 类加载，因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则，由于双亲委托机制的存在，会直接导致该类由 AppClassLoader 加载，而不会通过我们自定义类加载器来加载。\n"
  },
  {
    "path": "JVM/jvm系列(三)GC算法 垃圾收集器.md",
    "content": "## 概述\n垃圾收集 Garbage Collection 通常被称为“GC”，它诞生于1960年 MIT 的 Lisp 语言，经过半个多世纪，目前已经十分成熟了。\n\njvm 中，程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭，栈帧随着方法的进入和退出做入栈和出栈操作，实现了自动的内存清理，因此，我们的内存垃圾回收主要集中于 java 堆和方法区中，在程序运行期间，这部分内存的分配和使用都是动态的.\n\n## 对象存活判断\n判断对象是否存活一般有两种方式：\n\n引用计数：每个对象有一个引用计数属性，新增一个引用时计数加1，引用释放时计数减1，计数为0时可以回收。此方法简单，无法解决对象相互循环引用的问题。\n\n可达性分析（Reachability Analysis）：从GC Roots开始向下搜索，搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时，则证明此对象是不可用的。不可达对象。\n\n>在Java语言中，GC Roots包括：\n  虚拟机栈中引用的对象。\n  方法区中类静态属性实体引用的对象。\n  方法区中常量引用的对象。\n  本地方法栈中JNI引用的对象。\n\n\n## 垃圾收集算法\n\n### 标记-清除算法\n “标记-清除”（Mark-Sweep）算法，如它的名字一样，算法分为“标记”和“清除”两个阶段：首先标记出所有需要回收的对象，在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法，是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。\n\n它的主要缺点有两个：一个是效率问题，标记和清除过程的效率都不高；另外一个是空间问题，标记清除之后会产生大量不连续的内存碎片，空间碎片太多可能会导致，当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。\n\n![8b1a4c0777e1e037ff6874f9fbcc56ec](jvm系列(三)GC算法 垃圾收集器.resources/1082B388-1A45-4177-98E2-30394876769B.png)\n\n### 复制算法\n“复制”（Copying）的收集算法，它将可用内存按容量划分为大小相等的两块，每次只使用其中的一块。当这一块的内存用完了，就将还存活着的对象复制到另外一块上面，然后再把已使用过的内存空间一次清理掉。\n\n这样使得每次都是对其中的一块进行内存回收，内存分配时也就不用考虑内存碎片等复杂情况，只要移动堆顶指针，按顺序分配内存即可，实现简单，运行高效。只是这种算法的代价是将内存缩小为原来的一半，持续复制长生存期的对象则导致效率降低。\n![b060ac47eba636510cf599a87cfb9d2d](jvm系列(三)GC算法 垃圾收集器.resources/02057726-396E-47F9-9339-2DEF5B338ED2.png)\n\n### 标记-压缩算法\n\n复制收集算法在对象存活率较高时就要执行较多的复制操作，效率将会变低。更关键的是，如果不想浪费50%的空间，就需要有额外的空间进行分配担保，以应对被使用的内存中所有对象都100%存活的极端情况，所以在老年代一般不能直接选用这种算法。\n\n根据老年代的特点，有人提出了另外一种“标记-整理”（Mark-Compact）算法，标记过程仍然与“标记-清除”算法一样，但后续步骤不是直接对可回收对象进行清理，而是让所有存活的对象都向一端移动，然后直接清理掉端边界以外的内存\n\n![ae82f1a4dfad773ab0c8adf15104545c](jvm系列(三)GC算法 垃圾收集器.resources/7F6F3B11-DDA0-4E80-8162-442B66A71632.png)\n\n### 分代收集算法\nGC分代的基本假设：绝大部分对象的生命周期都非常短暂，存活时间短。\n\n“分代收集”（Generational Collection）算法，把Java堆分为新生代和老年代，这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中，每次垃圾收集时都发现有大批对象死去，只有少量存活，那就选用复制算法，只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保，就必须使用“标记-清理”或“标记-整理”算法来进行回收。\n\n## 垃圾收集器\n\n如果说收集算法是内存回收的方法论，垃圾收集器就是内存回收的具体实现\n\n### Serial收集器\n串行收集器是最古老，最稳定以及效率高的收集器，可能会产生较长的停顿，只使用一个线程去回收。新生代、老年代使用串行回收；新生代复制算法、老年代标记-压缩；垃圾收集的过程中会Stop The World（服务暂停）\n\n参数控制：-XX:+UseSerialGC  串行收集器\n\n![3d1073b4d51269932e5dcd6821defef3](jvm系列(三)GC算法 垃圾收集器.resources/BC391A2A-4905-4708-9052-2422C0E9D692.png)\n\n### ParNew收集器\nParNew收集器其实就是Serial收集器的多线程版本。新生代并行，老年代串行；新生代复制算法、老年代标记-压缩\n\n参数控制：-XX:+UseParNewGC  ParNew收集器\n-XX:ParallelGCThreads 限制线程数量\n\n![97ff89bcbbc18cd50ba5287850435f43](jvm系列(三)GC算法 垃圾收集器.resources/B6AC79A9-21A8-48B4-B29E-CF755E0970FC.png)\n\n### Parallel收集器\nParallel Scavenge收集器类似ParNew收集器，Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略，虚拟机会根据当前系统的运行情况收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或最大的吞吐量；也可以通过参数控制GC的时间不大于多少毫秒或者比例；新生代复制算法、老年代标记-压缩\n\n参数控制：-XX:+UseParallelGC  使用Parallel收集器+ 老年代串行\n\n\n### Parallel Old 收集器\nParallel Old是Parallel Scavenge收集器的老年代版本，使用多线程和“标记－整理”算法。这个收集器是在JDK 1.6中才开始提供\n\n参数控制： -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行\n\n### CMS收集器\nCMS（Concurrent Mark Sweep）收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上，这类应用尤其重视服务的响应速度，希望系统停顿时间最短，以给用户带来较好的体验。\n\n从名字（包含“Mark Sweep”）上就可以看出CMS收集器是基于“标记-清除”算法实现的，它的运作过程相对于前面几种收集器来说要更复杂一些，整个过程分为4个步骤，包括： \n\n初始标记（CMS initial mark）\n\n并发标记（CMS concurrent mark）\n\n重新标记（CMS remark）\n\n并发清除（CMS concurrent sweep）\n\n 其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象，速度很快，并发标记阶段就是进行GC Roots Tracing的过程，而重新标记阶段则是为了修正并发标记期间，因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录，这个阶段的停顿时间一般会比初始标记阶段稍长一些，但远比并发标记的时间短。 \n      由于整个过程中耗时最长的并发标记和并发清除过程中，收集器线程都可以与用户线程一起工作，所以总体上来说，CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器（新生代使用ParNew）\n\n  优点:并发收集、低停顿 \n\n   缺点：产生大量空间碎片、并发阶段会降低吞吐量\n\n   参数控制：-XX:+UseConcMarkSweepGC  使用CMS收集器\n          -XX:+ UseCMSCompactAtFullCollection Full GC后，进行一次碎片整理；整理过程是独占的，会引起停顿时间变长\n          -XX:+CMSFullGCsBeforeCompaction  设置进行几次Full GC后，进行一次碎片整理\n          -XX:ParallelCMSThreads  设定CMS的线程数量（一般情况约等于可用CPU数量）\n          \n![4f827822133a7ae26d463f1a686fcdc7](jvm系列(三)GC算法 垃圾收集器.resources/4F09EDEB-C98A-43BB-BF13-BAE28C01B961.png)\n\n### G1收集器\n\nG1是目前技术发展的最前沿成果之一，HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点：\n\n1. 空间整合，G1收集器采用标记整理算法，不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。\n\n2. 可预测停顿，这是G1的另一大优势，降低停顿时间是G1和CMS的共同关注点，但G1除了追求低停顿外，还能建立可预测的停顿时间模型，能让使用者明确指定在一个长度为N毫秒的时间片段内，消耗在垃圾收集上的时间不得超过N毫秒，这几乎已经是实时Java（RTSJ）的垃圾收集器的特征了。\n\n上面提到的垃圾收集器，收集的范围都是整个新生代或者老年代，而G1不再是这样。使用G1收集器时，Java堆的内存布局与其他收集器有很大差别，它将整个Java堆划分为多个大小相等的独立区域（Region），虽然还保留有新生代和老年代的概念，但新生代和老年代不再是物理隔阂了，它们都是一部分（可以不连续）Region的集合。\n\n![b09eae8555ae04dd04e2a19e05d9e830](jvm系列(三)GC算法 垃圾收集器.resources/C3FF8C8A-97F5-4DC1-8E90-54FFAACD3D25.jpg)\n\nG1的新生代收集跟ParNew类似，当新生代占用达到一定比例的时候，开始出发收集。和CMS类似，G1收集器收集老年代对象会有短暂停顿。\n\n收集步骤：\n\n1、标记阶段，首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event)，并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)\n\n2、Root Region Scanning，程序运行过程中会回收survivor区(存活到老年代)，这一过程必须在young GC之前完成。\n\n3、Concurrent Marking，在整个堆中进行并发标记(和应用程序并发执行)，此过程可能被young GC中断。在并发标记阶段，若发现区域对象中的所有对象都是垃圾，那个这个区域会被立即回收(图中打X)。同时，并发标记过程中，会计算每个区域的对象活性(区域中存活对象的比例)。\n\n![5757a98181cd217c55ea922727f3868e](jvm系列(三)GC算法 垃圾收集器.resources/E8512714-576B-47AD-928D-72543F14014C.png)\n\n4、Remark, 再标记，会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行)；G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。\n\n5、Copy/Clean up，多线程清除失活对象，会有STW。G1将回收区域的存活对象拷贝到新区域，清除Remember Sets，并发清空回收区域并把它返回到空闲区域链表中。\n\n![d6d277cbe1a4c3ce5e02c91ed00e0cb5](jvm系列(三)GC算法 垃圾收集器.resources/64EEE171-56CA-4292-B8C8-74512B629639.png)\n\n6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。\n\n![9c57c95634a548e8d22bb01f4ae1955d](jvm系列(三)GC算法 垃圾收集器.resources/A426A5C0-99CE-47B1-933C-EE2E0F34D408.png)\n\n常用的收集器组合\n\n![bd16928f321b5275f06a54366d85a128](jvm系列(三)GC算法 垃圾收集器.resources/7B29E97B-2070-485D-AACD-B47E5D8B6B65.png)\n"
  },
  {
    "path": "JVM/jvm系列(二)JVM内存结构.md",
    "content": "所有的Java开发人员可能会遇到这样的困惑？我该为堆内存设置多大空间呢？OutOfMemoryError的异常到底涉及到运行时数据的哪块区域？该怎么解决呢？其实如果你经常解决服务器性能问题，那么这些问题就会变的非常常见，了解JVM内存也是为了服务器出现性能问题的时候可以快速的了解那块的内存区域出现问题，以便于快速的解决生产故障。\n先看一张图，这张图能很清晰的说明JVM内存结构布局。\n\nJava的内存结构：\n![a87faa872e41f9de5330cb1c8c413927](jvm系列(二)JVM内存结构.resources/476AD977-8808-4422-8349-06754DB023F5.png)\nJVM内存结构主要有三大块：堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成，而年轻代内存又被分成三部分，Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配；\n\n方法区存储类信息、常量、静态变量等数据，是线程共享的区域，为与Java堆区分，方法区还有一个别名Non-Heap(非堆)；栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。\n\n在通过一张图来了解如何通过参数来控制各区域的内存大小\n![78f8eb4e67f1bf3d93cfd257be090683](jvm系列(二)JVM内存结构.resources/150E44D4-976A-4736-98B8-6E8681BA307B.png)\n\n\n控制参数\n-Xms设置堆的最小空间大小。\n\n-Xmx设置堆的最大空间大小。\n\n-XX:NewSize设置新生代最小空间大小。\n\n-XX:MaxNewSize设置新生代最大空间大小。\n\n-XX:PermSize设置永久代最小空间大小。\n\n-XX:MaxPermSize设置永久代最大空间大小。\n\n-Xss设置每个线程的堆栈大小。\n\n没有直接设置老年代的参数，但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。\n  **老年代空间大小=堆空间大小-年轻代大空间大小**\n\n从更高的一个维度再次来看JVM和系统调用之间的关系\n\n![10ffdb75c5cecded53db7a22b5b6e6cc](jvm系列(二)JVM内存结构.resources/CD206F3C-4EF8-47AC-9DB5-8B67574E8ADB.png)\n方法区和对是所有线程共享的内存区域；而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。\n\n 下面我们详细介绍每个区域的作用\n\n**Java堆（Heap）**\n    对于大多数应用来说，Java堆（Java Heap）是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域，在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例，几乎所有的对象实例都在这里分配内存。\n\n     Java堆是垃圾收集器管理的主要区域，因此很多时候也被称做“GC堆”。如果从内存回收的角度看，由于现在收集器基本都是采用的分代收集算法，所以Java堆中还可以细分为：新生代和老年代；再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。\n\n根据Java虚拟机规范的规定，Java堆可以处于物理上不连续的内存空间中，只要逻辑上是连续的即可，就像我们的磁盘空间一样。在实现时，既可以实现成固定大小的，也可以是可扩展的，不过当前主流的虚拟机都是按照可扩展来实现的（通过-Xmx和-Xms控制）。\n\n如果在堆中没有内存完成实例分配，并且堆也无法再扩展时，将会抛出OutOfMemoryError异常。\n\n**方法区（Method Area）**\n  方法区（Method Area）与Java堆一样，是各个线程共享的内存区域，它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分，但是它却有一个别名叫做Non-Heap（非堆），目的应该是与Java堆区分开来。\n\n对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说，很多人愿意把方法区称为“永久代”（Permanent Generation），本质上两者并不等价，仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区，或者说使用永久代来实现方法区而已。\n\nJava虚拟机规范对这个区域的限制非常宽松，除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外，还可以选择不实现垃圾收集。相对而言，垃圾收集行为在这个区域是比较少出现的，但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载，一般来说这个区域的回收“成绩”比较难以令人满意，尤其是类型的卸载，条件相当苛刻，但是这部分区域的回收确实是有必要的。\n\n根据Java虚拟机规范的规定，当方法区无法满足内存分配需求时，将抛出OutOfMemoryError异常。 \n\n**程序计数器（Program Counter Register）**\n程序计数器（Program Counter Register）是一块较小的内存空间，它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里（仅是概念模型，各种虚拟机可能会通过一些更高效的方式去实现），字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令，分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 \n由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的，在任何一个确定的时刻，一个处理器（对于多核处理器来说是一个内核）只会执行一条线程中的指令。因此，为了线程切换后能恢复到正确的执行位置，每条线程都需要有一个独立的程序计数器，各条线程之间的计数器互不影响，独立存储，我们称这类内存区域为“线程私有”的内存。 \n      如果线程正在执行的是一个Java方法，这个计数器记录的是正在执行的虚拟机字节码指令的地址；如果正在执行的是Natvie方法，这个计数器值则为空（Undefined）。\n\n此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。\n\n**JVM栈（JVM Stacks）**\n与程序计数器一样，Java虚拟机栈（Java Virtual Machine Stacks）也是线程私有的，它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型：每个方法被执行的时候都会同时创建一个栈帧（Stack Frame）用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程，就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 \n\n局部变量表存放了编译期可知的各种基本数据类型（boolean、byte、char、short、int、float、long、double）、对象引用（reference类型，它不等同于对象本身，根据不同的虚拟机实现，它可能是一个指向对象起始地址的引用指针，也可能指向一个代表对象的句柄或者其他与此对象相关的位置）和returnAddress类型（指向了一条字节码指令的地址）。\n\n其中64位长度的long和double类型的数据会占用2个局部变量空间（Slot），其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配，当进入一个方法时，这个方法需要在帧中分配多大的局部变量空间是完全确定的，在方法运行期间不会改变局部变量表的大小。\n\n在Java虚拟机规范中，对这个区域规定了两种异常状况：如果线程请求的栈深度大于虚拟机所允许的深度，将抛出StackOverflowError异常；如果虚拟机栈可以动态扩展（当前大部分的Java虚拟机都可动态扩展，只不过Java虚拟机规范中也允许固定长度的虚拟机栈），当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。\n\n**本地方法栈（Native Method Stacks）**\n本地方法栈（Native Method Stacks）与虚拟机栈所发挥的作用是非常相似的，其区别不过是虚拟机栈为虚拟机执行Java方法（也就是字节码）服务，而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定，因此具体的虚拟机可以自由实现它。甚至有的虚拟机（譬如Sun HotSpot虚拟机）直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样，本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。\n\n\n\n**参考：**\nhttp://ifeve.com/under-the-hood-runtime-data-areas-javas-memory-model/\n\n《深入理解Java虚拟机：JVM高级特性与最佳实践_周志明.高清扫描版.pdf》\n\n下载地址：http://download.csdn.net/detail/ityouknow/9557109"
  },
  {
    "path": "JVM/jvm系列(五)Java GC 分析.md",
    "content": "Java GC就是JVM记录仪，书画了JVM各个分区的表演。\n\n## 什么是 Java GC\n\nJava GC（Garbage Collection，垃圾收集，垃圾回收）机制，是Java与C++/C的主要区别之一，作为Java开发者，一般不需要专门编写内存回收和垃圾清理代码，对内存泄露和溢出的问题，也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中，存在自动内存管理和垃圾清扫机制。概括地说，该机制对JVM（Java Virtual Machine）中的内存进行标记，并确定哪些内存需要回收，根据一定的回收策略，自动的回收内存，永不停息（Nerver Stop）的保证JVM中的内存空间，防止出现内存泄露和溢出问题。\n\n在Java语言出现之前，就有GC机制的存在，如Lisp语言），Java GC机制已经日臻完善，几乎可以自动的为我们做绝大多数的事情。然而，如果我们从事较大型的应用软件开发，曾经出现过内存优化的需求，就必定要研究Java GC机制。\n\n简单总结一下，Java GC就是通过GC收集器回收不在存活的对象，保证JVM更加高效的运转。\n\n## 如何获取 Java GC日志\n\n一般情况可以通过两种方式来获取GC日志，一种是使用命令动态查看，一种是在容器中设置相关参数打印GC日志。\n\n\n命令动态查看\nJava 自动的工具行命令，jstat可以用来动态监控JVM内存的使用，统计垃圾回收的各项信息。\n\n比如常用命令，jstat -gc 统计垃圾回收堆的行为\n```\n$ jstat -gc 1262\n S0C    S1C     S0U     S1U   EC       EU        OC         OU        PC       PU         YGC    YGCT    FGC    FGCT     GCT   \n26112.0 24064.0 6562.5  0.0   564224.0 76274.5   434176.0   388518.3  524288.0 42724.7    320    6.417   1      0.398    6.815\n```\n也可以设置间隔固定时间来打印：\n```\n$ jstat -gc 1262 2000 20\n```\n这个命令意思就是每隔2000ms输出1262的gc情况，一共输出20次\n\n**GC参数**\nJVM的GC日志的主要参数包括如下几个：\n\n-XX:+PrintGC 输出GC日志\n-XX:+PrintGCDetails 输出GC的详细日志\n-XX:+PrintGCTimeStamps 输出GC的时间戳（以基准时间的形式）\n-XX:+PrintGCDateStamps 输出GC的时间戳（以日期的形式，如 2017-09-04T21:53:59.234+0800）\n-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息\n-Xloggc:../logs/gc.log 日志文件的输出路径\n在生产环境中，根据需要配置相应的参数来监控JVM运行情况。\n\n**Tomcat 设置示例**\n\n我们经常在tomcat的启动参数中添加JVM相关参数，这里有一个典型的示例：\n\n```\nJAVA_OPTS=\"-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4\n-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log \n-Djava.awt.headless=true \n-XX:+PrintGCTimeStamps -XX:+PrintGCDetails \n-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000\n-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15\"\n```\n根据上面的参数我们来做一下解析：\n\n-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m\nXms，即为jvm启动时得JVM初始堆大小,Xmx为jvm的最大堆大小，xmn为新生代的大小，permsize为永久代的初始大小，MaxPermSize为永久代的最大空间。\n\n-XX:SurvivorRatio=4\nSurvivorRatio为新生代空间中的Eden区和救助空间Survivor区的大小比值，默认是8，则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。调小这个参数将增大survivor区，让对象尽量在survitor区呆长一点，减少进入年老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代，加快年老代的回收频率，减少年老代暴涨的可能性，这个是通过将-XX:SurvivorRatio 设置成比较大的值（比如65536)来做到。\n\n-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log\n将虚拟机每次垃圾回收的信息写到日志文件中，文件名由file指定，文件格式是平文件，内容和-verbose:gc输出内容相同。\n\n-Djava.awt.headless=true Headless模式是系统的一种配置模式。在该模式下，系统缺少了显示设备、键盘或鼠标。\n\n-XX:+PrintGCTimeStamps -XX:+PrintGCDetails\n设置gc日志的格式\n\n-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000\n指定rmi调用时gc的时间间隔\n\n-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15 采用并发gc方式，经过15次minor gc 后进入年老代\n\n## 如何分析GC日志\n\n摘录GC日志一部分\n\nYoung GC回收日志:\n```\n2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]\n\n```\n\nFull GC回收日志:\n```\n2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]\n\n```\n通过上面日志分析得出，PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收前后年轻代的内存变化；ParOldGen表示gc回收前后老年代的内存变化；PSPermGen表示gc回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁，耗时短；full gc 会对整个堆内存进行回城，耗时长，因此一般尽量减少full gc的次数\n\n通过两张图非常明显看出gc日志构成：\n\nYoung GC日志:\n![c36e0c077a8a03f4d729eb2e8186edd9](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.resources/253C4E10-C025-406F-BCEC-360BD0B901AC.png)\n\nFull GC日志:\n![0d9fd6320ad97f3b5a08d1d8c836eae9](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.resources/E41583CF-6306-4B8F-95D7-396A3B91FBB1.png)\n\n## GC分析工具\n\nGChisto\nGChisto是一款专业分析gc日志的工具，可以通过gc日志来分析：Minor GC、full gc的时间、频率等等，通过列表、报表、图表等不同的形式来反应gc的情况。虽然界面略显粗糙，但是功能还是不错的。\n配置好本地的jdk环境之后，双击GChisto.jar,在弹出的输入框中点击 add 选择gc.log日志\n\n![49bfdc6b55d3cc7253ae9576a79741a6](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.resources/3BC499FA-2D44-4448-9720-AA4734BA2290.jpg)\n\nGC Pause Stats:可以查看GC 的次数、GC的时间、GC的开销、最大GC时间和最小GC时间等，以及相应的柱状图\n\n![65715c620529c3ecddba96af22e92486](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.resources/175F8410-B0BD-4288-A2C2-5C35AF57F933.jpg)\n\nGC Pause Distribution:查看GC停顿的详细分布，x轴表示垃圾收集停顿时间，y轴表示是停顿次数。\nGC Timeline：显示整个时间线上的垃圾收集\n\n![a9d2e4bc3d5ad85ad0f62005cd68b59b](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.resources/1550316713780.jpg)\n\n不过这款工具已经不再维护\nGC Easy\n这是一个web工具,在线使用非常方便.\n地址: http://gceasy.io\n进入官网，讲打包好的zip或者gz为后缀的压缩包上传，过一会就会拿到分析结果。\n\n![1c6eb44cfe99bf177388df9a2fc8f97d.png](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.resources/1550316679385.jpg)\n\n推荐使用此工具进行gc分析。\n\n![92ee2b2bea94d0ab7d5560cbb78bb8a2.png](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.resources/1550316713780.jpg)\n\n\n\n\n"
  },
  {
    "path": "JVM/jvm系列(四)jvm调优-命令大全（jps jstat jmap jhat jstack jinfo）.md",
    "content": "## 简介\n运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题！虽然jvm调优成熟的工具已经有很多：jconsole、大名鼎鼎的VisualVM，IBM的Memory Analyzer等等，但是在生产环境出现问题的时候，一方面工具的使用会有所限制，另一方面喜欢装X的我们，总喜欢在出现问题的时候在终端输入一些命令来解决。所有的工具几乎都是依赖于jdk的接口和底层的这些命令，研究这些命令的使用也让我们更能了解jvm构成和特性。\nSun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo下面做一一介绍\n\n**jps**\nJVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。\n\n命令格式\njps [options] [hostid]\noption参数\n-l : 输出主类全名或jar路径\n-q : 只输出LVMID\n-m : 输出JVM启动时传递给main()的参数\n-v : 输出JVM启动时显示指定的JVM参数\n其中[option]、[hostid]参数也可以不写。\n\n\n示例\n```\n$ jps -l -m\n  28920 org.apache.catalina.startup.Bootstrap start\n  11589 org.apache.catalina.startup.Bootstrap start\n  25816 sun.tools.jps.Jps -l -m\n```\n\n**jstat**\njstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令，它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。\n\n命令格式\njstat [option] LVMID [interval] [count]\n参数\n[option] : 操作参数\nLVMID : 本地虚拟机进程ID\n[interval] : 连续输出的时间间隔\n[count] : 连续输出的次数\n\noption 参数总览\n![9ae139c199be662b76b7860da58f0f07](jvm系列(四)jvm调优-命令大全（jps jstat jmap jhat jstack jinfo）.resources/3358A9D6-B4C7-4831-B26B-AD24973357EB.png)\n\noption 参数详解\n-class\n监视类装载、卸载数量、总空间以及耗费的时间\n```\n$ jstat -class 11589\n Loaded  Bytes  Unloaded  Bytes     Time   \n  7035  14506.3     0     0.0       3.67\nLoaded : 加载class的数量\nBytes : class字节大小\nUnloaded : 未加载class的数量\nBytes : 未加载class的字节大小\nTime : 加载时间\n-compiler\n输出JIT编译过的方法数量耗时等\n```\n$ jstat -compiler 1262\nCompiled Failed Invalid   Time   FailedType FailedMethod\n    2573      1       0    47.60          1 org/apache/catalina/loader/WebappClassLoader findResourceInternal  \nCompiled : 编译数量\nFailed : 编译失败数量\nInvalid : 无效数量\nTime : 编译耗时\nFailedType : 失败类型\nFailedMethod : 失败方法的全限定名\n-gc\n垃圾回收堆的行为统计，常用命令\n```\n$ jstat -gc 1262\n S0C    S1C     S0U     S1U   EC       EU        OC         OU        PC       PU         YGC    YGCT    FGC    FGCT     GCT   \n26112.0 24064.0 6562.5  0.0   564224.0 76274.5   434176.0   388518.3  524288.0 42724.7    320    6.417   1      0.398    6.815\n```\nC即Capacity 总容量，U即Used 已使用的容量\n\nS0C : survivor0区的总容量\nS1C : survivor1区的总容量\nS0U : survivor0区已使用的容量\nS1U : survivor1区已使用的容量\nEC : Eden区的总容量\nEU : Eden区已使用的容量\nOC : Old区的总容量\nOU : Old区已使用的容量\nPC\t当前perm的容量 (KB)\nPU\tperm的使用 (KB)\nYGC : 新生代垃圾回收次数\nYGCT : 新生代垃圾回收时间\nFGC : 老年代垃圾回收次数\nFGCT : 老年代垃圾回收时间\nGCT : 垃圾回收总消耗时间\n```\n$ jstat -gc 1262 2000 20\n```\n这个命令意思就是每隔2000ms输出1262的gc情况，一共输出20次\n\n-gccapacity\n同-gc，不过还会输出Java堆各区域使用到的最大、最小空间\n```\n$ jstat -gccapacity 1262\n NGCMN    NGCMX     NGC    S0C   S1C       EC         OGCMN      OGCMX      OGC        OC       PGCMN    PGCMX     PGC      PC         YGC    FGC \n614400.0 614400.0 614400.0 26112.0 24064.0 564224.0   434176.0   434176.0   434176.0   434176.0 524288.0 1048576.0 524288.0 524288.0    320     1  \n```\nNGCMN : 新生代占用的最小空间\nNGCMX : 新生代占用的最大空间\nOGCMN : 老年代占用的最小空间\nOGCMX : 老年代占用的最大空间\nOGC：当前年老代的容量 (KB)\nOC：当前年老代的空间 (KB)\nPGCMN : perm占用的最小空间\nPGCMX : perm占用的最大空间\n\n-gcutil\n\n同-gc，不过输出的是已使用空间占总空间的百分比\n```\n$ jstat -gcutil 28920\n  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT   \n 12.45   0.00  33.85   0.00   4.44  4       0.242     0    0.000    0.242\n```\n-gccause\n\n垃圾收集统计概述（同-gcutil），附加最近两次垃圾回收事件的原因\n```\n$ jstat -gccause 28920\n  S0     S1     E      O      P       YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC                 \n 12.45   0.00  33.85   0.00   4.44      4    0.242     0    0.000    0.242   Allocation Failure   No GC  \n```\nLGCC：最近垃圾回收的原因\nGCC：当前垃圾回收的原因\n-gcnew\n统计新生代的行为\n```\n$ jstat -gcnew 28920\n S0C      S1C      S0U        S1U  TT  MTT  DSS      EC        EU         YGC     YGCT  \n 419392.0 419392.0 52231.8    0.0  6   6    209696.0 3355520.0 1172246.0  4       0.242\n```\nTT：Tenuring threshold(提升阈值)\nMTT：最大的tenuring threshold\nDSS：survivor区域大小 (KB)\n\n-gcnewcapacity\n\n新生代与其相应的内存空间的统计\n```\n$ jstat -gcnewcapacity 28920\n  NGCMN      NGCMX       NGC      S0CMX     S0C     S1CMX     S1C       ECMX        EC        YGC   FGC \n 4194304.0  4194304.0  4194304.0 419392.0 419392.0 419392.0 419392.0  3355520.0  3355520.0     4     0\n```\nNGC:当前年轻代的容量 (KB)\nS0CMX:最大的S0空间 (KB)\nS0C:当前S0空间 (KB)\nECMX:最大eden空间 (KB)\nEC:当前eden空间 (KB)\n\n-gcold\n\n统计旧生代的行为\n```\n$ jstat -gcold 28920\n   PC       PU        OC           OU       YGC    FGC    FGCT     GCT   \n1048576.0  46561.7   6291456.0     0.0      4      0      0.000    0.242\n-gcoldcapacity\n```\n统计旧生代的大小和空间\n```\n$ jstat -gcoldcapacity 28920\n   OGCMN       OGCMX        OGC         OC         YGC   FGC    FGCT     GCT   \n  6291456.0   6291456.0   6291456.0   6291456.0     4     0    0.000    0.242\n```\n-gcpermcapacity\n\n永生代行为统计\n```\n$ jstat -gcpermcapacity 28920\n    PGCMN      PGCMX       PGC         PC      YGC   FGC    FGCT     GCT   \n 1048576.0  2097152.0  1048576.0  1048576.0     4     0    0.000    0.242\n```\n-printcompilation\n\nhotspot编译方法统计\n```\n$ jstat -printcompilation 28920\n    Compiled  Size  Type Method\n    1291      78     1    java/util/ArrayList indexOf\n```\nCompiled：被执行的编译任务的数量\nSize：方法字节码的字节数\nType：编译类型\nMethod：编译方法的类名和方法名。类名使用”/” 代替 “.” 作为空间分隔符. 方法名是给出类的方法名. 格式是一致于HotSpot - XX:+PrintComplation 选项\n\n**jmap**\njmap(JVM Memory Map)命令用于生成heap dump文件，如果不使用这个命令，还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。 jmap不仅能生成dump文件，还阔以查询finalize执行队列、Java堆和永久代的详细信息，如当前使用率、当前使用的是哪种收集器等。\n\n命令格式\njmap [option] LVMID\n\noption参数\n\ndump : 生成堆转储快照\nfinalizerinfo : 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象\nheap : 显示Java堆详细信息\nhisto : 显示堆中对象的统计信息\npermstat : to print permanent generation statistics\nF : 当-dump没有响应时，强制生成dump快照\n\n示例\n\n-dump\n常用格式\n```\n-dump::live,format=b,file=<filename> pid \n```\n\ndump堆到文件,format指定输出格式，live指明是活着的对象,file指定文件名\n```\n$ jmap -dump:live,format=b,file=dump.hprof 28920\n  Dumping heap to /home/xxx/dump.hprof ...\n  Heap dump file created\n```\ndump.hprof这个后缀是为了后续可以直接用MAT(Memory Anlysis Tool)打开。\n\n-finalizerinfo\n打印等待回收对象的信息\n\n```\n$ jmap -finalizerinfo 28920\n  Attaching to process ID 28920, please wait...\n  Debugger attached successfully.\n  Server compiler detected.\n  JVM version is 24.71-b01\n  Number of objects pending for finalization: 0\n```\n可以看到当前F-QUEUE队列中并没有等待Finalizer线程执行finalizer方法的对象。\n\n-heap\n打印heap的概要信息，GC使用的算法，heap的配置及wise heap的使用情况,可以用此来判断内存目前的使用情况以及垃圾回收情况\n\n```\n$ jmap -heap 28920\n  Attaching to process ID 28920, please wait...\n  Debugger attached successfully.\n  Server compiler detected.\n  JVM version is 24.71-b01  \n\n  using thread-local object allocation.\n  Parallel GC with 4 thread(s)//GC 方式  \n\n  Heap Configuration: //堆内存初始化配置\n     MinHeapFreeRatio = 0 //对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)\n     MaxHeapFreeRatio = 100 //对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)\n     MaxHeapSize      = 2082471936 (1986.0MB) //对应jvm启动参数-XX:MaxHeapSize=设置JVM堆的最大大小\n     NewSize          = 1310720 (1.25MB)//对应jvm启动参数-XX:NewSize=设置JVM堆的‘新生代’的默认大小\n     MaxNewSize       = 17592186044415 MB//对应jvm启动参数-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小\n     OldSize          = 5439488 (5.1875MB)//对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的‘老生代’的大小\n     NewRatio         = 2 //对应jvm启动参数-XX:NewRatio=:‘新生代’和‘老生代’的大小比率\n     SurvivorRatio    = 8 //对应jvm启动参数-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值 \n     PermSize         = 21757952 (20.75MB)  //对应jvm启动参数-XX:PermSize=<value>:设置JVM堆的‘永生代’的初始大小\n     MaxPermSize      = 85983232 (82.0MB)//对应jvm启动参数-XX:MaxPermSize=<value>:设置JVM堆的‘永生代’的最大大小\n     G1HeapRegionSize = 0 (0.0MB)  \n\n  Heap Usage://堆内存使用情况\n  PS Young Generation\n  Eden Space://Eden区内存分布\n     capacity = 33030144 (31.5MB)//Eden区总容量\n     used     = 1524040 (1.4534378051757812MB)  //Eden区已使用\n     free     = 31506104 (30.04656219482422MB)  //Eden区剩余容量\n     4.614088270399305% used //Eden区使用比率\n  From Space:  //其中一个Survivor区的内存分布\n     capacity = 5242880 (5.0MB)\n     used     = 0 (0.0MB)\n     free     = 5242880 (5.0MB)\n     0.0% used\n  To Space:  //另一个Survivor区的内存分布\n     capacity = 5242880 (5.0MB)\n     used     = 0 (0.0MB)\n     free     = 5242880 (5.0MB)\n     0.0% used\n  PS Old Generation //当前的Old区内存分布\n     capacity = 86507520 (82.5MB)\n     used     = 0 (0.0MB)\n     free     = 86507520 (82.5MB)\n     0.0% used\n  PS Perm Generation//当前的 “永生代” 内存分布\n     capacity = 22020096 (21.0MB)\n     used     = 2496528 (2.3808746337890625MB)\n     free     = 19523568 (18.619125366210938MB)\n     11.337498256138392% used  \n\n  670 interned Strings occupying 43720 bytes.\n```\n可以很清楚的看到Java堆中各个区域目前的情况。\n\n-histo\n打印堆的对象统计，包括对象数、内存大小等等 （因为在dump:live前会进行full gc，如果带上live则只统计活对象，因此不加live的堆大小要大于加live堆的大小 ）\n\n```\n$ jmap -histo:live 28920 | more\n num     #instances         #bytes  class name\n----------------------------------------------\n   1:         83613       12012248  <constMethodKlass>\n   2:         23868       11450280  [B\n   3:         83613       10716064  <methodKlass>\n   4:         76287       10412128  [C\n   5:          8227        9021176  <constantPoolKlass>\n   6:          8227        5830256  <instanceKlassKlass>\n   7:          7031        5156480  <constantPoolCacheKlass>\n   8:         73627        1767048  java.lang.String\n   9:          2260        1348848  <methodDataKlass>\n  10:          8856         849296  java.lang.Class\n  ....\n```\n仅仅打印了前10行\n\nxml class name是对象类型，说明如下：\n\nB  byte\nC  char\nD  double\nF  float\nI  int\nJ  long\nZ  boolean\n[  数组，如[I表示int[]\n[L+类名 其他对象\n\n-permstat\n打印Java堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言，它的名称、活跃度、地址、父类加载器、它所加载的类的数量和大小都会被打印。此外，包含的字符串数量和大小也会被打印。\n\n```\n$ jmap -permstat 28920\n  Attaching to process ID 28920, please wait...\n  Debugger attached successfully.\n  Server compiler detected.\n  JVM version is 24.71-b01\n  finding class loader instances ..done.\n  computing per loader stat ..done.\n  please wait.. computing liveness.liveness analysis may be inaccurate ...\n  \n  class_loader            classes bytes   parent_loader           alive?  type  \n  <bootstrap>             3111    18154296          null          live    <internal>\n  0x0000000600905cf8      1       1888    0x0000000600087f08      dead    sun/reflect/DelegatingClassLoader@0x00000007800500a0\n  0x00000006008fcb48      1       1888    0x0000000600087f08      dead    sun/reflect/DelegatingClassLoader@0x00000007800500a0\n  0x00000006016db798      0       0       0x00000006008d3fc0      dead    java/util/ResourceBundle$RBClassLoader@0x0000000780626ec0\n  0x00000006008d6810      1       3056      null          dead    sun/reflect/DelegatingClassLoader@0x00000007800500a0\n```\n-F\n强制模式。如果指定的pid没有响应，请使用jmap -dump或jmap -histo选项。此模式下，不支持live子选项。\n\njhat\njhat(JVM Heap Analysis Tool)命令是与jmap搭配使用，用来分析jmap生成的dump，jhat内置了一个微型的HTTP/HTML服务器，生成dump的分析结果后，可以在浏览器中查看。在此要注意，一般不会直接在服务器上进行分析，因为jhat是一个耗时并且耗费硬件资源的过程，一般把服务器生成的dump文件复制到本地或其他机器上进行分析。\n\n命令格式\njhat [dumpfile]\n\n参数\n\n* -stack false|true 关闭对象分配调用栈跟踪(tracking object allocation call stack)。 如果分配位置信息在堆转储中不可用. 则必须将此标志设置为 false. 默认值为 true.>\n\n* -refs false|true 关闭对象引用跟踪(tracking of references to objects)。 默认值为 true. 默认情况下, 返回的指针是指向其他特定对象的对象,如反向链接或输入引用(referrers or incoming references), 会统计/计算堆中的所有对象。>\n\n* -port port-number 设置 jhat HTTP server 的端口号. 默认值 7000.>\n\n* -exclude exclude-file 指定对象查询时需要排除的数据成员列表文件(a file that lists data members that should be excluded from the reachable objects query)。 例如, 如果文件列列出了 java.lang.String.value , 那么当从某个特定对象 Object o 计算可达的对象列表时, 引用路径涉及 java.lang.String.value 的都会被排除。>\n\n* -baseline exclude-file 指定一个基准堆转储(baseline heap dump)。 在两个 heap dumps 中有相同 object ID 的对象会被标记为不是新的(marked as not being new). 其他对象被标记为新的(new). 在比较两个不同的堆转储时很有用.>\n\n* -debug int 设置 debug 级别. 0 表示不输出调试信息。 值越大则表示输出更详细的 debug 信息.>\n\n* -version 启动后只显示版本信息就退出>\n\n* -J< flag > 因为 jhat 命令实际上会启动一个JVM来执行, 通过 -J 可以在启动JVM时传入一些启动参数. 例如, -J-Xmx512m 则指定运行 jhat 的Java虚拟机使用的最大堆内存为 512 MB. 如果需要使用多个JVM启动参数,则传入多个 -Jxxxxxx.\n\n示例\n\n```\n$ jhat -J-Xmx512m dump.hprof\n  eading from dump.hprof...\n  Dump file created Fri Mar 11 17:13:42 CST 2016\n  Snapshot read, resolving...\n  Resolving 271678 objects...\n  Chasing references, expect 54 dots......................................................\n  Eliminating duplicate references......................................................\n  Snapshot resolved.\n  Started HTTP server on port 7000\n  Server is ready.\n```\n\n中间的-J-Xmx512m是在dump快照很大的情况下分配512M内存去启动HTTP服务器，运行完之后就可在浏览器打开Http://localhost:7000进行快照分析 堆快照分析主要在最后面的Heap Histogram里，里面根据class列出了dump的时候所有存活对象。\n\n分析同样一个dump快照，MAT需要的额外内存比jhat要小的多的多，所以建议使用MAT来进行分析，当然也看个人偏好。\n\n分析\n打开浏览器Http://localhost:7000，该页面提供了几个查询功能可供使用：\n\n```\nAll classes including platform\nShow all members of the rootset\nShow instance counts for all classes (including platform)\nShow instance counts for all classes (excluding platform)\nShow heap histogram\nShow finalizer summary\nExecute Object Query Language (OQL) query\n```\n一般查看堆异常情况主要看这个两个部分： Show instance counts for all classes (excluding platform)，平台外的所有对象信息。如下图：\n![7f32f8469cc634a9c9a344ee905f9654](jvm系列(四)jvm调优-命令大全（jps jstat jmap jhat jstack jinfo）.resources/E7DB83E0-2344-4149-8603-C606D78AB943.png)\nShow heap histogram 以树状图形式展示堆情况。如下图：![80be25d2f1c6b019c28d96c42842fac6](jvm系列(四)jvm调优-命令大全（jps jstat jmap jhat jstack jinfo）.resources/8DE752AB-6C96-4A58-8E01-8970BA3E2014.png)\n具体排查时需要结合代码，观察是否大量应该被回收的对象在一直被引用或者是否有占用内存特别大的对象无法被回收。一般情况，会down到客户端用工具来分析\n\n**jstack**\njstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合，生成线程快照的主要目的是定位线程出现长时间停顿的原因，如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈，就可以知道没有响应的线程到底在后台做什么事情，或者等待什么资源。 如果java程序崩溃生成core文件，jstack工具可以用来获得core文件的java stack和native stack的信息，从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外，jstack工具还可以附属到正在运行的java程序中，看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态，jstack是非常有用的。\n\n命令格式\njstack [option] LVMID\noption参数\n-F : 当正常输出请求不被响应时，强制输出线程堆栈\n-l : 除堆栈外，显示关于锁的附加信息\n-m : 如果调用到本地方法的话，可以显示C/C++的堆栈\n\n示例\n```\n$ jstack -l 11494|more\n2016-07-28 13:40:04\nFull thread dump Java HotSpot(TM) 64-Bit Server VM (24.71-b01 mixed mode):\n\n\"Attach Listener\" daemon prio=10 tid=0x00007febb0002000 nid=0x6b6f waiting on condition [0x0000000000000000]\n   java.lang.Thread.State: RUNNABLE\n\n   Locked ownable synchronizers:\n        - None\n\n\"http-bio-8005-exec-2\" daemon prio=10 tid=0x00007feb94028000 nid=0x7b8c waiting on condition [0x00007fea8f56e000]\n   java.lang.Thread.State: WAITING (parking)\n        at sun.misc.Unsafe.park(Native Method)\n        - parking to wait for  <0x00000000cae09b80> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)\n        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)\n        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)\n        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)\n        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)\n        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)\n        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)\n        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)\n        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)\n        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n        at java.lang.Thread.run(Thread.java:745)\n\n   Locked ownable synchronizers:\n        - None\n      .....\n```\n这里有一篇文章解释的很好 [分析打印出的文件内容](http://www.hollischuang.com/archives/110)\njinfo\njinfo(JVM Configuration info)这个命令作用是实时查看和调整虚拟机运行参数。 之前的jps -v口令只能查看到显示指定的参数，如果想要查看未被显示指定的参数的值就要使用jinfo口令\n\n**jinfo**\njinfo(JVM Configuration info)这个命令作用是实时查看和调整虚拟机运行参数。 之前的jps -v口令只能查看到显示指定的参数，如果想要查看未被显示指定的参数的值就要使用jinfo口令\n\n命令格式\njinfo [option] [args] LVMID\noption参数\n-flag : 输出指定args参数的值\n-flags : 不需要args参数，输出所有JVM参数的值\n-sysprops : 输出系统属性，等同于System.getProperties()\n示例\n```\n$ jinfo -flag 11494\n-XX:CMSInitiatingOccupancyFraction=80\n```"
  },
  {
    "path": "JVM/内存分配与回收策略.md",
    "content": "# 内存分配与回收策略\n对象的内存分配，就是在堆上分配（也可能经过 JIT 编译后被拆散为标量类型并间接在栈上分配），对象主要分配在新生代的 Eden 区上，少数情况下可能直接分配在老年代，**分配规则不固定**，取决于当前使用的垃圾收集器组合以及相关的参数配置。\n\n以下列举几条最普遍的内存分配规则，供大家学习。\n## 对象优先在 Eden 分配\n大多数情况下，对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时，虚拟机将发起一次 Minor GC。\n\n👇**Minor GC** vs **Major GC**/**Full GC**：\n\n* Minor GC：回收新生代（包括 Eden 和 Survivor 区域），因为 Java 对象大多都具备朝生夕灭的特性，所以 Minor GC 非常频繁，一般回收速度也比较快。\n* Major GC / Full GC: 回收老年代，出现了 Major GC，经常会伴随至少一次的 Minor GC，但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。 \n\n\n> 在 JVM 规范中，Major GC 和 Full GC 都没有一个正式的定义，所以有人也简单地认为 Major GC 清理老年代，而 Full GC 清理整个内存堆。\n\n## 大对象直接进入老年代\n\n大对象是指需要大量连续内存空间的 Java 对象，如很长的字符串或数据。\n\n一个大对象能够存入 Eden 区的概率比较小，发生分配担保的概率比较大，而分配担保需要涉及大量的复制，就会造成效率低下。\n\n虚拟机提供了一个 -XX:PretenureSizeThreshold 参数，令大于这个设置值的对象直接在老年代分配，这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。（还记得吗，新生代采用复制算法回收垃圾）\n\n## 长期存活的对象将进入老年代\n\nJVM 给每个对象定义了一个对象年龄计数器。当新生代发生一次 Minor GC 后，存活下来的对象年龄 +1，当年龄超过一定值时，就将超过该值的所有对象转移到老年代中去。\n\n使用 -XXMaxTenuringThreshold 设置新生代的最大年龄，只要超过该参数的新生代对象都会被转移到老年代中去。\n\n## 动态对象年龄判定\n\n如果当前新生代的 Survivor 中，相同年龄所有对象大小的总和大于 Survivor 空间的一半，年龄 &gt;= 该年龄的对象就可以直接进入老年代，无须等到 MaxTenuringThreshold 中要求的年龄。\n\n## 空间分配担保\n\nJDK 6 Update 24 之前的规则是这样的：  \n 在发生 Minor GC 之前，虚拟机会先检查**老年代最大可用的连续空间是否大于新生代所有对象总空间**， 如果这个条件成立，Minor GC 可以确保是安全的； 如果不成立，则虚拟机会查看 HandlePromotionFailure 值是否设置为允许担保失败， 如果是，那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小， 如果大于，将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的； 如果小于，或者 HandlePromotionFailure 设置不允许冒险，那此时也要改为进行一次 Full GC。\n\nJDK 6 Update 24 之后的规则变为：  \n 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小，就会进行 Minor GC，否则将进行 Full GC。\n\n通过清除老年代中废弃数据来扩大老年代空闲空间，以便给新生代作担保。\n\n这个过程就是分配担保。\n\n---\n👇总结一下有哪些情况可能会触发 JVM 进行 Full GC。\n1. System.gc() 方法的调用<br>\n此方法的调用是建议 JVM 进行 Full GC，注意这**只是建议而非一定**，但在很多情况下它会触发 Full GC，从而增加 Full GC 的频率。通常情况下我们只需要让虚拟机自己去管理内存即可，我们可以通过 -XX:+ DisableExplicitGC 来禁止调用 System.gc()。\n\n2. 老年代空间不足<br>\n老年代空间不足会触发 Full GC操作，若进行该操作后空间依然不足，则会抛出如下错误：<br>\n` java.lang.OutOfMemoryError: Java heap space `\n\n3. 永久代空间不足<br>\nJVM 规范中运行时数据区域中的方法区，在 HotSpot 虚拟机中也称为永久代（Permanet Generation），存放一些类信息、常量、静态变量等数据，当系统要加载的类、反射的类和调用的方法较多时，永久代可能会被占满，会触发 Full GC。如果经过 Full GC 仍然回收不了，那么 JVM 会抛出如下错误信息：<br>\n`java.lang.OutOfMemoryError: PermGen space `\n\n4. CMS GC 时出现 promotion failed 和 concurrent mode failure<br>\npromotion failed，就是上文所说的担保失败，而 concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代，而此时老年代空间不足造成的。\n\n5. 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间\n\n（完）\n---\n👉 [Previous](/docs/04-hotspot-gc.md)<br>\n👉 [Next](/docs/06-jvm-performance-tuning.md)<br>\n👉 [Back to README](../README.md)\n"
  },
  {
    "path": "JVM/垃圾收集策略与算法.md",
    "content": "# 垃圾收集策略与算法\n\n程序计数器、虚拟机栈、本地方法栈随线程而生，也随线程而灭；栈帧随着方法的开始而入栈，随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性，在这几个区域内不需要过多考虑回收的问题，因为方法结束或者线程结束时，内存自然就跟随着回收了。\n\n而对于 Java 堆和方法区，我们只有在程序运行期间才能知道会创建哪些对象，这部分内存的分配和回收都是动态的，垃圾收集器所关注的正是这部分内存。\n\n## 判定对象是否存活\n\n若一个对象不被任何对象或变量引用，那么它就是无效对象，需要被回收。\n\n### 引用计数法\n\n在对象头维护着一个 counter 计数器，对象被引用一次则计数器 +1；若引用失效则计数器 -1。当计数器为 0 时，就认为该对象无效了。\n\n\n引用计数算法的实现简单，判定效率也很高，在大部分情况下它都是一个不错的算法。但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存，主要是因为它很难解决对象之间循环引用的问题。\n\n> 举个栗子👉对象 objA 和 objB 都有字段 instance，令 objA.instance = objB 并且 objB.instance = objA，由于它们互相引用着对方，导致它们的引用计数都不为 0，于是引用计数算法无法通知 GC 收集器回收它们。\n\n### 可达性分析法\n\n所有和 GC Roots 直接或间接关联的对象都是有效对象，和 GC Roots 没有关联的对象就是无效对象。\n\nGC Roots 是指：\n\n* Java 虚拟机栈（栈帧中的本地变量表）中引用的对象\n* 本地方法栈中引用的对象\n* 方法区中常量引用的对象\n* 方法区中类静态属性引用的对象\n\nGC Roots 并不包括堆中对象所引用的对象，这样就不会有循环引用的问题。\n\n## 引用的种类\n\n判定对象是否存活与“引用”有关。在 JDK 1.2 以前，Java 中的引用定义很传统，一个对象只有被引用或者没有被引用两种状态，我们希望能描述这一类对象：当内存空间还足够时，则保留在内存中；如果内存空间在进行垃圾手收集后还是非常紧张，则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。\n\n在 JDK 1.2 之后，Java 对引用的概念进行了扩充，将引用分为了以下四种。不同的引用类型，主要体现的是对象不同的可达性状态`reachable`和垃圾收集的影响。\n\n### 强引用（Strong Reference）\n\n类似 \"Object obj = new Object()\" 这类的引用，就是强引用，只要强引用存在，垃圾收集器永远不会回收被引用的对象。但是，如果我们**错误地保持了强引用**，比如：赋值给了 static 变量，那么对象在很长一段时间内不会被回收，会产生内存泄漏。\n\n### 软引用（Soft Reference）\n\n软引用是一种相对强引用弱化一些的引用，可以让对象豁免一些垃圾收集，只有当 JVM 认为内存不足时，才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前，清理软引用指向的对象。软引用通常用来**实现内存敏感的缓存**，如果还有空闲内存，就可以暂时保留缓存，当内存不足时清理掉，这样就保证了使用缓存的同时，不会耗尽内存。\n\n### 弱引用（Weak Reference）\n\n弱引用的**强度比软引用更弱**一些。当 JVM 进行垃圾回收时，**无论内存是否充足，都会回收**被软引用关联的对象。\n\n### 虚引用（Phantom Reference）\n\n虚引用也称幽灵引用或者幻影引用，它是**最弱**的一种引用关系。一个对象是否有虚引用的存在，完全不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 以后，做某些事情的机制，比如，通常用来做所谓的 Post-Mortem 清理机制。\n\n## 回收堆中无效对象\n\n对于可达性分析中不可达的对象，也并不是没有存活的可能。\n\n### 判定 finalize() 是否有必要执行\n\nJVM 会判断此对象是否有必要执行 finalize() 方法，如果对象没有覆盖 finalize() 方法，或者 finalize() 方法已经被虚拟机调用过，那么视为“没有必要执行”。那么对象基本上就真的被回收了。\n\n如果对象被判定为有必要执行 finalize() 方法，那么对象会被放入一个 F-Queue 队列中，虚拟机会以较低的优先级执行这些 finalize()方法，但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作，虚拟机就直接停止指向该方法，将对象清除。\n\n### 对象重生或死亡\n\n如果在执行 finalize() 方法时，将 this 赋给了某一个引用，那么该对象就重生了。如果没有，那么就会被垃圾收集器清除。\n\n> 任何一个对象的 finalize() 方法只会被系统自动调用一次，如果对象面临下一次回收，它的 finalize() 方法不会被再次执行，想继续在 finalize() 中自救就失效了。\n\n## 回收方法区内存\n\n方法区中存放生命周期较长的类信息、常量、静态变量，每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾：\n\n* 废弃常量\n* 无用的类\n\n### 判定废弃常量\n\n只要常量池中的常量不被任何变量或对象引用，那么这些常量就会被清除掉。比如，一个字符串 \"bingo\" 进入了常量池，但是当前系统没有任何一个 String 对象引用常量池中的 \"bingo\" 常量，也没有其它地方引用这个字面量，必要的话，\"bingo\"常量会被清理出常量池。\n\n### 判定无用的类\n\n判定一个类是否是“无用的类”，条件较为苛刻。\n\n* 该类的所有对象都已经被清除\n* 加载该类的 ClassLoader 已经被回收\n* 该类的 java.lang.Class 对象没有在任何地方被引用，无法在任何地方通过反射访问该类的方法。\n\n> 一个类被虚拟机加载进方法区，那么在堆中就会有一个代表该类的对象：java.lang.Class。这个对象在类被加载进方法区时创建，在方法区该类被删除时清除。\n\n## 垃圾收集算法\n\n学会了如何判定无效对象、无用类、废弃常量之后，剩余工作就是回收这些垃圾。常见的垃圾收集算法有以下几个：\n\n### 标记-清除算法\n\n判断哪些数据需要清除，并对它们进行标记，然后清除被标记的数据。\n\n这种方法有两个**不足**：\n\n* 效率问题：标记和清除两个过程的效率都不高。\n* 空间问题：标记清除之后会产生大量不连续的内存碎片，碎片太多可能导致以后需要分配较大对象时，无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。\n\n### 复制算法（新生代）\n\n为了解决效率问题，“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块，每次只使用其中的一块。当这一块内存用完，需要进行垃圾收集时，就将存活者的对象复制到另一块上面，然后将第一块内存全部清除。这种算法有优有劣：\n\n* 优点：不会有内存碎片的问题。\n* 缺点：内存缩小为原来的一半，浪费空间。\n\n为了解决空间利用率问题，可以将内存分为三块： Eden、From Survivor、To Survivor，比例是 8:1:1，每次使用 Eden 和其中一块 Survivor。回收时，将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上，最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。\n\n但是我们无法保证每次回收都只有不多于 10% 的对象存活，当 Survivor 空间不够，需要依赖其他内存（指老年代）进行分配担保。\n\n#### 分配担保\n\n为对象分配内存空间时，如果 Eden+Survivor 中空闲区域无法装下该对象，会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活，这样存活的对象直接通过分配担保机制进入老年代，然后再将新对象存入 Eden 区。\n\n### 标记-整理算法（老年代）\n\n在回收垃圾前，首先将废弃对象做上标记，然后将未标记的对象移到一边，最后清空另一边区域即可。\n\n这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长，因此每次垃圾回收会有大量对象存活，如果采用复制算法，每次需要复制大量存活的对象，效率很低。\n\n### 分代收集算法\n\n根据对象存活周期的不同，将内存划分为几块。一般是把 Java 堆分为新生代和老年代，针对各个年代的特点采用最适当的收集算法。  \n\n* 新生代：复制算法\n* 老年代：标记-清除算法、标记-整理算法\n\n（完）"
  },
  {
    "path": "JVM/我的Markdown笔记/jvm系列(五)Java GC 分析.md",
    "content": "Java GC就是JVM记录仪，书画了JVM各个分区的表演。\n\n## 什么是 Java GC\n\nJava GC（Garbage Collection，垃圾收集，垃圾回收）机制，是Java与C++/C的主要区别之一，作为Java开发者，一般不需要专门编写内存回收和垃圾清理代码，对内存泄露和溢出的问题，也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中，存在自动内存管理和垃圾清扫机制。概括地说，该机制对JVM（Java Virtual Machine）中的内存进行标记，并确定哪些内存需要回收，根据一定的回收策略，自动的回收内存，永不停息（Nerver Stop）的保证JVM中的内存空间，防止出现内存泄露和溢出问题。\n\n\n在Java语言出现之前，就有GC机制的存在，如Lisp语言），Java GC机制已经日臻完善，几乎可以自动的为我们做绝大多数的事情。然而，如果我们从事较大型的应用软件开发，曾经出现过内存优化的需求，就必定要研究Java GC机制。\n\n简单总结一下，Java GC就是通过GC收集器回收不在存活的对象，保证JVM更加高效的运转。\n\n## 如何获取 Java GC日志\n\n一般情况可以通过两种方式来获取GC日志，一种是使用命令动态查看，一种是在容器中设置相关参数打印GC日志。\n\n命令动态查看\nJava 自动的工具行命令，jstat可以用来动态监控JVM内存的使用，统计垃圾回收的各项信息。\n\n比如常用命令，jstat -gc 统计垃圾回收堆的行为\n```\n$ jstat -gc 1262\n S0C    S1C     S0U     S1U   EC       EU        OC         OU        PC       PU         YGC    YGCT    FGC    FGCT     GCT   \n26112.0 24064.0 6562.5  0.0   564224.0 76274.5   434176.0   388518.3  524288.0 42724.7    320    6.417   1      0.398    6.815\n```\n也可以设置间隔固定时间来打印：\n```\n$ jstat -gc 1262 2000 20\n```\n这个命令意思就是每隔2000ms输出1262的gc情况，一共输出20次\n\n**GC参数**\nJVM的GC日志的主要参数包括如下几个：\n\n-XX:+PrintGC 输出GC日志\n-XX:+PrintGCDetails 输出GC的详细日志\n-XX:+PrintGCTimeStamps 输出GC的时间戳（以基准时间的形式）\n-XX:+PrintGCDateStamps 输出GC的时间戳（以日期的形式，如 2017-09-04T21:53:59.234+0800）\n-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息\n-Xloggc:../logs/gc.log 日志文件的输出路径\n在生产环境中，根据需要配置相应的参数来监控JVM运行情况。\n\n**Tomcat 设置示例**\n\n我们经常在tomcat的启动参数中添加JVM相关参数，这里有一个典型的示例：\n\n```\nJAVA_OPTS=\"-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4\n-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log \n-Djava.awt.headless=true \n-XX:+PrintGCTimeStamps -XX:+PrintGCDetails \n-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000\n-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15\"\n```\n根据上面的参数我们来做一下解析：\n\n-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m\nXms，即为jvm启动时得JVM初始堆大小,Xmx为jvm的最大堆大小，xmn为新生代的大小，permsize为永久代的初始大小，MaxPermSize为永久代的最大空间。\n\n-XX:SurvivorRatio=4\nSurvivorRatio为新生代空间中的Eden区和救助空间Survivor区的大小比值，默认是8，则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。调小这个参数将增大survivor区，让对象尽量在survitor区呆长一点，减少进入年老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代，加快年老代的回收频率，减少年老代暴涨的可能性，这个是通过将-XX:SurvivorRatio 设置成比较大的值（比如65536)来做到。\n\n-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log\n将虚拟机每次垃圾回收的信息写到日志文件中，文件名由file指定，文件格式是平文件，内容和-verbose:gc输出内容相同。\n\n-Djava.awt.headless=true Headless模式是系统的一种配置模式。在该模式下，系统缺少了显示设备、键盘或鼠标。\n\n-XX:+PrintGCTimeStamps -XX:+PrintGCDetails\n设置gc日志的格式\n\n-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000\n指定rmi调用时gc的时间间隔\n\n-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15 采用并发gc方式，经过15次minor gc 后进入年老代\n\n## 如何分析GC日志\n\n摘录GC日志一部分\n\nYoung GC回收日志:\n```\n2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]\n\n```\n\nFull GC回收日志:\n```\n2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]\n\n```\n通过上面日志分析得出，PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收前后年轻代的内存变化；ParOldGen表示gc回收前后老年代的内存变化；PSPermGen表示gc回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁，耗时短；full gc 会对整个堆内存进行回城，耗时长，因此一般尽量减少full gc的次数\n\n通过两张图非常明显看出gc日志构成：\n\nYoung GC日志:![c36e0c077a8a03f4d729eb2e8186edd9](jvm系列(五)Java GC 分析.resources/253C4E10-C025-406F-BCEC-360BD0B901AC.png)\n\nFull GC日志:![0d9fd6320ad97f3b5a08d1d8c836eae9](jvm系列(五)Java GC 分析.resources/E41583CF-6306-4B8F-95D7-396A3B91FBB1.png)\n\n## GC分析工具\n\nGChisto\nGChisto是一款专业分析gc日志的工具，可以通过gc日志来分析：Minor GC、full gc的时间、频率等等，通过列表、报表、图表等不同的形式来反应gc的情况。虽然界面略显粗糙，但是功能还是不错的。\n配置好本地的jdk环境之后，双击GChisto.jar,在弹出的输入框中点击 add 选择gc.log日志\n![49bfdc6b55d3cc7253ae9576a79741a6](jvm系列(五)Java GC 分析.resources/3BC499FA-2D44-4448-9720-AA4734BA2290.jpg)\nGC Pause Stats:可以查看GC 的次数、GC的时间、GC的开销、最大GC时间和最小GC时间等，以及相应的柱状图![65715c620529c3ecddba96af22e92486](jvm系列(五)Java GC 分析.resources/175F8410-B0BD-4288-A2C2-5C35AF57F933.jpg)\nGC Pause Distribution:查看GC停顿的详细分布，x轴表示垃圾收集停顿时间，y轴表示是停顿次数。\nGC Timeline：显示整个时间线上的垃圾收集\n![a9d2e4bc3d5ad85ad0f62005cd68b59b](jvm系列(五)Java GC 分析.resources/AE9DCFB3-730C-4431-964A-6C54DDE8DCC0.jpg)\n不过这款工具已经不再维护\nGC Easy\n这是一个web工具,在线使用非常方便.\n地址: http://gceasy.io\n进入官网，讲打包好的zip或者gz为后缀的压缩包上传，过一会就会拿到分析结果。\n![1c6eb44cfe99bf177388df9a2fc8f97d.png](evernotecid://DF961740-2AB0-48AB-AAE7-53BB9D286C7A/appyinxiangcom/12131181/ENNote/p266?hash=1c6eb44cfe99bf177388df9a2fc8f97d)\n推荐使用此工具进行gc分析。\n![92ee2b2bea94d0ab7d5560cbb78bb8a2.png](evernotecid://DF961740-2AB0-48AB-AAE7-53BB9D286C7A/appyinxiangcom/12131181/ENNote/p266?hash=92ee2b2bea94d0ab7d5560cbb78bb8a2)\n\n\n\n"
  },
  {
    "path": "JVM/类加载器.md",
    "content": "# 类加载器\n\n## 类与类加载器\n\n### 判断类是否“相等”\n\n任意一个类，都由**加载它的类加载器**和这个**类本身**一同确立其在 Java 虚拟机中的唯一性，每一个类加载器，都有一个独立的类名称空间。\n\n因此，比较两个类是否“相等”，只有在这两个类是由同一个类加载器加载的前提下才有意义，否则，即使这两个类来源于同一个 Class 文件，被同一个虚拟机加载，只要加载它们的类加载器不同，那么这两个类就必定不相等。\n\n这里的“相等”，包括代表类的 Class 对象的 equals\\(\\) 方法、isInstance\\(\\) 方法的返回结果，也包括使用 instanceof 关键字做对象所属关系判定等情况。\n\n\n### 加载器种类\n\n系统提供了 3 种类加载器：\n\n* 启动类加载器（Bootstrap ClassLoader）：  负责将存放在 `<JAVA_HOME>\\lib` 目录中的，并且能被虚拟机识别的（仅按照文件名识别，如 rt.jar，名字不符合的类库即使放在 lib 目录中也不会被加载）类库加载到虚拟机内存中。\n* 扩展类加载器（Extension ClassLoader）：  负责加载 `<JAVA_HOME>\\lib\\ext` 目录中的所有类库，开发者可以直接使用扩展类加载器。\n* 应用程序类加载器（Application ClassLoader）：  由于这个类加载器是 ClassLoader 中的 getSystemClassLoader\\(\\) 方法的返回值，所以一般也称它为“系统类加载器”。它负责加载用户类路径（classpath）上所指定的类库，开发者可以直接使用这个类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。\n\n![c90fc73c0a0e1afdf5ec4c606142707f](类加载器.resources/03A2290D-59DF-4E6C-9849-6E91930777E4.png)\n\n\n当然，如果有必要，还可以加入自己定义的类加载器。\n\n## 双亲委派模型\n\n### 什么是双亲委派模型\n\n双亲委派模型是描述类加载器之间的层次关系。它要求除了顶层的启动类加载器外，其余的类加载器都应当有自己的父类加载器。（父子关系一般不会以继承的关系实现，而是以组合关系来复用父加载器的代码）\n\n### 工作过程\n\n如果一个类加载器收到了类加载的请求，它首先不会自己去尝试加载这个类，而是把这个请求委派给父类加载器去完成，每一个层次的类加载器都是如此，因此所有的加载请求最终都应该传送到顶层的启动类加载器中，只有当父加载器反馈自己无法完成这个加载请求（找不到所需的类）时，子加载器才会尝试自己去加载。\n\n在 java.lang.ClassLoader 中的 loadClass\\(\\) 方法中实现该过程。\n\n### 为什么使用双亲委派模型\n\n像 java.lang.Object 这些存放在 rt.jar 中的类，无论使用哪个类加载器加载，最终都会委派给最顶端的启动类加载器加载，从而使得不同加载器加载的 Object 类都是同一个。\n\n相反，如果没有使用双亲委派模型，由各个类加载器自行去加载的话，如果用户自己编写了一个称为 java.lang.Object 的类，并放在 classpath 下，那么系统将会出现多个不同的 Object 类，Java 类型体系中最基础的行为也就无法保证。\n\n（完）\n"
  },
  {
    "path": "JVM/类加载的时机.md",
    "content": "# 类加载的时机\n\n## 类的生命周期\n\n类从被加载到虚拟机内存开始，到卸载出内存为止，它的整个生命周期包括以下 7 个阶段：\n\n* 加载\n* 验证\n* 准备\n* 解析\n* 初始化\n* 使用\n* 卸载\n\n验证、准备、解析 3 个阶段统称为连接。\n\n\n![ee1b2f75d51d55768af061871a9d7f78.png](evernotecid://DF961740-2AB0-48AB-AAE7-53BB9D286C7A/appyinxiangcom/12131181/ENNote/p259?hash=ee1b2f75d51d55768af061871a9d7f78)\n\n加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的，类的加载过程必须按照这种顺序按部就班地开始（注意是“开始”，而不是“进行”或“完成”），而解析阶段则不一定：它在某些情况下可以在初始化后再开始，这是为了支持 Java 语言的运行时绑定。\n\n## 类加载过程中“初始化”开始的时机\n\nJava 虚拟机规范没有强制约束类加载过程的第一阶段（即：加载）什么时候开始，但对于“初始化”阶段，有着严格的规定。有且仅有 5 种情况必须立即对类进行“初始化”：\n\n* 在遇到 new、putstatic、getstatic、invokestatic 字节码指令时，如果类尚未初始化，则需要先触发其初始化。\n* 对类进行反射调用时，如果类还没有初始化，则需要先触发其初始化。\n* 初始化一个类时，如果其父类还没有初始化，则需要先初始化父类。\n* 虚拟机启动时，用于需要指定一个包含 main\\(\\) 方法的主类，虚拟机会先初始化这个主类。\n* 当使用 JDK 1.7 的动态语言支持时，如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF\\_getStatic、REF\\_putStatic、REF\\_invokeStatic 的方法句柄，并且这个方法句柄所对应的类还没初始化，则需要先触发其初始化。\n\n这 5 种场景中的行为称为对一个类进行**主动引用**，除此之外，其它所有引用类的方式都不会触发初始化，称为**被动引用**。\n\n## 被动引用演示 Demo\n\n### Demo1\n\n```java\n/**\n * 被动引用 Demo1:\n * 通过子类引用父类的静态字段，不会导致子类初始化。\n * \n * @author ylb\n *\n */\nclass SuperClass {\n    static {\n        System.out.println(\"SuperClass init!\");\n    }\n\n    public static int value = 123;\n}\n\nclass SubClass extends SuperClass {\n    static {\n        System.out.println(\"SubClass init!\");\n    }\n}\n\npublic class NotInitialization {\n\n    public static void main(String[] args) {\n        System.out.println(SubClass.value);\n        // SuperClass init!\n    }\n\n}\n```\n\n对于静态字段，只有直接定义这个字段的类才会被初始化，因此通过其子类来引用父类中定义的静态字段，只会触发父类的初始化而不会触发子类的初始化。\n\n### Demo2\n\n```java\n/**\n * 被动引用 Demo2:\n * 通过数组定义来引用类，不会触发此类的初始化。\n * \n * @author ylb\n *\n */\n\npublic class NotInitialization {\n\n    public static void main(String[] args) {\n        SuperClass[] superClasses = new SuperClass[10];\n    }\n\n}\n```\n\n这段代码不会触发父类的初始化，但会触发“\\[L 全类名”这个类的初始化，它由虚拟机自动生成，直接继承自 java.lang.Object，创建动作由字节码指令 newarray 触发。\n\n### Demo3\n\n```java\n/**\n * 被动引用 Demo3:\n * 常量在编译阶段会存入调用类的常量池中，本质上并没有直接引用到定义常量的类，因此不会触发定义常量的类的初始化。\n * \n * @author ylb\n *\n */\nclass ConstClass {\n    static {\n        System.out.println(\"ConstClass init!\");\n    }\n\n    public static final String HELLO_BINGO = \"Hello Bingo\";\n\n}\n\npublic class NotInitialization {\n\n    public static void main(String[] args) {\n        System.out.println(ConstClass.HELLO_BINGO);\n    }\n\n}\n```\n\n编译通过之后，常量存储到 NotInitialization 类的常量池中，NotInitialization 的 Class 文件中并没有 ConstClass 类的符号引用入口，这两个类在编译成 Class 之后就没有任何联系了。\n\n## 接口的加载过程\n\n接口加载过程与类加载过程稍有不同。\n\n当一个类在初始化时，要求其父类全部都已经初始化过了，但是一个接口在初始化时，并不要求其父接口全部都完成了初始化，当真正用到父接口的时候才会初始化。\n\n（完）\n"
  },
  {
    "path": "JVM/类加载的过程.md",
    "content": "# 类加载的过程\n类加载过程包括 5 个阶段：加载、验证、准备、解析和初始化。\n## 加载\n\n### 加载的过程\n\n“加载”是“类加载”过程的一个阶段，不能混淆这两个名词。在加载阶段，虚拟机需要完成 3 件事：\n\n* 通过类的全限定名获取该类的二进制字节流。\n* 将二进制字节流所代表的静态结构转化为方法区的运行时数据结构。\n* 在内存中创建一个代表该类的 java.lang.Class 对象，作为方法区这个类的各种数据的访问入口。\n\n### 获取二进制字节流\n\n对于 Class 文件，虚拟机没有指明要从哪里获取、怎样获取。除了直接从编译好的 .class 文件中读取，还有以下几种方式：\n\n\n* 从 zip 包中读取，如 jar、war等\n* 从网络中获取，如 Applect\n* 通过动态代理计数生成代理类的二进制字节流\n* 由 JSP 文件生成对应的 Class 类\n* 从数据库中读取，如 有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发。\n\n### “非数组类”与“数组类”加载比较\n\n* 非数组类加载阶段可以使用系统提供的引导类加载器，也可以由用户自定义的类加载器完成，开发人员可以通过定义自己的类加载器控制字节流的获取方式（如重写一个类加载器的 loadClass\\(\\) 方法）\n* 数组类本身不通过类加载器创建，它是由 Java 虚拟机直接创建的，再由类加载器创建数组中的元素类。\n\n### 注意事项\n\n* 虚拟机规范未规定 Class 对象的存储位置，对于 HotSpot 虚拟机而言，Class 对象比较特殊，它虽然是对象，但存放在方法区中。\n* 加载阶段与连接阶段的部分内容交叉进行，加载阶段尚未完成，连接阶段可能已经开始了。但这两个阶段的开始实践仍然保持着固定的先后顺序。\n\n## 验证\n\n### 验证的重要性\n\n验证阶段确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。\n\n### 验证的过程\n\n* 文件格式验证  验证字节流是否符合 Class 文件格式的规范，并且能被当前版本的虚拟机处理，验证点如下： \n  * 是否以魔数 0XCAFEBABE 开头\n  * 主次版本号是否在当前虚拟机处理范围内\n  * 常量池是否有不被支持的常量类型\n  * 指向常量的索引值是否指向了不存在的常量\n  * CONSTANT\\_Utf8\\_info 型的常量是否有不符合 UTF8 编码的数据\n  * ......\n* 元数据验证  对字节码描述信息进行语义分析，确保其符合 Java 语法规范。\n* 字节码验证  本阶段是验证过程中最复杂的一个阶段，是对方法体进行语义分析，保证方法在运行时不会出现危害虚拟机的事件。\n* 符号引用验证 本阶段发生在解析阶段，确保解析正常执行。\n\n## 准备\n\n准备阶段是正式为类变量（或称“静态成员变量”）分配内存并设置初始值的阶段。这些变量（不包括实例变量）所使用的内存都在方法区中进行分配。\n\n初始值“通常情况下”是数据类型的零值（0, null...），假设一个类变量的定义为：\n\n```java\npublic static int value = 123;\n```\n\n那么变量 value 在准备阶段过后的初始值为 0 而不是 123，因为这时候尚未开始执行任何 Java 方法。\n\n存在“特殊情况”：如果类字段的字段属性表中存在 ConstantValue 属性，那么在准备阶段 value 就会被初始化为 ConstantValue 属性所指定的值，假设上面类变量 value 的定义变为：\n\n```java\npublic static final int value = 123;\n```\n\n那么在准备阶段虚拟机会根据 ConstantValue 的设置将 value 赋值为 123。\n\n## 解析\n\n解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。\n\n## 初始化\n\n类初始化阶段是类加载过程的最后一步，是执行类构造器 &lt;clinit&gt;\\(\\) 方法的过程。\n\n&lt;clinit&gt;\\(\\) 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块（static {} 块）中的语句合并产生的，编译器收集的顺序是由语句在源文件中出现的顺序所决定的。\n\n静态语句块中只能访问定义在静态语句块之前的变量，定义在它之后的变量，在前面的静态语句块中可以赋值，但不能访问。如下方代码所示：\n\n```java\npublic class Test {\n    static {\n        i = 0;  // 给变量赋值可以正常编译通过\n        System.out.println(i);  // 这句编译器会提示“非法向前引用”\n    }\n    static int i = 1;\n}\n```\n\n&lt;clinit&gt;\\(\\) 方法不需要显式调用父类构造器，虚拟机会保证在子类的 &lt;clinit&gt;\\(\\) 方法执行之前，父类的 &lt;clinit&gt;\\(\\) 方法已经执行完毕。\n\n由于父类的 &lt;clinit&gt;\\(\\) 方法先执行，意味着父类中定义的静态语句块要优先于子类的变量赋值操作。如下方代码所示：\n\n```java\nstatic class Parent {\n    public static int A = 1;\n    static {\n        A = 2;\n    }\n}\n\nstatic class Sub extends Parent {\n    public static int B = A;\n}\n\npublic static void main(String[] args) {\n    System.out.println(Sub.B); // 输出 2\n}\n```\n\n&lt;clinit&gt;\\(\\) 方法不是必需的，如果一个类没有静态语句块，也没有对类变量的赋值操作，那么编译器可以不为这个类生成 &lt;clinit&gt;\\(\\) 方法。\n\n接口中不能使用静态代码块，但接口也需要通过 &lt;clinit&gt;\\(\\) 方法为接口中定义的静态成员变量显式初始化。但接口与类不同，接口的 &lt;clinit&gt;\\(\\) 方法不需要先执行父类的 &lt;clinit&gt;\\(\\) 方法，只有当父接口中定义的变量使用时，父接口才会初始化。\n\n虚拟机会保证一个类的 &lt;clinit&gt;\\(\\) 方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类，那么只会有一个线程去执行这个类的 &lt;clinit&gt;\\(\\) 方法。\n\n（完）"
  },
  {
    "path": "JVM/类文件结构.md",
    "content": "# 类文件结构\n\n## JVM 的“无关性”\n\n谈论 JVM 的无关性，主要有以下两个：  \n\n* 平台无关性：任何操作系统都能运行 Java 代码\n* 语言无关性： JVM 能运行除 Java 以外的其他代码\n\nJava 源代码首先需要使用 Javac 编译器编译成 .class 文件，然后由 JVM 执行 .class 文件，从而程序开始运行。\n\nJVM 只认识 .class 文件，它不关心是何种语言生成了 .class 文件，只要 .class 文件符合 JVM 的规范就能运行。 目前已经有 JRuby、Jython、Scala 等语言能够在 JVM 上运行。它们有各自的语法规则，不过它们的编译器 都能将各自的源码编译成符合 JVM 规范的 .class 文件，从而能够借助 JVM 运行它们。\n\n> Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的， 因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更加强大。 因此，有一些 Java 语言本身无法有效支持的语言特性，不代表字节码本身无法有效支持。\n\n\n## Class 文件结构\n\nClass 文件时二进制文件，它的内容具有严格的规范，文件中没有任何空格，全都是连续的 0/1。Class 文件 中的所有内容被分为两种类型：无符号数、表。\n\n* 无符号数  无符号数表示 Class 文件中的值，这些值没有任何类型，但有不同的长度。u1、u2、u4、u8 分别代表 1/2/4/8 字节的无符号数。\n* 表  由多个无符号数或者其他表作为数据项构成的符合数据类型。\n\nClass 文件具体由以下几个构成:\n\n* 魔数\n* 版本信息\n* 常量池\n* 访问标志\n* 类索引、父类索引、接口索引集合\n* 字段表集合\n* 方法表集合\n* 属性表集合\n\n### 魔数\n\nClass 文件的头 4 个字节称为魔数，用来表示这个 Class 文件的类型。\n\nClass 文件的魔数是用 16 进制表示的“CAFE BABE”，是不是很具有浪漫色彩？\n\n> 魔数相当于文件后缀名，只不过后缀名容易被修改，不安全，因此在 Class 文件中标识文件类型比较合适。\n\n### 版本信息\n\n紧接着魔数的 4 个字节是版本信息，5-6 字节表示次版本号，7-8 字节表示主版本号，它们表示当前 Class 文件中使用的是哪个版本的 JDK。\n\n高版本的 JDK 能向下兼容以前版本的 Class 文件，但不能运行以后版本的 Class 文件，即时文件格式并未发生任何变化，虚拟机也必需拒绝执行超过其版本号的 Class 文件。\n\n### 常量池\n\n版本信息之后就是常量池，常量池中存放两种类型的常量：  \n\n\n* 字面值常量  \n\n\n  字面值常量就是我们在程序中定义的字符串、被 final 修饰的值。\n\n* 符号引用  \n\n\n  符号引用就是我们定义的各种名字：类和接口的全限定名、字段的名字和描述符、方法的名字和描述符。\n\n#### 常量池的特点\n\n* 常量池中常量数量不固定，因此常量池开头放置一个 u2 类型的无符号数，用来存储当前常量池的容量。\n* 常量池的每一项常量都是一个表，表开始的第一位是一个 u1 类型的标志位（tag），代表当前这个常量属于哪种常量类型。\n\n#### 常量池中常量类型\n\n| 类型    | tag   | 描述　|\n|---|---|---|\n| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |\n| CONSTANT_Integer_info | 3 | 整型字面量 |\n| CONSTANT_Float_info | 4 | 浮点型字面量 |\n| CONSTANT_Long_info  |5 | 长整型字面量 |\n| CONSTANT_Double_info | 6 |  双精度浮点型字面量 |\n| CONSTANT_Class_info | 7 |   类或接口的符号引用 |\n| CONSTANT_String_info    | 8 |   字符串类型字面量 |\n| CONSTANT_Fieldref_info  | 9 |   字段的符号引用 |\n| CONSTANT_Methodref_info | 10 |  类中方法的符号引用 |\n| CONSTANT_InterfaceMethodref_info    | 11 |  接口中方法的符号引用 |\n| CONSTANT_NameAndType_info | 12 |    字段或方法的符号引用 |\n| CONSTANT_MethodHandle_info  | 15 |  表示方法句柄 |\n| CONSTANT_MethodType_info    | 16 |  标识方法类型 |\n| CONSTANT_InvokeDynamic_info | 18 |  表示一个动态方法调用点 |\n\n对于 CONSTANT\\_Class\\_info（此类型的常量代表一个类或者接口的符号引用），它的二维表结构如下：\n\n| 类型 | 名称 | 数量 |\n| --- | --- | --- |\n| u1 | tag | 1 |\n| u2 | name\\_index | 1 |\n\ntag 是标志位，用于区分常量类型；name\\_index 是一个索引值，它指向常量池中一个 CONSTANT\\_Utf8\\_info 类型常量，此常量代表这个类（或接口）的全限定名，这里 name\\_index 值若为 0x0002，也即是指向了常量池中的第二项常量。\n\nCONSTANT\\_Utf8\\_info 型常量的结构如下：\n\n| 类型 | 名称 | 数量 |\n| --- | --- | --- |\n| u1 | tag | 1 |\n| u2 | length | 1 |\n| u1 | bytes | length |\n\ntag 是当前常量的类型；length 表示这个字符串的长度；bytes 是这个字符串的内容（采用缩略的 UTF8 编码）\n\n### 访问标志\n\n在常量池结束之后，紧接着的两个字节代表访问标志，这个标志用于识别一些类或者接口层次的访问信息，包括：这个 Class 是类还是接口；是否定义为 public 类型；是否被 abstract/final 修饰。\n\n### 类索引、父类索引、接口索引集合\n\n类索引和父类索引都是一个 u2 类型的数据，而接口索引集合是一组 u2 类型的数据的集合，Class 文件中由这三项数据来确定类的继承关系。类索引用于确定这个类的全限定名，父类索引用于确定这个类的父类的全限定名。\n\n由于 Java 不允许多重继承，所以父类索引只有一个，除了 java.lang.Object 之外，所有的 Java 类都有父类，因此除了 java.lang.Object 外，所有 Java 类的父类索引都不为 0。一个类可能实现了多个接口，因此用接口索引集合来描述。这个集合第一项为 u2 类型的数据，表示索引表的容量，接下来就是接口的名字索引。\n\n类索引和父类索引用两个 u2 类型的索引值表示，它们各自指向一个类型为 CONSTANT\\_Class\\_info 的类描述符常量，通过该常量总的索引值可以找到定义在 CONSTANT\\_Utf8\\_info 类型的常量中的全限定名字符串。\n\n### 字段表集合\n\n字段表集合存储本类涉及到的成员变量，包括实例变量和类变量，但不包括方法中的局部变量。\n\n每一个字段表只表示一个成员变量，本类中的所有成员变量构成了字段表集合。字段表结构如下：\n\n| 类型 | 名称 | 数量 | 说明 |\n| --- | --- | --- | --- |\n| u2 | access\\_flags | 1 | 字段的访问标志，与类稍有不同 |\n| u2 | name\\_index | 1 | 字段名字的索引 |\n| u2 | descriptor\\_index | 1 | 描述符，用于描述字段的数据类型。 基本数据类型用大写字母表示； 对象类型用“L 对象类型的全限定名”表示。 |\n| u2 | attributes\\_count | 1 | 属性表集合的长度 |\n| u2 | attributes | attributes\\_count | 属性表集合，用于存放属性的额外信息，如属性的值。 |\n\n> 字段表集合中不会出现从父类（或接口）中继承而来的字段，但有可能出现原本 Java 代码中不存在的字段，譬如在内部类中为了保持对外部类的访问性，会自动添加指向外部类实例的字段。\n\n### 方法表集合\n\n方法表结构与属性表类似。\n\nvolatile 关键字 和 transient 关键字不能修饰方法，所以方法表的访问标志中没有 ACC\\_VOLATILE 和 ACC\\_TRANSIENT 标志。\n\n方法表的属性表集合中有一张 Code 属性表，用于存储当前方法经编译器编译后的字节码指令。\n\n### 属性表集合\n\n每个属性对应一张属性表，属性表的结构如下：\n\n| 类型 | 名称 | 数量 |\n| --- | --- | --- |\n| u2 | attribute\\_name\\_index | 1 |\n| u4 | attribute\\_length | 1 |\n| u1 | info | attribute\\_length |\n\n（完）"
  },
  {
    "path": "Java高级特性增强/Java NIO之Buffer(缓冲区).md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n\n## Java NIO之Buffer(缓冲区)\n\n\n#### Buffer(缓冲区)介绍\nJava NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里，从Buffer把数据写入到Channels.\n\nBuffer本质上就是一块内存区，可以用来写入数据，并在稍后读取出来。这块内存被NIO Buffer包裹起来，对外提供一系列的读写方便开发的接口。\n\n在Java NIO中使用的核心缓冲区如下（覆盖了通过I/O发送的基本数据类型：byte, char、short, int, long, float, double ，long）：\n\n* ByteBuffer\n* CharBuffer\n* ShortBuffer\n* IntBuffer\n* FloatBuffer\n* DoubleBuffer\n* LongBuffer\n![481220701ebf3276c284ea0a2fa17928](Java NIO之Buffer(缓冲区).resources/1E718F2D-CAEB-4378-8FDB-780BE9803BF5.png)\n利用Buffer读写数据，通常遵循四个步骤：\n\n* 把数据写入buffer\n* 调用flip\n* 从Buffer中读取数据\n* 调用buffer.clear()或者buffer.compact()\n\n当写入数据到buffer中时，buffer会记录已经写入的数据大小。当需要读数据时，通过 flip() 方法把buffer从写模式调整为读模式；在读模式下，可以读取所有已经写入的数据。\n当读取完数据后，需要清空buffer，以满足后续写入操作。清空buffer有两种方式：调用 clear() 或 compact() 方法。clear会清空整个buffer，compact则只清空已读取的数据，未被读取的数据会被移动到buffer的开始位置，写入位置则近跟着未读数据之后。\n\n**Buffer的容量，位置，上限（Buffer Capacity, Position and Limit）**\nBuffer缓冲区实质上就是一块内存，用于写入数据，也供后续再次读取数据。这块内存被NIO Buffer管理，并提供一系列的方法用于更简单的操作这块内存。\n一个Buffer有三个属性是必须掌握的，分别是：\n\n* capacity容量\n* position位置\n* limit限制\n\nposition和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。\n下面有张示例图，描诉了读写模式下position和limit的含义：\n\n![ccf1e3514f39dbc5ebc2b74818005ca0](Java NIO之Buffer(缓冲区).resources/CDDCF910-B3A2-41C1-AB22-6EAFAAD9BE35.png)\n\n>**容量（Capacity）**\n作为一块内存，buffer有一个固定的大小，叫做capacit（容量）。也就是最多只能写入容量值得字节，整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据.\n\n>**位置（Position）**\n>当写入数据到Buffer的时候需要从一个确定的位置开始，默认初始化时这个位置position为0，一旦写入了数据比如一个字节，整形数据，那么position的值就会指向数据之后的一个单元，position最大可以到capacity-1.\n>\n>当从Buffer读取数据时，也需要从一个确定的位置开始。buffer从写入模式变为读取模式时，position会归零，每次读取后，position向后移动。\n\n>**上限（Limit）**\n在写模式，limit的含义是我们所能写入的最大数据量，它等同于buffer的容量。\n\n一旦切换到读模式，limit则代表我们所能读取的最大数据量，他的值等同于写模式下position的位置。换句话说，您可以读取与写入数量相同的字节数（限制设置为写入的字节数，由位置标记）\n\n#### Buffer的常见方法\n\n![58fb3ee7569b404a67362f82a7c9296c](Java NIO之Buffer(缓冲区).resources/03F3F860-14A4-4D45-A998-313304B775E1.png)\n\n\n#### Buffer的使用方式/方法介绍\n\n**分配缓冲区（Allocating a Buffer）**\n\n为了获得缓冲区对象，我们必须首先分配一个缓冲区。在每个Buffer类中，allocate()方法用于分配缓冲区。\n下面来看看ByteBuffer分配容量为28字节的例子:\n```\nByteBuffer buf = ByteBuffer.allocate(28);\n```\n下面来看看另一个示例：CharBuffer分配空间大小为2048个字符\n```\nCharBuffer buf = CharBuffer.allocate(2048);\n```\n\n**写入数据到缓冲区（Writing Data to a Buffer）**\n\n写数据到Buffer有两种方法：\n\n从Channel中写数据到Buffer\n手动写数据到Buffer，调用put方法\n下面是一个实例，演示从Channel写数据到Buffer：\n```\n int bytesRead = inChannel.read(buf); //read into buffer.\n```\n通过put写数据：\n```\nbuf.put(127);\n```\nput方法有很多不同版本，对应不同的写数据方法。例如把数据写到特定的位置，或者把一个字节数据写入buffer。看考JavaDoc文档可以查阅的更多数据。\n\n**翻转(flip())**\n\nflip()方法可以吧Buffer从写模式切换到读模式。调用flip方法会把position归零，并设置limit为之前的position的值。 也就是说，现在position代表的是读取位置，limit标示的是已写入的数据位置。\n\n**从Buffer读取数据（Reading Data from a Buffer）**\n\n从Buffer读数据也有两种方式\n\n* 从buffer读数据到channel\n* 从buffer直接读取数据，调用get方法\n\n读取数据到channel的例子：\n```\nint bytesWritten = inChannel.write(buf);\n```\n调用get读取数据的例子：\n```\nbyte aByte = buf.get();\n```\nget也有诸多版本，对应了不同的读取方式。\n\n**rewind()**\n\nBuffer.rewind()方法将position置为0，这样我们可以重复读取buffer中的数据。limit保持不变。\n\n**clear() and compact()**\n\n一旦我们从buffer中读取完数据，需要复用buffer为下次写数据做准备。只需要调用clear（）或compact（）方法。\n如果调用的是clear()方法，position将被设回0，limit被设置成 capacity的值。换句话说，Buffer 被清空了。Buffer中的数据并未清除，只是这些标记告诉我们可以从哪里开始往Buffer里写数据。\n如果Buffer还有一些数据没有读取完，调用clear就会导致这部分数据被“遗忘”，因为我们没有标记这部分数据未读。\n针对这种情况，如果需要保留未读数据，那么可以使用compact。 因此 compact() 和 clear() 的区别就在于: 对未读数据的处理，是保留这部分数据还是一起清空 。\n\n**mark()与reset()方法**\n\n通过调用Buffer.mark()方法，可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如：\n```\nbuffer.mark();\n//call buffer.get() a couple of times, e.g. during parsing.\nbuffer.reset();  //set position back to mark.    \n```\n\n**equals() and compareTo()**\n\n可以用eqauls和compareTo比较两个buffer\n**equals():**\n\n判断两个buffer相对，需满足：\n\n* 类型相同\n* buffer中剩余字节数相同\n* 所有剩余字节相等\n\n从上面的三个条件可以看出，equals只比较buffer中的部分内容，并不会去比较每一个元素。\n**compareTo():**\ncompareTo也是比较buffer中的剩余元素，只不过这个方法适用于比较排序的：\n#### Buffer常用方法测试\n这里以ByteBuffer为例子说明抽象类Buffer的实现类的一些常见方法的使用：\n```\npackage channel;\n\nimport java.nio.ByteBuffer;\n\npublic class ByteBufferMethods {\n    public static void main(String args[]){\n        //分配缓冲区（Allocating a Buffer）\n        ByteBuffer buffer = ByteBuffer.allocate(33);\n\n        System.out.println(\"-------------Test reset-------------\");\n        //clear()方法，position将被设回0，limit被设置成 capacity的值\n        buffer.clear();\n       // 设置这个缓冲区的位置\n        buffer.position(5);\n        //将此缓冲区的标记设置在其位置。没有buffer.mark();这句话会报错\n        buffer.mark();\n        buffer.position(10);\n        System.out.println(\"before reset:      \" + buffer);\n        //将此缓冲区的位置重置为先前标记的位置。（buffer.position(5)）\n        buffer.reset();\n        System.out.println(\"after reset:       \" + buffer);\n\n        System.out.println(\"-------------Test rewind-------------\");\n        buffer.clear();\n        buffer.position(10);\n        //返回此缓冲区的限制。\n        buffer.limit(15);\n        System.out.println(\"before rewind:       \" + buffer);\n        //把position设为0，mark设为-1，不改变limit的值\n        buffer.rewind();\n        System.out.println(\"before rewind:       \" + buffer);\n\n        System.out.println(\"-------------Test compact-------------\");\n        buffer.clear();\n        buffer.put(\"abcd\".getBytes());\n        System.out.println(\"before compact:       \" + buffer);\n        System.out.println(new String(buffer.array()));\n        //limit = position;position = 0;mark = -1; 翻转，也就是让flip之后的position到limit这块区域变成之前的0到position这块，\n        //翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态\n        buffer.flip();\n        System.out.println(\"after flip:       \" + buffer);\n        //get()方法：相对读，从position位置读取一个byte，并将position+1，为下次读写作准备\n        System.out.println((char) buffer.get());\n        System.out.println((char) buffer.get());\n        System.out.println((char) buffer.get());\n        System.out.println(\"after three gets:       \" + buffer);\n        System.out.println(\"\\t\" + new String(buffer.array()));\n        //把从position到limit中的内容移到0到limit-position的区域内，position和limit的取值也分别变成limit-position、capacity。\n        // 如果先将positon设置到limit，再compact，那么相当于clear()\n        buffer.compact();\n        System.out.println(\"after compact:       \" + buffer);\n        System.out.println(\"\\t\" + new String(buffer.array()));\n\n        System.out.println(\"-------------Test get-------------\");\n        buffer = ByteBuffer.allocate(32);\n        buffer.put((byte) 'a').put((byte) 'b').put((byte) 'c').put((byte) 'd')\n                .put((byte) 'e').put((byte) 'f');\n        System.out.println(\"before flip():       \" + buffer);\n        // 转换为读取模式\n        buffer.flip();\n        System.out.println(\"before get():       \" + buffer);\n        System.out.println((char) buffer.get());\n        System.out.println(\"after get():       \" + buffer);\n        // get(index)不影响position的值\n        System.out.println((char) buffer.get(2));\n        System.out.println(\"after get(index):       \" + buffer);\n        byte[] dst = new byte[10];\n        buffer.get(dst, 0, 2);\n        System.out.println(\"after get(dst, 0, 2):       \" + buffer);\n        System.out.println(\"\\t dst:\" + new String(dst));\n        System.out.println(\"buffer now is:       \" + buffer);\n        System.out.println(\"\\t\" + new String(buffer.array()));\n\n        System.out.println(\"-------------Test put-------------\");\n        ByteBuffer bb = ByteBuffer.allocate(32);\n        System.out.println(\"before put(byte):       \" + bb);\n        System.out.println(\"after put(byte):       \" + bb.put((byte) 'z'));\n        System.out.println(\"\\t\" + bb.put(2, (byte) 'c'));\n        // put(2,(byte) 'c')不改变position的位置\n        System.out.println(\"after put(2,(byte) 'c'):       \" + bb);\n        System.out.println(\"\\t\" + new String(bb.array()));\n        // 这里的buffer是 abcdef[pos=3 lim=6 cap=32]\n        bb.put(buffer);\n        System.out.println(\"after put(buffer):       \" + bb);\n        System.out.println(\"\\t\" + new String(bb.array()));\n    }\n}\n```\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* ByteBuffer常用方法详解\n* JavaNIO易百教程\n\n参考文章：\n《Netty官网》\n>https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "Java高级特性增强/Java NIO之Channel(通道).md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n\n## Java NIO之Channel（通道）\n\n#### Buffer(缓冲区)介绍\n\n\n通常来说NIO中的所有IO都是从 Channel（通道） 开始的。\n\n* 从通道进行数据读取 ：创建一个缓冲区，然后请求通道读取数据。\n* 从通道进行数据写入 ：创建一个缓冲区，填充数据，并要求通道写入数据。\n\n数据读取和写入操作图示：\n![342194a2fdfeaf96e6051e08c9951de3](Java NIO之Channel(通道).resources/2958433B-EEAF-4D8B-98A2-39941C7C1733.png)\n\n**Java NIO Channel通道和流非常相似，主要有以下几点区别：**\n\n通道可以读也可以写，流一般来说是单向的（只能读或者写，所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流）。\n通道可以异步读写。\n通道总是基于缓冲区Buffer来读写。\n\n**Java NIO中最重要的几个Channel的实现：**\n\n* FileChannel： 用于文件的数据读写\n* DatagramChannel： 用于UDP的数据读写\n* SocketChannel： 用于TCP的数据读写，一般是客户端实现\n* ServerSocketChannel: 允许我们监听TCP链接请求，每个请求会创建会一个SocketChannel，一般是服务器实现\n\n**类层次结构：**\n下面的UML图使用Idea生成的。\n![5153431ea4cfbf8d64f746d098f8bda5](Java NIO之Channel(通道).resources/3A2E73E4-2445-4B90-93F0-0EB34EB8C82B.png)\n\n\n#### FileChannel的使用\n使用FileChannel读取数据到Buffer（缓冲区）以及利用Buffer（缓冲区）写入数据到FileChannel：\n```\npackage filechannel;\n\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\n\npublic class FileChannelTxt {\n    public static void main(String args[]) throws IOException {\n        //1.创建一个RandomAccessFile（随机访问文件）对象，\n        RandomAccessFile raf=new RandomAccessFile(\"D:\\\\niodata.txt\", \"rw\");\n        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。\n        FileChannel inChannel=raf.getChannel();\n        //2.创建一个读数据缓冲区对象\n        ByteBuffer buf=ByteBuffer.allocate(48);\n        //3.从通道中读取数据\n        int bytesRead = inChannel.read(buf);\n        //创建一个写数据缓冲区对象\n        ByteBuffer buf2=ByteBuffer.allocate(48);\n        //写入数据\n        buf2.put(\"filechannel test\".getBytes());\n        buf2.flip();\n        inChannel.write(buf);\n        while (bytesRead != -1) {\n\n            System.out.println(\"Read \" + bytesRead);\n            //Buffer有两种模式，写模式和读模式。在写模式下调用flip()之后，Buffer从写模式变成读模式。\n            buf.flip();\n           //如果还有未读内容\n            while (buf.hasRemaining()) {\n                System.out.print((char) buf.get());\n            }\n            //清空缓存区\n            buf.clear();\n            bytesRead = inChannel.read(buf);\n        }\n        //关闭RandomAccessFile（随机访问文件）对象\n        raf.close();\n    }\n}\n\n```\n运行效果：\n![93e3d051206ec5c22f1997fae7e3a143](Java NIO之Channel(通道).resources/0CC9E605-79FB-455E-AF3F-1CD41832B4A6.png)\n通过上述实例代码，我们可以大概总结出FileChannel的一般使用规则：\n>**1. 开启FileChannel**\n\n使用之前，FileChannel必须被打开 ，但是你无法直接打开FileChannel（FileChannel是抽象类）。需要通过 InputStream ， OutputStream 或 RandomAccessFile 获取FileChannel。\n我们上面的例子是通过RandomAccessFile打开FileChannel的：\n```\n//1.创建一个RandomAccessFile（随机访问文件）对象，\n        RandomAccessFile raf=new RandomAccessFile(\"D:\\\\niodata.txt\", \"rw\");\n        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。\n        FileChannel inChannel=raf.getChannel();\n```\n>**2. 从FileChannel读取数据/写入数据**\n从FileChannel中读取数据/写入数据之前首先要创建一个Buffer（缓冲区）对象，Buffer（缓冲区）对象的使用我们在上一篇文章中已经详细说明了，如果不了解的话可以看我的上一篇关于Buffer的文章。\n\n使用FileChannel的read()方法读取数据：\n```\n//2.创建一个读数据缓冲区对象\n  ByteBuffer buf=ByteBuffer.allocate(48);\n//3.从通道中读取数据\n  int bytesRead = inChannel.read(buf);\n```\n使用FileChannel的write()方法写入数据：\n```\n //创建一个写数据缓冲区对象\n   ByteBuffer buf2=ByteBuffer.allocate(48);\n //写入数据\n   buf2.put(\"filechannel test\".getBytes());\n   buf2.flip();\n   inChannel.write(buf);\n```\n> **3. 关闭FileChannel**\n\n完成使用后，FileChannel您必须关闭它。\n```\nchannel.close();   \n```\n\n#### SocketChannel和ServerSocketChannel的使用\n利用SocketChannel和ServerSocketChannel实现客户端与服务器端简单通信：\nSocketChannel 用于创建基于tcp协议的客户端对象，因为SocketChannel中不存在accept()方法，所以，它不能成为一个服务端程序。通过 connect()方法 ，SocketChannel对象可以连接到其他tcp服务器程序。\n客户端:\n```\npackage socketchannel;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SocketChannel;\n\npublic class WebClient {\n    public static void main(String[] args) throws IOException {\n        //1.通过SocketChannel的open()方法创建一个SocketChannel对象\n        SocketChannel socketChannel = SocketChannel.open();\n        //2.连接到远程服务器（连接此通道的socket）\n        socketChannel.connect(new InetSocketAddress(\"127.0.0.1\", 3333));\n        // 3.创建写数据缓存区对象\n        ByteBuffer writeBuffer = ByteBuffer.allocate(128);\n        writeBuffer.put(\"hello WebServer this is from WebClient\".getBytes());\n        writeBuffer.flip();\n        socketChannel.write(writeBuffer);\n        //创建读数据缓存区对象\n        ByteBuffer readBuffer = ByteBuffer.allocate(128);\n        socketChannel.read(readBuffer);\n        //String 字符串常量，不可变；StringBuffer 字符串变量（线程安全），可变；StringBuilder 字符串变量（非线程安全），可变\n        StringBuilder stringBuffer=new StringBuilder();\n        //4.将Buffer从写模式变为可读模式\n        readBuffer.flip();\n        while (readBuffer.hasRemaining()) {\n            stringBuffer.append((char) readBuffer.get());\n        }\n        System.out.println(\"从服务端接收到的数据：\"+stringBuffer);\n\n        socketChannel.close();\n    }\n\n}\n```\nServerSocketChannel 允许我们监听TCP链接请求，通过ServerSocketChannelImpl的 accept()方法 可以创建一个SocketChannel对象用户从客户端读/写数据。\n\n服务端：\n```\npackage socketchannel;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ServerSocketChannel;\nimport java.nio.channels.SocketChannel;\n\npublic class WebServer {\n    public static void main(String args[]) throws IOException {\n        try {\n            //1.通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象，open方法的作用：打开套接字通道\n            ServerSocketChannel ssc = ServerSocketChannel.open();\n            //2.通过ServerSocketChannel绑定ip地址和port(端口号)\n            ssc.socket().bind(new InetSocketAddress(\"127.0.0.1\", 3333));\n            //通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据\n            SocketChannel socketChannel = ssc.accept();\n            //3.创建写数据的缓存区对象\n            ByteBuffer writeBuffer = ByteBuffer.allocate(128);\n            writeBuffer.put(\"hello WebClient this is from WebServer\".getBytes());\n            writeBuffer.flip();\n            socketChannel.write(writeBuffer);\n            //创建读数据的缓存区对象\n            ByteBuffer readBuffer = ByteBuffer.allocate(128);\n            //读取缓存区数据\n            socketChannel.read(readBuffer);\n            StringBuilder stringBuffer=new StringBuilder();\n            //4.将Buffer从写模式变为可读模式\n            readBuffer.flip();\n            while (readBuffer.hasRemaining()) {\n                stringBuffer.append((char) readBuffer.get());\n            }\n            System.out.println(\"从客户端接收到的数据：\"+stringBuffer);\n            socketChannel.close();\n            ssc.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n**运行效果**\n客户端：\n![37ac5661df301bcc55f3bab690d6c3ea](Java NIO之Channel(通道).resources/6AF85EF7-83C7-48B6-A6AB-C70AD22A91D4.png)\n服务端：\n![d6b8298bd2108e3fcd6ed422cec8daa8](Java NIO之Channel(通道).resources/821A61BD-80DF-493F-99D8-4F5330211339.png)\n通过上述实例代码，我们可以大概总结出SocketChannel和ServerSocketChannel的使用的一般使用规则：\n考虑到篇幅问题，下面只给出大致步骤，不贴代码，可以结合上述实例理解。\n**客户端**\n1.通过SocketChannel连接到远程服务器\n2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据\n3.关闭SocketChannel\n**服务端**\n1.通过ServerSocketChannel 绑定ip地址和端口号\n2.通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据\n3.创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据\n4. 关闭SocketChannel和ServerSocketChannel\n\n#### DatagramChannel的使用\n\nDataGramChannel，类似于java 网络编程的DatagramSocket类；使用UDP进行网络传输， UDP是无连接，面向数据报文段的协议，对传输的数据不保证安全与完整 ；和上面介绍的SocketChannel和ServerSocketChannel的使用方法类似，所以这里就简单介绍一下如何使用。\n**1.获取DataGramChannel**\n```\n//1.通过DatagramChannel的open()方法创建一个DatagramChannel对象\n DatagramChannel datagramChannel = DatagramChannel.open();\n  //绑定一个port（端口）\n datagramChannel.bind(new InetSocketAddress(1234));\n```\n上面代码表示程序可以在1234端口接收数据报。\n\n**2.接收/发送消息**\n接收消息：\n先创建一个缓存区对象，然后通过receive方法接收消息，这个方法返回一个SocketAddress对象，表示发送消息方的地址：\n```\nByteBuffer buf = ByteBuffer.allocate(48);\nbuf.clear();\nchannel.receive(buf);\n```\n发送消息：\n由于UDP下，服务端和客户端通信并不需要建立连接，只需要知道对方地址即可发出消息，但是是否发送成功或者成功被接收到是没有保证的;发送消息通过send方法发出，改方法返回一个int值，表示成功发送的字节数：\n```\nByteBuffer buf = ByteBuffer.allocate(48);\nbuf.clear();\nbuf.put(\"datagramchannel\".getBytes());\nbuf.flip();\nint send = channel.send(buffer, new InetSocketAddress(\"localhost\",1234));\n```\n这个例子发送一串字符：“datagramchannel”到主机名为”localhost”服务器的端口1234上。\n\n#### Scatter/Gather\nChannel 提供了一种被称为 Scatter/Gather 的新功能，也称为本地矢量 I/O。Scatter/Gather 是指在多个缓冲区上实现一个简单的 I/O 操作。正确使用 Scatter / Gather可以明显提高性能。\n大多数现代操作系统都支持本地矢量I/O（native vectored I/O）操作。当您在一个通道上请求一个Scatter/Gather操作时，该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区，减少或避免了缓冲区拷贝和系统调用；\nScatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。\nScatter/Gather功能是通道(Channel)提供的  并不是Buffer。\n\n* Scatter:  从一个Channel读取的信息分散到N个缓冲区中(Buufer).\n* Gather:  将N个Buffer里面内容按照顺序发送到一个Channel.\n\n**Scattering Reads**\n\"scattering read\"是把数据从单个Channel写入到多个buffer,如下图所示：\n![820b8ed4fd205e451772c9d18e0d629f](Java NIO之Channel(通道).resources/D2633F82-0A59-488A-AEC6-AB443A3125F4.png)\n示例代码:\n```\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body   = ByteBuffer.allocate(1024);\nByteBuffer[] bufferArray = { header, body };\nchannel.read(bufferArray);\n```\nread()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后，接着写到下一个buffer中。\n举个例子，假如通道中有200个字节数据，那么header会被写入128个字节数据，body会被写入72个字节数据；\n注意：\n无论是scatter还是gather操作，都是按照buffer在数组中的顺序来依次读取或写入的；\n**Gathering Writes**\n\"gathering write\"把多个buffer的数据写入到同一个channel中，下面是示意图\n![f39ff57a4463a05cc93ae22f402e6683](Java NIO之Channel(通道).resources/19060EA5-78B2-49F1-A706-0C99F3BC51A5.png)\n示例代码：\n```\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body   = ByteBuffer.allocate(1024);\n//write data into buffers\nByteBuffer[] bufferArray = { header, body };\nchannel.write(bufferArray);\n```\nwrite()方法内部会负责把数据按顺序写入到channel中。\n注意：\n并不是所有数据都写入到通道，写入的数据要根据position和limit的值来判断，只有position和limit之间的数据才会被写入；\n举个例子，假如以上header缓冲区中有128个字节数据，但此时position=0，limit=58；那么只有下标索引为0-57的数据才会被写入到通道中.\n\n#### 通道之间的数据传输\n在Java NIO中如果一个channel是FileChannel类型的，那么他可以直接把数据传输到另一个channel。\n\n\n* **transferFrom():** transferFrom方法把数据从通道源传输到FileChannel\n* **transferTo():** transferTo方法把FileChannel数据传输到另一个channel\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* ByteBuffer常用方法详解\n* JavaNIO易百教程\n\n\n参考文章：\n《Netty官网》\n>https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "Java高级特性增强/Java NIO之Selector(选择器).md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n## Java NIO之Selector（选择器）\n\n#### Selector（选择器）介绍\nSelector一般称为选择器,当然你也可以翻译为多路复用器。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel（通道）的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。\n![e87095c49bef56cb3cce7c4529cac9ad](Java NIO之Selector(选择器).resources/C32A7750-CD6E-469A-8E9E-BEC983989522.png)\n使用Selector的好处在于:使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。\n\n\n#### Selector（选择器）的使用方法介绍\n**1. Selector的创建**\n通过调用Selector.open()方法创建一个Selector对象，如下：\n```\nSelector selector = Selector.open();\n```\n**2. 注册Channel到Selector**\n```\nchannel.configureBlocking(false);\nSelectionKey key = channel.register(selector, Selectionkey.OP_READ);\n```\n**Channel必须是非阻塞的。**\n所以FileChannel不适用Selector，因为FileChannel不能切换为非阻塞模式，更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。\nSelectableChannel抽象类 有一个 configureBlocking（） 方法用于使通道处于阻塞模式或非阻塞模式。\n```\nabstract SelectableChannel configureBlocking(boolean block)  \n```\n注意：\nSelectableChannel抽象类的configureBlocking（） 方法是由 AbstractSelectableChannel抽象类实现的，SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了 AbstractSelectableChannel抽象类 。\n大家有兴趣可以看看NIO的源码，各种抽象类和抽象类上层的抽象类。我本人暂时不准备研究NIO源码，因为还有很多事情要做，需要研究的同学可以自行看看。\nregister() 方法的第二个参数。这是一个“ interest集合 ”，意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件：\n* Connect\n* Accept\n* Read\n* Write\n\n通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为\"连接就绪\"。一个Server Socket Channel准备好接收新进入的连接称为\"接收就绪\"。一个有数据可读的通道可以说是\"读就绪\"。等待写数据的通道可以说是\"写就绪\"。\n这四种事件用SelectionKey的四个常量来表示：\nSelectionKey.OP_CONNECT\nSelectionKey.OP_ACCEPT\nSelectionKey.OP_READ\nSelectionKey.OP_WRITE\n\n如果你对不止一种事件感兴趣，使用或运算符即可，如下：\nint interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;\n\n**3. SelectionKey介绍**\n一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。\n```\nkey.attachment(); //返回SelectionKey的attachment，attachment可以在注册channel的时候指定。\nkey.channel(); // 返回该SelectionKey对应的channel。\nkey.selector(); // 返回该SelectionKey对应的Selector。\nkey.interestOps(); //返回代表需要Selector监控的IO操作的bit mask\nkey.readyOps(); // 返回一个bit mask，代表在相应channel上可以进行的IO操作。\n```\n**key.interestOps():**\n\n我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣\n```\nint interestSet = selectionKey.interestOps(); \nboolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT；\nboolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;\nboolean isInterestedInRead = interestSet & SelectionKey.OP_READ;\nboolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;\n```\n**key.readyOps()**\nready 集合是通道已经准备就绪的操作的集合。JAVA中定义以下几个方法用来检查这些操作是否就绪.\n```\n//创建ready集合的方法\nint readySet = selectionKey.readyOps();\n//检查这些操作是否就绪的方法\nkey.isAcceptable();//是否可读，是返回 true\nboolean isWritable()：//是否可写，是返回 true\nboolean isConnectable()：//是否可连接，是返回 true\nboolean isAcceptable()：//是否可接收，是返回 true\n```\n**从SelectionKey访问Channel和Selector很简单。如下：**\n```\nChannel channel = key.channel();\nSelector selector = key.selector();\nkey.attachment();\n```\n可以将一个对象或者更多信息附着到SelectionKey上，这样就能方便的识别某个给定的通道。例如，可以附加 与通道一起使用的Buffer，或是包含聚集数据的某个对象。使用方法如下：\n```\nkey.attach(theObject);\nObject attachedObj = key.attachment();\n```\n还可以在用register()方法向Selector注册Channel的时候附加对象。如：\n```\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);\n```\n**4. 从Selector中选择channel(Selecting Channels via a Selector)**\n选择器维护注册过的通道的集合，并且这种注册关系都被封装在SelectionKey当中.\n\n**Selector维护的三种类型SelectionKey集合：**\n\n* **已注册的键的集合(Registered key set)**\n\n所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回，并且可能是空的。这个已注册的键的集合不是可以直接修改的；试图这么做的话将引发java.lang.UnsupportedOperationException。\n\n\n* **已选择的键的集合(Selected key set)**\n\n所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回，并且可能是空的。这个已注册的键的集合不是可以直接修改的；试图这么做的话将引发java.lang.UnsupportedOperationException。\n\n\n* **已取消的键的集合(Cancelled key set)**\n\n已注册的键的集合的子集，这个集合包含了 cancel() 方法被调用过的键(这个键已经被无效化)，但它们还没有被注销。这个集合是选择器对象的私有成员，因而无法直接访问。\n注意：\n当键被取消（ 可以通过isValid( ) 方法来判断）时，它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消，但键会立即失效。当再次调用 select( ) 方法时（或者一个正在进行的select()调用结束时），已取消的键的集合中的被取消的键将被清理掉，并且相应的注销也将完成。通道会被注销，而新的SelectionKey将被返回。当通道关闭时，所有相关的键会自动取消（记住，一个通道可以被注册到多个选择器上）。当选择器关闭时，所有被注册到该选择器的通道都将被注销，并且相关的键将立即被无效化（取消）。一旦键被无效化，调用它的与选择相关的方法就将抛出CancelledKeyException。\n\n**select()方法介绍：**\n\n在刚初始化的Selector对象中，这三个集合都是空的。 通过Selector的select（）方法可以选择已经准备就绪的通道 （这些通道包含你感兴趣的事件）。比如你对读就绪的通道感兴趣，那么select（）方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法：\n\n* int select()：阻塞到至少有一个通道在你注册的事件上就绪了。\n* int select(long timeout)：和select()一样，但最长阻塞时间为timeout毫秒。\n* int selectNow()：非阻塞，只要有通道就绪就立刻返回。\n\nselect()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select（）调用时进入就绪的通道不会在本次调用中被记入，而在前一次select（）调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如：首次调用select()方法，如果有一个通道变成就绪状态，返回了1，若再次调用select()方法，如果另一个通道就绪了，它会再次返回1。如果对第一个就绪的channel没有做任何操作，现在就有两个就绪的通道，但在每次select()方法调用之间，只有一个通道就绪了。\n一旦调用select()方法，并且返回值不为0时，则 可以通过调用Selector的selectedKeys()方法来访问已选择键集合 。如下：\n```\nSet selectedKeys=selector.selectedKeys();\n进而可以放到和某SelectionKey关联的Selector和Channel。如下所示：\nSet selectedKeys = selector.selectedKeys();\nIterator keyIterator = selectedKeys.iterator();\nwhile(keyIterator.hasNext()) {\n    SelectionKey key = keyIterator.next();\n    if(key.isAcceptable()) {\n        // a connection was accepted by a ServerSocketChannel.\n    } else if (key.isConnectable()) {\n        // a connection was established with a remote server.\n    } else if (key.isReadable()) {\n        // a channel is ready for reading\n    } else if (key.isWritable()) {\n        // a channel is ready for writing\n    }\n    keyIterator.remove();\n}\n```\n**5. 停止选择的方法**\n选择器执行选择的过程，系统底层会依次询问每个通道是否已经就绪，这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select（）方法中阻塞的线程。\n\n\n* wakeup()方法 ：通过调用Selector对象的wakeup（）方法让处在阻塞状态的select()方法立刻返回\n该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作，那么下一次对select()方法的一次调用将立即返回。\n\n* close()方法 ：通过close（）方法关闭Selector\n该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销，所有的键将被取消，但是Channel本身并不会关闭。\n\n#### 模板代码\n一个服务端的模板代码：\n有了模板代码我们在编写程序时，大多数时间都是在模板代码中添加相应的业务代码\n```\nServerSocketChannel ssc = ServerSocketChannel.open();\nssc.socket().bind(new InetSocketAddress(\"localhost\", 8080));\nssc.configureBlocking(false);\n\nSelector selector = Selector.open();\nssc.register(selector, SelectionKey.OP_ACCEPT);\n\nwhile(true) {\n    int readyNum = selector.select();\n    if (readyNum == 0) {\n        continue;\n    }\n\n    Set<SelectionKey> selectedKeys = selector.selectedKeys();\n    Iterator<SelectionKey> it = selectedKeys.iterator();\n    \n    while(it.hasNext()) {\n        SelectionKey key = it.next();\n        \n        if(key.isAcceptable()) {\n            // 接受连接\n        } else if (key.isReadable()) {\n            // 通道可读\n        } else if (key.isWritable()) {\n            // 通道可写\n        }\n        \n        it.remove();\n    }\n}\n```\n#### 客户端与服务端简单交互实例\n服务端：\n```\npackage selector;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.ServerSocketChannel;\nimport java.nio.channels.SocketChannel;\nimport java.util.Iterator;\nimport java.util.Set;\n\npublic class WebServer {\n    public static void main(String[] args) {\n        try {\n            ServerSocketChannel ssc = ServerSocketChannel.open();\n            ssc.socket().bind(new InetSocketAddress(\"127.0.0.1\", 8000));\n            ssc.configureBlocking(false);\n\n            Selector selector = Selector.open();\n            // 注册 channel，并且指定感兴趣的事件是 Accept\n            ssc.register(selector, SelectionKey.OP_ACCEPT);\n\n            ByteBuffer readBuff = ByteBuffer.allocate(1024);\n            ByteBuffer writeBuff = ByteBuffer.allocate(128);\n            writeBuff.put(\"received\".getBytes());\n            writeBuff.flip();\n\n            while (true) {\n                int nReady = selector.select();\n                Set<SelectionKey> keys = selector.selectedKeys();\n                Iterator<SelectionKey> it = keys.iterator();\n\n                while (it.hasNext()) {\n                    SelectionKey key = it.next();\n                    it.remove();\n\n                    if (key.isAcceptable()) {\n                        // 创建新的连接，并且把连接注册到selector上，而且，\n                        // 声明这个channel只对读操作感兴趣。\n                        SocketChannel socketChannel = ssc.accept();\n                        socketChannel.configureBlocking(false);\n                        socketChannel.register(selector, SelectionKey.OP_READ);\n                    }\n                    else if (key.isReadable()) {\n                        SocketChannel socketChannel = (SocketChannel) key.channel();\n                        readBuff.clear();\n                        socketChannel.read(readBuff);\n\n                        readBuff.flip();\n                        System.out.println(\"received : \" + new String(readBuff.array()));\n                        key.interestOps(SelectionKey.OP_WRITE);\n                    }\n                    else if (key.isWritable()) {\n                        writeBuff.rewind();\n                        SocketChannel socketChannel = (SocketChannel) key.channel();\n                        socketChannel.write(writeBuff);\n                        key.interestOps(SelectionKey.OP_READ);\n                    }\n                }\n            }\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n客户端：\n```\npackage selector;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SocketChannel;\n\npublic class WebClient {\n    public static void main(String[] args) throws IOException {\n        try {\n            SocketChannel socketChannel = SocketChannel.open();\n            socketChannel.connect(new InetSocketAddress(\"127.0.0.1\", 8000));\n\n            ByteBuffer writeBuffer = ByteBuffer.allocate(32);\n            ByteBuffer readBuffer = ByteBuffer.allocate(32);\n\n            writeBuffer.put(\"hello\".getBytes());\n            writeBuffer.flip();\n\n            while (true) {\n                writeBuffer.rewind();\n                socketChannel.write(writeBuffer);\n                readBuffer.clear();\n                socketChannel.read(readBuffer);\n            }\n        } catch (IOException e) {\n        }\n    }\n}\n```\n**运行结果：**\n先运行服务端，再运行客户端，服务端会不断收到客户端发送过来的消息。\n\n![6164e7d53ea6af8d8578f1f9ae9e2d6e](Java NIO之Selector(选择器).resources/3B614359-3026-4B01-938C-605FA70D1FCD.png)\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* ByteBuffer常用方法详解\n* JavaNIO易百教程\n* https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "Java高级特性增强/Java NIO之拥抱Path和Files.md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n## Java NIO之拥抱Path和Files\n\n\n#### 文件I/O基石：Path\nJava7中文件IO发生了很大的变化，专门引入了很多新的类来取代原来的基于java.io.File的文件IO操作方式:\n```\nimport java.nio.file.DirectoryStream;\nimport java.nio.file.FileSystem;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.FileAttribute;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.nio.file.attribute.PosixFilePermissions;·\n......\n```\n我们将从下面几个方面来学习Path类:\n\n* 创建一个Path\n* File和Path之间的转换，File和URI之间的转换\n* 获取Path的相关信息\n* 移除Path中的冗余项\n\n**1 创建一个Path**\n创建Path实例可以通过 Paths工具类 的 get（）方法：\n```\n//使用绝对路径\n Path path= Paths.get(\"c:\\\\data\\\\myfile.txt\");\n//使用相对路径\nPath path = Paths.get(\"/home/jakobjenkov/myfile.txt\");\n```\n下面这种创建方式和上面等效：\n```\nPath path = FileSystems.getDefault().getPath(\"c:\\\\data\\\\myfile.txt\");\n```\n**2 File和Path之间的转换，File和URI之间的转换**\n```\n        File file = new File(\"C:/my.ini\");\n        Path p1 = file.toPath();\n        p1.toFile();\n        file.toURI();\n```\n**3 获取Path的相关信息**\n       \n ```\n        //使用Paths工具类的get()方法创建\n        Path path = Paths.get(\"D:\\\\XMind\\\\bcl-java.txt\");\n        System.out.println(\"文件名：\" + path.getFileName());\n        System.out.println(\"名称元素的数量：\" + path.getNameCount());\n        System.out.println(\"父路径：\" + path.getParent());\n        System.out.println(\"根路径：\" + path.getRoot());\n        System.out.println(\"是否是绝对路径：\" + path.isAbsolute());\n        //startsWith()方法的参数既可以是字符串也可以是Path对象\n        System.out.println(\"是否是以为给定的路径D:开始：\" + path.startsWith(\"D:\\\\\") );\n        System.out.println(\"该路径的字符串形式：\" + path.toString());\n```\n结果：\n```\n文件名：bcl-java.txt\n名称元素的数量：2\n父路径：D:\\XMind\n根路径：D:\\\n是否是绝对路径：true\n是否是以为给定的路径D:开始：true\n该路径的字符串形式：D:\\XMind\\bcl-java.txt\n```\n**4 移除冗余项**\n某些时候在我们需要处理的Path路径中可能会有一个或两个点\n\n* .表示的是当前目录\n* ..表示父目录或者说是上一级目录：\n\n下面通过实例来演示一下使用Path类的normalize()和toRealPath()方法把.和..去除。\n\n* normalize() : 返回一个路径，该路径是冗余名称元素的消除。\n* toRealPath() : 融合了toAbsolutePath()方法和normalize()方法\n```\n\n        //.表示的是当前目录\n        Path currentDir = Paths.get(\".\");\n        System.out.println(currentDir.toAbsolutePath());//输出C:\\Users\\Administrator\\NIODemo\\.\n        Path currentDir2 = Paths.get(\".\\\\NIODemo.iml\");\n        System.out.println(\"原始路径格式：\"+currentDir2.toAbsolutePath());\n        System.out.println(\"执行normalize（）方法之后：\"+currentDir2.toAbsolutePath().normalize());\n        System.out.println(\"执行toRealPath()方法之后：\"+currentDir2.toRealPath());\n        //..表示父目录或者说是上一级目录：\n        Path currentDir3 = Paths.get(\"..\");\n        System.out.println(\"原始路径格式：\"+currentDir3.toAbsolutePath());\n        System.out.println(\"执行normalize（）方法之后：\"+currentDir3.toAbsolutePath().normalize());\n        System.out.println(\"执行toRealPath()方法之后：\"+currentDir3.toRealPath());\n```\n结果：\n```\nC:\\Users\\Administrator\\NIODemo\\.\n原始路径格式：C:\\Users\\Administrator\\NIODemo\\.\\NIODemo.iml\n执行normalize（）方法之后：C:\\Users\\Administrator\\NIODemo\\NIODemo.iml\n执行toRealPath()方法之后：C:\\Users\\Administrator\\NIODemo\\NIODemo.iml\n原始路径格式：C:\\Users\\Administrator\\NIODemo\\..\n执行normalize（）方法之后：C:\\Users\\Administrator\n执行toRealPath()方法之后：C:\\Users\\Administrator\n```\n![d6a70ed9337b5e22fa34de22f36236b4](Java NIO之拥抱Path和Files.resources/1E6CACD6-76A0-4D6C-8E05-C1D9353E293A.png)\n\n#### 拥抱Files类\nJava NIO中的Files类（java.nio.file.Files）提供了多种操作文件系统中文件的方法。本节教程将覆盖大部分方法。Files类包含了很多方法，所以如果本文没有提到的你也可以直接查询JavaDoc文档。\njava.nio.file.Files类是和java.nio.file.Path相结合使用的\n\n**1 检查给定的Path在文件系统中是否存在**\n通过 Files.exists() 检测文件路径是否存在：\n```\n       Path path = Paths.get(\"D:\\\\XMind\\\\bcl-java.txt\");\n        boolean pathExists =\n                Files.exists(path,\n                        new LinkOption[]{LinkOption.NOFOLLOW_LINKS});\n        System.out.println(pathExists);//true\n```\n注意Files.exists()的第二个参数。它是一个数组，这个参数直接影响到Files.exists()如何确定一个路径是否存在。在本例中，这个数组内包含了LinkOptions.NOFOLLOW_LINKS，表示检测时不包含符号链接文件。\n\n**2 创建文件/文件夹**\n\n**创建文件：**\n通过 Files.createFile() 创建文件:\n```\n        Path target2 = Paths.get(\"C:\\\\mystuff.txt\");\n        try {\n            if(!Files.exists(target2))\n                Files.createFile(target2);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n```\n**创建文件夹：**\n\n* 通过 Files.createDirectory() 创建文件夹\n* 通过 Files.createDirectories() 创建文件夹\n\nFiles.createDirectories()会首先创建所有不存在的父目录来创建目录，而Files.createDirectory()方法只是创建目录，如果它的上级目录不存在就会报错。比如下面的程序使用Files.createDirectory() 方法创建就会报错，这是因为我的D盘下没有data文件夹，加入存在data文件夹的话则没问题。\n```\n    Path path = Paths.get(\"D://data//test\");\n    try {\n        Path newDir = Files.createDirectories(path);\n    } catch(FileAlreadyExistsException e){\n        // the directory already exists.\n    } catch (IOException e) {\n        //something else went wrong\n        e.printStackTrace();\n    }\n```\n**3 删除文件或目录**\n通过 Files.delete()方法 可以删除一个文件或目录：\n```\nPath path = Paths.get(\"data/subdir/logging-moved.properties\");\n\ntry {\n    Files.delete(path);\n} catch (IOException e) {\n    //deleting file failed\n    e.printStackTrace();\n}\n```\n**4 把一个文件从一个地址复制到另一个位置**\n通过Files.copy()方法可以吧一个文件从一个地址复制到另一个位置\n```\nPath sourcePath = Paths.get(\"data/logging.properties\");\nPath destinationPath = Paths.get(\"data/logging-copy.properties\");\n\ntry {\n    Files.copy(sourcePath, destinationPath);\n} catch(FileAlreadyExistsException e) {\n    //destination file already exists\n} catch (IOException e) {\n    //something else went wrong\n    e.printStackTrace();\n}\n```\ncopy操作还可可以强制覆盖已经存在的目标文件，只需要将上面的copy()方法改为如下格式：\n```\n    Files.copy(sourcePath, destinationPath,\n            StandardCopyOption.REPLACE_EXISTING);\n```\n**5 获取文件属性**\n```\n        Path path = Paths.get(\"D:\\\\XMind\\\\bcl-java.txt\");\n        System.out.println(Files.getLastModifiedTime(path));\n        System.out.println(Files.size(path));\n        System.out.println(Files.isSymbolicLink(path));\n        System.out.println(Files.isDirectory(path));\n        System.out.println(Files.readAttributes(path, \"*\"));\n```\n结果：\n```\n2016-05-18T08:01:44Z\n18934\nfalse\nfalse\n{lastAccessTime=2017-04-12T01:42:21.149351Z, lastModifiedTime=2016-05-18T08:01:44Z, size=18934, creationTime=2017-04-12T01:42:21.149351Z, isSymbolicLink=false, isRegularFile=true, fil\n```\n**6 遍历一个文件夹**\n```\n        Path dir = Paths.get(\"D:\\\\Java\");\n        try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)){\n            for(Path e : stream){\n                System.out.println(e.getFileName());\n            }\n        }catch(IOException e){\n\n        }\n```\n结果：\n```\napache-maven-3.5.0\nEclipse\nintellij idea\nJar\nJDK\nMarvenRespository\nMyEclipse 2017 CI\nNodejs\nRedisDesktopManager\nsolr-7.2.1\n```\n上面是遍历单个目录，它不会遍历整个目录。遍历整个目录需要使用：Files.walkFileTree().Files.walkFileTree()方法具有递归遍历目录的功能。\n\n**7 遍历整个文件目录：**\nwalkFileTree接受一个Path和FileVisitor作为参数。Path对象是需要遍历的目录，FileVistor则会在每次遍历中被调用。\nFileVisitor需要调用方自行实现，然后作为参数传入walkFileTree().FileVisitor的每个方法会在遍历过程中被调用多次。如果不需要处理每个方法，那么可以继承它的默认实现类SimpleFileVisitor，它将所有的接口做了空实现。\n```\npublic class WorkFileTree {\n    public static void main(String[] args) throws IOException{\n        Path startingDir = Paths.get(\"D:\\\\apache-tomcat-9.0.0.M17\");\n        List<Path> result = new LinkedList<Path>();\n        Files.walkFileTree(startingDir, new FindJavaVisitor(result));\n        System.out.println(\"result.size()=\" + result.size());\n    }\n\n    private static class FindJavaVisitor extends SimpleFileVisitor<Path>{\n        private List<Path> result;\n        public FindJavaVisitor(List<Path> result){\n            this.result = result;\n        }\n        @Override\n        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs){\n            if(file.toString().endsWith(\".java\")){\n                result.add(file.getFileName());\n            }\n            return FileVisitResult.CONTINUE;\n        }\n    }\n}\n```\n上面这个例子输出了我的D:\\apache-tomcat-9.0.0.M17也就是我的Tomcat安装目录下以.java结尾文件的数量。\n结果：\n```\nresult.size()=4\n```\nFiles类真的很强大，除了我讲的这些操作之外还有其他很多操作比如：读取和设置文件权限、更新文件所有者等等操作。\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* 《Java 8编程官方参考教程（第9版）》\n* Java7新特性之文件操作"
  },
  {
    "path": "Java高级特性增强/NIO概览.md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n\n## NIO概览\n\n\n#### 从Java IO入手\n先看一张网上流传的http://java.io包的类结构图：\n![3db10ad6b31d95ebfa36d39645e342fc](NIO概览.resources/1EA58812-D4D0-40FA-9860-6F6C6E103FFA.png)\n当你看到这幅图的时候，我相信，你跟我一样内心是崩溃的。\n有些人不怕枯燥，不怕寂寞，硬着头皮看源码，但是，能坚持下去全部看完的又有几个呢！\n然而，就算源码全部看完看懂，过不了几天，脑子里也会变成一团浆糊。\n因为这里的类实在太多了。可能我们反复看，反复记，也很难做到清晰明白。\n他就像是一块超级硬的骨头，怎么啃都啃不烂。\n面对这样的做法，要坚决对他说，NO。\n\n我的做法是找出他们的共性，给他们分类，只记典型，触类旁通。\n上面的图虽然有分类，但是还不够细，而且没有总结出方便记忆的规律，所以我们要重新整理和归类。\n这篇文章中，使用了两种分时给他们分组，目的是更全面的了解共性，帮助记忆。\n\n#### 分类一：按操作方式（类结构）\n\n**字节流和字符流:**\n字节流：以字节为单位，每次次读入或读出是8位数据。可以读任何类型数据。\n字符流：以字符为单位，每次次读入或读出是16位数据。其只能读取字符类型数据。\n**输出流和输入流:**\n输出流：从内存读出到文件。只能进行写操作。\n输入流：从文件读入到内存。只能进行读操作。\n注意： 这里的出和入，都是相对于系统内存而言的。\n**节点流和处理流:**\n节点流：直接与数据源相连，读入或读出。\n处理流：与节点流一块使用，在节点流的基础上，再套接一层，套接在节点流上的就是处理流。\n**为什么要有处理流？**直接使用节点流，读写不方便，为了更快的读写文件，才有了处理流。\n根据以上分类，以及jdk的说明，我们可以画出更详细的类结构图，如下:\n![08a43f0086bd0b2f2c6adbe12ba53203](NIO概览.resources/E97A1DBA-0CC4-4679-A081-B164B1645040.jpg)\n**分类说明：**\n**1） 输入字节流InputStream:**\n\nByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流，它们分别从Byte 数组、StringBuffer、和本地文件中读取数据。\n\nPipedInputStream 是从与其它线程共用的管道中读取数据。PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用，共同完成管道的读取写入操作。主要用于线程操作。\n\nDataInputStream： 将基础数据类型读取出来\n\nObjectInputStream 和所有 FilterInputStream 的子类都是装饰流（装饰器模式的主角）。\n\n**2）输出字节流OutputStream:**\n\nByteArrayOutputStream、FileOutputStream： 是两种基本的介质流，它们分别向- Byte 数组、和本地文件中写入数据。\n\nPipedOutputStream 是向与其它线程共用的管道中写入数据。\n\nDataOutputStream 将基础数据类型写入到文件中\n\nObjectOutputStream 和所有 FilterOutputStream 的子类都是装饰流。\n\n节流的输入和输出类结构图：\n![ad1daa76924b325f7f5a5b580c5d5872](NIO概览.resources/D96C7B52-7E5A-44FA-9EB3-6D146ADE7EEF.png)\n3）字符输入流Reader：\n\nFileReader、CharReader、StringReader 是三种基本的介质流，它们分在本地文件、Char 数组、String中读取数据。\n\nPipedReader：是从与其它线程共用的管道中读取数据\n\nBufferedReader ：加缓冲功能，避免频繁读写硬盘\n\nInputStreamReader： 是一个连接字节流和字符流的桥梁，它将字节流转变为字符流。\n\n4）字符输出流Writer：\n\nStringWriter:向String 中写入数据。\n\nCharArrayWriter：实现一个可用作字符输入流的字符缓冲区\n\nPipedWriter:是向与其它线程共用的管道中写入数据\n\nBufferedWriter ： 增加缓冲功能，避免频繁读写硬盘。\n\nPrintWriter 和PrintStream 将对象的格式表示打印到文本输出流。 极其类似，功能和使用也非常相似\n\nOutputStreamWriter： 是OutputStream 到Writer 转换的桥梁，它的子类FileWriter 其实就是一个实现此功能的具体类（具体可以研究一SourceCode）。功能和使用和OutputStream 极其类似，后面会有它们的对应图。\n\n字符流的输入和输出类结构图：\n![952c1fdeadfaeb2ed13a785208e0aea2](NIO概览.resources/CA9A534F-8DEF-448B-A946-3ADE41538F9D.png)\n\n#### **分类二：按操作对象**\n![2539ba1fc433a54b14cebfc79019c2ba](NIO概览.resources/8F7AD527-634A-4D4E-B31B-6E1FB35BB4EC.jpg)\n**分类说明：**\n**对文件进行操作（节点流）：**\n\n* FileInputStream（字节输入流）\n* FileOutputStream（字节输出流）\n* FileReader（字符输入流）\n* FileWriter（字符输出流）\n\n**对管道进行操作（节点流）：**\n\n* PipedInputStream（字节输入流）\n* PipedOutStream（字节输出流）\n* PipedReader（字符输入流）\n* PipedWriter（字符输出流）\n* PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用，共同完成管道的读取写入操作。主要用于线程操作。\n\n**字节/字符数组流（节点流）：**\n\n* ByteArrayInputStream\n* ByteArrayOutputStream\n* CharArrayReader\n* CharArrayWriter\n\n除了上述三种是节点流，其他都是处理流，需要跟节点流配合使用。\n\n**Buffered缓冲流（处理流）：**\n带缓冲区的处理流，缓冲区的作用的主要目的是：避免每次和硬盘打交道，提高数据访问的效率。\n\n* BufferedInputStream\n* BufferedOutputStream\n* BufferedReader\n* BufferedWriter\n\n**转化流（处理流）：**\n\n* InputStreamReader：把字节转化成字符；\n* OutputStreamWriter：把字节转化成字符。\n\n**基本类型数据流（处理流）：用于操作基本数据类型值。**\n因为平时若是我们输出一个8个字节的long类型或4个字节的float类型，那怎么办呢？可以一个字节一个字节输出，也可以把转换成字符串输出，但是这样转换费时间，若是直接输出该多好啊，因此这个数据流就解决了我们输出数据类型的困难。数据流可以直接输出float类型或long类型，提高了数据读写的效率。\n\n* DataInputStream\n* DataOutputStream\n\n**打印流（处理流）：**\n\n一般是打印到控制台，可以进行控制打印的地方。\n\n* PrintStream\n* PrintWriter\n\n**对象流（处理流）：**\n\n把封装的对象直接输出，而不是一个个在转换成字符串再输出。\n\n* ObjectInputStream，对象反序列化\n* ObjectOutputStream，对象序列化\n\n合并流（处理流）：\n* SequenceInputStream：可以认为是一个工具类，将两个或者多个输入流当成一个输入流依次读取\n\n#### 其他类：File\nFile类是对文件系统中文件以及文件夹进行封装的对象，可以通过对象的思想来操作文件和文件夹。 File类保存文件或目录的各种元数据信息，包括文件名、文件长度、最后修改时间、是否可读、获取当前文件的路径名，判断指定文件是否存在、获得当前目录中的文件列表，创建、删除文件和目录等方法。\n\n#### 其他类：RandomAccessFile\n该对象并不是流体系中的一员，其封装了字节流，同时还封装了一个缓冲区（字符数组），通过内部的指针来操作字符数组中的数据。 该对象特点：\n该对象只能操作文件，所以构造函数接收两种类型的参数：a.字符串文件路径；b.File对象。\n该对象既可以对文件进行读操作，也能进行写操作，在进行对象实例化时可指定操作模式(r,rw)。\n注意:IO中的很多内容都可以使用NIO完成，这些知识点大家知道就好，使用的话还是尽量使用NIO/AIO。\n\n\n参考文章：\n《Netty官网》\n>https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(HashMap).md",
    "content": "### **Java高级特性增强-集合框架(HashMap)**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n* * *\n## 集合框架\n#### Java中的集合框架\n\n\nArrayList/Vector\nLinkedList\nHashMap\nHashSet\nLinkedHashMap\n...\n本章内容参考引用网上的内容为主，网上有大量优质的资源，作者在这里做了整理如下：\n\n#### HashMap（基于JDK1.8）\n##### HashMap简介\n\nHashMap 主要用来存放键值对，它基于哈希表的Map接口实现，是常用的Java集合之一。\nJDK1.8 之前 HashMap 由 数组+链表 组成的，数组是 HashMap 的主体，链表则是主要为了解决哈希冲突而存在的（“拉链法”解决冲突）.JDK1.8 以后在解决哈希冲突时有了较大的变化，当链表长度大于阈值（默认为 8）时，将链表转化为红黑树，以减少搜索时间。\n\n##### 底层数据结构分析\n\nJDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值，然后通过 (n - 1) & hash 判断当前元素存放的位置（这里的 n 指的是数组的长度），如果当前位置存在元素的话，就判断该元素与要存入的元素的 hash 值以及 key 是否相同，如果相同的话，直接覆盖，不相同就通过拉链法解决冲突。\n所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。\nJDK 1.8 HashMap 的 hash 方法源码:\nJDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化，但是原理不变。\n```\nstatic final int hash(Object key) {\n      int h;\n      // key.hashCode()：返回散列值也就是hashcode\n      // ^ ：按位异或\n      // >>>:无符号右移，忽略符号位，空位都以0补齐\n      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);\n  }\n```\n对比一下 JDK1.7的 HashMap 的 hash 方法源码.\n```\nstatic int hash(int h) {\n    // This function ensures that hashCodes that differ only by\n    // constant multiples at each bit position have a bounded\n    // number of collisions (approximately 8 at default load factor).\n\n    h ^= (h >>> 20) ^ (h >>> 12);\n    return h ^ (h >>> 7) ^ (h >>> 4);\n}\n```\n相比于 JDK1.8 的 hash 方法 ，JDK 1.7 的 hash 方法的性能会稍差一点点，因为毕竟扰动了 4 次。\n\n所谓 “拉链法” 就是：将链表和数组相结合。也就是说创建一个链表数组，数组中每一格就是一个链表。若遇到哈希冲突，则将冲突的值加到链表中即可。\n\n![06e4f7c0144706690bae15e7d5ceab61](大数据成神之路-Java高级特性增强(HashMap).resources/6EBF1755-C6EA-48A3-A99F-55D598EDDFD2.png)\nJDK1.8之后\n相比于之前的版本，jdk1.8在解决哈希冲突时有了较大的变化，当链表长度大于阈值（默认为8）时，将链表转化为红黑树，以减少搜索时间。\n![6a14a1fe10e977fd7221ffa11f42dcc1](大数据成神之路-Java高级特性增强(HashMap).resources/435C0F08-CE65-413F-8D3A-EE5B20EDCA0D.jpg)\n\n类的属性：\n```\npublic class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {\n    // 序列号\n    private static final long serialVersionUID = 362498820763181265L;    \n    // 默认的初始容量是16\n    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   \n    // 最大容量\n    static final int MAXIMUM_CAPACITY = 1 << 30; \n    // 默认的填充因子\n    static final float DEFAULT_LOAD_FACTOR = 0.75f;\n    // 当桶(bucket)上的结点数大于这个值时会转成红黑树\n    static final int TREEIFY_THRESHOLD = 8; \n    // 当桶(bucket)上的结点数小于这个值时树转链表\n    static final int UNTREEIFY_THRESHOLD = 6;\n    // 桶中结构转化为红黑树对应的table的最小大小\n    static final int MIN_TREEIFY_CAPACITY = 64;\n    // 存储元素的数组，总是2的幂次倍\n    transient Node<k,v>[] table; \n    // 存放具体元素的集\n    transient Set<map.entry<k,v>> entrySet;\n    // 存放元素的个数，注意这个不等于数组的长度。\n    transient int size;\n    // 每次扩容和更改map结构的计数器\n    transient int modCount;   \n    // 临界值 当实际大小(容量*填充因子)超过临界值时，会进行扩容\n    int threshold;\n    // 填充因子\n    final float loadFactor;\n}\n```\n**loadFactor加载因子**\n\nloadFactor加载因子是控制数组存放数据的疏密程度，loadFactor越趋近于1，那么 数组中存放的数据(entry)也就越多，也就越密，也就是会让链表的长度增加，load Factor越小，也就是趋近于0，\n\nloadFactor太大导致查找元素效率低，太小导致数组的利用率低，存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。\n\n给定的默认容量为 16，负载因子为 0.75。Map 在使用过程中不断的往里面存放数据，当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容，而扩容这个过程涉及到 rehash、复制数据等操作，所以非常消耗性能。\n\n**threshold**\n\nthreshold = capacity * loadFactor，当Size>=threshold的时候，那么就要考虑对数组的扩增了，也就是说，这个的意思就是 衡量数组是否需要扩增的一个标准。\n\n**Node节点类源码:**\n```\n// 继承自 Map.Entry<K,V>\nstatic class Node<K,V> implements Map.Entry<K,V> {\n       final int hash;// 哈希值，存放元素到hashmap中时用来与其他元素hash值比较\n       final K key;//键\n       V value;//值\n       // 指向下一个节点\n       Node<K,V> next;\n       Node(int hash, K key, V value, Node<K,V> next) {\n            this.hash = hash;\n            this.key = key;\n            this.value = value;\n            this.next = next;\n        }\n        public final K getKey()        { return key; }\n        public final V getValue()      { return value; }\n        public final String toString() { return key + \"=\" + value; }\n        // 重写hashCode()方法\n        public final int hashCode() {\n            return Objects.hashCode(key) ^ Objects.hashCode(value);\n        }\n\n        public final V setValue(V newValue) {\n            V oldValue = value;\n            value = newValue;\n            return oldValue;\n        }\n        // 重写 equals() 方法\n        public final boolean equals(Object o) {\n            if (o == this)\n                return true;\n            if (o instanceof Map.Entry) {\n                Map.Entry<?,?> e = (Map.Entry<?,?>)o;\n                if (Objects.equals(key, e.getKey()) &&\n                    Objects.equals(value, e.getValue()))\n                    return true;\n            }\n            return false;\n        }\n}\n```\n**树节点类源码:**\n```\nstatic final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {\n        TreeNode<K,V> parent;  // 父\n        TreeNode<K,V> left;    // 左\n        TreeNode<K,V> right;   // 右\n        TreeNode<K,V> prev;    // needed to unlink next upon deletion\n        boolean red;           // 判断颜色\n        TreeNode(int hash, K key, V val, Node<K,V> next) {\n            super(hash, key, val, next);\n        }\n        // 返回根节点\n        final TreeNode<K,V> root() {\n            for (TreeNode<K,V> r = this, p;;) {\n                if ((p = r.parent) == null)\n                    return r;\n                r = p;\n       }\n```\n\n##### HashMap源码分析\n**构造方法**\n![3b09ddf77229ea3bb9d4c70e64c5c6a0](大数据成神之路-Java高级特性增强(HashMap).resources/D02AB301-FD17-40DF-9B99-7B6C6911D4F9.jpg)\n\n```\n// 默认构造函数。\n    public More ...HashMap() {\n        this.loadFactor = DEFAULT_LOAD_FACTOR; // all   other fields defaulted\n     }\n     \n     // 包含另一个“Map”的构造函数\n     public More ...HashMap(Map<? extends K, ? extends V> m) {\n         this.loadFactor = DEFAULT_LOAD_FACTOR;\n         putMapEntries(m, false);//下面会分析到这个方法\n     }\n     \n     // 指定“容量大小”的构造函数\n     public More ...HashMap(int initialCapacity) {\n         this(initialCapacity, DEFAULT_LOAD_FACTOR);\n     }\n     \n     // 指定“容量大小”和“加载因子”的构造函数\n     public More ...HashMap(int initialCapacity, float loadFactor) {\n         if (initialCapacity < 0)\n             throw new IllegalArgumentException(\"Illegal initial capacity: \" + initialCapacity);\n         if (initialCapacity > MAXIMUM_CAPACITY)\n             initialCapacity = MAXIMUM_CAPACITY;\n         if (loadFactor <= 0 || Float.isNaN(loadFactor))\n             throw new IllegalArgumentException(\"Illegal load factor: \" + loadFactor);\n         this.loadFactor = loadFactor;\n         this.threshold = tableSizeFor(initialCapacity);\n     }\n```\nputMapEntries方法：\n```\nfinal void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {\n    int s = m.size();\n    if (s > 0) {\n        // 判断table是否已经初始化\n        if (table == null) { // pre-size\n            // 未初始化，s为m的实际元素个数\n            float ft = ((float)s / loadFactor) + 1.0F;\n            int t = ((ft < (float)MAXIMUM_CAPACITY) ?\n                    (int)ft : MAXIMUM_CAPACITY);\n            // 计算得到的t大于阈值，则初始化阈值\n            if (t > threshold)\n                threshold = tableSizeFor(t);\n        }\n        // 已初始化，并且m元素个数大于阈值，进行扩容处理\n        else if (s > threshold)\n            resize();\n        // 将m中的所有元素添加至HashMap中\n        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {\n            K key = e.getKey();\n            V value = e.getValue();\n            putVal(hash(key), key, value, false, evict);\n        }\n    }\n}\n```\n**put方法**\nHashMap只提供了put用于添加元素，putVal方法只是给put方法调用的一个方法，并没有提供给用户使用。\n\n对putVal方法添加元素的分析如下：\n\n①如果定位到的数组位置没有元素 就直接插入。\n②如果定位到的数组位置有元素就和要插入的key比较，如果key相同就直接覆盖，如果key不相同，就判断p是否是一个树节点，如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入。\n![4bead5208e185a1bbac5ccdfe2fa1975](大数据成神之路-Java高级特性增强(HashMap).resources/97F816CA-9593-4F26-A189-38BEEF2FEE24.png)\n\n```\npublic V put(K key, V value) {\n    return putVal(hash(key), key, value, false, true);\n}\n\nfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent,\n                   boolean evict) {\n    Node<K,V>[] tab; Node<K,V> p; int n, i;\n    // table未初始化或者长度为0，进行扩容\n    if ((tab = table) == null || (n = tab.length) == 0)\n        n = (tab = resize()).length;\n    // (n - 1) & hash 确定元素存放在哪个桶中，桶为空，新生成结点放入桶中(此时，这个结点是放在数组中)\n    if ((p = tab[i = (n - 1) & hash]) == null)\n        tab[i] = newNode(hash, key, value, null);\n    // 桶中已经存在元素\n    else {\n        Node<K,V> e; K k;\n        // 比较桶中第一个元素(数组中的结点)的hash值相等，key相等\n        if (p.hash == hash &&\n            ((k = p.key) == key || (key != null && key.equals(k))))\n                // 将第一个元素赋值给e，用e来记录\n                e = p;\n        // hash值不相等，即key不相等；为红黑树结点\n        else if (p instanceof TreeNode)\n            // 放入树中\n            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);\n        // 为链表结点\n        else {\n            // 在链表最末插入结点\n            for (int binCount = 0; ; ++binCount) {\n                // 到达链表的尾部\n                if ((e = p.next) == null) {\n                    // 在尾部插入新结点\n                    p.next = newNode(hash, key, value, null);\n                    // 结点数量达到阈值，转化为红黑树\n                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st\n                        treeifyBin(tab, hash);\n                    // 跳出循环\n                    break;\n                }\n                // 判断链表中结点的key值与插入的元素的key值是否相等\n                if (e.hash == hash &&\n                    ((k = e.key) == key || (key != null && key.equals(k))))\n                    // 相等，跳出循环\n                    break;\n                // 用于遍历桶中的链表，与前面的e = p.next组合，可以遍历链表\n                p = e;\n            }\n        }\n        // 表示在桶中找到key值、hash值与插入元素相等的结点\n        if (e != null) { \n            // 记录e的value\n            V oldValue = e.value;\n            // onlyIfAbsent为false或者旧值为null\n            if (!onlyIfAbsent || oldValue == null)\n                //用新值替换旧值\n                e.value = value;\n            // 访问后回调\n            afterNodeAccess(e);\n            // 返回旧值\n            return oldValue;\n        }\n    }\n    // 结构性修改\n    ++modCount;\n    // 实际大小大于阈值则扩容\n    if (++size > threshold)\n        resize();\n    // 插入后回调\n    afterNodeInsertion(evict);\n    return null;\n} \n```\n我们再来对比一下 JDK1.7 put方法的代码\n\n对于put方法的分析如下：\n\n①如果定位到的数组位置没有元素 就直接插入。\n②如果定位到的数组位置有元素，遍历以这个元素为头结点的链表，依次和插入的key比较，如果key相同就直接覆盖，不同就采用头插法插入元素。\n\n```\npublic V put(K key, V value)\n    if (table == EMPTY_TABLE) { \n    inflateTable(threshold); \n}  \n    if (key == null)\n        return putForNullKey(value);\n    int hash = hash(key);\n    int i = indexFor(hash, table.length);\n    for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 先遍历\n        Object k;\n        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {\n            V oldValue = e.value;\n            e.value = value;\n            e.recordAccess(this);\n            return oldValue; \n        }\n    }\n\n    modCount++;\n    addEntry(hash, key, value, i);  // 再插入\n    return null;\n}\n```\n\n**get方法**\n```\npublic V get(Object key) {\n    Node<K,V> e;\n    return (e = getNode(hash(key), key)) == null ? null : e.value;\n}\n\nfinal Node<K,V> getNode(int hash, Object key) {\n    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;\n    if ((tab = table) != null && (n = tab.length) > 0 &&\n        (first = tab[(n - 1) & hash]) != null) {\n        // 数组元素相等\n        if (first.hash == hash && // always check first node\n            ((k = first.key) == key || (key != null && key.equals(k))))\n            return first;\n        // 桶中不止一个节点\n        if ((e = first.next) != null) {\n            // 在树中get\n            if (first instanceof TreeNode)\n                return ((TreeNode<K,V>)first).getTreeNode(hash, key);\n            // 在链表中get\n            do {\n                if (e.hash == hash &&\n                    ((k = e.key) == key || (key != null && key.equals(k))))\n                    return e;\n            } while ((e = e.next) != null);\n        }\n    }\n    return null;\n}\n```\n**resize方法**\n进行扩容，会伴随着一次重新hash分配，并且会遍历hash表中所有的元素，是非常耗时的。在编写程序中，要尽量避免resize。\n```\nfinal Node<K,V>[] resize() {\n    Node<K,V>[] oldTab = table;\n    int oldCap = (oldTab == null) ? 0 : oldTab.length;\n    int oldThr = threshold;\n    int newCap, newThr = 0;\n    if (oldCap > 0) {\n        // 超过最大值就不再扩充了，就只好随你碰撞去吧\n        if (oldCap >= MAXIMUM_CAPACITY) {\n            threshold = Integer.MAX_VALUE;\n            return oldTab;\n        }\n        // 没超过最大值，就扩充为原来的2倍\n        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)\n            newThr = oldThr << 1; // double threshold\n    }\n    else if (oldThr > 0) // initial capacity was placed in threshold\n        newCap = oldThr;\n    else { \n        signifies using defaults\n        newCap = DEFAULT_INITIAL_CAPACITY;\n        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);\n    }\n    // 计算新的resize上限\n    if (newThr == 0) {\n        float ft = (float)newCap * loadFactor;\n        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);\n    }\n    threshold = newThr;\n    @SuppressWarnings({\"rawtypes\",\"unchecked\"})\n        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];\n    table = newTab;\n    if (oldTab != null) {\n        // 把每个bucket都移动到新的buckets中\n        for (int j = 0; j < oldCap; ++j) {\n            Node<K,V> e;\n            if ((e = oldTab[j]) != null) {\n                oldTab[j] = null;\n                if (e.next == null)\n                    newTab[e.hash & (newCap - 1)] = e;\n                else if (e instanceof TreeNode)\n                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);\n                else { \n                    Node<K,V> loHead = null, loTail = null;\n                    Node<K,V> hiHead = null, hiTail = null;\n                    Node<K,V> next;\n                    do {\n                        next = e.next;\n                        // 原索引\n                        if ((e.hash & oldCap) == 0) {\n                            if (loTail == null)\n                                loHead = e;\n                            else\n                                loTail.next = e;\n                            loTail = e;\n                        }\n                        // 原索引+oldCap\n                        else {\n                            if (hiTail == null)\n                                hiHead = e;\n                            else\n                                hiTail.next = e;\n                            hiTail = e;\n                        }\n                    } while ((e = next) != null);\n                    // 原索引放到bucket里\n                    if (loTail != null) {\n                        loTail.next = null;\n                        newTab[j] = loHead;\n                    }\n                    // 原索引+oldCap放到bucket里\n                    if (hiTail != null) {\n                        hiTail.next = null;\n                        newTab[j + oldCap] = hiHead;\n                    }\n                }\n            }\n        }\n    }\n    return newTab;\n}\n```\n\n##### HashMap常用方法测试\n\n```\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Set;\n\npublic class HashMapDemo {\n\n    public static void main(String[] args) {\n        HashMap<String, String> map = new HashMap<String, String>();\n        // 键不能重复，值可以重复\n        map.put(\"san\", \"张三\");\n        map.put(\"si\", \"李四\");\n        map.put(\"wu\", \"王五\");\n        map.put(\"wang\", \"老王\");\n        map.put(\"wang\", \"老王2\");// 老王被覆盖\n        map.put(\"lao\", \"老王\");\n        System.out.println(\"-------直接输出hashmap:-------\");\n        System.out.println(map);\n        /**\n         * 遍历HashMap\n         */\n        // 1.获取Map中的所有键\n        System.out.println(\"-------foreach获取Map中所有的键:------\");\n        Set<String> keys = map.keySet();\n        for (String key : keys) {\n            System.out.print(key+\"  \");\n        }\n        System.out.println();//换行\n        // 2.获取Map中所有值\n        System.out.println(\"-------foreach获取Map中所有的值:------\");\n        Collection<String> values = map.values();\n        for (String value : values) {\n            System.out.print(value+\"  \");\n        }\n        System.out.println();//换行\n        // 3.得到key的值的同时得到key所对应的值\n        System.out.println(\"-------得到key的值的同时得到key所对应的值:-------\");\n        Set<String> keys2 = map.keySet();\n        for (String key : keys2) {\n            System.out.print(key + \"：\" + map.get(key)+\"   \");\n\n        }\n        /**\n         * 另外一种不常用的遍历方式\n         */\n        // 当我调用put(key,value)方法的时候，首先会把key和value封装到\n        // Entry这个静态内部类对象中，把Entry对象再添加到数组中，所以我们想获取\n        // map中的所有键值对，我们只要获取数组中的所有Entry对象，接下来\n        // 调用Entry对象中的getKey()和getValue()方法就能获取键值对了\n        Set<java.util.Map.Entry<String, String>> entrys = map.entrySet();\n        for (java.util.Map.Entry<String, String> entry : entrys) {\n            System.out.println(entry.getKey() + \"--\" + entry.getValue());\n        }\n        \n        /**\n         * HashMap其他常用方法\n         */\n        System.out.println(\"after map.size()：\"+map.size());\n        System.out.println(\"after map.isEmpty()：\"+map.isEmpty());\n        System.out.println(map.remove(\"san\"));\n        System.out.println(\"after map.remove()：\"+map);\n        System.out.println(\"after map.get(si)：\"+map.get(\"si\"));\n        System.out.println(\"after map.containsKey(si)：\"+map.containsKey(\"si\"));\n        System.out.println(\"after containsValue(李四)：\"+map.containsValue(\"李四\"));\n        System.out.println(map.replace(\"si\", \"李四2\"));\n        System.out.println(\"after map.replace(si, 李四2):\"+map);\n    }\n\n}\n```\n\n\n\n-----------\n**参考文章和书籍：**\n《Effective Java》\n感谢以下作者：\nhttps://www.cnblogs.com/skywang12345/p/3308556.html\nhttps://crossoverjie.top/JCSprout/#/collections/ArrayList\nhttps://github.com/Snailclimb/JavaGuide/blob/master/Java%E7%9B%B8%E5%85%B3/ArrayList.md\nhttps://blog.csdn.net/qq_34337272/article/details/79680771\nhttps://www.jianshu.com/p/a5f99f25329a\nhttps://www.jianshu.com/p/506c1e38a922"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(HashSet).md",
    "content": "### **Java高级特性增强-集合框架(HashSet)**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n* * *\n## 集合框架\n#### Java中的集合框架\n\n\nArrayList/Vector\nLinkedList\nHashMap\nHashSet\nLinkedHashMap\n...\n本章内容参考引用网上的内容为主，网上有大量优质的资源，作者在这里做了整理如下：\n\n#### HashSet\n##### HashSet简介\n\nHashSet 是一个不允许存储重复元素的集合，它的实现比较简单，只要理解了 HashMap，HashSet就水到渠成了。\n\n![77c391135721e3ab98ac61791046d6bc](大数据成神之路-Java高级特性增强(HashSet).resources/8C932B6E-3C26-40E7-B797-EAAE2194E5BF.jpg)\n从图中可以看出：\n1. HashSet继承于AbstractSet，并且实现了Set接口。\n2. HashSet的本质是一个\"没有重复元素\"的集合，它是通过HashMap实现的。HashSet中含有一个\"HashMap类型的成员变量\"map，HashSet的操作函数，实际上都是通过map实现的。\n\n##### 成员变量\n首先了解下 HashSet 的成员变量:\n```\n private transient HashMap<E,Object> map;\n\n    // Dummy value to associate with an Object in the backing Map\n    private static final Object PRESENT = new Object();\n```\n发现主要就两个变量:\n\nmap: 用于存放最终数据的。\nPRESENT: 是所有写入 map 的 value 值。\n\n##### 构造函数\n\n```\n    public HashSet() {\n        map = new HashMap<>();\n    }\n\n    public HashSet(int initialCapacity, float loadFactor) {\n        map = new HashMap<>(initialCapacity, loadFactor);\n    }    \n```\n构造函数很简单，利用了HashMap初始化了map。\n\n**add**\n```\npublic boolean add(E e) {\n        return map.put(e, PRESENT)==null;\n    }\n```\n比较关键的就是这个add()方法。可以看出它是将存放的对象当做了HashMap 的健,value都是相同的PRESENT。由于HashMap的key是不能重复的,所以每当有重复的值写入到HashSet时,value会被覆盖,但key不会受到影响,这样就保证了HashSet中只能存放不重复的元素。\nHashSet的原理比较简单,几乎全部借助于HashMap来实现的。\n\n\n\n-----------\n**参考书籍：**\n《Effective Java》\n"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(LinkedHashMap).md",
    "content": "### **Java高级特性增强-集合框架(LinkedHashMap)**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n* * *\n## 集合框架\n#### Java中的集合框架\n\n\nArrayList/Vector\nLinkedList\nHashMap\nHashSet\nLinkedHashMap\n...\n本章内容参考引用网上的内容为主，网上有大量优质的资源，作者在这里做了整理如下：\n\n#### LinkedHashMap\n##### LinkedHashMap底层分析\n\n众所周知HashMap是一个无序的Map,因为每次根据key的hashcode映射到Entry数组上,所以遍历出来的顺序并不是写入的顺序。\n因此JDK推出一个基于HashMap但具有顺序的LinkedHashMap来解决有排序需求的场景。\n它的底层是继承于HashMap实现的,由一个双向链表所构成。\nLinkedHashMap的排序方式有两种：\n根据写入顺序排序。\n根据访问顺序排序。\n其中根据访问顺序排序时,每次get都会将访问的值移动到链表末尾,这样重复操作就能得到一个按照访问顺序排序的链表。\n\n##### 数据结构\n\n```\n @Test\n    public void test(){\n        Map<String, Integer> map = new LinkedHashMap<String, Integer>();\n        map.put(\"1\",1) ;\n        map.put(\"2\",2) ;\n        map.put(\"3\",3) ;\n        map.put(\"4\",4) ;\n        map.put(\"5\",5) ;\n        System.out.println(map.toString());\n\n    }\n```\n调试可以看到 map 的组成：\n![fbafa65540e4fca96fa09fdc5b0db83b](大数据成神之路-Java高级特性增强(LinkedHashMap).resources/2BC36CA6-D029-4249-A984-86F29FE10381.jpg)\n打开源码可以看到：\n```\n    /**\n     * The head of the doubly linked list.\n     */\n    private transient Entry<K,V> header;\n\n    /**\n     * The iteration ordering method for this linked hash map: <tt>true</tt>\n     * for access-order, <tt>false</tt> for insertion-order.\n     *\n     * @serial\n     */\n    private final boolean accessOrder;\n\n    private static class Entry<K,V> extends HashMap.Entry<K,V> {\n        // These fields comprise the doubly linked list used for iteration.\n        Entry<K,V> before, after;\n\n        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {\n            super(hash, key, value, next);\n        }\n    }  \n```\n其中 Entry 继承于 HashMap 的 Entry，并新增了上下节点的指针，也就形成了双向链表。\n还有一个 header 的成员变量，是这个双向链表的头结点。\n上边的 demo 总结成一张图如下：\n![b4bdc740a7b9d5820b9e7960aaf44bec](大数据成神之路-Java高级特性增强(LinkedHashMap).resources/A9332FA3-2758-40CD-95DC-2A2BAC724F73.jpg)\n第一个类似于 HashMap 的结构，利用 Entry 中的 next 指针进行关联。\n\n下边则是 LinkedHashMap 如何达到有序的关键。\n\n就是利用了头节点和其余的各个节点之间通过 Entry 中的 after 和 before 指针进行关联。\n\n其中还有一个 accessOrder 成员变量，默认是 false，默认按照插入顺序排序，为 true 时按照访问顺序排序，也可以调用:\n```\npublic LinkedHashMap(int initialCapacity,\n                         float loadFactor,\n                         boolean accessOrder) {\n        super(initialCapacity, loadFactor);\n        this.accessOrder = accessOrder;\n}\n```\n这个构造方法可以显示的传入 accessOrder。\n\n##### 构造方法\nLinkedHashMap 的构造方法:\n```\n     public LinkedHashMap() {\n        super();\n        accessOrder = false;\n    }\n```\n其实就是调用的 HashMap 的构造方法:\nHashMap 实现:\n```\npublic HashMap(int initialCapacity, float loadFactor) {\n        if (initialCapacity < 0)\n            throw new IllegalArgumentException(\"Illegal initial capacity: \" +\n                                               initialCapacity);\n        if (initialCapacity > MAXIMUM_CAPACITY)\n            initialCapacity = MAXIMUM_CAPACITY;\n        if (loadFactor <= 0 || Float.isNaN(loadFactor))\n            throw new IllegalArgumentException(\"Illegal load factor: \" +\n                                               loadFactor);\n\n        this.loadFactor = loadFactor;\n        threshold = initialCapacity;\n        //HashMap 只是定义了改方法，具体实现交给了 LinkedHashMap\n        init();\n    }\n```\n可以看到里面有一个空的 init(), 具体是由 LinkedHashMap 来实现的:\n```\n@Override\n    void init() {\n        header = new Entry<>(-1, null, null, null);\n        header.before = header.after = header;\n    }\n```\n其实也就是对 header 进行了初始化。\n\n##### put() 方法\n看 LinkedHashMap 的 put() 方法之前先看看 HashMap 的 put 方法:\n```\npublic V put(K key, V value) {\n        if (table == EMPTY_TABLE) {\n            inflateTable(threshold);\n        }\n        if (key == null)\n            return putForNullKey(value);\n        int hash = hash(key);\n        int i = indexFor(hash, table.length);\n        for (Entry<K,V> e = table[i]; e != null; e = e.next) {\n            Object k;\n            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {\n                V oldValue = e.value;\n                e.value = value;\n                //空实现，交给 LinkedHashMap 自己实现\n                e.recordAccess(this);\n                return oldValue;\n            }\n        }\n\n        modCount++;\n        // LinkedHashMap 对其重写\n        addEntry(hash, key, value, i);\n        return null;\n    }\n\n    // LinkedHashMap 对其重写\n    void addEntry(int hash, K key, V value, int bucketIndex) {\n        if ((size >= threshold) && (null != table[bucketIndex])) {\n            resize(2 * table.length);\n            hash = (null != key) ? hash(key) : 0;\n            bucketIndex = indexFor(hash, table.length);\n        }\n\n        createEntry(hash, key, value, bucketIndex);\n    }\n\n    // LinkedHashMap 对其重写\n    void createEntry(int hash, K key, V value, int bucketIndex) {\n        Entry<K,V> e = table[bucketIndex];\n        table[bucketIndex] = new Entry<>(hash, key, value, e);\n        size++;\n    }      \n```\n主体的实现都是借助于 HashMap 来完成的，只是对其中的 recordAccess(), addEntry(), createEntry() 进行了重写。\nLinkedHashMap 的实现：\n```\n//就是判断是否是根据访问顺序排序，如果是则需要将当前这个 Entry 移动到链表的末尾\n        void recordAccess(HashMap<K,V> m) {\n            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;\n            if (lm.accessOrder) {\n                lm.modCount++;\n                remove();\n                addBefore(lm.header);\n            }\n        }\n\n\n    //调用了 HashMap 的实现，并判断是否需要删除最少使用的 Entry(默认不删除)    \n    void addEntry(int hash, K key, V value, int bucketIndex) {\n        super.addEntry(hash, key, value, bucketIndex);\n\n        // Remove eldest entry if instructed\n        Entry<K,V> eldest = header.after;\n        if (removeEldestEntry(eldest)) {\n            removeEntryForKey(eldest.key);\n        }\n    }\n\n    void createEntry(int hash, K key, V value, int bucketIndex) {\n        HashMap.Entry<K,V> old = table[bucketIndex];\n        Entry<K,V> e = new Entry<>(hash, key, value, old);\n        //就多了这一步，将新增的 Entry 加入到 header 双向链表中\n        table[bucketIndex] = e;\n        e.addBefore(header);\n        size++;\n    }\n\n        //写入到双向链表中\n        private void addBefore(Entry<K,V> existingEntry) {\n            after  = existingEntry;\n            before = existingEntry.before;\n            before.after = this;\n            after.before = this;\n        }  \n```\n##### get方法\nLinkedHashMap 的 get() 方法也重写了:\n```\n public V get(Object key) {\n        Entry<K,V> e = (Entry<K,V>)getEntry(key);\n        if (e == null)\n            return null;\n\n        //多了一个判断是否是按照访问顺序排序，是则将当前的 Entry 移动到链表头部。   \n        e.recordAccess(this);\n        return e.value;\n    }\n\n    void recordAccess(HashMap<K,V> m) {\n        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;\n        if (lm.accessOrder) {\n            lm.modCount++;\n\n            //删除\n            remove();\n            //添加到头部\n            addBefore(lm.header);\n        }\n    }\n\n```\nclear() 清空就要比较简单了：\n```\n//只需要把指针都指向自己即可，原本那些 Entry 没有引用之后就会被 JVM 自动回收。\n    public void clear() {\n        super.clear();\n        header.before = header.after = header;\n    }\n```\n总的来说 LinkedHashMap 其实就是对 HashMap 进行了拓展，使用了双向链表来保证了顺序性。\n因为是继承与 HashMap 的，所以一些 HashMap 存在的问题 LinkedHashMap 也会存在，比如不支持并发等。\n\n\n-----------\n**参考书籍：**\n《Effective Java》\nhttps://www.jianshu.com/p/eeffc764f231\nhttps://www.jianshu.com/p/83648fa22c4c\nhttps://crossoverjie.top/JCSprout/#/collections/LinkedHashMap\n"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(Synchronized关键字).md",
    "content": "### **Java高级特性增强-Synchronized**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n* * *\n## Synchronized关键字\n\n* * *\n\n\n#### 简介\nJava并发编程这个领域中synchronized关键字一直都是元老级的角色，很久之前很多人都会称它为“重量级锁”。但是，在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后变得在某些情况下并不是那么重了。\n\n#### 变量安全性\n“非线程安全”问题存在于“实例变量”中，如果是方法内部的私有变量，则不存在“非线程安全”问题，所得结果也就是“线程安全”的了。\n\n如果两个线程同时操作对象中的实例变量，则会出现“非线程安全”，解决办法就是在方法前加上synchronized关键字即可。\n\n#### Synchronized的使用\n**修饰代码块**\n```\n/**\n * 同步线程\n */\nclass SyncThread implements Runnable {\n   private static int count;\n \n   public SyncThread() {\n      count = 0;\n   }\n \n   public  void run() {\n      synchronized(this) {\n         for (int i = 0; i < 5; i++) {\n            try {\n               System.out.println(Thread.currentThread().getName() + \":\" + (count++));\n               Thread.sleep(100);\n            } catch (InterruptedException e) {\n               e.printStackTrace();\n            }\n         }\n      }\n   }\n \n   public int getCount() {\n      return count;\n   }\n}\nSyncThread的调用：\nSyncThread syncThread = new SyncThread();\nThread thread1 = new Thread(syncThread, \"SyncThread1\");\nThread thread2 = new Thread(syncThread, \"SyncThread2\");\nthread1.start();\nthread2.start();\n\n结果如下：\n\nSyncThread1:0\nSyncThread1:1\nSyncThread1:2\nSyncThread1:3\nSyncThread1:4\nSyncThread2:5\nSyncThread2:6\nSyncThread2:7\nSyncThread2:8\nSyncThread2:9\n```\n当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时，在同一时刻只能有一个线程得到执行，另一个线程受阻塞，必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的，因为在执行synchronized代码块时会锁定当前的对象，只有执行完该代码块才能释放该对象锁，下一个线程才能执行并锁定该对象。\n我们再把SyncThread的调用稍微改一下：\n```\nThread thread1 = new Thread(new SyncThread(), \"SyncThread1\");\nThread thread2 = new Thread(new SyncThread(), \"SyncThread2\");\nthread1.start();\nthread2.start();\n```\n\n结果如下：\n```\nSyncThread1:0\nSyncThread2:1\nSyncThread1:2\nSyncThread2:3\nSyncThread1:4\nSyncThread2:5\nSyncThread2:6\nSyncThread1:7\nSyncThread1:8\nSyncThread2:9\n```\n不是说一个线程执行synchronized代码块时其它的线程受阻塞吗？为什么上面的例子中thread1和thread2同时在执行。这是因为synchronized只锁定对象，每个对象只有一个锁（lock）与之相关联，而上面的代码等同于下面这段代码：\n```\nSyncThread syncThread1 = new SyncThread();\nSyncThread syncThread2 = new SyncThread();\nThread thread1 = new Thread(syncThread1, \"SyncThread1\");\nThread thread2 = new Thread(syncThread2, \"SyncThread2\");\nthread1.start();\nthread2.start();\n```\n这时创建了两个SyncThread的对象syncThread1和syncThread2，线程thread1执行的是syncThread1对象中的synchronized代码(run)，而线程thread2执行的是syncThread2对象中的synchronized代码(run)；我们知道synchronized锁定的是对象，这时会有两把锁分别锁定syncThread1对象和syncThread2对象，而这两把锁是互不干扰的，不形成互斥，所以两个线程可以同时执行。\n\n**修饰一个方法**\nSynchronized修饰一个方法很简单，就是在方法的前面加synchronized，public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似，只是作用范围不一样，修饰代码块是大括号括起来的范围，而修饰方法范围是整个函数。\n```\npublic synchronized void run() {\n   for (int i = 0; i < 5; i ++) {\n      try {\n         System.out.println(Thread.currentThread().getName() + \":\" + (count++));\n         Thread.sleep(100);\n      } catch (InterruptedException e) {\n         e.printStackTrace();\n      }\n   }\n```\n**修饰一个静态的方法**\nSynchronized也可修饰一个静态方法，用法如下：\n```\npublic synchronized static void method() {\n   // todo\n}\n```\n我们知道静态方法是属于类的而不属于对象的。同样的，synchronized修饰的静态方法锁定的是这个类的所有对象.\n\n**修饰一个类**\nSynchronized还可作用于一个类，用法如下：\n```\nclass ClassName {\n   public void method() {\n      synchronized(ClassName.class) {\n         // todo\n      }\n   }\n}\n```\n**总结：**\n![34110231aa12f351a94b5384a1245a59](大数据成神之路-Java高级特性增强(Synchronized关键字).resources/07D5A65F-74BE-4357-8309-B0D71C4D45B4.png)\nA. 无论synchronized关键字加在方法上还是对象上，如果它作用的对象是非静态的，则它取得的锁是对象；如果synchronized作用的对象是一个静态方法或一个类，则它取得的锁是对类，该类所有的对象同一把锁。\nB. 每个对象只有一个锁（lock）与之相关联，谁拿到这个锁谁就可以运行它所控制的那段代码。\nC. 实现同步是要很大的系统开销作为代价的，甚至可能造成死锁，所以尽量避免无谓的同步控制。\n\n#### Synchronized的原理\n\n##### 对象锁（monitor）机制\n\n现在我们来看看synchronized的具体底层实现。先写一个简单的demo:\n```\npublic class SynchronizedDemo {\n    public static void main(String[] args) {\n        synchronized (SynchronizedDemo.class) {\n        }\n        method();\n    }\n\n    private static void method() {\n    }\n}\n```\n上面的代码中有一个同步代码块，锁住的是类对象，并且还有一个同步静态方法，锁住的依然是该类的类对象。编译之后，切换到SynchronizedDemo.class的同级目录之后，然后用javap -v SynchronizedDemo.class查看字节码文件:\n![98cdb1130796f19ed87ac94054035d7c](大数据成神之路-Java高级特性增强(Synchronized关键字).resources/57E615FE-9961-40F9-832C-FE2313570D85.png)\nsynchronized关键字基于上述两个指令实现了锁的获取和释放过程，解释器执行monitorenter时会进入到InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函数，具体实现如下：\n![0ffb2d827a6b326cd8ad5b40b444eb71](大数据成神之路-Java高级特性增强(Synchronized关键字).resources/6C874101-939A-42B6-A2F8-4A502472DC6D.png)\n执行同步代码块后首先要先执行monitorenter指令，退出的时候monitorexit指令。通过分析之后可以看出，使用Synchronized进行同步，其关键就是必须要对对象的监视器monitor进行获取，当线程获取monitor后才能继续往下执行，否则就只能等待。而这个获取的过程是互斥的，即同一时刻只有一个线程能够获取到monitor。上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法，而这个方法锁的对象依然就这个类对象，那么这个正在执行的线程还需要获取该锁吗？答案是不必的，从上图中就可以看出来，执行静态同步方法的时候就只有一条monitorexit指令，并没有monitorenter获取锁的指令。这就是锁的重入性，即在同一锁程中，线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器，当线程获取该对象锁后，计数器就会加一，释放锁后就会将计数器减一。\n\n##### synchronized的happens-before关系\n\n###### 什么是happens-before\n**概念**\nhappens-before的概念最初由Leslie Lamport在其一篇影响深远的论文（《Time，Clocks and the Ordering of Events in a Distributed System》）中提出，有兴趣的可以google一下。JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内，也可以是在不同线程之间。\n因此，JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证（如果A线程的写操作a与B线程的读操作b之间存在happens-before关系，尽管a操作和b操作在不同的线程中执行，但JMM向程序员保证a操作将对b操作可见）。具体的定义为：\n1）如果一个操作happens-before另一个操作，那么第一个操作的执行结果将对第二个操作可见，而且第一个操作的执行顺序排在第二个操作之前。\n2）两个操作之间存在happens-before关系，并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果，与按happens-before关系来执行的结果一致，那么这种重排序并不非法（也就是说，JMM允许这种重排序）。\n上面的1）是JMM对程序员的承诺。从程序员的角度来说，可以这样理解happens-before关系：如果A happens-before B，那么Java内存模型将向程序员保证——A操作的结果将对B可见，且A的执行顺序排在B之前。注意，这只是Java内存模型向程序员做出的保证！\n上面的2）是JMM对编译器和处理器重排序的约束原则。正如前面所言，JMM其实是在遵循一个基本原则：只要不改变程序的执行结果（指的是单线程程序和正确同步的多线程程序），编译器和处理器怎么优化都行。JMM这么做的原因是：程序员对于这两个操作是否真的被重排序并不关心，程序员关心的是程序执行时的语义不能被改变（即执行结果不能被改变）。因此，happens-before关系本质上和as-if-serial语义是一回事。\n\n**具体规则**\n\n具体规则如下：\n\n1. 程序顺序规则：一个线程中的每个操作，happens-before于该线程中的任意后续操作。\n2. 监视器锁规则：对一个锁的解锁，happens-before于随后对这个锁的加锁。\n3. volatile变量规则：对一个volatile域的写，happens-before于任意后续对这个volatile域的读。\n4. 传递性：如果A happens-before B，且B happens-before C，那么A happens-before C。\n5. start()规则：如果线程A执行操作ThreadB.start()（启动线程B），那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。\n6. join()规则：如果线程A执行操作ThreadB.join()并成功返回，那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。\n7. 程序中断规则：对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。\n8. 对象finalize规则：一个对象的初始化完成（构造函数执行结束）先行于发生它的finalize()方法的开始。\n\n###### synchronized的happens-before关系\nSynchronized的happens-before规则，即监视器锁规则：对同一个监视器的解锁，happens-before于对该监视器的加锁。继续来看代码：\n```\npublic class MonitorDemo {\n    private int a = 0;\n\n    public synchronized void writer() {     // 1\n        a++;                                // 2\n    }                                       // 3\n\n    public synchronized void reader() {    // 4\n        int i = a;                         // 5\n    }                                      // 6\n}\n```\n该代码的happens-before关系如图所示：\n![b3d7851276b01f579cac06a858d67df7](大数据成神之路-Java高级特性增强(Synchronized关键字).resources/B079BB99-25B3-4365-8938-A75246E6237E.png)\n在图中每一个箭头连接的两个节点就代表之间的happens-before关系，黑色的是通过程序顺序规则推导出来，红色的为监视器锁规则推导而出：线程A释放锁happens-before线程B加锁，蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系，通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5，通过这个关系我们可以得出什么？\n根据happens-before的定义中的一条:如果A happens-before B，则A的执行结果对B可见，并且A的执行顺序先于B。线程A先对共享变量A进行加一，由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。\n###### synchronized的优化\n\n通过上面的讨论现在我们对Synchronized应该有所印象了，它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器（monitor），从而进入到同步代码块或者同步方法之中，即表现为互斥性（排它性）。这种方式肯定效率低下，每次只能通过一个线程，既然每次只能通过一个，这种形式不能改变的话，那么我们能不能让每次通过的速度变快一点了。打个比方，去收银台付款，之前的方式是，大家都去排队，然后去纸币付款收银员找零，有的时候付款的时候在包里拿出钱包再去拿出钱，这个过程是比较耗时的，然后，支付宝解放了大家去钱包找钱的过程，现在只需要扫描下就可以完成付款了，也省去了收银员跟你找零的时间的了。同样是需要排队，但整个付款的时间大大缩短，是不是整体的效率变高速率变快了？这种优化方式同样可以引申到锁优化上，缩短获取锁的时间。\n\n#### CAS操作\n这里做一个介绍，CAS为后续锁的章节做一个铺垫O(∩_∩)O~\n\n推荐文章：https://www.jianshu.com/p/24ffe531e9ee\n**什么是CAS?**\n使用锁时，线程获取锁是一种悲观锁策略，即假设每一次执行临界区代码都会产生冲突，所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作（又称为无锁操作）是一种乐观锁策略，它假设所有线程访问共享资源的时候不会出现冲突，既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此，线程就不会出现阻塞停顿的状态。那么，如果出现冲突了怎么办？无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突，出现冲突就重试当前操作直到没有冲突为止。\n\n**CAS的操作过程**\nCAS比较交换的过程可以通俗的理解为CAS(V,O,N)，包含三个值分别为：V 内存地址存放的实际值；O 预期的值（旧值）；N 更新的新值。当V和O相同时，也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过，即该旧值O就是目前来说最新的值了，自然而然可以将新值N赋值给V。反之，V和O不相同，表明该值已经被其他线程改过了则该旧值O不是最新版本的值了，所以不能将新值N赋给V，返回V即可。当多个线程使用CAS操作一个变量是，只有一个线程会成功，并成功更新，其余会失败。失败的线程会重新尝试，当然也可以选择挂起线程\nCAS的实现需要硬件指令集的支撑，在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。\nCAS的应用场景\n在J.U.C包中利用CAS实现类有很多，可以说是支撑起整个concurrency包的实现，在Lock实现中会有CAS改变state变量，在atomic包中的实现类也几乎都是用CAS实现，关于这些具体的实现场景在之后会详细聊聊，现在有个印象就好了（微笑脸）。\n\n**CAS的问题**\n1. ABA问题\n因为CAS会检查旧值有没有变化，这里存在这样一个有意思的问题。比如一个旧值A变为了成B，然后再变成A，刚好在做CAS时检查发现旧值并没有变化依然为A，但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式，添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言，当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题，解决思路就是这样的。\n2. 自旋时间过长\n使用CAS时非阻塞同步，也就是说不会将线程挂起，会自旋（无非就是一个死循环）进行下一次尝试，如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令，那么在效率上会有一定的提升。\n3. 只能保证一个共享变量的原子操作\n当对一个共享变量执行操作时CAS能保证其原子性，如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量，即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。\n\n\n参考文章目录：\n感谢各位大大的劳动成果~深表敬意~\n\nhttps://blog.csdn.net/qq_34337272/article/details/79655194\n\nhttps://blog.csdn.net/qq_34337272/article/details/79670775\n\nhttps://www.jianshu.com/p/d53bf830fa09\n\nhttps://www.jianshu.com/p/c5058b6fe8e5\n\n"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(volatile关键字).md",
    "content": "### **Java高级特性增强-Volatile**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n\n* * *\n## volatile关键字\n\n\n#### volatile特性\nvolatile就可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解，也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。Java内存模型告诉我们，各个线程会将共享变量从主内存中拷贝到工作内存，然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中？这个时机对普通变量是没有规定的，而针对volatile修饰的变量给java虚拟机特殊的约定，线程对volatile变量的修改会立刻被其他线程所感知，即不会出现数据脏读的现象，从而保证数据的“可见性”。\n通俗来说就是，线程A对一个volatile变量的修改，对于其它线程来说是可见的，即线程每次获取volatile变量的值都是最新的。\n\n#### volatile的实现原理\n在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令。我们想这个Lock指令肯定有神奇的地方，那么Lock前缀的指令在多核处理器下会发现什么事情了？主要有这两个方面的影响：\n\n将当前处理器缓存行的数据写回系统内存；\n这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效\n\n为了提高处理速度，处理器不直接和内存进行通信，而是先将系统内存的数据读到内部缓存（L1，L2或其他）后再进行操作，但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作，JVM就会向处理器发送一条Lock前缀的指令，将这个变量所在缓存行的数据写回到系统内存。但是，就算写回到内存，如果其他处理器缓存的值还是旧的，再执行计算操作就会有问题。所以，在多处理器下，为了保证各个处理器的缓存是一致的，就会实现缓存一致性协议，每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了，当处理器发现自己缓存行对应的内存地址被修改，就会将当前处理器的缓存行设置成无效状态，当处理器对这个数据进行修改操作的时候，会重新从系统内存中把数据读到处理器缓存里。因此，经过分析我们可以得出如下结论：\n\nLock前缀的指令会引起处理器缓存写回内存；\n一个处理器的缓存回写到内存会导致其他处理器的缓存失效；\n当处理器发现本地缓存失效后，就会从内存中重读该变量数据，即可以获取当前最新值。\n\n这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。\n\n#### 我们在项目中如何使用？\n1、状态标记量\n在高并发的场景中，通过一个boolean类型的变量isopen，控制代码是否走促销逻辑，该如何实现？\n```\npublic class ServerHandler {\n    private volatile isopen;\n    public void run() {\n        if (isopen) {\n           //isopen=true逻辑\n        } else {\n          //其他逻辑\n        }\n    }\n    public void setIsopen(boolean isopen) {\n        this.isopen = isopen\n    }\n}\n```\n场景细节无需过分纠结，这里只是举个例子说明volatile的使用方法，用户的请求线程执行run方法，如果需要开启促销活动，可以通过后台设置，具体实现可以发送一个请求，调用setIsopen方法并设置isopen为true，由于isopen是volatile修饰的，所以一经修改，其他线程都可以拿到isopen的最新值，用户请求就可以执行isopen=true的逻辑。\n\n2、double check\n单例模式的一种实现方式，但很多人会忽略volatile关键字，因为没有该关键字，程序也可以很好的运行，只不过代码的稳定性总不是100%，说不定在未来的某个时刻，隐藏的bug就出来了。\n```\nclass Singleton {\n    private volatile static Singleton instance;\n    public static Singleton getInstance() {\n        if (instance == null) {\n            syschronized(Singleton.class) {\n                if (instance == null) {\n                    instance = new Singleton();\n                }\n            }\n        }\n        return instance;\n    } \n}\n```\n不过在众多单例模式的实现中，我比较推荐懒加载的优雅写法Initialization on Demand Holder（IODH）。\n```\npublic class Singleton {  \n    static class SingletonHolder {  \n        static Singleton instance = new Singleton();  \n    }  \n      \n    public static Singleton getInstance(){  \n        return SingletonHolder.instance;  \n    }  \n}  \n```\n\n#### 如何保证内存可见性\n\n在java虚拟机的内存模型中，有主内存和工作内存的概念，每个线程对应一个工作内存，并共享主内存的数据，下面看看操作普通变量和volatile变量有什么不同：\n1、对于普通变量：读操作会优先读取工作内存的数据，如果工作内存中不存在，则从主内存中拷贝一份数据到工作内存中；写操作只会修改工作内存的副本数据，这种情况下，其它线程就无法读取变量的最新值。\n2、对于volatile变量，读操作时JMM会把工作内存中对应的值设为无效，要求线程从主内存中读取数据；写操作时JMM会把工作内存中对应的数据刷新到主内存中，这种情况下，其它线程就可以读取变量的最新值。\nvolatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的，什么是内存屏障？内存屏障，又称内存栅栏，是一个CPU指令。在程序运行时，为了提高执行性能，编译器和处理器会对指令进行重排序，JMM为了保证在不同的编译器和CPU上有相同的结果，通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序，插入一条内存屏障会告诉编译器和CPU：不管什么指令都不能和这条Memory Barrier指令重排序。\n\n举例如下：\n```\nclass Singleton {\n    private volatile static Singleton instance;\n    private int a;\n    private int b;\n    private int b;\n    public static Singleton getInstance() {\n        if (instance == null) {\n            syschronized(Singleton.class) {\n                if (instance == null) {\n                    a = 1;  // 1\n                     b = 2;  // 2\n                    instance = new Singleton();  // 3\n                    c = a + b;  // 4\n                }\n            }\n        }\n        return instance;\n    } \n}\n```\n1、如果变量instance没有volatile修饰，语句1、2、3可以随意的进行重排序执行，即指令执行过程可能是3214或1324。\n2、如果是volatile修饰的变量instance，会在语句3的前后各插入一个内存屏障。\n通过观察volatile变量和普通变量所生成的汇编代码可以发现，操作volatile变量会多出一个lock前缀指令：\n```\nJava代码：\ninstance = new Singleton();\n\n汇编代码：\n0x01a3de1d: movb $0x0,0x1104800(%esi);\n0x01a3de24: **lock** addl $0x0,(%esp);\n```\n这个lock前缀指令相当于上述的内存屏障，提供了以下保证：\n1、将当前CPU缓存行的数据写回到主内存；\n2、这个写回内存的操作会导致在其它CPU里缓存了该内存地址的数据无效。\nCPU为了提高处理性能，并不直接和内存进行通信，而是将内存的数据读取到内部缓存（L1，L2）再进行操作，但操作完并不能确定何时写回到内存，如果对volatile变量进行写操作，当CPU执行到Lock前缀指令时，会将这个变量所在缓存行的数据写回到内存，不过还是存在一个问题，就算内存的数据是最新的，其它CPU缓存的还是旧值，所以为了保证各个CPU的缓存一致性，每个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性，当发现自己缓存行对应的内存地址的数据被修改，就会将该缓存行设置成无效状态，当CPU读取该变量时，发现所在的缓存行被设置为无效，就会重新从内存中读取数据到缓存中。\n这也是我们之前讲的原理部分的解释~\n\n\n#### volatile的happens-before关系\nvolatile变量可以通过缓存一致性协议保证每个线程都能获得最新值，即满足数据的“可见性”。我们继续延续上一篇分析问题的方式（我一直认为思考问题的方式是属于自己，也才是最重要的，也在不断培养这方面的能力），我一直将并发分析的切入点分为两个核心，三大性质。两大核心：JMM内存模型（主内存和工作内存）以及happens-before；三条性质：原子性，可见性，有序性（关于三大性质的总结在以后得文章会和大家共同探讨）。废话不多说，先来看两个核心之一：volatile的happens-before关系。\n在六条happens-before规则中有一条是：volatile变量规则：对一个volatile域的写，happens-before于任意后续对这个volatile域的读。下面我们结合具体的代码，我们利用这条规则推导下：\n```\npublic class VolatileExample {\n    private int a = 0;\n    private volatile boolean flag = false;\n    public void writer(){\n        a = 1;          //1\n        flag = true;   //2\n    }\n    public void reader(){\n        if(flag){      //3\n            int i = a; //4\n        }\n    }\n}\n```\n上面的实例代码对应的happens-before关系如下图所示：\n![ab3fc4589fa61bf75ad91d7080664a7d](大数据成神之路-Java高级特性增强(volatile关键字).resources/14BF4468-D1E0-4FBF-B503-A888E309418D.png)\n加锁线程A先执行writer方法，然后线程B执行reader方法图中每一个箭头两个节点就代码一个happens-before关系，黑色的代表根据程序顺序规则推导出来，红色的是根据volatile变量的写happens-before 于任意后续对volatile变量的读，而蓝色的就是根据传递性规则推导出来的。这里的2 happen-before 3，同样根据happens-before规则定义：如果A happens-before B,则A的执行结果对B可见，并且A的执行顺序先于B的执行顺序，我们可以知道操作2执行结果对操作3来说是可见的，也就是说当线程A将volatile变量 flag更改为true后线程B就能够迅速感知。\n\n-----------\n**参考文章和书籍：**\n\n《Java并发编程的艺术》\n《实战Java高并发程序设计》\n\nhttps://blog.csdn.net/qq_34337272/article/details/79680771\n\nhttps://www.jianshu.com/p/a5f99f25329a\n\nhttps://www.jianshu.com/p/506c1e38a922\n"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(多线程).md",
    "content": "### **Java高级特性增强-多线程**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n* * *\n## 多线程\n![89bf0392f832b459ed62efb31af4461e](大数据成神之路-Java高级特性增强(多线程).resources/F18CB21B-41D4-4D8D-890D-4B632F69F96A.jpg)\n参考资料列表：\njava并发编程指南\n**https://blog.csdn.net/qq_34337272/column/info/20860**\n死磕系列：\n**http://cmsblogs.com/?p=2611**\n面试题系列：\n**https://blog.csdn.net/linzhiqiang0316/article/details/80473906**\n简书：\n**https://www.jianshu.com/nb/4893857**\n以上几个博客足够了，着重推荐一下死磕系列和简书的文章，比较深入\n\n\n#### 进程和多线程简介\n##### 进程和线程\n进程和线程的对比这一知识点由于过于基础，所以在面试中很少碰到，但是极有可能会在笔试题中碰到。常见的提问形式是这样的：“什么是线程和进程?，请简要描述线程与进程的关系、区别及优缺点？ ”。\n\n##### 何为进程？\n进程是程序的一次执行过程，是系统运行程序的基本单位，因此进程是动态的。系统运行一个程序即是一个进程从创建，运行到消亡的过程。\n或者我们可以这样说：\n进程，是程序的一次执行过程，是系统运行程序的基本单位，因此进程是动态的。系统运行一个程序即是一个进程从创建，运行到消亡的过程。简单来说，一个进程就是一个执行中的程序，它在计算机中一个指令接着一个指令地执行着，同时，每个进程还占有某些系统资源如CPU时间，内存空间，文件，文件，输入输出设备的使用权等等。换句话说，当程序在执行时，将会被操作系统载入内存中。\n##### 何为线程？\n线程与进程相似，但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源，所以系统在产生一个线程，或是在各个线程之间作切换工作时，负担要比进程小得多，也正因为如此，线程也被称为轻量级进程。\n\n##### 何为多线程\n多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行，也就是交替运行。多核CPU的话，因为每个CPU有自己的运算器，所以在多个CPU中可以同时运行。\n\n##### 为什么多线程是必要的\n个人觉得可以用一句话概括：开发高并发系统的基础，利用好多线程机制可以大大提高系统整体的并发能力以及性能。\n\n##### 为什么提倡多线程而不是多进程\n线程就是轻量级进程，是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计，是因为线程间的切换和调度的成本远远小于进程。\n\n##### 线程有什么优缺点\n1）好处\n使用多线程可以把程序中占据时间长的任务放到后台去处理，如图片、视屏的下载。\n发挥多核处理器的优势，并发执行让系统运行的更快、更流畅，用户体验更好。\n2）坏处\n大量的线程降低代码的可读性。\n更多的线程需要更多的内存空间。\n当多个线程对同一个资源出现争夺时候要注意线程安全的问题。\n##### 多线程中重要的概念\n**同步和异步**\n同步和异步通常用来形容一次方法调用。同步方法调用一旦开始，调用者必须等到方法调用返回后，才能继续后续的行为。异步方法调用更像一个消息传递，一旦开始，方法调用就会立即返回，调用者可以继续后续的操作。\n\n关于异步目前比较经典以及常用的实现方式就是消息队列：在不使用消息队列服务器的时候，用户的请求数据直接写入数据库，在高并发的情况下数据库压力剧增，使得响应速度变慢。但是在使用消息队列之后，用户的请求数据发送给消息队列之后立即 返回，再由消息队列的消费者进程从消息队列中获取数据，异步写入数据库。由于消息队列服务器处理速度快于数据库（消息队列也比数据库有更好的伸缩性），因此响应速度得到大幅改善。\n\n**并发(Concurrency)和并行(Parallelism)**\n并发和并行是两个非常容易被混淆的概念。它们都可以表示两个或者多个任务一起执行，但是偏重点有些不同。并发偏重于多个任务交替执行，而多个任务之间有可能还是串行的。而并行是真正意义上的“同时执行”。\n\n多线程在单核CPU的话是顺序执行，也就是交替运行（并发）。多核CPU的话，因为每个CPU有自己的运算器，所以在多个CPU中可以同时运行（并行）。\n\n**高并发**\n高并发（High Concurrency）是互联网分布式系统架构设计中必须考虑的因素之一，它通常是指，通过设计保证系统能够同时并行处理很多请求。\n\n高并发相关常用的一些指标有响应时间（Response Time），吞吐量（Throughput），每秒查询率QPS（Query Per Second），并发用户数等。\n\n**临界区**\n临界区用来表示一种公共资源或者说是共享数据，可以被多个线程使用。但是每一次，只能有一个线程使用它，一旦临界区资源被占用，其他线程要想使用这个资源，就必须等待。在并行程序中，临界区资源是保护的对象。\n\n**阻塞和非阻塞**\n非阻塞指在不能立刻得到结果之前，该函数不会阻塞当前线程，而会立刻返回，而阻塞与之相反。\n\n##### 多线程的创建方式\n\n**继承`Thread`类**\n```java\npublic class MyThread extends Thread {\n\t@Override\n\tpublic void run() {\n\t\tsuper.run();\n\t\tSystem.out.println(\"MyThread\");\n\t}\n}\n```\n**实现`Runnable`接口**\n```java\npublic class MyRunnable implements Runnable {\n\t@Override\n\tpublic void run() {\n\t\tSystem.out.println(\"MyRunnable\");\n\t}\n}\n```\n**实现`Callable`接口**\n```java\nclass ImplementsCallable implements Callable<String>{\n\n    @Override\n    public String call() throws Exception {\n        return UUID.randomUUID().toString().substring(0,8);\n    }\n}\n```\n```java\nprivate static void implementsCallable() throws ExecutionException, InterruptedException {\n        //A\n        FutureTask futureTaskA = new FutureTask<String>(new ImplementsCallable());\n        new Thread(futureTaskA,\"implementsCallable-A\").start();\n\n}\n```\n**线程池**\n> 【强制】线程资源必须通过线程池提供，不允许在应用中自行显式创建线程。 说明：线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销，解决资源不足的问题。如果不使用线程池，有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。\n—————《阿里巴巴Java开发手册》泰山版第一章第七节并发处理第3点。\n\n> 【强制】线程池不允许使用Executors去创建，而是通过`ThreadPoolExecutor`的方式，这样的处理方式让写的同学更加明确线程池的运行规则，规避资源耗尽的风险。 说明：`Executors`返回的线程池对象的弊端如下： 1） `FixedThreadPool`和`SingleThreadPool`： 允许的请求队列长度为`Integer.MAX_VALUE`，可能会堆积大量的请求，从而导致OOM。 2） `CachedThreadPool`： 允许的创建线程数量为`Integer.MAX_VALUE`，可能会创建大量的线程，从而导致OOM。————《阿里巴巴Java开发手册》泰山版第一章第七节并发处理第4点。\n\n我们在实际开发环境中，建议使用线程池的方式创建线程。\n\n```java\npublic class ThreadPool\n{\n\tprivate static int POOL_NUM = 10;\n\t\n\tpublic static void main(String[] args)\n\t{\n        ExecutorService executorService = new ThreadPoolExecutor(\n                        5,\n                        5,\n                        1l,\n                        TimeUnit.SECONDS,\n                        new LinkedBlockingQueue<>(100),\n                        Executors.defaultThreadFactory(),\n                        new ThreadPoolExecutor.AbortPolicy()\n                );\n\t\tfor(int i = 0; i<POOL_NUM; i++)\n\t\t{\n\t\tRunnableThread thread = new RunnableThread();\n\t\texecutorService.execute(thread);\n\t\t}\n\t}\n}\n \nclass RunnableThread implements Runnable\n{\n\tprivate int THREAD_NUM = 10;\n\tpublic void run()\n\t{\n\t\tfor(int i = 0; i<THREAD_NUM; i++)\n\t\t{\n\t\t\tSystem.out.println(\"线程\" + Thread.currentThread() + \" \" + i);\n\t\t} \n\t}\n}\n```\n##### 线程的生命周期\n\n线程一共有五个状态，分别如下：\n**新建(new)：**\n当创建Thread类的一个实例（对象）时，此线程进入新建状态（未被启动）。例如：Thread t1 = new Thread() 。\n\n**可运行(runnable)：**\n线程对象创建后，其他线程(比如 main 线程）调用了该对象的 start 方法。该状态的线程位于可运行线程池中，等待被线程调度选中，获取 cpu 的使用权。例如：t1.start() 。\n\n**运行(running)：**\n线程获得 CPU 资源正在执行任务（#run() 方法），此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入，线程将一直运行到结束。\n死亡(dead)：当线程执行完毕或被其它线程杀死，线程就进入死亡状态，这时线程不可能再进入就绪状态等待执行。\n**自然终止：**\n正常运行完 #run()方法，终止。\n**异常终止：**\n调用 #stop() 方法，让一个线程终止运行。\n**堵塞(blocked)：**\n由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行，即进入堵塞状态。直到线程进入可运行(runnable)状态，才有机会再次获得 CPU 资源，转到运行(running)状态。阻塞的情况有三种：\n**正在睡眠：**\n调用 #sleep(long t) 方法，可使线程进入睡眠方式。\n一个睡眠着的线程在指定的时间过去可进入可运行(runnable)状态。\n**正在等待：**\n调用 #wait() 方法。\n调用 notify() 方法，回到就绪状态。\n**被另一个线程所阻塞：**\n调用 #suspend() 方法。\n调用 #resume() 方法，就可以恢复。\n\n见下图：\n![5eeec5f68f4fc412246efd4111d6fdec](大数据成神之路-Java高级特性增强(多线程).resources/6AC11272-0DE3-44D7-9533-2647D0A652BF.png)\n\n##### 线程的优先级\n每个线程都具有各自的优先级，线程的优先级可以在程序中表明该线程的重要性，如果有很多线程处于就绪状态，系统会根据优先级来决定首先使哪个线程进入运行状态。但这个并不意味着低。\n优先级的线程得不到运行，而只是它运行的几率比较小，如垃圾回收机制线程的优先级就比较低。所以很多垃圾得不到及时的回收处理。\n\n线程优先级具有继承特性比如A线程启动B线程，则B线程的优先级和A是一样的。\n\n线程优先级具有随机性也就是说线程优先级高的不一定每一次都先执行完。\n\nThread类中包含的成员变量代表了线程的某些优先级。如Thread.MIN_PRIORITY（常数1），Thread.NORM_PRIORITY（常数5）,\nThread.MAX_PRIORITY（常数10）。其中每个线程的优先级都在Thread.MIN_PRIORITY（常数1） 到Thread.MAX_PRIORITY（常数10） 之间，在默认情况下优先级都是Thread.NORM_PRIORITY（常数5）。\n\n学过操作系统这门课程的话，我们可以发现多线程优先级或多或少借鉴了操作系统对进程的管理\n\n##### 线程的终止\n**interrupt()方法**\n注意：interrupt()方法的使用效果并不像for+break语句那样，马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志，并不是真的停止线程。\n```\npublic class MyThread extends Thread {\n    public void run(){\n        super.run();\n        for(int i=0; i<500000; i++){\n            System.out.println(\"i=\"+(i+1));\n        }\n    }\n}\n\npublic class Run {\n    public static void main(String args[]){\n        Thread thread = new MyThread();\n        thread.start();\n        try {\n            Thread.sleep(2000);\n            thread.interrupt();\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n输出结果：\n```\n...\ni=499994\ni=499995\ni=499996\ni=499997\ni=499998\ni=499999\ni=500000\n```\n**判断线程是否停止状态**\nThread.java类中提供了两种方法：\n\nthis.interrupted(): 测试当前线程是否已经中断；\nthis.isInterrupted(): 测试线程是否已经中断；\n那么这两个方法有什么图区别呢？\n我们先来看看this.interrupted()方法的解释：测试当前线程是否已经中断，当前线程是指运行this.interrupted()方法的线程。\n\n```\npublic class MyThread extends Thread {\n    public void run(){\n        super.run();\n        for(int i=0; i<500000; i++){\n            i++;\n        }\n    }\n}\n\npublic class Run {\n    public static void main(String args[]){\n        Thread thread = new MyThread();\n        thread.start();\n        try {\n            Thread.sleep(2000);\n            thread.interrupt();\n\n            System.out.println(\"stop 1->\" + thread.interrupted());\n            System.out.println(\"stop 2->\" + thread.interrupted());\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n运行结果：\n```\nstop 1->false\nstop 2->false\n```\n类Run.java中虽然是在thread对象上调用以下代码：thread.interrupt(), 后面又使用\n\n```\nSystem.out.println(\"stop 1->\" + thread.interrupted());\nSystem.out.println(\"stop 2->\" + thread.interrupted());  \n```\n来判断thread对象所代表的线程是否停止，但从控制台打印的结果来看，线程并未停止，这也证明了interrupted()方法的解释，测试当前线程是否已经中断。这个当前线程是main，它从未中断过，所以打印的结果是两个false.\n\n如何使main线程产生中断效果呢？\n```\npublic class Run2 {\n    public static void main(String args[]){\n        Thread.currentThread().interrupt();\n        System.out.println(\"stop 1->\" + Thread.interrupted());\n        System.out.println(\"stop 2->\" + Thread.interrupted());\n\n        System.out.println(\"End\");\n    }\n}    \n```\n运行结果为：\n```\nstop 1->true\nstop 2->false\nEnd\n```\n方法interrupted()的确判断出当前线程是否是停止状态。但为什么第2个布尔值是false呢？ 官方帮助文档中对interrupted方法的解释：\n测试当前线程是否已经中断。线程的中断状态由该方法清除。 换句话说，如果连续两次调用该方法，则第二次调用返回false。\n\n下面来看一下isInterrupted()方法。\n```\npublic class Run3 {\n    public static void main(String args[]){\n        Thread thread = new MyThread();\n        thread.start();\n        thread.interrupt();\n        System.out.println(\"stop 1->\" + thread.isInterrupted());\n        System.out.println(\"stop 2->\" + thread.isInterrupted());\n    }\n}\n```\n\n运行结果：\n\n```\nstop 1->true\nstop 2->true\n```\nisInterrupted()并未清除状态，所以打印了两个true。\n\n**能停止的线程--异常法**\n有了前面学习过的知识点，就可以在线程中用for语句来判断一下线程是否是停止状态，如果是停止状态，则后面的代码不再运行即可：\n\n```\npublic class MyThread extends Thread {\n    public void run(){\n        super.run();\n        for(int i=0; i<500000; i++){\n            if(this.interrupted()) {\n                System.out.println(\"线程已经终止， for循环不再执行\");\n                break;\n            }\n            System.out.println(\"i=\"+(i+1));\n        }\n    }\n}\n\npublic class Run {\n    public static void main(String args[]){\n        Thread thread = new MyThread();\n        thread.start();\n        try {\n            Thread.sleep(2000);\n            thread.interrupt();\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n运行结果：\n```\n...\ni=202053\ni=202054\ni=202055\ni=202056\n线程已经终止， for循环不再执行\n```\n上面的示例虽然停止了线程，但如果for语句下面还有语句，还是会继续运行的。看下面的例子：\n```\npublic class MyThread extends Thread {\n    public void run(){\n        super.run();\n        for(int i=0; i<500000; i++){\n            if(this.interrupted()) {\n                System.out.println(\"线程已经终止， for循环不再执行\");\n                break;\n            }\n            System.out.println(\"i=\"+(i+1));\n        }\n\n        System.out.println(\"这是for循环外面的语句，也会被执行\");\n    }\n}\n```\n使用Run.java执行的结果是：\n```\n...\ni=180136\ni=180137\ni=180138\ni=180139\n线程已经终止， for循环不再执行\n这是for循环外面的语句，也会被执行\n```\n如何解决语句继续运行的问题呢？ 看一下更新后的代码：\n```\npublic class MyThread extends Thread {\n    public void run(){\n        super.run();\n        try {\n            for(int i=0; i<500000; i++){\n                if(this.interrupted()) {\n                    System.out.println(\"线程已经终止， for循环不再执行\");\n                        throw new InterruptedException();\n                }\n                System.out.println(\"i=\"+(i+1));\n            }\n\n            System.out.println(\"这是for循环外面的语句。因为有InterruptedException，所以不会被执行\");\n        } catch (InterruptedException e) {\n            System.out.println(\"进入MyThread.java类中的catch了。。。\");\n            e.printStackTrace();\n        }\n    }\n}\n```\n使用Run.java运行的结果如下：\n```\n...\ni=203798\ni=203799\ni=203800\n线程已经终止， for循环不再执行\n进入MyThread.java类中的catch了。。。\njava.lang.InterruptedException\n    at thread.MyThread.run(MyThread.java:13)\n```\n**在沉睡中停止**\n如果线程在sleep()状态下停止线程，会是什么效果呢？\n```\npublic class MyThread extends Thread {\n    public void run(){\n        super.run();\n\n        try {\n            System.out.println(\"线程开始。。。\");\n            Thread.sleep(200000);\n            System.out.println(\"线程结束。\");\n        } catch (InterruptedException e) {\n            System.out.println(\"在沉睡中被停止, 进入catch， 调用isInterrupted()方法的结果是：\" + this.isInterrupted());\n            e.printStackTrace();\n        }\n\n    }\n}\n```\n使用Run.java运行的结果是：\n```\n线程开始。。。\n在沉睡中被停止, 进入catch， 调用isInterrupted()方法的结果是：false\njava.lang.InterruptedException: sleep interrupted\n    at java.lang.Thread.sleep(Native Method)\n    at thread.MyThread.run(MyThread.java:12)\n```\n从打印的结果来看， 如果在sleep状态下停止某一线程，会进入catch语句，并且清除停止状态值，使之变为false。\n\n前一个实验是先sleep然后再用interrupt()停止，与之相反的操作在学习过程中也要注意：\n```\npublic class MyThread extends Thread {\n    public void run(){\n        super.run();\n        try {\n            System.out.println(\"线程开始。。。\");\n            for(int i=0; i<10000; i++){\n                System.out.println(\"i=\" + i);\n            }\n            Thread.sleep(200000);\n            System.out.println(\"线程结束。\");\n        } catch (InterruptedException e) {\n             System.out.println(\"先停止，再遇到sleep，进入catch异常\");\n            e.printStackTrace();\n        }\n\n    }\n}\n\npublic class Run {\n    public static void main(String args[]){\n        Thread thread = new MyThread();\n        thread.start();\n        thread.interrupt();\n    }\n}\n```\n运行结果：\n```\ni=9998\ni=9999\n先停止，再遇到sleep，进入catch异常\njava.lang.InterruptedException: sleep interrupted\n    at java.lang.Thread.sleep(Native Method)\n    at thread.MyThread.run(MyThread.java:15)\n```\n**能停止的线程---暴力停止**\n使用stop()方法停止线程则是非常暴力的。\n```\npublic class MyThread extends Thread {\n    private int i = 0;\n    public void run(){\n        super.run();\n        try {\n            while (true){\n                System.out.println(\"i=\" + i);\n                i++;\n                Thread.sleep(200);\n            }\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n    }\n}\n\npublic class Run {\n    public static void main(String args[]) throws InterruptedException {\n        Thread thread = new MyThread();\n        thread.start();\n        Thread.sleep(2000);\n        thread.stop();\n    }\n}\n```\n运行结果：\n```\ni=0\ni=1\ni=2\ni=3\ni=4\ni=5\ni=6\ni=7\ni=8\ni=9\n\nProcess finished with exit code 0\n```\n**方法stop()与java.lang.ThreadDeath异常**\n\n调用stop()方法时会抛出java.lang.ThreadDeath异常，但是通常情况下，此异常不需要显示地捕捉。\n```\npublic class MyThread extends Thread {\n    private int i = 0;\n    public void run(){\n        super.run();\n        try {\n            this.stop();\n        } catch (ThreadDeath e) {\n            System.out.println(\"进入异常catch\");\n            e.printStackTrace();\n        }\n    }\n}\n\npublic class Run {\n    public static void main(String args[]) throws InterruptedException {\n        Thread thread = new MyThread();\n        thread.start();\n    }\n}\n```\nstop()方法以及作废，因为如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁，导致数据得不到同步的处理，出现数据不一致的问题。\n\n**释放锁的不良后果**\n\n使用stop()释放锁将会给数据造成不一致性的结果。如果出现这样的情况，程序处理的数据就有可能遭到破坏，最终导致程序执行的流程错误，一定要特别注意：\n```\npublic class SynchronizedObject {\n    private String name = \"a\";\n    private String password = \"aa\";\n\n    public synchronized void printString(String name, String password){\n        try {\n            this.name = name;\n            Thread.sleep(100000);\n            this.password = password;\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n}\n\npublic class MyThread extends Thread {\n    private SynchronizedObject synchronizedObject;\n    public MyThread(SynchronizedObject synchronizedObject){\n        this.synchronizedObject = synchronizedObject;\n    }\n\n    public void run(){\n        synchronizedObject.printString(\"b\", \"bb\");\n    }\n}\n\npublic class Run {\n    public static void main(String args[]) throws InterruptedException {\n        SynchronizedObject synchronizedObject = new SynchronizedObject();\n        Thread thread = new MyThread(synchronizedObject);\n        thread.start();\n        Thread.sleep(500);\n        thread.stop();\n        System.out.println(synchronizedObject.getName() + \"  \" + synchronizedObject.getPassword());\n    }\n}\n```\n输出结果：\n```\nb  aa\n```\n由于stop()方法以及在JDK中被标明为“过期/作废”的方法，显然它在功能上具有缺陷，所以不建议在程序张使用stop()方法。\n\n **使用return停止线程**\n 将方法interrupt()与return结合使用也能实现停止线程的效果：\n```\n public class MyThread extends Thread {\n    public void run(){\n        while (true){\n            if(this.isInterrupted()){\n                System.out.println(\"线程被停止了！\");\n                return;\n            }\n            System.out.println(\"Time: \" + System.currentTimeMillis());\n        }\n    }\n}\n\npublic class Run {\n    public static void main(String args[]) throws InterruptedException {\n        Thread thread = new MyThread();\n        thread.start();\n        Thread.sleep(2000);\n        thread.interrupt();\n    }\n}\n```\n输出结果：\n```\n...\nTime: 1467072288503\nTime: 1467072288503\nTime: 1467072288503\n线程被停止了！\n```\n笔者花了巨大篇幅介绍线程的终止，因为这是在实际开发中最容易犯的错误，千万注意哦~"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(锁).md",
    "content": "### **Java高级特性增强-锁**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n\n* * *\n## 锁\n#### Java中的锁分类\n在读很多并发文章中，会提及各种各样锁如公平锁，乐观锁等等，这篇文章介绍就是各种锁。介绍的内容如下：\n公平锁/非公平锁\n可重入锁\n独享锁/共享锁\n互斥锁/读写锁\n乐观锁/悲观锁\n分段锁\n偏向锁/轻量级锁/重量级锁\n自旋锁\n上面是很多锁的名词，这些分类并不是全是指锁的状态，有的指锁的特性，有的指锁的设计，下面总结的内容是对每个锁的名词进行一定的解释。\n**公平锁/非公平锁**\n公平锁是指多个线程按照申请锁的顺序来获取锁。\n非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序，有可能后申请的线程比先申请的线程优先获取锁。有可能，会造成优先级反转或者饥饿现象。\n对于Java ReentrantLock而言，通过构造函数指定该锁是否是公平锁，默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。\n对于Synchronized而言，也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度，所以并没有任何办法使其变成公平锁。\n\n**可重入锁**\n可重入锁又名递归锁，是指在同一个线程在外层方法获取锁的时候，在进入内层方法会自动获取锁。说的有点抽象，下面会有一个代码的示例。\n对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁，其名字是Re entrant Lock重新进入锁。\n对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。\n```\nsynchronized void setA() throws Exception{\n    Thread.sleep(1000);\n    setB();\n}\n\nsynchronized void setB() throws Exception{\n    Thread.sleep(1000);\n}\n```\n上面的代码就是一个可重入锁的一个特点，如果不是可重入锁的话，setB可能不会被当前线程执行，可能造成死锁。\n\n**独享锁/共享锁**\n独享锁是指该锁一次只能被一个线程所持有。\n共享锁是指该锁可被多个线程所持有。\n\n对于Java ReentrantLock而言，其是独享锁。但是对于Lock的另一个实现类ReadWriteLock，其读锁是共享锁，其写锁是独享锁。\n读锁的共享锁可保证并发读是非常高效的，读写，写读 ，写写的过程是互斥的。\n独享锁与共享锁也是通过AQS来实现的，通过实现不同的方法，来实现独享或者共享。\n对于Synchronized而言，当然是独享锁。\n\n**互斥锁/读写锁**\n上面讲的独享锁/共享锁就是一种广义的说法，互斥锁/读写锁就是具体的实现。\n互斥锁在Java中的具体实现就是ReentrantLock\n读写锁在Java中的具体实现就是ReadWriteLock\n\n**乐观锁/悲观锁**\n乐观锁与悲观锁不是指具体的什么类型的锁，而是指看待并发同步的角度。\n悲观锁认为对于同一个数据的并发操作，一定是会发生修改的，哪怕没有修改，也会认为修改。因此对于同一个数据的并发操作，悲观锁采取加锁的形式。悲观的认为，不加锁的并发操作一定会出问题。\n乐观锁则认为对于同一个数据的并发操作，是不会发生修改的。在更新数据的时候，会采用尝试更新，不断重新的方式更新数据。乐观的认为，不加锁的并发操作是没有事情的。\n\n从上面的描述我们可以看出，悲观锁适合写操作非常多的场景，乐观锁适合读操作非常多的场景，不加锁会带来大量的性能提升。\n悲观锁在Java中的使用，就是利用各种锁。\n乐观锁在Java中的使用，是无锁编程，常常采用的是CAS算法，典型的例子就是原子类，通过CAS自旋实现原子操作的更新。\n\n**分段锁**\n分段锁其实是一种锁的设计，并不是具体的一种锁，对于ConcurrentHashMap而言，其并发的实现就是通过分段锁的形式来实现高效的并发操作。\n我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想，ConcurrentHashMap中的分段锁称为Segment，它即类似于HashMap（JDK7与JDK8中HashMap的实现）的结构，即内部拥有一个Entry数组，数组中的每个元素又是一个链表；同时又是一个ReentrantLock（Segment继承了ReentrantLock)。\n当需要put元素的时候，并不是对整个hashmap进行加锁，而是先通过hashcode来知道他要放在那一个分段中，然后对这个分段进行加锁，所以当多线程put的时候，只要不是放在一个分段中，就实现了真正的并行的插入。\n但是，在统计size的时候，可就是获取hashmap全局信息的时候，就需要获取所有的分段锁才能统计。\n分段锁的设计目的是细化锁的粒度，当操作不需要更新整个数组的时候，就仅仅针对数组中的一项进行加锁操作。\n\n**偏向锁/轻量级锁/重量级锁**\n这三种锁是指锁的状态，并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。\n偏向锁是指一段同步代码一直被一个线程所访问，那么该线程会自动获取锁。降低获取锁的代价。\n轻量级锁是指当锁是偏向锁的时候，被另一个线程所访问，偏向锁就会升级为轻量级锁，其他线程会通过自旋的形式尝试获取锁，不会阻塞，提高性能。\n重量级锁是指当锁为轻量级锁的时候，另一个线程虽然是自旋，但自旋不会一直持续下去，当自旋一定次数的时候，还没有获取到锁，就会进入阻塞，该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞，性能降低。\n\n**自旋锁**\n在Java中，自旋锁是指尝试获取锁的线程不会立即阻塞，而是采用循环的方式去尝试获取锁，这样的好处是减少线程上下文切换的消耗，缺点是循环会消耗CPU。\n\n#### Lock接口\n\n在Lock接口出现之前，Java程序是靠synchronized关键字实现锁功能的。JDK1.5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。\n\n虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程，并且有助于避免涉及锁的许多常见编程错误，但是有时您需要以更灵活的方式处理锁。例如，用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”：您获取节点A的锁定，然后获取节点B，然后释放A并获取C，然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了，使用Lock接口容易很多。\n\nLock接口的实现类： \nReentrantLock ， ReentrantReadWriteLock.ReadLock ， ReentrantReadWriteLock.WriteLock\n\n##### AbstractQueuedSynchronizer\n当你查看源码时你会惊讶的发现ReentrantLock并没有多少代码，另外有一个很明显的特点是：基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法，而Sync类继承了AbstractQueuedSynchronizer（AQS）。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer（简称同步器）的理解。\n\n在同步组件的实现中，AQS是核心部分，同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义，AQS则实现了对同步状态的管理，以及对阻塞线程进行排队，等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列，独占式锁的获取和释放，共享锁的获取和释放以及可中断锁，超时等待锁获取这些特性的实现，而这些实际上则是AQS提供出来的模板方法，归纳整理如下：\n**独占式锁：**\n```\nvoid acquire(int arg):\n独占式获取同步状态，如果获取失败则插入同步队列进行等待；\nvoid acquireInterruptibly(int arg):\n与acquire方法相同，但在同步队列中进行等待的时候可以检测中断；\nboolean tryAcquireNanos(int arg, long nanosTimeout):\n在acquireInterruptibly基础上增加了超时等待功能，在超时时间内没有获得同步状态返回false;\nboolean release(int arg):\n释放同步状态，该方法会唤醒在同步队列中的下一个节点\n```\n\n**共享式锁：**\n```\nvoid acquireShared(int arg):\n共享式获取同步状态，与独占式的区别在于同一时刻有多个线程获取同步状态\nvoid acquireSharedInterruptibly(int arg):\n在acquireShared方法基础上增加了能响应中断的功能\nboolean tryAcquireSharedNanos(int arg, long nanosTimeout):\n在acquireSharedInterruptibly基础上增加了超时等待的功能\nboolean releaseShared(int arg):共享式释放同步状态\n```\n\n##### ReentrantLock\nReentrantLock重入锁，是实现Lock接口的一个类，也是在实际编程中使用频率很高的一个锁，支持重入性，表示能够对共享资源能够重复加锁，即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增，释放自减的方式实现重入。与此同时，ReentrantLock还支持公平锁和非公平锁两种方式。那么，要想完完全全的弄懂ReentrantLock的话，主要也就是ReentrantLock同步语义的学习：1. 重入性的实现原理；2. 公平锁和非公平锁。\n\n##### 重入性的实现原理\n要想支持重入性，就要解决两个问题：1. 在线程获取锁的时候，如果已经获取锁的线程是当前线程的话则直接再次获取成功；2. 由于锁会被获取n次，那么只有锁在被释放同样的n次之后，该锁才算是完全释放成功。通过这篇文章，我们知道，同步组件主要是通过重写AQS的几个protected方法来表达自己的同步语义。针对第一个问题，我们来看看ReentrantLock是怎样实现的，以非公平锁为例，判断当前线程能否获得锁为例，核心方法为nonfairTryAcquire：\n\n```\nfinal boolean nonfairTryAcquire(int acquires) {\n    final Thread current = Thread.currentThread();\n    int c = getState();\n    //1. 如果该锁未被任何线程占有，该锁能被当前线程获取\n\tif (c == 0) {\n        if (compareAndSetState(0, acquires)) {\n            setExclusiveOwnerThread(current);\n            return true;\n        }\n    }\n\t//2.若被占有，检查占有线程是否是当前线程\n    else if (current == getExclusiveOwnerThread()) {\n\t\t// 3. 再次获取，计数加一\n        int nextc = c + acquires;\n        if (nextc < 0) // overflow\n            throw new Error(\"Maximum lock count exceeded\");\n        setState(nextc);\n        return true;\n    }\n    return false;\n}\n\n```\n这段代码的逻辑也很简单，具体请看注释。为了支持重入性，在第二步增加了处理逻辑，如果该锁已经被线程所占有了，会继续检查占有线程是否为当前线程，如果是的话，同步状态加1返回true，表示可以再次获取成功。每次重新获取都会对同步状态进行加一的操作，那么释放的时候处理思路是怎样的了？（依然还是以非公平锁为例）核心方法为tryRelease：\n```\nprotected final boolean tryRelease(int releases) {\n\t//1. 同步状态减1\n    int c = getState() - releases;\n    if (Thread.currentThread() != getExclusiveOwnerThread())\n        throw new IllegalMonitorStateException();\n    boolean free = false;\n    if (c == 0) {\n\t\t//2. 只有当同步状态为0时，锁成功被释放，返回true\n        free = true;\n        setExclusiveOwnerThread(null);\n    }\n\t// 3. 锁未被完全释放，返回false\n    setState(c);\n    return free;\n}\n\n```\n代码的逻辑请看注释，需要注意的是，重入锁的释放必须得等到同步状态为0时锁才算成功释放，否则锁仍未释放。如果锁被获取n次，释放了n-1次，该锁未完全释放返回false，只有被释放n次才算成功释放，返回true。到现在我们可以理清ReentrantLock重入性的实现了，也就是理解了同步语义的第一条.\n\n##### 公平锁与非公平锁\nReentrantLock支持两种锁：公平锁和非公平锁。何谓公平性，是针对获取锁而言的，如果一个锁是公平的，那么锁的获取顺序就应该符合请求上的绝对时间顺序，满足FIFO。ReentrantLock的构造方法无参时是构造非公平锁，源码为：\n```\npublic ReentrantLock() {\n    sync = new NonfairSync();\n}\n```\n另外还提供了另外一种方式，可传入一个boolean值，true时为公平锁，false时为非公平锁，源码为：\n```\npublic ReentrantLock(boolean fair) {\n    sync = fair ? new FairSync() : new NonfairSync();\n}\n```\n在上面非公平锁获取时（nonfairTryAcquire方法）只是简单的获取了一下当前状态做了一些逻辑处理，并没有考虑到当前同步队列中线程等待的情况。我们来看看公平锁的处理逻辑是怎样的，核心方法为：\n\n```\nprotected final boolean tryAcquire(int acquires) {\n    final Thread current = Thread.currentThread();\n    int c = getState();\n    if (c == 0) {\n        if (!hasQueuedPredecessors() &&\n            compareAndSetState(0, acquires)) {\n            setExclusiveOwnerThread(current);\n            return true;\n        }\n    }\n    else if (current == getExclusiveOwnerThread()) {\n        int nextc = c + acquires;\n        if (nextc < 0)\n            throw new Error(\"Maximum lock count exceeded\");\n        setState(nextc);\n        return true;\n    }\n    return false;\n  }\n}\n```\n这段代码的逻辑与nonfairTryAcquire基本上一直，唯一的不同在于增加了hasQueuedPredecessors的逻辑判断，方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断，如果有前驱节点说明有线程比当前线程更早的请求资源，根据公平性，当前线程请求资源失败。如果当前节点没有前驱节点的话，再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁，而非公平性锁则不一定，有可能刚释放锁的线程能再次获取到锁。\n\n**公平锁 VS 非公平锁**\n> 公平锁每次获取到锁为同步队列中的第一个节点，保证请求资源时间上的绝对顺序，而非公平锁有可能刚释放锁的线程下次继续获取该锁，则有可能导致其他线程永远无法获取到锁，造成“饥饿”现象。\n> \n>公平锁为了保证时间上的绝对顺序，需要频繁的上下文切换，而非公平锁会降低一定的上下文切换，降低性能开销。因此，ReentrantLock默认选择的是非公平锁，则是为了减少一部分上下文切换，保证了系统更大的吞吐量。\n\n##### ReentrantReadWriteLock\n在并发场景中用于解决线程安全的问题，我们几乎会高频率的使用到独占式锁，通常使用java提供的关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁，也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中，大部分只是读数据，写数据很少，如果仅仅是读数据的话并不会影响数据正确性（出现脏读），而如果在这种业务场景下，依然使用独占锁的话，很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况，java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问，但是在写线程访问时，所有的读线程和其他的写线程都会被阻塞。在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间，WriteLock与ReadLock之间以及ReadLock与ReadLock之间进行分析。这里做一个归纳总结：\n\n公平性选择：支持非公平性（默认）和公平的锁获取方式，吞吐量还是非公平优于公平；\n重入性：支持重入，读锁获取后能再次获取，写锁获取之后能够再次获取写锁，同时也能够获取读锁；\n锁降级：遵循获取写锁，获取读锁再释放写锁的次序，写锁能够降级成为读锁\n\n要想能够彻底的理解读写锁必须能够理解这样几个问题：1. 读写锁是怎样实现分别记录读写状态的？2. 写锁是怎样获取和释放的？3.读锁是怎样获取和释放的？我们带着这样的三个问题，再去了解下读写锁。\n\n###### 写锁详解\n\n**写锁的获取**\n\n同步组件的实现聚合了同步器（AQS），并通过重写重写同步器（AQS）中的方法实现同步组件的同步语义。因此，写锁的实现依然也是采用这种方式。在同一时刻写锁是不能被多个线程所获取，很显然写锁是独占式锁，而实现写锁的同步语义是通过重写AQS中的tryAcquire方法实现的。源码为:\n```\nprotected final boolean tryAcquire(int acquires) {\n    /*\n     * Walkthrough:\n     * 1. If read count nonzero or write count nonzero\n     *    and owner is a different thread, fail.\n     * 2. If count would saturate, fail. (This can only\n     *    happen if count is already nonzero.)\n     * 3. Otherwise, this thread is eligible for lock if\n     *    it is either a reentrant acquire or\n     *    queue policy allows it. If so, update state\n     *    and set owner.\n     */\n    Thread current = Thread.currentThread();\n\t// 1. 获取写锁当前的同步状态\n    int c = getState();\n\t// 2. 获取写锁获取的次数\n    int w = exclusiveCount(c);\n    if (c != 0) {\n        // (Note: if c != 0 and w == 0 then shared count != 0)\n\t\t// 3.1 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话\n\t\t// 当前线程获取写锁失败\n        if (w == 0 || current != getExclusiveOwnerThread())\n            return false;\n        if (w + exclusiveCount(acquires) > MAX_COUNT)\n            throw new Error(\"Maximum lock count exceeded\");\n        // Reentrant acquire\n\t\t// 3.2 当前线程获取写锁，支持可重复加锁\n        setState(c + acquires);\n        return true;\n    }\n\t// 3.3 写锁未被任何线程获取，当前线程可获取写锁\n    if (writerShouldBlock() ||\n        !compareAndSetState(c, c + acquires))\n        return false;\n    setExclusiveOwnerThread(current);\n    return true;\n}\n```\n这段代码的逻辑请看注释，这里有一个地方需要重点关注，exclusiveCount(c)方法，该方法源码为：\n```\nstatic int exclusiveCount(int c) { \n       return c & EXCLUSIVE_MASK; \n }\n```\n其中EXCLUSIVE_MASK为:  static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;      \nEXCLUSIVE_MASK为1左移16位然后减1，即为0x0000FFFF。而exclusiveCount方法是将同步状态（state为int类型）与0x0000FFFF相与，即取同步状态的低16位。那么低16位代表什么呢？根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数，现在就可以得出来一个结论同步状态的低16位用来表示写锁的获取次数。同时还有一个方法值得我们注意：\n```\nstatic int sharedCount(int c)    { return c >>> SHARED_SHIFT; }\n```\n该方法是获取读锁被获取的次数，是将同步状态（int c）右移16次，即取同步状态的高16位，现在我们可以得出另外一个结论同步状态的高16位用来表示读锁被获取的次数。现在还记得我们开篇说的需要弄懂的第一个问题吗？读写锁是怎样实现分别记录读锁和写锁的状态的，现在这个问题的答案就已经被我们弄清楚了，其示意图如下图所示：\n![f4bab9ebca5a35df042a681e4c91c7eb](大数据成神之路-Java高级特性增强(锁).resources/46AF9BE6-A1C5-418C-9836-CDE377311CC0.png)\n现在我们回过头来看写锁获取方法tryAcquire，其主要逻辑为：当读锁已经被读线程获取或者写锁已经被其他写线程获取，则写锁获取失败；否则，获取成功并支持重入，增加写状态。\n\n**写锁的释放**\n写锁释放通过重写AQS的tryRelease方法，源码为：\n```\nprotected final boolean tryRelease(int releases) {\n    if (!isHeldExclusively())\n        throw new IllegalMonitorStateException();\n\t//1. 同步状态减去写状态\n    int nextc = getState() - releases;\n\t//2. 当前写状态是否为0，为0则释放写锁\n    boolean free = exclusiveCount(nextc) == 0;\n    if (free)\n        setExclusiveOwnerThread(null);\n\t//3. 不为0则更新同步状态\n    setState(nextc);\n    return free;\n}\n\n```\n源码的实现逻辑请看注释，不难理解与ReentrantLock基本一致，这里需要注意的是，减少写状态int nextc = getState() - releases;只需要用当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的。\n\n###### 读锁详解\n\n**读锁的获取**\n看完了写锁，现在来看看读锁，读锁不是独占式锁，即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。按照之前对AQS介绍，实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法为：\n\n```\nprotected final int tryAcquireShared(int unused) {\n    /*\n     * Walkthrough:\n     * 1. If write lock held by another thread, fail.\n     * 2. Otherwise, this thread is eligible for\n     *    lock wrt state, so ask if it should block\n     *    because of queue policy. If not, try\n     *    to grant by CASing state and updating count.\n     *    Note that step does not check for reentrant\n     *    acquires, which is postponed to full version\n     *    to avoid having to check hold count in\n     *    the more typical non-reentrant case.\n     * 3. If step 2 fails either because thread\n     *    apparently not eligible or CAS fails or count\n     *    saturated, chain to version with full retry loop.\n     */\n    Thread current = Thread.currentThread();\n    int c = getState();\n\t//1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话，当前\n\t// 线程获取读锁失败返回-1\n    if (exclusiveCount(c) != 0 &&\n        getExclusiveOwnerThread() != current)\n        return -1;\n    int r = sharedCount(c);\n    if (!readerShouldBlock() &&\n        r < MAX_COUNT &&\n\t\t//2. 当前线程获取读锁\n        compareAndSetState(c, c + SHARED_UNIT)) {\n\t\t//3. 下面的代码主要是新增的一些功能，比如getReadHoldCount()方法\n\t\t//返回当前获取读锁的次数\n        if (r == 0) {\n            firstReader = current;\n            firstReaderHoldCount = 1;\n        } else if (firstReader == current) {\n            firstReaderHoldCount++;\n        } else {\n            HoldCounter rh = cachedHoldCounter;\n            if (rh == null || rh.tid != getThreadId(current))\n                cachedHoldCounter = rh = readHolds.get();\n            else if (rh.count == 0)\n                readHolds.set(rh);\n            rh.count++;\n        }\n        return 1;\n    }\n\t//4. 处理在第二步中CAS操作失败的自旋已经实现重入性\n    return fullTryAcquireShared(current);\n}\n\n```\n代码的逻辑请看注释，需要注意的是  当写锁被其他线程获取后，读锁获取失败，否则获取成功利用CAS更新同步状态。另外，当前同步状态需要加上SHARED_UNIT（(1 << SHARED_SHIFT)即0x00010000）的原因这是我们在上面所说的同步状态的高16位用来表示读锁被获取的次数。如果CAS失败或者已经获取读锁的线程再次获取读锁时，是靠fullTryAcquireShared方法实现的，有兴趣可以看看。\n\n**读锁的释放**\n读锁释放的实现主要通过方法tryReleaseShared，源码如下，主要逻辑请看注释：\n```\nprotected final boolean tryReleaseShared(int unused) {\n    Thread current = Thread.currentThread();\n\t// 前面还是为了实现getReadHoldCount等新功能\n    if (firstReader == current) {\n        // assert firstReaderHoldCount > 0;\n        if (firstReaderHoldCount == 1)\n            firstReader = null;\n        else\n            firstReaderHoldCount--;\n    } else {\n        HoldCounter rh = cachedHoldCounter;\n        if (rh == null || rh.tid != getThreadId(current))\n            rh = readHolds.get();\n        int count = rh.count;\n        if (count <= 1) {\n            readHolds.remove();\n            if (count <= 0)\n                throw unmatchedUnlockException();\n        }\n        --rh.count;\n    }\n    for (;;) {\n        int c = getState();\n\t\t// 读锁释放 将同步状态减去读状态即可\n        int nextc = c - SHARED_UNIT;\n        if (compareAndSetState(c, nextc))\n            // Releasing the read lock has no effect on readers,\n            // but it may allow waiting writers to proceed if\n            // both read and write locks are now free.\n            return nextc == 0;\n    }\n}\n\n```\n###### 锁降级\n读写锁支持锁降级，遵循按照获取写锁，获取读锁再释放写锁的次序，写锁能够降级成为读锁，不支持锁升级，关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中：\n```\nvoid processCachedData() {\n        rwl.readLock().lock();\n        if (!cacheValid) {\n            // Must release read lock before acquiring write lock\n            rwl.readLock().unlock();\n            rwl.writeLock().lock();\n            try {\n                // Recheck state because another thread might have\n                // acquired write lock and changed state before we did.\n                if (!cacheValid) {\n                    data = ...\n            cacheValid = true;\n          }\n          // Downgrade by acquiring read lock before releasing write lock\n          rwl.readLock().lock();\n        } finally {\n          rwl.writeLock().unlock(); // Unlock write, still hold read\n        }\n      }\n \n      try {\n        use(data);\n      } finally {\n        rwl.readLock().unlock();\n      }\n    }\n}\n```\n\n-----------\n**参考文章和书籍：**\n\n《Java并发编程的艺术》\n《实战Java高并发程序设计》\nhttps://blog.csdn.net/qq_34337272/article/details/79680771\nhttps://www.jianshu.com/p/a5f99f25329a\nhttps://www.jianshu.com/p/506c1e38a922"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强(集合框架).md",
    "content": "### **Java高级特性增强-集合框架(ArrayList/Vector)**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n\n* * *\n## 集合框架\n#### Java中的集合框架\n\nArrayList/Vector\nLinkedList\nHashMap\nHashSet\nLinkedHashMap\n...\n本章内容参考引用网上的内容为主，网上有大量优质的资源，作者在这里做了整理如下：\n\n##### ArrayList/Vector\n**ArrayList简介**\n　　ArrayList 的底层是数组队列，相当于动态数组。与 Java 中的数组相比，它的容量能动态增长。在添加大量元素前，应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。\n\n它继承于 AbstractList，实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。\n\n在我们学数据结构的时候就知道了线性表的顺序存储，插入删除元素的时间复杂度为O（n）,求表长以及增加元素，取第 i 元素的时间复杂度为O（1）\n\n　 ArrayList 继承了AbstractList，实现了List。它是一个数组队列，提供了相关的添加、删除、修改、遍历等功能。\n\n　　ArrayList 实现了RandomAccess 接口，即提供了随机访问功能。RandomAccess 是 Java 中用来被 List 实现，为 List 提供快速访问功能的。在 ArrayList 中，我们即可以通过元素的序号快速获取元素对象，这就是快速随机访问。\n　　ArrayList 实现了Cloneable 接口，即覆盖了函数 clone()，能被克隆。\n　　ArrayList 实现java.io.Serializable 接口，这意味着ArrayList支持序列化，能通过序列化去传输。\n\n　　和Vector 不同，ArrayList 中的操作不是线程安全的！所以，建议在单线程中才使用 ArrayList，而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。\n\n**ArrayList核心源码**\n```\npackage java.util;\n\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.function.UnaryOperator;\n\n\npublic class ArrayList<E> extends AbstractList<E>\n        implements List<E>, RandomAccess, Cloneable, java.io.Serializable\n{\n    private static final long serialVersionUID = 8683452581122892189L;\n\n    /**\n     * 默认初始容量大小\n     */\n    private static final int DEFAULT_CAPACITY = 10;\n\n    /**\n     * 空数组（用于空实例）。\n     */\n    private static final Object[] EMPTY_ELEMENTDATA = {};\n\n     //用于默认大小空实例的共享空数组实例。\n      //我们把它从EMPTY_ELEMENTDATA数组中区分出来，以知道在添加第一个元素时容量需要增加多少。\n    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};\n\n    /**\n     * 保存ArrayList数据的数组\n     */\n    transient Object[] elementData; // non-private to simplify nested class access\n\n    /**\n     * ArrayList 所包含的元素个数\n     */\n    private int size;\n\n    /**\n     * 带初始容量参数的构造函数。（用户自己指定容量）\n     */\n    public ArrayList(int initialCapacity) {\n        if (initialCapacity > 0) {\n            //创建initialCapacity大小的数组\n            this.elementData = new Object[initialCapacity];\n        } else if (initialCapacity == 0) {\n            //创建空数组\n            this.elementData = EMPTY_ELEMENTDATA;\n        } else {\n            throw new IllegalArgumentException(\"Illegal Capacity: \"+\n                                               initialCapacity);\n        }\n    }\n\n    /**\n     *默认构造函数，DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10，也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10\n     */\n    public ArrayList() {\n        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;\n    }\n\n    /**\n     * 构造一个包含指定集合的元素的列表，按照它们由集合的迭代器返回的顺序。\n     */\n    public ArrayList(Collection<? extends E> c) {\n        //\n        elementData = c.toArray();\n        //如果指定集合元素个数不为0\n        if ((size = elementData.length) != 0) {\n            // c.toArray 可能返回的不是Object类型的数组所以加上下面的语句用于判断，\n            //这里用到了反射里面的getClass()方法\n            if (elementData.getClass() != Object[].class)\n                elementData = Arrays.copyOf(elementData, size, Object[].class);\n        } else {\n            // 用空数组代替\n            this.elementData = EMPTY_ELEMENTDATA;\n        }\n    }\n\n    /**\n     * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 \n     */\n    public void trimToSize() {\n        modCount++;\n        if (size < elementData.length) {\n            elementData = (size == 0)\n              ? EMPTY_ELEMENTDATA\n              : Arrays.copyOf(elementData, size);\n        }\n    }\n//下面是ArrayList的扩容机制\n//ArrayList的扩容机制提高了性能，如果每次只扩充一个，\n//那么频繁的插入会导致频繁的拷贝，降低性能，而ArrayList的扩容机制避免了这种情况。\n    /**\n     * 如有必要，增加此ArrayList实例的容量，以确保它至少能容纳元素的数量\n     * @param   minCapacity   所需的最小容量\n     */\n    public void ensureCapacity(int minCapacity) {\n        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)\n            // any size if not default element table\n            ? 0\n            // larger than default for default empty table. It's already\n            // supposed to be at default size.\n            : DEFAULT_CAPACITY;\n\n        if (minCapacity > minExpand) {\n            ensureExplicitCapacity(minCapacity);\n        }\n    }\n   //得到最小扩容量\n    private void ensureCapacityInternal(int minCapacity) {\n        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {\n              // 获取默认的容量和传入参数的较大值\n            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);\n        }\n\n        ensureExplicitCapacity(minCapacity);\n    }\n  //判断是否需要扩容\n    private void ensureExplicitCapacity(int minCapacity) {\n        modCount++;\n\n        // overflow-conscious code\n        if (minCapacity - elementData.length > 0)\n            //调用grow方法进行扩容，调用此方法代表已经开始扩容了\n            grow(minCapacity);\n    }\n\n    /**\n     * 要分配的最大数组大小\n     */\n    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;\n\n    /**\n     * ArrayList扩容的核心方法。\n     */\n    private void grow(int minCapacity) {\n        // oldCapacity为旧容量，newCapacity为新容量\n        int oldCapacity = elementData.length;\n        //将oldCapacity 右移一位，其效果相当于oldCapacity /2，\n        //我们知道位运算的速度远远快于整除运算，整句运算式的结果就是将新容量更新为旧容量的1.5倍，\n        int newCapacity = oldCapacity + (oldCapacity >> 1);\n        //然后检查新容量是否大于最小需要容量，若还是小于最小需要容量，那么就把最小需要容量当作数组的新容量，\n        if (newCapacity - minCapacity < 0)\n            newCapacity = minCapacity;\n        //再检查新容量是否超出了ArrayList所定义的最大容量，\n        //若超出了，则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE，\n        //如果minCapacity大于最大容量，则新容量则为ArrayList定义的最大容量，否则，新容量大小则为 minCapacity。 \n        if (newCapacity - MAX_ARRAY_SIZE > 0)\n            newCapacity = hugeCapacity(minCapacity);\n        // minCapacity is usually close to size, so this is a win:\n        elementData = Arrays.copyOf(elementData, newCapacity);\n    }\n    //比较minCapacity和 MAX_ARRAY_SIZE\n    private static int hugeCapacity(int minCapacity) {\n        if (minCapacity < 0) // overflow\n            throw new OutOfMemoryError();\n        return (minCapacity > MAX_ARRAY_SIZE) ?\n            Integer.MAX_VALUE :\n            MAX_ARRAY_SIZE;\n    }\n\n    /**\n     *返回此列表中的元素数。 \n     */\n    public int size() {\n        return size;\n    }\n\n    /**\n     * 如果此列表不包含元素，则返回 true 。\n     */\n    public boolean isEmpty() {\n        //注意=和==的区别\n        return size == 0;\n    }\n\n    /**\n     * 如果此列表包含指定的元素，则返回true 。\n     */\n    public boolean contains(Object o) {\n        //indexOf()方法：返回此列表中指定元素的首次出现的索引，如果此列表不包含此元素，则为-1 \n        return indexOf(o) >= 0;\n    }\n\n    /**\n     *返回此列表中指定元素的首次出现的索引，如果此列表不包含此元素，则为-1 \n     */\n    public int indexOf(Object o) {\n        if (o == null) {\n            for (int i = 0; i < size; i++)\n                if (elementData[i]==null)\n                    return i;\n        } else {\n            for (int i = 0; i < size; i++)\n                //equals()方法比较\n                if (o.equals(elementData[i]))\n                    return i;\n        }\n        return -1;\n    }\n\n    /**\n     * 返回此列表中指定元素的最后一次出现的索引，如果此列表不包含元素，则返回-1。.\n     */\n    public int lastIndexOf(Object o) {\n        if (o == null) {\n            for (int i = size-1; i >= 0; i--)\n                if (elementData[i]==null)\n                    return i;\n        } else {\n            for (int i = size-1; i >= 0; i--)\n                if (o.equals(elementData[i]))\n                    return i;\n        }\n        return -1;\n    }\n\n    /**\n     * 返回此ArrayList实例的浅拷贝。 （元素本身不被复制。） \n     */\n    public Object clone() {\n        try {\n            ArrayList<?> v = (ArrayList<?>) super.clone();\n            //Arrays.copyOf功能是实现数组的复制，返回复制后的数组。参数是被复制的数组和复制的长度\n            v.elementData = Arrays.copyOf(elementData, size);\n            v.modCount = 0;\n            return v;\n        } catch (CloneNotSupportedException e) {\n            // 这不应该发生，因为我们是可以克隆的\n            throw new InternalError(e);\n        }\n    }\n\n    /**\n     *以正确的顺序（从第一个到最后一个元素）返回一个包含此列表中所有元素的数组。 \n     *返回的数组将是“安全的”，因为该列表不保留对它的引用。 （换句话说，这个方法必须分配一个新的数组）。\n     *因此，调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。\n     */\n    public Object[] toArray() {\n        return Arrays.copyOf(elementData, size);\n    }\n\n    /**\n     * 以正确的顺序返回一个包含此列表中所有元素的数组（从第一个到最后一个元素）; \n     *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组，则返回其中。 \n     *否则，将为指定数组的运行时类型和此列表的大小分配一个新数组。 \n     *如果列表适用于指定的数组，其余空间（即数组的列表数量多于此元素），则紧跟在集合结束后的数组中的元素设置为null 。\n     *（这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。） \n     */\n    @SuppressWarnings(\"unchecked\")\n    public <T> T[] toArray(T[] a) {\n        if (a.length < size)\n            // 新建一个运行时类型的数组，但是ArrayList数组的内容\n            return (T[]) Arrays.copyOf(elementData, size, a.getClass());\n            //调用System提供的arraycopy()方法实现数组之间的复制\n        System.arraycopy(elementData, 0, a, 0, size);\n        if (a.length > size)\n            a[size] = null;\n        return a;\n    }\n\n    // Positional Access Operations\n\n    @SuppressWarnings(\"unchecked\")\n    E elementData(int index) {\n        return (E) elementData[index];\n    }\n\n    /**\n     * 返回此列表中指定位置的元素。\n     */\n    public E get(int index) {\n        rangeCheck(index);\n\n        return elementData(index);\n    }\n\n    /**\n     * 用指定的元素替换此列表中指定位置的元素。 \n     */\n    public E set(int index, E element) {\n        //对index进行界限检查\n        rangeCheck(index);\n\n        E oldValue = elementData(index);\n        elementData[index] = element;\n        //返回原来在这个位置的元素\n        return oldValue;\n    }\n\n    /**\n     * 将指定的元素追加到此列表的末尾。 \n     */\n    public boolean add(E e) {\n        ensureCapacityInternal(size + 1);  // Increments modCount!!\n        //这里看到ArrayList添加元素的实质就相当于为数组赋值\n        elementData[size++] = e;\n        return true;\n    }\n\n    /**\n     * 在此列表中的指定位置插入指定的元素。 \n     *先调用 rangeCheckForAdd 对index进行界限检查；然后调用 ensureCapacityInternal 方法保证capacity足够大；\n     *再将从index开始之后的所有成员后移一个位置；将element插入index位置；最后size加1。\n     */\n    public void add(int index, E element) {\n        rangeCheckForAdd(index);\n\n        ensureCapacityInternal(size + 1);  // Increments modCount!!\n        //arraycopy()这个实现数组之间复制的方法一定要看一下，下面就用到了arraycopy()方法实现数组自己复制自己\n        System.arraycopy(elementData, index, elementData, index + 1,\n                         size - index);\n        elementData[index] = element;\n        size++;\n    }\n\n    /**\n     * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧（从其索引中减去一个元素）。 \n     */\n    public E remove(int index) {\n        rangeCheck(index);\n\n        modCount++;\n        E oldValue = elementData(index);\n\n        int numMoved = size - index - 1;\n        if (numMoved > 0)\n            System.arraycopy(elementData, index+1, elementData, index,\n                             numMoved);\n        elementData[--size] = null; // clear to let GC do its work\n      //从列表中删除的元素 \n        return oldValue;\n    }\n\n    /**\n     * 从列表中删除指定元素的第一个出现（如果存在）。 如果列表不包含该元素，则它不会更改。\n     *返回true，如果此列表包含指定的元素\n     */\n    public boolean remove(Object o) {\n        if (o == null) {\n            for (int index = 0; index < size; index++)\n                if (elementData[index] == null) {\n                    fastRemove(index);\n                    return true;\n                }\n        } else {\n            for (int index = 0; index < size; index++)\n                if (o.equals(elementData[index])) {\n                    fastRemove(index);\n                    return true;\n                }\n        }\n        return false;\n    }\n\n    /*\n     * Private remove method that skips bounds checking and does not\n     * return the value removed.\n     */\n    private void fastRemove(int index) {\n        modCount++;\n        int numMoved = size - index - 1;\n        if (numMoved > 0)\n            System.arraycopy(elementData, index+1, elementData, index,\n                             numMoved);\n        elementData[--size] = null; // clear to let GC do its work\n    }\n\n    /**\n     * 从列表中删除所有元素。 \n     */\n    public void clear() {\n        modCount++;\n\n        // 把数组中所有的元素的值设为null\n        for (int i = 0; i < size; i++)\n            elementData[i] = null;\n\n        size = 0;\n    }\n\n    /**\n     * 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。\n     */\n    public boolean addAll(Collection<? extends E> c) {\n        Object[] a = c.toArray();\n        int numNew = a.length;\n        ensureCapacityInternal(size + numNew);  // Increments modCount\n        System.arraycopy(a, 0, elementData, size, numNew);\n        size += numNew;\n        return numNew != 0;\n    }\n\n    /**\n     * 将指定集合中的所有元素插入到此列表中，从指定的位置开始。\n     */\n    public boolean addAll(int index, Collection<? extends E> c) {\n        rangeCheckForAdd(index);\n\n        Object[] a = c.toArray();\n        int numNew = a.length;\n        ensureCapacityInternal(size + numNew);  // Increments modCount\n\n        int numMoved = size - index;\n        if (numMoved > 0)\n            System.arraycopy(elementData, index, elementData, index + numNew,\n                             numMoved);\n\n        System.arraycopy(a, 0, elementData, index, numNew);\n        size += numNew;\n        return numNew != 0;\n    }\n\n    /**\n     * 从此列表中删除所有索引为fromIndex （含）和toIndex之间的元素。\n     *将任何后续元素移动到左侧（减少其索引）。\n     */\n    protected void removeRange(int fromIndex, int toIndex) {\n        modCount++;\n        int numMoved = size - toIndex;\n        System.arraycopy(elementData, toIndex, elementData, fromIndex,\n                         numMoved);\n\n        // clear to let GC do its work\n        int newSize = size - (toIndex-fromIndex);\n        for (int i = newSize; i < size; i++) {\n            elementData[i] = null;\n        }\n        size = newSize;\n    }\n\n    /**\n     * 检查给定的索引是否在范围内。\n     */\n    private void rangeCheck(int index) {\n        if (index >= size)\n            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));\n    }\n\n    /**\n     * add和addAll使用的rangeCheck的一个版本\n     */\n    private void rangeCheckForAdd(int index) {\n        if (index > size || index < 0)\n            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));\n    }\n\n    /**\n     * 返回IndexOutOfBoundsException细节信息\n     */\n    private String outOfBoundsMsg(int index) {\n        return \"Index: \"+index+\", Size: \"+size;\n    }\n\n    /**\n     * 从此列表中删除指定集合中包含的所有元素。 \n     */\n    public boolean removeAll(Collection<?> c) {\n        Objects.requireNonNull(c);\n        //如果此列表被修改则返回true\n        return batchRemove(c, false);\n    }\n\n    /**\n     * 仅保留此列表中包含在指定集合中的元素。\n     *换句话说，从此列表中删除其中不包含在指定集合中的所有元素。 \n     */\n    public boolean retainAll(Collection<?> c) {\n        Objects.requireNonNull(c);\n        return batchRemove(c, true);\n    }\n\n\n    /**\n     * 从列表中的指定位置开始，返回列表中的元素（按正确顺序）的列表迭代器。\n     *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 \n     *返回的列表迭代器是fail-fast 。 \n     */\n    public ListIterator<E> listIterator(int index) {\n        if (index < 0 || index > size)\n            throw new IndexOutOfBoundsException(\"Index: \"+index);\n        return new ListItr(index);\n    }\n\n    /**\n     *返回列表中的列表迭代器（按适当的顺序）。 \n     *返回的列表迭代器是fail-fast 。\n     */\n    public ListIterator<E> listIterator() {\n        return new ListItr(0);\n    }\n\n    /**\n     *以正确的顺序返回该列表中的元素的迭代器。 \n     *返回的迭代器是fail-fast 。 \n     */\n    public Iterator<E> iterator() {\n        return new Itr();\n    }\n\n```\n**ArrayList源码分析**\n\n**System.arraycopy()和Arrays.copyOf()方法**\n　　通过上面源码我们发现这两个实现数组复制的方法被广泛使用而且很多地方都特别巧妙。比如下面add(int index, E element)方法就很巧妙的用到了arraycopy()方法让数组自己复制自己实现让index开始之后的所有成员后移一个位置:\n```\n    /**\n     * 在此列表中的指定位置插入指定的元素。 \n     *先调用 rangeCheckForAdd 对index进行界限检查；然后调用 ensureCapacityInternal 方法保证capacity足够大；\n     *再将从index开始之后的所有成员后移一个位置；将element插入index位置；最后size加1。\n     */\n    public void add(int index, E element) {\n        rangeCheckForAdd(index);\n\n        ensureCapacityInternal(size + 1);  // Increments modCount!!\n        //arraycopy()方法实现数组自己复制自己\n        //elementData:源数组;index:源数组中的起始位置;elementData：目标数组；index + 1：目标数组中的起始位置； size - index：要复制的数组元素的数量；\n        System.arraycopy(elementData, index, elementData, index + 1, size - index);\n        elementData[index] = element;\n        size++;\n    }\n```\n又如toArray()方法中用到了copyOf()方法\n```\n /**\n     *以正确的顺序（从第一个到最后一个元素）返回一个包含此列表中所有元素的数组。 \n     *返回的数组将是“安全的”，因为该列表不保留对它的引用。 （换句话说，这个方法必须分配一个新的数组）。\n     *因此，调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。\n     */\n    public Object[] toArray() {\n    //elementData：要复制的数组；size：要复制的长度\n        return Arrays.copyOf(elementData, size);\n    }\n```\n**两者联系与区别**\n联系:看两者源代码可以发现copyOf()内部调用了System.arraycopy()方法 区别:\narraycopy()需要目标数组，将原数组拷贝到你自己定义的数组里，而且可以选择拷贝的起点和长度以及放入新数组中的位置;\ncopyOf()是系统自动在内部新建一个数组，并返回该数组。\n\n**ArrayList 核心扩容技术**\n```\n//下面是ArrayList的扩容机制\n//ArrayList的扩容机制提高了性能，如果每次只扩充一个，\n//那么频繁的插入会导致频繁的拷贝，降低性能，而ArrayList的扩容机制避免了这种情况。\n    /**\n     * 如有必要，增加此ArrayList实例的容量，以确保它至少能容纳元素的数量\n     * @param   minCapacity   所需的最小容量\n     */\n    public void ensureCapacity(int minCapacity) {\n        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)\n            // any size if not default element table\n            ? 0\n            // larger than default for default empty table. It's already\n            // supposed to be at default size.\n            : DEFAULT_CAPACITY;\n\n        if (minCapacity > minExpand) {\n            ensureExplicitCapacity(minCapacity);\n        }\n    }\n   //得到最小扩容量\n    private void ensureCapacityInternal(int minCapacity) {\n        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {\n              // 获取默认的容量和传入参数的较大值\n            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);\n        }\n\n        ensureExplicitCapacity(minCapacity);\n    }\n  //判断是否需要扩容,上面两个方法都要调用\n    private void ensureExplicitCapacity(int minCapacity) {\n        modCount++;\n\n        // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话，就需要调用grow(minCapacity)方法扩容。\n        //这个minCapacity到底为多少呢？举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1\n        if (minCapacity - elementData.length > 0)\n            //调用grow方法进行扩容，调用此方法代表已经开始扩容了\n            grow(minCapacity);\n    }\n```\n```\n/**\n     * ArrayList扩容的核心方法。\n     */\n    private void grow(int minCapacity) {\n       //elementData为保存ArrayList数据的数组\n       ///elementData.length求数组长度elementData.size是求数组中的元素个数\n        // oldCapacity为旧容量，newCapacity为新容量\n        int oldCapacity = elementData.length;\n        //将oldCapacity 右移一位，其效果相当于oldCapacity /2，\n        //我们知道位运算的速度远远快于整除运算，整句运算式的结果就是将新容量更新为旧容量的1.5倍，\n        int newCapacity = oldCapacity + (oldCapacity >> 1);\n        //然后检查新容量是否大于最小需要容量，若还是小于最小需要容量，那么就把最小需要容量当作数组的新容量，\n        if (newCapacity - minCapacity < 0)\n            newCapacity = minCapacity;\n        //再检查新容量是否超出了ArrayList所定义的最大容量，\n        //若超出了，则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE，\n        //如果minCapacity大于最大容量，则新容量则为ArrayList定义的最大容量，否则，新容量大小则为 minCapacity。 \n        if (newCapacity - MAX_ARRAY_SIZE > 0)\n            newCapacity = hugeCapacity(minCapacity);\n        // minCapacity is usually close to size, so this is a win:\n        elementData = Arrays.copyOf(elementData, newCapacity);\n    }\n```\n扩容机制代码已经做了详细的解释。另外值得注意的是大家很容易忽略的一个运算符：移位运算符 　　简介：移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(带符号右移)和>>>(无符号右移)。 　　作用：对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源 　　比如这里：int newCapacity = oldCapacity + (oldCapacity >> 1); 右移一位相当于除2，右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。\n\n另外需要注意的是：\n\njava 中的length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.\n\njava 中的length()方法是针对字 符串String说的,如果想看这个字符串的长度则用到 length()这个方法.\n\n.java 中的size()方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!\n\n内部类\n```\n (1)private class Itr implements Iterator<E>  \n    (2)private class ListItr extends Itr implements ListIterator<E>  \n    (3)private class SubList extends AbstractList<E> implements RandomAccess  \n    (4)static final class ArrayListSpliterator<E> implements Spliterator<E>  \n```\nArrayList有四个内部类，其中的Itr是实现了Iterator接口，同时重写了里面的hasNext()，next()，remove()等方法；其中的ListItr继承Itr，实现了ListIterator接口，同时重写了hasPrevious()，nextIndex()，previousIndex()，previous()，set(E e)，add(E e)等方法，所以这也可以看出了Iterator和ListIterator的区别:ListIterator在Iterator的基础上增加了添加对象，修改对象，逆向遍历等方法，这些是Iterator不能实现的。\n\n**ArrayList经典Demo**\n```\nimport java.util.ArrayList;\nimport java.util.Iterator;\n\npublic class ArrayListDemo {\n\n    public static void main(String[] srgs){\n         ArrayList<Integer> arrayList = new ArrayList<Integer>();\n\n         System.out.printf(\"Before add:arrayList.size() = %d\\n\",arrayList.size());\n\n         arrayList.add(1);\n         arrayList.add(3);\n         arrayList.add(5);\n         arrayList.add(7);\n         arrayList.add(9);\n         System.out.printf(\"After add:arrayList.size() = %d\\n\",arrayList.size());\n\n         System.out.println(\"Printing elements of arrayList\");\n         // 三种遍历方式打印元素\n         // 第一种：通过迭代器遍历\n         System.out.print(\"通过迭代器遍历:\");\n         Iterator<Integer> it = arrayList.iterator();\n         while(it.hasNext()){\n             System.out.print(it.next() + \" \");\n         }\n         System.out.println();\n\n         // 第二种：通过索引值遍历\n         System.out.print(\"通过索引值遍历:\");\n         for(int i = 0; i < arrayList.size(); i++){\n             System.out.print(arrayList.get(i) + \" \");\n         }\n         System.out.println();\n\n         // 第三种：for循环遍历\n         System.out.print(\"for循环遍历:\");\n         for(Integer number : arrayList){\n             System.out.print(number + \" \");\n         }\n\n         // toArray用法\n         // 第一种方式(最常用)\n         Integer[] integer = arrayList.toArray(new Integer[0]);\n\n         // 第二种方式(容易理解)\n         Integer[] integer1 = new Integer[arrayList.size()];\n         arrayList.toArray(integer1);\n\n         // 抛出异常，java不支持向下转型\n         //Integer[] integer2 = new Integer[arrayList.size()];\n         //integer2 = arrayList.toArray();\n         System.out.println();\n\n         // 在指定位置添加元素\n         arrayList.add(2,2);\n         // 删除指定位置上的元素\n         arrayList.remove(2);    \n         // 删除指定元素\n         arrayList.remove((Object)3);\n         // 判断arrayList是否包含5\n         System.out.println(\"ArrayList contains 5 is: \" + arrayList.contains(5));\n\n         // 清空ArrayList\n         arrayList.clear();\n         // 判断ArrayList是否为空\n         System.out.println(\"ArrayList is empty: \" + arrayList.isEmpty());\n    }\n}\n```\n#### Vector\nVector 也是实现于 List 接口，底层数据结构和 ArrayList 类似,也是一个动态数组存放数据。不过是在 add() 方法的时候使用 synchronized 进行同步写数据，但是开销较大，所以 Vector 是一个同步容器并不是一个并发容器。\n以下是 add() 方法：\n```\npublic synchronized boolean add(E e) {\n        modCount++;\n        ensureCapacityHelper(elementCount + 1);\n        elementData[elementCount++] = e;\n        return true;\n    }\n```\n以及指定位置插入数据:\n```\npublic void add(int index, E element) {\n        insertElementAt(element, index);\n    }\n    public synchronized void insertElementAt(E obj, int index) {\n        modCount++;\n        if (index > elementCount) {\n            throw new ArrayIndexOutOfBoundsException(index\n                                                     + \" > \" + elementCount);\n        }\n        ensureCapacityHelper(elementCount + 1);\n        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);\n        elementData[index] = obj;\n        elementCount++;\n    }\n```\n\n-----------\n**参考文章和书籍：**\n《Effective Java》\n感谢以下作者：\nhttps://www.cnblogs.com/skywang12345/p/3308556.html\nhttps://crossoverjie.top/JCSprout/#/collections/ArrayList\nhttps://github.com/Snailclimb/JavaGuide/blob/master/Java%E7%9B%B8%E5%85%B3/ArrayList.md\nhttps://blog.csdn.net/qq_34337272/article/details/79680771\nhttps://www.jianshu.com/p/a5f99f25329a\nhttps://www.jianshu.com/p/506c1e38a922"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强-NIO.md",
    "content": "### **Java高级特性增强-NIO**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n\n\n#### **多线程**\n\n\n### **集合框架**\n\n\n### **NIO**\n\n\n\n### **Java并发容器**\n\n\n## NIO大纲\n\n\nNIO概览\n\n\nJava NIO之Buffer(缓冲区)\n\n\nJava NIO之Channel（通道）\n\n\nJava NIO之Selector（选择器）\n\n\nJava NIO之拥抱Path和Files\n"
  },
  {
    "path": "Java高级特性增强/大数据成神之路-Java高级特性增强.md",
    "content": "### **Java高级特性增强-集合框架(LinkedList)**\n本部分网络上有大量的资源可以参考，在这里做了部分整理，感谢前辈的付出，每节文章末尾有引用列表，源码推荐看JDK1.8以后的版本，注意甄别~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n\n* * *\n## 集合框架\n#### Java中的集合框架\n\nArrayList/Vector\nLinkedList\nHashMap\nHashSet\nLinkedHashMap\n...\n本章内容参考引用网上的内容为主，网上有大量优质的资源，作者在这里做了整理如下：\n\n#### LinkedList（基于JDK1.8）\n##### LinkedList 定义\n**LinkedList 是一个用链表实现的集合，元素有序且可以重复。**\n```\npublic class LinkedList<E>\n     extends AbstractSequentialList<E>\n     implements List<E>, Deque<E>, Cloneable, java.io.Serializable\n```\n![5c1b35ed236b91d19b2ca1c0990e634a](大数据成神之路-Java高级特性增强.resources/1120165-20180329133938645-733252704.png)\n和 ArrayList 集合一样，LinkedList 集合也实现了Cloneable接口和Serializable接口，分别用来支持克隆以及支持序列化。List 接口也不用多说，定义了一套 List 集合类型的方法规范。\n　　注意，相对于 ArrayList 集合，LinkedList 集合多实现了一个 Deque 接口，这是一个双向队列接口，双向队列就是两端都可以进行增加和删除操作。\n\n##### 字段属性\n```\n//链表元素（节点）的个数\n    transient int size = 0;\n\n    /**\n     *指向第一个节点的指针\n     */\n    transient Node<E> first;\n\n    /**\n     *指向最后一个节点的指针\n     */\n    transient Node<E> last;\n```\n注意这里出现了一个 Node 类，这是 LinkedList 类中的一个内部类，其中每一个元素就代表一个 Node 类对象，LinkedList 集合就是由许多个 Node 对象类似于手拉着手构成。\n```\nprivate static class Node<E> {\n        E item;//实际存储的元素\n        Node<E> next;//指向上一个节点的引用\n        Node<E> prev;//指向下一个节点的引用\n\n        //构造函数\n        Node(Node<E> prev, E element, Node<E> next) {\n            this.item = element;\n            this.next = next;\n            this.prev = prev;\n        }\n    }\n```\n如下图所示：\n![5ae4ad6157d68b83b54b3e4f9684f7aa](大数据成神之路-Java高级特性增强.resources/1120165-20180402091402743-458763981.png)\n上图的 LinkedList 是有四个元素，也就是由 4 个 Node 对象组成，size=4，head 指向第一个elementA,tail指向最后一个节点elementD。\n\n##### 构造函数 \n```\npublic LinkedList() {\n    }\n    public LinkedList(Collection<? extends E> c) {\n        this();\n        addAll(c);\n    }\n```\nLinkedList 有两个构造函数，第一个是默认的空的构造函数，第二个是将已有元素的集合Collection 的实例添加到 LinkedList 中，调用的是 addAll() 方法，这个方法下面我们会介绍。\n　　注意：LinkedList 是没有初始化链表大小的构造函数，因为链表不像数组，一个定义好的数组是必须要有确定的大小，然后去分配内存空间，而链表不一样，它没有确定的大小，通过指针的移动来指向下一个内存地址的分配。\n\n##### 添加元素\n**addFirst(E e)**\n将指定元素添加到链表头\n```\n//将指定的元素附加到链表头节点\n    public void addFirst(E e) {\n        linkFirst(e);\n    }\n    private void linkFirst(E e) {\n        final Node<E> f = first;//将头节点赋值给 f\n        final Node<E> newNode = new Node<>(null, e, f);//将指定元素构造成一个新节点，此节点的指向下一个节点的引用为头节点\n        first = newNode;//将新节点设为头节点，那么原先的头节点 f 变为第二个节点\n        if (f == null)//如果第二个节点为空，也就是原先链表是空\n            last = newNode;//将这个新节点也设为尾节点（前面已经设为头节点了）\n        else\n            f.prev = newNode;//将原先的头节点的上一个节点指向新节点\n        size++;//节点数加1\n        modCount++;//和ArrayList中一样，iterator和listIterator方法返回的迭代器和列表迭代器实现使用。\n    }\n```\n**addLast(E e)和add(E e)**\n将指定元素添加到链表尾\n```\n//将元素添加到链表末尾\n    public void addLast(E e) {\n        linkLast(e);\n    }\n    //将元素添加到链表末尾\n    public boolean add(E e) {\n        linkLast(e);\n        return true;\n    }\n    void linkLast(E e) {\n        final Node<E> l = last;//将l设为尾节点\n        final Node<E> newNode = new Node<>(l, e, null);//构造一个新节点，节点上一个节点引用指向尾节点l\n        last = newNode;//将尾节点设为创建的新节点\n        if (l == null)//如果尾节点为空，表示原先链表为空\n            first = newNode;//将头节点设为新创建的节点（尾节点也是新创建的节点）\n        else\n            l.next = newNode;//将原来尾节点下一个节点的引用指向新节点\n        size++;//节点数加1\n        modCount++;//和ArrayList中一样，iterator和listIterator方法返回的迭代器和列表迭代器实现使用。\n    }\n```\n**add(int index, E element)**\n将指定的元素插入此列表中的指定位置\n```\n//将指定的元素插入此列表中的指定位置\n    public void add(int index, E element) {\n        //判断索引 index >= 0 && index <= size中时抛出IndexOutOfBoundsException异常\n        checkPositionIndex(index);\n\n        if (index == size)//如果索引值等于链表大小\n            linkLast(element);//将节点插入到尾节点\n        else\n            linkBefore(element, node(index));\n    }\n    void linkLast(E e) {\n        final Node<E> l = last;//将l设为尾节点\n        final Node<E> newNode = new Node<>(l, e, null);//构造一个新节点，节点上一个节点引用指向尾节点l\n        last = newNode;//将尾节点设为创建的新节点\n        if (l == null)//如果尾节点为空，表示原先链表为空\n            first = newNode;//将头节点设为新创建的节点（尾节点也是新创建的节点）\n        else\n            l.next = newNode;//将原来尾节点下一个节点的引用指向新节点\n        size++;//节点数加1\n        modCount++;//和ArrayList中一样，iterator和listIterator方法返回的迭代器和列表迭代器实现使用。\n    }\n    Node<E> node(int index) {\n        if (index < (size >> 1)) {//如果插入的索引在前半部分\n            Node<E> x = first;//设x为头节点\n            for (int i = 0; i < index; i++)//从开始节点到插入节点索引之间的所有节点向后移动一位\n                x = x.next;\n            return x;\n        } else {//如果插入节点位置在后半部分\n            Node<E> x = last;//将x设为最后一个节点\n            for (int i = size - 1; i > index; i--)//从最后节点到插入节点的索引位置之间的所有节点向前移动一位\n                x = x.prev;\n            return x;\n        }\n    }\n    void linkBefore(E e, Node<E> succ) {\n        final Node<E> pred = succ.prev;//将pred设为插入节点的上一个节点\n        final Node<E> newNode = new Node<>(pred, e, succ);//将新节点的上引用设为pred,下引用设为succ\n        succ.prev = newNode;//succ的上一个节点的引用设为新节点\n        if (pred == null)//如果插入节点的上一个节点引用为空\n            first = newNode;//新节点就是头节点\n        else\n            pred.next = newNode;//插入节点的下一个节点引用设为新节点\n        size++;\n        modCount++;\n    }\n```\naddAll(Collection<? extends E> c)\n　　按照指定集合的迭代器返回的顺序，将指定集合中的所有元素追加到此列表的末尾\n\n　　此方法还有一个 addAll(int index, Collection<? extends E> c)，将集合 c 中所有元素插入到指定索引的位置。其实 \naddAll(Collection<? extends E> c) ==  addAll(size, Collection<? extends E> c)\n##### 删除元素\n删除元素和添加元素一样，也是通过更改指向上一个节点和指向下一个节点的引用即可.\n**remove()和removeFirst()**\n　　从此列表中移除并返回第一个元素\n**removeLast()**\n　　从该列表中删除并返回最后一个元素\n**remove(int index)**\n　　删除此列表中指定位置的元素\n**remove(Object o)**\n　　如果存在，则从该列表中删除指定元素的第一次出现\n　　此方法本质上和 remove(int index) 没多大区别，通过循环判断元素进行删除，需要注意的是，是删除第一次出现的元素，不是所有的。\n  \n##### 修改元素\n通过调用 set(int index, E element) 方法，用指定的元素替换此列表中指定位置的元素。\n```\npublic E set(int index, E element) {\n        //判断索引 index >= 0 && index <= size中时抛出IndexOutOfBoundsException异常\n        checkElementIndex(index);\n        Node<E> x = node(index);//获取指定索引处的元素\n        E oldVal = x.item;\n        x.item = element;//将指定位置的元素替换成要修改的元素\n        return oldVal;//返回指定索引位置原来的元素\n    }\n```\n这里主要是通过 node(index) 方法获取指定索引位置的节点，然后修改此节点位置的元素即可。\n##### 查找元素\ngetFirst()\n　　返回此列表中的第一个元素\ngetLast()\n　　返回此列表中的最后一个元素\nget(int index)\n　　返回指定索引处的元素\nindexOf(Object o)\n　　返回此列表中指定元素第一次出现的索引，如果此列表不包含元素，则返回-1。\n  \n##### 遍历集合\n**普通for循环**\n```\nLinkedList<String> linkedList = new LinkedList<>();\nlinkedList.add(\"A\");\nlinkedList.add(\"B\");\nlinkedList.add(\"C\");\nlinkedList.add(\"D\");\nfor(int i = 0 ; i < linkedList.size() ; i++){\n    System.out.print(linkedList.get(i)+\" \");//A B C D\n}\n```\n代码很简单，我们就利用 LinkedList 的 get(int index) 方法，遍历出所有的元素。\n　　但是需要注意的是， get(int index) 方法每次都要遍历该索引之前的所有元素，这句话这么理解：\n　　比如上面的一个 LinkedList 集合，我放入了 A,B,C,D是个元素。总共需要四次遍历：\n　　第一次遍历打印 A：只需遍历一次。\n　　第二次遍历打印 B：需要先找到 A，然后再找到 B 打印。\n　　第三次遍历打印 C：需要先找到 A，然后找到 B，最后找到 C 打印。\n　　第四次遍历打印 D：需要先找到 A，然后找到 B，然后找到 C，最后找到 D。\n　　这样如果集合元素很多，越查找到后面（当然此处的get方法进行了优化，查找前半部分从前面开始遍历，查找后半部分从后面开始遍历，但是需要的时间还是很多）花费的时间越多。那么如何改进呢？\n  \n**迭代器**\n```\nLinkedList<String> linkedList = new LinkedList<>();\nlinkedList.add(\"A\");\nlinkedList.add(\"B\");\nlinkedList.add(\"C\");\nlinkedList.add(\"D\");\n\n\nIterator<String> listIt = linkedList.listIterator();\nwhile(listIt.hasNext()){\n    System.out.print(listIt.next()+\" \");//A B C D\n}\n\n//通过适配器模式实现的接口，作用是倒叙打印链表\nIterator<String> it = linkedList.descendingIterator();\nwhile(it.hasNext()){\n    System.out.print(it.next()+\" \");//D C B A\n}\n```\n在 LinkedList 集合中也有一个内部类 ListItr，方法实现大体上也差不多，通过移动游标指向每一次要遍历的元素，不用在遍历某个元素之前都要从头开始。其方法实现也比较简单：\n```\npublic ListIterator<E> listIterator(int index) {\n        checkPositionIndex(index);\n        return new ListItr(index);\n    }\n\n    private class ListItr implements ListIterator<E> {\n        private Node<E> lastReturned;\n        private Node<E> next;\n        private int nextIndex;\n        private int expectedModCount = modCount;\n\n        ListItr(int index) {\n            // assert isPositionIndex(index);\n            next = (index == size) ? null : node(index);\n            nextIndex = index;\n        }\n\n        public boolean hasNext() {\n            return nextIndex < size;\n        }\n\n        public E next() {\n            checkForComodification();\n            if (!hasNext())\n                throw new NoSuchElementException();\n\n            lastReturned = next;\n            next = next.next;\n            nextIndex++;\n            return lastReturned.item;\n        }\n\n        public boolean hasPrevious() {\n            return nextIndex > 0;\n        }\n\n        public E previous() {\n            checkForComodification();\n            if (!hasPrevious())\n                throw new NoSuchElementException();\n\n            lastReturned = next = (next == null) ? last : next.prev;\n            nextIndex--;\n            return lastReturned.item;\n        }\n\n        public int nextIndex() {\n            return nextIndex;\n        }\n\n        public int previousIndex() {\n            return nextIndex - 1;\n        }\n\n        public void remove() {\n            checkForComodification();\n            if (lastReturned == null)\n                throw new IllegalStateException();\n\n            Node<E> lastNext = lastReturned.next;\n            unlink(lastReturned);\n            if (next == lastReturned)\n                next = lastNext;\n            else\n                nextIndex--;\n            lastReturned = null;\n            expectedModCount++;\n        }\n\n        public void set(E e) {\n            if (lastReturned == null)\n                throw new IllegalStateException();\n            checkForComodification();\n            lastReturned.item = e;\n        }\n\n        public void add(E e) {\n            checkForComodification();\n            lastReturned = null;\n            if (next == null)\n                linkLast(e);\n            else\n                linkBefore(e, next);\n            nextIndex++;\n            expectedModCount++;\n        }\n\n        public void forEachRemaining(Consumer<? super E> action) {\n            Objects.requireNonNull(action);\n            while (modCount == expectedModCount && nextIndex < size) {\n                action.accept(next.item);\n                lastReturned = next;\n                next = next.next;\n                nextIndex++;\n            }\n            checkForComodification();\n        }\n\n        final void checkForComodification() {\n            if (modCount != expectedModCount)\n                throw new ConcurrentModificationException();\n        }\n    }\n```\n这里需要重点注意的是 modCount 字段，前面我们在增加和删除元素的时候，都会进行自增操作 modCount，这是因为如果想一边迭代，一边用集合自带的方法进行删除或者新增操作，都会抛出异常。（使用迭代器的增删方法不会抛异常）\n```\nfinal void checkForComodification() {\n            if (modCount != expectedModCount)\n                throw new ConcurrentModificationException();\n        }\n```\n比如：\n```\nLinkedList<String> linkedList = new LinkedList<>();\nlinkedList.add(\"A\");\nlinkedList.add(\"B\");\nlinkedList.add(\"C\");\nlinkedList.add(\"D\");\n\n\nIterator<String> listIt = linkedList.listIterator();\nwhile(listIt.hasNext()){\n    System.out.print(listIt.next()+\" \");//A B C D\n    //linkedList.remove();//此处会抛出异常\n    listIt.remove();//这样可以进行删除操作\n}\n```\n迭代器的另一种形式就是使用 foreach 循环，底层实现也是使用的迭代器.\n```\nLinkedList<String> linkedList = new LinkedList<>();\nlinkedList.add(\"A\");\nlinkedList.add(\"B\");\nlinkedList.add(\"C\");\nlinkedList.add(\"D\");\nfor(String str : linkedList){\n    System.out.print(str + \"\");\n}\n```\n\n-----------\n**参考文章和书籍：**\n《Effective Java》\n感谢以下作者：\nhttps://www.cnblogs.com/skywang12345/p/3308556.html\nhttps://crossoverjie.top/JCSprout/#/collections/ArrayList\nhttps://github.com/Snailclimb/JavaGuide/blob/master/Java%E7%9B%B8%E5%85%B3/ArrayList.md\nhttps://blog.csdn.net/qq_34337272/article/details/79680771\nhttps://www.jianshu.com/p/a5f99f25329a\nhttps://www.jianshu.com/p/506c1e38a922"
  },
  {
    "path": "Kafka/Apache-Kafka安装和使用.md",
    "content": "**Apache Kafka 编程实战您可能感性的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n**单机环境**\n\n官方建议使用JDK 1.8版本，因此本文使用的环境都是JDK1.8。关于JDK的安装，本文不再详述，默认Java环境已经具备。\n\n由于Kafka依赖zookeeper，kafka通过zookeeper现实分布式系统的协调，所以我们需要先安装zookeeper。\n\n接下来我们按照如下步骤，一步步来安装kafka：\n\n1、下载zookeeper，解压。\n\n下载地址：[https://zookeeper.apache.org/releases.html#download](http://link.zhihu.com/?target=https%3A//zookeeper.apache.org/releases.html%23download)\n\n2、创建zookeeper配置文件\n\n在zookeeper解压后的目录下找到conf文件夹，进入后，复制文件zoo_sample.cfg，并命名为zoo.cfg\n\nzoo.cfg中一共四个配置项，可以使用默认配置。\n\n3、启动zookeeper。\n\n进入zookeeper根目录执行 bin/zkServer.sh start\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-45642ac6542526af.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n4、下载kafka，解压。\n\nkafka 2.0版本下载地址：[https://www.apache.org/dyn/closer.cgi?path=/kafka/2.0.0/kafka_2.11-2.0.0.tgz](http://link.zhihu.com/?target=https%3A//www.apache.org/dyn/closer.cgi%3Fpath%3D/kafka/2.0.0/kafka_2.11-2.0.0.tgz)\n\n5、修改kafka的配置文件\n\n进入kafka根目录下的config文件夹下，打开server.properties,修改如下配置\n\nzookeeper.connect=localhost:2181\n\nbroker.id=0\n\nlog.dirs=/tmp/kafka-logs\n\nzookeeper.connect是zookeeper的链接信息，broker.id是当前kafka实例的id，log.dirs是kafka存储消息内容的路径。\n\n6、启动kafka\n\n进入kafka根目录执行 bin/kafka-server-start.sh config/server.properties\n\n此命令告诉kaka启动时使用config/server.properties配置项\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-7daff2291823ea47.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n启动kafka后，如果控制台没有报错信息，那么kafka应该已经启动成功了，我们可以通过查看zookeeper中相关节点值来确认。步骤如下：\n\n1、启动zookeeper的client\n\n进入zookeeper根目录下，执行 bin/zkCli.sh -server 127.0.0.1:2181。启动成功后如下图\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-e93e674f8687f166.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n2、输入命令 ls /brokers，回车，可以看到如下信息：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-332ed3a117128b6a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n这些子节点存储的就是kafka集群管理的数据。broker是kafka的一个服务单元实例\n\n3、我看看一下ids这个节点下的数据，输入命令 ls /brokers/ids，可以看到如下信息：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-d562b8ccbc167ca9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n还记得我们在配置单机环境时，修改的kafka配置项broker.id=0 吗？这里的0就是表示那个kafka的实例已经加入了kafka集群。\n\n### **集群环境**\n\n集群环境的搭建也很简单，在单机环境的基础上，让多个单机连接到同一个zookeeper即可。需要注意两点：\n\n1、每个实例设置不同的broker.id。\n\n2、如果多个实例部署在同一台服务器，还要注意修改log.dirs为不同目录，确保消息存储时不会有冲突。集群环境的具体搭建，在此精简教程中不再做详细讨论。\n\n发出你的第一条kafka消息\n\n我们通过kafka带的工具来创建一个topic，然后尝试发送和消费一个消息，直观的去感受下kafka。\n\n1、创建topic\n\n进入kafka根目录，执行如下命令：\n\n```\nbin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic study\n\n```\n\n执行成功后，创建了study这个topic，如下图所示：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-1ee1e88312b5607e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n此命令行有几个参数，分别指明了zookeeper的链接信息，分区和副本的数量等。关于分区和副本后续会仔细讲解，现在不用过多关注。\n\n2、启动消费者\n\n我们开启一个消费者并且订阅study这个topic，执行如下命令:\n\n```\nbin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic study --from-beginning\n\n```\n\n看到如下图，光标停留在最前面，没有任何信息输出，说明启动消费者成功，此时在等待新的消息。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-4c9ec1250f2a7d52.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n3、开启生产者\n\n新打开一个命令窗口，输入命令\n\n```\nbin/kafka-console-producer.sh --broker-list localhost:9092 --topic study\n\n```\n\n启动成功后，如下图，等待你输入新的消息。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-1dc35a39b6a6c1a6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n4、发送你的第一条消息\n\n在上面生产者的窗口输入一条消息 hello kafka,点击回车，如下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-aa03f34cc9fdc77d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n此时切换到消费者的窗口，可以看到消费者已经消费到这条消息，在窗口中打印了出来。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-148e5147382d1712.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n至此我们走完了一个发送消息的流程，可以看到我们经历了创建topic、启动生产者、消费者、生产者生产消息、消费者消费消息，这几个步骤。\n\n小结：通过本章节学习，相信你已经能够成功搭建起kafka单机环境，甚至集群环境。然后通过kafka自带的工具，直观的感受了kafka运转的整个过程。接下来的章节我们将会进入kafka的核心领域，也是本教程的重点章节，只有理解了kafka内在的设计理念和原理，才能做到活学活用。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-2349cea8df6b9d79.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"
  },
  {
    "path": "Kafka/Apache-Kafka核心概念.md",
    "content": "**Apache Kafka 编程实战您可能感性的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n本章是学习kafka的核心章节，涵盖内容比较多，在理解上有一定的难度，需要反复阅读理解，才能参透Kafka的设计思想。\n\n## **1、Kafka集群结构**\n\n在第一章我给出过一个消息系统通用的结构图，也就是下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-97a804c5b792e098.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n实际上kafka的结构图是有些区别的，现在我们看下面的图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-13008fb6c743c391.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\nproducer和consumer想必大家都很熟悉，一个生产消息，一个消费掉消息。这里就不再做太多解释。\n\n此图和第一张图可以看到有几个区别：\n\n1、多了zookeeper集群，通过前几章的学习我们已经知道kafka是配合zookeeper进行工作的。\n\n2、kafka集群中可以看到有若干个Broker，其中一个broker是leader，其他的broker是follower\n\n3、consumer外面包裹了一层Consumer group。\n\n我们先讲解一下Broker和consumer group的概念，以及Topic。\n\n### **Broker**\n\n一个Borker就是Kafka集群中的一个实例，或者说是一个服务单元。连接到同一个zookeeper的多个broker实例组成kafka的集群。在若干个broker中会有一个broker是leader，其余的broker为follower。leader在集群启动时候选举出来，负责和外部的通讯。当leader死掉的时候，follower们会再次通过选举，选择出新的leader，确保集群的正常工作。\n\n### **Consumer Group**\n\nKafka和其它消息系统有一个不一样的设计，在consumer之上加了一层group。同一个group的consumer可以并行消费同一个topic的消息，但是同group的consumer，不会重复消费。这就好比多个consumer组成了一个团队，一起干活，当然干活的速度就上来了。group中的consumer是如何配合协调的，其实和topic的分区相关联，后面我们会详细论述。\n\n如果同一个topic需要被多次消费，可以通过设立多个consumer group来实现。每个group分别消费，互不影响。\n\n通过本节学习，我们从全局的层面了解了kafka的结构，接下来我们会深入到kafka内部，来看看它是怎么工作的。\n\n### **Topic**\n\nkafka中消息订阅和发送都是基于某个topic。比如有个topic叫做NBA赛事信息，那么producer会把NBA赛事信息的消息发送到此topic下面。所有订阅此topic的consumer将会拉取到此topic下的消息。Topic就像一个特定主题的收件箱，producer往里丢，consumer取走。\n\n## **2、Kafka核心概念简介**\n\nkafka采用分区（Partition）的方式，使得消费者能够做到并行消费，从而大大提高了自己的吞吐能力。同时为了实现高可用，每个分区又有若干份副本（Replica），这样在某个broker挂掉的情况下，数据不会丢失。\n\n接下来我们详细分析kafka是如何基于Partition和Replica工作的。\n\n### **分区（Partition）**\n\n大多数消息系统，同一个topic下的消息，存储在一个队列。分区的概念就是把这个队列划分为若干个小队列，每一个小队列就是一个分区，如下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-9a3c2f94e9a3f1e2.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n这样做的好处是什么呢？其实从上图已经可以看出来。无分区时，一个topic只有一个消费者在消费这个消息队列。采用分区后，如果有两个分区，最多两个消费者同时消费，消费的速度肯定会更快。如果觉得不够快，可以加到四个分区，让四个消费者并行消费。分区的设计大大的提升了kafka的吞吐量！！\n\n我们再结合下图继续讲解Partition。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-6244f962f753896a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n此图包含如下几个知识点：\n\n1、一个partition只能被同组的一个consumer消费（图中只会有一个箭头指向一个partition）\n\n2、同一个组里的一个consumer可以消费多个partition（图中第一个consumer消费Partition 0和3）\n\n3、消费效率最高的情况是partition和consumer数量相同。这样确保每个consumer专职负责一个partition。\n\n4、consumer数量不能大于partition数量。由于第一点的限制，当consumer多于partition时，就会有consumer闲置。\n\n5、consumer group可以认为是一个订阅者的集群，其中的每个consumer负责自己所消费的分区\n\n为了加深理解，我举个吃苹果的例子。\n\n问题：有一篮子苹果，你如何把这一篮子苹果尽可能快的吃完？\n\n办法一：\n\n我一个人，一个一个苹果吃，如下图。这样显然很慢，我吃完一个才能拿下一个。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-9814acef701020a3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n办法二：\n\n我再找两个人来一块吃，第一个人拿走一个去吃，然后第二个人拿一个去吃，接着第三个人拿一个去吃，如此循环。速度肯定快了，但是三个人还是会排队等待。三个人排队时间可能很短，但是如果叫了100个人帮忙吃呢？会有大量时间消耗在排队上。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-db84e50fced1175b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n办法三：\n\n我还是找两个人来一块吃，但我把苹果提前分到三个盘子里，每人分一个盘子，自己吃自己的，这样不但能三个人同时吃苹果，还无须排队。速度显然是最快的。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-55c15a807f854860.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n办法三正是kafka所采用的设计方式，盘子就是partition，每个人就是一个consumer，每个苹果就是一条message。办法三每个盘子中苹果的消费是有序的，而办法二的消费是完全无序的。\n\n相信通过这个例子你一定能充分理解partition的概念，以及为什么kafka会如此设计。\n\n关于partition暂时说到这里，接下来介绍副本。\n\n### **副本（Replica）**\n\n提到副本，肯定就会想到正本。副本是正本的拷贝。在kafka中，正本和副本都称之为副本（Repalica），但存在leader和follower之分。活跃的称之为leader，其他的是follower。\n\n每个分区的数据都会有多份副本，以此来保证Kafka的高可用。\n\nTopic、partition、replica的关系如下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-717fb16f478b3c06.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\ntopic下会划分多个partition，每个partition都有自己的replica，其中只有一个是leader replica，其余的是follower replica。\n\n消息进来的时候会先存入leader replica，然后从leader replica复制到follower replica。只有复制全部完成时，consumer才可以消费此条消息。这是为了确保意外发生时，数据可以恢复。consumer的消费也是从leader replica读取的。\n\n由此可见，leader replica做了大量的工作。所以如果不同partition的leader replica在kafka集群的broker上分布不均匀，就会造成负载不均衡。\n\nkafka通过轮询算法保证leader replica是均匀分布在多个broker上。如下图。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-4b32d7dc8b180a44.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n可以看到每个partition的leader replica均匀的分布在三个broker上，follower replica也是均匀分布的。关于Replica，有如下知识点：\n\n1、Replica均匀分配在Broker上，同一个partition的replica不会在同一个borker上\n\n2、同一个partition的Replica数量不能多于broker数量。多个replica为了数据安全，一台server存多个replica没有意义。server挂掉，上面的副本都要挂掉。\n\n3、分区的leader replica均衡分布在broker上。此时集群的负载是均衡的。这就叫做分区平衡\n\n分区平衡是个很重要的概念，接下来我们就来讲解分区平衡。\n\n### **分区平衡**\n\n在讲分区平衡前，先讲几个概念：\n\n1、AR： assigned replicas，已分配的副本。每个partition都有自己的AR列表，里面存储着这个partition最初分配的所有replica。注意AR列表不会变化，除非增加分区。\n\n2、PR（优先replica）：AR列表中的第一个replica就是优先replica，而且永远是优先replica。最初，优先replica和leader replica是同一个replica。\n\n3、ISR：in sync replicas，同步副本。每个partition都有自己的ISR列表。ISR是会根据同步情况动态变化的。\n\n最初ISR列表和AR列表是一致的，但由于某个节点死掉，或者某个节点的follower replica落后leader replica太多，那么该节点就会被从ISR列表中移除。此时，ISR和AR就不再一致\n\n接下来我们通过一个例子来理解分区平衡。\n\n1、根据以上信息，一个拥有3个replica的partition，最初是下图的样子。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-8fc5666a51f5c6cd.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n可以看到AR和ISR保持一致，并且初始时刻，优先副本和leader副本都指向replica 0.\n\n2、接下来，replica 0所在的机器下线了，那么情况会变成如下图所示：\n\n可以看到replica 0已经从ISR中移除掉了。同时，由于重新选举，leader副本变成了replica 1，而优先副本还是replica 0。优先副本是不会改变的。\n\n由于最初时，leader副本在broker均匀分布，分区是平衡的。但此时，由于此partition的leader副本换成了另外一个，所以此时分区平衡已经被破坏。\n\n3、replica 0所在的机器修复了，又重新上线，情况如下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-30d31e9efe80e4ed.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n可以看到replica 0重新回到ISR列表中，不过此时他没能恢复leader的身份。只能作为follower当一名小弟。此时分区依旧是不平衡的。那是否意味着分区永远都会不平衡下去呢？不是的。\n\n4、kafka会定时触发分区平衡操作，也可以主动触发分区平衡。这就是所谓的分区平衡操作，操作完后如下图。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-da43c555b4838071.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n可以看到此时leader副本通过选举，会重新变回来replica 0，因为replica 0是优先副本，其实优先的含义就是选择leader时被优先选择。这样整个分区又回到了初始状态，而初始时，leader副本是均匀分布的。此时已经分区平衡了。\n\n由此可见，分区平衡操作就是使leader副本和优先副本保持一致的操作。可以把优先副本理解为分区的平衡状态位，平衡操作就是让leader副本归位。\n\n### **Partition的读和写**\n\n通过之前的学习，我们知道topic下划分了多个partition，消息的生产和消费最终都是发生在partition之上。下图是一个三个partition的topic的读写示意。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-6f58d9fc95bcfea6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n我们先看右边的producer，可以看到写的时候，采用round-robin算法，轮询往每个partition写入。\n\n而在消费者端，每个consumer都维护一个offset值，指向的是它所消费到的消息坐标。\n\n我们先看group A的三个consumer，他们分别独立消费不同的三个partition。每个consumer维护了自己的offset。\n\n我们再看group B，可以看到两个group是并行消费整个topic，同一条消息会被不同group消费到。\n\n此处有如下知识点：\n\n1、每个partition都是有序的不可变的。\n\n2、Kafka可以保证partition的消费顺序，但不能保证topic消费顺序。\n\n3、无论消费与否，保留周期默认两天（可配置）。\n\n4、每个consumer维护的唯一元数据是offset，代表消费的位置，一般线性向后移动。\n\n5、consumer也可以重置offset到之前的位置，可以以任何顺序消费，不一定线性后移。\n\n### **回顾**\n\n本章是理解kafka设计的核心，通过本章学习你应该理解如下知识点：\n\n*   producer\n*   consumer\n*   consumer group\n*   broker\n*   分区（partition）\n*   副本（replica）\n*   分区平衡\n*   消息读写\n\n如果对上面提到的知识点还有不清晰的地方，请再复习，或者找其它学习资料进行学习。还有不懂的，请加我微信，单独讲。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-17e10203f5fa433a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"
  },
  {
    "path": "Kafka/Apache-Kafka核心组件和流程(副本管理器).md",
    "content": "**Apache Kafka 编程实战您可能感性的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n本章简单介绍了副本管理器，副本管理器负责分区及其副本的管理。副本管理器具体的工作流程可以参考牟大恩所著的《Kafka入门与实践》\n\n## 副本管理器\n\n副本机制使得kafka整个集群中，只要有一个代理存活，就可以保证集群正常运行。这大大提高了Kafka的可靠性和稳定性。Kafka中代理的存活，需要满足以下两个条件：\n\n*   存活的节点要维持和zookeeper的session连接，通过zookeeper的心跳机制实现\n*   Follower副本要与leader副本保持同步，不能落后太多。\n\n满足以上条件的节点在ISR中，一旦宕机，或者中断时间太长，Leader就会把同步副本从ISR中踢出。\n\n所有节点中，leader节点负责接收客户端的读写操作，follower节点从leader复制数据。\n\n副本管理器负责对副本管理。由于副本是分区的副本，所以对副本的管理体现在对分区的管理。\n\n在第三章已经对分区和副本有了详细的讲解，这里再介绍两个重要的概念，LEO和HW。\n\n*   LEO是Log End Offset缩写。表示每个分区副本的最后一条消息的位置，也就是说每个副本都有LEO。\n*   HW是Hight Watermark缩写，他是一个分区所有副本中，最小的那个LEO。\n\n看下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-ee582b2718509fbe.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n分区test-0有三个副本，每个副本的LEO就是自己最后一条消息的offset。可以看到最小的LEO是Replica2的，等于3，也就是说HW=3。这代表offset=4的消息还没有被所有副本复制，是无法被消费的。而offset<=3的数据已经被所有副本复制，是可以被消费的。\n\n副本管理器所承担的职责如下：\n\n*   副本过期检查\n*   追加消息\n*   拉取消息\n*   副本同步过程\n*   副本角色转换\n*   关闭副本\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-55d26b05293d757f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"
  },
  {
    "path": "Kafka/Apache-Kafka核心组件和流程-协调器.md",
    "content": "**Apache Kafka 编程实战您可能感性的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n上一节介绍了kafka工作的核心组件--控制器。本节将介绍消费者密切相关的组件--协调器。它负责消费者的出入组工作。大家可以回想一下kafka核心概念中关于吃苹果的场景，如果我邀请了100个人过来吃苹果，如果没有人告诉每个吃苹果的人哪个是他的盘子，那岂不是要乱了套？协调器做的就是这个工作。当然还有更多。\n\n## **2 协调器**\n\n顾名思义，协调器负责协调工作。本节所讲的协调器，是用来协调消费者工作分配的。简单点说，就是消费者启动后，到可以正常消费前，这个阶段的初始化工作。消费者能够正常运转起来，全有赖于协调器。\n\n主要的协调器有如下两个：\n\n1、消费者协调器（ConsumerCoordinator）\n\n2、组协调器（GroupCoordinator）\n\n此外还有任务管理协调器（WorkCoordinator），用作kafka connect的works管理，本教程不做讲解。\n\nkafka引入协调器有其历史过程，原来consumer信息依赖于zookeeper存储，当代理或消费者发生变化时，引发消费者平衡，此时消费者之间是互不透明的，每个消费者和zookeeper单独通信，容易造成羊群效应和脑裂问题。\n\n为了解决这些问题，kafka引入了协调器。服务端引入组协调器（GroupCoordinator），消费者端引入消费者协调器（ConsumerCoordinator）。每个broker启动的时候，都会创建GroupCoordinator实例，管理部分消费组（集群负载均衡）和组下每个消费者消费的偏移量（offset）。每个consumer实例化时，同时实例化一个ConsumerCoordinator对象，负责同一个消费组下各个消费者和服务端组协调器之前的通信。如下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-65e89093e5a3b19f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n## **2.1 消费者协调器**\n\n消费者协调器，可以看作是消费者做操作的代理类（其实并不是），消费者很多操作通过消费者协调器进行处理。\n\n消费者协调器主要负责如下工作：\n\n1、更新消费者缓存的MetaData\n\n2、向组协调器申请加入组\n\n3、消费者加入组后的相应处理\n\n4、请求离开消费组\n\n5、向组协调器提交偏移量\n\n6、通过心跳，保持组协调器的连接感知。\n\n7、被组协调器选为leader的消费者的协调器，负责消费者分区分配。分配结果发送给组协调器。\n\n8、非leader的消费者，通过消费者协调器和组协调器同步分配结果。\n\n消费者协调器主要依赖的组件和说明见下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-8c514c18e67958ae.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n可以看到这些组件和消费者协调器担负的工作是可以对照上的。\n\n## **2.2 组协调器**\n\n组协调器负责处理消费者协调器发过来的各种请求。它主要提供如下功能：\n\n*   在与之连接的消费者中选举出消费者leader\n*   下发leader消费者返回的消费者分区分配结果给所有的消费者\n*   管理消费者的消费偏移量提交，保存在kafka的内部主题中\n*   和消费者心跳保持，知道哪些消费者已经死掉，组中存活的消费者是哪些。\n\n组协调器在broker启动的时候实例化，每个组协调器负责一部分消费组的管理。它主要依赖的组件见下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-42eed73e1e066cd9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n这些组件也是和组协调器的功能能够对应上的。具体内容不在详述。\n\n### **2.3 消费者入组过程**\n\n下图展示了消费者启动选取leader、入组的过程。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-fef3d911771d721b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n消费者入组的过程，很好的展示了消费者协调器和组协调器之间是如何配合工作的。leader consumer会承担分区分配的工作，这样kafka集群的压力会小很多。同组的consumer通过组协调器保持同步。消费者和分区的对应关系持久化在kafka内部主题。\n\n## **2.4 消费偏移量管理**\n\n消费者消费时，会在本地维护消费到的位置（offset），就是偏移量，这样下次消费才知道从哪里开始消费。如果整个环境没有变化，这样做就足够了。但一旦消费者平衡操作或者分区变化后，消费者不再对应原来的分区，而每个消费者的offset也没有同步到服务器，这样就无法接着前任的工作继续进行了。\n\n因此只有把消费偏移量定期发送到服务器，由GroupCoordinator集中式管理，分区重分配后，各个消费者从GroupCoordinator读取自己对应分区的offset，在新的分区上继续前任的工作。\n\n下图展示了不提交offset到服务端的问题：\n\n![image](https://mmbiz.qpic.cn/mmbiz_jpg/UdK9ByfMT2O97piaBAkbLvms7mTItc3GmcJ3ibW26cdA9HYBPAS00Azia4DNfEkMbNc7mia1t8NEOoWibO4baM4RzmQ/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)\n\n开始时，consumer 0消费partition 0 和1，后来由于新的consumer 2入组，分区重新进行了分配。consumer 0不再消费partition2，而由consumer 2来消费partition 2，但由于consumer之间是不能通讯的，所有consumer2并不知道从哪里开始自己的消费。\n\n因此consumer需要定期提交自己消费的offset到服务端，这样在重分区操作后，每个consumer都能在服务端查到分配给自己的partition所消费到的offset，继续消费。\n\n由于kafka有高可用和横向扩展的特性，当有新的分区出现或者新的消费入组后，需要重新分配消费者对应的分区，所以如果偏移量提交的有问题，会重复消费或者丢消息。偏移量提交的时机和方式要格外注意！！\n\n下面两种情况分别会造成重复消费和丢消息：\n\n*   如果提交的偏移量小于消费者最后一次消费的偏移量，那么再均衡后，两个offset之间的消息就会被重复消费\n*   如果提交的偏移量大于消费者最后一次消费的偏移量，那么再均衡后，两个offset之间的消息就会丢失\n\n以上两种情况是如何产生的呢？我们继续往下看。\n\n### 2.4.1 偏移量有两种提交方式\n\n**1、自动提交偏移量**\n\n设置 enable.auto.commit为true，设定好周期，默认5s。消费者每次调用轮询消息的poll() 方法时，会检查是否超过了5s没有提交偏移量，如果是，提交上一次轮询返回的偏移量。\n\n这样做很方便，但是会带来重复消费的问题。假如最近一次偏移量提交3s后，触发了再均衡，服务器端存储的还是上次提交的偏移量，那么再均衡结束后，新的消费者会从最后一次提交的偏移量开始拉取消息，此3s内消费的消息会被重复消费。\n\n**2、手动提交偏移量**\n\n设置 enable.auto.commit为false。程序中手动调用commitSync()提交偏移量，此时提交的是poll方法返回的最新的偏移量。\n\n我们来看下面两个提交时机：\n\n*   如果poll完马上调用commitSync(),那么一旦处理到中间某条消息的时候异常，由于偏移量已经提交，那么出问题的消息位置到提交偏移量之间的消息就会丢失。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-3e03d55d0fba891f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n*   如果处理完所有消息后才调用commitSync()。有可能在处理到一半的时候发生再均衡，此时偏移量还未提交，那么再均衡后，会从上次提交的位置开始消费，造成重复消费。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-bc84d94aeed14fb6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n比较起来，重复消费要比丢消息好一些，所以我们程序应采用第二种方式，同时消费逻辑中，要能够检查重复消费。\n\ncommitSync()是同步提交偏移量，主程序会一直阻塞，偏移量提交成功后才往下运行。这样会限制程序的吞吐量。如果降低提交频次，又很容易发生重复消费。\n\n这里我们可以使用commitAsync()异步提交偏移量。只管提交，而不会等待broker返回提交结果\n\ncommitSync只要没有发生不可恢复错误，会进行重试，直到成功。而commitAsync不会进行重试，失败就是失败了。commitAsync不重试，是因为重试提交时，可能已经有其它更大偏移量已经提交成功了，如果此时重试提交成功，那么更小的偏移量会覆盖大的偏移量。那么如果此时发生再均衡，新的消费者将会重复消费消息。\n\ncommitAsync也支持回调，由于上述原因，回调中最好不要因为失败而重试提交。而是应该记录错误，以便后续分析和补偿。\n\n### 2.4.2 偏移量提交的最佳实践\n\n关于偏移量的提交方式和时机，上文已经有了大量的讲解。但看完后好像还不知道应该怎么提交偏移量才是最合适的。是不是觉得无论怎么提交，都无法避免重复消费？没错，事实就是这样，我们只能采用合理的方式，最大可能的去降低发生此类问题的概率。此外做好补偿处理。\n\n一般来说，偶尔的提交失败，不去重试，是没有问题的。因为一般是因为临时的问题而失败，后续的提交总会成功。如果我们在关闭消费者或者再均衡前，确保所有的消费者都能成功提交一次偏移量，也可以保证再均衡后，消费者能接着消费数据。\n\n因此我们采用同步和异步混合的方式提交偏移量。\n\n*   正常消费消息时，消费结束提交偏移量，采用异步方式\n*   如果程序报错，finally中，提交偏移量，采用同步方式，确保提交成功\n*   再均衡前的回调方法中，提交偏移量，采用同步方式，确保提交成功\n\n这样既保证了吞吐量，也保证了提交偏移量的安全性。另外由于再均衡前提交偏移量，降低了重复消费可能。\n\nkafka还提供了提交特定偏移量的方法。我们可以指定分区和offset进行提交。分区和offset的值可以从消息对象中取得。\n\n另外，如果担心一次取回数据量太大，可能处理到一半的时候出现再均衡，导致偏移量没有提交，重复消费。那么可以每n条提交一次。\n\n而当n=1时，也就是处理一条数据就提交一次，会把重复消费的可能降到最低。同时由于增加了和服务端的通讯，效率大大降低。\n\n其实即使这样，也是可能重复消费的，试想如下场景：\n\n*   消费者拉取到数据后，开始逻辑处理\n*   处理第一条offset=2，成功了，提交offset=3\n*   开始处理offset=3的消息，处理完成后，但提交offset=4前，此消费者突然意外挂掉了，所以也没能进入异常处理。偏移量没能成功提交。\n*   消费者进行了再均衡，新的消费者接手此分区进行消费，取到的offset还是上一次提交的3，那么将会重复消费offset=3的消息。\n\n所以我们应平衡重复消费发生的概率和程序的效率，来设置提交的时机。同时程序逻辑一定做好重复消费的检查工作！\n\n## **2.5 回顾**\n\n本节从协调器讲起，首先介绍了消费者协调器和组协调器，以及他们是如何配合工作的。从消费偏移量的管理展开，详细介绍了偏移量的提交，及提交的最佳实践。本节没有涉及代码部分，所有知识点相关的代码将在最后一章中统一给出。现在的要求只是理解知识点。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-42f66dc30c500185.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"
  },
  {
    "path": "Kafka/Apache-Kafka核心组件和流程-控制器.md",
    "content": "**Apache Kafka 编程实战您可能感性的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n通过前几章的学习，我们已经从宏观层面了解了kafka的设计理念。包括kafka集群的组成、消息的主题、主题的分区、分区的副本等内容。接下来我们会继续深入，了解kafka的主要组件以及核心的流程，最后还会介绍kafka的消息是如何存储的。此章非常重要，通过本章和上一章的学习，你已经能够掌握kafka 80%的核心内容。当然随着学习的深入，难度也会越来越大，有任何问题欢迎留言或者私信。\n\nKafka主要的组件如下\n\n*   控制器\n*   协调器\n*   日志管理器\n*   副本管理器\n\n我们将会逐个进行讲解，讲解过长还将保持前面章节的特点，多用有形的图表帮助读者理解。本篇博客先讲解控制器部分。\n\n## **1、控制器**\n\n在前一章的学习中，我们已经知道Kafka的集群由n个的broker所组成，每个broker就是一个kafka的实例或者称之为kafka的服务。其实控制器也是一个broker，控制器也叫leader broker。他除了具有一般broker的功能外，还负责分区leader的选取，也就是负责选举partition的leader replica。控制器是kafka核心中的核心，需要重点学习和理解。\n\n### **控制器选举**\n\nkafka每个broker启动的时候，都会实例化一个KafkaController，并将broker的id注册到zookeeper，这在第二章中已经通过例子做过讲解。集群在启动过程中，通过选举机制选举出其中一个broker作为leader，也就是前面所说的控制器。\n\n包括集群启动在内，有三种情况触发控制器选举：\n\n1、集群启动\n\n2、控制器所在代理发生故障\n\n3、zookeeper心跳感知，控制器与自己的session过期\n\n按照惯例，先看图。我们根据下图来讲解集群启动时，控制器选举过程。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-93a458f1fbe73cc1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n假设此集群有三个broker，同时启动。\n\n（一）3个broker从zookeeper获取/controller临时节点信息。/controller存储的是选举出来的leader信息。此举是为了确认是否已经存在leader。\n\n（二）如果还没有选举出leader，那么此节点是不存在的，返回-1。如果返回的不是-1，而是leader的json数据，那么说明已经有leader存在，选举结束。\n\n（三）三个broker发现返回-1，了解到目前没有leader，于是均会触发向临时节点/controller写入自己的信息。最先写入的就会成为leader。\n\n（四）假设broker 0的速度最快，他先写入了/controller节点，那么他就成为了leader。而broker1、broker2很不幸，因为晚了一步，他们在写/controller的过程中会抛出ZkNodeExistsException，也就是zk告诉他们，此节点已经存在了。\n\n经过以上四步，broker 0成功写入/controller节点，其它broker写入失败了，所以broker 0成功当选leader。\n\n此外zk中还有controller_epoch节点，存储了leader的变更次数，初始值为0，以后leader每变一次，该值+1。所有向控制器发起的请求，都会携带此值。如果控制器和自己内存中比较，请求值小，说明kafka集群已经发生了新的选举，此请求过期，此请求无效。如果请求值大于控制器内存的值，说明已经有新的控制器当选了，自己已经退位，请求无效。kafka通过controller_epoch保证集群控制器的唯一性及操作的一致性。\n\n由此可见，Kafka控制器选举就是看谁先争抢到/controller节点写入自身信息。\n\n### **控制器初始化**\n\n控制器的初始化，其实是初始化控制器所用到的组件及监听器，准备元数据。\n\n前面提到过每个broker都会实例化并启动一个KafkaController。KafkaController和他的组件关系，以及各个组件的介绍如下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-c6f2a068fca4b8d4.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n图中箭头为组件层级关系，组件下面还会再初始化其他组件。可见控制器内部还是有些复杂的，主要有以下组件：\n\n1、ControllerContext，此对象存储了控制器工作需要的所有上下文信息，包括存活的代理、所有主题及分区分配方案、每个分区的AR、leader、ISR等信息。\n\n2、一系列的listener，通过对zookeeper的监听，触发相应的操作，黄色的框的均为listener\n\n3、分区和副本状态机，管理分区和副本。\n\n4、当前代理选举器ZookeeperLeaderElector，此选举器有上位和退位的相关回调方法。\n\n5、分区leader选举器，PartitionLeaderSelector\n\n6、主题删除管理器，TopicDeletetionManager\n\n7、leader向broker批量通信的ControllerBrokerRequestBatch。缓存状态机处理后产生的request，然后统一发送出去。\n\n8、控制器平衡操作的KafkaScheduler，仅在broker作为leader时有效。\n\n图片是我根据资料所总结，个人认为对于理解kafkaController的全貌很有帮助。本章节后面讲到相应组件和流程时，还需要反复回来理解此图，思考组件所处的位置，对整体的作用。\n\n### **故障转移**\n\n故障转移其实就是leader所在broker发生故障，leader转移为其他的broker。转移的过程就是重新选举leader的过程。\n\n重新选举leader后，需要为该broker注册相应权限，调用的是ZookeeperLeaderElector的onControllerFailover()方法。在这个方法中初始化和启动了一系列的组件来完成leader的各种操作。具体如下，其实和控制器初始化有很大的相似度。\n\n1、注册分区管理的相关监听器\n\n监听名称监听zookeeper节点作用PartitionsReassignedListener/admin/reassign_partitions节点变化将会引发分区重分配IsrChangeNotificationListener/isr_change_notification处理分区的ISR发生变化引发的操作PreferredReplicaElectionListener/admin/preferred_replica_election将优先副本选举为leader副本\n\n2、注册主题管理的相关监听\n\n监听名称监听zookeeper节点作用TopicChangeListener/brokers/topics监听主题发生变化时进行相应操作DeleteTopicsListener/admin/delete_topics完成服务器端删除主题的相应操作。否则客户端删除主题仅仅是表示删除\n\n3、注册代理变化监听器\n\n监听名称监听zookeeper节点作用BrokerChangeListener/brokers/ids代理发生增减的时候进行相应的处理\n\n4、重新初始化ControllerContext，\n\n5、启动控制器和其他代理之间通信的ControllerChannelManager\n\n6、创建用于删除主题的TopicDeletionManager对象,并启动。\n\n7、启动分区状态机和副本状态机\n\n8、轮询每个主题，添加监听分区变化的PartitionModificationsListener\n\n9、如果设置了分区平衡定时操作，那么创建分区平衡的定时任务，默认300秒检查并执行。\n\n除了这些组件的启动外，onControllerFailover方法中还做了如下操作：\n\n1、/controller_epoch值+1，并且更新到ControllerContext\n\n2、检查是否出发分区重分配，并做相关操作\n\n3、检查需要将优先副本选为leader，并做相关操作\n\n4、向kafka集群所有代理发送更新元数据的请求。\n\n下面来看代理下线的方法onControllerResignation\n\n1、该方法中注销了控制器的权限。取消在zookeeper中对于分区、副本感知的相应监听器的监听。\n\n2、关闭启动的各个组件\n\n3、最后把ControllerContext中记录控制器版本的数值清零，并设置当前broker为RunnignAsBroker，变为普通的broker。\n\n通过对控制器启动过程的学习，我们应该已经对kafka工作的原理有了了解，核心是监听zookeeper的相关节点，节点变化时触发相应的操作。其它的处理流程都是相类似的。本篇教程接下来做简要介绍，想要了解详情的，可以先找其它资料。我后续也会再补充更为详细的教程。\n\n### **代理上下线**\n\n有新的broker加入集群时，称为代理上线。反之，当broker关闭，推出集群时，称为代理下线。\n\n代理上线：\n\n1、新代理启动时向/brokers/ids写数据\n\n2、BrokerChangeListener监听到变化。对新上线节点调用controllerChannelManager.addBroker()，完成新上线代理网络层初始化\n\n3、调用KafkaController.onBrokerStartup()处理\n\n3.1通过向所有代理发送UpdateMetadataRequest，告诉所有代理有新代理加入\n\n3.2根据分配给新上线节点的副本集合，对副本状态做变迁。对分区也进行处理。\n\n3.3触发一次leader选举，确认新加入的是否为分区leader\n\n3.4轮询分配给新broker的副本，调用KafkaController.onPartitionReassignment()，执行分区副本分配\n\n3.5恢复因新代理上线暂停的删除主题操作线程\n\n**代理下线：**\n\n1、查找下线节点集合\n\n2、轮询下线节点，调用controllerChannelManager.removeBroker()，关闭每个下线节点网络连接。清空下线节点消息队列，关闭下线节点request请求\n\n3、轮询下线节点，调用KafkaController.onBrokerFailure处理\n\n3.1处理leader副本在下线节点上上的分区，重新选出leader副本，发送updateMetadataRequest请求。\n\n3.2处理下线节点上的副本集合，做下线处理，从ISR集合中删除，不再同步，发送updateMetadataRequest请求。\n\n4、向集群全部存活代理发送updateMetadataRequest请求\n\n### **主题管理**\n\n通过分区状态机及副本状态机来进行主题管理\n\n1、创建主题\n\n/brokers/topics下创建主题对应子节点\n\nTopicChangeListener监听此节点\n\n变化时获取重入锁ReentrantLock,调用handleChildChange方法进行处理。\n\n通过对比zookeeper中/brokers/topics存储的主题集合及控制器的ControllerContext中缓存的主题集合的差集，得到新增的主题。反过来求差集，得到删除的主题。\n\n接下来遍历新增的主题集合，进行主题操作的实质性操作。之前仅仅是在zookeeper中添加了主题。新增主题涉及的操作有分区、副本状态的转化、分区leader的分配、分区存储日志的创建等。\n\n2、删除主题\n\n/admin/delete_topics创建删除主题的子节点\n\nDeleteTopicsListener监听此节点，\n\n变化时获取重入锁ReentrantLock,进行处理\n\n具体的删除逻辑再次就不再详述。\n\n### **分区管理**\n\n1、分区自动平衡\n\nonControllerFailover方法中启动分区自动平衡任务。定时检查是否失去平衡。\n\n自动平衡的操作就是把优先副本选为分区leader，AR中第一个副本为优先副本。\n\n先查出所有可用副本，以分区AR头节点分组。\n\n轮询代理节点，判断分区不平衡率是否超过10%(leader为非优先副本的分区/该代理分区总数)，则调用onPreferredReplicaElection()，让优先副本成为leader。达到自动平衡。\n\n分区平衡操作的流程已经在第三章做了很详细的讲解，此处不再重复，可以参考kafka核心概念。\n\n2、分区重分配\n\n当zk节点/admin/reassign_partitions变化时，触发分区重分配操作。该节点存储分区重分配的方案。\n\n通过计算主题分区原AR（OAR）和重新分配后的AR（RAR），分别做相应处理：\n\n1、OAR+RAR：更新到该主题分区AR，并通知副本节点同步。leader_epoch+1\n\n2、RAR-OAR：副本设为NewReplica。\n\n3、（OAR+RAR）- RAR：需要下线的副本，做下线操作\n\n具体流程不再详述\n\n**小结：**关于控制器的相关知识点就先讲到这里，控制器初始化中的那张图需要充分去理解，理解了此图，对控制器内部的构造，以及控制器要做什么事情、如何做的，就已经掌握了。\n\n**你真的不关注一下嘛~**\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-5f342b13f76b19b9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"
  },
  {
    "path": "Kafka/Apache-Kafka核心组件和流程-日志管理器.md",
    "content": "**Apache Kafka 编程实战您可能感兴趣的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n上一节介绍了协调器。协调器主要负责消费者和kafka集群间的协调。那么消费者消费时，如何定位消息呢？消息是如何存储呢？本节将为你揭开答案。\n\n## **3.1 日志的存储**\n\nKafka的消息以日志文件的形式进行存储。不同主题下不同分区的消息是分开存储的。同一个分区的不同副本也是以日志的形式，分布在不同的broker上存储。\n\n这样看起来，日志的存储是以副本为单位的。在程序逻辑上，日志确实是以副本为单位的，每个副本对应一个log对象。但实际在物理上，一个log又划分为多个logSegment进行存储。\n\n举个例子，创建一个topic名为test，拥有3个分区。为了简化例子，我们设定只有1个broker，1个副本。那么所有的分区副本都存储在同一个broker上。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-e6306eff29254b82.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n第二章中，我们在kafka的配置文件中配置了log.dirs=/tmp/kafka-logs。此时在/tmp/kafka-logs下面会创建test-0，test-1，test-2三个文件夹，代表三个分区。命名规则为“topic名称-分区编号”\n\n我们看test-0这个文件夹，注意里面的logSegment并不代表这个文件夹，logSegment代表逻辑上的一组文件，这组文件就是.log、.index、.timeindex这三个不同文件扩展名，但是同文件名的文件。\n\n*   .log存储消息\n*   .index存储消息的索引\n*   .timeIndex，时间索引文件，通过时间戳做索引。\n\n这三个文件配合使用，用来保存和消费时快速查找消息。\n\n刚才说到同一个logSegment的三个文件，文件名是一样的。命名规则为.log文件中第一条消息的前一条消息偏移量，也称为基础偏移量，左边补0，补齐20位。比如说第一个LogSegement的日志文件名为00000000000000000000.log，假如存储了200条消息后，达到了log.segment.bytes配置的阈值（默认1个G），那么将会创建新的logSegment，文件名为00000000000000000200.log。以此类推。另外即使没有达到log.segment.bytes的阈值，而是达到了log.roll.ms或者log.roll.hours设置的时间触发阈值，同样会触发产生新的logSegment。\n\n## **3.2 日志定位**\n\n日志定位也就是消息定位，输入一个消息的offset，kafka如何定位到这条消息呢？\n\n日志定位的过程如下:\n\n1、根据offset定位logSegment。（kafka将基础偏移量也就是logsegment的名称作为key存在concurrentSkipListMap中）\n\n2、根据logSegment的index文件查找到距离目标offset最近的被索引的offset的position x。\n\n3、找到logSegment的.log文件中的x位置，向下逐条查找，找到目标offset的消息。\n\n结合下图中例子，我再做详细的讲解：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-3a2056772ea61f52.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n这里先说明一下.index文件的存储方式。.index文件中存储了消息的索引，存储内容是消息的offset及物理位置position。并不是每条消息都有自己的索引，kafka采用的是稀疏索引，说白了就是隔n条消息存一条索引数据。这样做比每一条消息都建索引，查找起来会慢，但是也极大的节省了存储空间。此例中我们假设跨度为2，实际kafka中跨度并不是固定条数，而是取决于消息累积字节数大小。\n\n例子中consumer要消费offset=15的消息。我们假设目前可供消费的消息已经存储了三个logsegment，分别是00000000000000000，0000000000000000010，0000000000000000020。为了讲解方便，下面提到名称时，会把前面零去掉。\n\n下面我们详细讲一下查找过程。\n\n*   kafka收到查询offset=15的消息请求后，通过二分查找，从concurrentSkipListMap中找到对应的logsegment名称，也就是10。\n*   从10.index中找到offset小于等于15的最大值，offset=14，它对应的position=340\n*   从10.log文件中物理位置340，顺序往下扫描文件，找到offset=15的消息内容。\n\n可以看到通过稀疏索引，kafka既加快了消息查找的速度，也顾及了存储的开销。\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-6304d17154705a81.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"
  },
  {
    "path": "Kafka/Apache-Kafka简介.md",
    "content": "**您可能感兴趣的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n**kafka的定位**\n\n提到kafka，不太熟悉或者稍有接触的开发人员，第一想法可能会觉得它是一个消息系统。其实Kafka的定位并不止于此。\n\nKafka官方文档介绍说，Apache Kafka是一个分布式流平台，并给出了如下解释：\n\n流平台有三个关键的能力：\n\n*   发布订阅记录流，和消息队列或者企业新消息系统类似。\n*   以可容错、持久的方式保存记录流\n*   当记录流产生时就进行处理\n\nKafka通常用于应用中的两种广播类型：\n\n*   在系统和应用间建立实时的数据管道，能够可信赖的获取数据。\n*   建立实时的流应用，可以处理或者响应数据流。\n\n由此可见，kafka给自身的定位并不只是一个消息系统，而是通过发布订阅消息这种机制实现了流平台。\n\n其实不管kafka给自己的定位如何，他都逃脱不了发布订阅消息的底层机制。本文讲解的重点，也是kafka发布订阅消息的特性。\n\nKafka和大多数消息系统一样，搭建好kafka集群后，生产者向特定的topic生产消息，而消费者通过订阅topic，能够准实时的拉取到该topic新消息，进行消费。如下图：\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-eabf90da50c94506.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n**Kafka特性**\n\nkafka和有以下主要的特性：\n\n*   消息持久化\n*   高吞吐量\n*   可扩展性\n\n尤其是高吞吐量，是他的最大卖点。kafka之所以能够实现高吞吐量，是基于他自身优良的设计，及集群的可扩展性。后面章节会展开来分析。\n\n**Kafka应用场景**\n\n*   消息系统\n*   日志系统\n*   流处理\n"
  },
  {
    "path": "Kafka/Apache-Kafka编程实战.md",
    "content": "**Apache Kafka 编程实战您可能感性的文章:**\n\n[Apache-Kafka简介](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000482%26idx%3D1%26sn%3D22b13749ed0352cd286eac7697f39f23%26chksm%3D7d3d44774a4acd6189d082976e90087a9a955e6ca12b21193395536643a302ac4c13c88fe212%23rd)\n\n[Apache Kafka安装和使用](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000470%26idx%3D1%26sn%3D41ee111a073c51af4f9e87c2cdc4d584%26chksm%3D7d3d44434a4acd55b67414765a7b79152d7ef430ba00bec8af6cdddd8e8cf161777ee4a15841%23rd)\n\n[Apache-Kafka核心概念](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000472%26idx%3D1%26sn%3D99353b901d1174c3edd4a9ebbe394975%26chksm%3D7d3d444d4a4acd5bf0017210f55ec394abda01d163674d540988ca94863a51411be951711553%23rd)\n\n\n[Apache-Kafka核心组件和流程-协调器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000476%26idx%3D1%26sn%3D34b2127b1a09664087e3b2079844c2db%26chksm%3D7d3d44494a4acd5f3bc70d914ae2842409282780d19d57043d168895e55f160b3be7835e2446%23rd)\n\n[Apache-Kafka核心组件和流程(副本管理器)](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000480%26idx%3D1%26sn%3D054cdf620eb82c4ecfaccd226d49d0e0%26chksm%3D7d3d44754a4acd638ca37afcfdaad802bb3dec01758b18cdf2c607ec494526832ee58ff43451%23rd)\n\n[Apache-Kafka 核心组件和流程-控制器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000474%26idx%3D1%26sn%3Dc9b9d8fbb942f5299eb1d23a9363c0a4%26chksm%3D7d3d444f4a4acd597607e33ee59aad92db50084a5ab7edb84449df6f2f3ecc504e97f05977bb%23rd)\n\n[Apache-Kafka核心组件和流程-日志管理器](http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s%3F__biz%3DMzU3MzgwNTU2Mg%3D%3D%26mid%3D100000478%26idx%3D1%26sn%3Deeb3310214d7fa24ca86c4afad421baa%26chksm%3D7d3d444b4a4acd5d1987dc78f89d40a20833cec682b30b9f1a0735a26681f681a38853a6ff63%23rd)\n\n....\n\n本章通过实际例子，讲解了如何使用java进行kafka开发。\n\n添加依赖：\n\n```\n<dependency>\n<groupId>org.apache.kafka</groupId>\n<artifactId>kafka-clients</artifactId>\n<version>2.0.0</version>\n</dependency>\n\n```\n\n下面是创建主题的代码：\n\n```\npublic class TopicProcessor {\nprivate static final String ZK_CONNECT=\"localhost:2181\";\nprivate static final int SESSION_TIME_OUT=30000;\nprivate static final int CONNECT_OUT=30000;\n\npublic static void createTopic(String topicName,int partitionNumber,int replicaNumber,Properties properties){\nZkUtils zkUtils = null;\ntry{\nzkUtils=ZkUtils.apply(ZK_CONNECT,SESSION_TIME_OUT,CONNECT_OUT, JaasUtils.isZkSecurityEnabled());\nif(!AdminUtils.topicExists(zkUtils,topicName)){\nAdminUtils.createTopic(zkUtils,topicName,partitionNumber,replicaNumber,properties,AdminUtils.createTopic$default$6());\n}\n}catch (Exception e){\ne.printStackTrace();\n}finally {\nzkUtils.close();\n}\n}\n\npublic static void main(String[] args){\ncreateTopic(\"javatopic\",1,1,new Properties());\n}\n}\n\n```\n\n首先定义了zookeeper相关连接信息。然后在createTopic中，先初始化ZkUtils，和zookeeper交互依赖于它。然后通过AdminUtils先判断是否存在你要创建的主题，如果不存在，则通过createTopic方法进行创建。传入参数包括主题名称，分区数量，副本数量等。\n\n## **生产者生产消息**\n\n生产者生产消息代码如下：\n\n```\npublic class MessageProducer {\nprivate static final String TOPIC=\"education-info\";\nprivate static final String BROKER_LIST=\"localhost:9092\";\nprivate static KafkaProducer<String,String> producer = null;\n\nstatic{\nProperties configs = initConfig();\nproducer = new KafkaProducer<String, String>(configs);\n}\n\nprivate static Properties initConfig(){\nProperties properties = new Properties();\nproperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,BROKER_LIST);\nproperties.put(ProducerConfig.ACKS_CONFIG,\"all\");\nproperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());\nproperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());\nreturn properties;\n}\n\npublic static void main(String[] args){\ntry{\nString message = \"hello world\";\nProducerRecord<String,String> record = new ProducerRecord<String,String>(TOPIC,message);\nproducer.send(record, new Callback() {\n@Override\npublic void onCompletion(RecordMetadata metadata, Exception exception) {\nif(null==exception){\nSystem.out.println(\"perfect!\");\n}\nif(null!=metadata){\nSystem.out.print(\"offset:\"+metadata.offset()+\";partition:\"+metadata.partition());\n}\n}\n}).get();\n}catch (Exception e){\ne.printStackTrace();\n}finally {\nproducer.close();\n}\n}\n}\n\n```\n\n1、首先初始化KafkaProducer对象。\n\n```\nproducer = new KafkaProducer<String, String>(configs);\n\n```\n\n2、创建要发送的消息对象。\n\n```\nProducerRecord<String,String> record = new ProducerRecord<String,String>(TOPIC,message);\n\n```\n\n3、通过producer的send方法，发送消息\n\n4、发送消息时，可以通过回调函数，取得消息发送的结果。异常发生时，对异常进行处理。\n\n初始化producer时候,需要注意下面属性设置：\n\n```\nproperties.put(ProducerConfig.ACKS_CONFIG,\"all\");\n\n```\n\n这里有三种值可供选择：\n\n*   0，不等服务器响应，直接返回发送成功。速度最快，但是丢了消息是无法知道的\n*   1，leader副本收到消息后返回成功\n*   all，所有参与的副本都复制完成后返回成功。这样最安全，但是延迟最高。\n\n## 消费者消费消息\n\n我们直接看代码\n\n```\npublic class MessageConsumer {\n\nprivate static final String TOPIC=\"education-info\";\nprivate static final String BROKER_LIST=\"localhost:9092\";\nprivate static KafkaConsumer<String,String> kafkaConsumer = null;\n\nstatic {\nProperties properties = initConfig();\nkafkaConsumer = new KafkaConsumer<String, String>(properties);\nkafkaConsumer.subscribe(Arrays.asList(TOPIC));\n}\n\nprivate static Properties initConfig(){\nProperties properties = new Properties();\nproperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,BROKER_LIST);\nproperties.put(ConsumerConfig.GROUP_ID_CONFIG,\"test\");\nproperties.put(ConsumerConfig.CLIENT_ID_CONFIG,\"test\");\nproperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());\nproperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());\nreturn properties;\n}\n\npublic static void main(String[] args){\ntry{\nwhile(true){\nConsumerRecords<String,String> records = kafkaConsumer.poll(100);\nfor(ConsumerRecord record:records){\ntry{\nSystem.out.println(record.value());\n}catch(Exception e){\ne.printStackTrace();\n}\n}\n}\n\n}catch(Exception e){\ne.printStackTrace();\n}finally {\nkafkaConsumer.close();\n}\n}\n}\n\n```\n\n代码逻辑如下：\n\n1、初始化消费者KafkaConsumer，并订阅主题。\n\n```\nkafkaConsumer = new KafkaConsumer<String, String>(properties);\nkafkaConsumer.subscribe(Arrays.asList(TOPIC));\n\n```\n\n2、循环拉取消息\n\n```\nConsumerRecords<String,String> records = kafkaConsumer.poll(100);\n\n```\n\npoll方法传入的参数100，是等待broker返回数据的时间，如果超过100ms没有响应，则不再等待。\n\n3、拉取回消息后，循环处理。\n\n```\nfor(ConsumerRecord record:records){\ntry{\nSystem.out.println(record.value());\n}catch(Exception e){\ne.printStackTrace();\n}\n}\n\n```\n\n消费相关代码比较简单，不过这个版本没有处理偏移量提交。学习过第四章-协调器相关的同学应该还记得偏移量提交的问题。我曾说过最佳实践是同步和异步提交相结合，同时在特定的时间点，比如再均衡前进行手动提交。\n\n加入偏移量提交，需要做如下修改：\n\n1、enable.auto.commit设置为false\n\n2、消费代码如下：\n\n```\npublic static void main(String[] args){\ntry{\nwhile(true){\nConsumerRecords<String,String> records =\nkafkaConsumer.poll(100);\nfor(ConsumerRecord record:records){\ntry{\nSystem.out.println(record.value());\n}catch(Exception e){\ne.printStackTrace();\n}\n}\nkafkaConsumer.commitAsync();\n}\n\n}catch(Exception e){\ne.printStackTrace();\n}finally {\ntry{\nkafkaConsumer.commitSync();\n}finally {\nkafkaConsumer.close();\n}\n}\n}\n\n```\n\n3、订阅消息时，实现再均衡的回调方法，在此方法中手动提交偏移量\n\n```\nkafkaConsumer.subscribe(Arrays.asList(TOPIC), new ConsumerRebalanceListener() {\n@Override\npublic void onPartitionsRevoked(Collection<TopicPartition> partitions) {\n//再均衡之前和消费者停止读取消息之后调用\nkafkaConsumer.commitSync(currentOffsets);\n}\n});\n\n```\n\n通过以上三步，我们把自动提交偏移量改为了手动提交。正常消费时，异步提交kafkaConsumer.commitAsync()。即使偶尔失败，也会被后续成功的提交覆盖掉。而在发生异常的时候，手动提交 kafkaConsumer.commitSync()。此外在步骤3中，我们通过实现再均衡时的回调方法，手动同步提交偏移量，确保了再均衡前偏移量提交成功。\n\n以上面的最佳实践提交偏移量，既能保证消费时较高的效率，又能够尽量避免重复消费。不过由于重复消费无法100%避免，消费逻辑需要自己处理重复消费的判断。\n\n**你真的不关注一下嘛~**\n\n![image](http://upload-images.jianshu.io/upload_images/16241060-0a3239c0e954c793.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"
  },
  {
    "path": "Linux基础/Linux基础和命令.md",
    "content": "\n## Linux命令\n\n我是小白，我从来没玩过Linux,请点这里：\n```\nhttp://www.runoob.com/linux/Linux-intro.html\n```\n## 推荐的一个Git仓库\n\n我有些基础，推荐一个快速查询命令的手册，请点这里：\n```\nhttps://github.com/jaywcjlove/linux-command\n```\n## 必须学会的命令\n\n#### 1.man和page\n\n\n```\n1.内部命令：echo\n查看内部命令帮助：help echo 或者 man echo\n\n2.外部命令：ls\n查看外部命令帮助：ls --help 或者 man ls 或者 info ls\n\n3.man文档的类型(1~9)\nman 7 man\nman 5 passwd\n\n4.快捷键：\nctrl + c：停止进程\n\nctrl + l：清屏\n\nctrl + r：搜索历史命令\n\nctrl + q：退出\n\n5.善于用tab键\n\n```\n\n#### 2.常用\n```\n说明：安装linux时，创建一个hadoop用户，然后使用root用户登陆系统\n\n1.进入到用户根目录\ncd ~ 或 cd\n\n2.查看当前所在目录\npwd\n\n3.进入到hadoop用户根目录\ncd ~hadoop\n\n4.返回到原来目录\ncd -\n\n5.返回到上一级目录\ncd ..\n\n6.查看hadoop用户根目录下的所有文件\nls -la\n\n7.在根目录下创建一个hadoop的文件夹\nmkdir /hadoop\n\n8.在/hadoop目录下创建src和WebRoot两个文件夹\n分别创建：mkdir /hadoop/src\n\t\t  mkdir /hadoop/WebRoot\n同时创建：mkdir /hadoop/{src,WebRoot}\n\n进入到/hadoop目录，在该目录下创建.classpath和README文件\n分别创建：touch .classpath\n\t\t  touch README\n同时创建：touch {.classpath,README}\n\n查看/hadoop目录下面的所有文件\nls -la\n\n在/hadoop目录下面创建一个test.txt文件,同时写入内容\"this is test\"\necho \"this is test\" > test.txt\n\n查看一下test.txt的内容\ncat test.txt\nmore test.txt\nless test.txt\n\n向README文件追加写入\"please read me first\"\necho \"please read me first\" >> README\n\n将test.txt的内容追加到README文件中\ncat test.txt >> README\n\n拷贝/hadoop目录下的所有文件到/hadoop-bak\ncp -r /hadoop /hadoop-bak\n\n进入到/hadoop-bak目录，将test.txt移动到src目录下，并修改文件名为Student.java\nmv test.txt src/Student.java\n\n在src目录下创建一个struts.xml\n> struts.xml\n\n删除所有的xml类型的文件\nrm -rf *.xml\n\n删除/hadoop-bak目录和下面的所有文件\nrm -rf /hadoop-bak\n\n返回到/hadoop目录，查看一下README文件有多单词，多少个少行\nwc -w README\nwc -l README\n\n返回到根目录，将/hadoop目录先打包，再用gzip压缩\n分步完成：tar -cvf hadoop.tar hadoop\n\t\t  gzip hadoop.tar\n一步完成：tar -zcvf hadoop.tar.gz hadoop\n\t\t  \n将其解压缩，再取消打包\n分步完成：gzip -d hadoop.tar.gz 或 gunzip hadoop.tar.gz\n一步完成：tar -zxvf hadoop.tar.gz\n\n将/hadoop目录先打包，同时用bzip2压缩，并保存到/tmp目录下\ntar -jcvf /tmp/hadoop.tar.bz2 hadoop\n\n将/tmp/hadoop.tar.bz2解压到/usr目录下面\ntar -jxvf hadoop.tar.bz2 -C /usr/\n\n```\n\n2. 文件命令\n```\n1.进入到用户根目录\ncd ~ 或者 cd\ncd ~hadoop\n回到原来路径\ncd -\n\n2.查看文件详情\nstat a.txt\n\n3.移动\nmv a.txt /ect/\n改名\nmv b.txt a.txt\n移动并改名\nmv a.txt ../b.txt\n\n4拷贝并改名\ncp a.txt /etc/b.txt\n\n5.vi撤销修改\nctrl + u (undo)\n恢复\nctrl + r (redo)\n\n6.名令设置别名(重启后无效)\nalias ll=\"ls -l\"\n取消\nunalias ll\n\n7.如果想让别名重启后仍然有效需要修改\nvi ~/.bashrc\n\n8.添加用户\nuseradd hadoop\npasswd hadoop\n\n9创建多个文件\ntouch a.txt b.txt\ntouch /home/{a.txt,b.txt}\n\n10.将一个文件的内容复制到里另一个文件中\ncat a.txt > b.txt\n追加内容\ncat a.txt >> b.txt \n\n\n11.将a.txt 与b.txt设为其拥有者和其所属同一个组者可写入，但其他以外的人则不可写入:\nchmod ug+w,o-w a.txt b.txt\n\nchmod a=wx c.txt\n\n12.将当前目录下的所有文件与子目录皆设为任何人可读取:\nchmod -R a+r *\n\n13.将a.txt的用户拥有者设为users,组的拥有者设为jessie:\nchown users:jessie a.txt\n\n14.将当前目录下的所有文件与子目录的用户的使用者为lamport,组拥有者皆设为users，\nchown -R lamport:users *\n\n15.将所有的java语言程式拷贝至finished子目录中:\ncp *.java finished\n\n16.将目前目录及其子目录下所有扩展名是java的文件列出来。\nfind -name \"*.java\"\n查找当前目录下扩展名是java 的文件\nfind -name *.java\n\n17.删除当前目录下扩展名是java的文件\nrm -f *.java\n\n\n```\n\n3.系统命令\n```\n1.查看主机名\nhostname\n\n2.修改主机名(重启后无效)\nhostname hadoop\n\n3.修改主机名(重启后永久生效)\nvi /ect/sysconfig/network\n\n4.修改IP(重启后无效)\nifconfig eth0 192.168.12.22\n\n5.修改IP(重启后永久生效)\nvi /etc/sysconfig/network-scripts/ifcfg-eth0\n\n6.查看系统信息\nuname -a\nuname -r\n\n7.查看ID命令\nid -u\nid -g\n\n8.日期\ndate\ndate +%Y-%m-%d\ndate +%T\ndate +%Y-%m-%d\" \"%T\n\n9.日历\ncal 2012\n\n10.查看文件信息\nfile filename\n\n11.挂载硬盘\nmount\numount\n加载windows共享\nmount -t cifs //192.168.1.100/tools /mnt\n\n12.查看文件大小\ndu -h\ndu -ah\n\n13.查看分区\ndf -h\n\n14.ssh\nssh hadoop@192.168.1.1\n\n15.关机\nshutdown -h now /init 0\nshutdown -r now /reboot\n\n```\n\n4.用户和组\n\n```\n添加一个tom用户，设置它属于users组，并添加注释信息\n分步完成：useradd tom\n          usermod -g users tom\n\t      usermod -c \"hr tom\" tom\n一步完成：useradd -g users -c \"hr tom\" tom\n\n设置tom用户的密码\npasswd tom\n\n修改tom用户的登陆名为tomcat\nusermod -l tomcat tom\n\n将tomcat添加到sys和root组中\nusermod -G sys,root tomcat\n\n查看tomcat的组信息\ngroups tomcat\n\n添加一个jerry用户并设置密码\nuseradd jerry\npasswd jerry\n\n添加一个交america的组\ngroupadd america\n\n将jerry添加到america组中\nusermod -g america jerry\n\n将tomcat用户从root组和sys组删除\ngpasswd -d tomcat root\ngpasswd -d tomcat sys\n\n将america组名修改为am\ngroupmod -n am america\n\n```\n\n5. 权限\n\n```\n创建a.txt和b.txt文件，将他们设为其拥有者和所在组可写入，但其他以外的人则不可写入:\nchmod ug+w,o-w a.txt b.txt\n\n创建c.txt文件所有人都可以写和执行\nchmod a=wx c.txt 或chmod 666 c.txt\n\n将/hadoop目录下的所有文件与子目录皆设为任何人可读取\nchmod -R a+r /hadoop\n\n将/hadoop目录下的所有文件与子目录的拥有者设为root，用户拥有组为users\nchown -R root:users /hadoop\n\n将当前目录下的所有文件与子目录的用户皆设为hadoop，组设为users\nchown -R hadoop:users *\n\n```\n\n6.目录属性\n\n```\n1.查看文件夹属性\nls -ld test\n\n2.文件夹的rwx\n--x:可以cd进去\nr-x:可以cd进去并ls\n-wx:可以cd进去并touch，rm自己的文件，并且可以vi其他用户的文件\n-wt:可以cd进去并touch，rm自己的文件\n\nls -ld /tmp\ndrwxrwxrwt的权限值是1777(sticky)\n\n```\n\n7.软件安装\n\n```\n1.安装JDK\n\t*添加执行权限 \n\t\tchmod u+x jdk-6u45-linux-i586.bin\n\t*解压\n\t\t./jdk-6u45-linux-i586.bin\n\t*在/usr目录下创建java目录\n\t\tmkdir /usr/java\n\t*将/soft目录下的解压的jdk1.6.0_45剪切到/usr/java目录下\n\t\tmv jdk1.6.0_45/ /usr/java/\n\t*添加环境变量\n\t\tvim /etc/profile\n\t\t*在/etc/profile文件最后添加\n\t\t\texport JAVA_HOME=/usr/java/jdk1.6.0_45\n\t\t\texport CLASSPATH=$JAVA_HOME/lib\n\t\t\texport PATH=$PATH:$JAVA_HOME/bin\n\t*更新配置\n\t\tsource /etc/profile\n\t\t\n2.安装tomcat\n\ttar -zxvf /soft/apache-tomcat-7.0.47.tar.gz -C /programs/\n\tcd /programs/apache-tomcat-7.0.47/bin/\n\t./startup.sh\n\t\n3.安装eclipse\n\t\n\t\t \n```\n8.vim\n```\ni\na/A\no/O\nr + ?替换\n\n0:文件当前行的开头\n$:文件当前行的末尾\nG:文件的最后一行开头\n1 + G到第一行 \n9 + G到第九行 = :9\n\ndd:删除一行\n3dd：删除3行\nyy:复制一行\n3yy:复制3行\np:粘贴\nu:undo\nctrl + r:redo\n\n\"a剪切板a\n\"b剪切板b\n\n\"ap粘贴剪切板a的内容\n\n每次进入vi就有行号\nvi ~/.vimrc\nset nu\n\n:w a.txt另存为\n:w >> a.txt内容追加到a.txt\n\n:e!恢复到最初状态\n\n:1,$s/hadoop/root/g 将第一行到追后一行的hadoop替换为root\n:1,$s/hadoop/root/c 将第一行到追后一行的hadoop替换为root(有提示)\n\n\n```\n\n9.查找\n\n```\n1.查找可执行的命令：\nwhich ls\n\n2.查找可执行的命令和帮助的位置：\nwhereis ls\n\n3.查找文件(需要更新库:updatedb)\nlocate hadoop.txt\n\n4.从某个文件夹开始查找\nfind / -name \"hadooop*\"\nfind / -name \"hadooop*\" -ls\n\n5.查找并删除\nfind / -name \"hadooop*\" -ok rm {} \\;\nfind / -name \"hadooop*\" -exec rm {} \\;\n\n6.查找用户为hadoop的文件\nfind /usr -user hadoop -ls\n\n7.查找用户为hadoop并且(-a)拥有组为root的文件\nfind /usr -user hadoop -a -group root -ls\n\n8.查找用户为hadoop或者(-o)拥有组为root并且是文件夹类型的文件\nfind /usr -user hadoop -o -group root -a -type d\n\n9.查找权限为777的文件\nfind / -perm -777 -type d -ls\n\n10.显示命令历史\nhistory\n\n11.grep\ngrep hadoop /etc/password\n\n```\n\n10.打包与压缩\n\n```\n1.gzip压缩\ngzip a.txt\n\n2.解压\ngunzip a.txt.gz\ngzip -d a.txt.gz\n\n3.bzip2压缩\nbzip2 a\n\n4.解压\nbunzip2 a.bz2\nbzip2 -d a.bz2\n\n5.将当前目录的文件打包\ntar -cvf bak.tar .\n将/etc/password追加文件到bak.tar中(r)\ntar -rvf bak.tar /etc/password\n\n6.解压\ntar -xvf bak.tar\n\n7.打包并压缩gzip\ntar -zcvf a.tar.gz\n\n8.解压缩\ntar -zxvf a.tar.gz\n解压到/usr/下\ntar -zxvf a.tar.gz -C /usr\n\n9.查看压缩包内容\ntar -ztvf a.tar.gz\n\nzip/unzip\n\n10.打包并压缩成bz2\ntar -jcvf a.tar.bz2\n\n11.解压bz2\ntar -jxvf a.tar.bz2\n\n\n```\n\n11.正则\n\n```\n1.cut截取以:分割保留第七段\ngrep hadoop /etc/passwd | cut -d: -f7\n\n2.排序\ndu | sort -n \n\n3.查询不包含hadoop的\ngrep -v hadoop /etc/passwd\n\n4.正则表达包含hadoop\ngrep 'hadoop' /etc/passwd\n\n5.正则表达(点代表任意一个字符)\ngrep 'h.*p' /etc/passwd\n\n6.正则表达以hadoop开头\ngrep '^hadoop' /etc/passwd\n\n7.正则表达以hadoop结尾\ngrep 'hadoop$' /etc/passwd\n\n规则：\n.  : 任意一个字符\na* : 任意多个a(零个或多个a)\na? : 零个或一个a\na+ : 一个或多个a\n.* : 任意多个任意字符\n\\. : 转义.\n\\<h.*p\\> ：以h开头，p结尾的一个单词\no\\{2\\} : o重复两次\n\ngrep '^i.\\{18\\}n$' /usr/share/dict/words\n\n查找不是以#开头的行\ngrep -v '^#' a.txt | grep -v '^$' \n\n以h或r开头的\ngrep '^[hr]' /etc/passwd\n\n不是以h和r开头的\ngrep '^[^hr]' /etc/passwd\n\n不是以h到r开头的\ngrep '^[^h-r]' /etc/passwd\n\n```\n\n12.输入输出重定向及管道\n\n```\n1.新建一个文件\ntouch a.txt\n> b.txt\n\n2.错误重定向:2>\nfind /etc -name zhaoxing.txt 2> error.txt\n\n3.将正确或错误的信息都输入到log.txt中\nfind /etc -name passwd > /tmp/log.txt 2>&1 \nfind /etc -name passwd &> /tmp/log.txt\n\n4.追加>>\n\n5.将小写转为大写（输入重定向）\ntr \"a-z\" \"A-Z\" < /etc/passwd\n\n6.自动创建文件\ncat > log.txt << EXIT\n> ccc\n> ddd\n> EXI\n\n7.查看/etc下的文件有多少个？\nls -l /etc/ | grep '^d' | wc -l\n\n8.查看/etc下的文件有多少个，并将文件详情输入到result.txt中\nls -l /etc/ | grep '^d' | tee result.txt | wc -l\n\n```\n\n13.进程控制\n\n```\n1.查看用户最近登录情况\nlast\nlastlog\n\n2.查看硬盘使用情况\ndf\n\n3.查看文件大小\ndu\n\n4.查看内存使用情况\nfree\n\n5.查看文件系统\n/proc\n\n6.查看日志\nls /var/log/\n\n7.查看系统报错日志\ntail /var/log/messages\n\n8.查看进程\ntop\n\n9.结束进程\nkill 1234\nkill -9 4333\n\n```"
  },
  {
    "path": "NIO/Java NIO之Buffer(缓冲区).md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n\n## Java NIO之Buffer(缓冲区)\n\n\n#### Buffer(缓冲区)介绍\nJava NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里，从Buffer把数据写入到Channels.\n\nBuffer本质上就是一块内存区，可以用来写入数据，并在稍后读取出来。这块内存被NIO Buffer包裹起来，对外提供一系列的读写方便开发的接口。\n\n在Java NIO中使用的核心缓冲区如下（覆盖了通过I/O发送的基本数据类型：byte, char、short, int, long, float, double ，long）：\n\n* ByteBuffer\n* CharBuffer\n* ShortBuffer\n* IntBuffer\n* FloatBuffer\n* DoubleBuffer\n* LongBuffer\n![481220701ebf3276c284ea0a2fa17928](Java NIO之Buffer(缓冲区).resources/1E718F2D-CAEB-4378-8FDB-780BE9803BF5.png)\n利用Buffer读写数据，通常遵循四个步骤：\n\n* 把数据写入buffer\n* 调用flip\n* 从Buffer中读取数据\n* 调用buffer.clear()或者buffer.compact()\n\n当写入数据到buffer中时，buffer会记录已经写入的数据大小。当需要读数据时，通过 flip() 方法把buffer从写模式调整为读模式；在读模式下，可以读取所有已经写入的数据。\n当读取完数据后，需要清空buffer，以满足后续写入操作。清空buffer有两种方式：调用 clear() 或 compact() 方法。clear会清空整个buffer，compact则只清空已读取的数据，未被读取的数据会被移动到buffer的开始位置，写入位置则近跟着未读数据之后。\n\n**Buffer的容量，位置，上限（Buffer Capacity, Position and Limit）**\nBuffer缓冲区实质上就是一块内存，用于写入数据，也供后续再次读取数据。这块内存被NIO Buffer管理，并提供一系列的方法用于更简单的操作这块内存。\n一个Buffer有三个属性是必须掌握的，分别是：\n\n* capacity容量\n* position位置\n* limit限制\n\nposition和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。\n下面有张示例图，描诉了读写模式下position和limit的含义：\n\n![ccf1e3514f39dbc5ebc2b74818005ca0](Java NIO之Buffer(缓冲区).resources/CDDCF910-B3A2-41C1-AB22-6EAFAAD9BE35.png)\n\n>**容量（Capacity）**\n作为一块内存，buffer有一个固定的大小，叫做capacit（容量）。也就是最多只能写入容量值得字节，整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据.\n\n>**位置（Position）**\n>当写入数据到Buffer的时候需要从一个确定的位置开始，默认初始化时这个位置position为0，一旦写入了数据比如一个字节，整形数据，那么position的值就会指向数据之后的一个单元，position最大可以到capacity-1.\n>\n>当从Buffer读取数据时，也需要从一个确定的位置开始。buffer从写入模式变为读取模式时，position会归零，每次读取后，position向后移动。\n\n>**上限（Limit）**\n在写模式，limit的含义是我们所能写入的最大数据量，它等同于buffer的容量。\n\n一旦切换到读模式，limit则代表我们所能读取的最大数据量，他的值等同于写模式下position的位置。换句话说，您可以读取与写入数量相同的字节数（限制设置为写入的字节数，由位置标记）\n\n#### Buffer的常见方法\n\n![58fb3ee7569b404a67362f82a7c9296c](Java NIO之Buffer(缓冲区).resources/03F3F860-14A4-4D45-A998-313304B775E1.png)\n\n\n#### Buffer的使用方式/方法介绍\n\n**分配缓冲区（Allocating a Buffer）**\n\n为了获得缓冲区对象，我们必须首先分配一个缓冲区。在每个Buffer类中，allocate()方法用于分配缓冲区。\n下面来看看ByteBuffer分配容量为28字节的例子:\n```\nByteBuffer buf = ByteBuffer.allocate(28);\n```\n下面来看看另一个示例：CharBuffer分配空间大小为2048个字符\n```\nCharBuffer buf = CharBuffer.allocate(2048);\n```\n\n**写入数据到缓冲区（Writing Data to a Buffer）**\n\n写数据到Buffer有两种方法：\n\n从Channel中写数据到Buffer\n手动写数据到Buffer，调用put方法\n下面是一个实例，演示从Channel写数据到Buffer：\n```\n int bytesRead = inChannel.read(buf); //read into buffer.\n```\n通过put写数据：\n```\nbuf.put(127);\n```\nput方法有很多不同版本，对应不同的写数据方法。例如把数据写到特定的位置，或者把一个字节数据写入buffer。看考JavaDoc文档可以查阅的更多数据。\n\n**翻转(flip())**\n\nflip()方法可以吧Buffer从写模式切换到读模式。调用flip方法会把position归零，并设置limit为之前的position的值。 也就是说，现在position代表的是读取位置，limit标示的是已写入的数据位置。\n\n**从Buffer读取数据（Reading Data from a Buffer）**\n\n从Buffer读数据也有两种方式\n\n* 从buffer读数据到channel\n* 从buffer直接读取数据，调用get方法\n\n读取数据到channel的例子：\n```\nint bytesWritten = inChannel.write(buf);\n```\n调用get读取数据的例子：\n```\nbyte aByte = buf.get();\n```\nget也有诸多版本，对应了不同的读取方式。\n\n**rewind()**\n\nBuffer.rewind()方法将position置为0，这样我们可以重复读取buffer中的数据。limit保持不变。\n\n**clear() and compact()**\n\n一旦我们从buffer中读取完数据，需要复用buffer为下次写数据做准备。只需要调用clear（）或compact（）方法。\n如果调用的是clear()方法，position将被设回0，limit被设置成 capacity的值。换句话说，Buffer 被清空了。Buffer中的数据并未清除，只是这些标记告诉我们可以从哪里开始往Buffer里写数据。\n如果Buffer还有一些数据没有读取完，调用clear就会导致这部分数据被“遗忘”，因为我们没有标记这部分数据未读。\n针对这种情况，如果需要保留未读数据，那么可以使用compact。 因此 compact() 和 clear() 的区别就在于: 对未读数据的处理，是保留这部分数据还是一起清空 。\n\n**mark()与reset()方法**\n\n通过调用Buffer.mark()方法，可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如：\n```\nbuffer.mark();\n//call buffer.get() a couple of times, e.g. during parsing.\nbuffer.reset();  //set position back to mark.    \n```\n\n**equals() and compareTo()**\n\n可以用eqauls和compareTo比较两个buffer\n**equals():**\n\n判断两个buffer相对，需满足：\n\n* 类型相同\n* buffer中剩余字节数相同\n* 所有剩余字节相等\n\n从上面的三个条件可以看出，equals只比较buffer中的部分内容，并不会去比较每一个元素。\n**compareTo():**\ncompareTo也是比较buffer中的剩余元素，只不过这个方法适用于比较排序的：\n#### Buffer常用方法测试\n这里以ByteBuffer为例子说明抽象类Buffer的实现类的一些常见方法的使用：\n```\npackage channel;\n\nimport java.nio.ByteBuffer;\n\npublic class ByteBufferMethods {\n    public static void main(String args[]){\n        //分配缓冲区（Allocating a Buffer）\n        ByteBuffer buffer = ByteBuffer.allocate(33);\n\n        System.out.println(\"-------------Test reset-------------\");\n        //clear()方法，position将被设回0，limit被设置成 capacity的值\n        buffer.clear();\n       // 设置这个缓冲区的位置\n        buffer.position(5);\n        //将此缓冲区的标记设置在其位置。没有buffer.mark();这句话会报错\n        buffer.mark();\n        buffer.position(10);\n        System.out.println(\"before reset:      \" + buffer);\n        //将此缓冲区的位置重置为先前标记的位置。（buffer.position(5)）\n        buffer.reset();\n        System.out.println(\"after reset:       \" + buffer);\n\n        System.out.println(\"-------------Test rewind-------------\");\n        buffer.clear();\n        buffer.position(10);\n        //返回此缓冲区的限制。\n        buffer.limit(15);\n        System.out.println(\"before rewind:       \" + buffer);\n        //把position设为0，mark设为-1，不改变limit的值\n        buffer.rewind();\n        System.out.println(\"before rewind:       \" + buffer);\n\n        System.out.println(\"-------------Test compact-------------\");\n        buffer.clear();\n        buffer.put(\"abcd\".getBytes());\n        System.out.println(\"before compact:       \" + buffer);\n        System.out.println(new String(buffer.array()));\n        //limit = position;position = 0;mark = -1; 翻转，也就是让flip之后的position到limit这块区域变成之前的0到position这块，\n        //翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态\n        buffer.flip();\n        System.out.println(\"after flip:       \" + buffer);\n        //get()方法：相对读，从position位置读取一个byte，并将position+1，为下次读写作准备\n        System.out.println((char) buffer.get());\n        System.out.println((char) buffer.get());\n        System.out.println((char) buffer.get());\n        System.out.println(\"after three gets:       \" + buffer);\n        System.out.println(\"\\t\" + new String(buffer.array()));\n        //把从position到limit中的内容移到0到limit-position的区域内，position和limit的取值也分别变成limit-position、capacity。\n        // 如果先将positon设置到limit，再compact，那么相当于clear()\n        buffer.compact();\n        System.out.println(\"after compact:       \" + buffer);\n        System.out.println(\"\\t\" + new String(buffer.array()));\n\n        System.out.println(\"-------------Test get-------------\");\n        buffer = ByteBuffer.allocate(32);\n        buffer.put((byte) 'a').put((byte) 'b').put((byte) 'c').put((byte) 'd')\n                .put((byte) 'e').put((byte) 'f');\n        System.out.println(\"before flip():       \" + buffer);\n        // 转换为读取模式\n        buffer.flip();\n        System.out.println(\"before get():       \" + buffer);\n        System.out.println((char) buffer.get());\n        System.out.println(\"after get():       \" + buffer);\n        // get(index)不影响position的值\n        System.out.println((char) buffer.get(2));\n        System.out.println(\"after get(index):       \" + buffer);\n        byte[] dst = new byte[10];\n        buffer.get(dst, 0, 2);\n        System.out.println(\"after get(dst, 0, 2):       \" + buffer);\n        System.out.println(\"\\t dst:\" + new String(dst));\n        System.out.println(\"buffer now is:       \" + buffer);\n        System.out.println(\"\\t\" + new String(buffer.array()));\n\n        System.out.println(\"-------------Test put-------------\");\n        ByteBuffer bb = ByteBuffer.allocate(32);\n        System.out.println(\"before put(byte):       \" + bb);\n        System.out.println(\"after put(byte):       \" + bb.put((byte) 'z'));\n        System.out.println(\"\\t\" + bb.put(2, (byte) 'c'));\n        // put(2,(byte) 'c')不改变position的位置\n        System.out.println(\"after put(2,(byte) 'c'):       \" + bb);\n        System.out.println(\"\\t\" + new String(bb.array()));\n        // 这里的buffer是 abcdef[pos=3 lim=6 cap=32]\n        bb.put(buffer);\n        System.out.println(\"after put(buffer):       \" + bb);\n        System.out.println(\"\\t\" + new String(bb.array()));\n    }\n}\n```\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* ByteBuffer常用方法详解\n* JavaNIO易百教程\n\n参考文章：\n《Netty官网》\n>https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "NIO/Java NIO之Channel(通道).md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n\n## Java NIO之Channel（通道）\n\n\n#### Buffer(缓冲区)介绍\n\n通常来说NIO中的所有IO都是从 Channel（通道） 开始的。\n\n* 从通道进行数据读取 ：创建一个缓冲区，然后请求通道读取数据。\n* 从通道进行数据写入 ：创建一个缓冲区，填充数据，并要求通道写入数据。\n\n数据读取和写入操作图示：\n![342194a2fdfeaf96e6051e08c9951de3](Java NIO之Channel(通道).resources/2958433B-EEAF-4D8B-98A2-39941C7C1733.png)\n\n**Java NIO Channel通道和流非常相似，主要有以下几点区别：**\n\n通道可以读也可以写，流一般来说是单向的（只能读或者写，所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流）。\n通道可以异步读写。\n通道总是基于缓冲区Buffer来读写。\n\n**Java NIO中最重要的几个Channel的实现：**\n\n* FileChannel： 用于文件的数据读写\n* DatagramChannel： 用于UDP的数据读写\n* SocketChannel： 用于TCP的数据读写，一般是客户端实现\n* ServerSocketChannel: 允许我们监听TCP链接请求，每个请求会创建会一个SocketChannel，一般是服务器实现\n\n**类层次结构：**\n下面的UML图使用Idea生成的。\n![5153431ea4cfbf8d64f746d098f8bda5](Java NIO之Channel(通道).resources/3A2E73E4-2445-4B90-93F0-0EB34EB8C82B.png)\n\n\n#### FileChannel的使用\n使用FileChannel读取数据到Buffer（缓冲区）以及利用Buffer（缓冲区）写入数据到FileChannel：\n```\npackage filechannel;\n\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\n\npublic class FileChannelTxt {\n    public static void main(String args[]) throws IOException {\n        //1.创建一个RandomAccessFile（随机访问文件）对象，\n        RandomAccessFile raf=new RandomAccessFile(\"D:\\\\niodata.txt\", \"rw\");\n        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。\n        FileChannel inChannel=raf.getChannel();\n        //2.创建一个读数据缓冲区对象\n        ByteBuffer buf=ByteBuffer.allocate(48);\n        //3.从通道中读取数据\n        int bytesRead = inChannel.read(buf);\n        //创建一个写数据缓冲区对象\n        ByteBuffer buf2=ByteBuffer.allocate(48);\n        //写入数据\n        buf2.put(\"filechannel test\".getBytes());\n        buf2.flip();\n        inChannel.write(buf);\n        while (bytesRead != -1) {\n\n            System.out.println(\"Read \" + bytesRead);\n            //Buffer有两种模式，写模式和读模式。在写模式下调用flip()之后，Buffer从写模式变成读模式。\n            buf.flip();\n           //如果还有未读内容\n            while (buf.hasRemaining()) {\n                System.out.print((char) buf.get());\n            }\n            //清空缓存区\n            buf.clear();\n            bytesRead = inChannel.read(buf);\n        }\n        //关闭RandomAccessFile（随机访问文件）对象\n        raf.close();\n    }\n}\n\n```\n运行效果：\n![93e3d051206ec5c22f1997fae7e3a143](Java NIO之Channel(通道).resources/0CC9E605-79FB-455E-AF3F-1CD41832B4A6.png)\n通过上述实例代码，我们可以大概总结出FileChannel的一般使用规则：\n>**1. 开启FileChannel**\n\n使用之前，FileChannel必须被打开 ，但是你无法直接打开FileChannel（FileChannel是抽象类）。需要通过 InputStream ， OutputStream 或 RandomAccessFile 获取FileChannel。\n我们上面的例子是通过RandomAccessFile打开FileChannel的：\n```\n//1.创建一个RandomAccessFile（随机访问文件）对象，\n        RandomAccessFile raf=new RandomAccessFile(\"D:\\\\niodata.txt\", \"rw\");\n        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。\n        FileChannel inChannel=raf.getChannel();\n```\n>**2. 从FileChannel读取数据/写入数据**\n从FileChannel中读取数据/写入数据之前首先要创建一个Buffer（缓冲区）对象，Buffer（缓冲区）对象的使用我们在上一篇文章中已经详细说明了，如果不了解的话可以看我的上一篇关于Buffer的文章。\n\n使用FileChannel的read()方法读取数据：\n```\n//2.创建一个读数据缓冲区对象\n  ByteBuffer buf=ByteBuffer.allocate(48);\n//3.从通道中读取数据\n  int bytesRead = inChannel.read(buf);\n```\n使用FileChannel的write()方法写入数据：\n```\n //创建一个写数据缓冲区对象\n   ByteBuffer buf2=ByteBuffer.allocate(48);\n //写入数据\n   buf2.put(\"filechannel test\".getBytes());\n   buf2.flip();\n   inChannel.write(buf);\n```\n> **3. 关闭FileChannel**\n\n完成使用后，FileChannel您必须关闭它。\n```\nchannel.close();   \n```\n\n#### SocketChannel和ServerSocketChannel的使用\n利用SocketChannel和ServerSocketChannel实现客户端与服务器端简单通信：\nSocketChannel 用于创建基于tcp协议的客户端对象，因为SocketChannel中不存在accept()方法，所以，它不能成为一个服务端程序。通过 connect()方法 ，SocketChannel对象可以连接到其他tcp服务器程序。\n客户端:\n```\npackage socketchannel;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SocketChannel;\n\npublic class WebClient {\n    public static void main(String[] args) throws IOException {\n        //1.通过SocketChannel的open()方法创建一个SocketChannel对象\n        SocketChannel socketChannel = SocketChannel.open();\n        //2.连接到远程服务器（连接此通道的socket）\n        socketChannel.connect(new InetSocketAddress(\"127.0.0.1\", 3333));\n        // 3.创建写数据缓存区对象\n        ByteBuffer writeBuffer = ByteBuffer.allocate(128);\n        writeBuffer.put(\"hello WebServer this is from WebClient\".getBytes());\n        writeBuffer.flip();\n        socketChannel.write(writeBuffer);\n        //创建读数据缓存区对象\n        ByteBuffer readBuffer = ByteBuffer.allocate(128);\n        socketChannel.read(readBuffer);\n        //String 字符串常量，不可变；StringBuffer 字符串变量（线程安全），可变；StringBuilder 字符串变量（非线程安全），可变\n        StringBuilder stringBuffer=new StringBuilder();\n        //4.将Buffer从写模式变为可读模式\n        readBuffer.flip();\n        while (readBuffer.hasRemaining()) {\n            stringBuffer.append((char) readBuffer.get());\n        }\n        System.out.println(\"从服务端接收到的数据：\"+stringBuffer);\n\n        socketChannel.close();\n    }\n\n}\n```\nServerSocketChannel 允许我们监听TCP链接请求，通过ServerSocketChannelImpl的 accept()方法 可以创建一个SocketChannel对象用户从客户端读/写数据。\n\n服务端：\n```\npackage socketchannel;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ServerSocketChannel;\nimport java.nio.channels.SocketChannel;\n\npublic class WebServer {\n    public static void main(String args[]) throws IOException {\n        try {\n            //1.通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象，open方法的作用：打开套接字通道\n            ServerSocketChannel ssc = ServerSocketChannel.open();\n            //2.通过ServerSocketChannel绑定ip地址和port(端口号)\n            ssc.socket().bind(new InetSocketAddress(\"127.0.0.1\", 3333));\n            //通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据\n            SocketChannel socketChannel = ssc.accept();\n            //3.创建写数据的缓存区对象\n            ByteBuffer writeBuffer = ByteBuffer.allocate(128);\n            writeBuffer.put(\"hello WebClient this is from WebServer\".getBytes());\n            writeBuffer.flip();\n            socketChannel.write(writeBuffer);\n            //创建读数据的缓存区对象\n            ByteBuffer readBuffer = ByteBuffer.allocate(128);\n            //读取缓存区数据\n            socketChannel.read(readBuffer);\n            StringBuilder stringBuffer=new StringBuilder();\n            //4.将Buffer从写模式变为可读模式\n            readBuffer.flip();\n            while (readBuffer.hasRemaining()) {\n                stringBuffer.append((char) readBuffer.get());\n            }\n            System.out.println(\"从客户端接收到的数据：\"+stringBuffer);\n            socketChannel.close();\n            ssc.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n**运行效果**\n客户端：\n![37ac5661df301bcc55f3bab690d6c3ea](Java NIO之Channel(通道).resources/6AF85EF7-83C7-48B6-A6AB-C70AD22A91D4.png)\n服务端：\n![d6b8298bd2108e3fcd6ed422cec8daa8](Java NIO之Channel(通道).resources/821A61BD-80DF-493F-99D8-4F5330211339.png)\n通过上述实例代码，我们可以大概总结出SocketChannel和ServerSocketChannel的使用的一般使用规则：\n考虑到篇幅问题，下面只给出大致步骤，不贴代码，可以结合上述实例理解。\n**客户端**\n1.通过SocketChannel连接到远程服务器\n2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据\n3.关闭SocketChannel\n**服务端**\n1.通过ServerSocketChannel 绑定ip地址和端口号\n2.通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据\n3.创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据\n4. 关闭SocketChannel和ServerSocketChannel\n\n#### DatagramChannel的使用\n\nDataGramChannel，类似于java 网络编程的DatagramSocket类；使用UDP进行网络传输， UDP是无连接，面向数据报文段的协议，对传输的数据不保证安全与完整 ；和上面介绍的SocketChannel和ServerSocketChannel的使用方法类似，所以这里就简单介绍一下如何使用。\n**1.获取DataGramChannel**\n```\n//1.通过DatagramChannel的open()方法创建一个DatagramChannel对象\n DatagramChannel datagramChannel = DatagramChannel.open();\n  //绑定一个port（端口）\n datagramChannel.bind(new InetSocketAddress(1234));\n```\n上面代码表示程序可以在1234端口接收数据报。\n\n**2.接收/发送消息**\n接收消息：\n先创建一个缓存区对象，然后通过receive方法接收消息，这个方法返回一个SocketAddress对象，表示发送消息方的地址：\n```\nByteBuffer buf = ByteBuffer.allocate(48);\nbuf.clear();\nchannel.receive(buf);\n```\n发送消息：\n由于UDP下，服务端和客户端通信并不需要建立连接，只需要知道对方地址即可发出消息，但是是否发送成功或者成功被接收到是没有保证的;发送消息通过send方法发出，改方法返回一个int值，表示成功发送的字节数：\n```\nByteBuffer buf = ByteBuffer.allocate(48);\nbuf.clear();\nbuf.put(\"datagramchannel\".getBytes());\nbuf.flip();\nint send = channel.send(buffer, new InetSocketAddress(\"localhost\",1234));\n```\n这个例子发送一串字符：“datagramchannel”到主机名为”localhost”服务器的端口1234上。\n\n#### Scatter/Gather\nChannel 提供了一种被称为 Scatter/Gather 的新功能，也称为本地矢量 I/O。Scatter/Gather 是指在多个缓冲区上实现一个简单的 I/O 操作。正确使用 Scatter / Gather可以明显提高性能。\n大多数现代操作系统都支持本地矢量I/O（native vectored I/O）操作。当您在一个通道上请求一个Scatter/Gather操作时，该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区，减少或避免了缓冲区拷贝和系统调用；\nScatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。\nScatter/Gather功能是通道(Channel)提供的  并不是Buffer。\n\n* Scatter:  从一个Channel读取的信息分散到N个缓冲区中(Buufer).\n* Gather:  将N个Buffer里面内容按照顺序发送到一个Channel.\n\n**Scattering Reads**\n\"scattering read\"是把数据从单个Channel写入到多个buffer,如下图所示：\n![820b8ed4fd205e451772c9d18e0d629f](Java NIO之Channel(通道).resources/D2633F82-0A59-488A-AEC6-AB443A3125F4.png)\n示例代码:\n```\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body   = ByteBuffer.allocate(1024);\nByteBuffer[] bufferArray = { header, body };\nchannel.read(bufferArray);\n```\nread()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后，接着写到下一个buffer中。\n举个例子，假如通道中有200个字节数据，那么header会被写入128个字节数据，body会被写入72个字节数据；\n注意：\n无论是scatter还是gather操作，都是按照buffer在数组中的顺序来依次读取或写入的；\n**Gathering Writes**\n\"gathering write\"把多个buffer的数据写入到同一个channel中，下面是示意图\n![f39ff57a4463a05cc93ae22f402e6683](Java NIO之Channel(通道).resources/19060EA5-78B2-49F1-A706-0C99F3BC51A5.png)\n示例代码：\n```\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body   = ByteBuffer.allocate(1024);\n//write data into buffers\nByteBuffer[] bufferArray = { header, body };\nchannel.write(bufferArray);\n```\nwrite()方法内部会负责把数据按顺序写入到channel中。\n注意：\n并不是所有数据都写入到通道，写入的数据要根据position和limit的值来判断，只有position和limit之间的数据才会被写入；\n举个例子，假如以上header缓冲区中有128个字节数据，但此时position=0，limit=58；那么只有下标索引为0-57的数据才会被写入到通道中.\n\n#### 通道之间的数据传输\n在Java NIO中如果一个channel是FileChannel类型的，那么他可以直接把数据传输到另一个channel。\n\n\n* **transferFrom():** transferFrom方法把数据从通道源传输到FileChannel\n* **transferTo():** transferTo方法把FileChannel数据传输到另一个channel\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* ByteBuffer常用方法详解\n* JavaNIO易百教程\n\n\n参考文章：\n《Netty官网》\n>https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "NIO/Java NIO之Selector(选择器).md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n## Java NIO之Selector（选择器）\n\n\n#### Selector（选择器）介绍\nSelector一般称为选择器,当然你也可以翻译为多路复用器。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel（通道）的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。\n![e87095c49bef56cb3cce7c4529cac9ad](Java NIO之Selector(选择器).resources/C32A7750-CD6E-469A-8E9E-BEC983989522.png)\n使用Selector的好处在于:使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。\n\n#### Selector（选择器）的使用方法介绍\n**1. Selector的创建**\n通过调用Selector.open()方法创建一个Selector对象，如下：\n```\nSelector selector = Selector.open();\n```\n**2. 注册Channel到Selector**\n```\nchannel.configureBlocking(false);\nSelectionKey key = channel.register(selector, Selectionkey.OP_READ);\n```\n**Channel必须是非阻塞的。**\n所以FileChannel不适用Selector，因为FileChannel不能切换为非阻塞模式，更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。\nSelectableChannel抽象类 有一个 configureBlocking（） 方法用于使通道处于阻塞模式或非阻塞模式。\n```\nabstract SelectableChannel configureBlocking(boolean block)  \n```\n注意：\nSelectableChannel抽象类的configureBlocking（） 方法是由 AbstractSelectableChannel抽象类实现的，SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了 AbstractSelectableChannel抽象类 。\n大家有兴趣可以看看NIO的源码，各种抽象类和抽象类上层的抽象类。我本人暂时不准备研究NIO源码，因为还有很多事情要做，需要研究的同学可以自行看看。\nregister() 方法的第二个参数。这是一个“ interest集合 ”，意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件：\n* Connect\n* Accept\n* Read\n* Write\n\n通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为\"连接就绪\"。一个Server Socket Channel准备好接收新进入的连接称为\"接收就绪\"。一个有数据可读的通道可以说是\"读就绪\"。等待写数据的通道可以说是\"写就绪\"。\n这四种事件用SelectionKey的四个常量来表示：\nSelectionKey.OP_CONNECT\nSelectionKey.OP_ACCEPT\nSelectionKey.OP_READ\nSelectionKey.OP_WRITE\n\n如果你对不止一种事件感兴趣，使用或运算符即可，如下：\nint interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;\n\n**3. SelectionKey介绍**\n一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。\n```\nkey.attachment(); //返回SelectionKey的attachment，attachment可以在注册channel的时候指定。\nkey.channel(); // 返回该SelectionKey对应的channel。\nkey.selector(); // 返回该SelectionKey对应的Selector。\nkey.interestOps(); //返回代表需要Selector监控的IO操作的bit mask\nkey.readyOps(); // 返回一个bit mask，代表在相应channel上可以进行的IO操作。\n```\n**key.interestOps():**\n\n我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣\n```\nint interestSet = selectionKey.interestOps(); \nboolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT；\nboolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;\nboolean isInterestedInRead = interestSet & SelectionKey.OP_READ;\nboolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;\n```\n**key.readyOps()**\nready 集合是通道已经准备就绪的操作的集合。JAVA中定义以下几个方法用来检查这些操作是否就绪.\n```\n//创建ready集合的方法\nint readySet = selectionKey.readyOps();\n//检查这些操作是否就绪的方法\nkey.isAcceptable();//是否可读，是返回 true\nboolean isWritable()：//是否可写，是返回 true\nboolean isConnectable()：//是否可连接，是返回 true\nboolean isAcceptable()：//是否可接收，是返回 true\n```\n**从SelectionKey访问Channel和Selector很简单。如下：**\n```\nChannel channel = key.channel();\nSelector selector = key.selector();\nkey.attachment();\n```\n可以将一个对象或者更多信息附着到SelectionKey上，这样就能方便的识别某个给定的通道。例如，可以附加 与通道一起使用的Buffer，或是包含聚集数据的某个对象。使用方法如下：\n```\nkey.attach(theObject);\nObject attachedObj = key.attachment();\n```\n还可以在用register()方法向Selector注册Channel的时候附加对象。如：\n```\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);\n```\n**4. 从Selector中选择channel(Selecting Channels via a Selector)**\n选择器维护注册过的通道的集合，并且这种注册关系都被封装在SelectionKey当中.\n\n**Selector维护的三种类型SelectionKey集合：**\n\n* **已注册的键的集合(Registered key set)**\n\n所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回，并且可能是空的。这个已注册的键的集合不是可以直接修改的；试图这么做的话将引发java.lang.UnsupportedOperationException。\n\n\n* **已选择的键的集合(Selected key set)**\n\n所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回，并且可能是空的。这个已注册的键的集合不是可以直接修改的；试图这么做的话将引发java.lang.UnsupportedOperationException。\n\n\n* **已取消的键的集合(Cancelled key set)**\n\n已注册的键的集合的子集，这个集合包含了 cancel() 方法被调用过的键(这个键已经被无效化)，但它们还没有被注销。这个集合是选择器对象的私有成员，因而无法直接访问。\n注意：\n当键被取消（ 可以通过isValid( ) 方法来判断）时，它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消，但键会立即失效。当再次调用 select( ) 方法时（或者一个正在进行的select()调用结束时），已取消的键的集合中的被取消的键将被清理掉，并且相应的注销也将完成。通道会被注销，而新的SelectionKey将被返回。当通道关闭时，所有相关的键会自动取消（记住，一个通道可以被注册到多个选择器上）。当选择器关闭时，所有被注册到该选择器的通道都将被注销，并且相关的键将立即被无效化（取消）。一旦键被无效化，调用它的与选择相关的方法就将抛出CancelledKeyException。\n\n**select()方法介绍：**\n\n在刚初始化的Selector对象中，这三个集合都是空的。 通过Selector的select（）方法可以选择已经准备就绪的通道 （这些通道包含你感兴趣的事件）。比如你对读就绪的通道感兴趣，那么select（）方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法：\n\n* int select()：阻塞到至少有一个通道在你注册的事件上就绪了。\n* int select(long timeout)：和select()一样，但最长阻塞时间为timeout毫秒。\n* int selectNow()：非阻塞，只要有通道就绪就立刻返回。\n\nselect()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select（）调用时进入就绪的通道不会在本次调用中被记入，而在前一次select（）调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如：首次调用select()方法，如果有一个通道变成就绪状态，返回了1，若再次调用select()方法，如果另一个通道就绪了，它会再次返回1。如果对第一个就绪的channel没有做任何操作，现在就有两个就绪的通道，但在每次select()方法调用之间，只有一个通道就绪了。\n一旦调用select()方法，并且返回值不为0时，则 可以通过调用Selector的selectedKeys()方法来访问已选择键集合 。如下：\n```\nSet selectedKeys=selector.selectedKeys();\n进而可以放到和某SelectionKey关联的Selector和Channel。如下所示：\nSet selectedKeys = selector.selectedKeys();\nIterator keyIterator = selectedKeys.iterator();\nwhile(keyIterator.hasNext()) {\n    SelectionKey key = keyIterator.next();\n    if(key.isAcceptable()) {\n        // a connection was accepted by a ServerSocketChannel.\n    } else if (key.isConnectable()) {\n        // a connection was established with a remote server.\n    } else if (key.isReadable()) {\n        // a channel is ready for reading\n    } else if (key.isWritable()) {\n        // a channel is ready for writing\n    }\n    keyIterator.remove();\n}\n```\n**5. 停止选择的方法**\n选择器执行选择的过程，系统底层会依次询问每个通道是否已经就绪，这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select（）方法中阻塞的线程。\n\n\n* wakeup()方法 ：通过调用Selector对象的wakeup（）方法让处在阻塞状态的select()方法立刻返回\n该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作，那么下一次对select()方法的一次调用将立即返回。\n\n* close()方法 ：通过close（）方法关闭Selector\n该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销，所有的键将被取消，但是Channel本身并不会关闭。\n\n#### 模板代码\n一个服务端的模板代码：\n有了模板代码我们在编写程序时，大多数时间都是在模板代码中添加相应的业务代码\n```\nServerSocketChannel ssc = ServerSocketChannel.open();\nssc.socket().bind(new InetSocketAddress(\"localhost\", 8080));\nssc.configureBlocking(false);\n\nSelector selector = Selector.open();\nssc.register(selector, SelectionKey.OP_ACCEPT);\n\nwhile(true) {\n    int readyNum = selector.select();\n    if (readyNum == 0) {\n        continue;\n    }\n\n    Set<SelectionKey> selectedKeys = selector.selectedKeys();\n    Iterator<SelectionKey> it = selectedKeys.iterator();\n    \n    while(it.hasNext()) {\n        SelectionKey key = it.next();\n        \n        if(key.isAcceptable()) {\n            // 接受连接\n        } else if (key.isReadable()) {\n            // 通道可读\n        } else if (key.isWritable()) {\n            // 通道可写\n        }\n        \n        it.remove();\n    }\n}\n```\n#### 客户端与服务端简单交互实例\n服务端：\n```\npackage selector;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SelectionKey;\nimport java.nio.channels.Selector;\nimport java.nio.channels.ServerSocketChannel;\nimport java.nio.channels.SocketChannel;\nimport java.util.Iterator;\nimport java.util.Set;\n\npublic class WebServer {\n    public static void main(String[] args) {\n        try {\n            ServerSocketChannel ssc = ServerSocketChannel.open();\n            ssc.socket().bind(new InetSocketAddress(\"127.0.0.1\", 8000));\n            ssc.configureBlocking(false);\n\n            Selector selector = Selector.open();\n            // 注册 channel，并且指定感兴趣的事件是 Accept\n            ssc.register(selector, SelectionKey.OP_ACCEPT);\n\n            ByteBuffer readBuff = ByteBuffer.allocate(1024);\n            ByteBuffer writeBuff = ByteBuffer.allocate(128);\n            writeBuff.put(\"received\".getBytes());\n            writeBuff.flip();\n\n            while (true) {\n                int nReady = selector.select();\n                Set<SelectionKey> keys = selector.selectedKeys();\n                Iterator<SelectionKey> it = keys.iterator();\n\n                while (it.hasNext()) {\n                    SelectionKey key = it.next();\n                    it.remove();\n\n                    if (key.isAcceptable()) {\n                        // 创建新的连接，并且把连接注册到selector上，而且，\n                        // 声明这个channel只对读操作感兴趣。\n                        SocketChannel socketChannel = ssc.accept();\n                        socketChannel.configureBlocking(false);\n                        socketChannel.register(selector, SelectionKey.OP_READ);\n                    }\n                    else if (key.isReadable()) {\n                        SocketChannel socketChannel = (SocketChannel) key.channel();\n                        readBuff.clear();\n                        socketChannel.read(readBuff);\n\n                        readBuff.flip();\n                        System.out.println(\"received : \" + new String(readBuff.array()));\n                        key.interestOps(SelectionKey.OP_WRITE);\n                    }\n                    else if (key.isWritable()) {\n                        writeBuff.rewind();\n                        SocketChannel socketChannel = (SocketChannel) key.channel();\n                        socketChannel.write(writeBuff);\n                        key.interestOps(SelectionKey.OP_READ);\n                    }\n                }\n            }\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n客户端：\n```\npackage selector;\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.SocketChannel;\n\npublic class WebClient {\n    public static void main(String[] args) throws IOException {\n        try {\n            SocketChannel socketChannel = SocketChannel.open();\n            socketChannel.connect(new InetSocketAddress(\"127.0.0.1\", 8000));\n\n            ByteBuffer writeBuffer = ByteBuffer.allocate(32);\n            ByteBuffer readBuffer = ByteBuffer.allocate(32);\n\n            writeBuffer.put(\"hello\".getBytes());\n            writeBuffer.flip();\n\n            while (true) {\n                writeBuffer.rewind();\n                socketChannel.write(writeBuffer);\n                readBuffer.clear();\n                socketChannel.read(readBuffer);\n            }\n        } catch (IOException e) {\n        }\n    }\n}\n```\n**运行结果：**\n先运行服务端，再运行客户端，服务端会不断收到客户端发送过来的消息。\n\n![6164e7d53ea6af8d8578f1f9ae9e2d6e](Java NIO之Selector(选择器).resources/3B614359-3026-4B01-938C-605FA70D1FCD.png)\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* ByteBuffer常用方法详解\n* JavaNIO易百教程\n* https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "NIO/Java NIO之拥抱Path和Files.md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n## Java NIO之拥抱Path和Files\n\n#### 文件I/O基石：Path\n\nJava7中文件IO发生了很大的变化，专门引入了很多新的类来取代原来的基于java.io.File的文件IO操作方式:\n```\nimport java.nio.file.DirectoryStream;\nimport java.nio.file.FileSystem;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.FileAttribute;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.nio.file.attribute.PosixFilePermissions;·\n......\n```\n我们将从下面几个方面来学习Path类:\n\n* 创建一个Path\n* File和Path之间的转换，File和URI之间的转换\n* 获取Path的相关信息\n* 移除Path中的冗余项\n\n**1 创建一个Path**\n创建Path实例可以通过 Paths工具类 的 get（）方法：\n```\n//使用绝对路径\n Path path= Paths.get(\"c:\\\\data\\\\myfile.txt\");\n//使用相对路径\nPath path = Paths.get(\"/home/jakobjenkov/myfile.txt\");\n```\n下面这种创建方式和上面等效：\n```\nPath path = FileSystems.getDefault().getPath(\"c:\\\\data\\\\myfile.txt\");\n```\n**2 File和Path之间的转换，File和URI之间的转换**\n```\n        File file = new File(\"C:/my.ini\");\n        Path p1 = file.toPath();\n        p1.toFile();\n        file.toURI();\n```\n**3 获取Path的相关信息**\n       \n ```\n        //使用Paths工具类的get()方法创建\n        Path path = Paths.get(\"D:\\\\XMind\\\\bcl-java.txt\");\n        System.out.println(\"文件名：\" + path.getFileName());\n        System.out.println(\"名称元素的数量：\" + path.getNameCount());\n        System.out.println(\"父路径：\" + path.getParent());\n        System.out.println(\"根路径：\" + path.getRoot());\n        System.out.println(\"是否是绝对路径：\" + path.isAbsolute());\n        //startsWith()方法的参数既可以是字符串也可以是Path对象\n        System.out.println(\"是否是以为给定的路径D:开始：\" + path.startsWith(\"D:\\\\\") );\n        System.out.println(\"该路径的字符串形式：\" + path.toString());\n```\n结果：\n```\n文件名：bcl-java.txt\n名称元素的数量：2\n父路径：D:\\XMind\n根路径：D:\\\n是否是绝对路径：true\n是否是以为给定的路径D:开始：true\n该路径的字符串形式：D:\\XMind\\bcl-java.txt\n```\n**4 移除冗余项**\n某些时候在我们需要处理的Path路径中可能会有一个或两个点\n\n* .表示的是当前目录\n* ..表示父目录或者说是上一级目录：\n\n下面通过实例来演示一下使用Path类的normalize()和toRealPath()方法把.和..去除。\n\n* normalize() : 返回一个路径，该路径是冗余名称元素的消除。\n* toRealPath() : 融合了toAbsolutePath()方法和normalize()方法\n```\n\n        //.表示的是当前目录\n        Path currentDir = Paths.get(\".\");\n        System.out.println(currentDir.toAbsolutePath());//输出C:\\Users\\Administrator\\NIODemo\\.\n        Path currentDir2 = Paths.get(\".\\\\NIODemo.iml\");\n        System.out.println(\"原始路径格式：\"+currentDir2.toAbsolutePath());\n        System.out.println(\"执行normalize（）方法之后：\"+currentDir2.toAbsolutePath().normalize());\n        System.out.println(\"执行toRealPath()方法之后：\"+currentDir2.toRealPath());\n        //..表示父目录或者说是上一级目录：\n        Path currentDir3 = Paths.get(\"..\");\n        System.out.println(\"原始路径格式：\"+currentDir3.toAbsolutePath());\n        System.out.println(\"执行normalize（）方法之后：\"+currentDir3.toAbsolutePath().normalize());\n        System.out.println(\"执行toRealPath()方法之后：\"+currentDir3.toRealPath());\n```\n结果：\n```\nC:\\Users\\Administrator\\NIODemo\\.\n原始路径格式：C:\\Users\\Administrator\\NIODemo\\.\\NIODemo.iml\n执行normalize（）方法之后：C:\\Users\\Administrator\\NIODemo\\NIODemo.iml\n执行toRealPath()方法之后：C:\\Users\\Administrator\\NIODemo\\NIODemo.iml\n原始路径格式：C:\\Users\\Administrator\\NIODemo\\..\n执行normalize（）方法之后：C:\\Users\\Administrator\n执行toRealPath()方法之后：C:\\Users\\Administrator\n```\n![d6a70ed9337b5e22fa34de22f36236b4](Java NIO之拥抱Path和Files.resources/1E6CACD6-76A0-4D6C-8E05-C1D9353E293A.png)\n\n#### 拥抱Files类\nJava NIO中的Files类（java.nio.file.Files）提供了多种操作文件系统中文件的方法。本节教程将覆盖大部分方法。Files类包含了很多方法，所以如果本文没有提到的你也可以直接查询JavaDoc文档。\njava.nio.file.Files类是和java.nio.file.Path相结合使用的\n\n**1 检查给定的Path在文件系统中是否存在**\n通过 Files.exists() 检测文件路径是否存在：\n```\n       Path path = Paths.get(\"D:\\\\XMind\\\\bcl-java.txt\");\n        boolean pathExists =\n                Files.exists(path,\n                        new LinkOption[]{LinkOption.NOFOLLOW_LINKS});\n        System.out.println(pathExists);//true\n```\n注意Files.exists()的第二个参数。它是一个数组，这个参数直接影响到Files.exists()如何确定一个路径是否存在。在本例中，这个数组内包含了LinkOptions.NOFOLLOW_LINKS，表示检测时不包含符号链接文件。\n\n**2 创建文件/文件夹**\n\n**创建文件：**\n通过 Files.createFile() 创建文件:\n```\n        Path target2 = Paths.get(\"C:\\\\mystuff.txt\");\n        try {\n            if(!Files.exists(target2))\n                Files.createFile(target2);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n```\n**创建文件夹：**\n\n* 通过 Files.createDirectory() 创建文件夹\n* 通过 Files.createDirectories() 创建文件夹\n\nFiles.createDirectories()会首先创建所有不存在的父目录来创建目录，而Files.createDirectory()方法只是创建目录，如果它的上级目录不存在就会报错。比如下面的程序使用Files.createDirectory() 方法创建就会报错，这是因为我的D盘下没有data文件夹，加入存在data文件夹的话则没问题。\n```\n    Path path = Paths.get(\"D://data//test\");\n    try {\n        Path newDir = Files.createDirectories(path);\n    } catch(FileAlreadyExistsException e){\n        // the directory already exists.\n    } catch (IOException e) {\n        //something else went wrong\n        e.printStackTrace();\n    }\n```\n**3 删除文件或目录**\n通过 Files.delete()方法 可以删除一个文件或目录：\n```\nPath path = Paths.get(\"data/subdir/logging-moved.properties\");\n\ntry {\n    Files.delete(path);\n} catch (IOException e) {\n    //deleting file failed\n    e.printStackTrace();\n}\n```\n**4 把一个文件从一个地址复制到另一个位置**\n通过Files.copy()方法可以吧一个文件从一个地址复制到另一个位置\n```\nPath sourcePath = Paths.get(\"data/logging.properties\");\nPath destinationPath = Paths.get(\"data/logging-copy.properties\");\n\ntry {\n    Files.copy(sourcePath, destinationPath);\n} catch(FileAlreadyExistsException e) {\n    //destination file already exists\n} catch (IOException e) {\n    //something else went wrong\n    e.printStackTrace();\n}\n```\ncopy操作还可可以强制覆盖已经存在的目标文件，只需要将上面的copy()方法改为如下格式：\n```\n    Files.copy(sourcePath, destinationPath,\n            StandardCopyOption.REPLACE_EXISTING);\n```\n**5 获取文件属性**\n```\n        Path path = Paths.get(\"D:\\\\XMind\\\\bcl-java.txt\");\n        System.out.println(Files.getLastModifiedTime(path));\n        System.out.println(Files.size(path));\n        System.out.println(Files.isSymbolicLink(path));\n        System.out.println(Files.isDirectory(path));\n        System.out.println(Files.readAttributes(path, \"*\"));\n```\n结果：\n```\n2016-05-18T08:01:44Z\n18934\nfalse\nfalse\n{lastAccessTime=2017-04-12T01:42:21.149351Z, lastModifiedTime=2016-05-18T08:01:44Z, size=18934, creationTime=2017-04-12T01:42:21.149351Z, isSymbolicLink=false, isRegularFile=true, fil\n```\n**6 遍历一个文件夹**\n```\n        Path dir = Paths.get(\"D:\\\\Java\");\n        try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)){\n            for(Path e : stream){\n                System.out.println(e.getFileName());\n            }\n        }catch(IOException e){\n\n        }\n```\n结果：\n```\napache-maven-3.5.0\nEclipse\nintellij idea\nJar\nJDK\nMarvenRespository\nMyEclipse 2017 CI\nNodejs\nRedisDesktopManager\nsolr-7.2.1\n```\n上面是遍历单个目录，它不会遍历整个目录。遍历整个目录需要使用：Files.walkFileTree().Files.walkFileTree()方法具有递归遍历目录的功能。\n\n**7 遍历整个文件目录：**\nwalkFileTree接受一个Path和FileVisitor作为参数。Path对象是需要遍历的目录，FileVistor则会在每次遍历中被调用。\nFileVisitor需要调用方自行实现，然后作为参数传入walkFileTree().FileVisitor的每个方法会在遍历过程中被调用多次。如果不需要处理每个方法，那么可以继承它的默认实现类SimpleFileVisitor，它将所有的接口做了空实现。\n```\npublic class WorkFileTree {\n    public static void main(String[] args) throws IOException{\n        Path startingDir = Paths.get(\"D:\\\\apache-tomcat-9.0.0.M17\");\n        List<Path> result = new LinkedList<Path>();\n        Files.walkFileTree(startingDir, new FindJavaVisitor(result));\n        System.out.println(\"result.size()=\" + result.size());\n    }\n\n    private static class FindJavaVisitor extends SimpleFileVisitor<Path>{\n        private List<Path> result;\n        public FindJavaVisitor(List<Path> result){\n            this.result = result;\n        }\n        @Override\n        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs){\n            if(file.toString().endsWith(\".java\")){\n                result.add(file.getFileName());\n            }\n            return FileVisitResult.CONTINUE;\n        }\n    }\n}\n```\n上面这个例子输出了我的D:\\apache-tomcat-9.0.0.M17也就是我的Tomcat安装目录下以.java结尾文件的数量。\n结果：\n```\nresult.size()=4\n```\nFiles类真的很强大，除了我讲的这些操作之外还有其他很多操作比如：读取和设置文件权限、更新文件所有者等等操作。\n\n**参考文档：**\n\n* 官方JDK相关文档\n* 谷歌搜索排名第一的Java NIO教程\n* 《Java程序员修炼之道》\n* 《Java 8编程官方参考教程（第9版）》\n* Java7新特性之文件操作"
  },
  {
    "path": "NIO/NIO概览.md",
    "content": "### **Java高级特性增强-NIO\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了部分勘误，感谢前辈的付出，每节文章末尾有引用列表~\n* * *\n**写在所有文字的前面**：作者在此特别推荐Google排名第一的关于NIO的文章：\nhttp://tutorials.jenkov.com/java-nio/index.html\n虽然是英文的，但是看下来并不困难。后面如果各位看官呼声很高，作者会翻译这一系列文章。\n\n\n## NIO概览\n\n\n#### 从Java IO入手\n先看一张网上流传的http://java.io包的类结构图：\n![3db10ad6b31d95ebfa36d39645e342fc](NIO概览.resources/1EA58812-D4D0-40FA-9860-6F6C6E103FFA.png)\n当你看到这幅图的时候，我相信，你跟我一样内心是崩溃的。\n有些人不怕枯燥，不怕寂寞，硬着头皮看源码，但是，能坚持下去全部看完的又有几个呢！\n然而，就算源码全部看完看懂，过不了几天，脑子里也会变成一团浆糊。\n因为这里的类实在太多了。可能我们反复看，反复记，也很难做到清晰明白。\n他就像是一块超级硬的骨头，怎么啃都啃不烂。\n面对这样的做法，要坚决对他说，NO。\n\n我的做法是找出他们的共性，给他们分类，只记典型，触类旁通。\n上面的图虽然有分类，但是还不够细，而且没有总结出方便记忆的规律，所以我们要重新整理和归类。\n这篇文章中，使用了两种分时给他们分组，目的是更全面的了解共性，帮助记忆。\n\n#### 分类一：按操作方式（类结构）\n\n**字节流和字符流:**\n字节流：以字节为单位，每次次读入或读出是8位数据。可以读任何类型数据。\n字符流：以字符为单位，每次次读入或读出是16位数据。其只能读取字符类型数据。\n**输出流和输入流:**\n输出流：从内存读出到文件。只能进行写操作。\n输入流：从文件读入到内存。只能进行读操作。\n注意： 这里的出和入，都是相对于系统内存而言的。\n**节点流和处理流:**\n节点流：直接与数据源相连，读入或读出。\n处理流：与节点流一块使用，在节点流的基础上，再套接一层，套接在节点流上的就是处理流。\n**为什么要有处理流？**直接使用节点流，读写不方便，为了更快的读写文件，才有了处理流。\n根据以上分类，以及jdk的说明，我们可以画出更详细的类结构图，如下:\n![08a43f0086bd0b2f2c6adbe12ba53203](NIO概览.resources/E97A1DBA-0CC4-4679-A081-B164B1645040.jpg)\n**分类说明：**\n**1） 输入字节流InputStream:**\n\nByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流，它们分别从Byte 数组、StringBuffer、和本地文件中读取数据。\n\nPipedInputStream 是从与其它线程共用的管道中读取数据。PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用，共同完成管道的读取写入操作。主要用于线程操作。\n\nDataInputStream： 将基础数据类型读取出来\n\nObjectInputStream 和所有 FilterInputStream 的子类都是装饰流（装饰器模式的主角）。\n\n**2）输出字节流OutputStream:**\n\nByteArrayOutputStream、FileOutputStream： 是两种基本的介质流，它们分别向- Byte 数组、和本地文件中写入数据。\n\nPipedOutputStream 是向与其它线程共用的管道中写入数据。\n\nDataOutputStream 将基础数据类型写入到文件中\n\nObjectOutputStream 和所有 FilterOutputStream 的子类都是装饰流。\n\n节流的输入和输出类结构图：\n![ad1daa76924b325f7f5a5b580c5d5872](NIO概览.resources/D96C7B52-7E5A-44FA-9EB3-6D146ADE7EEF.png)\n3）字符输入流Reader：\n\nFileReader、CharReader、StringReader 是三种基本的介质流，它们分在本地文件、Char 数组、String中读取数据。\n\nPipedReader：是从与其它线程共用的管道中读取数据\n\nBufferedReader ：加缓冲功能，避免频繁读写硬盘\n\nInputStreamReader： 是一个连接字节流和字符流的桥梁，它将字节流转变为字符流。\n\n4）字符输出流Writer：\n\nStringWriter:向String 中写入数据。\n\nCharArrayWriter：实现一个可用作字符输入流的字符缓冲区\n\nPipedWriter:是向与其它线程共用的管道中写入数据\n\nBufferedWriter ： 增加缓冲功能，避免频繁读写硬盘。\n\nPrintWriter 和PrintStream 将对象的格式表示打印到文本输出流。 极其类似，功能和使用也非常相似\n\nOutputStreamWriter： 是OutputStream 到Writer 转换的桥梁，它的子类FileWriter 其实就是一个实现此功能的具体类（具体可以研究一SourceCode）。功能和使用和OutputStream 极其类似，后面会有它们的对应图。\n\n字符流的输入和输出类结构图：\n![952c1fdeadfaeb2ed13a785208e0aea2](NIO概览.resources/CA9A534F-8DEF-448B-A946-3ADE41538F9D.png)\n\n#### **分类二：按操作对象**\n![2539ba1fc433a54b14cebfc79019c2ba](NIO概览.resources/8F7AD527-634A-4D4E-B31B-6E1FB35BB4EC.jpg)\n**分类说明：**\n**对文件进行操作（节点流）：**\n\n* FileInputStream（字节输入流）\n* FileOutputStream（字节输出流）\n* FileReader（字符输入流）\n* FileWriter（字符输出流）\n\n**对管道进行操作（节点流）：**\n\n* PipedInputStream（字节输入流）\n* PipedOutStream（字节输出流）\n* PipedReader（字符输入流）\n* PipedWriter（字符输出流）\n* PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用，共同完成管道的读取写入操作。主要用于线程操作。\n\n**字节/字符数组流（节点流）：**\n\n* ByteArrayInputStream\n* ByteArrayOutputStream\n* CharArrayReader\n* CharArrayWriter\n\n除了上述三种是节点流，其他都是处理流，需要跟节点流配合使用。\n\n**Buffered缓冲流（处理流）：**\n带缓冲区的处理流，缓冲区的作用的主要目的是：避免每次和硬盘打交道，提高数据访问的效率。\n\n* BufferedInputStream\n* BufferedOutputStream\n* BufferedReader\n* BufferedWriter\n\n**转化流（处理流）：**\n\n* InputStreamReader：把字节转化成字符；\n* OutputStreamWriter：把字节转化成字符。\n\n**基本类型数据流（处理流）：用于操作基本数据类型值。**\n因为平时若是我们输出一个8个字节的long类型或4个字节的float类型，那怎么办呢？可以一个字节一个字节输出，也可以把转换成字符串输出，但是这样转换费时间，若是直接输出该多好啊，因此这个数据流就解决了我们输出数据类型的困难。数据流可以直接输出float类型或long类型，提高了数据读写的效率。\n\n* DataInputStream\n* DataOutputStream\n\n**打印流（处理流）：**\n\n一般是打印到控制台，可以进行控制打印的地方。\n\n* PrintStream\n* PrintWriter\n\n**对象流（处理流）：**\n\n把封装的对象直接输出，而不是一个个在转换成字符串再输出。\n\n* ObjectInputStream，对象反序列化\n* ObjectOutputStream，对象序列化\n\n合并流（处理流）：\n* SequenceInputStream：可以认为是一个工具类，将两个或者多个输入流当成一个输入流依次读取\n\n#### 其他类：File\nFile类是对文件系统中文件以及文件夹进行封装的对象，可以通过对象的思想来操作文件和文件夹。 File类保存文件或目录的各种元数据信息，包括文件名、文件长度、最后修改时间、是否可读、获取当前文件的路径名，判断指定文件是否存在、获得当前目录中的文件列表，创建、删除文件和目录等方法。\n\n#### 其他类：RandomAccessFile\n该对象并不是流体系中的一员，其封装了字节流，同时还封装了一个缓冲区（字符数组），通过内部的指针来操作字符数组中的数据。 该对象特点：\n该对象只能操作文件，所以构造函数接收两种类型的参数：a.字符串文件路径；b.File对象。\n该对象既可以对文件进行读操作，也能进行写操作，在进行对象实例化时可指定操作模式(r,rw)。\n注意:IO中的很多内容都可以使用NIO完成，这些知识点大家知道就好，使用的话还是尽量使用NIO/AIO。\n\n\n参考文章：\n《Netty官网》\n>https://www.jianshu.com/nb/18340870"
  },
  {
    "path": "Netty/Netty源码解析-概述篇.md",
    "content": "本文是由code4craft发表在博客上的，原文基于Netty3.7的版本，源码部分对buffer、Pipeline、Reactor模式等进行了部分讲解，个人又继续新增了后续的几个核心组件的源码解读，新增了具体的案例。\nNetty的源码非常好，质量极高，是Java中质量最高的开源项目之一，(比Spring系列源码高几层楼...)\n我十分建议大家花上一周时间自习读一读。\n\n## 概述\n\n### Netty是什么\n\n大概用Netty的，无论新手还是老手，都知道它是一个“网络通讯框架”。所谓框架，基本上都是一个作用：基于底层API，提供更便捷的编程模型。那么\"通讯框架\"到底做了什么事情呢？回答这个问题并不太容易，我们不妨反过来看看，不使用netty，直接基于NIO编写网络程序，你需要做什么(以Server端TCP连接为例，这里我们使用Reactor模型)：\n\n\n1. 监听端口，建立Socket连接\n2. 建立线程，处理内容\n\t1. 读取Socket内容，并对协议进行解析\n\t2. 进行逻辑处理\n\t3. 回写响应内容\n\t4. 如果是多次交互的应用(SMTP、FTP)，则需要保持连接多进行几次交互\n3. 关闭连接\n\n建立线程是一个比较耗时的操作，同时维护线程本身也有一些开销，所以我们会需要多线程机制，幸好JDK已经有很方便的多线程框架了，这里我们不需要花很多心思。\n\t\n此外，因为TCP连接的特性，我们还要使用连接池来进行管理：\n\n1. 建立TCP连接是比较耗时的操作，对于频繁的通讯，保持连接效果更好\n2. 对于并发请求，可能需要建立多个连接\n3. 维护多个连接后，每次通讯，需要选择某一可用连接\n4. 连接超时和关闭机制\n\n想想就觉得很复杂了！实际上，基于NIO直接实现这部分东西，即使是老手也容易出现错误，而使用Netty之后，你只需要关注逻辑处理部分就可以了。\n\n\n### 体验Netty\n\n这里我们引用Netty的example包里的一个例子，一个简单的EchoServer，它接受客户端输入，并将输入原样返回。其主要代码如下：\n\n```java\n    public void run() {\n        // Configure the server.\n        ServerBootstrap bootstrap = new ServerBootstrap(\n                new NioServerSocketChannelFactory(\n                        Executors.newCachedThreadPool(),\n                        Executors.newCachedThreadPool()));\n\n        // Set up the pipeline factory.\n        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {\n            public ChannelPipeline getPipeline() throws Exception {\n                return Channels.pipeline(new EchoServerHandler());\n            }\n        });\n\n        // Bind and start to accept incoming connections.\n        bootstrap.bind(new InetSocketAddress(port));\n    }\n```\n\n这里`EchoServerHandler`是其业务逻辑的实现者，大致代码如下：\n\n```java\n\tpublic class EchoServerHandler extends SimpleChannelUpstreamHandler {\n\n\t    @Override\n\t    public void messageReceived(\n\t            ChannelHandlerContext ctx, MessageEvent e) {\n\t        // Send back the received message to the remote peer.\n\t        e.getChannel().write(e.getMessage());\n\t    }\n\t}\n```\n\t\n还是挺简单的，不是吗？\n\n### Netty背后的事件驱动机制\n\n完成了以上一段代码，我们算是与Netty进行了第一次亲密接触。如果想深入学习呢？\n\n阅读源码是了解一个开源工具非常好的手段，但是Java世界的框架大多追求大而全，功能完备，如果逐个阅读，难免迷失方向，Netty也并不例外。相反，抓住几个重点对象，理解其领域概念及设计思想，从而理清其脉络，相当于打通了任督二脉，以后的阅读就不再困难了。\n\n理解Netty的关键点在哪呢？我觉得，除了NIO的相关知识，另一个就是事件驱动的设计思想。什么叫事件驱动？我们回头看看`EchoServerHandler`的代码，其中的参数：`public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)`，MessageEvent就是一个事件。这个事件携带了一些信息，例如这里`e.getMessage()`就是消息的内容，而`EchoServerHandler`则描述了处理这种事件的方式。一旦某个事件触发，相应的Handler则会被调用，并进行处理。这种事件机制在UI编程里广泛应用，而Netty则将其应用到了网络编程领域。\n\n在Netty里，所有事件都来自`ChannelEvent`接口，这些事件涵盖监听端口、建立连接、读写数据等网络通讯的各个阶段。而事件的处理者就是`ChannelHandler`，这样，不但是业务逻辑，连网络通讯流程中底层的处理，都可以通过实现`ChannelHandler`来完成了。事实上，Netty内部的连接处理、协议编解码、超时等机制，都是通过handler完成的。当博主弄明白其中的奥妙时，不得不佩服这种设计！\n\n下图描述了Netty进行事件处理的流程。`Channel`是连接的通道，是ChannelEvent的产生者，而`ChannelPipeline`可以理解为ChannelHandler的集合。\n\n![event driven in Netty][1]\n\n\n### 开启Netty源码之门\n\n理解了Netty的事件驱动机制，我们现在可以来研究Netty的各个模块了。Netty的包结构如下：\n\n\torg\n\t└── jboss\n\t    └── netty\n\t\t\t├── bootstrap 配置并启动服务的类\n\t\t\t├── buffer 缓冲相关类，对NIO Buffer做了一些封装\n\t\t\t├── channel 核心部分，处理连接\n\t\t\t├── container 连接其他容器的代码\n\t\t\t├── example 使用示例\n\t\t\t├── handler 基于handler的扩展部分，实现协议编解码等附加功能\n\t\t\t├── logging 日志\n\t\t\t└── util 工具类\n\n在这里面，`channel`和`handler`两部分比较复杂。我们不妨与Netty官方的结构图对照一下，来了解其功能。\n\n![components in Netty][2]\n\n具体的解释可以看这里：[http://netty.io/3.7/guide/#architecture](http://netty.io/3.7/guide/#architecture)。图中可以看到，除了之前说到的事件驱动机制之外，Netty的核心功能还包括两部分：\n\n* Zero-Copy-Capable Rich Byte Buffer\n\n\t零拷贝的Buffer。为什么叫零拷贝？因为在数据传输时，最终处理的数据会需要对单个传输层的报文，进行组合或者拆分。NIO原生的ByteBuffer无法做到这件事，而Netty通过提供Composite(组合)和Slice(切分)两种Buffer来实现零拷贝。这部分代码在`org.jboss.netty.buffer`包中。\n\t这里需要额外注意，不要和操作系统级别的Zero-Copy混淆了, 操作系统中的零拷贝主要是用户空间和内核空间之间的数据拷贝, NIO中通过DirectBuffer做了实现.\n\n* Universal Communication API\n\t\n\t统一的通讯API。这个是针对Java的Old I/O和New I/O，使用了不同的API而言。Netty则提供了统一的API(`org.jboss.netty.channel.Channel`)来封装这两种I/O模型。这部分代码在`org.jboss.netty.channel`包中。\n\t\n此外，Protocol Support功能通过handler机制实现。\n\n接下来的文章，我们会根据模块，详细的对Netty源码进行分析。\n\n\n### 参考资料：\n\n* Netty 3.7 User Guide [http://netty.io/3.7/guide/](http://netty.io/3.7/guide/)\n\n* What is Netty? [http://ayedo.github.io/netty/2013/06/19/what-is-netty.html](http://ayedo.github.io/netty/2013/06/19/what-is-netty.html)\n\n  [1]: http://static.oschina.net/uploads/space/2013/0921/174032_18rb_190591.png\n  [2]: http://static.oschina.net/uploads/space/2013/0921/225721_R0w2_190591.png\n"
  },
  {
    "path": "Netty/Netty源码解析1-Buffer.md",
    "content": "\n上一篇文章我们概要介绍了Netty的原理及结构，下面几篇文章我们开始对Netty的各个模块进行比较详细的分析。Netty的结构最底层是buffer机制，这部分也相对独立，我们就先从buffer讲起。\n\n## What：buffer简介\n\nbuffer中文名又叫缓冲区，按照维基百科的解释，是\"在数据传输时，在内存里开辟的一块临时保存数据的区域\"。它其实是一种化同步为异步的机制，可以解决数据传输的速率不对等以及不稳定的问题。\n\n根据这个定义，我们可以知道涉及I/O(特别是I/O写)的地方，基本会有Buffer了。就Java来说，我们非常熟悉的Old I/O--`InputStream`&`OutputStream`系列API，基本都是在内部使用到了buffer。Java课程老师就教过，必须调用`OutputStream.flush()`，才能保证数据写入生效！\n\n\n而NIO中则直接将buffer这个概念封装成了对象，其中最常用的大概是ByteBuffer了。于是使用方式变为了：将数据写入Buffer，flip()一下，然后将数据读出来。于是，buffer的概念更加深入人心了！\n\nNetty中的buffer也不例外。不同的是，Netty的buffer专为网络通讯而生，所以它又叫ChannelBuffer(好吧其实没有什么因果关系…)。我们下面就来讲讲Netty中得buffer。当然，关于Netty，我们必须讲讲它的所谓\"Zero-Copy-Capable\"机制。\n\n## TCP/IP协议与buffer\n\nTCP/IP协议是目前的主流网络协议。它是一个多层协议，最下层是物理层，最上层是应用层(HTTP协议等)，而做Java应用开发，一般只接触TCP以上，即传输层和应用层的内容。这也是Netty的主要应用场景。\n\nTCP报文有个比较大的特点，就是它传输的时候，会先把应用层的数据项拆开成字节，然后按照自己的传输需要，选择合适数量的字节进行传输。什么叫\"自己的传输需要\"？首先TCP包有最大长度限制，那么太大的数据项肯定是要拆开的。其次因为TCP以及下层协议会附加一些协议头信息，如果数据项太小，那么可能报文大部分都是没有价值的头信息，这样传输是很不划算的。因此有了收集一定数量的小数据，并打包传输的Nagle算法(这个东东在HTTP协议里会很讨厌，Netty里可以用setOption(\"tcpNoDelay\", true)关掉它)。\n\n这么说可能太学院派了一点，我们举个例子吧：\n\n发送时，我们这样分3次写入('|'表示两个buffer的分隔):\n\n\t   +-----+-----+-----+\n\t   | ABC | DEF | GHI |\n\t   +-----+-----+-----+\n\n接收时，可能变成了这样:\n\n\t   +----+-------+---+---+\n\t   | AB | CDEFG | H | I |\n\t   +----+-------+---+---+\n\n很好懂吧？可是，说了这么多，跟buffer有个什么关系呢？别急，我们来看下面一部分。\n\n## Buffer中的分层思想\n\n我们先回到之前的`messageReceived`方法：\n\n```java\n    public void messageReceived(\n            ChannelHandlerContext ctx, MessageEvent e) {\n        // Send back the received message to the remote peer.\n        transferredBytes.addAndGet(((ChannelBuffer) e.getMessage()).readableBytes());\n        e.getChannel().write(e.getMessage());\n    }\n```\n    \n这里`MessageEvent.getMessage()`默认的返回值是一个`ChannelBuffer`。我们知道，业务中需要的\"Message\"，其实是一条应用层级别的完整消息，而一般的buffer工作在传输层，与\"Message\"是不能对应上的。那么这个ChannelBuffer是什么呢？\n\n来一个官方给的图，我想这个答案就很明显了：\n\n![virtual buffer in Netty][3]\n\n这里可以看到，TCP层HTTP报文被分成了两个ChannelBuffer，这两个Buffer对我们上层的逻辑(HTTP处理)是没有意义的。但是两个ChannelBuffer被组合起来，就成为了一个有意义的HTTP报文，这个报文对应的ChannelBuffer，才是能称之为\"Message\"的东西。这里用到了一个词\"Virtual Buffer\"，也就是所谓的\"Zero-Copy-Capable Byte Buffer\"了。顿时觉得豁然开朗了有没有！\n\n我这里总结一下，**如果说NIO的Buffer和Netty的ChannelBuffer最大的区别的话，就是前者仅仅是传输上的Buffer，而后者其实是传输Buffer和抽象后的逻辑Buffer的结合。**延伸开来说，NIO仅仅是一个网络传输框架，而Netty是一个网络应用框架，包括网络以及应用的分层结构。\n\n当然，在Netty里，默认使用`ChannelBuffer`表示\"Message\"，不失为一个比较实用的方法，但是`MessageEvent.getMessage()`是可以存放一个POJO的，这样子抽象程度又高了一些，这个我们在以后讲到`ChannelPipeline`的时候会说到。\n\n## Netty中的ChannelBuffer及实现\n\n好了，终于来到了代码实现部分。之所以啰嗦了这么多，因为我觉得，关于\"Zero-Copy-Capable Rich Byte Buffer\"，理解为什么需要它，比理解它是怎么实现的，可能要更重要一点。\n\n我想可能很多朋友跟我一样，喜欢\"顺藤摸瓜\"式读代码--找到一个入口，然后顺着查看它的调用，直到理解清楚。很幸运，`ChannelBuffers`(注意有s!)就是这样一根\"藤\"，它是所有ChannelBuffer实现类的入口，它提供了很多静态的工具方法来创建不同的Buffer，靠“顺藤摸瓜”式读代码方式，大致能把各种ChannelBuffer的实现类摸个遍。先列一下ChannelBuffer相关类图。\n\n![channel buffer in Netty][1]\n\n此外还有`WrappedChannelBuffer`系列也是继承自`AbstractChannelBuffer`，图放到了后面。\n\n### ChannelBuffer中的readerIndex和writerIndex\n\n开始以为Netty的ChannelBuffer是对NIO ByteBuffer的一个封装，其实不是的，**它是把ByteBuffer重新实现了一遍**。\n\n以最常用的`HeapChannelBuffer`为例，其底层也是一个byte[]，与ByteBuffer不同的是，它是可以同时进行读和写的，而不需要使用flip()进行读写切换。ChannelBuffer读写的核心代码在`AbstactChannelBuffer`里，这里通过readerIndex和writerIndex两个整数，分别指向当前读的位置和当前写的位置，并且，readerIndex总是小于writerIndex的。贴两段代码，让大家能看的更明白一点：\n\n```java\n    public void writeByte(int value) {\n        setByte(writerIndex ++, value);\n    }\n\n    public byte readByte() {\n        if (readerIndex == writerIndex) {\n            throw new IndexOutOfBoundsException(\"Readable byte limit exceeded: \"\n                    + readerIndex);\n        }\n        return getByte(readerIndex ++);\n    }\n\n    public int writableBytes() {\n        return capacity() - writerIndex;\n    }\n    \n    public int readableBytes() {\n        return writerIndex - readerIndex;\n    }\n```\n\n我倒是觉得这样的方式非常自然，比单指针与flip()要更加好理解一些。AbstactChannelBuffer还有两个相应的mark指针`markedReaderIndex`和`markedWriterIndex`，跟NIO的原理是一样的，这里不再赘述了。\n\n### 字节序Endianness与HeapChannelBuffer\n\n在创建Buffer时，我们注意到了这样一个方法：`public static ChannelBuffer buffer(ByteOrder endianness, int capacity);`，其中`ByteOrder`是什么意思呢？\n\n这里有个很基础的概念：字节序(ByteOrder/Endianness)。它规定了多余一个字节的数字(int啊long什么的)，如何在内存中表示。BIG_ENDIAN(大端序)表示高位在前，整型数`12`会被存储为`0 0 0 12`四字节，而LITTLE_ENDIAN则正好相反。可能搞C/C++的程序员对这个会比较熟悉，而Javaer则比较陌生一点，因为Java已经把内存给管理好了。但是在网络编程方面，根据协议的不同，不同的字节序也可能会被用到。目前大部分协议还是采用大端序，可参考[RFC1700](http://tools.ietf.org/html/rfc1700)。\n\n了解了这些知识，我们也很容易就知道为什么会有`BigEndianHeapChannelBuffer`和`LittleEndianHeapChannelBuffer`了！\n\n### DynamicChannelBuffer\n\nDynamicChannelBuffer是一个很方便的Buffer，之所以叫Dynamic是因为它的长度会根据内容的长度来扩充，你可以像使用ArrayList一样，无须关心其容量。实现自动扩容的核心在于`ensureWritableBytes`方法，算法很简单：在写入前做容量检查，容量不够时，新建一个容量x2的buffer，跟ArrayList的扩容是相同的。贴一段代码吧(为了代码易懂，这里我删掉了一些边界检查，只保留主逻辑)：\n\n```java\n    public void writeByte(int value) {\n        ensureWritableBytes(1);\n        super.writeByte(value);\n    }\n\n    public void ensureWritableBytes(int minWritableBytes) {\n        if (minWritableBytes <= writableBytes()) {\n            return;\n        }\n\n        int newCapacity = capacity();\n        int minNewCapacity = writerIndex() + minWritableBytes;\n        while (newCapacity < minNewCapacity) {\n            newCapacity <<= 1;\n        }\n\n        ChannelBuffer newBuffer = factory().getBuffer(order(), newCapacity);\n        newBuffer.writeBytes(buffer, 0, writerIndex());\n        buffer = newBuffer;\n    }\n```\n\n### CompositeChannelBuffer\n\n`CompositeChannelBuffer`是由多个ChannelBuffer组合而成的，可以看做一个整体进行读写。这里有一个技巧：CompositeChannelBuffer并不会开辟新的内存并直接复制所有ChannelBuffer内容，而是直接保存了所有ChannelBuffer的引用，并在子ChannelBuffer里进行读写，从而实现了\"Zero-Copy-Capable\"了。来段简略版的代码吧：\n\n```java\n\tpublic class CompositeChannelBuffer{\n\n\t    //components保存所有内部ChannelBuffer\n\t    private ChannelBuffer[] components;\n\t    //indices记录在整个CompositeChannelBuffer中，每个components的起始位置\n\t    private int[] indices;\n\t    //缓存上一次读写的componentId\n\t    private int lastAccessedComponentId;\n\n\t    public byte getByte(int index) {\n\t        //通过indices中记录的位置索引到对应第几个子Buffer\n\t        int componentId = componentId(index);\n\t        return components[componentId].getByte(index - indices[componentId]);\n\t    }\n\n\t    public void setByte(int index, int value) {\n\t        int componentId = componentId(index);\n\t        components[componentId].setByte(index - indices[componentId], value);\n\t    }\n\n\t}\t\t\n```\n\n查找componentId的算法再次不作介绍了，大家自己实现起来也不会太难。值得一提的是，基于ChannelBuffer连续读写的特性，使用了顺序查找(而不是二分查找)，并且用`lastAccessedComponentId`来进行缓存。\n\n### ByteBufferBackedChannelBuffer\n\n前面说ChannelBuffer是自己的实现的，其实只说对了一半。`ByteBufferBackedChannelBuffer`就是封装了NIO ByteBuffer的类，用于实现堆外内存的Buffer(使用NIO的`DirectByteBuffer`)。当然，其实它也可以放其他的ByteBuffer的实现类。代码实现就不说了，也没啥可说的。\n\n### WrappedChannelBuffer\n\n![virtual buffer in Netty][2]\n\n`WrappedChannelBuffer`都是几个对已有ChannelBuffer进行包装，完成特定功能的类。代码不贴了，实现都比较简单，列一下功能吧。\n\n![d205e7c6ea983ad4080661d14b44efc9](Netty源码解析1-Buffer.resources/DCE71693-EAB2-4A70-9F4B-879F154FE421.png)\n\n\n可以看到，关于实现方面，Netty 3.7的buffer相关内容还是比较简单的，也没有太多费脑细胞的地方。\n\n而Netty 4.0之后就不同了。4.0，ChannelBuffer改名ByteBuf，成了单独项目buffer，并且为了性能优化，加入了BufferPool之类的机制，已经变得比较复杂了(本质倒没怎么变)。性能优化是个很复杂的事情，研究源码时，建议先避开这些东西，除非你对算法情有独钟。举个例子，Netty4.0里为了优化，将Map换成了Java 8里6000行的[ConcurrentHashMapV8](https://github.com/netty/netty/blob/master/common/src/main/java/io/netty/util/internal/chmv8/ConcurrentHashMapV8.java)，你们感受一下…\n\n  [1]: http://static.oschina.net/uploads/space/2013/0925/081551_v8pK_190591.png\n  [2]: http://static.oschina.net/uploads/space/2013/0925/074748_oSkl_190591.png\n  [3]: http://static.oschina.net/uploads/space/2013/0925/225747_kDAk_190591.png\n  \n\n参考资料：\n\n* TCP/IP协议 [http://zh.wikipedia.org/zh-cn/TCP/IP%E5%8D%8F%E8%AE%AE](http://zh.wikipedia.org/zh-cn/TCP/IP%E5%8D%8F%E8%AE%AE)\n* Data_buffer [http://en.wikipedia.org/wiki/Data_buffer](http://en.wikipedia.org/wiki/Data_buffer)\n* Endianness [http://en.wikipedia.org/wiki/Endianness](http://en.wikipedia.org/wiki/Endianness)\n"
  },
  {
    "path": "Netty/Netty源码解析2-Reactor.md",
    "content": "\n## 一：Netty、NIO、多线程？\n\n理清NIO与Netty的关系之前，我们必须先要来看看Reactor模式。Netty是一个典型的多线程的Reactor模式的使用，理解了这部分，在宏观上理解Netty的NIO及多线程部分就不会有什么困难了。\n\n## 二：Reactor\n\n### 1、Reactor的由来\n\nReactor是一种广泛应用在服务器端开发的设计模式。Reactor中文大多译为“反应堆”，我当初接触这个概念的时候，就感觉很厉害，是不是它的原理就跟“核反应”差不多？后来才知道其实没有什么关系，从Reactor的兄弟“Proactor”（多译为前摄器）就能看得出来，这两个词的中文翻译其实都不是太好，不够形象。实际上，Reactor模式又有别名“Dispatcher”或者“Notifier”，我觉得这两个都更加能表明它的本质。\n\n\n那么，Reactor模式究竟是个什么东西呢？这要从事件驱动的开发方式说起。我们知道，对于应用服务器，一个主要规律就是，CPU的处理速度是要远远快于IO速度的，如果CPU为了IO操作（例如从Socket读取一段数据）而阻塞显然是不划算的。好一点的方法是分为多进程或者线程去进行处理，但是这样会带来一些进程切换的开销，试想一个进程一个数据读了500ms，期间进程切换到它3次，但是CPU却什么都不能干，就这么切换走了，是不是也不划算？\n\n这时先驱们找到了事件驱动，或者叫回调的方式，来完成这件事情。这种方式就是，应用业务向一个中间人注册一个回调（event handler），当IO就绪后，就这个中间人产生一个事件，并通知此handler进行处理。*这种回调的方式，也体现了“好莱坞原则”（Hollywood principle）-“Don't call us, we'll call you”，在我们熟悉的IoC中也有用到。看来软件开发真是互通的！*\n\n好了，我们现在来看Reactor模式。在前面事件驱动的例子里有个问题：我们如何知道IO就绪这个事件，谁来充当这个中间人？Reactor模式的答案是：由一个不断等待和循环的单独进程（线程）来做这件事，它接受所有handler的注册，并负责先操作系统查询IO是否就绪，在就绪后就调用指定handler进行处理，这个角色的名字就叫做Reactor。\n\n### 2、Reactor与NIO\n\nJava中的NIO可以很好的和Reactor模式结合。关于NIO中的Reactor模式，我想没有什么资料能比Doug Lea大神（不知道Doug Lea？看看JDK集合包和并发包的作者吧）在[《Scalable IO in Java》](http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf)解释的更简洁和全面了。NIO中Reactor的核心是`Selector`，我写了一个简单的Reactor示例，这里我贴一个核心的Reactor的循环（这种循环结构又叫做`EventLoop`），剩余代码在[learning-src](learning-src/src/main/java/us/codecraft/netty/reactor)目录下。\n\n```java\n\tpublic void run() {\n\t\ttry {\n\t\t\twhile (!Thread.interrupted()) {\n\t\t\t\tselector.select();\n\t\t\t\tSet selected = selector.selectedKeys();\n\t\t\t\tIterator it = selected.iterator();\n\t\t\t\twhile (it.hasNext())\n\t\t\t\t\tdispatch((SelectionKey) (it.next()));\n\t\t\t\tselected.clear();\n\t\t\t}\n\t\t} catch (IOException ex) { /* ... */\n\t\t}\n\t}\n```\n\n### 3、与Reactor相关的其他概念\n\n前面提到了Proactor模式，这又是什么呢？简单来说，Reactor模式里，操作系统只负责通知IO就绪，具体的IO操作（例如读写）仍然是要在业务进程里阻塞的去做的，而Proactor模式则更进一步，由操作系统将IO操作执行好（例如读取，会将数据直接读到内存buffer中），而handler只负责处理自己的逻辑，真正做到了IO与程序处理异步执行。所以我们一般又说Reactor是同步IO，Proactor是异步IO。\n\n关于阻塞和非阻塞、异步和非异步，以及UNIX底层的机制，大家可以看看这篇文章[IO - 同步，异步，阻塞，非阻塞 （亡羊补牢篇）](http://blog.csdn.net/historyasamirror/article/details/5778378)，以及陶辉（《深入理解nginx》的作者）[《高性能网络编程》](http://blog.csdn.net/russell_tao/article/details/17452997)的系列。\n\n## 三：由Reactor出发来理解Netty\n\n### 1、多线程下的Reactor\n\n讲了一堆Reactor，我们回到Netty。在《Scalable IO in Java》中讲到了一种多线程下的Reactor模式。在这个模式里，mainReactor只有一个，负责响应client的连接请求，并建立连接，它使用一个NIO Selector；subReactor可以有一个或者多个，每个subReactor都会在一个独立线程中执行，并且维护一个独立的NIO Selector。\n\n这样的好处很明显，因为subReactor也会执行一些比较耗时的IO操作，例如消息的读写，使用多个线程去执行，则更加有利于发挥CPU的运算能力，减少IO等待时间。\n\n![Multiple Reactors][2]\n\n### 2、Netty中的Reactor与NIO\n\n好了，了解了多线程下的Reactor模式，我们来看看Netty吧（以下部分主要针对NIO，OIO部分更加简单一点，不重复介绍了）。Netty里对应mainReactor的角色叫做“Boss”，而对应subReactor的角色叫做\"Worker\"。Boss负责分配请求，Worker负责执行，好像也很贴切！以TCP的Server端为例，这两个对应的实现类分别为`NioServerBoss`和`NioWorker`（Server和Client的Worker没有区别，因为建立连接之后，双方就是对等的进行传输了）。\n\nNetty 3.7中Reactor的EventLoop在`AbstractNioSelector.run()`中，它实现了`Runnable`接口。这个类是Netty NIO部分的核心。它的逻辑非常复杂，其中还包括一些对JDK Bug的处理（例如`rebuildSelector`），刚开始读的时候不需要深入那么细节。我精简了大部分代码，保留主干如下：\n\n```java\nabstract class AbstractNioSelector implements NioSelector {\n\n    \n    //NIO Selector\n    protected volatile Selector selector;\n\n    //内部任务队列\n    private final Queue<Runnable> taskQueue = new ConcurrentLinkedQueue<Runnable>();\n\n    //selector循环\n    public void run() {\n        for (;;) {\n            try {\n                //处理内部任务队列\n                processTaskQueue();\n                //处理selector事件对应逻辑\n                process(selector);\n            } catch (Throwable t) {\n                try {\n                    Thread.sleep(1000);\n                } catch (InterruptedException e) {\n                    // Ignore.\n                }\n            }\n        }\n    }\n\n    private void processTaskQueue() {\n        for (;;) {\n            final Runnable task = taskQueue.poll();\n            if (task == null) {\n                break;\n            }\n            task.run();\n        }\n    }\n\n    protected abstract void process(Selector selector) throws IOException;\n\n}\n```\n\n其中process是主要的处理事件的逻辑，例如在`AbstractNioWorker`中，处理逻辑如下：\n\n```java\n    protected void process(Selector selector) throws IOException {\n        Set<SelectionKey> selectedKeys = selector.selectedKeys();\n        if (selectedKeys.isEmpty()) {\n            return;\n        }\n        for (Iterator<SelectionKey> i = selectedKeys.iterator(); i.hasNext();) {\n            SelectionKey k = i.next();\n            i.remove();\n            try {\n                int readyOps = k.readyOps();\n                if ((readyOps & SelectionKey.OP_READ) != 0 || readyOps == 0) {\n                    if (!read(k)) {\n                        // Connection already closed - no need to handle write.\n                        continue;\n                    }\n                }\n                if ((readyOps & SelectionKey.OP_WRITE) != 0) {\n                    writeFromSelectorLoop(k);\n                }\n            } catch (CancelledKeyException e) {\n                close(k);\n            }\n\n            if (cleanUpCancelledKeys()) {\n                break; // break the loop to avoid ConcurrentModificationException\n            }\n        }\n    }\n```\n\n这不就是第二部分提到的selector经典用法了么？\n\n在Netty 4.0之后，作者觉得`NioSelector`这个叫法，以及区分`NioBoss`和`NioWorker`的做法稍微繁琐了点，干脆就将这些合并成了`NioEventLoop`，从此这两个角色就不做区分了。我倒是觉得新版本的会更优雅一点。\n\n### 3、Netty中的多线程\n\n下面我们来看Netty的多线程部分。一旦对应的Boss或者Worker启动，就会分配给它们一个线程去一直执行。对应的概念为`BossPool`和`WorkerPool`。对于每个`NioServerSocketChannel`，Boss的Reactor有一个线程，而Worker的线程数由Worker线程池大小决定，但是默认最大不会超过CPU核数*2，当然，这个参数可以通过`NioServerSocketChannelFactory`构造函数的参数来设置。\n\n```java\n    public NioServerSocketChannelFactory(\n            Executor bossExecutor, Executor workerExecutor,\n            int workerCount) {\n        this(bossExecutor, 1, workerExecutor, workerCount);\n    }\n```\n\n最后我们比较关心一个问题，我们之前`ChannlePipeline`中的ChannleHandler是在哪个线程执行的呢？答案是在Worker线程里执行的，并且会阻塞Worker的EventLoop。例如，在`NioWorker`中，读取消息完毕之后，会触发`MessageReceived`事件，这会使得Pipeline中的handler都得到执行。\n\n```java\n    protected boolean read(SelectionKey k) {\n        ....\n\n        if (readBytes > 0) {\n            // Fire the event.\n            fireMessageReceived(channel, buffer);\n        }\n\n        return true;\n    }\n```\n\n可以看到，对于处理事件较长的业务，并不太适合直接放到ChannelHandler中执行。那么怎么处理呢？我们在Handler部分会进行介绍。\n\n\n参考资料：\n\n* Scalable IO in Java [http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf](http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf)\n* Netty5.0架构剖析和源码解读 [http://vdisk.weibo.com/s/C9LV9iVqH13rW/1391437855](http://vdisk.weibo.com/s/C9LV9iVqH13rW/1391437855)\n* Reactor pattern [http://en.wikipedia.org/wiki/Reactor_pattern](http://en.wikipedia.org/wiki/Reactor_pattern)\n* Reactor - An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events [http://www.cs.wustl.edu/~schmidt/PDF/reactor-siemens.pdf](http://www.cs.wustl.edu/~schmidt/PDF/reactor-siemens.pdf)\n* 高性能网络编程6--reactor反应堆与定时器管理 [http://blog.csdn.net/russell_tao/article/details/17452997](http://blog.csdn.net/russell_tao/article/details/17452997)\n* IO - 同步，异步，阻塞，非阻塞 （亡羊补牢篇）[http://blog.csdn.net/historyasamirror/article/details/5778378](http://blog.csdn.net/historyasamirror/article/details/5778378)\n\n题图来自：[http://www.worldindustrialreporter.com/france-gives-green-light-to-tokamak-fusion-reactor/](http://www.worldindustrialreporter.com/france-gives-green-light-to-tokamak-fusion-reactor/)\n\n  [1]: http://static.oschina.net/uploads/space/2014/0208/164000_EQQb_190591.jpg\n  [2]: http://static.oschina.net/uploads/space/2013/1125/130828_uKWD_190591.jpeg"
  },
  {
    "path": "Netty/Netty源码解析3-Pipeline.md",
    "content": "\n## Channel实现概览\n\n在Netty里，`Channel`是通讯的载体，而`ChannelHandler`负责Channel中的逻辑处理。\n\n那么`ChannelPipeline`是什么呢？我觉得可以理解为ChannelHandler的容器：一个Channel包含一个ChannelPipeline，所有ChannelHandler都会注册到ChannelPipeline中，并按顺序组织起来。\n\n在Netty中，`ChannelEvent`是数据或者状态的载体，例如传输的数据对应`MessageEvent`，状态的改变对应`ChannelStateEvent`。当对Channel进行操作时，会产生一个ChannelEvent，并发送到`ChannelPipeline`。ChannelPipeline会选择一个ChannelHandler进行处理。这个ChannelHandler处理之后，可能会产生新的ChannelEvent，并流转到下一个ChannelHandler。\n\n![channel pipeline][1]\n\n\n例如，一个数据最开始是一个`MessageEvent`，它附带了一个未解码的原始二进制消息`ChannelBuffer`，然后某个Handler将其解码成了一个数据对象，并生成了一个新的`MessageEvent`，并传递给下一步进行处理。\n\n到了这里，可以看到，其实Channel的核心流程位于`ChannelPipeline`中。于是我们进入ChannelPipeline的深层梦境里，来看看它具体的实现。\n\n## ChannelPipeline的主流程\n\nNetty的ChannelPipeline包含两条线路：Upstream和Downstream。Upstream对应上行，接收到的消息、被动的状态改变，都属于Upstream。Downstream则对应下行，发送的消息、主动的状态改变，都属于Downstream。`ChannelPipeline`接口包含了两个重要的方法:`sendUpstream(ChannelEvent e)`和`sendDownstream(ChannelEvent e)`，就分别对应了Upstream和Downstream。\n\n对应的，ChannelPipeline里包含的ChannelHandler也包含两类：`ChannelUpstreamHandler`和`ChannelDownstreamHandler`。每条线路的Handler是互相独立的。它们都很简单的只包含一个方法：`ChannelUpstreamHandler.handleUpstream`和`ChannelDownstreamHandler.handleDownstream`。\n\nNetty官方的javadoc里有一张图(`ChannelPipeline`接口里)，非常形象的说明了这个机制(我对原图进行了一点修改，加上了`ChannelSink`，因为我觉得这部分对理解代码流程会有些帮助)：\n\n![channel pipeline][2]\n\n什么叫`ChannelSink`呢？ChannelSink包含一个重要方法`ChannelSink.eventSunk`，可以接受任意ChannelEvent。\"sink\"的意思是\"下沉\"，那么\"ChannelSink\"好像可以理解为\"Channel下沉的地方\"？实际上，它的作用确实是这样，也可以换个说法：\"处于末尾的万能Handler\"。最初读到这里，也有些困惑，这么理解之后，就感觉简单许多。**只有Downstream包含`ChannelSink`**，这里会做一些建立连接、绑定端口等重要操作。为什么UploadStream没有ChannelSink呢？我只能认为，一方面，不符合\"sink\"的意义，另一方面，也没有什么处理好做的吧！\n\n这里有个值得注意的地方：在一条“流”里，一个`ChannelEvent`并不会主动的\"流\"经所有的Handler，而是由**上一个Handler显式的调用`ChannelPipeline.sendUp(Down)stream`产生，并交给下一个Handler处理**。也就是说，每个Handler接收到一个ChannelEvent，并处理结束后，如果需要继续处理，那么它需要调用`sendUp(Down)stream`新发起一个事件。如果它不再发起事件，那么处理就到此结束，即使它后面仍然有Handler没有执行。这个机制可以保证最大的灵活性，当然对Handler的先后顺序也有了更严格的要求。\n\n顺便说一句，在Netty 3.x里，这个机制会导致大量的ChannelEvent对象创建，因此Netty 4.x版本对此进行了改进。twitter的[finagle](https://github.com/twitter/finagle)框架实践中，就提到从Netty 3.x升级到Netty 4.x，可以大大降低GC开销。有兴趣的可以看看这篇文章：[https://blog.twitter.com/2013/netty-4-at-twitter-reduced-gc-overhead](https://blog.twitter.com/2013/netty-4-at-twitter-reduced-gc-overhead)\n\n下面我们从代码层面来对这里面发生的事情进行深入分析，这部分涉及到一些细节，需要打开项目源码，对照来看，会比较有收获。\n\n## 深入ChannelPipeline内部\n\n### DefaultChannelPipeline的内部结构\n\n`ChannelPipeline`的主要的实现代码在`DefaultChannelPipeline`类里。列一下DefaultChannelPipeline的主要字段：\n\n```java\n    public class DefaultChannelPipeline implements ChannelPipeline {\n    \n        private volatile Channel channel;\n        private volatile ChannelSink sink;\n        private volatile DefaultChannelHandlerContext head;\n        private volatile DefaultChannelHandlerContext tail;\n        private final Map<String, DefaultChannelHandlerContext> name2ctx =\n            new HashMap<String, DefaultChannelHandlerContext>(4);\n    }\n```\n\n这里需要介绍一下`ChannelHandlerContext`这个接口。顾名思义，ChannelHandlerContext保存了Netty与Handler相关的上下文信息。而咱们这里的`DefaultChannelHandlerContext`，则是对`ChannelHandler`的一个包装。一个`DefaultChannelHandlerContext`内部，除了包含一个`ChannelHandler`，还保存了\"next\"和\"prev\"两个指针，从而形成一个双向链表。\n\n因此，在`DefaultChannelPipeline`中，我们看到的是对`DefaultChannelHandlerContext`的引用，而不是对`ChannelHandler`的直接引用。这里包含\"head\"和\"tail\"两个引用，分别指向链表的头和尾。而name2ctx则是一个按名字索引DefaultChannelHandlerContext用户的一个map，主要在按照名称删除或者添加ChannelHandler时使用。\n\n### sendUpstream和sendDownstream\n\n前面提到了，`ChannelPipeline`接口的两个重要的方法：`sendUpstream(ChannelEvent e)`和`sendDownstream(ChannelEvent e)`。**所有事件**的发起都是基于这两个方法进行的。`Channels`类有一系列`fireChannelBound`之类的`fireXXXX`方法，其实都是对这两个方法的facade包装。\n\n下面来看一下这两个方法的实现。先看sendUpstream(对代码做了一些简化，保留主逻辑)：\n\n```java\n    public void sendUpstream(ChannelEvent e) {\n        DefaultChannelHandlerContext head = getActualUpstreamContext(this.head);\n        head.getHandler().handleUpstream(head, e);\n    }\n    \n    private DefaultChannelHandlerContext getActualUpstreamContext(DefaultChannelHandlerContext ctx) {\n        DefaultChannelHandlerContext realCtx = ctx;\n        while (!realCtx.canHandleUpstream()) {\n            realCtx = realCtx.next;\n            if (realCtx == null) {\n                return null;\n            }\n        }\n        return realCtx;\n    }\n```\n\n这里最终调用了`ChannelUpstreamHandler.handleUpstream`来处理这个ChannelEvent。有意思的是，这里我们看不到任何\"将Handler向后移一位\"的操作，但是我们总不能每次都用同一个Handler来进行处理啊？实际上，我们更为常用的是`ChannelHandlerContext.handleUpstream`方法(实现是`DefaultChannelHandlerContext.sendUpstream`方法)：\n\n```java\n\tpublic void sendUpstream(ChannelEvent e) {\n\t\tDefaultChannelHandlerContext next = getActualUpstreamContext(this.next);\n\t\tDefaultChannelPipeline.this.sendUpstream(next, e);\n\t}\n```\n\n可以看到，这里最终仍然调用了`ChannelPipeline.sendUpstream`方法，但是**它会将Handler指针后移**。\n\n我们接下来看看`DefaultChannelHandlerContext.sendDownstream`:\n\n```java\n\tpublic void sendDownstream(ChannelEvent e) {\n\t\tDefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev);\n\t\tif (prev == null) {\n\t\t\ttry {\n\t\t\t\tgetSink().eventSunk(DefaultChannelPipeline.this, e);\n\t\t\t} catch (Throwable t) {\n\t\t\t\tnotifyHandlerException(e, t);\n\t\t\t}\n\t\t} else {\n\t\t\tDefaultChannelPipeline.this.sendDownstream(prev, e);\n\t\t}\n\t}\n```\n\n与sendUpstream好像不大相同哦？这里有两点：一是到达末尾时，就如梦境二所说，会调用ChannelSink进行处理；二是这里指针是**往前移**的，所以我们知道了：\n\n**UpstreamHandler是从前往后执行的，DownstreamHandler是从后往前执行的。**在ChannelPipeline里添加时需要注意顺序了！\n\nDefaultChannelPipeline里还有些机制，像添加/删除/替换Handler，以及`ChannelPipelineFactory`等，比较好理解，就不细说了。\n\n## 回到现实：Pipeline解决的问题\n\n好了，深入分析完代码，有点头晕了，我们回到最开始的地方，来想一想，Netty的Pipeline机制解决了什么问题？\n\n我认为至少有两点：\n\n一是提供了ChannelHandler的编程模型，基于ChannelHandler开发业务逻辑，基本不需要关心网络通讯方面的事情，专注于编码/解码/逻辑处理就可以了。Handler也是比较方便的开发模式，在很多框架中都有用到。\n\n二是实现了所谓的\"Universal Asynchronous API\"。这也是Netty官方标榜的一个功能。用过OIO和NIO的都知道，这两套API风格相差极大，要从一个迁移到另一个成本是很大的。即使是NIO，异步和同步编程差距也很大。而Netty屏蔽了OIO和NIO的API差异，通过Channel提供对外接口，并通过ChannelPipeline将其连接起来，因此替换起来非常简单。\n\n![universal API][3]\n\n理清了ChannelPipeline的主流程，我们对Channel部分的大致结构算是弄清楚了。可是到了这里，我们依然对一个连接具体怎么处理没有什么概念，下篇文章，我们会分析一下，在Netty中，捷径如何处理连接的建立、数据的传输这些事情。\n\n\n  [1]: http://static.oschina.net/uploads/space/2013/0921/174032_18rb_190591.png\n  [2]: http://static.oschina.net/uploads/space/2013/1109/075339_Kjw6_190591.png\n  [3]: http://static.oschina.net/uploads/space/2013/1124/001528_TBb5_190591.jpg\n\n参考资料：\n\n* Sink [http://en.wikipedia.org/wiki/Sink_\\(computing\\)](http://en.wikipedia.org/wiki/Sink_\\(computing\\))"
  },
  {
    "path": "Netty/Netty源码解析4-Handler综述.md",
    "content": "## Netty中的Handler简介\n`Handler`在Netty中，占据着非常重要的地位。`Handler`与Servlet中的filter很像，通过Handler可以完成通讯报文的解码编码、拦截指定的报文、\n\n统一对日志错误进行处理、统一对请求进行计数、控制Handler执行与否。一句话，没有它做不到的只有你想不到的\n\n　　Netty中的所有handler都实现自ChannelHandler接口。按照输入输出来分，分为`ChannelInboundHandler`、`ChannelOutboundHandler`两大类\n\n`ChannelInboundHandler`对从客户端发往服务器的报文进行处理，一般用来执行解码、读取客户端数据、进行业务处理等；`ChannelOutboundHandler`\n\n对从服务器发往客户端的报文进行处理，一般用来进行编码、发送报文到客户端\n\n\n　　Netty中可以注册多个handler。`ChannelInboundHandler`按照注册的先后顺序执行；`ChannelOutboundHandler`按照注册的先后顺序逆序执行。\n  \n  \nChannelPipeline中的事件不会自动流动，而我们一般需求事件自动流动，Netty提供了两个Adapter：ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter来满足这种需求。其中的实现类似如下：\n\n```\n    // inboud事件默认处理过程\n    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {\n        ctx.fireChannelRegistered();    // 事件传播到下一个Handler\n    }\n    \n    // outboud事件默认处理过程\n    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,\n            ChannelPromise promise) throws Exception {\n        ctx.bind(localAddress, promise);  // 事件传播到下一个Handler\n    }\n   \n```\n\n在Adapter中，事件默认自动传播到下一个Handler，这样带来的另一个好处是：用户的Handler类可以继承Adapter且覆盖自己感兴趣的事件实现，其他事件使用默认实现，不用再实现ChannelIn/outboudHandler接口中所有方法，提高效率。\n我们常常遇到这样的需求：在一个业务逻辑处理器中，需要写数据库、进行网络连接等耗时业务。Netty的原则是不阻塞I/O线程，所以需指定Handler执行的线程池，可使用如下代码：\n\n```\n static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);\n    ...\n    ChannelPipeline pipeline = ch.pipeline();\n    // 简单非阻塞业务，可以使用I/O线程执行\n    pipeline.addLast(\"decoder\", new MyProtocolDecoder());\n    pipeline.addLast(\"encoder\", new MyProtocolEncoder());\n    // 复杂耗时业务，使用新的线程池\n    pipeline.addLast(group, \"handler\", new MyBusinessLogicHandler());\n```\nChannelHandler中有一个Sharable注解，使用该注解后多个ChannelPipeline中的Handler对象实例只有一个，从而减少Handler对象实例的创建。代码示例如下：\n\n```\n    public class DataServerInitializer extends ChannelInitializer<Channel> {\n       private static final DataServerHandler SHARED = new DataServerHandler();\n  \n       @Override\n       public void initChannel(Channel channel) {\n           channel.pipeline().addLast(\"handler\", SHARED);\n       }\n   }\n```\n\nSharable注解的使用是有限制的，多个ChannelPipeline只有一个实例，所以该Handler要求无状态。上述示例中，DataServerHandler的事件处理方法中，不能使用或改变本身的私有变量，因为ChannelHandler是非线程安全的，使用私有变量会造成线程竞争而产生错误结果。\n\n##  ChannelHandlerContext\n\nContext指上下文关系，ChannelHandler的Context指的是ChannleHandler之间的关系以及ChannelHandler与ChannelPipeline之间的关系。ChannelPipeline中的事件传播主要依赖于ChannelHandlerContext实现，由于ChannelHandlerContext中有ChannelHandler之间的关系，所以能得到ChannelHandler的后继节点，从而将事件传播到下一个ChannelHandler。\n\nChannelHandlerContext继承自AttributeMap，所以提供了attr()方法设置和删除一些状态属性值，用户可将业务逻辑中所需使用的状态属性值存入到Context中。此外，Channel也继承自AttributeMap，也有attr()方法，在Netty4.0中，这两个attr()方法并不等效，这会给用户程序员带来困惑并且增加内存开销，所以Netty4.1中将channel.attr()==ctx.attr()。在使用Netty4.0时，建议只使用channel.attr()防止引起不必要的困惑。\n\n一个Channel对应一个ChannelPipeline，一个ChannelHandlerContext对应一个ChannelHandler，但一个ChannelHandler可以对应多个ChannelHandlerContext。当一个ChannelHandler使用Sharable注解修饰且添加同一个实例对象到不用的Channel时，只有一个ChannelHandler实例对象，但每个Channel中都有一个ChannelHandlerContext对象实例与之对应。\n\n\n"
  },
  {
    "path": "Netty/Netty源码解析5-ChannelHandler.md",
    "content": "\nChannelHandler并不处理事件，而由其子类代为处理：ChannelInboundHandler拦截和处理入站事件，ChannelOutboundHandler拦截和处理出站事件。ChannelHandler和ChannelHandlerContext通过组合或继承的方式关联到一起成对使用。事件通过ChannelHandlerContext主动调用如fireXXX()和write(msg)等方法，将事件传播到下一个处理器。注意：入站事件在ChannelPipeline双向链表中由头到尾正向传播，出站事件则方向相反。\n当客户端连接到服务器时，Netty新建一个ChannelPipeline处理其中的事件，而一个ChannelPipeline中含有若干ChannelHandler。如果每个客户端连接都新建一个ChannelHandler实例，当有大量客户端时，服务器将保存大量的ChannelHandler实例。为此，Netty提供了Sharable注解，如果一个ChannelHandler状态无关，那么可将其标注为Sharable，如此，服务器只需保存一个实例就能处理所有客户端的事件。\n\n## 核心类图\n\n![8367024ae2bcb818fd07d78781b83c62](Netty源码解析5-ChannelHandler.resources/673AB200-CF20-4FEC-BBC8-D9B0568F70CB.png)\n上图是ChannelHandler的核心类类图，其继承层次清晰，我们逐一分析。\n\n### 1.ChannelHandler\n\n\nChannaleHandler 作为最顶层的接口，并不处理入站和出站事件，所以接口中只包含最基本的方法：\n```\n// Handler本身被添加到ChannelPipeline时调用\n    void handlerAdded(ChannelHandlerContext ctx) throws Exception;\n    // Handler本身被从ChannelPipeline中删除时调用\n    void handlerRemoved(ChannelHandlerContext ctx) throws Exception;\n    // 发生异常时调用\n    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;\n```\n其中也定义了Sharable标记注解：\n```\n @Inherited\n    @Documented\n    @Target(ElementType.TYPE)\n    @Retention(RetentionPolicy.RUNTIME)\n    @interface Sharable {\n        // no value\n    }\n```\n作为ChannelHandler的默认实现，ChannelHandlerAdapter有个重要的方法isSharable()，代码如下：\n\n```\n public boolean isSharable() {\n        Class<?> clazz = getClass();\n        // 每个线程一个缓存\n        Map<Class<?>, Boolean> cache = \n                InternalThreadLocalMap.get().handlerSharableCache();\n        Boolean sharable = cache.get(clazz);\n        if (sharable == null) {\n            // Handler是否存在Sharable注解\n            sharable = clazz.isAnnotationPresent(Sharable.class);\n            cache.put(clazz, sharable);\n        }\n        return sharable;\n    }\n```\n\n这里引入了优化的线程局部变量InternalThreadLocalMap，将在以后分析，此处可简单理解为线程变量ThreadLocal，即每个线程都有一份ChannelHandler是否Sharable的缓存。这样可以减少线程间的竞争，提升性能。\n\n### 2.ChannelInboundHandler\n\nChannelInboundHandler处理入站事件，以及用户自定义事件：\n```\n    // 类似的入站事件\n    void channeXXX(ChannelHandlerContext ctx) throws Exception;\n    // 用户自定义事件\n    void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;\n```\nChannelInboundHandlerAdapter作为ChannelInboundHandler的实现，默认将入站事件自动传播到下一个入站处理器。其中的代码高度一致，如下：\n```\n    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        ctx.fireChannelRead(msg);\n    }\n```\n### 3.ChannelOutboundHandler\n\nChannelOutboundHandler处理出站事件：\n```\n// 类似的出站事件\n    void read(ChannelHandlerContext ctx) throws Exception;\n```\n同理，ChannelOutboundHandlerAdapter作为ChannelOutboundHandler的事件，默认将出站事件传播到下一个出站处理器：\n```\n    @Override\n    public void read(ChannelHandlerContext ctx) throws Exception {\n        ctx.read();\n    }\n```\n\n### 4.ChannelDuplexHandler\nChannelDuplexHandler则同时实现了ChannelInboundHandler和ChannelOutboundHandler接口。如果一个所需的ChannelHandler既要处理入站事件又要处理出站事件，推荐继承此类。\n至此，ChannelHandler的核心类已分析完毕，接下来将分析一些Netty自带的Handler。\n"
  },
  {
    "path": "Netty/Netty源码解析6-ChannelHandler实例之LoggingHandler.md",
    "content": "## LoggingHandler\n\n日志处理器LoggingHandler是使用Netty进行开发时的好帮手，它可以对入站\\出站事件进行日志记录，从而方便我们进行问题排查。首先看类签名：\n```\n    @Sharable\n    public class LoggingHandler extends ChannelDuplexHandler\n```\n注解Sharable说明LoggingHandler没有状态相关变量，所有Channel可以使用一个实例。继承自ChannelDuplexHandler表示对入站出站事件都进行日志记录。最佳实践：使用static修饰LoggingHandler实例，并在生产环境删除LoggingHandler。\n该类的成员变量如下：\n\n```\n // 实际使用的日志处理，slf4j、log4j等\n    protected final InternalLogger logger;\n    // 日志框架使用的日志级别\n    protected final InternalLogLevel internalLevel;\n    // Netty使用的日志级别\n    private final LogLevel level;\n    \n    // 默认级别为Debug\n    private static final LogLevel DEFAULT_LEVEL = LogLevel.DEBUG;\n```\n\n\n看完成员变量，在移目构造方法，LoggingHandler的构造方法较多，一个典型的如下：\n\n```\n  public LoggingHandler(LogLevel level) {\n        if (level == null) {\n            throw new NullPointerException(\"level\");\n        }\n        // 获得实际的日志框架\n        logger = InternalLoggerFactory.getInstance(getClass());\n        // 设置日志级别\n        this.level = level;\n        internalLevel = level.toInternalLevel();\n    }\n```\n\n在构造方法中获取用户实际使用的日志框架，如slf4j、log4j等，并日志设置记录级别。其他的构造方法也类似，不在赘述。\n记录出站、入站事件的过程类似，我们以ChannelRead()为例分析，代码如下：\n\n```\n public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        logMessage(ctx, \"RECEIVED\", msg);   // 记录日志\n        ctx.fireChannelRead(msg);   // 传播事件\n    }\n    \n    private void logMessage(ChannelHandlerContext ctx, String eventName, Object msg) {\n        if (logger.isEnabled(internalLevel)) {\n            logger.log(internalLevel, format(ctx, formatMessage(eventName, msg)));\n        }\n    }\n    \n    protected String formatMessage(String eventName, Object msg) {\n        if (msg instanceof ByteBuf) {\n            return formatByteBuf(eventName, (ByteBuf) msg);\n        } else if (msg instanceof ByteBufHolder) {\n            return formatByteBufHolder(eventName, (ByteBufHolder) msg);\n        } else {\n            return formatNonByteBuf(eventName, msg);\n        }\n    }\n```\n\n其中的代码都简单明了，主要分析formatByteBuf()方法：\n\n```\n protected String formatByteBuf(String eventName, ByteBuf msg) {\n        int length = msg.readableBytes();\n        if (length == 0) {\n            StringBuilder buf = new StringBuilder(eventName.length() + 4);\n            buf.append(eventName).append(\": 0B\");\n            return buf.toString();\n        } else {\n            int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;\n            StringBuilder buf = new StringBuilder(eventName.length() + \n                        2 + 10 + 1 + 2 + rows * 80);\n\n            buf.append(eventName)\n                      .append(\": \").append(length).append('B').append(NEWLINE);\n            appendPrettyHexDump(buf, msg);\n\n            return buf.toString();\n        }\n```\n\n其中的数字计算，容易让人失去耐心，使用逆向思维，放上结果反推：\n\n![42a9d3e0d278b97382fcbd706a3d29d2](Netty源码解析6-ChannelHandler实例之LoggingHandler.resources/37CFD95E-B857-4C56-94C5-7BA428F61F71.png)\n\n"
  },
  {
    "path": "Netty/Netty源码解析7-ChannelHandler实例之TimeoutHandler.md",
    "content": "## TimeoutHandler\n\n在开发TCP服务时，一个常见的需求便是使用心跳保活客户端。而Netty自带的三个超时处理器IdleStateHandler，ReadTimeoutHandler和WriteTimeoutHandler可完美满足此需求。其中IdleStateHandler可处理读超时（客户端长时间没有发送数据给服务端）、写超时（服务端长时间没有发送数据到客户端）和读写超时（客户端与服务端长时间无数据交互）三种情况。这三种情况的枚举为：\n\n```\npublic enum IdleState {\n        READER_IDLE,    // 读超时\n        WRITER_IDLE,    // 写超时\n        ALL_IDLE    // 数据交互超时\n    }\n```\n\n以IdleStateHandler的读超时事件为例进行分析，首先看类签名：\n\n```\n public class IdleStateHandler extends ChannelDuplexHandler\n```\n\n注意到此Handler没有Sharable注解，这是因为每个连接的超时时间是特有的即每个连接有独立的状态，所以不能标注Sharable注解。继承自ChannelDuplexHandler是因为既要处理读超时又要处理写超时。\n该类的一个典型构造方法如下：\n\n\n```\n    public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, \n                int allIdleTimeSeconds) {\n        this(readerIdleTimeSeconds, writerIdleTimeSeconds,  \n                allIdleTimeSeconds, TimeUnit.SECONDS);\n    }\n```\n\n分别设定各个超时事件的时间阈值。以读超时事件为例，有以下相关的字段：\n\n```\n// 用户配置的读超时时间\n    private final long readerIdleTimeNanos;\n    // 判定超时的调度任务Future\n    private ScheduledFuture<?> readerIdleTimeout;\n    // 最近一次读取数据的时间\n    private long lastReadTime;\n    // 是否第一次读超时事件\n    private boolean firstReaderIdleEvent = true;\n    // 状态，0 - 无关， 1 - 初始化完成 2 - 已被销毁\n    private byte state; \n    // 是否正在读取\n    private boolean reading;\n```\n\n首先看初始化方法initialize()：\n\n```\n    private void initialize(ChannelHandlerContext ctx) {\n        switch (state) {\n        case 1: // 初始化进行中或者已完成\n        case 2: // 销毁进行中或者已完成\n            return;\n        }\n        \n        state = 1;\n        lastReadTime = ticksInNanos();\n        if (readerIdleTimeNanos > 0) {\n            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),\n                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);\n        }\n```\n\n初始化的工作较为简单，设定最近一次读取时间lastReadTime为当前系统时间，然后在用户设置的读超时时间readerIdleTimeNanos截止时，执行一个ReaderIdleTimeoutTask进行检测。其中使用的方法很简洁，如下：\n\n```\n     long ticksInNanos() {\n        return System.nanoTime();\n    }\n    \n    ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, \n              long delay, TimeUnit unit) {\n        return ctx.executor().schedule(task, delay, unit);\n    }\n```\n\n然后，分析销毁方法destroy()：\n\n```\nprivate void destroy() {\n        state = 2;  // 这里结合initialize对比理解\n        if (readerIdleTimeout != null) {\n            // 取消调度任务，并置null\n            readerIdleTimeout.cancel(false);\n            readerIdleTimeout = null;\n        }\n    }\n```\n\n可知销毁的处理也很简单，分析完初始化和销毁，再看这两个方法被调用的地方，initialize()在三个方法中被调用：\n\n```\npublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {\n        if (ctx.channel().isActive() &&\n                ctx.channel().isRegistered()) {\n            initialize(ctx);\n        } \n    }\n    \n    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {\n        if (ctx.channel().isActive()) {\n            initialize(ctx);\n        }\n        super.channelRegistered(ctx);\n    }\n    \n    public void channelActive(ChannelHandlerContext ctx) throws Exception {\n        initialize(ctx);\n        super.channelActive(ctx);\n    }\n```\n\n当客户端与服务端成功建立连接后，Channel被激活，此时channelActive的初始化被调用；如果Channel被激活后，动态添加此Handler，则handlerAdded的初始化被调用；如果Channel被激活，用户主动切换Channel的执行线程Executor，则channelRegistered的初始化被调用。这一部分较难理解，请仔细体会。destroy()则有两处调用：\n\n\n```\n public void channelInactive(ChannelHandlerContext ctx) throws Exception {\n        destroy();\n        super.channelInactive(ctx);\n    }\n    \n    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {\n        destroy();\n    }\n```\n\n即该Handler被动态删除时，handlerRemoved的销毁被执行；Channel失效时，channelInactive的销毁被执行。\n分析完这些，在分析核心的调度任务ReaderIdleTimeoutTask：\n\n```\nprivate final class ReaderIdleTimeoutTask implements Runnable {\n        \n        private final ChannelHandlerContext ctx;\n        \n        ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {\n            this.ctx = ctx;\n        }\n\n        @Override\n        protected void run() {\n            if (!ctx.channel().isOpen()) {\n                // Channel不再有效\n                return;\n            }\n            \n            long nextDelay = readerIdleTimeNanos;\n            if (!reading) {\n                // nextDelay<=0 说明在设置的超时时间内没有读取数据\n                nextDelay -= ticksInNanos() - lastReadTime;\n            }\n            // 隐含正在读取时，nextDelay = readerIdleTimeNanos > 0\n\n            if (nextDelay <= 0) {\n                // 超时时间已到，则再次调度该任务本身\n                readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, \n                    TimeUnit.NANOSECONDS);\n\n                boolean first = firstReaderIdleEvent;\n                firstReaderIdleEvent = false;\n\n                try {\n                    IdleStateEvent event =\n                        newIdleStateEvent(IdleState.READER_IDLE, first);\n                    channelIdle(ctx, event); // 模板方法处理\n                } catch (Throwable t) {\n                    ctx.fireExceptionCaught(t);\n                }\n            } else {\n                // 注意此处的nextDelay值，会跟随lastReadTime刷新\n                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);\n            }\n        }\n    }\n```\n这个读超时检测任务执行的过程中又递归调用了它本身进行下一次调度，请仔细品味该种使用方法。再列出channelIdle()的代码：\n\n```\n protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) \n                  throws Exception {\n        ctx.fireUserEventTriggered(evt);\n    }\n```\n\n本例中，该方法将写超时事件作为用户事件传播到下一个Handler，用户需要在某个Handler中拦截该事件进行处理。该方法标记为protect说明子类通常可覆盖，ReadTimeoutHandler子类即定义了自己的处理：\n\n```\n@Override\n    protected final void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt)\n                   throws Exception {\n        assert evt.state() == IdleState.READER_IDLE;\n        readTimedOut(ctx);\n    }\n\n    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {\n        if (!closed) {\n            ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);\n            ctx.close();\n            closed = true;\n        }\n    }\n```\n\n可知在ReadTimeoutHandler中，如果发生读超时事件，将会关闭该Channel。当进行心跳处理时，使用IdleStateHandler较为麻烦，一个简便的方法是：直接继承ReadTimeoutHandler然后覆盖readTimedOut()进行用户所需的超时处理。"
  },
  {
    "path": "Netty/Netty源码解析8-ChannelHandler实例之CodecHandler.md",
    "content": "编解码处理器作为Netty编程时必备的ChannelHandler，每个应用都必不可少。Netty作为网络应用框架，在网络上的各个应用之间不断进行数据交互。而网络数据交换的基本单位是字节，所以需要将本应用的POJO对象编码为字节数据发送到其他应用，或者将收到的其他应用的字节数据解码为本应用可使用的POJO对象。这一部分，又和JAVA中的序列化和反序列化对应。幸运的是，有很多其他的开源工具（protobuf，thrift，json，xml等等）可方便的处理POJO对象的序列化，可参见这个链接。\n在互联网中，Netty使用TCP/UDP协议传输数据。由于Netty基于异步事件处理以及TCP的一些特性，使得TCP数据包会发生粘包现象。想象这样的情况，客户端与服务端建立连接后，连接发送了两条消息：\n\n    +------+   +------+\n    | MSG1 |   | MSG2 |\n    +------+   +------+\n在互联网上传输数据时，连续发送的两条消息，在服务端极有可能被合并为一条：\n\n     +------------+\n    | MSG1  MSG2 |\n    +------------+\n\n这还不是最坏的情况，由于路由器的拆包和重组，可能收到这样的两个数据包：\n\n     +----+     +---------+         +-------+    +-----+ \n        | MS |     |  G1MSG2 |  或者  | MSG1M |    | SG2 | \n        +----+     +---------+        +-------+    +-----+\n\n\n\n而服务端要正确的识别出这样的两条消息，就需要编码器的正确工作。为了正确的识别出消息，业界有以下几种做法：\n\n使用定界符分割消息，一个特例是使用换行符分隔每条消息。\n使用定长的消息。\n在消息的某些字段指明消息长度。\n\n明白了这些，进入正题，分析Netty的编码框架ByteToMessageDecoder。\n\n## ByteToMessageDecoder\n\n在分析之前，需要说明一点：ByteToMessage容易引起误解，解码结果Message会被认为是JAVA对象POJO，但实际解码结果是消息帧。也就是说该解码器处理TCP的粘包现象，将网络发送的字节流解码为具有确定含义的消息帧，之后的解码器再将消息帧解码为实际的POJO对象。\n明白了这点，再次回顾两条消息发送的最坏情况，可知要正确取得两条消息，需要一个内存区域存储消息，当收到MS时继续等待第二个包G1MSG2到达再进行解码操作。在ByteToMessageDecoder中，这个内存区域被抽象为Cumulator，直译累积器，可自动扩容累积字节数据，Netty将其定义为一个接口：\n\n```\n    public interface Cumulator {\n        ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in);\n    }\n```\n\n其中，两个ByteBuf参数cumulation指已经累积的字节数据，in表示该次channelRead()读取到的新数据。返回ByteBuf为累积数据后的新累积区（必要时候自动扩容）。自动扩容的代码如下：\n\n```\n static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, \n                                       int newReadBytes) {\n        ByteBuf oldCumulation = cumulation;\n        // 扩容后新的缓冲区\n        cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);\n        cumulation.writeBytes(oldCumulation);\n        // 旧的缓冲区释放\n        oldCumulation.release();\n        return cumulation;\n    }\n```\n\n自动扩容的方法简单粗暴，直接使用大容量的Bytebuf替换旧的ByteBuf。Netty定义了两个累积器，一个为MERGE_CUMULATOR：\n\n```\npublic static final Cumulator MERGE_CUMULATOR = new Cumulator() {\n        @Override\n        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {\n            ByteBuf buffer;\n            // 1.累积区容量不够容纳数据\n            // 2.用户使用了slice().retain()或duplicate().retain()使refCnt增加\n            if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()\n                    || cumulation.refCnt() > 1) {\n                buffer = expandCumulation(alloc, cumulation, in.readableBytes());\n            } else {\n                buffer = cumulation;\n            }\n            buffer.writeBytes(in);\n            in.release();\n            return buffer;\n        }\n    };\n```\n可知，两种情况下会扩容：\n\n1. 累积区容量不够容纳新读入的数据\n2. 用户使用了slice().retain()或duplicate().retain()使refCnt增加并且大于1，此时扩容返回一个新的累积区ByteBuf，方便用户对老的累积区ByteBuf进行后续处理。\n\n另一个累积器为COMPOSITE_CUMULATOR：\n\n```\npublic static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {\n        @Override\n        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {\n            ByteBuf buffer;\n            if (cumulation.refCnt() > 1) {\n                buffer = expandCumulation(alloc, cumulation, in.readableBytes());\n                buffer.writeBytes(in);\n                in.release();\n            } else {\n                CompositeByteBuf composite;\n                if (cumulation instanceof CompositeByteBuf) {\n                    composite = (CompositeByteBuf) cumulation;\n                } else {\n                    composite = alloc.compositeBuffer(Integer.MAX_VALUE);\n                    composite.addComponent(true, cumulation);\n                }\n                composite.addComponent(true, in);\n                buffer = composite;\n            }\n            return buffer;\n        }\n    };\n```\n\n这个累积器只在第二种情况refCnt>1时扩容，除此之外处理和MERGE_CUMULATOR一致，不同的是当cumulation不是CompositeByteBuf时会创建新的同类CompositeByteBuf，这样最后返回的ByteBuf必定是CompositeByteBuf。使用这个累积器后，当容量不够时并不会进行内存复制，只会讲新读入的in加到CompositeByteBuf中。需要注意的是：此种情况下虽然不需内存复制，却要求用户维护复杂的索引，在某些使用中可能慢于MERGE_CUMULATOR。故Netty默认使用MERGE_CUMULATOR累积器。\n累积器分析完毕，步入正题ByteToMessageDecoder，首先看类签名：\n\n```\npublic abstract class ByteToMessageDecoder extends\n                                ChannelInboundHandlerAdapter\n```\n\n该类是一个抽象类，其中的抽象方法只有一个decode()：\n\n```\nprotected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, \nList<Object> out) throws Exception;\n```\n\n用户使用了该解码框架后，只需实现该方法就可定义自己的解码器。参数in表示累积器已累积的数据，out表示本次可从累积数据解码出的结果列表，结果可为POJO对象或者ByteBuf等等Object。\n关注一下成员变量，以便更好的分析：\n\n```\n    ByteBuf cumulation; // 累积区\n    private Cumulator cumulator = MERGE_CUMULATOR; // 累积器\n    // 设置为true后每个channelRead事件只解码出一个结果\n    private boolean singleDecode;   // 某些特殊协议使用\n    private boolean decodeWasNull;  // 解码结果为空\n    private boolean first;  // 是否首个消息\n    // 累积区不丢弃字节的最大次数，16次后开始丢弃\n    private int discardAfterReads = 16;\n    private int numReads;   // 累积区不丢弃字节的channelRead次数\n```\n下面，直接进入channelRead()事件处理：\n\n```\n public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n        // 只对ByteBuf处理即只对字节数据进行处理\n        if (msg instanceof ByteBuf) {\n            // 解码结果列表\n            CodecOutputList out = CodecOutputList.newInstance();\n            try {\n                ByteBuf data = (ByteBuf) msg;\n                first = cumulation == null; // 累积区为空表示首次解码\n                if (first) {\n                    // 首次解码直接使用读入的ByteBuf作为累积区\n                    cumulation = data;\n                } else {\n                    // 非首次需要进行字节数据累积\n                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);\n                }\n                callDecode(ctx, cumulation, out); // 解码操作\n            } catch (DecoderException e) {\n                throw e;\n            } catch (Throwable t) {\n                throw new DecoderException(t);\n            } finally {\n                if (cumulation != null && !cumulation.isReadable()) {\n                    // 此时累积区不再有字节数据，已被处理完毕\n                    numReads = 0;\n                    cumulation.release();\n                    cumulation = null;\n                } else if (++ numReads >= discardAfterReads) {\n                    // 连续discardAfterReads次后\n                    // 累积区还有字节数据，此时丢弃一部分数据\n                    numReads = 0;\n                    discardSomeReadBytes(); // 丢弃一些已读字节\n                }\n\n                int size = out.size();\n                // 本次没有解码出数据，此时size=0\n                decodeWasNull = !out.insertSinceRecycled();\n                fireChannelRead(ctx, out, size); // 触发事件\n                out.recycle();  // 回收解码结果\n            }\n        } else {\n            ctx.fireChannelRead(msg);\n        }\n    }\n```\n\n解码结果列表CodecOutputList是Netty定制的一个特殊列表，该列表在线程中被缓存，可循环使用来存储解码结果，减少不必要的列表实例创建，从而提升性能。由于解码结果需要频繁存储，普通的ArrayList难以满足该需求，故定制化了一个特殊列表，由此可见Netty对优化的极致追求。\n注意finally块的第一个if情况满足时，即累积区的数据已被读取完毕，请考虑释放累积区的必要性。想象这样的情况，当一条消息被解码完毕后，如果客户端长时间不发送消息，那么，服务端保存该条消息的累积区将一直占据服务端内存浪费资源，所以必须释放该累积区。\n第二个if情况满足时，即累积区的数据一直在channelRead读取数据进行累积和解码，直到达到了discardAfterReads次（默认16），此时累积区依然还有数据。在这样的情况下，Netty主动丢弃一些字节，这是为了防止该累积区占用大量内存甚至耗尽内存引发OOM。\n处理完这些情况后，最后统一触发ChannelRead事件，将解码出的数据传递给下一个处理器。注意：当out=0时，统一到一起被处理了。\n再看细节的discardSomeReadBytes()和fireChannelRead()：\n\n\n```\n protected final void discardSomeReadBytes() {\n        if (cumulation != null && !first && cumulation.refCnt() == 1) {\n            cumulation.discardSomeReadBytes();\n        }\n    }\n    \n    static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, \n                        int numElements) {\n        for (int i = 0; i < numElements; i ++) {\n            ctx.fireChannelRead(msgs.getUnsafe(i));\n        }\n    }\n```\n\n代码比较简单，只需注意discardSomeReadBytes中，累积区的refCnt() == 1时才丢弃数据是因为：如果用户使用了slice().retain()和duplicate().retain()使refCnt>1，表明该累积区还在被用户使用，丢弃数据可能导致用户的困惑，所以须确定用户不再使用该累积区的已读数据，此时才丢弃。\n下面分析解码核心方法callDecode()：\n\n```\n protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {\n        try {\n            while (in.isReadable()) {\n                int outSize = out.size();\n\n                if (outSize > 0) {\n                    // 解码出消息就立即处理，防止消息等待\n                    fireChannelRead(ctx, out, outSize);\n                    out.clear();\n                    \n                    // 用户主动删除该Handler，继续操作in是不安全的\n                    if (ctx.isRemoved()) {\n                        break;\n                    }\n                    outSize = 0;\n                }\n\n                int oldInputLength = in.readableBytes();\n                decode(ctx, in, out);   // 子类需要实现的具体解码步骤\n\n                // 用户主动删除该Handler，继续操作in是不安全的\n                if (ctx.isRemoved()) {\n                    break; \n                }\n                \n                // 此时outSize都==0（这的代码容易产生误解 应该直接使用0）\n                if (outSize == out.size()) {\n                    if (oldInputLength == in.readableBytes()) {\n                        // 没有解码出消息，且没读取任何in数据\n                        break;\n                    } else {\n                        // 读取了一部份数据但没有解码出消息\n                        // 说明需要更多的数据，故继续\n                        continue;\n                    }\n                }\n\n                // 运行到这里outSize>0 说明已经解码出消息\n                if (oldInputLength == in.readableBytes()) {\n                    // 解码出消息但是in的读索引不变，用户的decode方法有Bug\n                    throw new DecoderException(\n                            \"did not read anything but decoded a message.\");\n                }\n                \n                // 用户设定一个channelRead事件只解码一次\n                if (isSingleDecode()) {\n                    break; \n                }\n            }\n        } catch (DecoderException e) {\n            throw e;\n        } catch (Throwable cause) {\n            throw new DecoderException(cause);\n        }\n    }\n```\n循环中的第一个if分支，检查解码结果，如果已经解码出消息则立即将消息传播到下一个处理器进行处理，这样可使消息得到及时处理。在调用decode()方法的前后，都检查该Handler是否被用户从ChannelPipeline中删除，如果删除则跳出解码步骤不对输入缓冲区in进行操作，因为继续操作in已经不安全。解码完成后，对in解码前后的读索引进行了检查，防止用户的错误使用，如果用户错误使用将抛出异常。\n至此，核心的解码框架已经分析完毕，再看最后的一些边角处理。首先是channelReadComplete()读事件完成后的处理：\n\n```\npublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {\n        numReads = 0;   // 连续读次数置0\n        discardSomeReadBytes(); // 丢弃已读数据，节约内存\n        if (decodeWasNull) {\n            // 没有解码出结果，则期待更多数据读入\n            decodeWasNull = false;\n            if (!ctx.channel().config().isAutoRead()) {\n                ctx.read();\n            }\n        }\n        ctx.fireChannelReadComplete();\n    }\n\n```\n\n如果channelRead()中没有解码出消息，极有可能是数据不够，由此调用ctx.read()期待读入更多的数据。如果设置了自动读取，将会在HeadHandler中调用ctx.read()；没有设置自动读取，则需要此处显式调用。\n最后再看Handler从ChannelPipelien中移除的处理handlerRemoved():\n\n```\n public final void handlerRemoved(ChannelHandlerContext ctx) throws Exception {\n        ByteBuf buf = cumulation;\n        if (buf != null) {\n            cumulation = null;  // 释放累积区，GC回收\n\n            int readable = buf.readableBytes();\n            if (readable > 0) {\n                ByteBuf bytes = buf.readBytes(readable);\n                buf.release();\n                // 解码器已被删除故不再解码，只将数据传播到下一个Handler\n                ctx.fireChannelRead(bytes);\n            } else {\n                buf.release();\n            }\n\n            numReads = 0;   // 置0，有可能被再次添加\n            ctx.fireChannelReadComplete();\n        }\n        handlerRemoved0(ctx);   // 用户可进行的自定义处理\n    }\n```\n\n当解码器被删除时，如果还有没被解码的数据，则将数据传播到下一个处理器处理，防止丢失数据。此外，当连接不再有效触发channelInactive事件或者触发ChannelInputShutdownEvent时，则会调用callDecode()解码，如果解码出消息，传播到下一个处理器。这部分的代码不再列出。\n至此，ByteToMessageDecoder解码框架已分析完毕，下面，我们选用具体的实例进行分析。\n\n### LineBasedFrameDecoder\n\n基于行分隔的解码器LineBasedFrameDecoder是一个特殊的分隔符解码器，该解码器使用的分隔符为：windows的\\r\\n和类linux的\\n。\n首先看该类定义的成员变量：\n```\n    // 最大帧长度，超过此长度将抛出异常TooLongFrameException\n    private final int maxLength;\n    // 是否快速失败，true-检测到帧长度过长立即抛出异常不在读取整个帧\n    // false-检测到帧长度过长依然读完整个帧再抛出异常\n    private final boolean failFast;\n    // 是否略过分隔符，true-解码结果不含分隔符\n    private final boolean stripDelimiter;\n\n    // 超过最大帧长度是否丢弃字节\n    private boolean discarding;\n    private int discardedBytes; // 丢弃的字节数\n```\n其中，前三个变量可由用户根据实际情况配置，后两个变量解码时使用。\n该子类覆盖的解码方法如下：\n\n```\nprotected final void decode(ChannelHandlerContext ctx, ByteBuf in, \n                   List<Object> out) throws Exception {\n        Object decoded = decode(ctx, in);\n        if (decoded != null) {\n            out.add(decoded);\n        }\n    }\n```\n其中又定义了decode(ctx, in)解码出单个消息帧，事实上这也是其他编码子类使用的方法。decode(ctx, in)方法处理很绕弯，只给出伪代码：\n\n```\nprotected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {\n        final int eol = findEndOfLine(buffer);\n        if (!discarding) {\n            if (eol >= 0) {\n                // 此时已找到换行符\n                if(!checkMaxLength()) {\n                    return getFrame().retain();\n                } \n                // 超过最大长度抛出异常\n            } else {\n                if (checkMaxLength()) {\n                    // 设置true表示下一次解码需要丢弃字节\n                    discarding = true;  \n                    if (failFast) {\n                        // 抛出异常\n                    }\n                } \n            }\n        } else {\n            if (eol >= 0) {\n                // 丢弃换行符以及之前的字节\n                buffer.readerIndex(eol + delimLength);\n            } else {\n                // 丢弃收到的所有字节\n                buffer.readerIndex(buffer.writerIndex());\n            }\n        }\n    }\n```\n\n该方法需要结合解码框架的while循环反复理解，每个if情况都是一次while循环，而变量discarding就成为控制每次解码流程的状态量，注意其中的状态转移。(想法：使用状态机实现，则流程更清晰)\n\n### DelimiterBasedFrameDecoder\n\n该解码器是更通用的分隔符解码器，可支持多个分隔符，每个分隔符可为一个或多个字符。如果定义了多个分隔符，并且可解码出多个消息帧，则选择产生最小帧长的结果。例如，使用行分隔符\\r\\n和\\n分隔：\n\n        +--------------+\n        | ABC\\nDEF\\r\\n |\n        +--------------+\n        \n可有两种结果：\n\n    +-----+-----+              +----------+   \n    | ABC | DEF |  (√)   和    | ABC\\nDEF |  (×)\n    +-----+-----+              +----------+\n    \n    \n该编码器可配置的变量与LineBasedFrameDecoder类似，只是多了一个ByteBuf[] delimiters用于配置具体的分隔符。\nNetty在Delimiters类中定义了两种默认的分隔符，分别是NULL分隔符和行分隔符：\n\n```\n  public static ByteBuf[] nulDelimiter() {\n        return new ByteBuf[] {\n                Unpooled.wrappedBuffer(new byte[] { 0 }) };\n    }\n    \n    public static ByteBuf[] lineDelimiter() {\n        return new ByteBuf[] {\n                Unpooled.wrappedBuffer(new byte[] { '\\r', '\\n' }),\n                Unpooled.wrappedBuffer(new byte[] { '\\n' }),\n        };\n    }\n```\n\n### FixedLengthFrameDecoder\n\n该解码器十分简单，按照固定长度frameLength解码出消息帧。如下的数据帧解码为固定长度3的消息帧示例如下：\n\n    +---+----+------+----+      +-----+-----+-----+\n    | A | BC | DEFG | HI |  ->  | ABC | DEF | GHI |\n    +---+----+------+----+      +-----+-----+-----+\n    \n\n其中的解码方法也十分简单：\n    \n```\n protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {\n        if (in.readableBytes() < frameLength) {\n            return null;\n        } else {\n            return in.readSlice(frameLength).retain();\n        }\n    }\n```\n\n### LengthFieldBasedFrameDecoder\n\n基于长度字段的消息帧解码器，该解码器可根据数据包中的长度字段动态的解码出消息帧。一个推荐的二进制传输协议可设计为如下格式：\n\n    +----------+------+----------+------+\n    |  头部长度 |  头部 |  数据长度 | 数据 |\n    +----------+------+----------+------+\n    \n\n这样的协议可满足大多数场景使用，但不幸的是：很多情况下并不可以设计新的协议，往往要在老旧的协议上传输数据。由此，Netty将该解码器设计的十分通用，只要有类似的长度字段便能正确解码出消息帧。当然前提是：正确使用解码器。\n没有什么是完美的，由于该解码器十分通用，所以有大量的配置变量：\n\n```\n    private final ByteOrder byteOrder;\n    private final int maxFrameLength;\n    private final boolean failFast;\n    private final int lengthFieldOffset;\n    private final int lengthFieldLength;\n    private final int lengthAdjustment;\n    private final int initialBytesToStrip;\n```\n\n变量byteOrder表示长度字段的字节序：大端或小端，默认为大端。如果对字节序有疑问，请查阅其他资料，不再赘述。maxFrameLength和failFast与其他解码器相同，控制最大帧长度和快速失败抛异常，注意：该解码器failFast默认为true。\n接下来将重点介绍其它四个变量：\n\n\n* lengthFieldOffset表示长度字段偏移量即在一个数据包中长度字段的具体下标位置。标准情况，该长度字段为数据部分长度。\n\n* lengthFieldLength表示长度字段的具体字节数，如一个int占4字节。该解码器支持的字节数有：1，2，3，4和8，其他则会抛出异常。另外，还需要注意的是：长度字段的结果为无符号数。\n\n* lengthAdjustment是一个长度调节量，当数据包的长度字段不是数据部分长度而是总长度时，可将此值设定为头部长度，便能正确解码出包含整个数据包的结果消息帧。注意：某些情况下，该值可设定为负数。\n\n* initialBytesToStrip表示需要略过的字节数，如果我们只关心数据部分而不关心头部，可将此值设定为头部长度从而丢弃头部。\n下面我们使用具体的例子来说明：\n\n\n\n* 需求1：如下待解码数据包，正确解码为消息帧，其中长度字段在最前面的2字节，数据部分为12字节的字符串\"HELLO, WORLD\"，长度字段0x000C=12 表示数据部分长度，数据包总长度则为14字节。\n\n\n         解码前(14 bytes)                 解码后(14 bytes)\n         +--------+----------------+      +--------+----------------+\n         | Length | Actual Content |----->| Length | Actual Content |\n         | 0x000C | \"HELLO, WORLD\" |      | 0x000C | \"HELLO, WORLD\" |\n         +--------+----------------+      +--------+----------------+\n         \n正确配置（只列出四个值中不为0的值）：\n\n```\n lengthFieldLength = 2;\n```\n\n* 需求2：需求1的数据包不变，消息帧中去除长度字段。\n\n        解码前(14 bytes)                 解码后(12 bytes)\n        +--------+----------------+      +----------------+\n        | Length | Actual Content |----->| Actual Content |\n        | 0x000C | \"HELLO, WORLD\" |      | \"HELLO, WORLD\" |\n        +--------+----------------+      +----------------+\n        \n正确配置：\n```\n lengthFieldLength   = 2;\n    initialBytesToStrip = 2;\n```\n\n需求3：需求1数据包中长度字段表示数据包总长度。\n\n    解码前(14 bytes)                 解码后(14 bytes)\n        +--------+----------------+      +--------+----------------+\n        | Length | Actual Content |----->| Length | Actual Content |\n        | 0x000E | \"HELLO, WORLD\" |      | 0x000E | \"HELLO, WORLD\" |\n        +--------+----------------+      +--------+----------------+\n        \n\n正确配置：\n\n```\n    lengthFieldLength =  2;\n    lengthAdjustment  = -2;  // 调整长度字段的2字节\n```\n\n需求4：综合难度，数据包有两个头部HDR1和HDR2，长度字段以及数据部分组成，其中长度字段值表示数据包总长度。结果消息帧需要第二个头部HDR2和数据部分。请先给出答案再与标准答案比较，结果正确说明你已完全掌握了该解码器的使用。\n\n    解码前 (16 bytes)                               解码后 (13 bytes)\n    +------+--------+------+----------------+      +------+----------------+\n    | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |\n    | 0xCA | 0x0010 | 0xFE | \"HELLO, WORLD\" |      | 0xFE | \"HELLO, WORLD\" |\n    +------+--------+------+----------------+      +------+----------------+\n    \n    \n\n正确配置：\n\n```\n lengthFieldOffset   =  1;\n    lengthFieldLength   =  2;\n    lengthAdjustment    = -3;\n    initialBytesToStrip =  3;\n```\n\n本解码器的解码过程总体上较为复杂，由于解码的代码是在while循环里面，decode方法return或者抛出异常时可看做一次循环结束，直到in中数据被解析完或者in的readerIndex读索引不再增加才会从while循环跳出。使用状态的思路理解，每个return或者抛出异常看为一个状态：\n\n\n状态1：丢弃过长帧状态，可能是用户设置了错误的帧长度或者实际帧过长。\n\n```\n if (discardingTooLongFrame) {\n        long bytesToDiscard = this.bytesToDiscard;\n        int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());\n        in.skipBytes(localBytesToDiscard); // 丢弃实际的字节数\n        \n        bytesToDiscard -= localBytesToDiscard;\n        this.bytesToDiscard = bytesToDiscard;\n        failIfNecessary(false);\n    }\n```\n\n变量localBytesToDiscard取得实际需要丢弃的字节数，由于过长帧有两种情况：a.用户设置了错误的长度字段，此时in中并没有如此多的字节；b.in中确实有如此长度的帧，这个帧确实超过了设定的最大长度。bytesToDiscard的计算是为了failIfNecessary()确定异常的抛出，其值为0表示当次丢弃状态已经丢弃了in中的所有数据，可以对新读入in的数据进行处理；否则，还处于异常状态。\n\n```\nprivate void failIfNecessary(boolean firstDetectionOfTooLongFrame) {\n        if (bytesToDiscard == 0) {\n            long tooLongFrameLength = this.tooLongFrameLength;\n            this.tooLongFrameLength = 0;\n            // 由于已经丢弃所有数据，关闭丢弃模式\n            discardingTooLongFrame = false;\n            // 已经丢弃了所有字节，当非快速失败模式抛异常\n            if (!failFast || firstDetectionOfTooLongFrame) {\n                fail(tooLongFrameLength);\n            }\n        } else {\n            if (failFast && firstDetectionOfTooLongFrame) {\n                // 帧长度异常，快速失败模式检测到即抛异常\n                fail(tooLongFrameLength);\n            }\n        }\n    }\n\n```\n\n可见，首次检测到帧长度是一种特殊情况，在之后的一个状态进行分析。请注意该状态并不是都抛异常，还有可能进入状态2。\n\n状态2：in中数据不足够组成消息帧，此时直接返回null等待更多数据到达。\n\n```\n    if (in.readableBytes() < lengthFieldEndOffset) {\n        return null;\n    }\n```\n\n状态3：帧长度错误检测，检测长度字段为负值得帧以及加入调整长度后总长小于长度字段的帧，均抛出异常。\n\n```\n int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;\n    // 该方法取出长度字段的值，不再深入分析\n    long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, \n                             lengthFieldLength, byteOrder);\n    if (frameLength < 0) {\n        in.skipBytes(lengthFieldEndOffset);\n        throw new CorruptedFrameException(\"...\");\n    }\n\n    frameLength += lengthAdjustment + lengthFieldEndOffset;\n    if (frameLength < lengthFieldEndOffset) {\n        in.skipBytes(lengthFieldEndOffset);\n        throw new CorruptedFrameException(\"...\");\n```\n\n状态4：帧过长，由前述可知：可能是用户设置了错误的帧长度或者实际帧过长\n\n```\n    if (frameLength > maxFrameLength) {\n            long discard = frameLength - in.readableBytes();\n            tooLongFrameLength = frameLength;\n\n            if (discard < 0) {\n                in.skipBytes((int) frameLength);\n            } else {\n                discardingTooLongFrame = true;\n                bytesToDiscard = discard;\n                in.skipBytes(in.readableBytes());\n            }\n            failIfNecessary(true);\n            return null;\n        }\n```\n\n变量discard<0表示当前收到的数据足以确定是实际的帧过长，所以直接丢弃过长的帧长度；>0表示当前in中的数据并不足以确定是用户设置了错误的帧长度，还是正确帧的后续数据字节还没有到达，但无论何种情况，将丢弃状态discardingTooLongFrame标记设置为true，之后后续数据字节进入状态1处理。==0时，在failIfNecessary(true)无论如何都将抛出异常，><0时，只有设置快速失败才会抛出异常。还需注意一点：failIfNecessary()的参数firstDetectionOfTooLongFrame的首次是指正确解析数据后发生的第一次发生的帧过长，可知会有很多首次。\n\n\n状态5：正确解码出消息帧。\n\n```\n int frameLengthInt = (int) frameLength;\n    if (in.readableBytes() < frameLengthInt) {\n        return null;    // 到达的数据还达不到帧长\n    }\n\n    if (initialBytesToStrip > frameLengthInt) {\n        in.skipBytes(frameLengthInt);   // 跳过字节数错误\n        throw new CorruptedFrameException(\"...\");\n    }\n    in.skipBytes(initialBytesToStrip);\n\n    // 正确解码出数据帧\n    int readerIndex = in.readerIndex();\n    int actualFrameLength = frameLengthInt - initialBytesToStrip;\n    ByteBuf frame = in.slice(readerIndex, actualFrameLength).retain();\n    in.readerIndex(readerIndex + actualFrameLength);\n    return frame;\n```\n\n代码中混合了两个简单状态，到达的数据还达不到帧长和用户设置的忽略字节数错误。由于较为简单，故合并到一起。\n至此解码框架分析完毕。可见，要正确的写出基于长度字段的解码器还是较为复杂的，如果开发时确有需求，特别要注意状态的转移。下面介绍较为简单的编码框架。\n\n"
  },
  {
    "path": "Netty/Netty源码解析9-ChannelHandler实例之MessageToByteEncoder.md",
    "content": "\nMessageToByteEncoder框架可见用户使用POJO对象编码为字节数据存储到ByteBuf。用户只需定义自己的编码方法encode()即可。\n首先看类签名：\n```\n    public abstract class MessageToByteEncoder<I> extends \n                                ChannelOutboundHandlerAdapter\n```\n可知该类只处理出站事件，切确的说是write事件。\n\n该类有两个成员变量，preferDirect表示是否使用内核的DirectedByteBuf，默认为true。TypeParameterMatcher用于检测泛型参数是否是期待的类型，比如说，如果需要编码String类的POJO对象，Matcher会确保write()传入的参数Object的实际切确类型为String。\n直接分析write()的处理：\n```\n    public void write(ChannelHandlerContext ctx, Object msg, \n                          ChannelPromise promise) throws Exception {\n        ByteBuf buf = null;\n        try {\n            if (acceptOutboundMessage(msg)) {\n                I cast = (I) msg;\n                // 分配一个输出缓冲区\n                buf = allocateBuffer(ctx, cast, preferDirect);\n                try {\n                    encode(ctx, cast, buf); // 用户定义的编码方法\n                } finally {\n                    ReferenceCountUtil.release(cast);\n                }\n\n                if (buf.isReadable()) {\n                    ctx.write(buf, promise); // 确实写入了数据\n                } else {\n                    // 没有需要写的数据，也有可能是用户编码错误\n                    buf.release();  \n                    ctx.write(Unpooled.EMPTY_BUFFER, promise);\n                }\n                buf = null;\n            } else {\n                ctx.write(msg, promise);\n            }\n        } catch (EncoderException e) {\n            throw e;\n        } catch (Throwable e) {\n            throw new EncoderException(e);\n        } finally {\n            if (buf != null) {\n                buf.release();\n            }\n        }\n    }\n```\n编码框架简单明了，再列出allocateBuffer()方法的代码：\n```\n    protected ByteBuf allocateBuffer(ChannelHandlerContext ctx,  I msg,\n                               boolean preferDirect) throws Exception {\n        if (preferDirect) {\n            return ctx.alloc().ioBuffer();  // 内核直接缓存\n        } else {\n            return ctx.alloc().heapBuffer(); // JAVA队缓存\n        }\n    }\n```\n总的来说，编码的复杂度大大小于解码的复杂度，这是因为编码不需考虑TCP粘包。编解码的处理还有一个常用的类MessageToMessageCodec用于POJO对象之间的转换。如果有兴趣，可下载源码查看。至此，编解码框架已分析完毕。\n\n"
  },
  {
    "path": "Netty/关于Netty我们都需要知道什么.md",
    "content": "\n## 1.BIO、NIO和AIO的区别？\n* BIO：一个连接一个线程，客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。\n* 伪异步IO：将请求连接放入线程池，一对多，但线程还是很宝贵的资源。\n* NIO：一个请求一个线程，但客户端发送的连接请求都会注册到多路复用器上，多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。\n* AIO：一个有效请求一个线程，客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理，\n\n* BIO是面向流的，NIO是面向缓冲区的；BIO的各种流是阻塞的。而NIO是非阻塞的；BIO的Stream是单向的，而NIO的channel是双向的。\n\n* NIO的特点：事件驱动模型、单线程处理多任务、非阻塞I/O，I/O读写不再阻塞，而是返回0、基于block的传输比基于流的传输更高效、更高级的IO函数zero-copy、IO多路复用大大提高了Java网络应用的可伸缩性和实用性。基于Reactor线程模型。\n\n* 在Reactor模式中，事件分发器等待某个事件或者可应用或个操作的状态发生，事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数，由后者来做实际的读写操作。如在Reactor中实现读：注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来，激活分发器，分发器调用事件对应的处理器、事件处理器完成实际的读操作，处理读到的数据，注册新的事件，然后返还控制权。\n\n## 2.NIO的组成？\nBuffer：与Channel进行交互，数据是从Channel读入缓冲区，从缓冲区写入Channel中的\n\nflip方法 ： 反转此缓冲区，将position给limit，然后将position置为0，其实就是切换读写模式\nclear方法 ：清除此缓冲区，将position置为0，把capacity的值给limit。\nrewind方法 ： 重绕此缓冲区，将position置为0\nDirectByteBuffer可减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高，不可控，通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下，可以考虑使用heapBuffer，由JVM进行管理。\n\nChannel：表示 IO 源与目标打开的连接，是双向的，但不能直接访问数据，只能与Buffer 进行交互。通过源码可知，FileChannel的read方法和write方法都导致数据复制了两次！\n\nSelector可使一个单独的线程管理多个Channel，open方法可创建Selector，register方法向多路复用器器注册通道，可以监听的事件类型：读、写、连接、accept。注册事件后会产生一个SelectionKey：它表示SelectableChannel 和Selector 之间的注册关系，wakeup方法：使尚未返回的第一个选择操作立即返回，唤醒的原因是：注册了新的channel或者事件；channel关闭，取消注册；优先级更高的事件触发（如定时器事件），希望及时处理。\n\nSelector在Linux的实现类是EPollSelectorImpl，委托给EPollArrayWrapper实现，其中三个native方法是对epoll的封装，而EPollSelectorImpl. implRegister方法，通过调用epoll_ctl向epoll实例中注册事件，还将注册的文件描述符(fd)与SelectionKey的对应关系添加到fdToKey中，这个map维护了文件描述符与SelectionKey的映射。\n\nfdToKey有时会变得非常大，因为注册到Selector上的Channel非常多（百万连接）；过期或失效的Channel没有及时关闭。fdToKey总是串行读取的，而读取是在select方法中进行的，该方法是非线程安全的。\n\nPipe：两个线程之间的单向数据连接，数据会被写到sink通道，从source通道读取\n\nNIO的服务端建立过程：Selector.open()：打开一个Selector；ServerSocketChannel.open()：创建服务端的Channel；bind()：绑定到某个端口上。并配置非阻塞模式；register()：注册Channel和关注的事件到Selector上；select()轮询拿到已经就绪的事件\n\n## 3.Netty的特点？\n一个高性能、异步事件驱动的NIO框架，它提供了对TCP、UDP和文件传输的支持\n使用更高效的socket底层，对epoll空轮询引起的cpu占用飙升在内部进行了处理，避免了直接使用NIO的陷阱，简化了NIO的处理方式。\n采用多种decoder/encoder 支持，对TCP粘包/分包进行自动化处理\n可使用接受/处理线程池，提高连接效率，对重连、心跳检测的简单支持\n可配置IO线程数、TCP参数， TCP接收和发送缓冲区使用直接内存代替堆内存，通过内存池的方式循环利用ByteBuf\n通过引用计数器及时申请释放不再引用的对象，降低了GC频率\n使用单线程串行化的方式，高效的Reactor线程模型\n大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用\n\n## 4.Netty的线程模型？\nNetty通过Reactor模型基于多路复用器接收并处理用户请求，内部实现了两个线程池，boss线程池和work线程池，其中boss线程池的线程负责处理请求的accept事件，当接收到accept事件的请求时，把对应的socket封装到一个NioSocketChannel中，并交给work线程池，其中work线程池负责请求的read和write事件，由对应的Handler处理。\n\n单线程模型：所有I/O操作都由一个线程完成，即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接，又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路，性能上无法支撑，速度慢，若线程进入死循环，整个程序不可用，对于高负载、大并发的应用场景不合适。\n\n多线程模型：有一个NIO 线程（Acceptor） 只负责监听服务端，接收客户端的TCP 连接请求；NIO 线程池负责网络IO 的操作，即消息的读取、解码、编码和发送；1 个NIO 线程可以同时处理N 条链路，但是1 个链路只对应1 个NIO 线程，这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时，一个Acceptor 线程可能会存在性能不足问题。\n\n主从多线程模型：Acceptor 线程用于绑定监听端口，接收客户端连接，将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除，重新注册到Sub 线程池的线程上，用于处理I/O 的读写等操作，从而保证mainReactor只负责接入认证、握手等操作；\n\n## 5.TCP 粘包/拆包的原因及解决方法？\nTCP是以流的方式来处理数据，一个完整的包可能会被TCP拆分成多个包进行发送，也可能把小的封装成一个大的数据包发送。\n\nTCP粘包/分包的原因：\n\n* 应用程序写入的字节大小大于套接字发送缓冲区的大小，会发生拆包现象，而应用程序写入数据小于套接字缓冲区大小，网卡将应用多次写入的数据发送到网络上，这将会发生粘包现象；\n* 进行MSS大小的TCP分段，当TCP报文长度-TCP头部长度>MSS的时候将发生拆包\n* 以太网帧的payload（净荷）大于MTU（1500字节）进行ip分片。\n\n\n解决方法\n\n* 消息定长：FixedLengthFrameDecoder类\n* 包尾增加特殊字符分割：行分隔符类：LineBasedFrameDecoder或自定义分隔符类 ：DelimiterBasedFrameDecoder\n* 将消息分为消息头和消息体：LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。\n\n## 6.了解哪几种序列化协议？\n* 序列化（编码）是将对象序列化为二进制形式（字节数组），主要用于网络传输、数据持久化等；而反序列化（解码）则是将从网络、磁盘等读取的字节数组还原成原始对象，主要用于网络传输对象的解码，以便完成远程调用。\n\n* 影响序列化性能的关键因素：序列化后的码流大小（网络带宽的占用）、序列化的性能（CPU资源占用）；是否支持跨语言（异构系统的对接和开发语言切换）。\n\n* Java默认提供的序列化：无法跨语言、序列化后的码流太大、序列化的性能差\n\n* XML，优点：人机可读性好，可指定元素或特性的名称。缺点：序列化数据只包含数据本身以及类的结构，不包括类型标识和程序集信息；只能序列化公共属性和字段；不能序列化方法；文件庞大，文件格式复杂，传输占带宽。适用场景：当做配置文件存储数据，实时数据转换。\n\n* JSON，是一种轻量级的数据交换格式，优点：兼容性高、数据格式比较简单，易于读写、序列化后数据较小，可扩展性好，兼容性好、与XML相比，其协议比较简单，解析速度比较快。缺点：数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。适用场景（可替代ＸＭＬ）：跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小，实时性要求相对低（例如秒级别）的服务。\n\n\n* Fastjson，采用一种“假定有序快速匹配”的算法。优点：接口简单易用、目前java语言中最快的json库。缺点：过于注重快，而偏离了“标准”及功能性、代码质量不高，文档不全。适用场景：协议交互、Web输出、Android客户端\n\n* Thrift，不仅是序列化协议，还是一个RPC框架。优点：序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。缺点：使用者较少、跨防火墙访问时，不安全、不具有可读性，调试代码时相对困难、不能与其他传输层协议共同使用（例如HTTP）、无法支持向持久层直接读写数据，即不适合做数据持久化序列化协议。适用场景：分布式系统的RPC解决方案\n\n* Avro，Hadoop的一个子项目，解决了JSON的冗长和没有IDL的问题。优点：支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。缺点：对于习惯于静态类型语言的用户不直观。适用场景：在Hadoop中做Hive、Pig和MapReduce的持久化数据格式。\n\n\n* Protobuf，将数据结构以.proto文件进行描述，通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。优点：序列化后码流小，性能高、结构化数据存储格式（XML JSON等）、通过标识字段的顺序，可以实现协议的前向兼容、结构化的文档更容易管理和维护。缺点：需要依赖于工具生成代码、支持的语言相对较少，官方只支持Java 、C++ 、python。适用场景：对性能要求高的RPC调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化\n\n\n* 其它\n\n    * protostuff 基于protobuf协议，但不需要配置proto文件，直接导包即可\n    * Jboss marshaling 可以直接序列化java类， 无须实java.io.Serializable接口\n    * Message pack 一个高效的二进制序列化格式\n    * Hessian 采用二进制协议的轻量级remoting onhttp工具\n    * kryo 基于protobuf协议，只支持java语言,需要注册（Registration），然后序列化（Output），反序列化（Input）\n\n## 7.如何选择序列化协议？\n\n* 具体场景\n\n*     对于公司间的系统调用，如果性能要求在100ms以上的服务，基于XML的SOAP协议是一个值得考虑的方案。\n*     基于Web browser的Ajax，以及Mobile app与服务端之间的通讯，JSON协议是首选。对于性能要求不太高，或者以动态类型语言为主，或者传输数据载荷很小的运用场景，JSON也是非常不错的选择。\n*     对于调试环境比较恶劣的场景，采用JSON或XML能够极大的提高调试效率，降低系统开发成本。\n*     当对性能和简洁性有极高要求的场景，Protobuf，Thrift，Avro之间具有一定的竞争关系。\n*     对于T级别的数据的持久化应用场景，Protobuf和Avro是首要选择。如果持久化后的数据存储在hadoop子项目里，Avro会是更好的选择。\n*     对于持久层非Hadoop项目，以静态类型语言为主的应用场景，Protobuf会更符合静态类型语言工程师的开发习惯。由于Avro的设计理念偏向于动态类型语言，对于动态语言为主的应用场景，Avro是更好的选择。\n*     如果需要提供一个完整的RPC解决方案，Thrift是一个好的选择。\n*     如果序列化之后需要支持不同的传输层协议，或者需要跨防火墙访问的高性能场景，Protobuf可以优先考虑。\n\n* protobuf的数据类型有多种：bool、double、float、int32、int64、string、bytes、enum、message。protobuf的限定符：required: 必须赋值，不能为空、optional:字段可以赋值，也可以不赋值、repeated: 该字段可以重复任意次数（包括0次）、枚举；只能用指定的常量集中的一个值作为其值；\n\n* protobuf的基本规则：每个消息中必须至少留有一个required类型的字段、包含0个或多个optional类型的字段；repeated表示的字段可以包含0个或多个数据；[1,15]之内的标识号在编码的时候会占用一个字节（常用），[16,2047]之内的标识号则占用2个字节，标识号一定不能重复、使用消息类型，也可以将消息嵌套任意多层，可用嵌套消息类型来代替组。\n\n* protobuf的消息升级原则：不要更改任何已有的字段的数值标识；不能移除已经存在的required字段，optional和repeated类型的字段可以被移除，但要保留标号不能被重用。新添加的字段必须是optional或repeated。因为旧版本程序无法读取或写入新增的required限定符的字段。\n\n* 编译器为每一个消息类型生成了一个.java文件，以及一个特殊的Builder类（该类是用来创建消息类接口的）。如：UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build()；\n\n* Netty中的使用：ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类；ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的UserProto.java文件中的解码类；ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段，用于标志这个消息的长度的类；ProtobufEncoder 是编码类\n\n* 将StringBuilder转换为ByteBuf类型：copiedBuffer()方法\n\n## 8.Netty的零拷贝实现\n\n* Netty的接收和发送ByteBuffer采用DIRECT BUFFERS，使用堆外直接内存进行Socket读写，不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝，JVM会将堆内存Buffer拷贝一份到直接内存中，然后才写入Socket中。ByteBuffer由ChannelConfig分配，而ChannelConfig创建ByteBufAllocator默认使用Direct Buffer\n\n* CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。addComponents方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体\n\n* 通过 FileRegion 包装的FileChannel.tranferTo方法 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel，避免了传统通过循环write方式导致的内存拷贝问题。\n\n* 通过 wrap方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。\n\n* Selector BUG：若Selector的轮询结果为空，也没有wakeup或新消息处理，则发生空轮询，CPU使用率100%，\n\n* Netty的解决办法：对Selector的select操作周期进行统计，每完成一次空的select操作进行一次计数，若在某个周期内连续发生N次空轮询，则触发了epoll死循环bug。重建Selector，判断是否是其他线程发起的重建请求，若不是则将原SocketChannel从旧的Selector上去除注册，重新注册到新的Selector上，并将原来的Selector关闭。\n\n## 9.Netty的高性能表现在哪些方面？\n\n* 心跳，对服务端：会定时清除闲置会话inactive(netty5)，对客户端:用来检测会话是否断开，是否重来，检测网络延迟，其中idleStateHandler类 用来检测会话状态\n\n* 串行无锁化设计，即消息的处理尽可能在同一个线程内完成，期间不进行线程切换，这样就避免了多线程竞争和同步锁。表面上看，串行化设计似乎CPU利用率不高，并发程度不够。但是，通过调整NIO线程池的线程参数，可以同时启动多个串行化的线程并行运行，这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。\n\n* 可靠性，链路有效性检测：链路空闲检测机制，读/写空闲超时机制；内存保护机制：通过内存池重用ByteBuf;ByteBuf的解码保护；优雅停机：不再接收新消息、退出前的预处理操作、资源的释放操作。\n\n* Netty安全性：支持的安全协议：SSL V2和V3，TLS，SSL单向认证、双向认证和第三方CA认证。\n\n* 高效并发编程的体现：volatile的大量、正确使用；CAS和原子类的广泛使用；线程安全容器的使用；通过读写锁提升并发性能。IO通信性能三原则：传输（AIO）、协议（Http）、线程（主从多线程）\n\n* 流量整型的作用（变压器）：防止由于上下游网元性能不均衡导致下游网元被压垮，业务流中断；防止由于通信模块接受消息过快，后端业务线程处理不及时导致撑死问题。\n\n* TCP参数配置：SO_RCVBUF和SO_SNDBUF：通常建议值为128K或者256K；SO_TCPNODELAY：NAGLE算法通过将缓冲区内的小封包自动相连，组成较大的封包，阻止大量小封包的发送阻塞网络，从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法；\n\n## 10.NIOEventLoopGroup源码\n\n![9a1e7b6b029777b40e9c8213d50be95c](关于Netty我们都需要知道什么.resources/95F6A224-61C4-48EC-90D4-A4681751B545.png)\n\n* NioEventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池，初始化EventExecutor时NioEventLoopGroup重载newChild方法，所以children元素的实际类型为NioEventLoop。\n\n* 线程启动时调用SingleThreadEventExecutor的构造方法，执行NioEventLoop类的run方法，首先会调用hasTasks()方法判断当前taskQueue是否有元素。如果taskQueue中有元素，执行 selectNow() 方法，最终执行selector.selectNow()，该方法会立即返回。如果taskQueue没有元素，执行 select(oldWakenUp) 方法\n\n* select ( oldWakenUp) 方法解决了 Nio 中的 bug，selectCnt 用来记录selector.select方法的执行次数和标识是否执行过selector.selectNow()，若触发了epoll的空轮询bug，则会反复执行selector.select(timeoutMillis)，变量selectCnt 会逐渐变大，当selectCnt 达到阈值（默认512），则执行rebuildSelector方法，进行selector重建，解决cpu占用100%的bug。\n\n* rebuildSelector方法先通过openSelector方法创建一个新的selector。然后将old selector的selectionKey执行cancel。最后将old selector的channel重新注册到新的selector中。rebuild后，需要重新执行方法selectNow，检查是否有已ready的selectionKey。\n\n* 接下来调用processSelectedKeys 方法（处理I/O任务），当selectedKeys != null时，调用processSelectedKeysOptimized方法，迭代 selectedKeys 获取就绪的 IO 事件的selectkey存放在数组selectedKeys中, 然后为每个事件都调用 processSelectedKey 来处理它，processSelectedKey 中分别处理OP_READ；OP_WRITE；OP_CONNECT事件。\n\n* 最后调用runAllTasks方法（非IO任务），该方法首先会调用fetchFromScheduledTaskQueue方法，把scheduledTaskQueue中已经超过延迟执行时间的任务移到taskQueue中等待被执行，然后依次从taskQueue中取任务执行，每执行64个任务，进行耗时检查，如果已执行时间超过预先设定的执行时间，则停止执行非IO任务，避免非IO任务太多，影响IO任务的执行。\n\n* 每个NioEventLoop对应一个线程和一个Selector，NioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上，NioEventLoop负责事件轮询。\n\n* Outbound 事件都是请求事件, 发起者是 Channel，处理者是 unsafe，通过 Outbound 事件进行通知，传播方向是 tail到head。Inbound 事件发起者是 unsafe，事件的处理者是 Channel, 是通知事件，传播方向是从头到尾。\n\n* 内存管理机制，首先会预申请一大块内存Arena，Arena由许多Chunk组成，而每个Chunk默认由2048个page组成。Chunk通过AVL树的形式组织Page，每个叶子节点表示一个Page，而中间节点表示内存区域，节点自己记录它在整个Arena中的偏移地址。当区域被分配出去后，中间节点上的标记位会被标记，这样就表示这个中间节点以下的所有节点都已被分配了。大于8k的内存分配在poolChunkList中，而PoolSubpage用于分配小于8k的内存，它会把一个page分割成多段，进行内存分配。\n\n* ByteBuf的特点：支持自动扩容（4M），保证put方法不会抛出异常、通过内置的复合缓冲类型，实现零拷贝（zero-copy）；不需要调用flip()来切换读/写模式，读取和写入索引分开；方法链；引用计数基于AtomicIntegerFieldUpdater用于内存回收；PooledByteBuf采用二叉树来实现一个内存池，集中管理内存的分配和释放，不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。\n"
  },
  {
    "path": "README.md",
    "content": "\n<br/>\n<div align=\"center\">\n    <a href=\"https://mp.weixin.qq.com/s/0N4XSMFPuD7U_paGsBsblw\" style=\"text-decoration:none\"><img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/logo.jpg\" width=\"256px\"></a>\n</div>\n<br/>\n\n\n- 🔥 关注[**❤️公众号**](https://mmbiz.qpic.cn/mmbiz_png/UdK9ByfMT2P8ylc0r5wg2SEHk9LxuL6xRMiczG82lZNwzVmm6otcpd26yZIDqOvbTIeNb2FdYZYatHvsoH6TbVg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)\n\n- 🔥 加我 [**👬好友**](https://mmbiz.qpic.cn/mmbiz_png/UdK9ByfMT2NLwWPfib27wK2aK6iaCicBu4VseYibyGQdiagT6zcMSTXbITlV5Wl3lq8T3icMt4ibtjGat0j4dqSnqf43A/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)\n\n- 🔥 关注 [**📺B站**](https://space.bilibili.com/22021870)\n\n\n## 🚜 **大数据成神之路目录**\n\n### ⛳ 上车前必读\n\n- [❤️❤️全网最全大数据面试通关手册!❤️❤️](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n- 图片打不开，点[这里](https://blog.csdn.net/u013411339/article/details/113097759)\n- 👉 如果你不知道自己要从哪开始、要学什么，请看：[**《八千里路云和月|从零到大数据专家学习路径指南》**](https://mp.weixin.qq.com/s/MAwD-UJgvIa_dZjmaykqrQ)\n这是一个大数据专家/架构师提升自身技术栈广度和深度的经验之路，也是我的亲身学习经历汇总！\n- 👉 如果你对某一个框架的学习路径有疑惑🤔，请看下面：\n    - [我们在学习Flink的时候，到底在学习什么?](https://mp.weixin.qq.com/s/xh4SEX9t-fRVdoiAl0KKSQ)\n    - [我们在学习Spark的时候，到底在学习什么？](https://mp.weixin.qq.com/s/pN0AqNJuFnlLjNW2OonGtA)\n    - [我们在学习Kafka的时候，到底在学习什么？](https://mp.weixin.qq.com/s/AurKLpHB0XWgbBat-M7Urw)\n\n------------\n\n### ⛳ 大数据开发基础篇\n| :ski:Java基础| :memo:NIO|:book:并发|:guitar:JVM|:dollar:分布式|:floppy_disk:Zookeeper|:punch:RPC|:art:Netty|:computer:Linux|\n| :------:| :------: | :------: |:------: |:------: |:------: |:------: |:------: |:------:|\n| [Java基础](#一Java基础) | [NIO](#二NIO基础) | [并发容器](#三Java并发容器) |[JVM](#四JVM深度解析和面试点) |[分布式](#五分布式理论基础和原理) |[zookeeper](#六大数据框架开发基础-zookeeper)|[RPC](#七大数据框架开发基础-RPC)|[Netty](#八大数据框架基石之网路通信-Netty)|[Linux](/Linux基础/Linux基础和命令.md)|\n\n------------\n### ⛳ 大数据框架学习篇\n\n<table>\n    <tr>\n      <th><img width=\"50px\" src=\"pictures/hadoop.jpg\"></th>\n      <th><img width=\"50px\" src=\"pictures/hive.jpg\"></th>\n      <th><img width=\"50px\" src=\"pictures/spark.jpg\"></th>\n      <th><img width=\"50px\" src=\"pictures/flink.png\"></th>\n      <th><img width=\"50px\" src=\"pictures/hbase.png\"></th>\n      <th><img width=\"50px\" src=\"pictures/kafka.png\"></th>\n      <th><img width=\"50px\" src=\"pictures/zookeeper.jpg\"></th>\n    </tr>\n    <tr>\n      <td align=\"center\"><a href=\"#一hadoop\">Hadoop</a></td>\n      <td align=\"center\"><a href=\"#二hive\">Hive</a></td>\n      <td align=\"center\"><a href=\"#三spark\">Spark</a></td>\n      <td align=\"center\"><a href=\"#四flink\">Flink</a></td>\n      <td align=\"center\"><a href=\"#五hbase\">HBase</a></td>\n      <td align=\"center\"><a href=\"#六kafka\">Kafka</a></td>\n      <td align=\"center\"><a href=\"#七zookeeper\">Zookeeper</a></td>\n    </tr>\n  </table>\n<br/>\n\n----------------\n### ⛳ 大数据开发实战进阶篇\n\n这里的文章主要是我平时发表在公众号，博客等的文章，精心挑选，以飨读者。\n\n<table>\n    <tr>\n      <th><img width=\"50px\" src=\"pictures/flink.png\"></th>\n      <th><img width=\"50px\" src=\"pictures/spark.jpg\"></th>\n      <th><img width=\"50px\" src=\"pictures/kafka.png\"></th>\n      <th><img width=\"50px\" src=\"pictures/olap.jpg\"></th>\n    </tr>\n    <tr>\n      <td align=\"center\"><a href=\"#Flink实战合集\">Flink实战进阶</a></td>\n      <td align=\"center\"><a href=\"#Spark实战合集\">Spark实战进阶</a></td>\n      <td align=\"center\"><a href=\"#Kafka实战合集\">Kafka实战进阶</a></td>\n      <td align=\"center\"><a href=\"#数据仓库实战合集\">OLAP实战进阶</a></td>\n    </tr>\n  </table>\n<br/>\n\n------------------------------------\n### ⛳ 大数据开发面试篇\n\n<table>\n    <tr>\n      <th><img width=\"50px\" src=\"pictures/olap.jpg\"></th>\n      <th><img width=\"50px\" src=\"pictures/olap.jpg\"></th>\n    </tr>\n    <tr>\n      <td align=\"center\"><a href=\"#面试系列合集\">面试系列合集</a></td>\n      <td align=\"center\"><a href=\"#大数据算法\">大数据算法</a></td>\n    </tr>\n  </table>\n<br/> \n\n------------------------------------\n### ⛳ 个人公众号:octocat: 大数据精品文章合集\n\n<table>\n    <tr>\n      <th><img width=\"50px\" src=\"pictures/bg.jpg\"></th>\n      <th><img width=\"50px\" src=\"pictures/bg.jpg\"></th>\n      <th><img width=\"50px\" src=\"pictures/bg.jpg\"></th>\n    </tr>\n    <tr>\n      <td align=\"center\"><a href=\"#2020精品文章合集\">2020精品文章合集</a></td>\n      <td align=\"center\"><a href=\"#2021精品文章合集\">2021精品文章合集</a></td>\n      <td align=\"center\"><a href=\"#硬刚系列文章合集\">硬刚系列文章合集</a></td>\n    </tr>\n  </table>\n<br/> \n\n------------------------------------\n### ⛳ 高屋建瓴总结篇\n\n- [Hadoop系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n- [Hive系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n- [Spark系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n- [Flink系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n- [Hbase系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n- [Kafka系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n\n---------------------------------------\n\n## `第一部分: 大数据开发基础篇`\n\n### 一、Java基础\n\n##### 📚 1.1 Java系统性学习\n\n- [《Java基础系统性学习专栏》](https://blog.csdn.net/u013411339/category_11681255.html)\n\n##### 分类导航\n\n* [大数据成神之路-Java高级特性增强(多线程)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(%E5%A4%9A%E7%BA%BF%E7%A8%8B).md)\n* [大数据成神之路-Java高级特性增强(Synchronized关键字)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(Synchronized%E5%85%B3%E9%94%AE%E5%AD%97).md)\n* [大数据成神之路-Java高级特性增强(volatile关键字)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(volatile%E5%85%B3%E9%94%AE%E5%AD%97).md)\n* [大数据成神之路-Java高级特性增强(锁)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(%E9%94%81).md)\n* [大数据成神之路-Java高级特性增强(ArrayList/Vector)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6).md)\n* [大数据成神之路-Java高级特性增强(LinkedList)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA.md)\n* [大数据成神之路-Java高级特性增强(HashMap)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(HashMap).md)\n* [大数据成神之路-Java高级特性增强(HashSet)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(HashSet).md)\n* [大数据成神之路-Java高级特性增强(LinkedHashMap)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(LinkedHashMap).md)\n\n### 二、NIO基础\n\n##### 📚 2.1 Netty系统系学习\n\n- [《Netty系统系学习专栏》](https://blog.csdn.net/u013411339/category_11681261.html)\n\n##### 📚 2.2 分类导航\n\n * [大数据成神之路-Java高级特性增强-NIO大纲](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA-NIO.md)\n * [NIO概览](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/NIO%E6%A6%82%E8%A7%88.md)\n * [Java NIO之Buffer(缓冲区)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/Java%20NIO%E4%B9%8BBuffer(%E7%BC%93%E5%86%B2%E5%8C%BA).md)\n * [Java NIO之Channel(通道)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/Java%20NIO%E4%B9%8BChannel(%E9%80%9A%E9%81%93).md)\n * [ava NIO之Selector(选择器)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/Java%20NIO%E4%B9%8BSelector(%E9%80%89%E6%8B%A9%E5%99%A8).md)\n * [Java NIO之拥抱Path和Files](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA/Java%20NIO%E4%B9%8B%E6%8B%A5%E6%8A%B1Path%E5%92%8CFiles.md)\n\n### 三、Java并发容器\n\n##### 📚 3.1 系统性学习\n\n- [《多线程&并发容器学习专栏》](https://blog.csdn.net/u013411339/category_11681268.html)\n\n##### 3.2 分类导航\n\n * [大数据成神之路-Java高级特性增强(并发容器大纲)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8%E5%A4%A7%E7%BA%B2).md)\n * [大数据成神之路-Java高级特性增强(LinkedBlockingQueue)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(LinkedBlockingQueue).md)\n * [大数据成神之路-Java高级特性增强(LinkedBlockingDeque)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(LinkedBlockingDeque).md)\n * [大数据成神之路-Java高级特性增强(CopyOnWriteArraySet)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(CopyOnWriteArraySet).md)\n * [大数据成神之路-Java高级特性增强(CopyOnWriteArrayList)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(CopyOnWriteArrayList).md)\n * [大数据成神之路-Java高级特性增强(ConcurrentSkipListSet)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(ConcurrentSkipListSet).md)\n * [大数据成神之路-Java高级特性增强(ConcurrentSkipListMap)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(ConcurrentSkipListMap).md)\n * [大数据成神之路-Java高级特性增强(ConcurrentLinkedQueue)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(ConcurrentLinkedQueue).md)\n * [大数据成神之路-Java高级特性增强(ConcurrentHashMap)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(ConcurrentHashMap).md)\n * [大数据成神之路-Java高级特性增强(ArrayBlockingQueue)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%88%90%E7%A5%9E%E4%B9%8B%E8%B7%AF-Java%E9%AB%98%E7%BA%A7%E7%89%B9%E6%80%A7%E5%A2%9E%E5%BC%BA(ArrayBlockingQueue).md)\n\n### 四、JVM深度解析和面试点\n\n##### 📚 4.1 系统性学习\n\n- [《JVM学习专栏》](https://blog.csdn.net/u013411339/category_11681275.html)\n\n##### 4.2 分类导航\n\n* [JVM内存结构](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/JVM%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84.md)\n* [HotSpot虚拟机对象探秘](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/HotSpot%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AF%B9%E8%B1%A1%E6%8E%A2%E7%A7%98.md)\n* [垃圾收集策略与算法](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E7%AD%96%E7%95%A5%E4%B8%8E%E7%AE%97%E6%B3%95.md)\n* [HotSpot垃圾收集器](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/HotSpot%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md)\n* [内存分配与回收策略](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8E%E5%9B%9E%E6%94%B6%E7%AD%96%E7%95%A5.md)\n* [JVM性能调优](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/JVM%20%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98.md)\n* [类文件结构](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/%E7%B1%BB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84.md)\n* [类加载的时机](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E6%97%B6%E6%9C%BA.md)\n* [类加载的过程](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E8%BF%87%E7%A8%8B.md)\n* [类加载器](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8.md)\n\n##### 4.3 JVM 详解\n\n* [java类的加载机制](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%B8%80)java%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6.md)\n* [JVM内存结构](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%8C)JVM%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84.md)\n* [GC算法 垃圾收集器](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%B8%89)GC%E7%AE%97%E6%B3%95%20%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md)\n* [jvm调优-命令大全](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E5%9B%9B)jvm%E8%B0%83%E4%BC%98-%E5%91%BD%E4%BB%A4%E5%A4%A7%E5%85%A8%EF%BC%88jps%20jstat%20jmap%20jhat%20jstack%20jinfo%EF%BC%89.md)\n* [Java GC 分析](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%BA%94)Java%20GC%20%E5%88%86%E6%9E%90.md)\n\n### 五、分布式理论基础和原理\n\n* [分布式系统的一些基本概念](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%9A%84%E4%B8%80%E4%BA%9B%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5.md)\n* [分布式系统理论基础一： 一致性、2PC和3PC](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%B8%80%EF%BC%9A%20%E4%B8%80%E8%87%B4%E6%80%A7%E3%80%812PC%E5%92%8C3PC.md)\n* [分布式系统理论基础二-CAP](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%BA%8C-CAP.md)\n* [分布式系统理论基础三-时间、时钟和事件顺序](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%B8%89-%E6%97%B6%E9%97%B4%E3%80%81%E6%97%B6%E9%92%9F%E5%92%8C%E4%BA%8B%E4%BB%B6%E9%A1%BA%E5%BA%8F.md)\n* [分布式系统理论进阶 - Paxos](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E8%BF%9B%E9%98%B6%20-%20Paxos.md)\n* [分布式系统理论进阶 - Raft、Zab](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E8%BF%9B%E9%98%B6%20-%20Raft%E3%80%81Zab.md)\n* [分布式系统理论进阶：选举、多数派和租约](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E8%BF%9B%E9%98%B6%EF%BC%9A%E9%80%89%E4%B8%BE%E3%80%81%E5%A4%9A%E6%95%B0%E6%B4%BE%E5%92%8C%E7%A7%9F%E7%BA%A6.md)\n* [分布式锁的解决方案](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88.md)\n* [分布式锁的解决方案(二)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88(%E4%BA%8C).md)\n* [分布式事务的解决方案](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88.md)\n* [分布式ID生成器解决方案](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8FID%E7%94%9F%E6%88%90%E5%99%A8%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88.md)\n\n### 六、大数据框架开发基础-Zookeeper\n\n* [安装和运行](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/zookeeeper/zk%E5%AE%89%E8%A3%85%E5%92%8C%E8%BF%90%E8%A1%8C.md)\n* [zookeeper服务](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/zookeeeper/zk%E6%9C%8D%E5%8A%A1.md)\n* [zookeeper应用程序](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/zookeeeper/ZooKeeper%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F.md)\n* [zookeeper开发实例](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/zookeeeper/zk%E5%BC%80%E5%8F%91%E5%AE%9E%E4%BE%8B.md)\n* [zookeeper集群构建](http://www.importnew.com/23237.html)\n\n### 七、大数据框架开发基础-RPC\n\n* [RPC简单介绍](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/RPC/RPC%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D.md)\n* [RPC的原理和框架](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/RPC/RPC%E7%9A%84%E5%8E%9F%E7%90%86%E5%92%8C%E6%A1%86%E6%9E%B6.md)\n* [手把手教你实现一个简单的RPC](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/RPC/%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84RPC.md)\n\n### 八、大数据框架基石之网路通信-Netty\n\n* [关于Netty我们都需要知道什么](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/%E5%85%B3%E4%BA%8ENetty%E6%88%91%E4%BB%AC%E9%83%BD%E9%9C%80%E8%A6%81%E7%9F%A5%E9%81%93%E4%BB%80%E4%B9%88.md)\n* [Netty源码解析-概述篇](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E6%A6%82%E8%BF%B0%E7%AF%87.md)\n* [Netty源码解析1-Buffer](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%901-Buffer.md) \n* [Netty源码解析2-Reactor](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%902-Reactor.md)\n* [Netty源码解析3-Pipeline](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%903-Pipeline.md)\n* [Netty源码解析4-Handler综述](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%904-Handler%E7%BB%BC%E8%BF%B0.md)\n* [Netty源码解析5-ChannelHandler](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%905-ChannelHandler.md)\n* [Netty源码解析6-ChannelHandler实例之LoggingHandler](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%906-ChannelHandler%E5%AE%9E%E4%BE%8B%E4%B9%8BLoggingHandler.md)\n* [Netty源码解析7-ChannelHandler实例之TimeoutHandler](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%907-ChannelHandler%E5%AE%9E%E4%BE%8B%E4%B9%8BTimeoutHandler.md)\n* [Netty源码解析8-ChannelHandler实例之CodecHandler](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%908-ChannelHandler%E5%AE%9E%E4%BE%8B%E4%B9%8BCodecHandler.md)\n* [Netty源码解析9-ChannelHandler实例之MessageToByteEncoder](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Netty/Netty%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%909-ChannelHandler%E5%AE%9E%E4%BE%8B%E4%B9%8BMessageToByteEncoder.md)\n\n\n## 第二部分:大数据框架学习篇\n\n### 一、Hadoop\n\n##### 📚 1.1 Hadoop系统性总结\n\n- [Hadoop系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n\n##### 📚 1.2 系统性学习\n\n[Hadoop学习专栏](https://blog.csdn.net/u013411339/category_11681229.html)\n\n##### 1.3 分类导航\n\n1. [分布式文件存储系统 —— HDFS](大数据框架学习/Hadoop-HDFS.md)\n2. [分布式计算框架 —— MapReduce](大数据框架学习/Hadoop-MapReduce.md)\n3. [集群资源管理器 —— YARN](大数据框架学习/Hadoop-YARN.md)\n4. [Hadoop 单机伪集群环境搭建](大数据框架学习/installation/Hadoop单机环境搭建.md)\n5. [Hadoop 集群环境搭建](大数据框架学习/installation/Hadoop集群环境搭建.md)\n6. [HDFS 常用 Shell 命令](大数据框架学习/HDFS常用Shell命令.md)\n7. [HDFS Java API 的使用](大数据框架学习/HDFS-Java-API.md)\n8. [基于 Zookeeper 搭建 Hadoop 高可用集群](大数据框架学习/installation/基于Zookeeper搭建Hadoop高可用集群.md)\n9. [Hadoop级简入门](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Hadoop/Hadoop%E6%9E%81%E7%AE%80%E5%85%A5%E9%97%A8.md)\n10. [MapReduce编程模型和计算框架架构原理](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Hadoop/MapReduce%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B%E5%92%8C%E8%AE%A1%E7%AE%97%E6%A1%86%E6%9E%B6%E6%9E%B6%E6%9E%84%E5%8E%9F%E7%90%86.md)\n\n### 二、Hive\n\n##### 📚 2.1 Hive系统性总结\n\n[Hive系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n\n##### 📚 2.2 系统性学习\n\n[Hive学习专栏](https://blog.csdn.net/u013411339/category_11681281.html)\n\n##### 📚 2.3 分类导航\n\n1. [Hive 简介及核心概念](大数据框架学习/Hive简介及核心概念.md)\n2. [Linux 环境下 Hive 的安装部署](大数据框架学习/installation/Linux环境下Hive的安装部署.md)\n4. [Hive CLI 和 Beeline 命令行的基本使用](大数据框架学习/HiveCLI和Beeline命令行的基本使用.md)\n6. [Hive 常用 DDL 操作](大数据框架学习/Hive常用DDL操作.md)\n7. [Hive 分区表和分桶表](大数据框架学习/Hive分区表和分桶表.md)\n8. [Hive 视图和索引](大数据框架学习/Hive视图和索引.md)\n9. [Hive常用 DML 操作](大数据框架学习/Hive常用DML操作.md)\n10. [Hive 数据查询详解](大数据框架学习/Hive数据查询详解.md)\n\n### 三、Spark\n\n##### 📚 3.1 Spark系统性总结\n\n- [Spark系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n\n####  📚 3.2 系统性学习\n\n- [Spark学习专栏](https://blog.csdn.net/u013411339/category_11681283.html)\n\n**Spark Core :**\n\n1. [Spark 简介](大数据框架学习/Spark简介.md)\n2. [Spark 开发环境搭建](大数据框架学习/installation/Spark开发环境搭建.md)\n4. [弹性式数据集 RDD](大数据框架学习/Spark_RDD.md)\n5. [RDD 常用算子详解](大数据框架学习/Spark_Transformation和Action算子.md)\n5. [Spark 运行模式与作业提交](大数据框架学习/Spark部署模式与作业提交.md)\n6. [Spark 累加器与广播变量](大数据框架学习/Spark累加器与广播变量.md)\n7. [基于 Zookeeper 搭建 Spark 高可用集群](大数据框架学习/installation/Spark集群环境搭建.md)\n\n**Spark SQL :**\n\n1. [DateFrame 和 DataSet ](大数据框架学习/SparkSQL_Dataset和DataFrame简介.md)\n2. [Structured API 的基本使用](大数据框架学习/Spark_Structured_API的基本使用.md)\n3. [Spark SQL 外部数据源](大数据框架学习/SparkSQL外部数据源.md)\n4. [Spark SQL 常用聚合函数](大数据框架学习/SparkSQL常用聚合函数.md)\n5. [Spark SQL JOIN 操作](大数据框架学习/SparkSQL联结操作.md)\n\n**Spark Streaming ：**\n\n1. [Spark Streaming 简介](大数据框架学习/Spark_Streaming与流处理.md)\n2. [Spark Streaming 基本操作](大数据框架学习/Spark_Streaming基本操作.md)\n3. [Spark Streaming 整合 Flume](大数据框架学习/Spark_Streaming整合Flume.md)\n4. [Spark Streaming 整合 Kafka](大数据框架学习/Spark_Streaming整合Kafka.md)\n\n\n## 四、Flink\n\n##### 📚 4.1 Flink系统性总结\n\n- [Flink系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n\n#### 📚 4.2 系统性学习\n\n- [Flink系统学习专栏](https://blog.csdn.net/u013411339/category_11681289.html)\n\n#### 📚 2.3 分类导航\n\n1. [Flink 核心概念综述](大数据框架学习/Flink核心概念综述.md)\n2. [Flink 开发环境搭建](大数据框架学习/Flink开发环境搭建.md)\n3. [Flink Data Source](大数据框架学习/Flink_Data_Source.md)\n4. [Flink Data Transformation](大数据框架学习/Flink_Data_Transformation.md)\n4. [Flink Data Sink](大数据框架学习/Flink_Data_Sink.md)\n6. [Flink 窗口模型](大数据框架学习/Flink_Windows.md)\n7. [Flink 状态管理与检查点机制](大数据框架学习/Flink状态管理与检查点机制.md)\n8. [Flink Standalone 集群部署](大数据框架学习/installation/Flink_Standalone_Cluster.md)\n\n#### Flink当前最火的实时计算引擎-入门篇\n\n* [Flink从入门到放弃(入门篇1)-Flink是什么](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/Flink%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83(%E5%85%A5%E9%97%A8%E7%AF%871)-Flink%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.md)\n* [Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/Flink%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83(%E5%85%A5%E9%97%A8%E7%AF%872)-%E6%9C%AC%E5%9C%B0%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%26%E6%9E%84%E5%BB%BA%E7%AC%AC%E4%B8%80%E4%B8%AAFlink%E5%BA%94%E7%94%A8.md)\n* [Flink从入门到放弃(入门篇3)-DataSetAPI](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/Flink%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83(%E5%85%A5%E9%97%A8%E7%AF%873)-DataSetAPI.md)\n* [Flink从入门到放弃(入门篇4)-DataStreamAPI](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/Flink%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83(%E5%85%A5%E9%97%A8%E7%AF%874)-DataStreamAPI.md)\n* [Flink集群部署](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/Flink%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.md)\n* [Flink重启策略](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/6-Flink%E9%87%8D%E5%90%AF%E7%AD%96%E7%95%A5.md)\n* [Flink的分布式缓存](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/7-Flink%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98.md)\n* [Flink中的窗口](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/8-Flink%E4%B8%AD%E7%9A%84%E7%AA%97%E5%8F%A3.md)\n* [Flink中的Time](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/9-Flink%E4%B8%AD%E7%9A%84Time.md)\n* [Flink集群搭建的HA](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/10-Flink%E9%9B%86%E7%BE%A4%E7%9A%84%E9%AB%98%E5%8F%AF%E7%94%A8(%E6%90%AD%E5%BB%BA%E7%AF%87%E8%A1%A5%E5%85%85).md)\n* [Flink中的时间戳和水印](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/11-%E6%97%B6%E9%97%B4%E6%88%B3%E5%92%8C%E6%B0%B4%E5%8D%B0.md)\n* [Flink广播变量](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/12-Broadcast%E5%B9%BF%E6%92%AD%E5%8F%98%E9%87%8F.md)\n* [Flink-Kafka-Connector](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/13-Flink-Kafka-Connector.md)\n* [Flink-Table-&-SQL实战](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/14-Flink-Table-%26-SQL.md)\n* [15-Flink实战项目之实时热销排行](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/15-Flink%E5%AE%9E%E6%88%98%E9%A1%B9%E7%9B%AE%E4%B9%8B%E5%AE%9E%E6%97%B6%E7%83%AD%E9%94%80%E6%8E%92%E8%A1%8C.md)\n* [16-Flink-Redis-Sink](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/16-Flink-Redis-Sink.md)\n* [17-Flink消费Kafka写入Mysql](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink/17-Flink%E6%B6%88%E8%B4%B9Kafka%E5%86%99%E5%85%A5Mysql.md)\n\n#### Flink当前最火的实时计算引擎-放弃篇\n\n* [Flink漫谈系列1-概述](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink%E6%BC%AB%E8%B0%88%E7%B3%BB%E5%88%97/Apache-Flink%E6%BC%AB%E8%B0%88%E7%B3%BB%E5%88%97(1)-%E6%A6%82%E8%BF%B0.md)\n* [Flink漫谈系列2-watermark](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink%E6%BC%AB%E8%B0%88%E7%B3%BB%E5%88%97/Apache-Flink-%E6%BC%AB%E8%B0%88%E7%B3%BB%E5%88%97(02)-Watermark.md)\n* [Flink漫谈系列3-state](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Flink%E6%BC%AB%E8%B0%88%E7%B3%BB%E5%88%97/Apache-Flink-%E6%BC%AB%E8%B0%88%E7%B3%BB%E5%88%97(03)-State.md)\n\n## 五、HBase\n\n##### 📚 5.1 Hbase系统性总结\n\n- [Hbase系统性总结(知识星球读者专享)](https://mp.weixin.qq.com/s/HJMolm5vMDbGWmprrxkzvA)\n\n##### 📚 5.2 系统性学习\n\n- [Hbase学习专栏](https://blog.csdn.net/u013411339/category_11681293.html)\n\n##### 📚 5.3 分类导航\n\n1. [Hbase 简介](大数据框架学习/Hbase简介.md)\n2. [HBase 系统架构及数据结构](大数据框架学习/Hbase系统架构及数据结构.md)\n3. [HBase 基本环境搭建 (Standalone /pseudo-distributed mode)](大数据框架学习/installation/HBase单机环境搭建.md)\n4. [HBase 集群环境搭建](大数据框架学习/installation/HBase集群环境搭建.md)\n5. [HBase 常用 Shell 命令](大数据框架学习/Hbase_Shell.md)\n6. [HBase Java API](大数据框架学习/Hbase_Java_API.md)\n7. [Hbase 过滤器详解](大数据框架学习/Hbase过滤器详解.md)\n8. [HBase 协处理器详解](大数据框架学习/Hbase协处理器详解.md)\n9. [HBase 容灾与备份](大数据框架学习/Hbase容灾与备份.md)\n10. [HBase的 SQL 中间层 —— Phoenix](大数据框架学习/Hbase的SQL中间层_Phoenix.md)\n11. [Spring/Spring Boot 整合 Mybatis + Phoenix](大数据框架学习/Spring+Mybtais+Phoenix整合.md)\n\n## 六、Kafka\n\n##### 📚 6.1 Kafka系统性总结\n\n[Kafka系统性总结](https://t.zsxq.com/eIMrjmE)\n\n##### 📚 6.2 Kafka系统性学习\n\n- [Kafka系统性学习专栏](https://blog.csdn.net/u013411339/category_11681300.html)\n\n**Kafka基本原理 ：**\n\n1. [Kafka 简介](大数据框架学习/Kafka简介.md)\n2. [基于 Zookeeper 搭建 Kafka 高可用集群](大数据框架学习/installation/基于Zookeeper搭建Kafka高可用集群.md)\n3. [Kafka 生产者详解](大数据框架学习/Kafka生产者详解.md)\n4. [Kafka 消费者详解](大数据框架学习/Kafka消费者详解.md)\n5. [深入理解 Kafka 副本机制](大数据框架学习/Kafka深入理解分区副本机制.md)\n\n**分布式消息队列Kafka原理及与流式计算的集成 ：**\n\n 1. [Apache-Kafka简介](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E7%AE%80%E4%BB%8B.md)\n 2. [Apache-Kafka核心概念](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5.md)\n 3. [Apache-Kafka安装和使用](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E5%AE%89%E8%A3%85%E5%92%8C%E4%BD%BF%E7%94%A8.md)\n 4. [Apache-Kafka编程实战](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98.md)\n 5. [Apache-Kafka核心组件和流程(副本管理器)](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6%E5%92%8C%E6%B5%81%E7%A8%8B(%E5%89%AF%E6%9C%AC%E7%AE%A1%E7%90%86%E5%99%A8).md)\n 6. [Apache-Kafka核心组件和流程-协调器](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6%E5%92%8C%E6%B5%81%E7%A8%8B-%E5%8D%8F%E8%B0%83%E5%99%A8.md)\n 7. [Apache-Kafka核心组件和流程-控制器](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6%E5%92%8C%E6%B5%81%E7%A8%8B-%E6%8E%A7%E5%88%B6%E5%99%A8.md)\n 8. [Apache-Kafka核心组件和流程-日志管理器](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/Kafka/Apache-Kafka%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6%E5%92%8C%E6%B5%81%E7%A8%8B-%E6%97%A5%E5%BF%97%E7%AE%A1%E7%90%86%E5%99%A8.md)\n\n\n## 七、Zookeeper\n\n1. [Zookeeper 简介及核心概念](大数据框架学习/Zookeeper简介及核心概念.md)\n2. [Zookeeper 单机环境和集群环境搭建](大数据框架学习/installation/Zookeeper单机环境和集群环境搭建.md) \n3. [Zookeeper 常用 Shell 命令](大数据框架学习/Zookeeper常用Shell命令.md)\n4. [Zookeeper Java 客户端 —— Apache Curator](大数据框架学习/Zookeeper_Java客户端Curator.md)\n5. [Zookeeper  ACL 权限控制](大数据框架学习/Zookeeper_ACL权限控制.md)\n\n## 八、大数据算法\n\n#### 大数据算法\n\n[大数据算法](https://blog.csdn.net/u013411339/article/details/113429172)\n\n## 第三部分:大数据开发实战进阶篇\n\n### 一、Flink实战进阶文章合集\n\n#### Flink实战合集\n\n[点我查看Flink实战合集](实战系列文章/Flink实战.md)\n\n#### 系统性学习\n\n- [Flink系统学习专栏](https://blog.csdn.net/u013411339/category_11681289.html)\n- [Flink高级进阶和企业级应用](https://blog.csdn.net/u013411339/category_9285579.html)\n\n1. [菜鸟供应链实时技术架构演进](https://mp.weixin.qq.com/s/fnx2GnbCWNcaptVPsSp7dw)\n2. [趣头条实战-基于Flink+ClickHouse构建实时数据平台](https://mp.weixin.qq.com/s/s6YFOINMw9TKg-QVOkZT9A)\n3. [ApacheFlink新场景-OLAP引擎](https://mp.weixin.qq.com/s/NLwYpjzNgkR8O5zRg7RCoQ)\n4. [说说Flink DataStream的八种物理分区逻辑](https://mp.weixin.qq.com/s/d_jzHb-b7LEGNz1CN34zMg)\n5. [State Processor API：如何读取，写入和修改 Flink 应用程序的状态](https://mp.weixin.qq.com/s/eHPQx3kGKnhXeZpLhUNvng)\n6. [Flink滑动窗口原理与细粒度滑动窗口的性能问题](https://mp.weixin.qq.com/s/Q4k0xgPCOUQ-A2DQ-XaJgw)\n7. [基于Flink快速开发实时TopN](https://mp.weixin.qq.com/s/Ppz5740WTB7lTTLHNL72Tg)\n8. [使用 Apache Flink 开发实时 ETL](https://mp.weixin.qq.com/s/kLjEceslHQxDDi3mRrjR-g)\n9. [Flink Source/Sink探究与实践：RocketMQ数据写入HBase](https://mp.weixin.qq.com/s/kZQRQBVjYiXKfMhKM7SSqQ)\n10. [Spark/Flink广播实现作业配置动态更新](https://mp.weixin.qq.com/s/GDylIWCDnjpX9_X6T9NmMA)\n11. [Flink全链路延迟的测量方式](https://mp.weixin.qq.com/s/A6CIPsGf-aCWXkB7O-toVw)\n12. [Flink原理-Flink中的数据抽象及数据交换过程](https://mp.weixin.qq.com/s/5BlCzguYiEP1h48jwkos2w)\n13. [Flink SQL Window源码全解析](https://mp.weixin.qq.com/s/UkpkS_JiRGR0ibZKYechbg)\n14. [Flink DataStream维度表Join的简单方案](https://mp.weixin.qq.com/s/e-lyViKV4NPmOVwA5Jn6Qw)\n15. [Apache Flink的内存管理](https://mp.weixin.qq.com/s/cBMrF814jGtEFdve0Lrr6g)\n16. [Flink1.9整合Kafka实战](https://mp.weixin.qq.com/s/e0BQoY5Y79NHhcQ9MqltFQ)\n17. [Apache Flink在小米的发展和应用](https://mp.weixin.qq.com/s/KbhmJCW80UmeFwRxM3jerg)\n18. [基于Kafka+Flink+Redis的电商大屏实时计算案例](https://mp.weixin.qq.com/s/BPzOBz7oTfn2_yW8tevEEw)\n19. [Flink实战-壳找房基于Flink的实时平台建设](https://mp.weixin.qq.com/s/TsU_5N0Csfw-afN9AdAihw)\n20. [用Flink取代Spark Streaming！知乎实时数仓架构演进](https://mp.weixin.qq.com/s/0M8XLTgpj6jWNcokNhyxAw)\n21. [Flink实时数仓-美团点评实战](https://mp.weixin.qq.com/s/Oom-TaEsT6GKGs95dJil5Q)\n22. [来将可留姓名？Flink最强学习资源合集!](https://mp.weixin.qq.com/s/13w43iYT3-riIj757HPGxw)\n23. [数据不撒谎，Flink-Kafka性能压测全记录!](https://mp.weixin.qq.com/s/0VXqbzLBj5rZjjf4jAc3UQ)\n24. [菜鸟在物流场景中基于Flink的流计算实践](https://mp.weixin.qq.com/s/2_8uOdDJwzYxUP-NLh6VhA)\n25. [基于Flink构建实时数据仓库](https://mp.weixin.qq.com/s/Rhgt33y102WzR9-Zq15iVQ)\n26. [Flink/Spark 如何实现动态更新作业配置](https://mp.weixin.qq.com/s/sjRV_F9tXEfqKL_00rJc7w)\n\n### 二、Spark实战进阶文章合集\n\n#### Spark实战合集\n\n[点我查看Spark实战合集](实战系列文章/Spark实战.md)\n\n1. [如果你在准备面试，好好看看这130道题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486986&idx=1&sn=422d1a3c11c72ff97b32cc01142839f4&chksm=fd3d489fca4ac1895242ab94b932b12c65dc57b5f3a16acc7084dc8a189e9026290245a64c4f&token=1999457569&lang=zh_CN#rd)\n2. [ORC文件存储格式的深入探究](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486981&idx=1&sn=9c8fc4c127d7e6108ac4e171e750d490&chksm=fd3d4890ca4ac186614f0dda8ffb2d35693a925b03861a01769c898652b53d0d436bca05ea12&token=1999457569&lang=zh_CN#rd)\n3. [基于SparkStreaming+Kafka+HBase实时点击流案例](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486938&idx=1&sn=83347b444fc721442d2b4e1a58eca0e8&chksm=fd3d4b4fca4ac2597abd4a39a21dea83220858ae5efd1ae287b471bb9970f165b4854426f4c2&token=1999457569&lang=zh_CN#rd)\n4. [HyperLogLog函数在Spark中的高级应用](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486931&idx=1&sn=9c3f3d6a677ed2aa6cc508046ca6da78&chksm=fd3d4b46ca4ac25044d6033458d3b43e7d1f50f447bded4eed8b246991cf620c91919c51e35b&token=1999457569&lang=zh_CN#rd)\n5. [我们常说的海量小文件的根源是什么？](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486931&idx=2&sn=1c5987f3bad7a805895484ebfd683e11&chksm=fd3d4b46ca4ac250d31502a76f0cfd02ebea29d1161b9e9faec23ea6aa8947d0dd7d4268427f&token=1999457569&lang=zh_CN#rd)\n6. [Structured Streaming | Apache Spark中处理实时数据的声明式API](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486904&idx=1&sn=5f4b673a87497a9c1dc9d7ce253994f3&chksm=fd3d4b2dca4ac23b9850c54d62ebe8be5920cbd4a491a46a0325e1fbbd91f6b1aef9adf2f638&token=1999457569&lang=zh_CN#rd)\n7. [Spark面对OOM问题的解决方法及优化总结](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486765&idx=1&sn=516a32e8c1e9842606a7670862ec7e97&chksm=fd3d4bb8ca4ac2ae24e315083cdb195fdf897e1c3cce69d2c94183d5c20c1af1bfbe0e5480fd&token=1999457569&lang=zh_CN#rd)\n8. [Spark 动态资源分配(Dynamic Resource Allocation) 解析](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486761&idx=1&sn=959aaa5266307a64181631ba1ae46e86&chksm=fd3d4bbcca4ac2aad8a3958da40c22e565997180c635bb6c32a9b4aa7e82a74a56423320c65a&token=1999457569&lang=zh_CN#rd)\n9. [Apache Spark在海致大数据平台中的优化实践](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486668&idx=1&sn=981028fdcf937ba45b3914e2d48b94db&chksm=fd3d4a59ca4ac34f089ff1dce9542a0f481a73439acec9223fa2d776ff3f2eaec498cc543d06&token=1999457569&lang=zh_CN#rd)\n10. [Spark/Flink广播实现作业配置动态更新](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486644&idx=1&sn=d2637a1e918c2b1be4c9fe3d74f75a92&chksm=fd3d4a21ca4ac3377cc8836939cc041cf934bb57f73b6b618fd1de608495d86e278c1c7e4cdc&token=1999457569&lang=zh_CN#rd)\n11. [Spark SQL读数据库时不支持某些数据类型的问题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486629&idx=2&sn=54d05858d659756a46b9be36b7b63ee5&chksm=fd3d4a30ca4ac326f69ba9b7cf6a82418afd31193e96aa28346779264a7d8a1df4c7a3a31fba&token=1999457569&lang=zh_CN#rd)\n12. [这个面试问题很难么 | 如何处理大数据中的数据倾斜](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486586&idx=1&sn=e35def6429adaada0910b91eed8d7b1f&chksm=fd3d4aefca4ac3f9918d57b947d4bc8f71afd9d7666ffe28e3660e5efab332c714bef0879a90&token=1999457569&lang=zh_CN#rd)\n13. [Spark难点 | Join的实现原理](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486582&idx=1&sn=7b6291dedb2e6892342e1ed705bdfb2e&chksm=fd3d4ae3ca4ac3f591f297635c0ff8e63e3deb6d6aec72c8fb18e19f8f6fcdb5cd2abcd37d09&token=1999457569&lang=zh_CN#rd)\n14. [面试注意点 | Spark&Flink的区别拾遗](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486577&idx=1&sn=49fd0138ad9837192b6eb78cbdc478e6&chksm=fd3d4ae4ca4ac3f2aec91f40435d1eb55a9e497fe7d8ab9ad52396838f467879ac56cb83c9c2&token=1999457569&lang=zh_CN#rd)\n15. [Spark Checkpoint的运行原理和源码实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486514&idx=1&sn=a3c3084b8a797a0889d7583fb10d70c9&chksm=fd3d4aa7ca4ac3b12416ba8269041032a515b7a4e71bda1709e359a15a04df4f638738c6e01a&token=1999457569&lang=zh_CN#rd)\n16. [阿里云Spark Shuffle的优化](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486505&idx=1&sn=91316d3aa5a99945ccd992c0a98e500d&chksm=fd3d4abcca4ac3aa26f051504244239ff1ab48eb71cf01c14f689eb4767c121b1477d93b60ea&token=1999457569&lang=zh_CN#rd)\n17. [使用Kafka+Spark+Cassandra构建实时处理引擎](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486485&idx=1&sn=999b84e0ac87f2aa1be4870921279a21&chksm=fd3d4a80ca4ac39695d9307582ff58938180d0c5b12f6db84ddae933edb345b0cbe46f6284ed&token=1999457569&lang=zh_CN#rd)\n18. [基于HBase和Spark构建企业级数据处理平台](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486477&idx=1&sn=21394cca8fe279bbc2032d48f65672f6&chksm=fd3d4a98ca4ac38e3bf9700cfe65131fffadbbf9493ccc658b3e7be97ddb6665ad41a70f67f9&token=1999457569&lang=zh_CN#rd)\n19. [SparkSQL在字节跳动的应用实践和优化实战](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486419&idx=1&sn=3bc8af144370a602817ca87415fe525d&chksm=fd3d4d46ca4ac450ec6f1ac84b2f3162071eb85221464bdb1976178ca2363cd2acb3d63d401c&token=1999457569&lang=zh_CN#rd)\n20. [SparkRDD转DataSet/DataFrame的一个深坑](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486369&idx=1&sn=9a760114ed2c7509e191ad29370eecce&chksm=fd3d4d34ca4ac4222d310c37fb67bcaafd5425e1a16c43434c4d1fea403756e266de21b83181&token=1999457569&lang=zh_CN#rd)\n21. [Spark和Flink的状态管理State的区别和应用](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486332&idx=1&sn=ecbe21981c5c36f6c420755e8d63fb8f&chksm=fd3d4de9ca4ac4ff1b11093432f444c9e4f66c45a6e01155d69cface70456fb418a98163757d&token=1999457569&lang=zh_CN#rd)\n22. [Kafka+Spark Streaming管理offset的几种方法](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486328&idx=1&sn=b5d53e0007032114fb277e440e5ce4bf&chksm=fd3d4dedca4ac4fbb5026338af7dbbfe1d845fdc11e7e2279035dddc8ff0b6dd24c32be129ce&token=1999457569&lang=zh_CN#rd)\n23. [从 PageRank Example谈Spark应用程序调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486322&idx=2&sn=00ddcd16109249e45a70233d5ef959ba&chksm=fd3d4de7ca4ac4f15f85d9a2873c5d1070af3bb929479bd66f7a0dc89ac0777b6960cbce5970&token=1999457569&lang=zh_CN#rd)\n24. [Spark调优|SparkSQL参数调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485574&idx=1&sn=723c02562ee1f44e88c389d6ac8a2c87&chksm=fd3d4e13ca4ac705ad892ada2c68792c906946f271bb1e6afd15a93749f8f87d0ea967ba2d37&token=1999457569&lang=zh_CN#rd)\n25. [Flink/Spark 如何实现动态更新作业配置](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485507&idx=1&sn=2cab7a3714ce4e16351b8edcde95e777&chksm=fd3d4ed6ca4ac7c058d820f36b2d03c3ccddc293e1035b6897e99544e55931f6cd6a2f343647&token=1999457569&lang=zh_CN#rd)\n26. [Stream SQL的执行原理与Flink的实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485361&idx=1&sn=8348203b6f17662a64fa5d412de97296&chksm=fd3d4124ca4ac8329bac00fae705f37c08236d583e9900f268796d463be9323d82ba4b8b1554&token=1999457569&lang=zh_CN#rd)\n27. [Spark将Dataframe数据写入Hive分区表的方案](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485321&idx=1&sn=13e73673fb29bd134ab79e03a369288c&chksm=fd3d411cca4ac80ac68204d18aee55b95cadde7bf18cc2f8a549fc0b6ff4ec4db570190566c3&token=1999457569&lang=zh_CN#rd)\n28. [Spark中几种ShuffleWriter的区别你都知道吗？](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485316&idx=1&sn=8a7a02023f15324885de7a5c93d4dd94&chksm=fd3d4111ca4ac807f34e1fa03023494d46f3770c035290ddefe1333419453a9258f504584aee&token=1999457569&lang=zh_CN#rd)\n29. [SparkSQL的3种Join实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485217&idx=1&sn=3ce9fa8ad179c008754873129e51fbe7&chksm=fd3d41b4ca4ac8a25957fc9541437e6546fc2926df2908bad5adbd30a6cecf9f797fef74f894&token=1999457569&lang=zh_CN#rd)\n30. [周期性清除Spark Streaming流状态的方法](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485138&idx=1&sn=8f71070470c8963e7c973b5f10bf3c03&chksm=fd3d4047ca4ac951981f7f0fa08f9f6a1821270441b5008da28115b67020b01ef2936b5f1e88&token=1999457569&lang=zh_CN#rd)\n31. [Structured Streaming之状态存储解析](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485119&idx=1&sn=fd172b1f9c9ef99eac2ed976a7d4459f&chksm=fd3d402aca4ac93ced929dfb9b3d785fa5e00d82939b4ea7ed31e68096b87f31a0d11c5f7414&token=1999457569&lang=zh_CN#rd)\n32. [Spark SQL重点知识总结](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485105&idx=1&sn=0bea228e6845d04739937b75bd2f8d9a&chksm=fd3d4024ca4ac9322cae55569b7bdc6d8546dc9c4583f25801d3820ab3b30094c50816c7c1c8&token=1999457569&lang=zh_CN#rd)\n33. [SparkSQL极简入门](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485058&idx=1&sn=4d3b5c25ca1fdf1f0fb0cd99959d2371&chksm=fd3d4017ca4ac90140442dfc6032346d5841d6705bd92441ad3d0a0366db346752a6b154b976&token=1999457569&lang=zh_CN#rd)\n34. [Spark Shuffle在网易的优化](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485051&idx=1&sn=a1a70cad450634ceae44d4e14a4fc3ef&chksm=fd3d40eeca4ac9f8901a93271683811da05c62e7a6f1d2825378c32542ef2edf9947273a601a&token=1999457569&lang=zh_CN#rd)\n35. [广告点击数实时统计：Spark StructuredStreaming + Redis Streams](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484999&idx=1&sn=f9cf6eae39bc1d54faaa144357731d2f&chksm=fd3d40d2ca4ac9c4e886bd0208521c45dbab98e9cfd34826e6808d414bcd66f203f0359382e1&token=1999457569&lang=zh_CN#rd)\n36. [Spark内存调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484972&idx=1&sn=ff9a2925c31e07b558504be17937872b&chksm=fd3d40b9ca4ac9afe2cce8ff4a6c50724a146b87d2d05d87d6dcdfb34a177f3be4d5ba79b492&token=1999457569&lang=zh_CN#rd)\n37. [Structured Streaming 实现思路与实现概述](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484959&idx=1&sn=2173f71a32e16b510f047fa716549bc2&chksm=fd3d408aca4ac99cef4ec3079d9c7376c14f457e80f814e1494f5324612b553fb5487783a486&token=1999457569&lang=zh_CN#rd)\n38. [Spark之数据倾斜调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484956&idx=1&sn=9182a40fcf1fced04acee81aa9261bfe&chksm=fd3d4089ca4ac99f23952f0d627db4600a81808d98b1a635ae40e06c939e4a229a47d666de47&token=1999457569&lang=zh_CN#rd)\n39. [你不得不知道的知识-零拷贝](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484766&idx=1&sn=8d0aeaa1166a9338df9f28bb47959f4a&chksm=fd3d43cbca4acadd0dfc9e753ca4fe1cc2ca49060d886b359bdf3537809015b04f82b4be27ac&token=1999457569&lang=zh_CN#rd)\n40. [Spark Streaming消费Kafka数据的两种方案](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484751&idx=1&sn=11315f599b39eac96c17a78da2fa1258&chksm=fd3d43daca4acaccc624947f5fa84f650e7f61d8638feda67db07cacf51bc985faae74cc94c3&token=1999457569&lang=zh_CN#rd)\n\n### 三、Kafka实战进阶文章合集\n\n#### Kafka实战合集\n\n[点我查看Kafka实战合集](实战系列文章/Kafka实战.md)\n\n### 四、数据仓库实战系列\n\n#### 数据仓库实战合集\n\n[点我查看数据仓库实战合集](实战系列文章/数据仓库.md)\n\n### 五、OLAP实战文章系列OLAP\n\n* [ClickHouse专栏](https://blog.csdn.net/u013411339/category_11681457.html)\n\n### 六、硬刚系列文章合集\n\n#### 硬刚系列文章合集\n\n * [《硬刚Presto|Presto原理&调优&面试&实战全面升级版》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247503995&idx=1&sn=ead9bbd4ea821c94efc1e18875c1722c&chksm=fd3e96eeca491ff8e0b6a0e1ad3c9ada5365457dd730ce9e19339803504ae10d8108c296fc27&token=935192335&lang=zh_CN&scene=21#wechat_redirect)\n * [《硬刚Apache Iceberg | 技术调研&在各大公司的实践应用大总结》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247503879&idx=2&sn=bd009e298f2bdf9bb8abc9271b515143&chksm=fd3e9692ca491f84722c922000754aafc4d5a271b0ee40d525ce177de3c4442ef83b5ade8e05&scene=21&token=935192335&lang=zh_CN#wechat_redirect)\n * [《硬刚ClickHouse | 4万字长文ClickHouse基础&实践&调优全视角解析》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247503675&idx=1&sn=3ee6af64d0126c78b48cad219308f81e&chksm=fd3e89aeca4900b8b8954e9569ee3c0877881fac8c792bfafc22e7e9d3e8524da8eb860d33d8&scene=21#wechat_redirect)\n * [《硬刚数据仓库|SQL Boy的福音之数据仓库体系建模&实施&注意事项小总结》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247503576&idx=1&sn=f9fc428799e0fcc78e94360e1cec7b95&chksm=fd3e884dca49015b6d38c437f603b4deffeeb0cefafd32d358a891bfe820f734116100395bba&scene=21#wechat_redirect)\n * [《硬刚Hive | 4万字基础调优面试小总结》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247502750&idx=1&sn=bd9a9173d060dc4e4ebd49c8efc6acfe&chksm=fd3e8d0bca49041dea84da93910e5efdc4935e520525c09887c986691377aeb48e5cf7fb5667&scene=21#wechat_redirect)\n * [《硬刚用户画像(一) | 标签体系下的用户画像建设小指南》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247503741&idx=1&sn=e5039be93123f2e337013756a818bfc3&chksm=fd3e89e8ca4900fe603b63c5722a6fb8a32bd63d6ba23e0028851948a71b877eb1f742d95087&scene=21#wechat_redirect)\n * [《硬刚用户画像(二) | 基于大数据的用户画像构建小百科全书》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489571&idx=1&sn=56a634d66fb689907b4ab51ed2d3707a&chksm=fd3d5eb6ca4ad7a0cc5fa4f895354e58ed7f2cb8558369ed6149560a5e7fca97b8545036fe87&scene=21#wechat_redirect)\n * [《我们在学习Spark的时候，到底在学习什么？](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247504460&idx=1&sn=897f9cebaade7861b110fec0a34fc38b&chksm=fd3e94d9ca491dcf85f611c6772703973035ce05e81defdc8d05597f2c478554e063aaa60f00&scene=21#wechat_redirect)》  \n * [《我们在学习Flink的时候，到底在学习什么？》](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247499604&idx=1&sn=d938dfb30d221774704982d2938b30c1&chksm=fd3eb9c1ca4930d76a391241333de461ca22d2aa27472eab3cffab1564872ae37f1b48fe2d3c&token=935192335&lang=zh_CN&scene=21#wechat_redirect)\n\n### 七、2020精品文章合集\n\n#### 2020精品文章合集\n\n**实时计算篇**\n\n*   [Structured Streaming | Apache Spark中处理实时数据的声明式API](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486904&idx=1&sn=5f4b673a87497a9c1dc9d7ce253994f3&chksm=fd3d4b2dca4ac23b9850c54d62ebe8be5920cbd4a491a46a0325e1fbbd91f6b1aef9adf2f638&scene=21#wechat_redirect)\n*   [HyperLogLog函数在Spark中的高级应用](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486931&idx=1&sn=9c3f3d6a677ed2aa6cc508046ca6da78&chksm=fd3d4b46ca4ac25044d6033458d3b43e7d1f50f447bded4eed8b246991cf620c91919c51e35b&scene=21#wechat_redirect)\n*   [基于SparkStreaming+Kafka+HBase实时点击流案例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486938&idx=1&sn=83347b444fc721442d2b4e1a58eca0e8&chksm=fd3d4b4fca4ac2597abd4a39a21dea83220858ae5efd1ae287b471bb9970f165b4854426f4c2&scene=21#wechat_redirect)\n*   [基于Flink SQL构建实时数据仓库](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486938&idx=1&sn=83347b444fc721442d2b4e1a58eca0e8&chksm=fd3d4b4fca4ac2597abd4a39a21dea83220858ae5efd1ae287b471bb9970f165b4854426f4c2&scene=21#wechat_redirect)\n*   [Flink异步之矛-锋利的Async I/O](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486945&idx=1&sn=f973d74b58d81e96db2bff431c39b2e2&chksm=fd3d4b74ca4ac2629b9f79232a56d8209627e32ebbfb72ca269bf49c863fdf6f3418f6dee5db&scene=21#wechat_redirect)\n*   [Spark SQL快速入门系列之Hive](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492891&idx=2&sn=c86bb4c8a3f96d1e5740e4b16d3638d0&chksm=fd3ea38eca492a9841b34a64a10272c83252bb9815dd164d038248fc71bda7119b266afa5c20&scene=21#wechat_redirect)\n*   [基于SparkStreaming+Kafka+HBase实时点击流案例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492891&idx=2&sn=c86bb4c8a3f96d1e5740e4b16d3638d0&chksm=fd3ea38eca492a9841b34a64a10272c83252bb9815dd164d038248fc71bda7119b266afa5c20&scene=21#wechat_redirect)\n*   [三万字长文 | Spark性能优化实战手册](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487074&idx=2&sn=02fc69f890ee9922df1ad6b831f448e5&chksm=fd3d48f7ca4ac1e1377befc32a25e97120c1dde9dcb38fa940834302238eb1ad5ceb8b504e7e&scene=21#wechat_redirect)\n*   [Flink整合OozieShellAction提交任务带Kerberos认证](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487182&idx=1&sn=e69012117af61a623e30e60c0c356444&chksm=fd3d485bca4ac14d360cff2d1e2e78105d350d71daf9b84d911d95b5cc639c528969a5c2fa71&scene=21#wechat_redirect)\n*   [Spark源码阅读的正确打开方式](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487182&idx=1&sn=e69012117af61a623e30e60c0c356444&chksm=fd3d485bca4ac14d360cff2d1e2e78105d350d71daf9b84d911d95b5cc639c528969a5c2fa71&scene=21#wechat_redirect)\n*   [消息队列常见面试问题小集合](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487224&idx=1&sn=340d084b84ae4d49de9f46a7974e8a4c&chksm=fd3d486dca4ac17b31a221ff1cdcd3920035eeb9f480c00955268686623d512b516214efb3a6&scene=21#wechat_redirect)\n*   [Flink1.10和Hive集成一些需要注意的点](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487242&idx=1&sn=b118be66baa230df95c317d0d14c8a4a&chksm=fd3d499fca4ac089102e0eec2c8248174c43724115a610802e89f5a54d4dfee53aa5d27e8417&scene=21#wechat_redirect)  \n*   [Flink事件时间、水印和迟到数据处理](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487259&idx=1&sn=ab3b02eb7e835831cfceb248ae63880a&chksm=fd3d498eca4ac098eac6864ff1f198575ab2822e26f0cbd18f7b666beb660ddef3e3790ab581&scene=21#wechat_redirect)  \n*   [Flink使用Broadcast State实现流处理配置实时更新](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487268&idx=2&sn=743482009dab625b6b9316e02e793c7f&chksm=fd3d49b1ca4ac0a7d9ffcde9cb9f76d0850ee67ed7861ee24bf5ded980b03bd3ca4d0c86442e&scene=21#wechat_redirect)\n*   [实战 | MySQL Binlog通过Canal同步HDFS](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487268&idx=1&sn=d2417bd649586c4d6dd73e91ec75538e&chksm=fd3d49b1ca4ac0a7bba4431b4206a84bb13e0dcea01d293a46ae7f69e5fe829e9b37cc46716b&scene=21#wechat_redirect)  \n*   [Flink最难知识点再解析 | 时间/窗口/水印/迟到数据处理](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487283&idx=2&sn=193b0544edb293fe0795dcfe7caf36ef&chksm=fd3d49a6ca4ac0b01c6f3fb7b93151936bef494ed06e236131a5bd4f86735a35e9c530c1229f&scene=21#wechat_redirect)\n*   [Hive on Spark参数调优姿势小结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487330&idx=1&sn=fd95cc865937188ee19944d8b643e6ab&chksm=fd3d49f7ca4ac0e17207d2e543b8803444b34b92a777acdec10cf4f63efef87480fb6e5e5543&scene=21#wechat_redirect)\n*   [Flink Logback日志与邮件报警配置](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487346&idx=3&sn=4426fc62ba4d0895bf66858fcf6b3873&chksm=fd3d49e7ca4ac0f112b05522f1897c97bdfa9a0278041e085768fd9faa8cfbd0e3e1b840f861&scene=21#wechat_redirect)  \n*   [Kafka设计-恰好一次和事务消息](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487346&idx=2&sn=971ffca7e79325e6cd3105eda31af4e1&chksm=fd3d49e7ca4ac0f191f516079b5e1e9451bdd924b4b8fbb8d690c1661e58b7e2dd353c657cdb&scene=21#wechat_redirect)  \n*   [基于Canal和Kafka实现MySQL的Binlog近实时同步](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487357&idx=1&sn=55e0bf4b45ac7f7cae6aadae69e39ace&chksm=fd3d49e8ca4ac0fe6780ac5cf753809e7fa0f0cb73cc70bb8246b1901765199f968bd84fc7b2&scene=21#wechat_redirect)  \n*   [一个基于RabbitMQ的可复用的事务消息方案](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487364&idx=2&sn=63567772a41545834791a7ab2696e4d4&chksm=fd3d4911ca4ac00729a17a37f70819d861ec9dde65df2f91406fbde7cae188594e386bff82ce&scene=21#wechat_redirect)\n*   [Spark性能优化总结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487387&idx=1&sn=8b769ead6d7407ac490af0fb1b147598&chksm=fd3d490eca4ac0183f13910819092bb22b454f2390f44fc40dc8bf15e91a844b18ec587dd97e&scene=21#wechat_redirect)\n*   [Flink常见异常和错误信息小结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487410&idx=1&sn=24dc08c17ac3d3ac7aaedef9359417d1&chksm=fd3d4927ca4ac0313bdd8999e4760dd6ad0a0471d1fce91493920a08480428d78972ff6f6bdd&scene=21#wechat_redirect)\n*   [Spark SQL快速入门系列之Hive](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487410&idx=1&sn=24dc08c17ac3d3ac7aaedef9359417d1&chksm=fd3d4927ca4ac0313bdd8999e4760dd6ad0a0471d1fce91493920a08480428d78972ff6f6bdd&scene=21#wechat_redirect)\n*   [实时计算双星-Flink VS Spark 部署模式对比](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489363&idx=1&sn=677472699acb15539b4183e463cd00d2&chksm=fd3d51c6ca4ad8d0c01c90a87ab4fc4f172c82b04f27fe12cee19d502ae88e7fc3182ee7a27b&scene=21#wechat_redirect)  \n*   [PID算法和Spark实现反压的原理](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487468&idx=1&sn=1fc593c4f8160b248a008889e098d4cb&chksm=fd3d4979ca4ac06f42518f5fe94d72646395beca75bf1f06dc347b7628baaf56582605705eaa&scene=21#wechat_redirect)  \n*   [关于SparkSQL的开窗函数，你应该知道这些!](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487480&idx=2&sn=a233125a9d7f665ed4b69951deb6105a&chksm=fd3d496dca4ac07b283fa8ca95eee2c638d62b894e563ff40c86e69a82386c355ad7ae2ac223&scene=21#wechat_redirect)  \n*   [Spark SQL是如何选择join策略的？](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487480&idx=1&sn=ec2325cb27dca653269b68f7d6101a07&chksm=fd3d496dca4ac07bbf25fea5a6dc4416be0a19d44ea5aeedc956c640e4acc74066898c195325&scene=21#wechat_redirect)  \n*   [Spark on Hive & Hive on Spark，傻傻分不清楚](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487491&idx=1&sn=4a78fdda3f40f04383f0001522e27c63&chksm=fd3d5696ca4adf804713d6a7651a5a7d0fa35e0e80f453778e6fe84c40d29162bf3874691b18&scene=21#wechat_redirect)  \n*   [来看看一个大二学生的Spark练习题](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487510&idx=1&sn=9d8170be7736ce6cae6e4ce983e2f65b&chksm=fd3d5683ca4adf95fcb1341b6df89dacd647ae890f7db5afad1d3e5251b9ba322b812bbcf767&scene=21#wechat_redirect)\n*   [Flink 自定义触发器实现带超时时间的 CountWindow](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491813&idx=1&sn=b0b76b7a41ea2a61705949496f07d03d&chksm=fd3ea670ca492f66da69213c0e74798687b4551102f035da0e973dcb8e92ffdb4d9f75e40c71&scene=21#wechat_redirect)  \n*   [Spark Kafka 基于Direct自己管理offset](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491836&idx=2&sn=3eacfa2e3f136f0314012bd4976ee883&chksm=fd3ea669ca492f7fefeb1a8b47af5f53f080512178cfacd9978218b11cbb9c75b078e45f12dd&scene=21#wechat_redirect)  \n*   [Apache Kylin | 麒麟出没，必有祥瑞](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491871&idx=1&sn=207b4283b8c75e3883d3af21e36d4ef5&chksm=fd3ea78aca492e9c442c68377a12d2cc7e4c967f30a24c55b1cc0191b916f7f3f9697bc187f1&scene=21#wechat_redirect)  \n*   [Flink 参数配置和常见参数调优](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491884&idx=2&sn=a13485ac6594e464fad3f1c64771b4e2&chksm=fd3ea7b9ca492eaf0b71a2a841168451c3ce9080f66d5e08838ea922299797d60e5d477265c5&scene=21#wechat_redirect)  \n*   [利用InfluxDB+Grafana搭建Flink on YARN作业监控大屏](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491911&idx=1&sn=ba027a574fd1a531cb3bec943c949363&chksm=fd3ea7d2ca492ec49f636ab5162b7d9f09730cfa9e4b9ff03589ca9eb1cf564c899aaef79ab3&scene=21#wechat_redirect)  \n*   [网站日志实时分析之Flink处理实时热门和PVUV统计](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491992&idx=2&sn=c7942a1ccc921ef5c3a1403752cc81fd&chksm=fd3ea70dca492e1be3e51372fe6414f7f740f0f822d99f1444530da8b1e1dadbf18fe957d8b8&scene=21#wechat_redirect)\n*   [大数据量下的集合过滤—Bloom Filter](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491992&idx=1&sn=08e4afd7c2a21d6948b7fd7c2351eff0&chksm=fd3ea70dca492e1b51347fb31567a205e5c501d08ed130788e43ab8750316c99b14e7d4bd049&scene=21#wechat_redirect)  \n*   [实时数仓链路分享：kafka =>SparkStreaming=>kudu集成kerberos](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492025&idx=2&sn=91cf0d75fcf8ce47ea4df45e012e23bb&chksm=fd3ea72cca492e3a206d5ea5efec625b7c9eb63991fa1dc4ccbae21d6f3742eb8e0382bf09fc&scene=21#wechat_redirect)\n*   [Flink CEP 原理和案例详解](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492025&idx=2&sn=91cf0d75fcf8ce47ea4df45e012e23bb&chksm=fd3ea72cca492e3a206d5ea5efec625b7c9eb63991fa1dc4ccbae21d6f3742eb8e0382bf09fc&scene=21#wechat_redirect)\n*   [ProcessFunction：Flink最底层API使用踩坑记录](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487531&idx=1&sn=183d114f36a697eb7df595fa24fec31c&chksm=fd3d56beca4adfa830cdbd99cf0adde30c7622c3541252cdf172ca0b66e60ae475e7796fdc96&scene=21#wechat_redirect)  \n*   [Flink 1.10之改进的TaskManager内存模型与配置](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487665&idx=2&sn=b6efbb6d5efb6e0b0b978fe8a1515f56&chksm=fd3d5624ca4adf32609d3950c0265420e6b75989f8d0e89bf68ceda2495d0e1d1bc4db2f37ec&scene=21#wechat_redirect)\n*   [打通实时流处理log4j-flume-kafka-structured-streaming](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487716&idx=2&sn=d9a765695a803b124857db4da4834118&chksm=fd3d5671ca4adf67a5a0b1880548ea9273336638ee073866f672d4edc85c3ff39e42de69e350&scene=21#wechat_redirect)\n*   [如何设计实时数据平台（设计篇）](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487761&idx=2&sn=9bad34ba9f0c59200f9b10e09b3e7dce&chksm=fd3d5784ca4ade9214b723ed29d6662efd690a52b7e7f667a09690df1e9be2ed6302fb6d7cc1&scene=21#wechat_redirect)  \n*   [如何设计实时数据平台（技术篇）](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487795&idx=2&sn=464480d4e075688ab389502e32ef0611&chksm=fd3d57a6ca4adeb0d31080c92b4f98f3187db6201eebb2be5c161b46c8da1806a275543cbbb7&scene=21#wechat_redirect)\n*   [SparkSQL内核解析-执行全过程概述](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488182&idx=1&sn=5bbdf75fbad79d01cdb9de65d32f8d39&chksm=fd3d5423ca4add357c23ead5c0fd9818ca3406e341bd91ab33b1a50c6cd27af86dc3b8d90594&scene=21#wechat_redirect)  \n*   [SparkSQL内核解析之逻辑计划](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488207&idx=1&sn=2148863c2624465675f8bf6650b5843b&chksm=fd3d545aca4add4c39d828872f206cf198b13fe4f7026ad2725b62068f92e0f7853d3f28dd73&scene=21#wechat_redirect)  \n*   [Flink-1.10中的StreamingFileSink相关特性](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488580&idx=2&sn=590724dcc514a3f16e0221265858ddde&chksm=fd3d52d1ca4adbc71de9a818b902e9e3e0d1ac2aef374f7f2eecbce3965179066d8e2e041f94&scene=21#wechat_redirect)\n*   [Kafka下的生产消费者模式与订阅发布模式](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488638&idx=1&sn=8a122c4b3d455292e13e5da1778de6e6&chksm=fd3d52ebca4adbfd4fe4860a90db3d3cfb0fa0048a896342a6ef6eb919c7dae6e3ade0d4f94e&scene=21#wechat_redirect)\n*   [Kafka+Spark Streaming如何保证exactly once语义](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489143&idx=1&sn=3bc211306e107113789f617d39454ee9&chksm=fd3d50e2ca4ad9f45d6302299f35ab3d0423fd170f5959b7acd1826a289c7f4ee2db7cd52460&scene=21#wechat_redirect)  \n*   [Flink之实时统计热门商品的TopN](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489166&idx=1&sn=7bd32ccc732d9c7e5392b5f3e9ab14e2&chksm=fd3d501bca4ad90dba7326060b08bb09b1523b27ca1bd7254fcd1e86d7ae3b4c8ae18c637e3a&scene=21#wechat_redirect)\n*   [SparkSQL的自适应执行-Adaptive Execution](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490000&idx=1&sn=984a48afb840b57343d00dfe19335b5c&chksm=fd3d5f45ca4ad6539ba1e979de8f60d29ee003414dfcbb8b34c07e86aa3f15a353303215b076&scene=21#wechat_redirect)  \n*   [Kafka KSQL实战](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490024&idx=2&sn=e4822af63cc067d3f5486acd5a668ad8&chksm=fd3d5f7dca4ad66ba406333b2ef2802fda513a0c6852a315f5e6760b321825ab5737eb52d87a&scene=21#wechat_redirect)\n*   [ELK+FileBeat+Kafka分布式系统搭建图文教程](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492241&idx=1&sn=8899979a87a17d9d4ed5ddf901e1a615&chksm=fd3ea404ca492d124e3404fe0d019c6642b90ebc146f2c0f500b82109f4056f7abd36413a38b&scene=21#wechat_redirect)  \n*   [HDFS应用场景、原理、基本架构及使用方法](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492293&idx=2&sn=f9354cc4d1ce1541c2aaae99a758437c&chksm=fd3ea450ca492d464331a96746135e9f4ef0205ee968abbcf7a6c4c837b5e1632ef107739f54&scene=21#wechat_redirect)  \n*   [数据模型⽆法复⽤，归根结底还是设计问题](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492293&idx=1&sn=c14bde7878c5b28cd3698f5a8df735ba&chksm=fd3ea450ca492d46fd9f1bdf3e6f45bed2c8839cc314e1239e73bb5f7daff3a067e4dc5ab914&scene=21#wechat_redirect)\n*   [Hadoop支持Lzo压缩配置及案例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492923&idx=1&sn=6b5a0594031c40ff0e543aa6d9740cc6&chksm=fd3ea3aeca492ab887f8b95a711d3c1bbbd4c2438d941efe42329d713569bb42482342dd92ba&scene=21#wechat_redirect)\n*   [快看 | Java连接集成Kerberos的HA HDFS方案](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492168&idx=3&sn=fac9d36291779f58afdf42035d7202a3&chksm=fd3ea4ddca492dcb252d2173f3b247f78552d068eac429f8f10c45e9fff0fbf2d87e0a33d671&scene=21#wechat_redirect)\n*   [Kafka消费者分区分配策略及自定义分配策略](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491557&idx=2&sn=ba688e1f3b70198975cc57fd845fa337&chksm=fd3d5970ca4ad0668cacb9c1623423fecccdc685e02de5dbf95249f8daf191c2dc732fa9bfc3&scene=21#wechat_redirect)\n*   [Spark Streaming整合log4j、Flume与Kafka的案例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492168&idx=2&sn=e460dc527658b327af8437074cc6ac99&chksm=fd3ea4ddca492dcb50a0196d9b65b46b5e90f605f09c4b8307d377955aa1bfbe4a38df33ffb5&scene=21#wechat_redirect)\n*   [面试必知的 Spark SQL 几种 Join 实现](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491813&idx=2&sn=557cc24073bef28f4f3589b63f71c967&chksm=fd3ea670ca492f660d96498041568c59c60a6390d75931a032d05c53af017451b60a1b9fba49&scene=21#wechat_redirect)\n*   [Flink在大规模状态数据集下的checkpoint调优](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490732&idx=3&sn=e1ed5667cd2e2f4f2f49c86ca20f40b8&chksm=fd3d5a39ca4ad32f239ed2002503b01bce90c091e636764d35371a1971c46d8a2d24662ff146&scene=21#wechat_redirect)  \n*   [Write-Ahead Log(WAL预写日志)的工作原理](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490732&idx=2&sn=e2771a1b14403478b801b02c28eedd03&chksm=fd3d5a39ca4ad32f031fc13032168907af08507f8e448d9a9bc786ac8602ba84c400a0f65722&scene=21#wechat_redirect)  \n*   [Kafka常见的导致重复消费原因和解决方案](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490749&idx=1&sn=c9ef0b3010bd1196efcef9df546985d6&chksm=fd3d5a28ca4ad33e2f1d9f20629fb5b1f7049760ded0d6eb9f935751bd78c7b41a188ff6bc42&scene=21#wechat_redirect)  \n*   [Spark-submit 参数调优完整攻略](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490768&idx=2&sn=7f78bd89f41d189aa4eee7d9de47359d&chksm=fd3d5a45ca4ad353f036260db7be1d5b91075d2de415745350b5d26c05162c53b092f1cd7de6&scene=21#wechat_redirect)  \n*   [Kafka数据可靠性保证三板斧-ACK/ISR/HW](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490798&idx=1&sn=2c0d6cceb9f1028ac1f2eb8192711c02&chksm=fd3d5a7bca4ad36d442069b4403a7a201c67a1cc72799de3664b63d360b559e58daacb86f396&scene=21#wechat_redirect)  \n*   [Spark常见错误问题汇总](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490873&idx=1&sn=2da5f475f178bda5ee5a74dc0306b25e&chksm=fd3d5bacca4ad2bab06dc1d526e28b71e714d499929a85675f2d40f5ec0d72200f54d42e038d&scene=21#wechat_redirect)  \n*   [HBase操作组件：Hive、Phoenix、Lealone](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490905&idx=2&sn=18196db069e5740503ee6be0907fafc2&chksm=fd3d5bccca4ad2da28a3ef31d17181f91bdedf4079ffea830328e726e0959011161fc41c8b86&scene=21#wechat_redirect)  \n*   [Redis系列 | 缓存穿透、击穿、雪崩、预热、更新、降级](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490966&idx=2&sn=f8f2ee62b1aa8353e0f3bba18a8c2f5e&chksm=fd3d5b03ca4ad2151a4c5898683a7eee080088760c4eca5d46df8eb619f01e8d44628cdead9e&scene=21#wechat_redirect)  \n*   [Kafka工作流程及文件存储机制](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490966&idx=1&sn=e81972c99e2792f449085d03715304b1&chksm=fd3d5b03ca4ad215cea217195cce7b29fd3150250e4d134c37ff57dabec52b8d849f62e0ca36&scene=21#wechat_redirect)  \n*   [Redis6.0主从、哨兵、集群搭建和原理](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491086&idx=2&sn=68f9365aff2a66846143b20943c4b138&chksm=fd3d589bca4ad18d971f9ef6709da0217f63ec12615f0ef54c6327ca7568dccfac1a79628cbc&scene=21#wechat_redirect)\n*   [Spark Streaming官方编程指南](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491086&idx=2&sn=68f9365aff2a66846143b20943c4b138&chksm=fd3d589bca4ad18d971f9ef6709da0217f63ec12615f0ef54c6327ca7568dccfac1a79628cbc&scene=21#wechat_redirect)\n*   [【从0开始の全记录】Flume+Kafka+Spark+Spring Boot 统计网页访问量项目](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492401&idx=1&sn=88a3634793c0a3b86e48c5cfe21f6ba3&chksm=fd3ea5a4ca492cb2f298af0a3605fcfe80928057ab24c26e697c11e0c82654636f2ce5190776&scene=21#wechat_redirect)  \n*   [Spark+Kudu的广告业务项目实战笔记(一)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492426&idx=1&sn=5cc1326f8cdec7f54eedea656acb89c5&chksm=fd3ea5dfca492cc98f89e32b2291d5439896d119fec077cafa65b36fbf46a875d9f3c0996fd4&scene=21#wechat_redirect)  \n*   [大数据入门：Spark+Kudu的广告业务项目实战笔记(二)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492558&idx=1&sn=82382cfce0d052380d1c4096951402a2&chksm=fd3ea55bca492c4dea78c0e039e2c1993cdf0bd22f56fb97f9445645033140d34cf520aab697&scene=21#wechat_redirect)  \n*   [大数据入门：Spark+Kudu的广告业务项目实战笔记(三)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492558&idx=2&sn=995b7887879a439dd0dac0d801d82e4d&chksm=fd3ea55bca492c4dd856e6084a727a15359cac7f5278ac0bbbdacd57aec55296d3979bee5f09&scene=21#wechat_redirect)  \n*   [大数据入门：Spark+Kudu的广告业务项目实战笔记(四)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492558&idx=3&sn=d2273c79be1d99270d41270a2b12d872&chksm=fd3ea55bca492c4dbbc8ccaafc5375fbff8c01101cfca029f5f36d29985201001b83856204d6&scene=21#wechat_redirect)  \n*   [大数据入门：Spark+Kudu的广告业务项目实战笔记(五)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492558&idx=4&sn=e5f0d6e8168c2c51eafe4952dc41ba19&chksm=fd3ea55bca492c4d6261bfe591627118f2c203bb70ed86a43d03ef2b4d7cc952eef2b93a3235&scene=21#wechat_redirect)  \n*   [大数据入门：Spark+Kudu的广告业务项目实战笔记(六)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492558&idx=5&sn=9fd6dcc935f6d73a4c57166bc94212be&chksm=fd3ea55bca492c4d1b0d477f02a7f2f40b3944068720db7f7452a165719f6979fee0b1d48eb0&scene=21#wechat_redirect)  \n*   [Flink 1.11新特性之SQL Hive Streaming简单示例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492648&idx=2&sn=eaef19686a06d04efaf3d6f17a5ca0ba&chksm=fd3ea2bdca492bab691ce6e47d81387ba0a0cdc2350265ef5eb58e38b466237c549267022eb3&scene=21#wechat_redirect)  \n*   [SparkSQL 整体运行架构和底层实现](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492699&idx=3&sn=fb58aad4cc9cd6aad71ab581816f0d0b&chksm=fd3ea2ceca492bd8c7bb490611e1d930b5833ede83d198e61555bc3fdd5687c2a513e700a800&scene=21#wechat_redirect)\n\n**离线计算篇**\n\n*   [ORC文件存储格式的深入探究](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486981&idx=1&sn=9c8fc4c127d7e6108ac4e171e750d490&chksm=fd3d4890ca4ac186614f0dda8ffb2d35693a925b03861a01769c898652b53d0d436bca05ea12&scene=21#wechat_redirect)\n*   [Hadoop支持Lzo压缩配置及案例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486981&idx=1&sn=9c8fc4c127d7e6108ac4e171e750d490&chksm=fd3d4890ca4ac186614f0dda8ffb2d35693a925b03861a01769c898652b53d0d436bca05ea12&scene=21#wechat_redirect)\n*   [神策数据分享 | 标签体系应用与建设(文末附下载链接)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487175&idx=1&sn=c00064434ee03bef0b6aa07d2b9033a4&chksm=fd3d4852ca4ac144a310dc7d901bdf262e3c929ef7ab414e4de03602ed83b3901c098a00145b&scene=21#wechat_redirect)\n*   [环形缓冲区-Hadoop Shuffle过程中的利器](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487204&idx=1&sn=f344eb2641175828815075cd6e57520e&chksm=fd3d4871ca4ac167e7cd49e7938936e861ca5c14f048730487a7b225561e1794cd5d9e69905d&scene=21#wechat_redirect)\n*   [eBay | 实践Hadoop任务的性能翻倍之路](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487233&idx=1&sn=dae402bafe49cf98f1b112bca384d7e1&chksm=fd3d4994ca4ac08275d0a3c57efbfcaca370edeec62e7b5cc982f7299c2937103b7dea1af782&scene=21#wechat_redirect)\n*   [PDFT/Paxos/Raft-分布式一致性协议解析](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487287&idx=1&sn=e632bbf074c0d2c64a892a481872c6e7&chksm=fd3d49a2ca4ac0b4aa36391a8f36f24e49ebcf645a0da96042b76bb582f9d218aa9a57edfb7f&scene=21#wechat_redirect)\n*   [谈谈经典限流方法—漏桶、令牌桶与Guava RateLimiter的实现](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487294&idx=2&sn=03c969be724fe60b0b14f921cbcec8f6&chksm=fd3d49abca4ac0bdda91ceab31deca10f2059e2e37e34e60e17f54f358ab25c7c7bb93d4b840&scene=21#wechat_redirect)  \n*   [轻量级异步屏障快照（ABS）算法解析](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487294&idx=1&sn=8b808225b1b792e892ab737485492ac6&chksm=fd3d49abca4ac0bdc9ecc3eb45bc89c2cb8f4af7f3a66c0e9e6290d374279fd0569049996143&scene=21#wechat_redirect)  \n*   [Hadoop小文件利器Ozone](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487297&idx=2&sn=c5ef9a22eb4c3e3da9e5fc32fe90c949&chksm=fd3d49d4ca4ac0c29d739867fae2fe796ee9a2fc1aa29b85c30d34efbb167ac885e134af2053&scene=21#wechat_redirect)\n*   [数据指标体系建设](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487297&idx=2&sn=c5ef9a22eb4c3e3da9e5fc32fe90c949&chksm=fd3d49d4ca4ac0c29d739867fae2fe796ee9a2fc1aa29b85c30d34efbb167ac885e134af2053&scene=21#wechat_redirect)\n*   [Hbase FAQ热门问答小集合](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487338&idx=1&sn=00e93834aef4c73cd3cd2a487e3e6cc9&chksm=fd3d49ffca4ac0e948bb40ba97c5da9e7fa3ffb78afb6001f9d06603e6ac1938d7f06b5ee38d&scene=21#wechat_redirect)\n*   [设计HBase RowKey需要注意的二三事](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487369&idx=1&sn=6ad49150d939209cc59965be922ae5ed&chksm=fd3d491cca4ac00a82671803f60c1848932bc45aa649dcfe4f0f765fe24cc364a1bab81ec550&scene=21#wechat_redirect)\n*   [HBase优化笔记](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487378&idx=2&sn=f9d4400a0677a70a8df94cc0530831e3&chksm=fd3d4907ca4ac011d278ad227be1c902f1ab9408e227f27c6082cfc2580e74fe1788365f97b1&scene=21#wechat_redirect)\n*   [HBase生产环境优化不完全指南](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487391&idx=2&sn=420d44df8c68349212df31fa4471576d&chksm=fd3d490aca4ac01c7536e0f628cd82be97d5d9dba1b9619a0c637795731813cc36acb8c52079&scene=21#wechat_redirect)  \n*   [Hive SQL50道练习题](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487395&idx=1&sn=7fdb8625da8cccd43ceb07ea6554fe92&chksm=fd3d4936ca4ac020bdc48bdb7cfb55564e8d2cd99c39e785ce0d7eb86cb75b347bcf5914d0c9&scene=21#wechat_redirect)  \n*   [Hive on Spark参数调优小结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487410&idx=2&sn=c6be05a74ac472b962583a2228996316&chksm=fd3d4927ca4ac031a2b1976393d12c6a78ea81817aded17ee2fd57dcc6f22cd1c4272d3f0e1b&scene=21#wechat_redirect)\n*   [Hadoop(CDH)分布式环境搭建(简单易懂,绝对有效)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487523&idx=1&sn=6719f902998dd24a0904444d49f72ec7&chksm=fd3d56b6ca4adfa0a3279696b65467093e4035e94df802e7d44c64853634c1a387c3fc99e989&scene=21#wechat_redirect)\n*   [ConcurrentHashMap锁机制进化的考量](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487527&idx=1&sn=e9dee7c31da53fd9a27acc49e41e4753&chksm=fd3d56b2ca4adfa49fdc66d49c746359632b85714a1edd4986c3028fbc7a0228650fa4c41ee8&scene=21#wechat_redirect)\n*   [HBASE列族不能太多的真相](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492803&idx=2&sn=b7a718dea53b5912149fe61f9f396fe1&chksm=fd3ea256ca492b40cf52f812cef0c437610350777d17022edf264e2b7ee6f7b30164e9f84039&scene=21#wechat_redirect)  \n*   [基于ClickHouse的用户行为分析实践](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492803&idx=1&sn=04283aa0913674e158a8519120fc14cd&chksm=fd3ea256ca492b40f60ac90eee4d23f9c2b8156a9c83439297317efcb826b32779b762f554e4&scene=21#wechat_redirect)\n*   [HBase的系统架构全视角解读](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492822&idx=1&sn=175f6febc758a56c7aa680f5b953de08&chksm=fd3ea243ca492b55e723f64a81f8b334b97cbe3c80cd72da0e5695380f33f25c74b9b7996978&scene=21#wechat_redirect)\n*   [Kylin Cube构建原理+调优](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492864&idx=1&sn=597c5e992305482d2d49f801b10e8efd&chksm=fd3ea395ca492a839c9c53c6fdd5056d9cec123ebc0c7acc578569480140c3cd6bb7762ebf43&scene=21#wechat_redirect)\n*   [Hadoop支持Lzo压缩配置及案例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492923&idx=1&sn=6b5a0594031c40ff0e543aa6d9740cc6&chksm=fd3ea3aeca492ab887f8b95a711d3c1bbbd4c2438d941efe42329d713569bb42482342dd92ba&scene=21#wechat_redirect)  \n*   [Apache Hudi 架构设计和基本概念](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492943&idx=1&sn=fcd6d6e67b7ff0792976b063374306ac&chksm=fd3ea3daca492acc631a0db5d99c82603d7935888e13ea086a5825651132396f3d48db20c5b5&scene=21#wechat_redirect)\n*   [HiveSQL常用优化方法全面总结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492943&idx=1&sn=fcd6d6e67b7ff0792976b063374306ac&chksm=fd3ea3daca492acc631a0db5d99c82603d7935888e13ea086a5825651132396f3d48db20c5b5&scene=21#wechat_redirect)\n*   [MapReduce性能优化大纲](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488182&idx=2&sn=b2b8e50829f5706295fa5b5201743621&chksm=fd3d5423ca4add3516c141897124468ce51bfa86f599dd499e728bfcf18d8928fafaabac4d4f&scene=21#wechat_redirect)\n*   [从NoSQL运动谈分布式系统的CAP、BASE理论](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487582&idx=1&sn=6882c98d94b6e3a52132b30ff4e404ef&chksm=fd3d56cbca4adfddcbaf2ded2f540a2b32cf4e0cace93a4a4f6a2b00a0482f80deaa55f6e8f1&scene=21#wechat_redirect)  \n*   [HDFS读写数据过程原理分析](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487609&idx=2&sn=5b5fc64b4c2628f77cfd902228ec78cf&chksm=fd3d56ecca4adffa3d66802fcb091468f96a51730b4a095b6de32842d7325165f797da8b1bf7&scene=21#wechat_redirect)\n*   [数据中台建设五步法](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487665&idx=1&sn=5a0a007f58a7c9a6872a4b7c65683f08&chksm=fd3d5624ca4adf32159835c42768938c2e455c8354d1ca1bef731ab320a378eb8af002e3cd96&scene=21#wechat_redirect)  \n*   [Step by Step 实现基于 Cloudera 5.8.2 的企业级安全大数据平台 - Kerberos的整合](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488083&idx=2&sn=30a40939a9ba29a2c55a1c973433e8f4&chksm=fd3d54c6ca4addd0224cd352085966896d45d02e110db5c90142947053f3a7af21b7c1b6673e&scene=21#wechat_redirect)\n*   [一篇文章全面了解监控知识体系](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488492&idx=1&sn=6059d4f245afad8230ee71a96e8a3d4e&chksm=fd3d5579ca4adc6f9b66549757ea8e3ac6bacdcdd17185ac87073f6fbbc88827d0b789606aa3&scene=21#wechat_redirect)\n*   [Sqoop 使用shell命令的各种参数的配置及使用方法](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491557&idx=1&sn=3641e76946290e82f393f2660c07c8dd&chksm=fd3d5970ca4ad0666a3b2449f3d0444177aec2afdc7fd15998a8f006c99d5bb41e28f916fab7&scene=21#wechat_redirect)  \n*   [Hive小知识之分桶抽样](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491603&idx=2&sn=339b7498e6c632cc5663625ef13c5dc1&chksm=fd3ea686ca492f90e7085a444431dea40c1e7f30884b5df283b695f30d4f591a9dfaec32c7b5&scene=21#wechat_redirect)  \n*   [数据仓库和数据集市建模体系化总结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491647&idx=1&sn=51dd15b000803c1ab1d4cea67fc02e81&chksm=fd3ea6aaca492fbc6fc2fe34f39cb31a585b3c53b02ca0304e7edf63680521ba7036a0bbfdce&scene=21#wechat_redirect)  \n*   [Phoenix(云HBase SQL)核心功能原理及应用场景介绍](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491759&idx=1&sn=2d0b25585442176b0ad74235fe799611&chksm=fd3ea63aca492f2cf8f482a0f5bc88ed3034124d90d0234991a67a9ab1dd4e0086c60e0a83e8&scene=21#wechat_redirect)\n*   [基于实际业务场景下的Flume部署](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492053&idx=1&sn=594dd94d1bf6d692680d77107d83794a&chksm=fd3ea740ca492e56c2a838e8f6656dc7c9dea787add1c801b6ed379554162c696fcc5e625c1a&scene=21#wechat_redirect)\n*   [斗转星移 | 三万字总结Kafka各个版本差异](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492958&idx=1&sn=1ef59283fddfcffa14425d8d51af6f81&chksm=fd3ea3cbca492add3660dc37a37b458694cd285ae061884bb8aecbba2725fab3baee89608eba&scene=21#wechat_redirect)\n*   [Spark SQL自定义函数UDF、UDAF聚合函数以及开窗函数的使用](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490378&idx=1&sn=f45be2b9cb97e896afb1ded5d6c37fcc&chksm=fd3d5ddfca4ad4c90ae2a6c75eee49d5c894d44c3603a94bf117807091467162f2b843e0c43e&scene=21#wechat_redirect)  \n*   [SparkSQL用UDAF实现Bitmap函数](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490581&idx=2&sn=d819fc83a5f553687b5c79bd64cc9843&chksm=fd3d5a80ca4ad39607611d9dc07613b1a5da27570f7b8331e17400a6bf6a4285fbe9ce942a42&scene=21#wechat_redirect)\n*   [一文了解Kafka核心概念和角色](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490065&idx=1&sn=e8e9539eecc25ef7a5f8703fac277be3&chksm=fd3d5c84ca4ad5925f407f84546146a4e8f75d97b58adb98f9dca32ab8aa90b14e9d2211e0c9&scene=21#wechat_redirect)  \n*   [Apache Spark 内存管理详解](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490160&idx=1&sn=a0bf8702286a348d6e486be62d07751e&chksm=fd3d5ce5ca4ad5f34f643f46662ef37067c1199c28ca4b15ec958bb51b68edc992d3e99ff922&scene=21#wechat_redirect)\n*   [经典限流方法——漏桶、令牌桶与Guava RateLimiter的实现](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490460&idx=3&sn=b2d23ba7b20ea8cf2d155999ec879b75&chksm=fd3d5d09ca4ad41f1f7328d91bac921168207e5a5e1f4af78b9a392f626ab26c72998cd5457e&scene=21#wechat_redirect)  \n*   [ZooKeeper在HBase集群中的作用](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490460&idx=2&sn=8491948f284d3bc53a4c3be7f285aae7&chksm=fd3d5d09ca4ad41f797ca0b584b83668c78cf5f449207b42dcdb49e90dcd558b759b249be498&scene=21#wechat_redirect)  \n*   [从B+树到LSM树，及LSM树在HBase中的应用](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490581&idx=3&sn=cbf752e85e3cf865e5a673965f020bbd&chksm=fd3d5a80ca4ad39616be9a8d0c6daddfe72f69993a43d322fcdd1966f566bae38b836753aa95&scene=21#wechat_redirect)\n*   [Hadoop Namenode元数据持久化机制与SecondaryNamenode的作用详解](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490581&idx=1&sn=57492e477ea3360e53eac23a20c3b0ad&chksm=fd3d5a80ca4ad396b7b11cf00ca2a87ff1097692c527208ad6ada9151da88edd6542161c4bc6&scene=21#wechat_redirect)  \n*   [干掉ELK | 使用Prometheus+Grafana搭建监控平台](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490610&idx=1&sn=f435639d0392156082a997411bf42f6c&chksm=fd3d5aa7ca4ad3b1a239b8ac29f156ce7b6198cee7ec613e44dc27579b8bcf27fede954a2883&scene=21#wechat_redirect)\n*   [盘点：SQL on Hadoop中用到的主要技术](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488854&idx=1&sn=5b1419b1813e4695c0750445078012d4&chksm=fd3d53c3ca4adad5e0852f1d44e9fb0592bf82e7c635a2900dff196bb5172443e9ce5dccd07b&scene=21#wechat_redirect)\n*   [用HiveSQL计算连续天数问题的方法](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489143&idx=2&sn=c25af83fc4bdeb42fb7d4236669489e4&chksm=fd3d50e2ca4ad9f474758e8e5b004a531fded129a5ed5bc7bc549e50f3e72872c4048628d288&scene=21#wechat_redirect)\n*   [浅谈Linux cgroup机制与YARN的CPU资源隔离](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489342&idx=2&sn=5354a5fcb497f9a2294b61c707838411&chksm=fd3d51abca4ad8bde70e5980531cf3e63ba314ba000df4df7e10420a093c3f1f80063094c854&scene=21#wechat_redirect)\n*   [京东JDHBase异地多活实践](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489415&idx=2&sn=303860edceafc625b97fdeae6e8e7acf&chksm=fd3d5112ca4ad804637f73ccd9d1177d5bb3521f0cda68cbabccd7fe2ab7047f4fd05563e6c2&scene=21#wechat_redirect)  \n*   [Kafka的分区数是不是越多越好？](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489433&idx=1&sn=cfe0e855bf0addfd06599cc4c4a6f61c&chksm=fd3d510cca4ad81a87c0a2f81e74eabbf55928e38ec605e8870ea96dc0bc5926302de51200ae&scene=21#wechat_redirect)\n*   [一文俯瞰Elasticsearch核心原理](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490187&idx=2&sn=c8cb0f332019d1bf73ac6d92ed913dbc&chksm=fd3d5c1eca4ad5089cb8949fd1ee6e51c7d8622cf691edfc759836591435f9246a0b729a6b2b&scene=21#wechat_redirect)  \n*   [不可不说的Java\"锁\"事](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490378&idx=2&sn=e3f62731d911f230463581dae96f7c49&chksm=fd3d5ddfca4ad4c9847a8effc42e9a63e019e2e25712706bc0c3816237bb9bd76fec411bdf2e&scene=21#wechat_redirect)\n\n**数据仓库篇**\n\n*   [一文了解数据库和数据仓库](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486890&idx=1&sn=004a86c329439558a4f234fa21744173&chksm=fd3d4b3fca4ac229c6af919153949d4bc60c603635fc432d5695c4ee14c61bd6408a6188ef22&scene=21#wechat_redirect)\n*   [数据仓库系统的实现与使用(含OLAP重点讲解)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486892&idx=2&sn=8e9a2c30a596cde9713601a22875af63&chksm=fd3d4b39ca4ac22f082760e7d4c8e0c4c7979e7431f9063215a8669cc5724adcc843df01b1e0&scene=21#wechat_redirect)\n*   [Data Lake 三剑客—Delta、Hudi、Iceberg 对比分析](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487196&idx=1&sn=313ceebfbf288e309f8e5da00451af5b&chksm=fd3d4849ca4ac15f800f6eebc95e597e47cafad3bfe1e0413f83db88ea7530448ad55b5d7680&scene=21#wechat_redirect)\n*   [数据也有温度？Elasticsearch 5.x 版本中的冷热数据架构](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489275&idx=2&sn=1c9783692bc3f9cceb3f403702057744&chksm=fd3d506eca4ad978f6d28a3f933b68367d80566e93a391617996f42c60d1398912c0a73908a7&scene=21#wechat_redirect)  \n*   [数据冷热分离技术](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489301&idx=1&sn=aa613e6311ae45487286c24f2134aa8d&chksm=fd3d5180ca4ad89606221360a1e7e35f5259d5a2eea4c1b4cf3e0fd0cee2d74b6579ea043546&scene=21#wechat_redirect)\n*   [冷热数据分离 | Alluxio元数据管理策略](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490610&idx=2&sn=c1751ded1965049fabd61df82484c4b3&chksm=fd3d5aa7ca4ad3b140d5ce00fa06b75e3577cb5bf9e3a944f25ca49c74e03072a03562d635cc&scene=21#wechat_redirect)  \n*   [数据之眼 | 数据探查服务的设计](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490691&idx=2&sn=2c8059004ba6086edb34e901efa5fdc0&chksm=fd3d5a16ca4ad30038888daa1ebf8f65be386cd3ba7ae2ba4fadc1f1eed24a9edc863ef10dc1&scene=21#wechat_redirect)  \n*   [元数据存储系统管理演变升级](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247490691&idx=1&sn=e77e2a1058cf3b9e7015a6451331e406&chksm=fd3d5a16ca4ad300b30467cf550015b6c2474ff18f5093dc1f2a954f1115dc4ee3fc33aeda99&scene=21#wechat_redirect)\n*   [数据湖 | 一文读懂Data Lake的概念、特征、架构与案例](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492241&idx=2&sn=a1a1c35ac0a71d648ac773b700c0478b&chksm=fd3ea404ca492d128493132600f9cebb2b34a3dc395fe4eb6db02d9a018ace5adcd171f6941a&scene=21#wechat_redirect)\n*   [用户行为数据采集系统](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488529&idx=1&sn=bc81e0fb68c13626c9202f1942fb3a80&chksm=fd3d5284ca4adb922308718382820ac48e4dcdf819ef8d2b672ee919cf6b67c4aa7a70053539&scene=21#wechat_redirect)  \n*   [创业公司数据仓库的建设](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488580&idx=3&sn=6152e6acfe7b600c8d8e4a2138c37aa9&chksm=fd3d52d1ca4adbc72ea737f35c9512fa8a3ef9e9733c14c57c1b269a6798e5a04dd285912d77&scene=21#wechat_redirect)\n*   [Kylin使用Spark构建Cube](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487697&idx=1&sn=b085af6dd063fa66d017ae8f96bc94af&chksm=fd3d5644ca4adf52f0bc616917ff1a817328ed1bb39a01a04fc839164a2705cf92e0ddcad6dd&scene=21#wechat_redirect)\n*   [实时统计分析系统-Apache Druid](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488311&idx=1&sn=f6a8c052d49c6abb0fc7956a2066f4c2&chksm=fd3d55a2ca4adcb4d5f1f5250013fb38aae2e75510f95364cbb58d4ea4f985b0dac7869b5fba&scene=21#wechat_redirect)\n*   [Elasticsearch索引和检索优化与压测监控总结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489054&idx=2&sn=2ec1061def576779947d31eee1c5ce74&chksm=fd3d508bca4ad99d7ea476c9ea70da6df33e3099a55faace760233e6833e8086ac26036420a9&scene=21#wechat_redirect)\n\n**面试题篇**\n\n*   [我们常说的海量小文件的根源是什么？](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486931&idx=2&sn=1c5987f3bad7a805895484ebfd683e11&chksm=fd3d4b46ca4ac250d31502a76f0cfd02ebea29d1161b9e9faec23ea6aa8947d0dd7d4268427f&scene=21#wechat_redirect)\n*   [如果你在准备面试，好好看看这130道题](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486986&idx=1&sn=422d1a3c11c72ff97b32cc01142839f4&chksm=fd3d489fca4ac1895242ab94b932b12c65dc57b5f3a16acc7084dc8a189e9026290245a64c4f&scene=21#wechat_redirect)\n*   [你可能需要知道的Kafka面试题与部分答案整理](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486992&idx=1&sn=8950101b408b0e02b7957f6d74c9cd74&chksm=fd3d4885ca4ac193a28d3f10f9d25c39ec57c294d51961cac69fdf8bacbcac46a989342d2136&scene=21#wechat_redirect)\n*   [28道关于ZooKeeper的面试题](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487021&idx=2&sn=bbfa3fb95e4fa3d0b6ae90a8933119ed&chksm=fd3d48b8ca4ac1ae0542948f3abaa00e69ca7b20806699c7a8dec7a13ddfb79a981ca3b45c90&scene=21#wechat_redirect)\n*   [【数据白皮书重磅分享】推荐|埋点|用研|标签](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487177&idx=2&sn=dea2d55951494a5bf4438ec5a888bdd7&chksm=fd3d485cca4ac14ac7ae87e6a8bf11d988e068c3548762cab91f11f45855431e8f759e131a76&scene=21#wechat_redirect)\n*   [一份优秀的简历该长成什么样](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487297&idx=1&sn=9b007050efaf50f17e3f46915c70f4ea&chksm=fd3d49d4ca4ac0c2b0e25aae9e4aba71c9cc18e6f388d1dd6615791a7d2432445c32d0039eaa&scene=21#wechat_redirect)  \n*   [1万2千字长文助力春招 | Netty面试篇](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487309&idx=2&sn=a33cc56116cb7891145394f9c5353be3&chksm=fd3d49d8ca4ac0ce28cd0a543109d60db47e8be60d63c6951fa30e4ab4c812f9025286fa5c37&scene=21#wechat_redirect)  \n*   [消息队列面面观](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487314&idx=1&sn=f832be362c7c2ea877c48d47923434c5&chksm=fd3d49c7ca4ac0d1444e8352b35ea3a706cdf496add814cf0fe3aa4844f4c25dc2f4d6c269f1&scene=21#wechat_redirect)\n*   [关于技术面试的一点点体会](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487326&idx=1&sn=1ba5867700bdfb5ad7f808a245ae3b1f&chksm=fd3d49cbca4ac0dd7d8401e1226ff054441709af6f0cacc3ceeab7354ac6f7452592239efb06&scene=21#wechat_redirect)\n*   [早点建立自己的知识体系](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487334&idx=1&sn=ce3453c1b63d6932438c8412fc5454d3&chksm=fd3d49f3ca4ac0e5ab43ecb1cc9be9320232bb0234b3c43b0903eca9e225c338b5c6990923cc&scene=21#wechat_redirect)  \n*   [Filter(过滤)|Project(映射)|Pushdowns(谓词下推)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487414&idx=2&sn=2afda2b854a4f875159a43903bb7b337&chksm=fd3d4923ca4ac035b7627c9a32b7fa003f817176d959aae98770416cf3a55384efdde060b03f&scene=21#wechat_redirect)  \n*   [阅读源码｜Spark 与 Flink 的 RPC 实现](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487449&idx=3&sn=5b3d8564d1c84b14f9d941c96d8f6a27&chksm=fd3d494cca4ac05a83b67a04ac5497bb13ecf79a3a5044083d0bf9e93d8bdacd357fa1ffce1e&scene=21#wechat_redirect)  \n*   [三万六千字通关MySQL面试](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487449&idx=1&sn=2cc97bf6669416267c25a7a46192b706&chksm=fd3d494cca4ac05a25e3e91a52d105bba187896cdf322ebbf82b7ce88db8f8a3ed25070cad77&scene=21#wechat_redirect)  \n*   [深入理解CAP理论和适用场景](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487458&idx=1&sn=11e0b029998ecdeccda8e18c1e43f980&chksm=fd3d4977ca4ac0613b5a32d6bed036231239f40021afa15a0b45cab6426a9afdcca7e9d7844c&scene=21#wechat_redirect)  \n*   [HDFS的SecondaryNameNode作用，你别答错了](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487464&idx=1&sn=c7358495832da5e4bf4807691be6ef29&chksm=fd3d497dca4ac06b1caa9b6a0746d01dd4dd44203d57e929193fa79053232c13b1668615f8e7&scene=21#wechat_redirect)\n*   [Kafka三种可视化监控管理工具Monitor/Manager/Eagle](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491086&idx=1&sn=f2f9bd1a55064ec7abf63666bf54be61&chksm=fd3d589bca4ad18d6e3f353c1d63cee77650d176b582c3f84b7ead45170e0c5523a0a472b1eb&scene=21#wechat_redirect)\n*   [Kafka体系架构详细分解](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491138&idx=1&sn=f42cac6584ca4c3b74f232e897b33206&chksm=fd3d58d7ca4ad1c19e851a02b88e6e8350e29e8a57f71acc5def38bc14ed5a0f5110122f9927&scene=21#wechat_redirect)\n*   [Kafka笔记—可靠性、幂等性和事务](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491138&idx=2&sn=a67e3044fd368e61a0290679e2765661&chksm=fd3d58d7ca4ad1c1cf2c6e79747b77005dd811e6d385c7b67a3970e6de74fbd1f307f06a658a&scene=21#wechat_redirect)\n*   [Kafka体系架构详细分解](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491138&idx=1&sn=f42cac6584ca4c3b74f232e897b33206&chksm=fd3d58d7ca4ad1c19e851a02b88e6e8350e29e8a57f71acc5def38bc14ed5a0f5110122f9927&scene=21#wechat_redirect)\n*   [Kafka面试题系列(进阶篇)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491176&idx=1&sn=81b44db6db3cb014dc9002ba840ad283&chksm=fd3d58fdca4ad1ebdb2aa17b6ffce1794daef5def22cdc84a7f280a107cba966658293cb14b6&scene=21#wechat_redirect)\n*   [Kafka面试题系列(基础篇)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491156&idx=2&sn=e18e2ae54f08ee34749fdb314652c2fe&chksm=fd3d58c1ca4ad1d777b950bf01f33f36dff4e8bc55779f3a1a50019136fd6d345148188e41db&scene=21#wechat_redirect)\n*   [面试知识点 | Kafka的数据存储与索引设计](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492699&idx=2&sn=776b93fb683a004d58d432d9b6b551b4&chksm=fd3ea2ceca492bd8f72e397ec63b331aa129f74f803be9f922633df01fdb552adb75371d3dad&scene=21#wechat_redirect)\n*   [面试必考点:HBase Compaction机制](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489094&idx=1&sn=76958401eb4fab0a69447b37bda2b4c9&chksm=fd3d50d3ca4ad9c5330d003ee5c8265e8a49f04bbccd9a9513cf2ec2f983b6d45842dae41c30&scene=21#wechat_redirect)\n*   [ZooKeeper需要关注的点](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488393&idx=1&sn=f820923ae3e782b509911f787a455a91&chksm=fd3d551cca4adc0a1caa74c53e628b5cf0a5ebe5a0d5584f899c550068eab70b483e5b796f6d&scene=21#wechat_redirect)  \n*   [MySQL中InnoDB及索引深入剖析](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491256&idx=2&sn=34535df6790951e7de0e08bdfceed108&chksm=fd3d582dca4ad13bc76b32b252995a979cd9ae888d510e2201e6c1a2ac169c1cccbf30009ab4&scene=21#wechat_redirect)  \n*   [Kafka面试题系列(进阶篇2)](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491256&idx=1&sn=0bd327392de5e2241b550a12eda43f29&chksm=fd3d582dca4ad13bf53916a5e693a93e2c526b63be165f06732f04940e1d8517803c1e81a322&scene=21#wechat_redirect)  \n*   [MySql的Binlog日志工具分析：Canal、Maxwell、Databus、DTS](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491276&idx=1&sn=133e0d6bcf897ae288a83f1ac6e7456c&chksm=fd3d5859ca4ad14f7d5e9898a335e78e1a9cf163303818e9a61eeb5785b4a02a12c5b86e5f1c&scene=21#wechat_redirect)  \n*   [Redis中的管道Pipeline操作](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491365&idx=3&sn=a1efd7804ceeeb5b5980ade4a7c75d90&chksm=fd3d59b0ca4ad0a6eedfb7333b30bf66f2f3da7a3adab8242a5c7ea949803d101b4f386b7b95&scene=21#wechat_redirect)  \n*   [查看YARN任务日志的几种方式](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491365&idx=2&sn=fdb72a0d6ac9b5f4723f8362e42acd9b&chksm=fd3d59b0ca4ad0a6ccf7e8ebd1ce5d931962575adba97281db5d6f5becc8f98781c543a49d4d&scene=21#wechat_redirect)  \n*   [Yarn 使用 Cgroup 实现任务资源限制](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491453&idx=2&sn=df9b9bf0127e3c4648eec90ef3499306&chksm=fd3d59e8ca4ad0fe12fa54cf22baea1223bfc944436e98ec89b7b61b75d1a0ed05dbf99f31ac&scene=21#wechat_redirect)  \n*   [分析和定位线上作业 OOM 问题利器-MAT](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491453&idx=1&sn=49e762ec986faf408c03b4bc52634872&chksm=fd3d59e8ca4ad0fe38d0827899e82865b9c913524b3c71913ffc596aad0f9292239a29f704b2&scene=21#wechat_redirect)\n*   [浅谈ZooKeeper中Kafka相关信息的存储](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487838&idx=1&sn=a82d191e12dc2590fc376258965e34f6&chksm=fd3d57cbca4adedd518eb3944835a2629e9e9cb77ec5fdaf127d87b6a7bb847780b700115020&scene=21#wechat_redirect)\n*   [JVM架构体系与GC命令小总结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488882&idx=1&sn=62cc37f20fdce1193f2400507b7d478a&chksm=fd3d53e7ca4adaf181d221711806599b49e165d3b73b1913fb703ff24aa23e95526413549897&scene=21#wechat_redirect)\n\n**其他**\n\n*   [腾讯如何用Elasticsearch挖掘万亿数据价值？](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487021&idx=1&sn=b9c9feaceae4d6315b801c021964c9cf&chksm=fd3d48b8ca4ac1aea20937d29058ab1ebabd480bf15e37a41dec52d4c7d712342be7f963f33e&scene=21#wechat_redirect)\n*   [Apache Beam 大数据处理一站式分析](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487555&idx=1&sn=4f46c221e6830c25371ed239bb7bb12c&chksm=fd3d56d6ca4adfc08dc936586a9d82d0b44e8b584ec21dd83f081d5c7a40a1ef2d4c52733384&scene=21#wechat_redirect)  \n*   [Apache Hudi 架构设计和基本概念](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492943&idx=1&sn=fcd6d6e67b7ff0792976b063374306ac&chksm=fd3ea3daca492acc631a0db5d99c82603d7935888e13ea086a5825651132396f3d48db20c5b5&scene=21#wechat_redirect)\n*   [Apache Hudi 架构设计和基本概念](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492943&idx=1&sn=fcd6d6e67b7ff0792976b063374306ac&chksm=fd3ea3daca492acc631a0db5d99c82603d7935888e13ea086a5825651132396f3d48db20c5b5&scene=21#wechat_redirect)\n*   [MySQL8.0发布，你熟悉又陌生的Hash Join？](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486990&idx=1&sn=9c8a238ca63dc52af251bbf03aafc75c&chksm=fd3d489bca4ac18d5fa9130df79ae4f7ccc9ad34ab99b91cc8fb80b9a2542d1987f6447d3e39&scene=21#wechat_redirect)\n*   [转载一个看不懂的文章：F1 Query](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487002&idx=1&sn=97bd47e0f4d3ee666c1a624872a358f0&chksm=fd3d488fca4ac199fbf0352b1df513d968d000a1ce4c38b3c16b480776da2827857f4a750215&scene=21#wechat_redirect)\n*   [Apache Hudi | 统一批和近实时分析的增量处理框架](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487043&idx=1&sn=91fcb6e573404cac6a89a60603cd138d&chksm=fd3d48d6ca4ac1c0d325a63bb25eba3df146e423d6ff484869cb76dff970c8dd21cc1f047a6f&scene=21#wechat_redirect)\n*   [寻找5亿次访问中，访问次数最多的人](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487056&idx=1&sn=27f648d927c248204f449f33c8fbc8bc&chksm=fd3d48c5ca4ac1d36785402c263924006ea6c4232565dca1dce490763bae5acc455aec8d537e&scene=21#wechat_redirect)\n*   [聊聊阿里巴巴的全链路压测](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487066&idx=1&sn=49a9a99927bb4223bb40080106cf4a91&chksm=fd3d48cfca4ac1d99976d5d7cc8ba6426b02bedb61a29657d262830cb334f02aed3834d74e49&scene=21#wechat_redirect)\n*   [年轻人你渴望力量吗 | 我读过的一些书推荐](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487080&idx=1&sn=71145f8a66f3d16d1e29526f81eeeb5f&chksm=fd3d48fdca4ac1eb7e00744bac1b03ac3e0e7a342675c3dc7234f182327cd2a4904a3e33d7e4&scene=21#wechat_redirect)\n*   [数据算法之反转排序 | 寻找相邻单词的数量](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487116&idx=1&sn=7ea20fdf1134eaceab3e36f03cb061c8&chksm=fd3d4819ca4ac10fc438c8efff61f744b2e1ebfb91c85c365d5f0b43e029ae5068d319ace90d&scene=21#wechat_redirect)\n*   [MySQL Binlog同步HDFS的方案](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487212&idx=1&sn=a42d9dba3c7a34df7134292a26793c3f&chksm=fd3d4879ca4ac16f9777aceb3ee3d09e271dab155a7ba4d27e5a07af4278b406bd10fceefc44&scene=21#wechat_redirect)\n*   [循环查询数据的性能问题及优化](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488638&idx=2&sn=8850f47e776a4bd9e6945c142dd4f74d&chksm=fd3d52ebca4adbfde14a5d24300338d1db4b231203f778ed242855cbbe95d17b5ffcfea735ee&scene=21#wechat_redirect)\n*   [推荐系统 embedding 技术实践总结](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488827&idx=1&sn=b8186e482bdf34ce24cc161df46f290c&chksm=fd3d53aeca4adab8810b5f96f2c08b996ca524ca7978ada3d529f5e617c73d89a21c7bcbc544&scene=21#wechat_redirect)\n*   [Prometheus+Clickhouse实现业务告警](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489470&idx=1&sn=734886246f2413f72d605cad726b59f2&chksm=fd3d512bca4ad83d516e5d6f0dd4c21c83867de758238277a162fecad0ec21a9eff9a3a59c23&scene=21#wechat_redirect)  \n*   [亿级用户的分布式数据存储解决方案](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489519&idx=2&sn=5c1b1a4738e2de958920d6f6adc99e95&chksm=fd3d517aca4ad86cc64260dcd41cd939cde52b6c3ebf0fa5322023ef6396ecf8cb24f0c392b4&scene=21#wechat_redirect)  \n*   [基于大数据的用户画像构建小百科全书](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489571&idx=1&sn=56a634d66fb689907b4ab51ed2d3707a&chksm=fd3d5eb6ca4ad7a0cc5fa4f895354e58ed7f2cb8558369ed6149560a5e7fca97b8545036fe87&scene=21#wechat_redirect)  \n*   [魅族持续交付平台建设实践](http://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489915&idx=1&sn=4c926ceac7fe3f0fd55eafa07ce0b223&chksm=fd3d5feeca4ad6f8f3583a541c306575a80d1f5e846e8b8e9561cb938d86e0c3bda460eeb7c9&scene=21#wechat_redirect)\n\n### 八、2021精品文章合集\n\n#### 2021精品文章合集\n\n**汇总部分**\n\n * [八千里路云和月|从零到大数据专家学习路径指南](https://mp.weixin.qq.com/s/MAwD-UJgvIa_dZjmaykqrQ)\n * [我们在学习Flink的时候，到底在学习什么?](https://mp.weixin.qq.com/s/xh4SEX9t-fRVdoiAl0KKSQ)\n * [我们在学习Spark的时候，到底在学习什么？](https://mp.weixin.qq.com/s/pN0AqNJuFnlLjNW2OonGtA)\n * [一线互联网公司面试进阶全攻略](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247499539&idx=1&sn=26b8569ae4e3d5c6c1a1b8a98807f625&scene=21#wechat_redirect)\n * [【大数据成神之路】第一版更新完毕](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247499250&idx=1&sn=ca900c226738048c28b56ae5c7b8f723&chksm=fd3ebb67ca4932712f602302ddc72aa616ff462a9c11605d6becbeaf347b5e7b1b2be2172cea&token=1917475764&lang=zh_CN&scene=21#wechat_redirect) \n * [关于技术面试的一点点体会](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487326&idx=1&sn=1ba5867700bdfb5ad7f808a245ae3b1f&scene=21#wechat_redirect)\n * [早点建立自己的知识体系](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487334&idx=1&sn=ce3453c1b63d6932438c8412fc5454d3&scene=21#wechat_redirect) \n \n**专题部分**\n \n**Hadoop系列**\n \n * [最新Hive/Hadoop高频面试点小集合](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501553&idx=1&sn=487e44e8fb362e97a0d418dff62de8bb&scene=21#wechat_redirect) \n * [Hadoop所支持的几种压缩格式](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484567&idx=1&sn=36e47d34cbdd7b5f0bf93f3bb5338c90&scene=21#wechat_redirect) \n * [【大数据面试之对线面试官】MapReduce/HDFS/YARN面试题70连击](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247497849&idx=1&sn=a82f9787765436b5514fe77aa6aa020f&scene=21#wechat_redirect) \n * [HDFS的SecondaryNameNode作用，你别答错了](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487464&idx=1&sn=c7358495832da5e4bf4807691be6ef29&scene=21#wechat_redirect) \n * [Yarn 使用 Cgroup 实现任务资源限制](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491453&idx=2&sn=df9b9bf0127e3c4648eec90ef3499306&scene=21#wechat_redirect) \n * [查看YARN任务日志的几种方式](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491365&idx=2&sn=fdb72a0d6ac9b5f4723f8362e42acd9b&scene=21#wechat_redirect)\n * [大数据哔哔集20210106 - Hadoop3.0有哪些新特性](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247498514&idx=2&sn=76808f8610e57360aced9c08b6e93c11&chksm=fd3ebd87ca49349155ddc062a4d812980f4b0075c65d448200136acdc903d86f2fc078972d72&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n\n**Hive系列**\n\n*   [Hive性能调优 | 数据倾斜](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247493923&idx=1&sn=bdb745dc04af43e1dc9e476cd1d67a86&scene=21#wechat_redirect)\n*   [面试必备技能-HiveSQL优化](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484617&idx=1&sn=cd734b7b04e0278f4445a09204d6a2bf&scene=21#wechat_redirect)\n*   [Hive常用参数调优十二板斧](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247494838&idx=1&sn=5f2aeba4509603bf50742987aff54469&chksm=fd3eaa23ca492335ae6e436806fd8de3da919766c80d661bb468c17a61559d034a2a16b2ec93&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n\n**HBase**\n\n*   [面试必考点:HBase Compaction机制](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247489094&idx=1&sn=76958401eb4fab0a69447b37bda2b4c9&scene=21#wechat_redirect)\n*   [Hbase性能优化百科全书](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247497379&idx=1&sn=f712373bc5d4826f255f8b47c4e7ff71&chksm=fd3eb036ca493920840f1c7808124b07e838c17b33b7f0188fa78fbb9877fdd8cade45563ec8&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n\n**ES等**\n\n*   [触类旁通Elasticearch之吊打同行系列：原理篇](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247494711&idx=1&sn=4846c2665cce5eb20e4a98af0b1b6ac6&chksm=fd3eaaa2ca4923b4619a40d63a89b0818b22df8f98e68d4382ac77a56f921a93f14ef1aeeaf2&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [触类旁通ElasticSearch之吊打同行系列：操作篇](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247494765&idx=1&sn=600f78c4cdf5875ed9524e737c55a8fb&chksm=fd3eaaf8ca4923ee3aa3907538f3b7fe06eca67b49003a6002f5d65e80800645914497d9159b&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [触类旁通Elasticearch之吊打同行系列：搜索篇](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247494808&idx=1&sn=67aa00f4fff4486825bea7a5bd3956c5&chksm=fd3eaa0dca49231b88c5a1bbb083bd9ea89aa20b1d1d4edb0f409ad4a192fcd6d87eaafba346&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n    \n\n**Kafka/消息队列**\n\n*   [面试知识点 | Kafka的数据存储与索引设计](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492699&idx=2&sn=776b93fb683a004d58d432d9b6b551b4&scene=21#wechat_redirect)\n*   [Kafka面试题系列(基础篇)](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491156&idx=2&sn=e18e2ae54f08ee34749fdb314652c2fe&scene=21#wechat_redirect)\n*   [Kafka面试题系列(进阶篇)](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491176&idx=1&sn=81b44db6db3cb014dc9002ba840ad283&scene=21#wechat_redirect)\n*   [Kafka面试题系列(进阶篇2)](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491256&idx=1&sn=0bd327392de5e2241b550a12eda43f29&scene=21#wechat_redirect)\n*   [关于MQ面试的几件小事 | 消息队列的用途、优缺点、技术选型](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485312&idx=2&sn=97c7d499760154a98cd1bfa416697a42&scene=21#wechat_redirect)\n*   [关于MQ面试的几件小事 | 如何保证消息不丢失](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485321&idx=3&sn=2e6769937df43c3555213bb17d8605fe&scene=21#wechat_redirect)\n*   [关于MQ面试的几件小事 | 如何保证消息按顺序执行](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485321&idx=2&sn=73e56ed1f46fd3337e13ee359631adc9&scene=21#wechat_redirect)\n*   [一道真实的阿里面试题 | 如何保证消息队列的高可用](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485184&idx=1&sn=19680883eb2b9739f834d743e5304179&scene=21#wechat_redirect)\n*   [你可能需要知道的Kafka面试题与部分答案整理](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486992&idx=1&sn=8950101b408b0e02b7957f6d74c9cd74&scene=21#wechat_redirect)\n*   [消息队列面面观](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487314&idx=1&sn=f832be362c7c2ea877c48d47923434c5&scene=21#wechat_redirect)\n*   [Kafka三种可视化监控管理工具Monitor/Manager/Eagle](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491086&idx=1&sn=f2f9bd1a55064ec7abf63666bf54be61&scene=21#wechat_redirect)\n*   [Kafka体系架构详细分解](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491138&idx=1&sn=f42cac6584ca4c3b74f232e897b33206&scene=21#wechat_redirect)\n*   [Kafka笔记—可靠性、幂等性和事务](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491138&idx=2&sn=a67e3044fd368e61a0290679e2765661&scene=21#wechat_redirect)\n*   [Kafka体系架构详细分解](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491138&idx=1&sn=f42cac6584ca4c3b74f232e897b33206&scene=21#wechat_redirect)\n*   [Kafka面试题系列(进阶篇)](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491176&idx=1&sn=81b44db6db3cb014dc9002ba840ad283&scene=21#wechat_redirect)\n*   [Kafka面试题系列(基础篇)](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491156&idx=2&sn=e18e2ae54f08ee34749fdb314652c2fe&scene=21#wechat_redirect)\n*   [面试知识点 | Kafka的数据存储与索引设计](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492699&idx=2&sn=776b93fb683a004d58d432d9b6b551b4&scene=21#wechat_redirect)\n*   [斗转星移 | 三万字总结Kafka各个版本差异](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492958&idx=1&sn=1ef59283fddfcffa14425d8d51af6f81&chksm=fd3ea3cbca492add3660dc37a37b458694cd285ae061884bb8aecbba2725fab3baee89608eba&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n\n**Spark**\n\n*   [面试必知的 Spark SQL 几种 Join 实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491813&idx=2&sn=557cc24073bef28f4f3589b63f71c967&scene=21#wechat_redirect)\n*   [面试注意点 | Spark&Flink的区别拾遗](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486577&idx=1&sn=49fd0138ad9837192b6eb78cbdc478e6&scene=21#wechat_redirect)\n*   [Spark如何协调来完成整个Job的运行详解](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501609&idx=2&sn=d92b8fee97a0832ae6aa8dab82a7a9c3&chksm=fd3e81bcca4908aa29f2d1bf4145f3163933a74a5f70b6b13f33811488c57517cb660fb92f84&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [独孤九剑-Spark面试80连击(上)](https://blog.csdn.net/u013411339/article/details/100179471)\n*   [独孤九剑-Spark面试80连击(下)](https://blog.csdn.net/u013411339/article/details/100179469)\n*   [Spark的Cache和Checkpoint区别和联系拾遗](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501217&idx=1&sn=75ab0ec8b505b87aac9ee37e2328db9b&chksm=fd3e8334ca490a226fbe5e779de27c43fd806f6c6d64e66d9efd8429a7f4b3acc9f4447a9157&scene=21#wechat_redirect)\n*   [Spark Job 逻辑执行图和数据依赖解析](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501259&idx=1&sn=8716f0fcb9b85d36bdfe69039983c518&chksm=fd3e835eca490a48cb02d75e7d5183961461546510c0c4b9f7edee01954c92f9bf2de4432a53&scene=21#wechat_redirect)\n*   [Spark Job 物理执行图详解](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501309&idx=1&sn=2a3edaed6845031acac7cc5c377bdd53&chksm=fd3e8368ca490a7e2b580983aba4aff7c6acadfba5f4863a776ed6cc3f63839353dbc935344e&scene=21#wechat_redirect)\n*   [Spark Shuffle过程详解](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501347&idx=1&sn=1809c8d80c4af0772b7eef999e2b84d7&chksm=fd3e80b6ca4909a028b29e014fb1e7a1de10203277baa57ded90507668fd429d8ae6690c31aa&scene=21#wechat_redirect)\n    \n\n**Flink**\n\n*   [【大数据面试题】Flink企业级面试题60连击](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247497541&idx=1&sn=521d3f59d7c5bfe56607ccc0867c7575&scene=21#wechat_redirect)\n*   [全网第一|Flink学习面试灵魂40问答案](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486150&idx=1&sn=691f8cab1ba9d72984c8e0250ab24b79&chksm=fd3d4c53ca4ac545eb01a6781c4317bc3a3f5e5c2129bd2d57e5e2ea53edcbeeae2acd2a4ed6&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [面试别人说他熟悉Flink，我问了他Flink如何实现exactly-once语义](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485260&idx=1&sn=7af0f27520b6fd74adf424d4d64f032d&scene=21#wechat_redirect)\n*   [阅读源码｜Spark与Flink的RPC实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487449&idx=3&sn=5b3d8564d1c84b14f9d941c96d8f6a27&scene=21#wechat_redirect)\n*   [Flink性能调优小小总结](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501028&idx=1&sn=40e36b5e0c58d909e7933622de0e53d0&chksm=fd3e8271ca490b6753b9c34671c4cf07c88ca953ddbe723abf5e28324f5248044f372f48abd4&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [生产上的坑才是真的坑 | 盘一盘Flink那些经典线上问题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247500470&idx=1&sn=a9ddfabd545a52e463dbc728fdaa872d&chksm=fd3e8423ca490d3578b802859924a5de96038e5f114788443f10763b90cea4955ff352982e5a&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [我们在学习Flink的时候，到底在学习什么？](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247499604&idx=1&sn=d938dfb30d221774704982d2938b30c1&chksm=fd3eb9c1ca4930d76a391241333de461ca22d2aa27472eab3cffab1564872ae37f1b48fe2d3c&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n    \n\n**数据仓库/数据湖**\n\n*   [数据湖存储架构选型](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501955&idx=2&sn=0732c077a76a99b67bafa57e3dedbf3e&chksm=fd3e8e16ca4907003d62f768b629f7f2a99c14f073cdd4a117559ec755d5876eec814a3fd9f7&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [数据湖架构、战略和分析的8大错误认知](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247501798&idx=1&sn=ee75c1c067833cf0cb285e99e71a91e4&chksm=fd3e8173ca4908655af05c56447dd728d14f0cdde511b4675c4ba12bbbb293e9060a7911972a&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [数据湖在大数据典型场景下应用调研个人笔记](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247500604&idx=1&sn=75e2492e954fbaac868afacc9397328d&chksm=fd3e85a9ca490cbfd5d10b6feb743ad172f09bc87c357bdc88b5be8b8fc39a7ee3581c3a0f81&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [数据湖VS数据仓库？湖仓一体了解一下](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247496181&idx=1&sn=90753808e8dd92a6d9fb95d29a9925a2&chksm=fd3eb760ca493e769275c92e26812154314222849bc96dde2ed6ca2b568ceaef493f277864c5&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n    \n\n**后端相关**\n\n*   [三万六千字通关MySQL面试](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487449&idx=1&sn=2cc97bf6669416267c25a7a46192b706&scene=21#wechat_redirect)\n*   [分析和定位线上作业 OOM 问题利器-MAT](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491453&idx=1&sn=49e762ec986faf408c03b4bc52634872&scene=21#wechat_redirect)\n*   [浅谈ZooKeeper中Kafka相关信息的存储](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487838&idx=1&sn=a82d191e12dc2590fc376258965e34f6&scene=21#wechat_redirect)\n*   [JVM架构体系与GC命令小总结](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488882&idx=1&sn=62cc37f20fdce1193f2400507b7d478a&scene=21#wechat_redirect)\n*   [深入理解CAP理论和适用场景](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487458&idx=1&sn=11e0b029998ecdeccda8e18c1e43f980&scene=21#wechat_redirect)\n*   [ZooKeeper需要关注的点](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247488393&idx=1&sn=f820923ae3e782b509911f787a455a91&scene=21#wechat_redirect)\n*   [MySQL中InnoDB及索引深入剖析](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491256&idx=2&sn=34535df6790951e7de0e08bdfceed108&scene=21#wechat_redirect)\n*   [MySql的Binlog日志工具分析：Canal、Maxwell、Databus、DTS](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247491276&idx=1&sn=133e0d6bcf897ae288a83f1ac6e7456c&scene=21#wechat_redirect)\n*   [一致性协议算法-2PC、3PC、Paxos、Raft、ZAB、NWR超详细解析](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247495156&idx=1&sn=3502f333eaaf19eccef2a721ab1fead1&chksm=fd3eab61ca4922771033e47326464bda019b6d99d3d6f8a81b4cc5ca6bcddbc9307a0ca91eaa&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n*   [MySQL数据库性能优化史诗级大总结](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247495088&idx=1&sn=f3e8460921135780a1e38fdfd163f909&chksm=fd3eab25ca492233ea3dae78b07db1f601b70155cdfd371df51beb4cacb2191fd9d383212999&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n    \n\n**不便分类的其他**\n\n*   [十道海量数据处理面试题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247495750&idx=1&sn=125e5a1800beeeb9faddd5c614136fec&scene=21#wechat_redirect)\n*   [这个面试问题很难么 | 如何处理大数据中的数据倾斜](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486586&idx=1&sn=e35def6429adaada0910b91eed8d7b1f&scene=21#wechat_redirect)\n*   [面试系列：十个海量数据处理方法大总](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485151&idx=1&sn=665e5e284934aecd2fcf3d69e26e3b52&scene=21#wechat_redirect)\n*   [我们常说的海量小文件的根源是什么？](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486931&idx=2&sn=1c5987f3bad7a805895484ebfd683e11&scene=21#wechat_redirect)\n*   [28道关于ZooKeeper的面试题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487021&idx=2&sn=bbfa3fb95e4fa3d0b6ae90a8933119ed&scene=21#wechat_redirect)\n*   [【数据白皮书重磅分享】推荐|埋点|用研|标签](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487177&idx=2&sn=dea2d55951494a5bf4438ec5a888bdd7&scene=21#wechat_redirect)\n*   [1万2千字长文助力春招 | Netty面试篇](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487309&idx=2&sn=a33cc56116cb7891145394f9c5353be3&scene=21#wechat_redirect)\n*   [Filter(过滤)|Project(映射)|Pushdowns(谓词下推)](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487414&idx=2&sn=2afda2b854a4f875159a43903bb7b337&scene=21#wechat_redirect)\n*   [代达罗斯之殇-大数据领域小文件问题解决攻略](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247495807&idx=1&sn=d23efef601586506e00435889a8e1657&chksm=fd3eb6eaca493ffcc18ccfc6c1112124be13486d8778427099e37b4ed536be1ffe1348f55131&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n\n**面试综合系列**\n\n*   [【对线面试官】阿里面试经历，有些人走一步看一步就挂了](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247497893&idx=1&sn=50837fcfceaf6df459936c2b4af36d3d&scene=21#wechat_redirect)\n*   [关于技术面试的一点点体会](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487326&idx=1&sn=1ba5867700bdfb5ad7f808a245ae3b1f&scene=21#wechat_redirect)\n*   [助力秋招-独孤九剑破剑式 | 10家企业面试真题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486054&idx=1&sn=a9385d955df56d5834f6575748bd5277&scene=21#wechat_redirect)\n*   [如果你在准备面试，好好看看这130道题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486986&idx=1&sn=422d1a3c11c72ff97b32cc01142839f4&scene=21#wechat_redirect)\n    \n**简历系列**\n\n*   [一份优秀的简历该长成什么样](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247487297&idx=1&sn=9b007050efaf50f17e3f46915c70f4ea&scene=21#wechat_redirect)\n*   [你过来，我给你看个宝贝](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247492974&idx=1&sn=2d2a3ff8e805072df331dea7a94a7541&chksm=fd3ea3fbca492aed6d43990896edbe806a47a16e15c1541378528792e87f8df84c9b43e5a6d9&token=1917475764&lang=zh_CN&scene=21#wechat_redirect)\n\n\n#### 面试系列合集\n---\n#### 一、Hadoop\n\n1. [Hadoop面试题总结（一）](面试系列/Hadoop面试题总结/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93%EF%BC%88%E4%B8%80%EF%BC%89.md)  \n2. [Hadoop面试题总结（二）——HDFS](面试系列/Hadoop面试题总结/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94HDFS.md)  \n3. [Hadoop面试题总结（三）——MapReduce](面试系列/Hadoop面试题总结/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94MapReduce.md)  \n4. [Hadoop面试题总结（四）——YARN](面试系列/Hadoop面试题总结/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93%EF%BC%88%E5%9B%9B%EF%BC%89%E2%80%94%E2%80%94YARN.md)  \n5. [Hadoop面试题总结（五）——优化问题](面试系列/Hadoop面试题总结/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93%EF%BC%88%E4%BA%94%EF%BC%89%E2%80%94%E2%80%94%E4%BC%98%E5%8C%96%E9%97%AE%E9%A2%98.md) \n\n\n#### 二、Zookeeper\n\n1. [Zookeeper面试题总结（一）](面试系列/Zookeeper面试题总结/Zookeeper.md)  \n\n#### 三、Hive\n\n1. [Hive面试题总结（一）](面试系列/Hive面试题总结/Hive%EF%BC%88%E4%B8%80%EF%BC%89.md)  \n2. [Hive面试题总结（二）](面试系列/Hive面试题总结/Hive%EF%BC%88%E4%BA%8C%EF%BC%89.md)  \n\n#### 四、HBase\n\n1. [HBase面试题总结（一）](面试系列/HBase面试题整理/HBase.md)  \n\n#### 五、Flume\n\n1. [Flume面试题总结（一）](面试系列/Flume面试题整理/Flume.md)  \n\n#### 六、Kafka\n\n1. [Kafka面试题总结（一）](面试系列/Kafka面试题整理/Kafka%EF%BC%88%E4%B8%80%EF%BC%89.md)  \n2. [Kafka面试题总结（二）](面试系列/Kafka面试题整理/Kafka%EF%BC%88%E4%BA%8C%EF%BC%89.md)  \n\n#### 七、Spark\n1. [Spark面试题总结（一）](面试系列/Spark面试题整理/Spark%EF%BC%88%E4%B8%80%EF%BC%89.md)  \n2. [Spark面试题总结（二）](面试系列/Spark面试题整理/Spark%EF%BC%88%E4%BA%8C%EF%BC%89.md)  \n3. [Spark面试题总结（三）](面试系列/Spark面试题整理/Spark%EF%BC%88%E4%B8%89%EF%BC%89.md)  \n4. [Spark面试题总结（四）](面试系列/Spark面试题整理/Spark%EF%BC%88%E5%9B%9B%EF%BC%89.md)  \n  \n**Spark性能优化：**  \n\n5. [Spark面试题总结（五）——几种常见的数据倾斜情况及调优方式](面试系列/Spark面试题整理/Spark调优/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C.md)  \n6. [Spark面试题总结（六）——Shuffle配置调优](面试系列/Spark面试题整理/Spark调优/Shuffle%E9%85%8D%E7%BD%AE%E8%B0%83%E4%BC%98.md)  \n7. [Spark面试题总结（七）——程序开发调优](面试系列/Spark面试题整理/Spark调优/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91%E8%B0%83%E4%BC%98.md)  \n8. [Spark面试题总结（八）——运行资源调优](面试系列/Spark面试题整理/Spark调优/%E8%B5%84%E6%BA%90%E8%B0%83%E4%BC%98.md)  \n\n本系列的大纲会根据实际情况进行调整，欢迎大家关注~\n\n----------------------\n\n## 声明\n\n文档中参考引用了网络上的博客和文章，大部分给出了出处，有些没写，如果造成了侵权行为，请您联系我，立即删除~\n\n## 转载分享\n\n建立本开源项目的初衷是基于个人学习与工作中对Java和大数据相关技术栈的总结记录，在这里也希望能帮助一些在学习Java和大数据过程中遇到问题的小伙伴，如果您需要转载本仓库的一些文章到自己的博客，请按照以下格式注明出处，谢谢合作。\n\n```\n作者：王知无\n链接：https://mp.weixin.qq.com/s/0N4XSMFPuD7U_paGsBsblw\n来源：《大数据技术与架构》\n```\n\n## 参与贡献\n\n1. 如果您对本项目有任何建议或发现文中内容有误的，欢迎提交 issues 进行指正。\n2. 对于文中我没有涉及到知识点，欢迎提交 PR。\n\n## 扫我关注公众号\n\n关注公众号：每天定时推送Hadoop/Spark/Flink等最新的**大数据领域最新动态和精品技术文章**!\n\n<div align=\"center\"> <img width=\"350px\" src=\"qrcodes/wechat01.png\"/> </div>\n\n**扫我加我好友,打造高质量、高知识密度朋友圈!**\n\n<div align=\"center\"> <img width=\"350px\" src=\"qrcodes/个人微信.jpg\"/> </div>\n\n## 扫我加群\n\n备注**来自GitHub加群**，小助手会拉你进大数据讨论组，一起学习交流，期待你的到来~\n\n<div align=\"center\"> <img width=\"350px\" src=\"qrcodes/个人微信.jpg\"/> </div>\n\n## 如果对你有用，欢迎请我喝杯咖啡\n\n备注Github，感谢您～\n\n<div align=\"center\"> <img width=\"350px\" src=\"qrcodes/wechat02.jpeg\"/> </div>\n\n## 言而总之\n\n**大数据成神之路** 该系列文章将为希望从事大数据开发或者由后端转型为大数据开发的工程师们指出需要学习的知识点和路径，本系列文章同时致敬我曾经在网络上看到无数个Java和大数据系列文章，深受启发同时也收货很多。\n"
  },
  {
    "path": "RPC/RPC的原理和框架.md",
    "content": "本文来自：csdn博客，作者在其中做了一些补充并添加了示例\n\nNelson 的论文中指出实现 RPC 的程序包括 5 个部分：\n\n1. User\n\n2. User-stub\n\n3. RPCRuntime\n\n4. Server-stub\n\n5. Server\n\n这 5 个部分的关系如下图所示\n\n![a3615b0de4fd967132726e4ffd191b03](RPC的原理和框架.resources/C5A0EF49-E0B0-4C66-A113-C7DF6F913415.png)\n\n\n这里 user 就是 client 端，当 user 想发起一个远程调用时，它实际是通过本地调用user-stub。user-stub 负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的 RPCRuntime 实例传输到远端的实例。远端 RPCRuntime 实例收到请求后交给 server-stub 进行解码后发起本地端调用，调用结果再返回给 user 端。\n\n![0834cc951b7fbaf68dbbe767869158ce](RPC的原理和框架.resources/DBE43382-274A-4E34-8D8B-A9F91644E2E9.png)\n\n粗粒度的 RPC 实现概念结构，这里我们进一步细化它应该由哪些组件构成，如下图所示。\n\n![ebd6ff5bee2bba18f3f8598f05e27ddd](RPC的原理和框架.resources/3BE7233B-EA64-424D-9FDC-472A2D7DAC50.png)\n\nRPC 服务方通过 RpcServer 去导出（export）远程接口方法，而客户方通过 RpcClient 去引入（import）远程接口方法。客户方像调用本地方法一样去调用远程接口方法，RPC 框架提供接口的代理实现，实际的调用将委托给代理RpcProxy 。代理封装调用信息并将调用转交给RpcInvoker 去实际执行。在客户端的RpcInvoker 通过连接器RpcConnector 去维持与服务端的通道RpcChannel，并使用RpcProtocol 执行协议编码（encode）并将编码后的请求消息通过通道发送给服务方。\n\nRPC 服务端接收器 RpcAcceptor 接收客户端的调用请求，同样使用RpcProtocol 执行协议解码（decode）。解码后的调用信息传递给RpcProcessor 去控制处理调用过程，最后再委托调用给RpcInvoker 去实际执行并返回调用结果。如下是各个部分的详细职责：\n\n    1. RpcServer  \n\n       负责导出（export）远程接口  \n\n    2. RpcClient  \n\n       负责导入（import）远程接口的代理实现  \n\n    3. RpcProxy  \n\n       远程接口的代理实现  \n\n    4. RpcInvoker  \n\n       客户方实现：负责编码调用信息和发送调用请求到服务方并等待调用结果返回  \n\n       服务方实现：负责调用服务端接口的具体实现并返回调用结果  \n\n    5. RpcProtocol  \n\n       负责协议编/解码  \n\n    6. RpcConnector  \n\n       负责维持客户方和服务方的连接通道和发送数据到服务方  \n\n    7. RpcAcceptor  \n\n       负责接收客户方请求并返回请求结果  \n\n    8. RpcProcessor  \n\n       负责在服务方控制调用过程，包括管理调用线程池、超时时间等  \n\n    9. RpcChannel  \n\n       数据传输通道  \n\n### Java中常用的RPC框架\n\n目前常用的RPC框架如下：\n\n1. Thrift：thrift是一个软件框架，用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎，以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。\n\n2. Dubbo：Dubbo是一个分布式服务框架，以及SOA治理方案。其功能主要包括：高性能NIO通讯及多协议集成，服务动态寻址与路由，软负载均衡与容错，依赖分析与降级等。 Dubbo是阿里巴巴内部的SOA服务化治理方案的核心框架，Dubbo自2011年开源后，已被许多非阿里系公司使用。 \n\n3. Spring Cloud：Spring Cloud由众多子项目组成，如Spring Cloud Config、Spring Cloud Netflix、Spring Cloud Consul 等，提供了搭建分布式系统及微服务常用的工具，如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性token、全局锁、选主、分布式会话和集群状态等，满足了构建微服务所需的所有解决方案。Spring Cloud基于Spring Boot, 使得开发部署极其简单。\n\n### RPC和消息队列的差异\n\n#### 1. 功能差异\n\n在架构上，RPC和Message的差异点是，Message有一个中间结点Message Queue，可以把消息存储。\n消息的特点\n1. Message Queue把请求的压力保存一下，逐渐释放出来，让处理者按照自己的节奏来处理。\n2. Message Queue引入一下新的结点，系统的可靠性会受Message Queue结点的影响。\n3. Message Queue是异步单向的消息。发送消息设计成是不需要等待消息处理的完成。\n所以对于有同步返回需求，用Message Queue则变得麻烦了。\nRPC的特点\n同步调用，对于要等待返回结果/处理结果的场景，RPC是可以非常自然直觉的使用方式(RPC也可以是异步调用)。\n由于等待结果，Consumer（Client）会有线程消耗。如果以异步RPC的方式使用，Consumer（Client）线程消耗可以去掉。但不能做到像消息一样暂存消息/请求，压力会直接传导到服务Provider。\n#### 2. 适用场合差异\n1. 希望同步得到结果的场合，RPC合适。\n2. 希望使用简单，则RPC；RPC操作基于接口，使用简单，使用方式模拟本地调用。异步的方式编程比较复杂。\n3. 不希望发送端（RPC Consumer、Message Sender）受限于处理端（RPC Provider、Message Receiver）的速度时，使用Message Queue。\n随着业务增长，有的处理端处理量会成为瓶颈，会进行同步调用到异步消息的改造。这样的改造实际上有调整业务的使用方式。比如原来一个操作页面提交后就下一个页面会看到处理结果；改造后异步消息后，下一个页面就会变成“操作已提交，完成后会得到通知”。\n#### 3. 不适用场合说明\n1. RPC同步调用使用Message Queue来传输调用信息。 上面分析可以知道，这样的做法，发送端是在等待，同时占用一个中间点的资源。变得复杂了，但没有对等的收益。\n2. 对于返回值是void的调用，可以这样做，因为实际上这个调用业务上往往不需要同步得到处理结果的，只要保证会处理即可。（RPC的方式可以保证调用返回即处理完成，使用消息方式后这一点不能保证了。）\n3. 返回值是void的调用，使用消息，效果上是把消息的使用方式Wrap成了服务调用（服务调用使用方式成简单，基于业务接口）。\n\n### RPC框架的核心技术点\n\nRPC框架实现的几个核心技术点：\n\n（1）服务暴露：\n\n远程提供者需要以某种形式提供服务调用相关的信息，包括但不限于服务接口定义、数据结构、或者中间态的服务定义文件。例如Facebook的Thrift的IDL文件，Web service的WSDL文件；服务的调用者需要通过一定的途径获取远程服务调用相关的信息。\n\n目前，大部分跨语言平台 RPC 框架采用根据 IDL 定义通过 code generator 去生成 stub 代码，这种方式下实际导入的过程就是通过代码生成器在编译期完成的。代码生成的方式对跨语言平台 RPC 框架而言是必然的选择，而对于同一语言平台的 RPC 则可以通过共享接口定义来实现。这里的导入方式本质也是一种代码生成技术，只不过是在运行时生成，比静态编译期的代码生成看起来更简洁些。\n\njava 中还有一种比较特殊的调用就是多态，也就是一个接口可能有多个实现，那么远程调用时到底调用哪个？这个本地调用的语义是通过 jvm 提供的引用多态性隐式实现的，那么对于 RPC 来说跨进程的调用就没法隐式实现了。如果前面DemoService 接口有 2 个实现，那么在导出接口时就需要特殊标记不同的实现需要，那么远程调用时也需要传递该标记才能调用到正确的实现类，这样就解决了多态调用的语义问题。\n\n（2）远程代理对象：\n\n服务调用者用的服务实际是远程服务的本地代理。说白了就是通过动态代理来实现。\n\njava 里至少提供了两种技术来提供动态代码生成，一种是 jdk 动态代理，另外一种是字节码生成。动态代理相比字节码生成使用起来更方便，但动态代理方式在性能上是要逊色于直接的字节码生成的，而字节码生成在代码可读性上要差很多。两者权衡起来，个人认为牺牲一些性能来获得代码可读性和可维护性显得更重要。\n\n（3）通信：\n\nRPC框架与具体的协议无关。RPC 可基于 HTTP 或 TCP 协议，Web Service 就是基于 HTTP 协议的 RPC，它具有良好的跨平台性，但其性能却不如基于 TCP 协议的 RPC。\n\n1. TCP/HTTP：众所周知，TCP 是传输层协议，HTTP 是应用层协议，而传输层较应用层更加底层，在数据传输方面，越底层越快，因此，在一般情况下，TCP 一定比 HTTP 快。\n\n2. 消息ID：RPC 的应用场景实质是一种可靠的请求应答消息流，和 HTTP 类似。因此选择长连接方式的 TCP 协议会更高效，与 HTTP 不同的是在协议层面我们定义了每个消息的唯一 id，因此可以更容易的复用连接。\n\n3. IO方式：为了支持高并发，传统的阻塞式 IO 显然不太合适，因此我们需要异步的 IO，即 NIO。Java 提供了 NIO 的解决方案，Java 7 也提供了更优秀的 NIO.2 支持。\n\n4. 多连接：既然使用长连接，那么第一个问题是到底 client 和 server 之间需要多少根连接？实际上单连接和多连接在使用上没有区别，对于数据传输量较小的应用类型，单连接基本足够。单连接和多连接最大的区别在于，每根连接都有自己私有的发送和接收缓冲区，因此大数据量传输时分散在不同的连接缓冲区会得到更好的吞吐效率。所以，如果你的数据传输量不足以让单连接的缓冲区一直处于饱和状态的话，那么使用多连接并不会产生任何明显的提升，反而会增加连接管理的开销。\n5. 心跳：连接是由 client 端发起建立并维持。如果 client 和 server 之间是直连的，那么连接一般不会中断（当然物理链路故障除外）。如果 client 和 server 连接经过一些负载中转设备，有可能连接一段时间不活跃时会被这些中间设备中断。为了保持连接有必要定时为每个连接发送心跳数据以维持连接不中断。心跳消息是 RPC 框架库使用的内部消息，在前文协议头结构中也有一个专门的心跳位，就是用来标记心跳消息的，它对业务应用透明。\n\n（4）序列化：\n\n两方面会直接影响 RPC 的性能，一是传输方式，二是序列化。\n\n1. 序列化方式：毕竟是远程通信，需要将对象转化成二进制流进行传输。不同的RPC框架应用的场景不同，在序列化上也会采取不同的技术。 就序列化而言，Java 提供了默认的序列化方式，但在高并发的情况下，这种方式将会带来一些性能上的瓶颈，于是市面上出现了一系列优秀的序列化框架，比如：Protobuf、Kryo、Hessian、Jackson 等，它们可以取代 Java 默认的序列化，从而提供更高效的性能。\n\n2. 编码内容：出于效率考虑，编码的信息越少越好（传输数据少），编码的规则越简单越好（执行效率高）。如下是编码需具备的信息：\n\n```\n-- 调用编码 --  \n1. 接口方法  \n   包括接口名、方法名  \n2. 方法参数  \n   包括参数类型、参数值  \n3. 调用属性  \n   包括调用属性信息，例如调用附件隐式参数、调用超时时间等  \n  \n-- 返回编码 --  \n1. 返回结果  \n   接口方法中定义的返回值  \n2. 返回码  \n   异常返回码  \n3. 返回异常信息  \n   调用异常信息\n\n```\n\n除了以上这些必须的调用信息，我们可能还需要一些元信息以方便程序编解码以及未来可能的扩展。这样我们的编码消息里面就分成了两部分，一部分是元信息、另一部分是调用的必要信息。如果设计一种 RPC 协议消息的话，元信息我们把它放在协议消息头中，而必要信息放在协议消息体中。下面给出一种概念上的 RPC 协议消息设计格式：\n\n```\n-- 消息头 --  \nmagic      : 协议魔数，为解码设计  \nheader size: 协议头长度，为扩展设计  \nversion    : 协议版本，为兼容设计  \nst         : 消息体序列化类型  \nhb         : 心跳消息标记，为长连接传输层心跳设计  \now         : 单向消息标记，  \nrp         : 响应消息标记，不置位默认是请求消息  \nstatus code: 响应消息状态码  \nreserved   : 为字节对齐保留  \nmessage id : 消息 id  \nbody size  : 消息体长度  \n  \n-- 消息体 --  \n采用序列化编码，常见有以下格式  \nxml   : 如 webservie soap  \njson  : 如 JSON-RPC  \nbinary: 如 thrift; hession; kryo 等\n\n```\n\n### RPC框架简易实现及其实例分析\n\n(1).服务端\n\n　　服务端提供客户端所期待的服务，一般包括三个部分：服务接口，服务实现以及服务的注册暴露三部分，如下：服务接口\n  ```\n  public interface HelloService {\n    String hello(String name);\n    String hi(String msg);\n}\n```\n\n服务实现\n\n```\npublic class HelloServiceImpl implements HelloService{\n    @Override\n    public String hello(String name) {\n        return \"Hello \" + name;\n    }\n \n    @Override\n    public String hi(String msg) {\n        return \"Hi, \" + msg;\n    }\n}\n\n```\n\n服务暴露：只有把服务暴露出来，才能让客户端进行调用，这是RPC框架功能之一。\n\n```\npublic class RpcProvider {\n    public static void main(String[] args) throws Exception {\n        HelloService service = new HelloServiceImpl();\n        // RPC框架将服务暴露出来，供客户端消费\n        RpcFramework.export(service, 1234);\n    }\n}\n\n```\n\n(2).客户端\n\n　　客户端消费服务端所提供的服务，一般包括两个部分：服务接口和服务引用两个部分，如下：服务接口：与服务端共享同一个服务接口\n  ```\n  public interface HelloService {\n    String hello(String name);\n    String hi(String msg);\n}\n```\n\n服务引用：消费端通过RPC框架进行远程调用，这也是RPC框架功能之一\n\n```\n\npublic class RpcConsumer {\n    public static void main(String[] args) throws Exception {\n        // 由RpcFramework生成的HelloService的代理\n        HelloService service = RpcFramework.refer(HelloService.class, \"127.0.0.1\", 1234);\n        String hello = service.hello(\"World\");\n        System.out.println(\"客户端收到远程调用的结果 ： \" + hello);\n    }\n}\n\n```\n\n(3).RPC框架原型实现\n\n　　RPC框架主要包括两大功能：一个用于服务端暴露服务，一个用于客户端引用服务。服务端暴露服务\n\n```\n    /**\n     * 暴露服务\n     *\n     * @param service 服务实现\n     * @param port    服务端口\n     * @throws Exception\n     */\n    public static void export(final Object service, int port) throws Exception {\n        if (service == null) {\n            throw new IllegalArgumentException(\"service instance == null\");\n        }\n        if (port <= 0 || port > 65535) {\n            throw new IllegalArgumentException(\"Invalid port \" + port);\n        }\n        System.out.println(\"Export service \" + service.getClass().getName() + \" on port \" + port);\n        // 建立Socket服务端\n        ServerSocket server = new ServerSocket(port);\n        for (; ; ) {\n            try {\n                // 监听Socket请求\n                final Socket socket = server.accept();\n                new Thread(new Runnable() {\n                    @Override\n                    public void run() {\n                        try {\n                            try {\n                                /* 获取请求流，Server解析并获取请求*/\n                                // 构建对象输入流，从源中读取对象到程序中\n                                ObjectInputStream input = new ObjectInputStream(\n                                    socket.getInputStream());\n                                try {\n \n                                    System.out.println(\"\\nServer解析请求 ： \");\n                                    String methodName = input.readUTF();\n                                    System.out.println(\"methodName : \" + methodName);\n                                    // 泛型与数组是不兼容的，除了通配符作泛型参数以外\n                                    Class<?>[] parameterTypes = (Class<?>[])input.readObject();\n                                    System.out.println(\n                                        \"parameterTypes : \" + Arrays.toString(parameterTypes));\n                                    Object[] arguments = (Object[])input.readObject();\n                                    System.out.println(\"arguments : \" + Arrays.toString(arguments));\n \n \n                                    /* Server 处理请求，进行响应*/\n                                    ObjectOutputStream output = new ObjectOutputStream(\n                                        socket.getOutputStream());\n                                    try {\n                                        // service类型为Object的(可以发布任何服务)，故只能通过反射调用处理请求\n                                        // 反射调用，处理请求\n                                        Method method = service.getClass().getMethod(methodName,\n                                            parameterTypes);\n                                        Object result = method.invoke(service, arguments);\n                                        System.out.println(\"\\nServer 处理并生成响应 ：\");\n                                        System.out.println(\"result : \" + result);\n                                        output.writeObject(result);\n                                    } catch (Throwable t) {\n                                        output.writeObject(t);\n                                    } finally {\n                                        output.close();\n                                    }\n                                } finally {\n                                    input.close();\n                                }\n                            } finally {\n                                socket.close();\n                            }\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n                }).start();\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        }\n    }\n\n```\n从该RPC框架的简易实现来看，RPC服务端逻辑是：首先创建ServerSocket负责监听特定端口并接收客户连接请求，然后使用Java原生的序列化/反序列化机制来解析得到请求，包括所调用方法的名称、参数列表和实参，最后反射调用服务端对服务接口的具体实现并将得到的结果回传至客户端。至此，一次简单PRC调用的服务端流程执行完毕。客户端引用服务\n\n```\n    /**\n     * 引用服务\n     *\n     * @param <T>            接口泛型\n     * @param interfaceClass 接口类型\n     * @param host           服务器主机名\n     * @param port           服务器端口\n     * @return 远程服务，返回代理对象\n     * @throws Exception\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T refer(final Class<T> interfaceClass, final String host, final int port)\n        throws Exception {\n        if (interfaceClass == null) {\n            throw new IllegalArgumentException(\"Interface class == null\");\n        }\n        // JDK 动态代理的约束，只能实现对接口的代理\n        if (!interfaceClass.isInterface()) {\n            throw new IllegalArgumentException(\n                \"The \" + interfaceClass.getName() + \" must be interface class!\");\n        }\n        if (host == null || host.length() == 0) {\n            throw new IllegalArgumentException(\"Host == null!\");\n        }\n        if (port <= 0 || port > 65535) {\n            throw new IllegalArgumentException(\"Invalid port \" + port);\n        }\n        System.out.println(\n            \"Get remote service \" + interfaceClass.getName() + \" from server \" + host + \":\" + port);\n \n        // JDK 动态代理\n        T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),\n            new Class<?>[] {interfaceClass}, new InvocationHandler() {\n                // invoke方法本意是对目标方法的增强，在这里用于发送RPC请求和接收响应\n                @Override\n                public Object invoke(Object proxy, Method method, Object[] arguments)\n                    throws Throwable {\n                    // 创建Socket客户端，并与服务端建立链接\n                    Socket socket = new Socket(host, port);\n                    try {\n                        /* 客户端像服务端进行请求，并将请求参数写入流中*/\n                        // 将对象写入到对象输出流，并将其发送到Socket流中去\n                        ObjectOutputStream output = new ObjectOutputStream(\n                            socket.getOutputStream());\n                        try {\n                            // 发送请求\n                            System.out.println(\"\\nClient发送请求 ： \");\n                            output.writeUTF(method.getName());\n                            System.out.println(\"methodName : \" + method.getName());\n                            output.writeObject(method.getParameterTypes());\n                            System.out.println(\"parameterTypes : \" + Arrays.toString(method\n                                .getParameterTypes()));\n                            output.writeObject(arguments);\n                            System.out.println(\"arguments : \" + Arrays.toString(arguments));\n \n \n                            /* 客户端读取并返回服务端的响应*/\n                            ObjectInputStream input = new ObjectInputStream(\n                                socket.getInputStream());\n                            try {\n                                Object result = input.readObject();\n                                if (result instanceof Throwable) {\n                                    throw (Throwable)result;\n                                }\n                                System.out.println(\"\\nClient收到响应 ： \");\n                                System.out.println(\"result : \" + result);\n                                return result;\n                            } finally {\n                                input.close();\n                            }\n                        } finally {\n                            output.close();\n                        }\n                    } finally {\n                        socket.close();\n                    }\n                }\n            });\n        return proxy;\n    }\n\n```\n\n从该RPC框架的简易实现来看，RPC客户端逻辑是：首先创建Socket客户端并与服务端建立链接，然后使用Java原生的序列化/反序列化机制将调用请求发送给客户端，包括所调用方法的名称、参数列表将服务端的响应返回给用户即可。至此，一次简单PRC调用的客户端流程执行完毕。特别地，从代码实现来看，实现透明的PRC调用的关键就是 动态代理，这是RPC框架实现的灵魂所在。RPC原型实现\n\n```\npublic class RpcFramework {\n    /**\n     * 暴露服务\n     *\n     * @param service 服务实现\n     * @param port    服务端口\n     * @throws Exception\n     */\n    public static void export(final Object service, int port) throws Exception {\n        if (service == null) {\n            throw new IllegalArgumentException(\"service instance == null\");\n        }\n        if (port <= 0 || port > 65535) {\n            throw new IllegalArgumentException(\"Invalid port \" + port);\n        }\n        System.out.println(\"Export service \" + service.getClass().getName() + \" on port \" + port);\n        // 建立Socket服务端\n        ServerSocket server = new ServerSocket(port);\n        for (; ; ) {\n            try {\n                // 监听Socket请求\n                final Socket socket = server.accept();\n                new Thread(new Runnable() {\n                    @Override\n                    public void run() {\n                        try {\n                            try {\n                                /* 获取请求流，Server解析并获取请求*/\n                                // 构建对象输入流，从源中读取对象到程序中\n                                ObjectInputStream input = new ObjectInputStream(\n                                    socket.getInputStream());\n                                try {\n \n                                    System.out.println(\"\\nServer解析请求 ： \");\n                                    String methodName = input.readUTF();\n                                    System.out.println(\"methodName : \" + methodName);\n                                    // 泛型与数组是不兼容的，除了通配符作泛型参数以外\n                                    Class<?>[] parameterTypes = (Class<?>[])input.readObject();\n                                    System.out.println(\n                                        \"parameterTypes : \" + Arrays.toString(parameterTypes));\n                                    Object[] arguments = (Object[])input.readObject();\n                                    System.out.println(\"arguments : \" + Arrays.toString(arguments));\n \n \n                                    /* Server 处理请求，进行响应*/\n                                    ObjectOutputStream output = new ObjectOutputStream(\n                                        socket.getOutputStream());\n                                    try {\n                                        // service类型为Object的(可以发布任何服务)，故只能通过反射调用处理请求\n                                        // 反射调用，处理请求\n                                        Method method = service.getClass().getMethod(methodName,\n                                            parameterTypes);\n                                        Object result = method.invoke(service, arguments);\n                                        System.out.println(\"\\nServer 处理并生成响应 ：\");\n                                        System.out.println(\"result : \" + result);\n                                        output.writeObject(result);\n                                    } catch (Throwable t) {\n                                        output.writeObject(t);\n                                    } finally {\n                                        output.close();\n                                    }\n                                } finally {\n                                    input.close();\n                                }\n                            } finally {\n                                socket.close();\n                            }\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n                }).start();\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        }\n    }\n \n \n    /**\n     * 引用服务\n     *\n     * @param <T>            接口泛型\n     * @param interfaceClass 接口类型\n     * @param host           服务器主机名\n     * @param port           服务器端口\n     * @return 远程服务，返回代理对象\n     * @throws Exception\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T refer(final Class<T> interfaceClass, final String host, final int port)\n        throws Exception {\n        if (interfaceClass == null) {\n            throw new IllegalArgumentException(\"Interface class == null\");\n        }\n        // JDK 动态代理的约束，只能实现对接口的代理\n        if (!interfaceClass.isInterface()) {\n            throw new IllegalArgumentException(\n                \"The \" + interfaceClass.getName() + \" must be interface class!\");\n        }\n        if (host == null || host.length() == 0) {\n            throw new IllegalArgumentException(\"Host == null!\");\n        }\n        if (port <= 0 || port > 65535) {\n            throw new IllegalArgumentException(\"Invalid port \" + port);\n        }\n        System.out.println(\n            \"Get remote service \" + interfaceClass.getName() + \" from server \" + host + \":\" + port);\n \n        // JDK 动态代理\n        T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),\n            new Class<?>[] {interfaceClass}, new InvocationHandler() {\n                // invoke方法本意是对目标方法的增强，在这里用于发送RPC请求和接收响应\n                @Override\n                public Object invoke(Object proxy, Method method, Object[] arguments)\n                    throws Throwable {\n                    // 创建Socket客户端，并与服务端建立链接\n                    Socket socket = new Socket(host, port);\n                    try {\n                        /* 客户端像服务端进行请求，并将请求参数写入流中*/\n                        // 将对象写入到对象输出流，并将其发送到Socket流中去\n                        ObjectOutputStream output = new ObjectOutputStream(\n                            socket.getOutputStream());\n                        try {\n                            // 发送请求\n                            System.out.println(\"\\nClient发送请求 ： \");\n                            output.writeUTF(method.getName());\n                            System.out.println(\"methodName : \" + method.getName());\n                            output.writeObject(method.getParameterTypes());\n                            System.out.println(\"parameterTypes : \" + Arrays.toString(method\n                                .getParameterTypes()));\n                            output.writeObject(arguments);\n                            System.out.println(\"arguments : \" + Arrays.toString(arguments));\n \n \n                            /* 客户端读取并返回服务端的响应*/\n                            ObjectInputStream input = new ObjectInputStream(\n                                socket.getInputStream());\n                            try {\n                                Object result = input.readObject();\n                                if (result instanceof Throwable) {\n                                    throw (Throwable)result;\n                                }\n                                System.out.println(\"\\nClient收到响应 ： \");\n                                System.out.println(\"result : \" + result);\n                                return result;\n                            } finally {\n                                input.close();\n                            }\n                        } finally {\n                            output.close();\n                        }\n                    } finally {\n                        socket.close();\n                    }\n                }\n            });\n        return proxy;\n    }\n}\n\n```\n以上是简易RPC框架实现的简易完整代码。\n\n\n### 关于RPC框架的若干问题说明\n\n(1).RPC框架如何做到透明化远程服务调用？\n\n　　如何封装通信细节才能让用户像以本地调用方式调用远程服务呢？就Java而言，动态代理恰是解决之道。Java动态代理有JDK动态代理和CGLIB动态代理两种方式。尽管字节码生成方式实现的代理更为强大和高效，但代码维护不易，因此RPC框架的大部分实现还是选择JDK动态代理的方式。在上面的例子中，RPCFramework实现中的invoke方法封装了与远端服务通信的细节，消费方首先从RPCFramework获得服务提供方的接口，当执行helloService.hi(“Panda”)方法时就会调用invoke方法。\n\n(2).如何发布自己的服务？\n\n　　如何让别人使用我们的服务呢？难道就像我们上面的代码一样直接写死服务的IP以及端口就可以了吗？事实上，在实际生产实现中，使用人肉告知的方式是不现实的，因为实际生产中服务机器上/下线太频繁了。如果你发现一台机器提供服务不够，要再添加一台，这个时候就要告诉调用者我现在有两个IP了，你们要轮询调用来实现负载均衡；调用者咬咬牙改了，结果某天一台机器挂了，调用者发现服务有一半不可用，他又只能手动修改代码来删除挂掉那台机器的ip。这必然是相当痛苦的！\n\n　　有没有一种方法能实现自动告知，即机器的上线/下线对调用方透明，调用者不再需要写死服务提供方地址？当然可以，生产中的RPC框架都采用的是自动告知的方式，比如，阿里内部使用的RPC框架HSF是通过ConfigServer来完成这项任务的。此外，Zookeeper也被广泛用于实现服务自动注册与发现功能。不管具体采用何种技术，他们大都采用的都是 发布/订阅模式。\n\n(3).序列化与反序列化\n\n　　我们知道，Java对象是无法直接在网络中进行传输的。那么，我们的RPC请求如何发给服务端，客户端又如何接收来自服务端的响应呢？答案是，在传输Java对象时，首先对其进行序列化，然后在相应的终端进行反序列化还原对象以便进行处理。事实上，序列化/反序列化技术也有很多种，比如Java的原生序列化方式、JSON、阿里的Hessian和ProtoBuff序列化等，它们在效率上存在差异，但又有各自的特点。\n"
  },
  {
    "path": "RPC/RPC简单介绍.md",
    "content": "## RPC\n### 1. RPC是什么\n\nRPC（Remote Procedure Call Protocol）——远程过程调用协议，它是一种通过网络从远程计算机程序上请求服务，而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在，如TCP或UDP，为通信程序之间携带信息数据。在OSI网络通信模型中，RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。\n\nRPC采用客户机/服务器模式。请求程序就是一个客户机，而服务提供程序就是一个服务器。首先，客户机调用进程发送一个有进程参数的调用信息到服务进程，然后等待应答信息。在服务器端，进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达，服务器获得进程参数，计算结果，发送答复信息，然后等待下一个调用信息，最后，客户端调用进程接收答复信息，获得进程结果，然后调用执行继续进行。\n\n![fb96c39c13839b5e3bf7246e897e4e36](RPC简单介绍.resources/FA8401E1-CCCF-413F-8720-7E91C912FA55.png)\n\n\n### 2. 为什么要用RPC？ \n其实这是应用开发到一定的阶段的强烈需求驱动的。\n\n\n1. 如果我们开发简单的单一应用，逻辑简单、用户不多、流量不大，那我们用不着；\n\n2. 当我们的系统访问量增大、业务增多时，我们会发现一台单机运行此系统已经无法承受。此时，我们可以将业务拆分成几个互不关联的应用，分别部署在各自机器上，以划清逻辑并减小压力。此时，我们也可以不需要RPC，因为应用之间是互不关联的。\n\n\n3. 当我们的业务越来越多、应用也越来越多时，自然的，我们会发现有些功能已经不能简单划分开来或者划分不出来。此时，可以将公共业务逻辑抽离出来，将之组成独立的服务Service应用 。而原有的、新增的应用都可以与那些独立的Service应用 交互，以此来完成完整的业务功能。所以此时，我们急需一种高效的应用程序之间的通讯手段来完成这种需求，所以你看，RPC大显身手的时候来了！\n\n\n其实3描述的场景也是服务化 、微服务 和分布式系统架构 的基础场景。即RPC框架就是实现以上结构的有力方式。"
  },
  {
    "path": "RPC/手把手教你实现一个简单的RPC.md",
    "content": "\n## RPC的实现原理\n\n上面2讲我们已经讲过，RPC主要是为了解决的两个问题：\n解决分布式系统中，服务之间的调用问题。\n远程调用时，要能够像本地调用一样方便，让调用者感知不到远程调用的逻辑。\n还是以计算器Calculator为例，如果实现类CalculatorImpl是放在本地的，那么直接调用即可：\n\n![116e0941623cad855218ce0f2372f72d](手把手教你实现一个简单的RPC.resources/35D32263-56C5-4251-A767-968E1CDD5AA0.png)\n\n现在系统变成分布式了，CalculatorImpl和调用方不在同一个地址空间，那么就必须要进行远程过程调用：\n\n![6163b3c7bf343d2704baf8191a4bb14b](手把手教你实现一个简单的RPC.resources/C94525C4-DC16-41E4-973D-7B438683B64C.png)\n\n那么如何实现远程过程调用，也就是RPC呢，一个完整的RPC流程，可以用下面这张图来描述：\n\n\n![e0876518a5b3b62cee126075de90d50d](手把手教你实现一个简单的RPC.resources/D40C4121-5F8D-46FA-B8F0-5F45B9DE1CA0.png)\n\n其中左边的Client，对应的就是前面的Service A，而右边的Server，对应的则是Service B。\n下面一步一步详细解释一下。\n\n* Service A的应用层代码中，调用了Calculator的一个实现类的add方法，希望执行一个加法运算；\n* 这个Calculator实现类，内部并不是直接实现计算器的加减乘除逻辑，而是通过远程调用Service B的RPC接口，来获取运算结果，因此称之为Stub；\n* Stub怎么和Service B建立远程通讯呢？这时候就要用到远程通讯工具了，也就是图中的Run-time Library，这个工具将帮你实现远程通讯的功能，比如Java的Socket，就是这样一个库，当然，你也可以用基于Http协议的HttpClient，或者其他通讯工具类，都可以，RPC并没有规定说你要用何种协议进行通讯；\n* Stub通过调用通讯工具提供的方法，和Service B建立起了通讯，然后将请求数据发给Service B。需要注意的是，由于底层的网络通讯是基于二进制格式的，因此这里Stub传给通讯工具类的数据也必须是二进制，比如calculator.add(1,2)，你必须把参数值1和2放到一个Request对象里头（这个Request对象当然不只这些信息，还包括要调用哪个服务的哪个RPC接口等其他信息），然后序列化为二进制，再传给通讯工具类，这一点也将在下面的代码实现中体现；\n* 二进制的数据传到Service B这一边了，Service B当然也有自己的通讯工具，通过这个通讯工具接收二进制的请求；\n* 既然数据是二进制的，那么自然要进行反序列化了，将二进制的数据反序列化为请求对象，然后将这个请求对象交给Service B的Stub处理；\n* 和之前的Service A的Stub一样，这里的Stub也同样是个“假玩意”，它所负责的，只是去解析请求对象，知道调用方要调的是哪个RPC接口，传进来的参数又是什么，然后再把这些参数传给对应的RPC接口，也就是Calculator的实际实现类去执行。很明显，如果是Java，那这里肯定用到了反射。\n* RPC接口执行完毕，返回执行结果，现在轮到Service B要把数据发给Service A了，怎么发？一样的道理，一样的流程，只是现在Service B变成了Client，Service A变成了Server而已：Service B反序列化执行结果->传输给Service A->Service A反序列化执行结果 -> 将结果返回给Application，完毕。\n\n理论的讲完了，是时候把理论变成实践了。\n\n## 把理论变成实践\n\n首先是Client端的应用层怎么发起RPC，ComsumerApp：\n\n```\npublic class ComsumerApp {\n    public static void main(String[] args) {\n        Calculator calculator = new CalculatorRemoteImpl();\n        int result = calculator.add(1, 2);\n    }\n}\n```\n\n通过一个CalculatorRemoteImpl，我们把RPC的逻辑封装进去了，客户端调用时感知不到远程调用的麻烦。下面再来看看CalculatorRemoteImpl，代码有些多，但是其实就是把上面的2、3、4几个步骤用代码实现了而已，CalculatorRemoteImpl：\n\n```\npublic class CalculatorRemoteImpl implements Calculator {\n    public int add(int a, int b) {\n        List<String> addressList = lookupProviders(\"Calculator.add\");\n        String address = chooseTarget(addressList);\n        try {\n            Socket socket = new Socket(address, PORT);\n\n            // 将请求序列化\n            CalculateRpcRequest calculateRpcRequest = generateRequest(a, b);\n            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());\n\n            // 将请求发给服务提供方\n            objectOutputStream.writeObject(calculateRpcRequest);\n\n            // 将响应体反序列化\n            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());\n            Object response = objectInputStream.readObject();\n\n            if (response instanceof Integer) {\n                return (Integer) response;\n            } else {\n                throw new InternalError();\n            }\n\n        } catch (Exception e) {\n            log.error(\"fail\", e);\n            throw new InternalError();\n        }\n    }\n}\n```\nadd方法的前面两行，lookupProviders和chooseTarget，可能大家会觉得不明觉厉。\n\n分布式应用下，一个服务可能有多个实例，比如Service B，可能有ip地址为198.168.1.11和198.168.1.13两个实例，lookupProviders，其实就是在寻找要调用的服务的实例列表。在分布式应用下，通常会有一个服务注册中心，来提供查询实例列表的功能。\n\n查到实例列表之后要调用哪一个实例呢，只时候就需要chooseTarget了，其实内部就是一个负载均衡策略。\n\n由于我们这里只是想实现一个简单的RPC，所以暂时不考虑服务注册中心和负载均衡，因此代码里写死了返回ip地址为127.0.0.1。\n\n代码继续往下走，我们这里用到了Socket来进行远程通讯，同时利用ObjectOutputStream的writeObject和ObjectInputStream的readObject，来实现序列化和反序列化。\n\n最后再来看看Server端的实现，和Client端非常类似，ProviderApp：\n\n```\npublic class ProviderApp {\n    private Calculator calculator = new CalculatorImpl();\n\n    public static void main(String[] args) throws IOException {\n        new ProviderApp().run();\n    }\n\n    private void run() throws IOException {\n        ServerSocket listener = new ServerSocket(9090);\n        try {\n            while (true) {\n                Socket socket = listener.accept();\n                try {\n                    // 将请求反序列化\n                    ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());\n                    Object object = objectInputStream.readObject();\n\n                    log.info(\"request is {}\", object);\n\n                    // 调用服务\n                    int result = 0;\n                    if (object instanceof CalculateRpcRequest) {\n                        CalculateRpcRequest calculateRpcRequest = (CalculateRpcRequest) object;\n                        if (\"add\".equals(calculateRpcRequest.getMethod())) {\n                            result = calculator.add(calculateRpcRequest.getA(), calculateRpcRequest.getB());\n                        } else {\n                            throw new UnsupportedOperationException();\n                        }\n                    }\n\n                    // 返回结果\n                    ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());\n                    objectOutputStream.writeObject(new Integer(result));\n                } catch (Exception e) {\n                    log.error(\"fail\", e);\n                } finally {\n                    socket.close();\n                }\n            }\n        } finally {\n            listener.close();\n        }\n    }\n\n}\n```\n\nServer端主要是通过ServerSocket的accept方法，来接收Client端的请求，接着就是反序列化请求->执行->序列化执行结果，最后将二进制格式的执行结果返回给Client。\n\n就这样我们实现了一个简陋而又详细的RPC。  \n\n说它简陋，是因为这个实现确实比较挫，在下一小节会说它为什么挫。\n说它详细，是因为它一步一步的演示了一个RPC的执行流程，方便大家了解RPC的内部机制。\n\n## 为什么说这个RPC实现很挫\n\n这个RPC实现只是为了给大家演示一下RPC的原理，要是想放到生产环境去用，那是绝对不行的。\n\n1、缺乏通用性\n我通过给Calculator接口写了一个CalculatorRemoteImpl，来实现计算器的远程调用，下一次要是有别的接口需要远程调用，是不是又得再写对应的远程调用实现类？这肯定是很不方便的。\n\n那该如何解决呢？先来看看使用Dubbo时是如何实现RPC调用的：\n\n```\n@Reference\nprivate Calculator calculator;\n\n...\n\ncalculator.add(1,2);\n\n...\n\n```\n\nDubbo通过和Spring的集成，在Spring容器初始化的时候，如果扫描到对象加了@Reference注解，那么就给这个对象生成一个代理对象，这个代理对象会负责远程通讯，然后将代理对象放进容器中。所以代码运行期用到的calculator就是那个代理对象了。\n我们可以先不和Spring集成，也就是先不采用依赖注入，但是我们要做到像Dubbo一样，无需自己手动写代理对象，怎么做呢？那自然是要求所有的远程调用都遵循一套模板，把远程调用的信息放到一个RpcRequest对象里面，发给Server端，Server端解析之后就知道你要调用的是哪个RPC接口、以及入参是什么类型、入参的值又是什么，就像Dubbo的RpcInvocation：\n\n\n```\npublic class RpcInvocation implements Invocation, Serializable {\n\n    private static final long serialVersionUID = -4355285085441097045L;\n\n    private String methodName;\n\n    private Class<?>[] parameterTypes;\n\n    private Object[] arguments;\n\n    private Map<String, String> attachments;\n\n    private transient Invoker<?> invoker;\n```\n\n2、集成Spring\n在实现了代理对象通用化之后，下一步就可以考虑集成Spring的IOC功能了，通过Spring来创建代理对象，这一点就需要对Spring的bean初始化有一定掌握了。\n\n3、长连接or短连接\n总不能每次要调用RPC接口时都去开启一个Socket建立连接吧？是不是可以保持若干个长连接，然后每次有rpc请求时，把请求放到任务队列中，然后由线程池去消费执行？只是一个思路，后续可以参考一下Dubbo是如何实现的。\n\n4、 服务端线程池\n我们现在的Server端，是单线程的，每次都要等一个请求处理完，才能去accept另一个socket的连接，这样性能肯定很差，是不是可以通过一个线程池，来实现同时处理多个RPC请求？同样只是一个思路。\n\n5、服务注册中心\n正如之前提到的，要调用服务，首先你需要一个服务注册中心，告诉你对方服务都有哪些实例。Dubbo的服务注册中心是可以配置的，官方推荐使用Zookeeper。如果使用Zookeeper的话，要怎样往上面注册实例，又要怎样获取实例，这些都是要实现的。\n\n6、负载均衡\n如何从多个实例里挑选一个出来，进行调用，这就要用到负载均衡了。负载均衡的策略肯定不只一种，要怎样把策略做成可配置的？又要如何实现这些策略？同样可以参考Dubbo，Dubbo - 负载均衡\n\n7、结果缓存\n每次调用查询接口时都要真的去Server端查询吗？是不是要考虑一下支持缓存？\n\n8、多版本控制\n服务端接口修改了，旧的接口怎么办？\n\n9、异步调用\n客户端调用完接口之后，不想等待服务端返回，想去干点别的事，可以支持不？\n\n10、优雅停机\n服务端要停机了，还没处理完的请求，怎么办？\n\n\n诸如此类的优化点还有很多，这也是为什么实现一个高性能高可用的RPC框架那么难的原因。\n当然，我们现在已经有很多很不错的RPC框架可以参考了，我们完全可以借鉴一下前人的智慧。\n\n"
  },
  {
    "path": "WechatArticles/flink.md",
    "content": "\n"
  },
  {
    "path": "zookeeeper/ZooKeeper应用程序.md",
    "content": "\n在对ZooKeeper有了一个深入的了解以后，我们来看一下用ZooKeeper可以实现哪些应用。\n\n## 配置服务 Configuration Service\n\n一个基本的ZooKeeper实现的服务就是“配置服务”，集群中的服务器可以通过ZooKeeper共享一个通用的配置数据。从表面上，ZooKeeper可以理解为一个配置数据的高可用存储服务，为应用提供检索和更新配置数据服务。我们可以使用ZooKeeper的观察模式实现一个活动的配置服务，当配置数据发生变化时，可以通知与配置相关客户端。\n\n接下来，我们来实现一个这样的活动配置服务。首先，我们设计用znode来存储key-value对，我们在znode中存储一个String类型的数据作为value，用znode的path来表示key。然后，我们实现一个client，这个client可以在任何时候对数据进行跟新操作。那么这个设计的ZooKeeper数据模型应该是：master来更新数据，其他的worker也随之将数据更新，就像HDFS的namenode那样。\n\n我们在一个叫做ActiveKeyValueStore的类中编写代码如下：\n\n```\npublic class ActiveKeyValueStore extends ConnectionWatcher {\n\n  private static final Charset CHARSET = Charset.forName(\"UTF-8\");\n\n  public void write(String path, String value) throws InterruptedException,\n      KeeperException {\n    Stat stat = zk.exists(path, false);\n    if (stat == null) {\n      zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE,\n          CreateMode.PERSISTENT);\n    } else {\n      zk.setData(path, value.getBytes(CHARSET), -1);\n    }\n  }\n}\n```\nwrite()方法主要实现将给定的key-value对写入到ZooKeeper中。这其中隐含了创建一个新的znode和更新一个已存在的znode的实现方法的不同。那么操作之前，我们需要根据exists()来判断znode是否存在，然后再根据情况进行相关的操作。其他值得一提的就是String类型的数据在转换成byte[]时，使用的字符集是UTF-8。\n\n我们为了说明ActiveKeyValueStore怎么使用，我们考虑实现一个ConfigUpdater类来实现更新配置。下面代码实现了一个在一些随机时刻更新配置数据的应用。\n\n```\npublic class ConfigUpdater {\n\n  public static final String PATH = \"/config\";\n\n  private ActiveKeyValueStore store;\n  private Random random = new Random();\n\n  public ConfigUpdater(String hosts) throws IOException, InterruptedException {\n    store = new ActiveKeyValueStore();\n    store.connect(hosts);\n  }\n\n  public void run() throws InterruptedException, KeeperException {\n    while (true) {\n      String value = random.nextInt(100) + \"\";\n      store.write(PATH, value);\n      System.out.printf(\"Set %s to %s\\n\", PATH, value);\n      TimeUnit.SECONDS.sleep(random.nextInt(10));\n    }\n  }\n\n  public static void main(String[] args) throws Exception {\n    ConfigUpdater configUpdater = new ConfigUpdater(args[0]);\n    configUpdater.run();\n  }\n}\n```\n上面的代码很简单。在ConfigUpdater的构造函数中，ActiveKeyValueStore对象连接到ZooKeeper服务。然后run()不断的循环运行，使用一个随机数不断的随机更新/configznode上的值。\n\n下面我们来看一下，如何读取/config上的值。首先，我们在ActiveKeyValueStore中实现一个读方法。\n\n```\npublic String read(String path, Watcher watcher) throws InterruptedException,\n      KeeperException {\n    byte[] data = zk.getData(path, watcher, null/*stat*/);\n    return new String(data, CHARSET);\n  }\n```\nZooKeeper的getData()方法的参数包含：path，一个Watcher对象和一个Stat对象。Stat对象中含有从getData()返回的值，并且负责接收回调信息。这种方式下，调用者不仅可以获得数据，还能够获得znode的metadata。\n\n做为服务的consumer，ConfigWatcher以观察者身份，创建一个ActiveKeyValueStore对象，并且在启动以后调用read()函数（在dispalayConfig()函数中）获得相关数据。\n\n下面的代码实现了一个以观察模式获得ZooKeeper中的数据更新的应用，并将值到后台中。\n\n```\npublic class ConfigWatcher implements Watcher {\n\n  private ActiveKeyValueStore store;\n\n  public ConfigWatcher(String hosts) throws IOException, InterruptedException {\n    store = new ActiveKeyValueStore();\n    store.connect(hosts);\n  }\n\n  public void displayConfig() throws InterruptedException, KeeperException {\n    String value = store.read(ConfigUpdater.PATH, this);\n    System.out.printf(\"Read %s as %s\\n\", ConfigUpdater.PATH, value);\n  }\n\n  @Override\n  public void process(WatchedEvent event) {\n    if (event.getType() == EventType.NodeDataChanged) {\n      try {\n        displayConfig();\n      } catch (InterruptedException e) {\n        System.err.println(\"Interrupted. Exiting.\");        \n        Thread.currentThread().interrupt();\n      } catch (KeeperException e) {\n        System.err.printf(\"KeeperException: %s. Exiting.\\n\", e);        \n      }\n    }\n  }\n\n  public static void main(String[] args) throws Exception {\n    ConfigWatcher configWatcher = new ConfigWatcher(args[0]);\n    configWatcher.displayConfig();\n\n    // stay alive until process is killed or thread is interrupted\n    Thread.sleep(Long.MAX_VALUE);\n  }\n}\n```\n\n当ConfigUpadater更新znode时，ZooKeeper将触发一个EventType.NodeDataChanged的事件给观察者。ConfigWatcher将在他的process()函数中获得这个时间，并将显示读取到的最新的版本的配置数据。\n\n由于观察模式的触发是一次性的，所以每次都要调用ActiveKeyValueStore的read()方法，这样才能获得未来的更新数据。我们不能确保一定能够接受到更新通知事件，因为在接受观察事件和下一次读取之间的窗口期内，znode可能被改变了（有可能很多次），但是client可能没有注册观察模式，所以client不会接到znode改变的通知。在配置服务中这不是一个什么问题，因为client只关心配置数据的最新版本。然而，建议读者关注一下这个潜在的问题。\n\n让我们来看一下控制台打印的ConfigUpdater运行结果：\n\n```\n% java ConfigUpdater localhost\nSet /config to 79\nSet /config to 14\nSet /config to 78\n```\n\n然后立即在另外的控制台终端窗口中运行ConfigWatcher:\n\n```\n% java ConfigWatcher localhost\nRead /config as 79\nRead /config as 14\nRead /config as 78\n```\n\n## 坚韧的ZooKeeper应用\n\n分布式计算设计的第一谬误就是认为“网络是稳定的”。我们所实现的程序目前都是假设网络稳定的情况下实现的，所以当我们在一个真实的网络环境下，会有很多原因可以使程序执行失败。下面我们将阐述一些可能造成失败的场景，并且讲述如何正确的处理这些失败，让我们的程序在面对这些异常时更具韧性。\n在ZooKeeper的API中，每一个ZooKeeper的操作都会声明抛出两个异常：InterruptedException和KeeperException。\n\n\n### InterrupedException\n\n当一个操作被中断时，会抛出一个InterruptedException。在JAVA中有一个标准的阻塞机制用来取消程序的执行，就是在需要阻塞的地方调用interrupt()。如果取消执行成功，会以抛出一个InterruptedException作为结果。ZooKeeper坚持了这个标准，所以我们可以用这种方式来取消client的对ZooKeeper的操作。用到ZooKeeper的类和库需要向上抛出InterruptedException，才能使我们的client实现取消操作。\n\nInterruptedException并不意味着程序执行失败，可能是人为设计中断的，所以在上面配置应用的例子中，当向上抛出InterruptedException时，会引起应用终止。\n\n### KeeperException\n\n当ZooKeeper服务器出现错误信号，或者出现了通信方面的问题，就会抛出一个KeeperException。由于错误的不同原因，所以KeeperException有很多子类。例如，KeeperException.NoNodeException当操作一个znode时，而这个znode并不存在，就会抛出这个异常。\n\n每一个之类都有一个异常码作为异常的类型。例如，KeeperException.NoNodeException的异常码就是KeeperException.Code.NONODE(一个枚举值)。\n有两种方法来处理KeeperException。一种是直接捕获KeeperException，然后根据异常码进行不同类型异常处理。另一种是捕获具体的子类，然后根据不同类型的异常进行处理。\n\nKeeperException包含了3大类异常。\n\n### 状态异常 State Exception\n\n当无法操作znode树造成操作失败时，会产生状态异常。通常引起状态异常的原因是有另外的程序在同时改变znode。例如，一个setData()操作时，会抛出KeeperException.BadVersionException。因为另外的一个程序已经在setData()操作之前修改了znode，造成setData()操作时版本号不匹配了。程序员必须了解，这种情况是很有可能发生的，我们必须靠编写处理这种异常的代码来解决他。\n\n有的一些异常是编写代码时的疏忽造成的，例如KeeperException.NoChildrenForEphemeralsException。这个异常是当我们给一个enphemeral类型的znode添加子节点时抛出的。\n\n### 重新获取异常 Recoverable Exception\n\n重新获取异常来至于那些能够获得同一个ZooKeeper session的应用。伴随的表现是抛出KeeperException.ConnectionLossException，表示与ZooKeeper的连接丢失。ZooKeeper将会尝试重新连接，大多数情况下重新连接都会成功并且能够保证session的完整性。\n\n然而，ZooKeeper无法通知客户端操作由于KeeperException.ConnectionLossException而失败。这就是一个部分失败的例子。只能依靠程序员编写代码来处理这个不确定性。\n\n在这点上，幂等操作和非幂等操作的差别就会变得非常有用了。一个幂等操作是指无论运行一次还是多次结果都是一样的，例如一个读请求，或者一个不设置任何值得setData操作。这些操作可以不断的重试。\n\n一个非幂等操作不能被不分青红皂白的不停尝试执行，就像一些操作执行一次的效率和执行多次的效率是不同。我们将在之后会讨论如何利用非幂等操作来处理Recovreable Exception。\n\n### 不能重新获取异常 Unrecoverable exceptions\n\n在一些情况下，ZooKeeper的session可能会变成不可用的——比如session过期，或者因为某些原因session被close掉（都会抛出KeeperException.SessionExpiredException），或者鉴权失败（KeeperException.AuthFailedException）。无论何种情况，ephemeral类型的znode上关联的session都会丢失，所以应用在重新连接到ZooKeeper之前都需要重新构建他的状态。\n\n## 稳定的配置服务\n\n\n回过头来看一下ActiveKeyValueStore中的write()方法，其中调用了exists()方法来判断znode是否存在，然后决定是创建一个znode还是调用setData来更新数据。\n\n```\npublic void write(String path, String value) throws InterruptedException,\n      KeeperException {\n    Stat stat = zk.exists(path, false);\n    if (stat == null) {\n      zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE,\n          CreateMode.PERSISTENT);\n    } else {\n      zk.setData(path, value.getBytes(CHARSET), -1);\n    }\n  }\n  ```\n  \n  \n从整体上来看，write()方法是一个幂等方法，所以我们可以不断的尝试执行它。我们来修改一个新版本的write()方法，实现在循环中不断的尝试write操作。我们为尝试操作设置了一个最大尝试次数参数（MAX_RETRIES）和每次尝试间隔的休眠(RETRY_PERIOD_SECONDS)时长：\n\n```\npublic void write(String path, String value) throws InterruptedException,\n      KeeperException {\n    int retries = 0;\n    while (true) {\n      try {\n        Stat stat = zk.exists(path, false);\n        if (stat == null) {\n          zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE,\n              CreateMode.PERSISTENT);\n        } else {\n          zk.setData(path, value.getBytes(CHARSET), stat.getVersion());\n        }\n        return;\n      } catch (KeeperException.SessionExpiredException e) {\n        throw e;\n      } catch (KeeperException e) {\n        if (retries++ == MAX_RETRIES) {\n          throw e;\n        }\n        // sleep then retry\n        TimeUnit.SECONDS.sleep(RETRY_PERIOD_SECONDS);\n      }\n    }\n  }\n  ```\n  \n  \n细心的读者可能会发现我们并没有在捕获KeeperException.SessionExpiredException时继续重新尝试操作，这是因为当session过期后，ZooKeeper会变为CLOSED状态，就不能再重新连接了。我们只是简单的抛出一个异常，通知调用者去创建一个新的ZooKeeper实例，所以write()方法可以不断的尝试执行。一个简单的方式来创建一个ZooKeeper实例就是重新new一个ConfigUpdater实例。\n\n```\npublic static void main(String[] args) throws Exception {\n    while (true) {\n      try {\n        ResilientConfigUpdater configUpdater =\n          new ResilientConfigUpdater(args[0]);\n        configUpdater.run();\n      } catch (KeeperException.SessionExpiredException e) {\n        // start a new session\n      } catch (KeeperException e) {\n        // already retried, so exit\n        e.printStackTrace();\n        break;\n      }\n    }\n  }\n  ```\n  \n  \n另一个可以替代处理session过期的方法就是使用watcher来监控Expired的KeeperState，然后重新建立一个连接。这种方法下，我们只需要不断的尝试执行write()，如果我们得到了KeeperException.SessionExpiredException异常，连接最终也会被重新建立起来。那么我们抛开如何从一个过期的session中恢复问题，我们的重点是连接丢失的问题也可以这样解决，只是处理方法不同而已。\n\n> 我们这里忽略了另外一种情况，在zookeeper实例不断的尝试连接了ensemble中的所有节点后发现都无法连接成功，就会抛出一个IOException，说明所有的集群节点都不可用。而有一些应用被设计为不断的尝试连接，直到ZooKeeper服务恢复可用为止。\n\n## 锁服务\n\n分布式锁用来为一组程序提供互斥机制。任意一个时刻仅有一个进程能够获得锁。分布式锁可以用来实现大型分布式系统的leader选举算法，即leader就是获取到锁的那个进程。\n\n> 不要把ZooKeeper的原生leader选举算法和我们这里所说的通用leader选举服务搞混淆了。ZooKeeper的原生leader选举算法并不是公开的算法，并不能向我们这里所说的通用leader选举服务那样，为一个分布式系统提供主进程选举服务。\n\n为了使用ZooKeeper实现分布式锁，我们使用可排序的znode来实现进程对锁的竞争。思路其实很简单：首先，我们需要一个表示锁的znode，获得锁的进程就表示被这把锁给锁定了（命名为，/leader）。然后，client为了获得锁，就需要在锁的znode下创建ephemeral类型的子znode。在任何时间点上，只有排序序号最小的znode的client获得锁，即被锁定。例如，如果两个client同时创建znode /leader/lock-1和/leader/lock-2，所以创建/leader/lock-1的client获得锁，因为他的排序序号最小。ZooKeeper服务被看作是排序的权威管理者，因为是由他来安排排序的序号的。\n锁可能因为删除了/leader/lock-1znode而被简单的释放。另外，如果相应的客户端死掉，使用ephemeral znode的价值就在这里，znode可以被自动删除掉。创建/leader/lock-2的client就获得了锁，因为他的序号现在最小。当然客户端需要启动观察模式，在znode被删除时才能获得通知：此时他已经获得了锁。\n获得锁的伪代码如下：\n\n  1. 在lock的znode下创建名字为lock-的ephemeral类型znode，并记录下创建的znode的path（会在创建函数中返回）。\n  2. 获取lock znode的子节点列表，并开启对lock的子节点的watch模式。\n  3. 如果创建的子节点的序号最小，则再执行一次第2步，那么就表示已经获得锁了。退出。\n\n等待第2步的观察模式的通知，如果获得通知，则再执行第2步。\n\n###  羊群效应 \n\n虽然这个算法是正确的，但是还是有一些问题。第一个问题是羊群效应。试想一下，当有成千成百的client正在试图获得锁。每一个client都对lock节点开启了观察模式，等待lock的子节点的变化通知。每次锁的释放和获取，观察模式将被触发，每个client都会得到消息。那么羊群效应就是指像这样，大量的client都会获得相同的事件通知，而只有很小的一部分client会对事件通知有响应。我们这里，只有一个client将获得锁，但是所有的client都得到了通知。那么这就像在网络公路上撒了把钉子，增加了ZooKeeper服务器的压力。\n\n为了避免羊群效应，通知的范围需要更精准。我们通过观察发现，只有当序号排在当前znode之前一个znode离开时，才有必要通知创建当前znode的client，而不必在任意一个znode删除或者创建时都通知client。在我们的例子中，如果client1、client2和client3创建了znode/leader/lock-1、/leader/lock-2和leader/lock-3，client3仅在/leader/lock-2消失时，才获得通知。而不需要在/leader/lock-1消失时，或者新建/leader/lock-4时，获得通知。\n\n\n### 重新获取异常 Recoverable Exception\n\n这个锁算法的另一个问题是没有处理当连接中断造成的创建失败。在这种情况下，我们根本就不知道之前的创建是否成功了。创建一个可排序的znode是一个非等幂操作，所以我们不能简单重试，因为如果第一次我们创建成功了，那么第一次创建的znode就成了一个孤立的znode了，将永远不会被删除直到会话结束。\n\n那么问题的关键在于，在重新连接以后，client不能确定是否之前创建过lock节点的子节点。我们在znode的名字中间嵌入一个client的ID，那么在重新连接后，就可以通过检查lock znode的子节点znode中是否有名字包含client ID的节点。如果有这样的节点，说明之前创建节点操作成功了，就不需要再创建了。如果没有这样的节点，那就重新创建一个。\n\nClient的会话ID是一个长整型数据，并且在ZooKeeper中是唯一的。我们可以使用会话的ID在处理连接丢失事件过程中作为client的id。在ZooKeeper的JAVA API中，我们可以调用getSessionId()方法来获得会话的ID。\n\n那么Ephemeral类型的可排序znode不要命名为lock-<sessionId>-，所以当加上序号后就变成了lock-<sessionId>-<sequenceNumber>。那么序号虽然针对上一级名字是唯一的，但是上一级名字本身就是唯一的，所以这个方法既可以标记znode的创建者，也可以实现创建的顺序排序。\n  \n### 不能恢复异常 Unrecoverable Exception\n\n如果client的会话过期，那么他创建的ephemeral znode将被删除，client将立即失去锁（或者至少放弃获得锁的机会）。应用需要意识到他不再拥有锁，然后清理一切状态，重新创建一个锁对象，并尝试再次获得锁。注意，应用必须在得到通知的第一时间进行处理，因为应用不知道如何在znode被删除事后判断是否需要清理他的状态。\n\n\n### 实现 Implementation\n\n考虑到所有的失败模式的处理的繁琐，所以实现一个正确的分布式锁是需要做很多细微的设计工作。好在ZooKeeper为我们提供了一个 产品级质量保证的锁的实现，我们叫做WriteLock。我们可以轻松的在client中应用。\n\n### 更多的分布式数据结构和协议\n\n我们可以用ZooKeeper来构建很多分布式数据结构和协议，例如，barriers，queues和two-phase commit。有趣的是我们注意到这些都是同步协议，而我们却使用ZooKeeper的原生异步特征（比如通知机制）来构建他们。\n在ZooKeeper官网上提供了一些数据结构和协议的伪代码。并且提供了实现这些的数据结构和协议的标准教程（包括locks、leader选举和队列）；你可以在recipes目录中找到。\n\nApache Curator project也提供了一些简单客户端的教程。\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "zookeeeper/zk安装和运行.md",
    "content": "\n\n> 本文由holynull发表在了gitbook上\n> 大家可以点击这里获取更好的阅读体验: https://holynull.gitbooks.io/zookeeper/content/chapter1.html\n\n> zk目前更新到了3.x版本，官网在这里：https://zookeeper.apache.org/doc/current/zookeeperStarted.html\n\n另外推荐文章：\nhttp://www.importnew.com/23237.html\n\n\n## 简介\n\nZookeeper是Hadoop分布式调度服务，用来构建分布式应用系统。构建一个分布式应用是一个很复杂的事情，主要的原因是我们需要合理有效的处理分布式集群中的部分失败的问题。例如，集群中的节点在相互通信时，A节点向B节点发送消息。A节点如果想知道消息是否发送成功，只能由B节点告诉A节点。那么如果B节点关机或者由于其他的原因脱离集群网络，问题就出现了。A节点不断的向B发送消息，并且无法获得B的响应。B也没有办法通知A节点已经离线或者关机。集群中其他的节点完全不知道B发生了什么情况，还在不断的向B发送消息。这时，你的整个集群就发生了部分失败的故障。\nZookeeper不能让部分失败的问题彻底消失，但是它提供了一些工具能够让你的分布式应用安全合理的处理部分失败的问题。\n\n## 安装和运行Zookeeper\n\n我们采用standalone模式，安装运行一个单独的zookeeper服务。安装前请确认您已经安装了Java运行环境。\n我们去Apache ZooKeeper releases page下载zookeeper安装包，并解压到本地：\n\n```\n% tar xzf zookeeper-x.y.z.tar.gz\n```\nZooKeeper提供了一些可执行程序的工具，为了方便起见，我们将这些工具的路径加入到PATH环境变量中：\n```\n% export ZOOKEEPER_HOME=~/sw/zookeeper-x.y.z\n% export PATH=$PATH:$ZOOKEEPER_HOME/bin\n```\n运行ZooKeeper之前我们需要编写配置文件。配置文件一般在安装目录下的conf/zoo.cfg。我们可以把这个文件放在/etc/zookeeper下，或者放到其他目录下，并在环境变量设置ZOOCFGDIR指向这个个目录。下面是配置文件的内容：\n```\ntickTime=2000\ndataDir=/Users/tom/zookeeper\nclientPort=2181\n```\ntickTime是zookeeper中的基本时间单元，单位是毫秒。datadir是zookeeper持久化数据存放的目录。clientPort是zookeeper监听客户端连接的端口，默认是2181.\n启动命令：\n```\n% zkServer.sh start\n```\n我们通过nc或者telnet命令访问2181端口，通过执行ruok（Are you OK?）命令来检查zookeeper是否启动成功：\n```\n% echo ruok | nc localhost 2181\nimok\n```\n那么我看见zookeeper回答我们“I’m OK”。\n\n"
  },
  {
    "path": "zookeeeper/zk开发实例.md",
    "content": "\n## ZooKeeper中的组和成员\n\n我们可以把Zookeeper理解为一个高可用的文件系统。但是它没有文件和文件夹的概念，只有一个叫做znode的节点概念。那么znode即是数据的容器，也是其他节点的容器。（其实znode就可以理解为文件或者是文件夹）我们用父节点和子节点的关系来表示组和成员的关系。那么一个节点代表一个组，组节点下的子节点代表组内的成员.\n如下图所示：\n\n![zk](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/zookeeeper/859b65a6868f6b56eadd77a226db5e03.jpeg)\n\n## 创建组\n\n我们使用zookeeper的Java API来创建一个/zoo的组节点：\n```\npublic class CreateGroup implements Watcher {\n private static final int SESSION_TIMEOUT = 5000;\n private ZooKeeper zk;\n private CountDownLatch connectedSignal = new CountDownLatch(1);\n public void connect(String hosts) throws IOException, InterruptedException {\n     zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);\n     connectedSignal.await();\n }\n\n @Override\n public void process(WatchedEvent event) { // Watcher interface\n     if (event.getState() == KeeperState.SyncConnected) {\n     connectedSignal.countDown();\n     }\n }\n\n public void create(String groupName) throws KeeperException,\n InterruptedException {\n     String path = \"/\" + groupName;\n     String createdPath = zk.create(path, null/*data*/, Ids.OPEN_ACL_UNSAFE,\n     CreateMode.PERSISTENT);\n     System.out.println(\"Created \" + createdPath);\n }\n\n public void close() throws InterruptedException {\n     zk.close();\n }\n\n public static void main(String[] args) throws Exception {\n     CreateGroup createGroup = new CreateGroup();\n     createGroup.connect(args[0]);\n     createGroup.create(args[1]);\n     createGroup.close();\n     }\n }\n ```\n当main()执行时，首先创建了一个CreateGroup的对象，然后调用connect()方法，通过zookeeper的API与zookeeper服务器连接。创建连接我们需要3个参数：一是服务器端主机名称以及端口号，二是客户端连接服务器session的超时时间，三是Watcher接口的一个实例。Watcher实例负责接收Zookeeper数据变化时产生的事件回调。\n\n在连接函数中创建了zookeeper的实例，然后建立与服务器的连接。建立连接函数会立即返回，所以我们需要等待连接建立成功后再进行其他的操作。我们使用CountDownLatch来阻塞当前线程，直到zookeeper准备就绪。这时，我们就看到Watcher的作用了。我们实现了Watcher接口的一个方法：\n```\npublic void process(WatchedEvent event);\n```\n当客户端连接上了zookeeper服务器，Watcher将由process()函数接收一个连接成功的事件。我们接下来调用CountDownLatch，释放之前的阻塞。\n\n连接成功后，我们调用create()方法。我们在这个方法中调用zookeeper实例的create()方法来创建一个znode。参数包括：一是znode的path；二是znode的内容（一个二进制数组），三是一个access control list(ACL，访问控制列表，这里使用完全开放模式)，最后是znode的性质。\n\nznode的性质分为ephemeral和persistent两种。ephemeral性质的znode在创建他的客户端的会话结束，或者客户端以其他原因断开与服务器的连接时，会被自动删除。而persistent性质的znode就不会被自动删除，除非客户端主动删除，而且不一定是创建它的客户端可以删除它，其他客户端也可以删除它。这里我们创建一个persistent的znode。\ncreate()将返回znode的path。我们讲新建znode的path打印出来。\n我们执行如上程序：\n```\n% export CLASSPATH=ch21-zk/target/classes/:$ZOOKEEPER_HOME/*:\\\n$ZOOKEEPER_HOME/lib/*:$ZOOKEEPER_HOME/conf\n% java CreateGroup localhost zoo\nCreated /zoo\n```\n\n### 加入组\n接下来我们实现如何在一个组中注册成员。我们将使用ephemeral znode来创建这些成员节点。那么当客户端程序退出时，这些成员将被删除。\n我们创建一个ConnetionWatcher类，然后继承实现一个JoinGroup类：\n\n```\npublic class ConnectionWatcher implements Watcher {\n\n private static final int SESSION_TIMEOUT = 5000;\n\n protected ZooKeeper zk;\n\n private CountDownLatch connectedSignal = new CountDownLatch(1);\n\n public void connect(String hosts) throws IOException, InterruptedException {\n     zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);\n     connectedSignal.await();\n }\n\n @Override\n public void process(WatchedEvent event) {\n     if (event.getState() == KeeperState.SyncConnected) {\n         connectedSignal.countDown();\n     }\n }\n\n public void close() throws InterruptedException {\n     zk.close();\n }\n}\npublic class JoinGroup extends ConnectionWatcher {\n\n public void join(String groupName, String memberName) throws KeeperException,\n InterruptedException {\n     String path = \"/\" + groupName + \"/\" + memberName;\n     String createdPath = zk.create(path, null/*data*/, Ids.OPEN_ACL_UNSAFE,\n     CreateMode.EPHEMERAL);\n     System.out.println(\"Created \" + createdPath);\n }\n\n public static void main(String[] args) throws Exception {\n     JoinGroup joinGroup = new JoinGroup();\n     joinGroup.connect(args[0]);\n     joinGroup.join(args[1], args[2]);\n     // stay alive until process is killed or thread is interrupted\n     Thread.sleep(Long.MAX_VALUE);\n }\n}\n```\n\n加入组与创建组非常相似。我们加入了一个ephemeral znode后，让线程阻塞住。然后我们可以使用命令行查看zookeeper中我们创建的znode。当我们将阻塞的程序强行关闭后，我们会发现我们创建的znode会自动消失。\n\n### 成员列表\n\n下面我们实现一个程序来列出一个组中的所有成员。\n\n```\npublic class ListGroup extends ConnectionWatcher {\n\n public void list(String groupName) throws KeeperException,\n InterruptedException {\n     String path = \"/\" + groupName;\n     try {\n         List<String> children = zk.getChildren(path, false);\n         if (children.isEmpty()) {\n             System.out.printf(\"No members in group %s\\n\", groupName);\n             System.exit(1);\n         }\n         for (String child : children) {\n             System.out.println(child);\n         }\n     } catch (KeeperException.NoNodeException e) {\n         System.out.printf(\"Group %s does not exist\\n\", groupName);\n         System.exit(1);\n     }\n }\n\n public static void main(String[] args) throws Exception {\n     ListGroup listGroup = new ListGroup();\n     listGroup.connect(args[0]);\n     listGroup.list(args[1]);\n     listGroup.close();\n }\n}\n\n```\n\n我们在list()方法中通过调用getChildren()方法来获得某一个path下的子节点，然后打印出来。我们这里会试着捕获KeeperException.NoNodeException，当znode不存在时会抛出这个异常。我们运行程序，会看见如下结果，说明我们还没在zoo组中添加任何成员几点：\n```\n% java ListGroup localhost zoo\nNo members in group zoo\n```\n我们可以运行之前的JoinGroup来添加成员。在后台运行一些JoinGroup程序，这些程序添加节点后都处于sleep状态：\n```\n% java JoinGroup localhost zoo duck &\n% java JoinGroup localhost zoo cow &\n% java JoinGroup localhost zoo goat &\n% goat_pid=$!\n```\n最后一行命令的作用是将最后一个启动的java程序的pid记录下来，我们好在列出zoo下面的成员后，将该进程kill掉。\n下面我们将zoo下的成员打印出来：\n```\n% java ListGroup localhost zoo\ngoat\nduck\ncow\n然后我们将kill掉最后启动的JoinGroup客户端：\n% kill $goat_pid\n过几秒后，我们发现goat节点不见了。因为之前我们创建的goat节点是一个ephemeral节点，而创建这个节点的客户端在ZooKeeper上的会话已经被终结了，因为这个回话在5秒后失效了（我们设置了会话的超时时间为5秒）：\n% java ListGroup localhost zoo\nduck\ncow\n```\n\n让我们回过头来看看，我们到底都做了一些什么？我们首先创建了一个节点组，这些节点的创建者都在同一个分布式系统中。这些节点的创建者之间互相都不知情。一个创建者想使用这些节点数据进行一些工作，例如通过znode节点是否存在来判断节点的创建者是否存在。\n\n最后一点，我们不能只依靠组成员关系来完全解决在与节点通信时的网络错误。当与一个集群组成员节点进行通信时，发生了通信失败，我们需要使用重试或者试验与组中其他的节点通信，来解决这次通信失败。\n\n### Zookeeper的命令行工具\n\nZookeeper有一套命令行工具。我们可以像如下使用，来查找zoo下的成员节点：\n\n```\n% zkCli.sh -server localhost ls /zoo\n[cow, duck]\n```\n\n你可以不加参数运行这个工具，来获得帮助。\n\n## 删除分组\n\n下面让我们来看一下如何删除一个分组？\nZooKeeper的API提供一个delete()方法来删除一个znode。我们通过输入znode的path和版本号（version number）来删除想要删除的znode。我们除了使用path来定位我们要删除的znode，还需要一个参数是版本号。只有当我们指定要删除的本版号，与znode当前的版本号一致时，ZooKeeper才允许我们将znode删除掉。这是一种optimistic locking机制，用来处理znode的读写冲突。我们也可以忽略版本号一致检查，做法就是版本号赋值为-1。\n删除一个znode之前，我们需要先删除它的子节点，就下如下代码中实现的那样：\n\n\n```\npublic class DeleteGroup extends ConnectionWatcher {\n\n public void delete(String groupName) throws KeeperException,\n InterruptedException {\n     String path = \"/\" + groupName;\n\n     try {\n         List<String> children = zk.getChildren(path, false);\n         for (String child : children) {\n             zk.delete(path + \"/\" + child, -1);\n         }\n         zk.delete(path, -1);\n     } catch (KeeperException.NoNodeException e) {\n         System.out.printf(\"Group %s does not exist\\n\", groupName);\n         System.exit(1);\n     }\n }\n\n public static void main(String[] args) throws Exception {\n     DeleteGroup deleteGroup = new DeleteGroup();\n     deleteGroup.connect(args[0]);\n     deleteGroup.delete(args[1]);\n     deleteGroup.close();\n }\n}\n```\n\n最后我们执行如下操作来删除zoo group：\n\n```\n% java DeleteGroup localhost zoo\n% java ListGroup localhost zoo\nGroup zoo does not exist\n```\n\n\n\n\n"
  },
  {
    "path": "zookeeeper/zk服务.md",
    "content": "\n## Zookeeper 服务\n\nZooKeeper 是一个高可用的高性能调度服务。这一节我们将讲述他的模型、操作和接口。\n\n## 数据模型 Data Model\n\nZooKeeper包含一个树形的数据模型，我们叫做znode。一个znode中包含了存储的数据和ACL（Access Control List）。ZooKeeper的设计适合存储少量的数据，并不适合存储大量数据，所以znode的存储限制最大不超过1M。\n\n数据的访问被定义成原子性的。什么是原子性呢？一个客户端访问一个znode时，不会只得到一部分数据；客户端访问数据要么获得全部数据，要么读取失败，什么也得不到。相似的，写操作时，要么写入全部数据，要么写入失败，什么也写不进去。ZooKeeper能够保证写操作只有两个结果，成功和失败。绝对不会出现只写入了一部分数据的情况。与HDFS不同，ZooKeeper不支持字符的append（连接）操作。原因是HDFS是被设计成支持数据流访问（streaming data access）的大数据存储，而ZooKeeper则不是。\n\n我们可以通过path来定位znode，就像Unix系统定位文件一样，使用斜杠来表示路径。但是，znode的路径只能使用绝对路径，而不能想Unix系统一样使用相对路径，即Zookeeper不能识别../和./这样的路径。\n\n节点的名称是由Unicode字符组成的，除了zookeeper这个字符串，我们可以任意命名节点。为什么不能使用zookeeper命名节点呢？因为ZooKeeper已经默认使用zookeeper来命名了一个根节点，用来存储一些管理数据。\n\n请注意，这里的path并不是URIs，在Java API中是一个String类型的变量。\n\n\n### Ephemeral znodes\n\n我们已经知道，znode有两种类型：ephemeral和persistent。在创建znode时，我们指定znode的类型，并且在之后不会再被修改。当创建znode的客户端的session结束后，ephemeral类型的znode将被删除。persistent类型的znode在创建以后，就与客户端没什么联系了，除非主动去删除它，否则他会一直存在。Ephemeral znode没有任何子节点。\n\n虽然Ephemeral znode绑定了客户端session，但是对任何其他客户端都是可见的，当然是在他们的ACL策略下允许访问的情况下。\n当我们在创建分布式系统时，需要知道分布式资源是否可用。Ephemeral znode就是为这种场景应运而生的。正如我们之前讲述的例子中，使用Ephemeral znode来实现一个成员关系管理，任何一个客户端进程任何时候都可以知道其他成员是否可用。\nZnode的序号\n\n如果在创建znode时，我们使用排序标志的话，ZooKeeper会在我们指定的znode名字后面增加一个数字。我们继续加入相同名字的znode时，这个数字会不断增加。这个序号的计数器是由这些排序znode的父节点来维护的。\n\n如果我们请求创建一个znode，指定命名为/a/b-，那么ZooKeeper会为我们创建一个名字为/a/b-3的znode。我们再请求创建一个名字为/a/b-的znode，ZooKeeper会为我们创建一个名字/a/b-5的znode。ZooKeeper给我们指定的序号是不断增长的。Java API中的create()的返回结果就是znode的实际名字。\n\n那么序号用来干什么呢？当然是用来排序用的！后面《A Lock Service》中我们将讲述如何使用znode的序号来构建一个share lock。\n\n### 观察模式 Watches\n\n观察模式可以使客户端在某一个znode发生变化时得到通知。观察模式有ZooKeeper服务的某些操作启动，并由其他的一些操作来触发。例如，一个客户端对一个znode进行了exists操作，来判断目标znode是否存在，同时在znode上开启了观察模式。如果znode不存在，这exists将返回false。如果稍后，另外一个客户端创建了这个znode，观察模式将被触发，将znode的创建事件通知之前开启观察模式的客户端。我们将在以后详细介绍其他的操作和触发。\n\n观察模式只能被触发一次。如果要一直获得znode的创建和删除的通知，那么就需要不断的在znode上开启观察模式。在上面的例子中，如果客户端还继续需要获得znode被删除的通知，那么在获得创建通知后，客户端还需要继续对这个znode进行exists操作，再开启一次观察模式。\n\n在《A Configuration Service》中，有一个例子将讲述如何使用观察模式在集群中更新配置。\n\n\n## 操作 Operations\n\n下面的表格中列出了9种ZooKeeper的操作。\n\n操作\t说明\ncreate\t Creates a znode (the parent znode must already exist)\ndelete\t Deletes a znode (the znode must not have any children)\nexists\t Tests whether a znode exists and retrieves its metadata\ngetACL, setACL\t Gets/sets the ACL for a znode\ngetChildren\t Gets a list of the children of a znode\ngetData,setData\t Gets/sets the data associated with a znode\nsync\tSynchronizes a client’s view of a znode with ZooKeeper\n\n\n调用delete和setData操作时，我们必须指定一个znode版本号（version number），即我们必须指定我们要删除或者更新znode数据的哪个版本。如果版本号不匹配，操作将会失败。失败的原因可能是在我们提交之前，该znode已经被修改过了，版本号发生了增量变化。那么我们该怎么办呢？我可以考虑重试，或者调用其他的操作。例如，我们提交更新失败后，可以重新获取znode当前的数据，看看当前的版本号是什么，再做更新操作。\n\nZooKeeper虽然可以被看作是一个文件系统，但是由于ZooKeeper文件很小，所以没有提供像一般文件系统所提供的open、close或者seek操作。\n\n> 注意\n> 这里的sync操作与POSIX文件系统的fsync()操作是不同的。就像我们早前讲过的，ZooKeeper的写操作是原子性的，一个成功的写操作只保证数据被持久化到大多数ZooKeeper的服务器存储上。所以读操作可能会读取不到最新状态的数据，sync操作用来让client强制所访问的ZooKeeper服务器上的数据状态更新到最新状态。我们会在《一致性 Consistentcy》一节中详细介绍。\n\n### 批量更新 Multiupdate\nZooKeeper支持将一些原始的操作组合成一个操作单元，然后执行这些操作。那么这种批量操作也是具有原子性的，只可能有两种执行结果，成功和失败。批量操作单元中的操作，不会出现一些操作执行成功，一些操作执行失败的情况，即要么都成功，要么都失败。\n\nMultiupdate对于绑定一些结构化的全局变量很有用处。例如绑定一个无向图（undirected graph）。无向图的顶点（vertex）由znode来表示。添加和删除边（edge）的操作，由修改边的两个关联znode来实现。如果我们使用ZooKeeper的原始的操作来实现对边（edge）的操作，那么就有可能产生两个znode修改不一致的情况（一个修改成功，一个修改失败）。那么我们将修改两个znode的操作放入到一个Multi修改单元中，就能够保证两个znode，要么都修改成功，要么都修改失败。这样就能够避免修改无向图的边时产生修改不一致的现象。\n\n### APIs\nZooKeeper客户端使用的核心编程语言有JAVA和C；同时也支持Perl、Python和REST。执行操作的方式呢，分为同步执行和异步执行。我们之前已经见识过了同步的Java API中的exists。\n\n```\npublic Stat exists(String path, Watcher watcher) throws KeeperException,\n InterruptedException\n ```\n \n下面代码则是异步方式的exists:\n\n```\npublic void exists(String path, Watcher watcher, StatCallback cb, Object ctx)\n```\nJava API中，异步的方法的返回类型都是void，而操作的返回的结果将传递到回调对象的回调函数中。回调对象将实现StatCallback接口中的一个回调函数，来接收操作返回的结果。函数接口如下：\n\n```\npublic void processResult(int rc, String path, Object ctx, Stat stat);\n```\n\n参数rc表示返回码，请参考KeeperException中的定义。在stat参数为null的情况下，非0的值表示一种异常。参数path和ctx与客户端调用的exists方法中的参数相等，这两个参数通常用来确定回调中获得的响应是来至于哪个请求的。参数ctx可以是任意对象，只有当path参数不能消灭请求的歧义时才会用到。如果不需要参数ctx，可以设置为null。\n\n> 应该使用同步API还是异步API呢?\n> 两种API提供了相同的功能，需要使用哪种API取决于你程序的模式。例如，你设计的程序模式是一个事件驱动模式的程序，那么你最好使用异步API。异步API也可以被用在追求一个比较好的数据吞吐量的场景。想象一下，如果你需要得去大量的znode数据，并且依靠独立的进程来处理他们。如果使用同步API,每次读取操作都会被阻塞住，直到返回结果。不如使用异步API，读取操作可以不必等待返回结果，继续执行。而使用另外的线程来处理返回结果。\n\n### 观察模式触发器 Watch triggers\n\n读操作，例如：exists、getChildren、getData会在znode上开启观察模式，并且写操作会触发观察模式事件，例如：create、delete和setData。ACL(Access Control List)操作不会启动观察模式。观察模式被触发时，会生成一个事件，这个事件的类型取决于触发他的操作：\n 1, exists启动的观察模式，由创建znode，删除znode和更新znode操作来触发。\n 2,getData启动的观察模式，由删除znode和更新znode操作触发。创建znode不会触发，是因为getData操作成功的前提是znode必须已经存在。\n 3,getChildren启动的观察模式，由子节点创建和删除，或者本节点被删除时才会被触发。我们可以通过事件的类型来判断是本节点被删除还是子节点被删除：NodeChildrenChanged表示子节点被删除，而NodeDeleted表示本节点删除。\n\n事件包含了触发事件的znode的path，所以我们通过NodeCreated和NodeDeleted事件就可以知道哪个znode被创建了或者删除了。如果我们需要在NodeChildrenChanged事件发生后知道哪个子节点被改变了，我们就需要再调用一次getChildren来获得一个新的子节点列表。与之类似，在NodeDataChanged事件发生后，我们需要调用getData来获得新的数据。我们在编写程序时，会在接收到事件通知后改变znode的状态，所以我们一定要清楚的记住znode的状态变化。\n\n### ACLs 访问控制操作\nznode的创建时，我们会给他一个ACL（Access Control List），来决定谁可以对znode做哪些操作。\nZooKeeper通过鉴权来获得客户端的身份，然后通过ACL来控制客户端的访问。鉴权方式有如下几种：\n * digest\n使用用户名和密码方式\n\n * sasl\n使用Kerberos鉴权\n\n * ip\n使用客户端的IP来鉴权\n\n客户端可以在与ZooKeeper建立会话连接后，自己给自己授权。授权是并不是必须的，虽然znode的ACL要求客户端必须是身份合法的，在这种情况下，客户端可以自己授权来访问znode。下面的例子，客户端使用用户名和密码为自己授权：\n\n```\n zk.addAuthInfo(\"digest\", \"tom:secret\".getBytes());\n ```\n \nACL是由鉴权方式、鉴权方式的ID和一个许可（permession）的集合组成。例如，我们想通过一个ip地址为10.0.0.1的客户端访问一个znode。那么，我们需要为znode设置一个ACL，鉴权方式使用IP鉴权方式，鉴权方式的ID为10.0.0.1，只允许读权限。使用JAVA我们将像如下方式创建一个ACL对象：\n\n```\nnew ACL(Perms.READ,new Id(\"ip\", \"10.0.0.1\"));\n```\n\n所有的许可权限将在下表中列出。请注意，exists操作不受ACL的控制，所以任何一个客户端都可以通过exists操作来获得任何znode的状态，从而得知znode是否真的存在。\n\n在ZooDefs.Ids类中，有一些ACL的预定义变量，包括OPEN_ACL_UNSAFE，这个设置表示将赋予所有的许可给客户端（除了ADMIN的许可）。\n\n另外，我们可以使用ZooKeeper鉴权的插件机制，来整合第三方的鉴权系统。\n\n\n## 实现 Implementation\n\nZooKeeper服务可以在两种模式下运行。在standalone模式下，我们可以运行一个单独的ZooKeeper服务器，我们可以在这种模式下进行基本功能的简单测试，但是这种模式没有办法体现ZooKeeper的高可用特性和快速恢复特性。在生产环境中，我们一般采用replicated（复制）模式安装在多台服务器上，组建一个叫做ensemble的集群。ZooKeeper在他的副本之间实现高可用性，并且只要ensemble集群中能够推举出主服务器，ZooKeeper的服务就可以一直不终断。例如，在一个5个节点的ensemble中，容忍有2个节点脱离集群，服务还是可用的。因为剩下的3个节点投票，可以产生超过集群半数的投票，来推选一台主服务器。而6个节点的ensemble中，也只能容忍2个节点的服务器死机。因为如果3个节点脱离集群，那么剩下的3个节点无论如何不能产生超过集群半数的投票来推选一个主服务器。所以，一般情况下ensemble中的服务器数量都是奇数。\n\n从概念上来看，ZooKeeper其实是很简单的。他所做的一切就是保证每一次对znode树的修改，都能够复制到ensemble的大多数服务器上。如果非主服务器脱离集群，那么至少有一台服务器上的副本保存了最新状态。剩下的其他的服务器上的副本，会很快更新这个最新的状态。\n\n为了实现这个简单而不平凡的设计思路，ZooKeeper使用了一个叫做Zab的协议。这个协议分为两阶段，并且不断的运行在ZooKeeper上：\n\n阶段 1：领导选举（Leader election）\nEnsemble中的成员通过一个程序来选举出一个首领成员，我们叫做leader。其他的成员就叫做follower。在大多数（quorum）follower完成与leader状态同步时，这个阶段才结束。\n\n阶段 2： 原子广播（Atomic broadcast）\n所有的写入请求都会发送给leader，leader在广播给follower。当大多数的follower已经完成了数据改变，leader才会将更新提交，客户端就会随之得到leader更新成功的消息。协议中的设计也是具有原子性的，所以写入操作只有成功和失败两个结果。\n\n如果leader脱离了集群，剩下的节点将选举一个新的leader。如果之前的leader回到了集群中，那么将被视作一个follower。leader的选举很快，大概200ms就能够产生结果，所以不会影响执行效率。\nEnsemble中的所有节点都会在更新内存中的znode树的副本之前，先将更新数据写入到硬盘上。读操作可以请求任何一台ZooKeeper服务器，而且读取速度很快，因为读取是内存中的数据副本。\n\n## 数据一致性 Consistency\n\n理解了ZooKeeper的实现原理，有助于理解ZooKeeper如何保证数据的一致性。就像字面上理解的“leader”和“follower”的意思一样，在ensemble中follower的update操作会滞后于leader的update完成。事实的结果使我们在提交更新数据之前，不必在每一台ZooKeeper服务器上执行持久化变更数据，而是仅需在主服务器上执行持久化变更数据。ZooKeeper客户端的最佳实践是全部链接到follower上。然而客户端是有可能连接到leader上的，并且客户端控制不了这个选择，甚至客户端并不知道连接到了follower还是leader。下图所示，读操作向follower请求即可，而写操作由leader来提交。\n\n每一个对znode树的更新操作，都会被赋予一个全局唯一的ID，我们称之为zxid（ZooKeeper Transaction ID）。更新操作的ID按照发生的时间顺序升序排序。例如，例如z1小于z2，那么z1的操作就早于z2的操作。\n\nZooKeeper在数据一致性上实现了如下几个方面：\n\n顺序一致性\n从客户端提交的更新操作是按照先后循序排序的。例如，如果一个客户端将一个znode z赋值为a，然后又将z的值改变成b，那么在这个过程中不会有客户端在z的值变为b后，取到的值是a。\n\n原子性\n更新操作的结果不是失败就是成功。即，如果更新操作失败，其他的客户端是不会知道的。\n\n系统视图唯一性\n无论客户端连接到哪个服务器，都将看见唯一的系统视图。如果客户端在同一个会话中去连接一个新的服务器，那么他所看见的视图的状态不会比之前服务器上看见的更旧。当ensemble中的一个服务器宕机，客户端去尝试连接另外一台服务器时，如果这台服务器的状态旧于之前宕机的服务器，那么服务器将不会接受客户端的连接请求，直到服务器的状态赶上之前宕机的服务器为止。\n\n持久性\n一旦更新操作成功，数据将被持久化到服务器上，并且不能撤销。所以服务器宕机重启，也不会影响数据。\n时效性\n\n系统视图的状态更新的延迟时间是有一个上限的，最多不过几十秒。如果服务器的状态落后于其他服务器太多，ZooKeeper会宁可关闭这个服务器上的服务，强制客户端去连接一个状态更新的服务器。\n\n从执行效率上考虑，读操作的目标是内存中的缓存数据，并且读操作不会参与到写操作的全局排序中。这就会引起客户端在读取ZooKeeper的状态时产生不一致。例如，A客户端将znode z的值由a改变成a1，然后通知客户端B去读取z的值，但是B读取到的值是a，而不是修改后的a1，为了阻止这种情况出现，B在读取z的值之前，需要调用sync方法。sync方法会强制B连接的服务器状态与leader的状态同步，这样B在读取z的值就是A重新更改过的值了。\n\n> sync操作只在异步调用时才可用，原因是你不需要等待操作结束再去执行其他的操作。因此，ZooKeeper保证所有的子操作都会在sync结束后再执行，甚至在sync操作之前发出的操作请求也不例外。\n\n\n## 会话 Sessions\n\nZooKeeper的客户端中，配置了一个ensemble服务器列表。当启动时，首先去尝试连接其中一个服务器。如果尝试连接失败，那么会继续尝试连接下一个服务器，直到连接成功或者全部尝试连接失败。\n\n一旦连接成功，服务器就会为客户端创建一个会话（session）。session的过期时间由创建会话的客户端应用来设定，如果在这个时间期间，服务器没有收到客户端的任何请求，那么session将被视为过期，并且这个session不能被重新创建，而创建的ephemeral znode将随着session过期被删除掉。在会话长期存在的情况下，session的过期事件是比较少见的，但是应用程序如何处理好这个事件是很重要的。（我们将在《The Resilient ZooKeeper Application》中详细介绍）\n在长时间的空闲情况下，客户端会不断的发送ping请求来保持session。（ZooKeeper的客户端开发工具的liberay实现了自动发送ping请求，所以我们不必去考虑如何维持session）ping请求的间隔被设置成足够短，以便能够及时发现服务器失败（由读操作的超时时长来设置），并且能够及时的在session过期前连接到其他服务器上。\n容错连接到其他服务器上，是由ZooKeeper客户端自动完成的。重要的是在连接到其他服务器上后，之前的session以及epemeral节点还保持可用状态。\n在容错的过程中，应用将收到与服务断开连接和连接的通知。Watch模式的通知在断开链接时，是不会发送断开连接事件给客户端的，断开连接事件是在重新连接成功后发送给客户端的。如果在重新连接到其他节点时，应用尝试一个操作，这个操作是一定会失败的。对于这一点的处理，是一个ZooKeeper应用的重点。\n\n## 时间 Time\n\n在ZooKeeper中有一些时间的参数。tick是ZooKeeper的基础时间单位，用来定义ensemble中服务器上运行的程序的时间表。其他时间相关的配置都是以tick为单位的，或者以tick的值为最大值或者最小值。例如，session的过期时间在2 ticks到20 ticks之间，那么你再设置时选择的session过期时间必须在2和20之间的一个数。\n\n通常情况1 tick等于2秒。那么就是说session的过期时间的设置范围在4秒到40秒之间。在session过期时间的设置上有一些考虑。过期时间太短会造成加快物理失败的监测频率。在组成员关系的例子中，session的过期时间与从组中移除失败的成员花费的时间相等。如果设置过低的session过期时间，那么网络延迟就有可能造成非预期的session过期。这种情况下，就会出现在短时间内一台机器不断的离开组，然后又从新加入组中。\n\n如果应用需要创建比较复杂的临时状态，那么就需要较长的session过期时间，因为重构花费的时间比较长。有一些情况下，需要在session的生命周期内重启，而且要保证重启完后session不过期（例如，应用维护和升级的情况）。服务器会给每一个session一个ID和密码，如果在连接创建时，ZooKeeper验证通过，那么session将被恢复使用（只要session没过期就行）。所以应用程序可以实现一个优雅的关机动作，在重启之前，将session的ID和密码存储在一个稳定的地方。重启之后，通过ID和密码恢复session。\n\n这仅仅是在一些特殊的情况下，我们需要使用这个特性来使用比较长的session过期时间。大多数情况下，我们还是要考虑当出现非预期的异常失败时，如何处理session过期，或者仅需要优雅的关闭应用，在session过期前不用重启应用。\n\n通常情况也越大规模的ensemble，就需要越长的session过期时间。Connetction Timeout、Read Timeout和Ping Periods都由一个以服务器数量为参数的函数计算得到，当ensemble的规模扩大，这些值需要逐渐减小。如果为了解决经常失去连接而需要增加timeout的时长，建议你先监控一下ZooKeeper的metrics，再去调整。\n\n\n## 状态 States\n\nZooKeeper对象在他的生命周期内会有不同的状态，我们通过getState()来获得当前的状态。\n\n```\npublic States getState()\n```\n\n状态是一个枚举类型的数据。新构建的ZooKeeper对象在尝试连接ZooKeeper服务时的状态是CONNECTING，一旦与服务建立了连接那么状态就变成了CONNECTED。\n\n客户端可以通过注册一个观察者对象来接收ZooKeeper对象状态的迁移。当通过CONNECTED状态后，观察者将接收到一个WatchedEvent事件，他的属性KeeperState的值是SyncConnected。\n\n> 观察者有两个职能：一是接收ZooKeeper的状态改变通知；二是接收znode的改变通知。ZooKeeper对象构造时传递进去的watcher对象，默认是用来接收状态改变通知的，但是znode的改变通知也可能会共享使用默认的watcher对象，或者使用一个专用的watcher。我们可以通过一个Boolean变量来指定是否使用共享默认watcher。\n\nZooKeeper实例会与服务连接断开或者重新连接，状态会在CONNECTING和CONNECTED之间转换。如果连接断开，watcher会收到一个断开连接事件。请注意，这两个状态都是ZooKeeper实例自己初始化的，并且在断开连接后会自动进行重连接。\n\n如果调用了close()或者session过期，ZooKeeper实例会转换为第三个状态CLOSED，此时在接受事件的KeeperState属性值为Expired。一旦ZooKeeper的状态变为CLOSED，说明实例已经不可用（可以通过isAlive()来判断），并且不能再被使用。如果要重新建立连接，就需要重新构建一个ZooKeeper实例。\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "分布式理论/分布式ID生成器解决方案.md",
    "content": "\n\n本文主要介绍在一个分布式系统中, 怎么样生成全局唯一的 ID\n\n## 一, 问题描述\n在分布式系统存在多个 Shard 的场景中, 同时在各个 Shard 插入数据时, 怎么给这些数据生成全局的 unique ID?\n\n在单机系统中 (例如一个 MySQL 实例), unique ID 的生成是非常简单的, 直接利用 MySQL 自带的自增 ID 功能就可以实现.\n\n但在一个存在多个 Shards 的分布式系统 (例如多个 MySQL 实例组成一个集群, 在这个集群中插入数据), 这个问题会变得复杂, 所生成的全局的 unique ID 要满足以下需求:\n\n* 保证生成的 ID 全局唯一\n* 今后数据在多个 Shards 之间迁移不会受到 ID 生成方式的限制\n* 生成的 ID 中最好能带上时间信息, 例如 ID 的前 k 位是 Timestamp, 这样能够直接通过对 ID 的前 k 位的排序来对数据按时间排序\n* 生成的 ID 最好不大于 64 bits\n* 生成 ID 的速度有要求. 例如, 在一个高吞吐量的场景中, 需要每秒生成几万个 ID (Twitter 最新的峰值到达了143,199 Tweets/s, 也就是 10万+/秒)\n* 整个服务最好没有单点\n\n\n如果没有上面这些限制, 问题会相对简单, 例如:\n\n* 直接利用 UUID.randomUUID() 接口来生成 unique ID (http://www.ietf.org/rfc/rfc4122.txt). 但这个方案生成的 ID 有 128 bits, 另外, 生成的 ID 中也没有带 Timestamp\n* 利用一个中心服务器来统一生成 unique ID. 但这种方案可能存在单点问题; 另外, 要支持高吞吐率的系统, 这个方案还要做很多改进工作 (例如, 每次从中心服务器批量获取一批 IDs, 提升 ID 产生的吞吐率)\n* Flickr 的做法 (http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/). 但他这个方案 ID 中没有带 Timestamp, 生成的 ID 不能按时间排序\n\n在要满足前面 6 点要求的场景中, 怎么来生成全局 unique ID 呢?\n\nTwitter 的 Snowflake 是一种比较好的做法. 下面主要介绍 Twitter Snowflake, 以及它的变种\n\n## 二, Twitter Snowflake\nhttps://github.com/twitter/snowflake\n\nSnowflake 生成的 unique ID 的组成 (由高位到低位):\n\n* 41 bits: Timestamp (毫秒级)\n* 10 bits: 节点 ID (datacenter ID 5 bits + worker ID 5 bits)\n* 12 bits: sequence number\n\n一共 63 bits (最高位是 0)\n\nunique ID 生成过程:\n\n* 10 bits 的机器号, 在 ID 分配 Worker 启动的时候, 从一个 Zookeeper 集群获取 (保证所有的 Worker 不会有重复的机器号)\n* 41 bits 的 Timestamp: 每次要生成一个新 ID 的时候, 都会获取一下当前的 Timestamp, 然后分两种情况生成 sequence number:\n* 如果当前的 Timestamp 和前一个已生成 ID 的 Timestamp 相同 (在同一毫秒中), 就用前一个 ID 的 sequence number + 1 作为新的 sequence number (12 bits); 如果本毫秒内的所有 ID 用完, 等到下一毫秒继续 (这个等待过程中, 不能分配出新的 ID)\n* 如果当前的 Timestamp 比前一个 ID 的 Timestamp 大, 随机生成一个初始 sequence number (12 bits) 作为本毫秒内的第一个 sequence number\n整个过程中, 只是在 Worker 启动的时候会对外部有依赖 (需要从 Zookeeper 获取 Worker 号), 之后就可以独立工作了, 做到了去中心化.\n\n异常情况讨论:\n\n* 在获取当前 Timestamp 时, 如果获取到的时间戳比前一个已生成 ID 的 Timestamp 还要小怎么办? Snowflake 的做法是继续获取当前机器的时间, 直到获取到更大的 Timestamp 才能继续工作 (在这个等待过程中, 不能分配出新的 ID)\n\n从这个异常情况可以看出, 如果 Snowflake 所运行的那些机器时钟有大的偏差时, 整个 Snowflake 系统不能正常工作 (偏差得越多, 分配新 ID 时等待的时间越久)\n\n从 Snowflake 的官方文档 (https://github.com/twitter/snowflake/#system-clock-dependency) 中也可以看到, 它明确要求 \"You should use NTP to keep your system clock accurate\". 而且最好把 NTP 配置成不会向后调整的模式. 也就是说, NTP 纠正时间时, 不会向后回拨机器时钟.\n\n## 三, Snowflake 的其他变种\nSnowflake 有一些变种, 各个应用结合自己的实际场景对 Snowflake 做了一些改动. 这里主要介绍 3 种.\n\n### 1. Boundary flake\nhttp://boundary.com/blog/2012/01/12/flake-a-decentralized-k-ordered-unique-id-generator-in-erlang/\n\n变化:\n\nID 长度扩展到 128 bits:\n最高 64 bits 时间戳;\n然后是 48 bits 的 Worker 号 (和 Mac 地址一样长);\n最后是 16 bits 的 Seq Number\n由于它用 48 bits 作为 Worker ID, 和 Mac 地址的长度一样, 这样启动时不需要和 Zookeeper 通讯获取 Worker ID. 做到了完全的去中心化\n基于 Erlang\n它这样做的目的是用更多的 bits 实现更小的冲突概率, 这样就支持更多的 Worker 同时工作. 同时, 每毫秒能分配出更多的 ID\n\n### 2. Simpleflake\nhttp://engineering.custommade.com/simpleflake-distributed-id-generation-for-the-lazy/\n\nSimpleflake 的思路是取消 Worker 号, 保留 41 bits 的 Timestamp, 同时把 sequence number 扩展到 22 bits;\n\nSimpleflake 的特点:\n\nsequence number 完全靠随机产生 (这样也导致了生成的 ID 可能出现重复)\n没有 Worker 号, 也就不需要和 Zookeeper 通讯, 实现了完全去中心化\nTimestamp 保持和 Snowflake 一致, 今后可以无缝升级到 Snowflake\nSimpleflake 的问题就是 sequence number 完全随机生成, 会导致生成的 ID 重复的可能. 这个生成 ID 重复的概率随着每秒生成的 ID 数的增长而增长.\n\n所以, Simpleflake 的限制就是每秒生成的 ID 不能太多 (最好小于 100次/秒, 如果大于 100次/秒的场景, Simpleflake 就不适用了, 建议切换回 Snowflake).\n\n### 3. instagram 的做法\n先简单介绍一下 instagram 的分布式存储方案:\n\n* 先把每个 Table 划分为多个逻辑分片 (logic Shard), 逻辑分片的数量可以很大, 例如 2000 个逻辑分片\n* 然后制定一个规则, 规定每个逻辑分片被存储到哪个数据库实例上面; 数据库实例不需要很多. 例如, 对有 2 个 PostgreSQL 实例的系统 (instagram 使用 PostgreSQL); 可以使用奇数逻辑分片存放到第一个数据库实例, 偶数逻辑分片存放到第二个数据库实例的规则\n* 每个 Table 指定一个字段作为分片字段 (例如, 对用户表, 可以指定 uid 作为分片字段)\n* 插入一个新的数据时, 先根据分片字段的值, 决定数据被分配到哪个逻辑分片 (logic Shard)\n* 然后再根据 logic Shard 和 PostgreSQL 实例的对应关系, 确定这条数据应该被存放到哪台 PostgreSQL 实例上\n\ninstagram unique ID 的组成:\n\n* 41 bits: Timestamp (毫秒)\n* 13 bits: 每个 logic Shard 的代号 (最大支持 8 x 1024 个 logic Shards)\n* 10 bits: sequence number; 每个 Shard 每毫秒最多可以生成 1024 个 ID\n\n生成 unique ID 时, 41 bits 的 Timestamp 和 Snowflake 类似, 这里就不细说了.\n\n主要介绍一下 13 bits 的 logic Shard 代号 和 10 bits 的 sequence number 怎么生成.\n\nlogic Shard 代号:\n\n* 假设插入一条新的用户记录, 插入时, 根据 uid 来判断这条记录应该被插入到哪个 logic Shard 中.\n* 假设当前要插入的记录会被插入到第 1341 号 logic Shard 中 (假设当前的这个 Table 一共有 2000 个 logic Shard)\n* 新生成 ID 的 13 bits 段要填的就是 1341 这个数字\n\nsequence number 利用 PostgreSQL 每个 Table 上的 auto-increment sequence 来生成:\n\n* 如果当前表上已经有 5000 条记录, 那么这个表的下一个 auto-increment sequence 就是 5001 (直接调用 PL/PGSQL 提供的方法可以获取到)\n* 然后把 这个 5001 对 1024 取模就得到了 10 bits 的 sequence number\n\ninstagram 这个方案的优势在于:\n\n* 利用 logic Shard 号来替换 Snowflake 使用的 Worker 号, 就不需要到中心节点获取 Worker 号了. 做到了完全去中心化\n* 另外一个附带的好处就是, 可以通过 ID 直接知道这条记录被存放在哪个 logic Shard 上\n\n同时, 今后做数据迁移的时候, 也是按 logic Shard 为单位做数据迁移的, 所以这种做法也不会影响到今后的数据迁移\n\n\n## 推荐一篇文章：微信的海量IM聊天消息序列号生成\nhttps://www.cnblogs.com/imstudy/p/9766549.html"
  },
  {
    "path": "分布式理论/分布式事务的解决方案.md",
    "content": "分布式事务的解决方案有如下几种：\n\n* 全局消息\n* 基于可靠消息服务的分布式事务\n* TCC\n* 最大努力通知\n\n\n## 方案1：全局事务（DTP模型）\n全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务，需要三种角色：\n\n* AP：Application 应用系统它就是我们开发的业务系统，在我们开发的过程中，可以使用资源管理器提供的事务接口来实现分布式事务。\n\n* TM：Transaction Manager 事务管理器\n\n    * 分布式事务的实现由事务管理器来完成，它会提供分布式事务的操作接口供我们的业务系统调用。这些接口称为TX接口。\n    * 事务管理器还管理着所有的资源管理器，通过它们提供的XA接口来同一调度这些资源管理器，以实现分布式事务。\n    * DTP只是一套实现分布式事务的规范，并没有定义具体如何实现分布式事务，TM可以采用2PC、3PC、Paxos等协议实现分布式事务。\n\n* RM：Resource Manager 资源管理器\n\n    * 能够提供数据服务的对象都可以是资源管理器，比如：数据库、消息中间件、缓存等。大部分场景下，数据库即为分布式事务中的资源管理器。\n    * 资源管理器能够提供单数据库的事务能力，它们通过XA接口，将本数据库的提交、回滚等能力提供给事务管理器调用，以帮助事务管理器实现分布式的事务管理。\n    * XA是DTP模型定义的接口，用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。\n    * DTP只是一套实现分布式事务的规范，RM具体的实现是由数据库厂商来完成的。\n\n### 实际方案：基于XA协议的两阶段提交\n\nXA是一个分布式事务协议，由Tuxedo提出。XA中大致分为两部分：事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现，比如Oracle、DB2这些商业数据库都实现了XA接口，而事务管理器作为全局的调度者，负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下：\n\n![af54327c64897b3b1a9770b7579aaa8c](分布式事务的解决方案.resources/CCA41F5C-D556-4C33-8408-F6256B6F6843.png)\n\n总的来说，XA协议比较简单，而且一旦商业数据库实现了XA协议，使用分布式事务的成本也比较低。但是，XA也有致命的缺点，那就是性能不理想，特别是在交易下单链路，往往并发量很高，XA无法满足高并发场景。XA目前在商业数据库支持的比较理想，在mysql数据库中支持的不太理想，mysql的XA实现，没有记录prepare阶段日志，主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA，这让XA的应用场景变得非常狭隘。\n\n## 方案2：基于可靠消息服务的分布式事务（事务消息中间件）\n\n这种实现分布式事务的方式需要通过消息中间件来实现。假设有A和B两个系统，分别可以处理任务A和任务B。此时系统A中存在一个业务流程，需要将任务A和任务B在同一个事务中处理。下面来介绍基于消息中间件来实现这种分布式事务。\n\n* 在系统A处理任务A前，首先向消息中间件发送一条消息\n* 消息中间件收到后将该条消息持久化，但并不投递。此时下游系统B仍然不知道该条消息的存在。\n* 消息中间件持久化成功后，便向系统A返回一个确认应答；\n* 系统A收到确认应答后，则可以开始处理任务A；\n* 任务A处理完成后，向消息中间件发送Commit请求。该请求发送完成后，对系统A而言，该事务的处理过程就结束了，此时它可以处理别的任务了。但commit消息可能会在传输途中丢失，从而消息中间件并不会向系统B投递这条消息，从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成，下文会介绍。\n* 消息中间件收到Commit指令后，便向系统B投递该消息，从而触发任务B的执行；\n* 当任务B执行完成后，系统B向消息中间件返回一个确认应答，告诉消息中间件该消息已经成功消费，此时，这个分布式事务完成。\n>上述过程可以得出如下几个结论：\n\n* 消息中间件扮演者分布式事务协调者的角色。\n* 系统A完成任务A后，到任务B执行完成之间，会存在一定的时间差。在这个时间差内，整个系统处于数据不一致的状态，但这短暂的不一致性是可以接受的，因为经过短暂的时间后，系统又可以保持数据一致性，满足BASE理论。\n\n上述过程中，如果任务A处理失败，那么需要进入回滚流程，如下图所示：\n\n![8476b19e1d5286c8dc60a29d4673b9dc](分布式事务的解决方案.resources/3E572D28-77A9-4AF6-AD05-5703DDA9E6EC.png)\n\n* 若系统A在处理任务A时失败，那么就会向消息中间件发送Rollback请求。和发送Commit请求一样，系统A发完之后便可以认为回滚已经完成，它便可以去做其他的事情。\n* 消息中间件收到回滚请求后，直接将该消息丢弃，而不投递给系统B，从而不会触发系统B的任务B。\n\n\n>此时系统又处于一致性状态，因为任务A和任务B都没有执行。\n\n上面所介绍的Commit和Rollback都属于理想情况，但在实际系统中，Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种情况的时候，消息中间件是如何保证数据一致性呢？——答案就是超时询问机制。\n\n![e71a074ed29bbcef19d8f3598fcb96e8](分布式事务的解决方案.resources/A6256DE9-A14B-4764-BDD2-EA1576BC5A5D.png)\n\n系统A除了实现正常的业务流程外，还需提供一个事务询问的接口，供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时，如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话，就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果：\n\n* 提交若获得的状态是“提交”，则将该消息投递给系统B。\n* 回滚若获得的状态是“回滚”，则直接将条消息丢弃。\n* 处理中若获得的状态是“处理中”，则继续等待。\n\n>消息中间件的超时询问机制能够防止上游系统因在传输过程中丢失Commit/Rollback指令而导致的系统不一致情况，而且能降低上游系统的阻塞时间，上游系统只要发出Commit/Rollback指令后便可以处理其他任务，无需等待确认应答。而Commit/Rollback指令丢失的情况通过超时询问机制来弥补，这样大大降低上游系统的阻塞时间，提升系统的并发度。\n\n下面来说一说消息投递过程的可靠性保证。当上游系统执行完任务并向消息中间件提交了Commit指令后，便可以处理其他任务了，此时它可以认为事务已经完成，接下来消息中间件**一定会保证消息被下游系统成功消费掉！**那么这是怎么做到的呢？这由消息中间件的投递流程来保证。\n\n消息中间件向下游系统投递完消息后便进入阻塞等待状态，下游系统便立即进行任务的处理，任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕！\n\n如果消息在投递过程中丢失，或消息的确认应答在返回途中丢失，那么消息中间件在等待确认应答超时之后就会重新投递，直到下游消费者返回消费成功响应为止。当然，一般消息中间件可以设置消息重试的次数和时间间隔，比如：当第一次投递失败后，每隔五分钟重试一次，一共重试3次。如果重试3次之后仍然投递失败，那么这条消息就需要人工干预。\n\n![8f45a688cf9c624952077873a0386451](分布式事务的解决方案.resources/E91A095E-A1E7-44DF-885B-109BE8EF69D0.png)\n\n![67a6c61ce65f8ba7cd49978a6a704c47](分布式事务的解决方案.resources/B32D1990-55AD-40FE-A288-1106067193DC.png)\n\n\n>有的同学可能要问：消息投递失败后为什么不回滚消息，而是不断尝试重新投递？\n\n这就涉及到整套分布式事务系统的实现成本问题。我们知道，当系统A将向消息中间件发送Commit指令后，它便去做别的事情了。如果此时消息投递失败，需要回滚的话，就需要让系统A事先提供回滚接口，这无疑增加了额外的开发成本，业务系统的复杂度也将提高。对于一个业务系统的设计目标是，在保证性能的前提下，最大限度地降低系统复杂度，从而能够降低系统的运维成本。\n\n>不知大家是否发现，上游系统A向消息中间件提交Commit/Rollback消息采用的是异步方式，也就是当上游系统提交完消息后便可以去做别的事情，接下来提交、回滚就完全交给消息中间件来完成，并且完全信任消息中间件，认为它一定能正确地完成事务的提交或回滚。然而，消息中间件向下游系统投递消息的过程是同步的。也就是消息中间件将消息投递给下游系统后，它会阻塞等待，等下游系统成功处理完任务返回确认应答后才取消阻塞等待。为什么这两者在设计上是不一致的呢？\n\n首先，上游系统和消息中间件之间采用异步通信是为了提高系统并发度。业务系统直接和用户打交道，用户体验尤为重要，因此这种异步通信方式能够极大程度地降低用户等待时间。此外，异步通信相对于同步通信而言，没有了长时间的阻塞等待，因此系统的并发性也大大增加。但异步通信可能会引起Commit/Rollback指令丢失的问题，这就由消息中间件的超时询问机制来弥补。\n\n那么，消息中间件和下游系统之间为什么要采用同步通信呢？\n\n异步能提升系统性能，但随之会增加系统复杂度；而同步虽然降低系统并发度，但实现成本较低。因此，在对并发度要求不是很高的情况下，或者服务器资源较为充裕的情况下，我们可以选择同步来降低系统的复杂度。我们知道，消息中间件是一个独立于业务系统的第三方中间件，它不和任何业务系统产生直接的耦合，它也不和用户产生直接的关联，它一般部署在独立的服务器集群上，具有良好的可扩展性，所以不必太过于担心它的性能，如果处理速度无法满足我们的要求，可以增加机器来解决。而且，即使消息中间件处理速度有一定的延迟那也是可以接受的，因为前面所介绍的BASE理论就告诉我们了，我们追求的是最终一致性，而非实时一致性，因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。\n\n\n## 方案3：最大努力通知（定期校对）也叫本地消息表\n\n最大努力通知也被称为定期校对，其实在方案二中已经包含，这里再单独介绍，主要是为了知识体系的完整性。这种方案也需要消息中间件的参与，其过程如下：\n\n\n![aebb68e6d427c4cb0a92d71fbb3d9c61](分布式事务的解决方案.resources/BE1A020D-7630-47BB-842F-45E9A8145C4E.png)\n\n* 上游系统在完成任务后，向消息中间件同步地发送一条消息，确保消息中间件成功持久化这条消息，然后上游系统可以去做别的事情了；\n* 消息中间件收到消息后负责将该消息同步投递给相应的下游系统，并触发下游系统的任务执行；\n* 当下游系统处理成功后，向消息中间件反馈确认应答，消息中间件便可以将该条消息删除，从而该事务完成。\n\n上面是一个理想化的过程，但在实际场景中，往往会出现如下几种意外情况：\n\n* 消息中间件向下游系统投递消息失败\n* 上游系统向消息中间件发送消息失败\n\n对于第一种情况，消息中间件具有重试机制，我们可以在消息中间件中设置消息的重试次数和重试时间间隔，对于网络不稳定导致的消息投递失败的情况，往往重试几次后消息便可以成功投递，如果超过了重试的上限仍然投递失败，那么消息中间件不再投递该消息，而是记录在失败消息表中，消息中间件需要提供失败消息的查询接口，下游系统会定期查询失败消息，并将其消费，这就是所谓的“定期校对”。\n\n如果重复投递和定期校对都不能解决问题，往往是因为下游系统出现了严重的错误，此时就需要人工干预。\n\n对于第二种情况，需要在上游系统中建立消息重发机制。可以在上游系统建立一张本地消息表，并将 任务处理过程 和 向本地消息表中插入消息 这两个步骤放在一个本地事务中完成。如果向本地消息表插入消息失败，那么就会触发回滚，之前的任务处理结果就会被取消。如果这量步都执行成功，那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息，如果发送失败它会返回重试。当然，也要给消息发送者设置重试的上限，一般而言，达到重试上限仍然发送失败，那就意味着消息中间件出现严重的问题，此时也只有人工干预才能解决问题。\n\n对于不支持事务型消息的消息中间件，如果要实现分布式事务的话，就可以采用这种方式。它能够通过重试机制+定期校对实现分布式事务，但相比于第二种方案，它达到数据一致性的周期较长，而且还需要在上游系统中实现消息重试发布机制，以确保消息成功发布给消息中间件，这无疑增加了业务系统的开发成本，使得业务系统不够纯粹，并且这些额外的业务逻辑无疑会占用业务系统的硬件资源，从而影响性能。\n\n因此，尽量选择支持事务型消息的消息中间件来实现分布式事务，如RocketMQ。\n\n## 方案4：TCC（两阶段型、补偿型）\n\n跨应用的业务操作原子性要求，其实是比较常见的。比如在第三方支付场景中的组合支付，用户在电商网站购物后，要同时使用余额和\n红包支付该笔订单，而余额系统和红包系统分别是不同的应用系统，支付系统在调用这两个系统进行支付时，就需要保证余额扣减和红\n包使用要么同时成功，要么同时失败。\n\nTCC事务的出现正是为了解决应用拆分带来的跨应用业务操作原子性的问题。当然，由于常规的XA事务(2PC，2 Phase Commit, 两阶段提交)\n性能上不尽如人意，也有通过TCC事务来解决数据库拆分的使用场景(如账务拆分)，这个本文后续部分再详述。\n\n故从整个系统架构的角度来看，分布式事务的不同方案是存在层次结构的。\n\n\n#### TCC的机制\n\n明眼一看就知道，TCC应该是三个英文单词的首字母缩写而来。没错，TCC分别对应Try、Confirm和Cancel三种操作，\n这三种操作的业务含义如下：\n\n>Try：预留业务资源\nConfirm：确认执行业务操作\nCancel：取消执行业务操作\n\n稍稍对照下关系型数据库事务的三种操作：DML、Commit和Rollback，会发现和TCC有异曲同工之妙。在一个跨应用的业务操作中，\nTry操作是先把多个应用中的业务资源预留和锁定住，为后续的确认打下基础，类似的，DML操作要锁定数据库记录行，持有数据库资源；\nConfirm操作是在Try操作中涉及的所有应用均成功之后进行确认，使用预留的业务资源，和Commit类似；\n而Cancel则是当Try操作中涉及的所有应用没有全部成功，需要将已成功的应用进行取消(即Rollback回滚)。\n其中Confirm和Cancel操作是一对反向业务操作。\n\n简而言之，TCC是应用层的2PC(2 Phase Commit, 两阶段提交)，如果你将应用看做资源管理器的话。\n详细来说，TCC每项操作需要做的事情如下：\n\n1、Try：尝试执行业务。\n完成所有业务检查(一致性)预留必须业务资源(准隔离性)\n2、Confirm：确认执行业务。\n真正执行业务\n不做任何业务检查\n只使用Try阶段预留的业务资源\n3、Cancel：取消执行业务\n释放Try阶段预留的业务资源\n\n\n一个完整的TCC事务参与方包括三部分：\n\n主业务服务：主业务服务为整个业务活动的发起方，如前面提到的组合支付场景，支付系统即是主业务服务。\n从业务服务：从业务服务负责提供TCC业务操作，是整个业务活动的操作方。从业务服务必须实现Try、Confirm和Cancel三个接口，\n供主业务服务调用。\n由于Confirm和Cancel操作可能被重复调用，故要求Confirm和Cancel两个接口必须是幂等的。前面的组合支付场景中的余额系统和\n红包系统即为从业务服务。\n业务活动管理器：业务活动管理器管理控制整个业务活动，包括记录维护TCC全局事务的事务状态和每个从业务服务的子事务状态，并在业务活动提交时确认所有的TCC型操作的confirm操作，在业务活动取消时调用所有TCC型操作的cancel操作。\n可见整个TCC事务对于主业务服务来说是透明的，其中业务活动管理器和从业务服务各自干了一部分工作。\n\n### TCC的优点和限制\n\nTCC事务的优点如下：\n解决了跨应用业务操作的原子性问题，在诸如组合支付、账务拆分场景非常实用。\nTCC实际上把数据库层的二阶段提交上提到了应用层来实现，对于数据库来说是一阶段提交，规避了数据库层的2PC性能低下问题。\n\nTCC事务的缺点，主要就一个：\nTCC的Try、Confirm和Cancel操作功能需业务提供，开发成本高。\n当然，对TCC事务的这个缺点是否是缺点，是一个见仁见智的事情。\n\n### 一个案例理解\nTCC说实话，TCC的理论有点让人费解。故接下来将以账务拆分为例，对TCC事务的流程做一个描述，希望对理解TCC有所帮助。\n账务拆分的业务场景如下，分别位于三个不同分库的帐户A、B、C，A和B一起向C转帐共80元：分布式事务之说说TCC事务\n\n1、Try：尝试执行业务。\n完成所有业务检查(一致性)：检查A、B、C的帐户状态是否正常，帐户A的余额是否不少于30元，帐户B的余额是否不少于50元。\n预留必须业务资源(准隔离性)：帐户A的冻结金额增加30元，帐户B的冻结金额增加50元，这样就保证不会出现其他并发进程扣减\n了这两个帐户的余额而导致在后续的真正转帐操作过程中，帐户A和B的可用余额不够的情况。\n\n2、Confirm：确认执行业务。\n真正执行业务：如果Try阶段帐户A、B、C状态正常，且帐户A、B余额够用，则执行帐户A给账户C转账30元、帐户B给账户C转账50元的\n转帐操作。\n不做任何业务检查：这时已经不需要做业务检查，Try阶段已经完成了业务检查。\n只使用Try阶段预留的业务资源：只需要使用Try阶段帐户A和帐户B冻结的金额即可。\n\n3、Cancel：取消执行业务\n释放Try阶段预留的业务资源：如果Try阶段部分成功，比如帐户A的余额够用，且冻结相应金额成功，帐户B的余额不够而冻结失败，则需要对帐户A做Cancel操作，将帐户A被冻结的金额解冻掉。"
  },
  {
    "path": "分布式理论/分布式系统理论基础一： 一致性、2PC和3PC.md",
    "content": "## 引言\n\n狭义的分布式系统指由网络连接的计算机系统，每个节点独立地承担计算或存储任务，节点间通过网络协同工作。广义的分布式系统是一个相对的概念，正如Leslie Lamport所说[1]：\nWhat is a distributed systeme. Distribution is in the eye of the beholder.To the user sitting at the keyboard, his IBM personal computer is a nondistributed system. To a flea crawling around on the circuit board, or to the engineer who designed it, it's very much a distributed system.\n \n 一致性是分布式理论中的根本性问题，近半个世纪以来，科学家们围绕着一致性问题提出了很多理论模型，依据这些理论模型，业界也出现了很多工程实践投影。下面我们从一致性问题、特定条件下解决一致性问题的两种方法(2PC、3PC)入门，了解最基础的分布式系统理论。\n \n## 一致性(consensus)\n何为一致性问题？简单而言，一致性问题就是相互独立的节点之间如何达成一项决议的问题。分布式系统中，进行数据库事务提交(commit transaction)、Leader选举、序列号生成等都会遇到一致性问题。这个问题在我们的日常生活中也很常见，比如牌友怎么商定几点在哪打几圈麻将：\n\n![49beb96b5111cd9dcbefaf4ab798d91a](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%B8%80%EF%BC%9A%20%E4%B8%80%E8%87%B4%E6%80%A7%E3%80%812PC%E5%92%8C3PC.resources/5AF77420-6312-4886-854A-D41314926E9A.jpg)\n\n假设一个具有N个节点的分布式系统，当其满足以下条件时，我们说这个系统满足一致性：\n\n* 全认同(agreement): 所有N个节点都认同一个结果\n\n* 值合法(validity): 该结果必须由N个节点中的节点提出\n\n* 可结束(termination): 决议过程在一定时间内结束，不会无休止地进行下去 \n\n有人可能会说，决定什么时候在哪搓搓麻将，4个人商量一下就ok，这不很简单吗？\n \n但就这样看似简单的事情，分布式系统实现起来并不轻松，因为它面临着这些问题：\n\n* 消息传递异步无序(asynchronous): 现实网络不是一个可靠的信道，存在消息延时、丢失，节点间消息传递做不到同步有序(synchronous)\n* 节点宕机(fail-stop): 节点持续宕机，不会恢复\n* 节点宕机恢复(fail-recover): 节点宕机一段时间后恢复，在分布式系统中最常见\n* 网络分化(network partition): 网络链路出现问题，将N个节点隔离成多个部分\n* 拜占庭将军问题(byzantine failure): 节点或宕机或逻辑失败，甚至不按套路出牌抛出干扰决议的信息 \n\n假设现实场景中也存在这样的问题，我们看看结果会怎样：\n\n![186e937c56c7de1117a88f1438099e4f](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%B8%80%EF%BC%9A%20%E4%B8%80%E8%87%B4%E6%80%A7%E3%80%812PC%E5%92%8C3PC.resources/870AB25D-37EE-4196-8CDC-C7613073D873.png)\n\n\n还能不能一起愉快地玩耍...\n\n我们把以上所列的问题称为系统模型(system model)，讨论分布式系统理论和工程实践的时候，必先划定模型。例如有以下两种模型：\n\n异步环境(asynchronous)下，节点宕机(fail-stop)\n异步环境(asynchronous)下，节点宕机恢复(fail-recover)、网络分化(network partition)\n2比1多了节点恢复、网络分化的考量，因而对这两种模型的理论研究和工程解决方案必定是不同的，在还没有明晰所要解决的问题前谈解决方案都是一本正经地耍流氓。\n\n \n\n一致性还具备两个属性，一个是强一致(safety)，它要求所有节点状态一致、共进退；一个是可用(liveness)，它要求分布式系统24x7无间断对外服务。FLP定理(FLP impossibility)已经证明在一个收窄的模型中(异步环境并只存在节点宕机)，不能同时满足 safety 和 liveness。\n\n\nFLP定理是分布式系统理论中的基础理论，正如物理学中的能量守恒定律彻底否定了永动机的存在，FLP定理否定了同时满足safety 和 liveness 的一致性协议的存在。\n\n\n工程实践上根据具体的业务场景，或保证强一致(safety)，或在节点宕机、网络分化的时候保证可用(liveness)。2PC、3PC是相对简单的解决一致性问题的协议，下面我们就来了解2PC和3PC。\n \n### 2PC\n2PC(tow phase commit)两阶段提交[5]顾名思义它分成两个阶段，先由一方进行提议(propose)并收集其他节点的反馈(vote)，再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator)，其他参与决议节点称为参与者(participants, 或cohorts)：\n\n![12556459703348236c4cd428ee27e8d5](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%B8%80%EF%BC%9A%20%E4%B8%80%E8%87%B4%E6%80%A7%E3%80%812PC%E5%92%8C3PC.resources/54705356-87B1-45ED-A7CA-03E10323A7AC.png)\n\n2PC, phase one\n在阶段1中，coordinator发起一个提议，分别问询各participant是否接受。\n\n![7d403dbae287e32fffe6383e225e24fc](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%B8%80%EF%BC%9A%20%E4%B8%80%E8%87%B4%E6%80%A7%E3%80%812PC%E5%92%8C3PC.resources/51DF40A2-A55D-4D75-9534-EEE633BB25CB.png)\n\n2PC, phase two\n在阶段2中，coordinator根据participant的反馈，提交或中止事务，如果participant全部同意则提交，只要有一个participant不同意就中止。\n \n在异步环境(asynchronous)并且没有节点宕机(fail-stop)的模型下，2PC可以满足全认同、值合法、可结束，是解决一致性问题的一种协议。但如果再加上节点宕机(fail-recover)的考虑，2PC是否还能解决一致性问题呢？\n \ncoordinator如果在发起提议后宕机，那么participant将进入阻塞(block)状态、一直等待coordinator回应以完成该次决议。这时需要另一角色把系统从不可结束的状态中带出来，我们把新增的这一角色叫协调者备份(coordinator watchdog)。coordinator宕机一定时间后，watchdog接替原coordinator工作，通过问询(query) 各participant的状态，决定阶段2是提交还是中止。这也要求 coordinator/participant 记录(logging)历史状态，以备coordinator宕机后watchdog对participant查询、coordinator宕机恢复后重新找回状态。\n \n从coordinator接收到一次事务请求、发起提议到事务完成，经过2PC协议后增加了2次RTT(propose+commit)，带来的时延(latency)增加相对较少。\n \n### 3PC\n3PC(three phase commit)即三阶段提交[6][7]，既然2PC可以在异步网络+节点宕机恢复的模型下实现一致性，那还需要3PC做什么，3PC是什么鬼？\n\n \n\n在2PC中一个participant的状态只有它自己和coordinator知晓，假如coordinator提议后自身宕机，在watchdog启用前一个participant又宕机，其他participant就会进入既不能回滚、又不能强制commit的阻塞状态，直到participant宕机恢复。这引出两个疑问：\n\n能不能去掉阻塞，使系统可以在commit/abort前回滚(rollback)到决议发起前的初始状态\n当次决议中，participant间能不能相互知道对方的状态，又或者participant间根本不依赖对方的状态\n\n![f2f443bf6560482a3a4dd3eeb002bedb](https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E4%B8%80%EF%BC%9A%20%E4%B8%80%E8%87%B4%E6%80%A7%E3%80%812PC%E5%92%8C3PC.resources/81875376-AFEA-4A0C-9770-958FEFD27509.png)\n\n图片截取自wikipediacoordinator\n接收完participant的反馈(vote)之后，进入阶段2，给各个participant发送准备提交(prepare to commit)指令。participant接到准备提交指令后可以锁资源，但要求相关操作必须可回滚。coordinator接收完确认(ACK)后进入阶段3、进行commit/abort，3PC的阶段3与2PC的阶段2无异。协调者备份(coordinator watchdog)、状态记录(logging)同样应用在3PC。\n \nparticipant如果在不同阶段宕机，我们来看看3PC如何应对：\n\n* 阶段1: coordinator或watchdog未收到宕机participant的vote，直接中止事务；宕机的participant恢复后，读取logging发现未发出赞成vote，自行中止该次事务\n* 阶段2: coordinator未收到宕机participant的precommit ACK，但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2)，coordinator进行commit；watchdog可以通过问询其他participant获得这些信息，过程同理；宕机的participant恢复后发现收到precommit或已经发出赞成vote，则自行commit该次事务\n* 阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK，也结束该次事务；宕机的participant恢复后发现收到commit或者precommit，也将自行commit该次事务\n\n因为有了准备提交(prepare to commit)阶段，3PC的事务处理延时也增加了1个RTT，变为3个RTT(propose+precommit+commit)，但是它防止participant宕机后整个系统进入阻塞态，增强了系统的可用性，对一些现实业务场景是非常值得的。\n\n \n## 小结\n以上介绍了分布式系统理论中的部分基础知识，阐述了一致性(consensus)的定义和实现一致性所要面临的问题，最后讨论在异步网络(asynchronous)、节点宕机恢复(fail-recover)模型下2PC、3PC怎么解决一致性问题。\n \n阅读前人对分布式系统的各项理论研究，其中有严谨地推理、证明，有一种数学的美；观现实中的分布式系统实现，是综合各种因素下妥协的结果。\n \n## 参考文献\n[1] Solved Problems, Unsolved Problems and Problems in Concurrency, Leslie Lamport, 1983\n[2] The Byzantine Generals Problem, Leslie Lamport,Robert Shostak and Marshall Pease, 1982\n[3] Impossibility of Distributed Consensus with One Faulty Process, Fischer, Lynch and Patterson, 1985\n[4] FLP Impossibility的证明, Daniel Wu, 2015\n[5] Consensus Protocols: Two-Phase Commit, Henry Robinson, 2008\n[6] Consensus Protocols: Three-phase Commit, Henry Robinson, 2008\n[7] Three-phase commit protocol, Wikipedia\n"
  },
  {
    "path": "分布式理论/分布式系统理论基础三-时间、时钟和事件顺序.md",
    "content": "现实生活中时间是很重要的概念，时间可以记录事情发生的时刻、比较事情发生的先后顺序。分布式系统的一些场景也需要记录和比较不同节点间事件发生的顺序，但不同于日常生活使用物理时钟记录时间，分布式系统使用逻辑时钟记录事件顺序关系，下面我们来看分布式系统中几种常见的逻辑时钟。\n\n\n## 物理时钟 vs 逻辑时钟\n\n可能有人会问，为什么分布式系统不使用物理时钟(physical clock)记录事件？每个事件对应打上一个时间戳，当需要比较顺序的时候比较相应时间戳就好了。\n\n这是因为现实生活中物理时间有统一的标准，而分布式系统中每个节点记录的时间并不一样，即使设置了 NTP 时间同步节点间也存在毫秒级别的偏差。因而分布式系统需要有另外的方法记录事件顺序关系，这就是逻辑时钟(logical clock)。\n\n## Lamport timestamps\n\nLeslie Lamport 在1978年提出逻辑时钟的概念，并描述了一种逻辑时钟的表示方法，这个方法被称为Lamport时间戳(Lamport timestamps)。\n\n分布式系统中按是否存在节点交互可分为三类事件，一类发生于节点内部，二是发送事件，三是接收事件。Lamport时间戳原理如下：\n\n![8f34d242db587edc776d863de2565845](分布式系统理论基础三-时间、时钟和事件顺序.resources/BAECD76D-CA03-46A9-851A-C275E3BF840E.png)\n\n图1: Lamport timestamps space time (图片来源: wikipedia)\n\n每个事件对应一个Lamport时间戳，初始值为0\n如果事件在节点内发生，时间戳加1\n如果事件属于发送事件，时间戳加1并在消息中带上该时间戳\n如果事件属于接收事件，时间戳 = Max(本地时间戳，消息中的时间戳) + 1\n\n假设有事件a、b，C(a)、C(b)分别表示事件a、b对应的Lamport时间戳，如果C(a) < C(b)，则有a发生在b之前(happened before)，记作 a -> b，例如图1中有 C1 -> B1。通过该定义，事件集中Lamport时间戳不等的事件可进行比较，我们获得事件的偏序关系(partial order)。\n\n\n如果C(a) = C(b)，那a、b事件的顺序又是怎样的？假设a、b分别在节点P、Q上发生，Pi、Qj分别表示我们给P、Q的编号，如果 C(a) = C(b) 并且 Pi < Qj，同样定义为a发生在b之前，记作 a => b。假如我们对图1的A、B、C分别编号Ai = 1、Bj = 2、Ck = 3，因 C(B4) = C(C3) 并且 Bj < Ck，则 B4 => C3。\n\n\n通过以上定义，我们可以对所有事件排序、获得事件的全序关系(total order)。上图例子，我们可以从C1到A4进行排序。\n\n## Vector clock\n\nLamport时间戳帮助我们得到事件顺序关系，但还有一种顺序关系不能用Lamport时间戳很好地表示出来，那就是同时发生关系(concurrent)。例如图1中事件B4和事件C3没有因果关系，属于同时发生事件，但Lamport时间戳定义两者有先后顺序。\n\n \n\nVector clock是在Lamport时间戳基础上演进的另一种逻辑时钟方法，它通过vector结构不但记录本节点的Lamport时间戳，同时也记录了其他节点的Lamport时间戳。Vector clock的原理与Lamport时间戳类似，使用图例如下：\n\n![2613ec2d7fd0fd6d7d936731306cb432](分布式系统理论基础三-时间、时钟和事件顺序.resources/24D5F5F7-E3C8-47FC-8EAF-BB00C49098C1.png)\n图2: Vector clock space time (图片来源: wikipedia)\n\n假设有事件a、b分别在节点P、Q上发生，Vector clock分别为Ta、Tb，如果 Tb[Q] > Ta[Q] 并且 Tb[P] >= Ta[P]，则a发生于b之前，记作 a -> b。到目前为止还和Lamport时间戳差别不大，那Vector clock怎么判别同时发生关系呢？\n\n\n如果 Tb[Q] > Ta[Q] 并且 Tb[P] < Ta[P]，则认为a、b同时发生，记作 a <-> b。例如图2中节点B上的第4个事件 (A:2，B:4，C:1) 与节点C上的第2个事件 (B:3，C:2) 没有因果关系、属于同时发生事件。\n\n## Version vector\n\n基于Vector clock我们可以获得任意两个事件的顺序关系，结果或为先后顺序或为同时发生，识别事件顺序在工程实践中有很重要的引申应用，最常见的应用是发现数据冲突(detect conflict)。\n\n \n\n分布式系统中数据一般存在多个副本(replication)，多个副本可能被同时更新，这会引起副本间数据不一致，Version vector的实现与Vector clock非常类似[8]，目的用于发现数据冲突。下面通过一个例子说明Version vector的用法：\n\n![e88cec85c18803dbd6f0f18ae72ea721](分布式系统理论基础三-时间、时钟和事件顺序.resources/EAE66F5E-7C9E-4FDC-B6FE-78328F26D3D9.png)\n图3: Version vector\n\n* client端写入数据，该请求被Sx处理并创建相应的vector ([Sx, 1])，记为数据D1\n* 第2次请求也被Sx处理，数据修改为D2，vector修改为([Sx, 2])\n* 第3、第4次请求分别被Sy、Sz处理，client端先读取到D2，然后D3、D4被写入Sy、Sz\n* 第5次更新时client端读取到D2、D3和D4 3个数据版本，通过类似Vector clock判断同时发生关系的方法可判断D3、D4存在数据冲突，最终通过一定方法解决数据冲突并写入D5\n\n Vector clock只用于发现数据冲突，不能解决数据冲突。如何解决数据冲突因场景而异，具体方法有以最后更新为准(last write win)，或将冲突的数据交给client由client端决定如何处理，或通过quorum决议事先避免数据冲突的情况发生。\n\n \n\n由于记录了所有数据在所有节点上的逻辑时钟信息，Vector clock和Version vector在实际应用中可能面临的一个问题是vector过大，用于数据管理的元数据(meta data)甚至大于数据本身。\n\n \n\n解决该问题的方法是使用server id取代client id创建vector (因为server的数量相对client稳定)，或设定最大的size、如果超过该size值则淘汰最旧的vector信息。\n\n小结\n\n以上介绍了分布式系统里逻辑时钟的表示方法，通过Lamport timestamps可以建立事件的全序关系，通过Vector clock可以比较任意两个事件的顺序关系并且能表示无因果关系的事件，将Vector clock的方法用于发现数据版本冲突，于是有了Version vector。\n\n \n\n### 参考资料：\n\n[1] Time is an illusion, George Neville-Neil, 2016\n\n[2] There is No Now, Justin Sheehy, 2015\n\n[3] Time, Clocks, and the Ordering of Events in a Distributed System, Leslie Lamport, 1978\n\n[4] Timestamps in Message-Passing Systems That Preserve the Partial Ordering, Colin J. Fidge, 1988\n\n[5] Virtual Time and Global States of Distributed Systems, Friedemann Mattern, 1988\n\n[6] Why Vector Clocks are Easy, Bryan Fink, 2010\n\n[7] Conflict Management, CouchDB\n\n[8] Version Vectors are not Vector Clocks, Carlos Baquero, 2011\n\n[9] Detection of Mutual Inconsistency in Distributed Systems, IEEE Transactions on Software Engineering , 1983\n\n[10] Dynamo: Amazon’s Highly Available Key-value Store, Amazon, 2007\n\n[11] Conflict Resolution, Jeff Darcy , 2010\n\n[12] Why Vector Clocks Are Hard, Justin Sheehy, 2010\n\n[13] Causality Is Expensive (and What To Do About It), Peter Bailis ,2014\n\n\n"
  },
  {
    "path": "分布式理论/分布式系统理论基础二-CAP.md",
    "content": "## 引言\n\nCAP是分布式系统、特别是分布式存储领域中被讨论最多的理论，“什么是CAP定理？”在Quora 分布式系统分类下排名 FAQ 的 No.1。CAP在程序员中也有较广的普及，它不仅仅是“C、A、P不能同时满足，最多只能3选2”，以下尝试综合各方观点，从发展历史、工程实践等角度讲述CAP理论。希望大家透过本文对CAP理论有更多地了解和认识。\n \n## CAP定理\n\nCAP由Eric Brewer在2000年PODC会议上提出[1][2]，是Eric Brewer在Inktomi[3]期间研发搜索引擎、分布式web缓存时得出的关于数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)的猜想：\nIt is impossible for a web service to provide the three following guarantees : Consistency, Availability and Partition-tolerance.\n \n该猜想在提出两年后被证明成立[4]，成为我们熟知的CAP定理：\n\n* 数据一致性(consistency)：如果系统对一个写操作返回成功，那么之后的读请求都必须读到这个新数据；如果返回失败，那么所有读操作都不能读到这个数据，对调用者而言数据具有强一致性(strong consistency) (又叫原子性 atomic、线性一致性 linearizable consistency)\n* 服务可用性(availability)：所有读写请求在一定时间内得到响应，可终止、不会一直等待\n* 分区容错性(partition-tolerance)：在网络分区的情况下，被分隔的节点仍能正常对外服务 \n\n在某时刻如果满足AP，分隔的节点同时对外服务但不能相互通信，将导致状态不一致，即不能满足C；如果满足CP，网络分区的情况下为达成C，请求只能一直等待，即不满足A；如果要满足CA，在一定时间内要达到节点状态一致，要求不能出现网络分区，则不能满足P。\n \nC、A、P三者最多只能满足其中两个，和FLP定理一样，CAP定理也指示了一个不可达的结果(impossibility result)。\n\n![91ac5357657dbc883f5b40725f7b4140](分布式系统理论基础二-CAP.resources/F2B0C729-3117-4256-BBED-AC3105092464.png)\n\n## CAP的工程启示\nCAP理论提出7、8年后，NoSql圈将CAP理论当作对抗传统关系型数据库的依据、阐明自己放宽对数据一致性(consistency)要求的正确性[6]，随后引起了大范围关于CAP理论的讨论。\n \nCAP理论看似给我们出了一道3选2的选择题，但在工程实践中存在很多现实限制条件，需要我们做更多地考量与权衡，避免进入CAP认识误区[7]。\n \n### 1、关于 P 的理解\nPartition字面意思是网络分区，即因网络因素将系统分隔为多个单独的部分，有人可能会说，网络分区的情况发生概率非常小啊，是不是不用考虑P，保证CA就好。要理解P，我们看回CAP证明中P的定义：\nIn order to model partition tolerance, the network will be allowed to lose arbitrarily many messages sent from one node to another.\n \n网络分区的情况符合该定义，网络丢包的情况也符合以上定义，另外节点宕机，其他节点发往宕机节点的包也将丢失，这种情况同样符合定义。现实情况下我们面对的是一个不可靠的网络、有一定概率宕机的设备，这两个因素都会导致Partition，因而分布式系统实现中 P 是一个必须项，而不是可选项。\n \n对于分布式系统工程实践，CAP理论更合适的描述是：在满足分区容错的前提下，没有算法能同时满足数据一致性和服务可用性[11]：\nIn a network subject to communication failures, it is impossible for any web service to implement an atomic read/write shared memory that guarantees a response to every request.\n \n### 2、CA非0/1的选择\nCAP定理证明中的一致性指强一致性，强一致性要求多节点组成的被调要能像单节点一样运作、操作具备原子性，数据在时间、时序上都有要求。如果放宽这些要求，还有其他一致性类型：\n\n* 序列一致性(sequential consistency)：不要求时序一致，A操作先于B操作，在B操作后如果所有调用端读操作得到A操作的结果，满足序列一致性\n\n* 最终一致性(eventual consistency)：放宽对时间的要求，在被调完成操作响应后的某个时间点，被调多个节点的数据最终达成一致\n\n \n可用性在CAP定理里指所有读写操作必须要能终止，实际应用中从主调、被调两个不同的视角，可用性具有不同的含义。当P(网络分区)出现时，主调可以只支持读操作，通过牺牲部分可用性达成数据一致。\n\n工程实践中，较常见的做法是通过异步拷贝副本(asynchronous replication)、quorum/NRW，实现在调用端看来数据强一致、被调端最终一致，在调用端看来服务可用、被调端允许部分节点不可用(或被网络分隔)的效果。\n \n### 3、跳出CAP\nCAP理论对实现分布式系统具有指导意义，但CAP理论并没有涵盖分布式工程实践中的所有重要因素。\n \n例如延时(latency)，它是衡量系统可用性、与用户体验直接相关的一项重要指标。CAP理论中的可用性要求操作能终止、不无休止地进行，除此之外，我们还关心到底需要多长时间能结束操作，这就是延时，它值得我们设计、实现分布式系统时单列出来考虑。\n \n延时与数据一致性也是一对“冤家”，如果要达到强一致性、多个副本数据一致，必然增加延时。加上延时的考量，我们得到一个CAP理论的修改版本PACELC：如果出现P(网络分区)，如何在A(服务可用性)、C(数据一致性)之间选择；否则，如何在L(延时)、C(数据一致性)之间选择。\n \n小结以上介绍了CAP理论的源起和发展，介绍了CAP理论给分布式系统工程实践带来的启示。\n \nCAP理论对分布式系统实现有非常重大的影响，我们可以根据自身的业务特点，在数据一致性和服务可用性之间作出倾向性地选择。通过放松约束条件，我们可以实现在不同时间点满足CAP(此CAP非CAP定理中的CAP，如C替换为最终一致性)。\n \n有非常非常多文章讨论和研究CAP理论，希望这篇对你认识和了解CAP理论有帮助。\n\n\n \n[1] Harvest, Yield, and Scalable Tolerant Systems, Armando Fox , Eric Brewer, 1999\n\n[2] Towards Robust Distributed Systems, Eric Brewer, 2000\n\n[3] Inktomi's wild ride - A personal view of the Internet bubble, Eric Brewer, 2004\n\n[4] Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web, Seth Gilbert, Nancy Lynch, 2002\n\n[5] Linearizability: A Correctness Condition for Concurrent Objects, Maurice P. Herlihy,Jeannette M. Wing, 1990\n\n[6] Brewer's CAP Theorem - The kool aid Amazon and Ebay have been drinking, Julian Browne, 2009\n\n[7] CAP Theorem between Claims and Misunderstandings: What is to be Sacrificed?, Balla Wade Diack,Samba Ndiaye,Yahya Slimani, 2013\n\n[8] Errors in Database Systems, Eventual Consistency, and the CAP Theorem, Michael Stonebraker, 2010\n\n[9] CAP Confusion: Problems with 'partition tolerance', Henry Robinson, 2010\n\n[10] You Can’t Sacrifice Partition Tolerance, Coda Hale, 2010\n\n[11] Perspectives on the CAP Theorem, Seth Gilbert, Nancy Lynch, 2012\n\n[12] CAP Twelve Years Later: How the \"Rules\" Have Changed, Eric Brewer, 2012\n\n[13] How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs, Lamport Leslie, 1979\n\n[14] Eventual Consistent Databases: State of the Art, Mawahib Elbushra , Jan Lindström, 2014\n\n[15] Eventually Consistent, Werner Vogels, 2008\n\n[16] Speed Matters for Google Web Search, Jake Brutlag, 2009\n\n[17] Consistency Tradeoffs in Modern Distributed Database System Design, Daniel J. Abadi, 2012\n\n[18] A CAP Solution (Proving Brewer Wrong), Guy's blog, 2008\n\n[19] How to beat the CAP theorem, nathanmarz , 2011\n\n[20] The CAP FAQ, Henry Robinson\n"
  },
  {
    "path": "分布式理论/分布式系统理论进阶 - Paxos.md",
    "content": "## 引言\n\n《分布式系统理论基础 - 一致性、2PC和3PC》一文介绍了一致性、达成一致性需要面临的各种问题以及2PC、3PC模型，Paxos协议在节点宕机恢复、消息无序或丢失、网络分化的场景下能保证决议的一致性，是被讨论最广泛的一致性协议。\n\n\n\nPaxos协议同时又以其“艰深晦涩”著称，下面结合 Paxos Made Simple、The Part-Time Parliament 两篇论文，尝试通过Paxos推演、学习和了解Paxos协议。\n\n \n## Basic Paxos\n\n何为一致性问题？简单而言，一致性问题是在节点宕机、消息无序等场景可能出现的情况下，相互独立的节点之间如何达成决议的问题，作为解决一致性问题的协议，Paxos的核心是节点间如何确定并只确定一个值(value)。\n\n\n也许你会疑惑只确定一个值能起什么作用，在Paxos协议里确定并只确定一个值是确定多值的基础，如何确定多值将在第二部分Multi Paxos中介绍，这部分我们聚焦在“Paxos如何确定并只确定一个值”这一问题上。\n\n和2PC类似，Paxos先把节点分成两类，发起提议(proposal)的一方为proposer，参与决议的一方为acceptor。假如只有一个proposer发起提议，并且节点不宕机、消息不丢包，那么acceptor做到以下这点就可以确定一个值：\n```\nP1. 一个acceptor接受它收到的第一项提议\n```\n当然上面要求的前提条件有些严苛，节点不能宕机、消息不能丢包，还只能由一个proposer发起提议。我们尝试放宽条件，假设多个proposer可以同时发起提议，又怎样才能做到确定并只确定一个值呢？\n\n\n首先proposer和acceptor需要满足以下两个条件：\n1. proposer发起的每项提议分别用一个ID标识，提议的组成因此变为(ID, value)\n\n2. acceptor可以接受(accept)不止一项提议，当多数(quorum) acceptor接受一项提议时该提议被确定(chosen)\n\n(注: 注意以上“接受”和“确定”的区别）\n我们约定后面发起的提议的ID比前面提议的ID大，并假设可以有多项提议被确定，为做到确定并只确定一个值acceptor要做到以下这点：\n\n```\nP2. 如果一项值为v的提议被确定，那么后续只确定值为v的提议\n```\n(注: 乍看这个条件不太好理解，谨记目标是“确定并只确定一个值”)\n\n由于一项提议被确定(chosen)前必须先被多数派acceptor接受(accepted)，为实现P2，实质上acceptor需要做到：\n```\nP2a. 如果一项值为v的提议被确定，那么acceptor后续只接受值为v的提议\n```\n满足P2a则P2成立 (P2a => P2)。\n\n \n\n目前在多个proposer可以同时发起提议的情况下，满足P1、P2a即能做到确定并只确定一个值。如果再加上节点宕机恢复、消息丢包的考量呢？\n\n \n\n假设acceptor c 宕机一段时间后恢复，c 宕机期间其他acceptor已经确定了一项值为v的决议但c 因为宕机并不知晓；c 恢复后如果有proposer马上发起一项值不是v的提议，由于条件P1，c 会接受该提议，这与P2a矛盾。为了避免这样的情况出现，进一步地我们对proposer作约束：\n\n\n```\nP2b. 如果一项值为v的提议被确定，那么proposer后续只发起值为v的提议\n```\n满足P2b则P2a成立 (P2b => P2a => P2)。\n\n \n\nP2b约束的是提议被确定(chosen)后proposer的行为，我们更关心提议被确定前proposer应该怎么做：\n```\nP2c. 对于提议(n,v)，acceptor的多数派S中，如果存在acceptor最近一次(即ID值最大)接受的提议的值为v'，那么要求v = v'；否则v可为任意值\n```\n满足P2c则P2b成立 (P2c => P2b => P2a => P2)。\n\n \n\n条件P2c是Basic Paxos的核心，光看P2c的描述可能会觉得一头雾水，我们通过 The Part-Time Parliament 中的例子加深理解：\n\n![97d42599c38593043960af7a662cd4c4](分布式系统理论进阶 - Paxos.resources/AAEEBF57-8486-4EAC-B8F7-C66AE6967BF9.png)\n\n假设有A~E 5个acceptor，- 表示acceptor因宕机等原因缺席当次决议，x 表示acceptor不接受提议，o 表示接受提议；多数派acceptor接受提议后提议被确定，以上表格对应的决议过程如下：\n\n1.ID为2的提议最早提出，根据P2c其提议值可为任意值，这里假设为a\n2.acceptor A/B/C/E 在之前的决议中没有接受(accept)任何提议，因而ID为5的提议的值也可以为任意值，这里假设为b\n3.acceptor B/D/E，其中D曾接受ID为2的提议，根据P2c，该轮ID为14的提议的值必须与ID为2的提议的值相同，为a\n4.acceptor A/C/D，其中D曾接受ID为2的提议、C曾接受ID为5的提议，相比之下ID 5较ID 2大，根据P2c，该轮ID为27的提议的值必须与ID为5的提议的值相同，为b；该轮决议被多数派acceptor接受，因此该轮决议得以确定\n5.acceptor B/C/D，3个acceptor之前都接受过提议，相比之下C、D曾接受的ID 27的ID号最大，该轮ID为29的提议的值必须与ID为27的提议的值相同，为b\n\n以上提到的各项约束条件可以归纳为3点，如果proposer/acceptor满足下面3点，那么在少数节点宕机、网络分化隔离的情况下，在“确定并只确定一个值”这件事情上可以保证一致性(consistency)：\nB1(ß): ß中每一轮决议都有唯一的ID标识\nB2(ß): 如果决议B被acceptor多数派接受，则确定决议B\nB3(ß): 对于ß中的任意提议B(n,v)，acceptor的多数派中如果存在acceptor最近一次(即ID值最大)接受的提议的值为v'，那么要求v = v'；否则v可为任意值\n\n(注: 希腊字母ß表示多轮决议的集合，字母B表示一轮决议)\n\n另外为保证P2c，我们对acceptor作两个要求：\n\n1. 记录曾接受的ID最大的提议，因proposer需要问询该信息以决定提议值\n\n2. 在回应提议ID为n的proposer自己曾接受过ID最大的提议时，acceptor同时保证(promise)不再接受ID小于n的提议\n\n \n\n至此，proposer/acceptor完成一轮决议可归纳为prepare和accept两个阶段。prepare阶段proposer发起提议问询提议值、acceptor回应问询并进行promise；accept阶段完成决议，图示如下：\n\n![9000315110586d6ebbdf455c7a03d3e0](分布式系统理论进阶 - Paxos.resources/CF8854FA-A0E7-4FEF-A9F2-ABC66CA49297.png)\n\n还有一个问题需要考量，假如proposer A发起ID为n的提议，在提议未完成前proposer B又发起ID为n+1的提议，在n+1提议未完成前proposer C又发起ID为n+2的提议…… 如此acceptor不能完成决议、形成活锁(livelock)，虽然这不影响一致性，但我们一般不想让这样的情况发生。解决的方法是从proposer中选出一个leader，提议统一由leader发起。\n\n \n\n最后我们再引入一个新的角色：learner，learner依附于acceptor，用于习得已确定的决议。以上决议过程都只要求acceptor多数派参与，而我们希望尽量所有acceptor的状态一致。如果部分acceptor因宕机等原因未知晓已确定决议，宕机恢复后可经本机learner采用pull的方式从其他acceptor习得。\n\n\n## Multi Paxos\n\n通过以上步骤分布式系统已经能确定一个值，“只确定一个值有什么用？这可解决不了我面临的问题。” 你心中可能有这样的疑问。\n \n其实不断地进行“确定一个值”的过程、再为每个过程编上序号，就能得到具有全序关系(total order)的系列值，进而能应用在数据库副本存储等很多场景。我们把单次“确定一个值”的过程称为实例(instance)，它由proposer/acceptor/learner组成，下图说明了A/B/C三机上的实例：\n\n![1824716a2ad8ce8300c01f2a876e63fb](分布式系统理论进阶 - Paxos.resources/225D3AF6-7029-4731-A45A-2A66050338BB.png)\n\n不同序号的实例之间互相不影响，A/B/C三机输入相同、过程实质等同于执行相同序列的状态机(state machine)指令 ，因而将得到一致的结果。\n\n\nproposer leader在Multi Paxos中还有助于提升性能，常态下统一由leader发起提议，可节省prepare步骤(leader不用问询acceptor曾接受过的ID最大的提议、只有leader提议也不需要acceptor进行promise)直至发生leader宕机、重新选主。\n\n## 小结\n\n以上介绍了Paxos的推演过程、如何在Basic Paxos的基础上通过状态机构建Multi Paxos。Paxos协议比较“艰深晦涩”，但多读几遍论文一般能理解其内涵，更难的是如何将Paxos真正应用到工程实践。\n\n \n\n微信后台开发同学实现并开源了一套基于Paxos协议的多机状态拷贝类库PhxPaxos，PhxPaxos用于将单机服务扩展到多机，其经过线上系统验证并在一致性保证、性能等方面作了很多考量。"
  },
  {
    "path": "分布式理论/分布式系统理论进阶 - Raft、Zab.md",
    "content": "## 引言\n\n《分布式系统理论进阶 - Paxos》介绍了一致性协议Paxos，今天我们来学习另外两个常见的一致性协议——Raft和Zab。通过与Paxos对比，了解Raft和Zab的核心思想、加深对一致性协议的认识。\n\n## Raft\n\nPaxos偏向于理论、对如何应用到工程实践提及较少。理解的难度加上现实的骨感，在生产环境中基于Paxos实现一个正确的分布式系统非常难[1]：\n\n>There are significant gaps between the description of the Paxos algorithm and the needs of a real-world system. In order to build a real-world system, an expert needs to use numerous ideas scattered in the literature and make several relatively small protocol extensions. The cumulative effort will be substantial and the final system will be based on an unproven protocol.\n\nRaft[2][3]在2013年提出，提出的时间虽然不长，但已经有很多系统基于Raft实现。相比Paxos，Raft的买点就是更利于理解、更易于实行。\n\n \n \n\n为达到更容易理解和实行的目的，Raft将问题分解和具体化：Leader统一处理变更操作请求，一致性协议的作用具化为保证节点间操作日志副本(log replication)一致，以term作为逻辑时钟(logical clock)保证时序，节点运行相同状态机(state machine)[4]得到一致结果。Raft协议具体过程如下：\n\n![c91c3cc5d7bfefe7b23c4df34a151e74](分布式系统理论进阶 - Raft、Zab.resources/FEA133A1-AA08-40B3-88E7-365EF36E0C0C.png)\n\n* Client发起请求，每一条请求包含操作指令\n* 请求交由Leader处理，Leader将操作指令(entry)追加(append)至操作日志，紧接着对Follower发起AppendEntries请求、尝试让操作日志副本在Follower落地\n* 如果Follower多数派(quorum)同意AppendEntries请求，Leader进行commit操作、把指令交由状态机处理\n* 状态机处理完成后将结果返回给Client\n\n指令通过log index(指令id)和term number保证时序，正常情况下Leader、Follower状态机按相同顺序执行指令，得出相同结果、状态一致。\n\n \n\n宕机、网络分化等情况可引起Leader重新选举(每次选举产生新Leader的同时，产生新的term)、Leader/Follower间状态不一致。Raft中Leader为自己和所有Follower各维护一个nextIndex值，其表示Leader紧接下来要处理的指令id以及将要发给Follower的指令id，LnextIndex不等于FnextIndex时代表Leader操作日志和Follower操作日志存在不一致，这时将从Follower操作日志中最初不一致的地方开始，由Leader操作日志覆盖Follower，直到LnextIndex、FnextIndex相等。\n\n \n\nPaxos中Leader的存在是为了提升决议效率，Leader的有无和数目并不影响决议一致性，Raft要求具备唯一Leader，并把一致性问题具体化为保持日志副本的一致性，以此实现相较Paxos而言更容易理解、更容易实现的目标。\n\n## Zab\n\nZab[5][6]的全称是Zookeeper atomic broadcast protocol，是Zookeeper内部用到的一致性协议。相比Paxos，Zab最大的特点是保证强一致性(strong consistency，或叫线性一致性linearizable consistency)。\n\n \n\n和Raft一样，Zab要求唯一Leader参与决议，Zab可以分解成discovery、sync、broadcast三个阶段：\n\n![e8f0afe96afec603ec0c9ec8fb33936d](分布式系统理论进阶 - Raft、Zab.resources/65E43779-4B2D-4D8C-93F1-D8161F8FD746.jpg)\n\n* discovery: 选举产生PL(prospective leader)，PL收集Follower epoch(cepoch)，根据Follower的反馈PL产生newepoch(每次选举产生新Leader的同时产生新epoch，类似Raft的term)\n* sync: PL补齐相比Follower多数派缺失的状态、之后各Follower再补齐相比PL缺失的状态，PL和Follower完成状态同步后PL变为正式Leader(established leader)\n* broadcast: Leader处理Client的写操作，并将状态变更广播至Follower，Follower多数派通过之后Leader发起将状态变更落地(deliver/commit)\n\nLeader和Follower之间通过心跳判别健康状态，正常情况下Zab处在broadcast阶段，出现Leader宕机、网络隔离等异常情况时Zab重新回到discovery阶段。\n\n \n\n了解完Zab的基本原理，我们再来看Zab怎样保证强一致性，Zab通过约束事务先后顺序达到强一致性，先广播的事务先commit、FIFO，Zab称之为primary order(以下简称PO)。实现PO的核心是zxid。\n\n \n\nZab中每个事务对应一个zxid，它由两部分组成：<e, c>，e即Leader选举时生成的epoch，c表示当次epoch内事务的编号、依次递增。假设有两个事务的zxid分别是z、z'，当满足 z.e < z'.e 或者 z.e = z'.e && z.c < z'.c 时，定义z先于z'发生(z < z')。\n\n \n\n为实现PO，Zab对Follower、Leader有以下约束：\n\n* 有事务z和z'，如果Leader先广播z，则Follower需保证先commit z对应的事务\n* 有事务z和z'，z由Leader p广播，z'由Leader q广播，Leader p先于Leader q，则Follower需保证先commit z对应的事务\n* 有事务z和z'，z由Leader p广播，z'由Leader q广播，Leader p先于Leader q，如果Follower已经commit z，则q需保证已commit z才能广播z'\n* 第1、2点保证事务FIFO，第3点保证Leader上具备所有已commit的事务。\n\n \n\n相比Paxos，Zab约束了事务顺序、适用于有强一致性需求的场景。\n\n## Paxos、Raft、Zab再比较\n\n除Paxos、Raft和Zab外，Viewstamped Replication(简称VR)[7/8]也是讨论比较多的一致性协议。这些协议包含很多共同的内容(Leader、quorum、state machine等)，因而我们不禁要问：Paxos、Raft、Zab和VR等分布式一致性协议区别到底在哪，还是根本就是一回事？[9]\n\n \n\nPaxos、Raft、Zab和VR都是解决一致性问题的协议，Paxos协议原文倾向于理论，Raft、Zab、VR倾向于实践，一致性保证程度等的不同也导致这些协议间存在差异。下图帮助我们理解这些协议的相似点和区别[10]：\n\n![c1cfddbcac1c7b471bf329d6e94eaecb](分布式系统理论进阶 - Raft、Zab.resources/C9F431A6-CA52-4A5A-8EFC-1225C4E96FDB.jpg)\n相比Raft、Zab、VR，Paxos更纯粹、更接近一致性问题本源，尽管Paxos倾向理论，但不代表Paxos不能应用于工程。基于Paxos的工程实践，须考虑具体需求场景(如一致性要达到什么程度)，再在Paxos原始语意上进行包装。\n\n\n## 小结\n\n以上介绍分布式一致性协议Raft、Zab的核心思想，分析Raft、Zab与Paxos的异同。实现分布式系统时，先从具体需求和场景考虑，Raft、Zab、VR、Paxos等协议没有绝对地好与不好，只是适不适合。\n\n\n### 引用列表\n\n[1] Paxos made live - An engineering perspective, Tushar Chandra, Robert Griesemer and Joshua Redstone, 2007\n\n[2] In Search of an Understandable Consensus Algorithm, Diego Ongaro and John Ousterhout, 2013\n\n[3] In Search of an Understandable Consensus Algorithm (Extended Version), Diego Ongaro and John Ousterhout, 2013\n\n[4] Implementing Fault-Tolerant Services Using the State Machine, Fred B. Schneider, 1990\n\n[5] Zab:High-performance broadcast for primary-backup systems, FlavioP.Junqueira,BenjaminC.Reed,andMarcoSeraﬁni, 2011\n\n[6] ZooKeeper's atomic broadcast protocol: Theory and practice, Andr´e Medeiros, 2012\n\n[7] Viewstamped Replication A New Primary Copy Method to Support Highly-Available Distributed Systems, Brian M.Oki and Barbar H.Liskov, 1988\n\n[8] Viewstamped Replication Revisited, Barbara Liskov and James Cowling, Barbara Liskov and James Cowling ,2012\n\n[9] Can’t we all just agree? The morning paper, 2015\n\n[10] Vive La Difference: Paxos vs. Viewstamped Replication vs. Zab, Robbert van Renesse, Nicolas Schiper and Fred B. Schneider, 2014"
  },
  {
    "path": "分布式理论/分布式系统理论进阶：选举、多数派和租约.md",
    "content": "选举(election)是分布式系统实践中常见的问题，通过打破节点间的对等关系，选得的leader(或叫master、coordinator)有助于实现事务原子性、提升决议效率。 多数派(quorum)的思路帮助我们在网络分化的情况下达成决议一致性，在leader选举的场景下帮助我们选出唯一leader。租约(lease)在一定期限内给予节点特定权利，也可以用于实现leader选举。\n\n\n\n下面我们就来学习分布式系统理论中的选举、多数派和租约。\n\n\n## 选举(election)\n\n一致性问题(consistency)是独立的节点间如何达成决议的问题，选出大家都认可的leader本质上也是一致性问题，因而如何应对宕机恢复、网络分化等在leader选举中也需要考量。\n\n \n\nBully算法[1]是最常见的选举算法，其要求每个节点对应一个序号，序号最高的节点为leader。leader宕机后次高序号的节点被重选为leader，过程如下：\n\n![dc23211dec7f58c211dc5c0cbab9c711](分布式系统理论进阶：选举、多数派和租约.resources/99A8C7D5-3E3D-451F-9C54-36F45C39056A.png)\n\n(a). 节点4发现leader不可达，向序号比自己高的节点发起重新选举，重新选举消息中带上自己的序号\n\n(b)(c). 节点5、6接收到重选信息后进行序号比较，发现自身的序号更大，向节点4返回OK消息并各自向更高序号节点发起重新选举\n\n(d). 节点5收到节点6的OK消息，而节点6经过超时时间后收不到更高序号节点的OK消息，则认为自己是leader\n\n(e). 节点6把自己成为leader的信息广播到所有节点\n\nBully算法中有2PC的身影，都具有提议(propose)和收集反馈(vote)的过程。\n\n \n\n在一致性算法Paxos、ZAB[2]、Raft[3]中，为提升决议效率均有节点充当leader的角色。ZAB、Raft中描述了具体的leader选举实现，与Bully算法类似ZAB中使用zxid标识节点，具有最大zxid的节点表示其所具备的事务(transaction)最新、被选为leader。\n\n## 多数派(quorum)\n\n在网络分化的场景下以上Bully算法会遇到一个问题，被分隔的节点都认为自己具有最大的序号、将产生多个leader，这时候就需要引入多数派(quorum)[4]。多数派的思路在分布式系统中很常见，其确保网络分化情况下决议唯一。\n\n \n\n多数派的原理说起来很简单，假如节点总数为2f+1，则一项决议得到多于 f 节点赞成则获得通过。leader选举中，网络分化场景下只有具备多数派节点的部分才可能选出leader，这避免了多leader的产生。\n\n多数派的思路还被应用于副本(replica)管理，根据业务实际读写比例调整写副本数Vw、读副本数Vr，用以在可靠性和性能方面取得平衡[5]。\n\n## 租约(lease)\n\n选举中很重要的一个问题，以上尚未提到：怎么判断leader不可用、什么时候应该发起重新选举？最先可能想到会通过心跳(heart beat)判别leader状态是否正常，但在网络拥塞或瞬断的情况下，这容易导致出现双主。\n\n \n\n租约(lease)是解决该问题的常用方法，其最初提出时用于解决分布式缓存一致性问题[6]，后面在分布式锁[7]等很多方面都有应用。\n\n![67ab132822e5429beb164ecbe017cc2c](分布式系统理论进阶：选举、多数派和租约.resources/46CB2C03-B211-45DD-B1D6-4A37477676C2.png)\n\n租约的原理同样不复杂，中心思想是每次租约时长内只有一个节点获得租约、到期后必须重新颁发租约。假设我们有租约颁发节点Z，节点0、1和2竞选leader，租约过程如下：\n\n(a). 节点0、1、2在Z上注册自己，Z根据一定的规则(例如先到先得)颁发租约给节点，该租约同时对应一个有效时长；这里假设节点0获得租约、成为leader\n\n(b). leader宕机时，只有租约到期(timeout)后才重新发起选举，这里节点1获得租约、成为leader\n\n \n\n租约机制确保了一个时刻最多只有一个leader，避免只使用心跳机制产生双主的问题。在实践应用中，zookeeper、ectd可用于租约颁发。\n\n## 小结\n\n在分布式系统理论和实践中，常见leader、quorum和lease的身影。分布式系统内不一定事事协商、事事民主，leader的存在有助于提升决议效率。\n\n本文以leader选举作为例子引入和讲述quorum、lease，当然quorum和lease是两种思想，并不限于leader选举应用。\n\n \n\n最后提一个有趣的问题与大家思考，leader选举的本质是一致性问题，Paxos、Raft和ZAB等解决一致性问题的协议和算法本身又需要或依赖于leader，怎么理解这个看似“蛋生鸡、鸡生蛋”的问题？[8]\n\n### 引用列表\n[1] Elections in a Distributed Computing System, Hector Garcia-Molina, 1982\n\n[2] ZooKeeper’s atomic broadcast protocol: Theory and practice, Andre Medeiros, 2012\n\n[3] In Search of an Understandable Consensus Algorithm, Diego Ongaro and John Ousterhout, 2013\n\n[4] A quorum-based commit protocol, Dale Skeen, 1982\n\n[5] Weighted Voting for Replicated Data, David K. Gifford, 1979\n\n[6] Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency, Cary G. Gray and David R. Cheriton, 1989\n\n[7] The Chubby lock service for loosely-coupled distributed systems, Mike Burrows, 2006\n\n[8] Why is Paxos leader election not done using Paxos?\n\n"
  },
  {
    "path": "分布式理论/分布式系统的一些基本概念.md",
    "content": "## 分布式\n**来自csdn,作者：陆小凤\n进阶篇来自：bangerlee\n作者对部分地方做了订正\n目前这系列文章是网络上分布式系统讲的最全最深入的系列，文中参考了大量国外英文文献。**\n\n小明的公司又3个系统：系统A，系统B和系统C，这三个系统所做的业务不同，被部署在3个独立的机器上运行，他们之间互相调用（当然是跨域网络的），通力合作完成公司的业务流程。\n![7bcd42020d3228116272009bc3ae37ea](分布式系统的一些基本概念.resources/4BCF7C3F-89B7-4428-B135-EC9F0F47AA8E.png)\n将不同的业务分部在不同的地方，就构成了一个分布式的系统，现在问题来了，系统A是整个分布式系统的脸面，用户直接访问，用户访问量大的时候要么是速度巨慢，要么直接挂掉，怎么办？\n\n由于系统A只有一份，所以会引起单点失败。。。\n\n\n\n## 集群（Cluster）\n\n小明的公司不差钱，就多买几台机器吧， 小明把系统A一下子部署了好几份（例如下图的3个服务器），每一份都是系统A的一个实例，对外提供同样的服务，这样，就不怕其中一个坏掉了，还有另外两个呢。\n\n这三个服务器的系统就组成了一个集群。\n![ed9d7109b55571b147e625b451313ce5](分布式系统的一些基本概念.resources/327ACC68-4D3E-4102-AFFA-7CC0A3A49803.png)\n\n可是对用户来说，一下子出现这么多系统A，每个系统的IP地址都不一样，到底访问哪一个呢？\n\n如果所有人都访问服务器1.1，那服务器1.1会被累死，剩下两个闲死，成了浪费钱的摆设\n\n \n\n## 负载均衡（Load Balancer）\n\n小明要尽可能的让3个机器上的系统A工作均衡一些，比如有3万个请求，那就让3个服务器各处理1万个（理想情况），这叫负载均衡\n\n很明显，这个负载均衡的工作最好独立出来，放到独立的服务器上（例如nginx）：\n![1722ff23812043033c6607829c05d282](分布式系统的一些基本概念.resources/6947A396-4ABB-4C41-A091-9BAF35FB1787.png)\n后来小明发现，这个负载均衡的服务器虽然工作内容简单，就是拿到请求，分发请求，但是它还是有可能挂掉，单点失败还是会出现。\n\n没办法，只好把负载均衡也搞成一个集群，but和系统A的集群有两点不同：\n\n1.这个新的集群中虽然有两个机器，但是我们可以用某种办法，让这个机器对外只提供一个IP地址，也就是用户看到的好像只有一个机器。\n\n2.同一时刻，我们只让一个负载均衡的机器工作，另外一个原地待命，如果工作的那个挂掉了，待命的那个就顶上去。\n![9156c4d1acbe4390ebf1989a2847158e](分布式系统的一些基本概念.resources/582A307F-D858-4E36-9DD6-6A08D1E42CEE.png)\n\n4、弹性\n\n如果3个系统A的实例还是满足不了大量请求，例如双十一，可以申请增加服务器，双十一过后，新增的服务器闲置，成了摆设，于是小明决定尝试云计算，在云端可以轻松的创建，删除虚拟的服务器，那样就可以轻松的随着用户的请求动图的增减服务器了。\n\n \n\n 5、失效转移\n\n上面的系统看起来很美好，但是做了一个不切实际的假设：\n\n所有的服务都是无状态的，换句话说，假设用户的两次请求直接是没有关联的。\n\n但是现实是，大部分服务都是有状态的，例如购物车。\n\n\n用户访问系统，在服务器上创建了一个购物车，并向其中加了几个商品，然后服务器1.1挂掉了，用户后续访问就找不到服务器1.1了，这时候就要做失效转移，让另外几个服务器去接管，去处理用户的请求。\n\n可是问题来了，在服务器1.2,1.3上有用户的购物车吗？如果没有，用户就会抱怨，我刚创建的购物车哪里去了？\n\n还有更严重的，假设用户登录过得信息保存到了该服务器1.1上登录的，用户登录过的信息保存到了该服务器的session中，现在这个服务器挂了，用的session就不见了，会把用户踢到了登录界面，让用户再次登录！\n\n \n\n处理不好状态的问题，集群的威力就大打折扣，无法完成真正的失效转移，甚至无法使用。\n\n \n\n怎么办？\n\n一种办法是把状态信息在集群的各个服务器之间复制，让集群的各个服务器达成一致，谁来干这个事情？只能像Webspher，Weblogic这样的应用服务器了。\n\n \n\n还有一种办法， 就是把状态信息几种存储在一个地方，让集群服务器的各个服务器都能访问到：\n![a702f2d55d10ea29fad1b86a8814d0d5](分布式系统的一些基本概念.resources/70D64F1D-098D-42D8-8EEF-23C819E61FE4.png)\n\n小明听说Redis不错，那就用Redis来保存吧！\n\n\n## 认识分布式架构\n随着计算机系统规模变得越来越大，将所有的业务单元集中部署在一个或若干个大型机上的体系结构，已经越来越不能满足当今计算机系统，尤其是大型互联网系统的快速发展，各种灵活多变的系统架构模型层出不穷。分布式的处理方式越来越受到业界的青睐——计算机系统正在经历一场前所未有的从集中式向分布式架构的变革。\n\n### 集中式与分布式\n**集中式系统**\n\n所谓的集中式系统就是指由一台或多台主计算机组成中心节点，数据集中存储于这个中心节点中，并且整个系统的所有业务单元都集中部署在这个中心节点上，系统的所有功能均由其集中处理。\n\n集中式系统的最大的特点就是部署结构非常简单，底层一般采用从IBM、HP等厂商购买到的昂贵的大型主机。因此无需考虑如何对服务进行多节点的部署，也就不用考虑各节点之间的分布式协作问题。但是，由于采用单机部署，很可能带来系统大而复杂、难于维护、发生单点故障（单个点发生故障的时候会波及到整个系统或者网络，从而导致整个系统或者网络的瘫痪）、扩展性差等问题。\n\n**分布式系统**\n\n分布式系统是一个硬件或软件组件分布在不同的网络计算机上，彼此之间仅仅通过消息传递进行通信和协调的系统。简单来说就是一群独立计算机集合共同对外提供服务，但是对于系统的用户来说，就像是一台计算机在提供服务一样。分布式意味着可以采用更多的普通计算机（相对于昂贵的大型机）组成分布式集群对外提供服务。计算机越多，CPU、内存、存储资源等也就越多，能够处理的并发访问量也就越大。\n\n从分布式系统的概念中我们知道，各个主机之间通信和协调主要通过网络进行，所以分布式系统中的计算机在空间上几乎没有任何限制，这些计算机可能被放在不同的机柜上，也可能被部署在不同的机房中，还可能在不同的城市中，对于大型的网站甚至可能分布在不同的国家和地区。但是，无论空间上如何分布，一个标准的分布式系统应该具有以下几个主要特征：\n\n* 分布性\n\n分布式系统中的多台计算机之间在空间位置上可以随意分布，同时，机器的分布情况也会随时变动。\n\n* 对等性\n\n分布式系统中的计算机没有主／从之分，即没有控制整个系统的主机，也没有被控制的从机，组成分布式系统的所有计算机节点都是对等的。副本（Replica）是分布式系统最常见的概念之一，指的是分布式系统对数据和服务提供的一种冗余方式。在常见的分布式系统中，为了对外提供高可用的服务，我们往往会对数据和服务进行副本处理。数据副本是指在不同节点上持久化同一份数据，当某一个节点上存储的数据丢失时，可以从副本上读取该数据，这是解决分布式系统数据丢失问题最为有效的手段。另一类副本是服务副本，指多个节点提供同样的服务，每个节点都有能力接收来自外部的请求并进行相应的处理。\n\n* 并发性\n\n在一个计算机网络中，程序运行过程的并发性操作是非常常见的行为。例如同一个分布式系统中的多个节点，可能会并发地操作一些共享的资源，如何准确并高效地协调分布式并发操作也成为了分布式系统架构与设计中最大的挑战之一。\n\n* 缺乏全局时钟\n\n在分布式系统中，很难定义两个事件究竟谁先谁后，原因就是因为分布式系统缺乏一个全局的时钟序列控制。\n\n* 故障总是会发生\n\n组成分布式系统的所有计算机，都有可能发生任何形式的故障。除非需求指标允许，在系统设计时不能放过任何异常情况。\n\n## 分布式系统面临的问题\n\n* 通信异常\n\n分布式系统需要在各个节点之间进行网络通信，因此网络通信都会伴随着网络不可用的风险或是系统不可用都会导致最终分布式系统无法顺利完成一次网络通信。另外，即使分布式系统各节点之间的网络通信能够正常进行，其延时也会远大于单机操作，会影响消息的收发的过程，因此消息丢失和消息延迟变得非常普遍。\n\n* 网络分区\n\n当网络由于发生异常情况，导致分布式系统中部分节点之间的网络延时不断增大，最终导致组成分布式系统的所有节点中，只有部分节点之间能够进行正常通信，而另一些节点则不能——我们将这个现象称为网络分区，就是俗称的“脑裂”。当网络分区出现时，分布式系统会出现局部小集群，在极端情况下，这些局部小集群会独立完成原本需要整个分布式才能完成的功能，这就对分布式一致性提出类非常大的挑战。\n\n* 三态\n\n分布式系统的每一次请求与响应，存在特有的“三态”概念，即成功、失败与超时。当出现超时现象时，网络通信的发起方是无法确定当前请求是否被成功处理的。\n\n* 节点故障\n\n节点故障则是分布式环境下另一个比较常见的问题，指的是组成分布式系统的服务器节点出现的宕机或“僵死”现象。\n\n## 分布式理论(一) - CAP定理\n前言\nCAP原则又称CAP定理，指的是在一个分布式系统中，Consistency（一致性）、 Availability（可用性）、Partition tolerance（分区容错性）这三个基本需求，最多只能同时满足其中的2个。\n\n### 初探分布式理论\n\n**1. CAP原则简介**\n选项\t描述\nConsistency（一致性）\t指数据在多个副本之间能够保持一致的特性（严格的一致性）\nAvailability（可用性）\t指系统提供的服务必须一直处于可用的状态，每次请求都能获取到非错的响应（不保证获取的数据为最新数据）\nPartition tolerance（分区容错性）\t分布式系统在遇到任何网络分区故障的时候，仍然能够对外提供满足一致性和可用性的服务，除非整个网络环境都发生了故障\n什么是分区？\n\n在分布式系统中，不同的节点分布在不同的子网络中，由于一些特殊的原因，这些子节点之间出现了网络不通的状态，但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域，这就是分区。\n\n**2. CAP原则论证**\n如图所示，是我们证明CAP的基本场景，网络中有两个节点N1和N2，可以简单的理解N1和N2分别是两台计算机，他们之间网络可以连通，N1中有一个应用程序A，和一个数据库V，N2也有一个应用程序B和一个数据库V。现在，A和B是分布式系统的两个部分，V是分布式系统的数据存储的两个子数据库。\n\n* 在满足一致性的时候，N1和N2中的数据是一样的，V0=V0。\n\n* 在满足可用性的时候，用户不管是请求N1或者N2，都会得到立即响应。\n\n* 在满足分区容错性的情况下，N1和N2有任何一方宕机，或者网络不通的时候，都不会影响N1和N2彼此之间的正常运作。\n\n如图所示，这是分布式系统正常运转的流程，用户向N1机器请求数据更新，程序A更新数据库V0为V1。分布式系统将数据进行同步操作M，将V1同步的N2中V0，使得N2中的数据V0也更新为V1，N2中的数据再响应N2的请求。\n![93c3c7f6706199eafd59c70ed75e6d52](分布式系统的一些基本概念.resources/51745943-3CE1-44CF-8395-AFF754BC8DA0.png)\n\n\n根据CAP原则定义，系统的一致性、可用性和分区容错性细分如下：\n\n一致性：N1和N2的数据库V之间的数据是否完全一样。\n可用性：N1和N2的对外部的请求能否做出正常的响应。\n分区容错性：N1和N2之间的网络是否互通。\n\n这是正常运作的场景，也是理想的场景。作为一个分布式系统，它和单机系统的最大区别，就在于网络。现在假设一种极端情况，N1和N2之间的网络断开了，我们要支持这种网络异常。相当于要满足分区容错性，能不能同时满足一致性和可用性呢？还是说要对他们进行取舍？\n![a66b61aa0e685a37074957247fb1b585](分布式系统的一些基本概念.resources/2087D2D6-41DF-4EE9-A6F5-98345BFB212E.png)\n\n假设在N1和N2之间网络断开的时候，有用户向N1发送数据更新请求，那N1中的数据V0将被更新为V1。由于网络是断开的，所以分布式系统同步操作M，所以N2中的数据依旧是V0。这个时候，有用户向N2发送数据读取请求，由于数据还没有进行同步，应用程序没办法立即给用户返回最新的数据V1，怎么办呢？\n\n这里有两种选择：\n\n第一：牺牲数据一致性，保证可用性。响应旧的数据V0给用户。\n第二：牺牲可用性，保证数据一致性。阻塞等待，直到网络连接恢复，数据更新操作M完成之后，再给用户响应最新的数据V1。\n这个过程，证明了要满足分区容错性的分布式系统，只能在一致性和可用性两者中，选择其中一个。\n\n3. CAP原则权衡\n通过CAP理论，我们知道无法同时满足一致性、可用性和分区容错性这三个特性，那要舍弃哪个呢？\n\n3.1. CA without P\n如果不要求P（不允许分区），则C（强一致性）和A（可用性）是可以保证的。但其实分区不是你想不想的问题，而是始终会存在，因此CA的系统更多的是允许分区后各子系统依然保持CA。\n\n3.2. CP without A\n如果不要求A（可用），相当于每个请求都需要在Server之间强一致，而P（分区）会导致同步时间无限延长，如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。\n\n3.3. AP wihtout C\n要高可用并允许分区，则需放弃一致性。一旦分区发生，节点之间可能会失去联系，为了高可用，每个节点只能用本地数据提供服务，而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。\n\n## 小结\n对于多数大型互联网应用的场景，主机众多、部署分散。而且现在的集群规模越来越大，所以节点故障、网络故障是常态。这种应用一般要保证服务可用性达到N个9，即保证P和A，只有舍弃C（退而求其次保证最终一致性）。虽然某些地方会影响客户体验，但没达到造成用户流程的严重程度。\n\n对于涉及到钱财这样不能有一丝让步的场景，C必须保证。网络发生故障宁可停止服务，这是保证CA，舍弃P。貌似这几年国内银行业发生了不下10起事故，但影响面不大，报到也不多，广大群众知道的少。还有一种是保证CP，舍弃A，例如网络故障时只读不写。\n\n孰优孰劣，没有定论，只能根据场景定夺，适合的才是最好的。\n\n## 分布式理论(二) - BASE理论\n\n### 前言\nBASE理论是由eBay架构师提出的。BASE是对CAP中一致性和可用性权衡的结果，其来源于对大规模互联网分布式系统实践的总结，是基于CAP定律逐步演化而来。其核心思想是即使无法做到强一致性，但每个应用都可以根据自身业务特点，才用适当的方式来使系统打到最终一致性。\n\n#### 1. CAP的3选2伪命题\n实际上，不是为了P（分区容错性），必须在C（一致性）和A（可用性）之间任选其一。分区的情况很少出现，CAP在大多时间能够同时满足C和A。\n\n对于分区存在或者探知其影响的情况下，需要提供一种预备策略做出处理：\n\n探知分区的发生；\n进入显示的分区模式，限制某些操作；\n启动恢复过程，恢复数据一致性，补偿分区发生期间的错误。\n#### 2. BASE理论简介\nBASE理论是Basically Available(基本可用)，Soft State（软状态）和Eventually Consistent（最终一致性）三个短语的缩写。\n\n其核心思想是：\n\n既是无法做到强一致性（Strong consistency），但每个应用都可以根据自身的业务特点，采用适当的方式来使系统达到最终一致性（Eventual consistency）。\n\n#### 3. BASE理论的内容\n基本可用（Basically Available）\n软状态（Soft State）\n最终一致性（Eventually Consistent）\n下面展开讨论：\n\n3.1. 基本可用\n什么是基本可用呢？假设系统，出现了不可预知的故障，但还是能用，相比较正常的系统而言：\n\n响应时间上的损失：正常情况下的搜索引擎0.5秒即返回给用户结果，而基本可用的搜索引擎可以在2秒作用返回结果。\n\n功能上的损失：在一个电商网站上，正常情况下，用户可以顺利完成每一笔订单。但是到了大促期间，为了保护购物系统的稳定性，部分消费者可能会被引导到一个降级页面。\n\n3.2. 软状态\n什么是软状态呢？相对于原子性而言，要求多个节点的数据副本都是一致的，这是一种“硬状态”。\n\n软状态指的是：允许系统中的数据存在中间状态，并认为该状态不影响系统的整体可用性，即允许系统在多个不同节点的数据副本存在数据延时。\n\n3.3. 最终一致性\n上面说软状态，然后不可能一直是软状态，必须有个时间期限。在期限过后，应当保证所有副本保持数据一致性，从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。\n\n而在实际工程实践中，最终一致性分为5种：\n\n3.3.1. 因果一致性（Causal consistency）\n\n因果一致性指的是：如果节点A在更新完某个数据后通知了节点B，那么节点B之后对该数据的访问和修改都是基于A更新后的值。于此同时，和节点A无因果关系的节点C的数据访问则没有这样的限制。\n\n3.3.2. 读己之所写（Read your writes）\n\n读己之所写指的是：节点A更新一个数据后，它自身总是能访问到自身更新过的最新值，而不会看到旧值。其实也算一种因果一致性。\n\n3.3.3. 会话一致性（Session consistency）\n\n会话一致性将对系统数据的访问过程框定在了一个会话当中：系统能保证在同一个有效的会话中实现 “读己之所写” 的一致性，也就是说，执行更新操作之后，客户端能够在同一个会话中始终读取到该数据项的最新值。\n\n3.3.4. 单调读一致性（Monotonic read consistency）\n\n单调读一致性指的是：如果一个节点从系统中读取出一个数据项的某个值后，那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。\n\n3.3.5. 单调写一致性（Monotonic write consistency）\n\n单调写一致性指的是：一个系统要能够保证来自同一个节点的写操作被顺序的执行。\n\n在实际的实践中，这5种系统往往会结合使用，以构建一个具有最终一致性的分布式系统。\n\n实际上，不只是分布式系统使用最终一致性，关系型数据库在某个功能上，也是使用最终一致性的。比如备份，数据库的复制过程是需要时间的，这个复制过程中，业务读取到的值就是旧的。当然，最终还是达成了数据一致性。这也算是一个最终一致性的经典案例。\n\n### 更具体的分布式问题\n#### 一、分布式事务\n指事务的操作位于不同的节点上，需要保证事务的 AICD 特性。例如在下单场景下，库存和订单如果不在同一个节点上，就需要涉及分布式事务。\n\n本地消息\n1. 原理\n本地消息表与业务数据表处于同一个数据库中，这样就能利用本地事务来保证在对这两个表的操作满足事务特性。\n\n在分布式事务操作的一方，它完成写业务数据的操作之后向本地消息表发送一个消息，本地事务能保证这个消息一定会被写入本地消息表中。\n之后将本地消息表中的消息转发到 Kafka 等消息队列（MQ）中，如果转发成功则将消息从本地消息表中删除，否则继续重新转发。\n在分布式事务操作的另一方从消息队列中读取一个消息，并执行消息中的操作。\n\n2. 分析\n本地消息表利用了本地事务来实现分布式事务，并且使用了消息队列来保证最终一致性。\n\n两阶段提交协议\n2PC\n参考这里：https://www.cnblogs.com/AndyAo/p/8228099.html\n\n#### 二、分布式锁\n可以使用 Java 提供的内置锁来实现进程同步：由 JVM 实现的 synchronized 和 JDK 提供的 Lock。但是在分布式场景下，需要同步的进程可能位于不同的节点上，那么就需要使用分布式锁来同步。\n\n原理\n锁可以有阻塞锁和乐观锁两种实现方式，这里主要探讨阻塞锁实现。阻塞锁通常使用互斥量来实现，互斥量为 1 表示有其它进程在使用锁，此时处于锁定状态，互斥量为 0 表示未锁定状态。1 和 0 可以用一个整型值来存储，也可以用某个数据存在或者不存在来存储，某个数据存在表示互斥量为 1，也就是锁定状态。\n\n实现\n##### 1. 数据库的唯一索引\n当想要获得锁时，就向表中插入一条记录，释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次，那么就可以用这个记录是否存在来判断是否存于锁定状态。\n\n这种方式存在以下几个问题：\n\n锁没有失效时间，解锁失败会导致死锁，其他线程无法再获得锁。\n只能是非阻塞锁，插入失败直接就报错了，无法重试。\n不可重入，同一线程在没有释放锁之前无法再获得锁。\n##### 2. Redis 的 SETNX 指令\n使用 SETNX（set if not exist）指令插入一个键值对，如果 Key 已经存在，那么会返回 False，否则插入成功并返回 True。\n\nSETNX 指令和数据库的唯一索引类似，可以保证只存在一个 Key 的键值对，可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。\n\nEXPIRE 指令可以为一个键值对设置一个过期时间，从而避免了死锁的发生。\n\n##### 3. Redis 的 RedLock 算法\n使用了多个 Redis 实例来实现分布式锁，这是为了保证在发生单点故障时仍然可用。\n\n尝试从 N 个相互独立 Redis 实例获取锁，如果一个实例不可用，应该尽快尝试下一个。\n计算获取锁消耗的时间，只有当这个时间小于锁的过期时间，并且从大多数（N/2+1）实例上获取了锁，那么就认为锁获取成功了。\n如果锁获取失败，会到每个实例上释放锁。\n##### 4. Zookeeper 的有序节点\nZookeeper 是一个为分布式应用提供一致性服务的软件，例如配置管理、分布式协同以及命名的中心化等，这些都是分布式系统中非常底层而且是必不可少的基本功能，但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性，实际上非常困难。\n\n（一）抽象模型\n\nZookeeper 提供了一种树形结构级的命名空间，/app1/p_1 节点表示它的父节点为 /app1。\n\n（二）节点类型\n\n永久节点：不会因为会话结束或者超时而消失；\n临时节点：如果会话结束或者超时就会消失；\n有序节点：会在节点名的后面加一个数字后缀，并且是有序的，例如生成的有序节点为 /lock/node-0000000000，它的下一个有序节点则为 /lock/node-0000000001，依次类推。\n（三）监听器\n\n为一个节点注册监听器，在节点状态发生改变时，会给客户端发送消息。\n\n（四）分布式锁实现\n\n创建一个锁目录 /lock；\n在 /lock 下创建临时的且有序的子节点，第一个客户端对应的子节点为 /lock/lock-0000000000，第二个为 /lock/lock-0000000001，以此类推；\n客户端获取 /lock 下的子节点列表，判断自己创建的子节点是否为当前子节点列表中序号最小的子节点，如果是则认为获得锁；否则监听自己的前一个子节点，获得子节点的变更通知后重复此步骤直至获得锁；\n执行业务代码，完成后，删除对应的子节点。\n（五）会话超时\n\n如果一个已经获得锁的会话超时了，因为创建的是临时节点，所以该会话对应的临时节点会被删除，其它会话就可以获得锁了。可以看到，Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的死锁问题。\n\n（六）羊群效应\n\n一个节点未获得锁，需要监听自己的前一个子节点，这是因为如果监听所有的子节点，那么任意一个子节点状态改变，其它所有子节点都会收到通知（羊群效应），而我们只希望它的后一个子节点收到通知。\n\n## 三、分布式 Session\n在分布式场景下，一个用户的 Session 如果只存储在一个服务器上，那么当负载均衡器把用户的下一个请求转发到另一个服务器上，该服务器没有用户的 Session，就可能导致用户需要重新进行登录等操作。\n\n\n\n1. Sticky Sessions\n需要配置负载均衡器，使得一个用户的所有请求都路由到一个服务器节点上，这样就可以把用户的 Session 存放在该服务器节点中。\n\n缺点：当服务器节点宕机时，将丢失该服务器节点上的所有 Session。\n\n\n2. Session Replication\n在服务器节点之间进行 Session 同步操作，这样的话用户可以访问任何一个服务器节点。\n\n缺点：需要更好的服务器硬件条件；需要对服务器进行配置。\n\n\n3. Persistent DataStore\n将 Session 信息持久化到一个数据库中。\n\n缺点：有可能需要去实现存取 Session 的代码。\n\n4. In-Memory DataStore\n可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储，可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。\n\n## 四、负载均衡\n算法\n1. 轮询（Round Robin）\n轮询算法把每个请求轮流发送到每个服务器上。下图中，一共有 6 个客户端产生了 6 个请求，这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后，(1, 3, 5) 的请求会被发送到服务器 1，(2, 4, 6) 的请求会被发送到服务器 2。\n\n\n该算法比较适合每个服务器的性能差不多的场景，如果有性能存在差异的情况下，那么性能较差的服务器可能无法承担过大的负载（下图的 Server 2）。\n\n2. 加权轮询（Weighted Round Robbin）\n加权轮询是在轮询的基础上，根据服务器的性能差异，为服务器赋予一定的权值。例如下图中，服务器 1 被赋予的权值为 5，服务器 2 被赋予的权值为 1，那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1，(6) 请求会被发送到服务器 2。\n\n\n3. 最少连接（least Connections）\n由于每个请求的连接时间不一样，使用轮询或者加权轮询算法的话，可能会让一台服务器当前连接数过大，而另一台服务器的连接过小，造成负载不均衡。例如下图中，(1, 3, 5) 请求会被发送到服务器 1，但是 (1, 3) 很快就断开连接，此时只有 (5) 请求连接服务器 1；(2, 4, 6) 请求被发送到服务器 2，只有 (2) 的连接断开。该系统继续运行时，服务器 2 会承担过大的负载。\n\n最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中，服务器 1 当前连接数最小，那么新到来的请求 6 就会被发送到服务器 1 上。\n\n\n4. 加权最少连接（Weighted Least Connection）\n在最少连接的基础上，根据服务器的性能为每台服务器分配权重，再根据权重计算出每台服务器能处理的连接数。\n\n5. 随机算法（Random）\n把请求随机发送到服务器上。和轮询算法类似，该算法比较适合服务器性能差不多的场景。\n\n6. 源地址哈希法 (IP Hash)\n源地址哈希通过对客户端 IP 哈希计算得到的一个数值，用该数值对服务器数量进行取模运算，取模结果便是目标服务器的序号。\n\n优点：保证同一 IP 的客户端都会被 hash 到同一台服务器上。\n缺点：不利于集群扩展，后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。\n\n\n### 实现\n1. HTTP 重定向\nHTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址，并将该地址写入 HTTP 重定向响应中返回给浏览器，浏览器收到后需要再次发送请求。\n\n缺点：\n\n用户访问的延迟会增加；\n如果负载均衡器宕机，就无法访问该站点。\n\n\n \n\n2. DNS 重定向\n使用 DNS 作为负载均衡器，根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段，然后在内部使用其它方式做第二级负载均衡。\n\n缺点：\n\nDNS 查找表可能会被客户端缓存起来，那么之后的所有请求都会被重定向到同一个服务器。\n\n\n \n\n3. 修改 MAC 地址\n使用 LVS（Linux Virtual Server）这种链路层负载均衡器，根据负载情况修改请求的 MAC 地址。\n\n\n\n \n\n4. 修改 IP 地址\n在网络层修改请求的目的 IP 地址。\n\n\n\n \n\n5. 代理自动配置\n正向代理与反向代理的区别：\n\n正向代理：发生在客户端，是由用户主动发起的。比如翻墙，客户端通过主动访问代理服务器，让代理服务器获得需要的外网数据，然后转发回客户端。\n反向代理：发生在服务器端，用户不知道代理的存在。\nPAC 服务器是用来判断一个请求是否要经过代理。\n\n\n\n \n\n### 高可用之“脑裂”\n在涉及到高可用性时，经常会听到”脑裂“，到底啥是”脑裂“？\n\n一句话：当两（多）个节点同时认为自已是唯一处于活动状态的服务器从而出现争用资源的情况，这种争用资源的场景即是所谓的“脑裂”（split-brain）或”区间集群“（partitioned cluster）。\n\nHeartBeat原理：\n\nHeartBeat运行于备用主机上的Heartbeat可以通过以太网连接检测主服务器的运行状态，一旦其无法检测到主服务器的\"心跳\"则自动接管主服务器的资源。通常情况下，主、备服务器间的心跳连接是一个独立的物理连接，这个连接可以是串行线缆、一个由\"交叉线\"实现的以太网连接。Heartbeat甚至可同时通过多个物理连接检测主服务器的工作状态，而其只要能通过其中一个连接收到主服务器处于活动状态的信息，就会认为主服务器处于正常状态。从实践经验的角度来说，建议为Heartbeat配置多条独立的物理连接，以避免Heartbeat通信线路本身存在单点故障。\n\n在“双机热备”高可用（HA）系统中，当联系2个节点的“心跳线”断开时，本来为一整体、动作协调的HA系统，就分裂成为2个独立的个体。由于相互失去了联系，都以为是对方出了故障，2个节点上的HA软件像“裂脑人”一样，“本能”地争抢“共享资源”、争起“应用服务”，就会发生严重后果：或者共享资源被瓜分、2边“服务”都起不来了；或者2边“服务”都起来了，但同时读写“共享存储”，导致数据损坏（常见如数据库轮询着的联机日志出错）。\n运行于备用主机上的Heartbeat可以通过以太网连接检测主服务器的运行状态，一旦其无法检测到主服务器的“心跳”则自动接管主服务器的资源。通常情况下，主、备服务器间的心跳连接是一个独立的物理连接，这个连接可以是串行线缆、一个由“交叉线”实现的以太网连接。Heartbeat甚至可同时通过多个物理连接检测主服务器的工作状态，而其只要能通过其中一个连接收到主服务器处于活动状态的信息，就会认为主服务器处于正常状态。从实践经验的角度来说，建议为Heartbeat配置多条独立的物理连接，以避免Heartbeat通信线路本身存在单点故障。\n1、串行电缆：被认为是比以太网连接安全性稍好些的连接方式，因为hacker无法通过串行连接运行诸如telnet、ssh或rsh类的程序，从而可以降低其通过已劫持的服务器再次侵入备份服务器的几率。但串行线缆受限于可用长度，因此主、备服务器的距离必须非常短。\n2、以太网连接：使用此方式可以消除串行线缆的在长度方面限制，并且可以通过此连接在主备服务器间同步文件系统，从而减少了从正常通信连接带宽的占用。\n  基于冗余的角度考虑，应该在主、备服务器使用两个物理连接传输heartbeat的控制信息；这样可以避免在一个网络或线缆故障时导致两个节点同时认为自已是唯一处于活动状态的服务器从而出现争用资源的情况，这种争用资源的场景即是所谓的“脑裂”（split-brain）或“partitioned cluster”。在两个节点共享同一个物理设备资源的情况下，脑裂会产生相当可怕的后果。\n  为了避免出现脑裂，可采用下面的预防措施：\n添加冗余的心跳线，例如双线条线。尽量减少“裂脑”发生机会。\n启用磁盘锁。正在服务一方锁住共享磁盘，“裂脑”发生时，让对方完全“抢不走”共享磁盘资源。但使用锁磁盘也会有一个不小的问题，如果占用共享盘的一方不主动“解锁”，另一方就永远得不到共享磁盘。现实中假如服务节点突然死机或崩溃，就不可能执行解锁命令。后备节点也就接管不了共享资源和应用服务。于是有人在HA中设计了“智能”锁。即，正在服务的一方只在发现心跳线全部断开（察觉不到对端）时才启用磁盘锁。平时就不上锁了。\n设置仲裁机制。例如设置参考IP（如网关IP），当心跳线完全断开时，2个节点都各自ping一下 参考IP，不通则表明断点就出在本端，不仅“心跳”、还兼对外“服务”的本端网络链路断了，即使启动（或继续）应用服务也没有用了，那就主动放弃竞争，让能够ping通参考IP的一端去起服务。更保险一些，ping不通参考IP的一方干脆就自我重启，以彻底释放有可能还占用着的那些共享资源。\n"
  },
  {
    "path": "分布式理论/分布式锁的解决方案(二).md",
    "content": "目前几乎很多大型网站及应用都是分布式部署的，分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性（Consistency）、可用性（Availability）和分区容错性（Partition tolerance），最多只能同时满足两项。”所以，很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中，都需要牺牲强一致性来换取系统的高可用性，系统往往只需要保证“最终一致性”，只要这个最终时间是在用户可以接受的范围内即可。\n\n在很多场景中，我们为了保证数据的最终一致性，需要很多的技术方案来支持，比如分布式事务、分布式锁等。有的时候，我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中，Java中其实提供了很多并发处理相关的API，但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。所以针对分布式锁的实现目前有多种方案。\n\n针对分布式锁的实现，目前比较常用的有以下几种方案：\n\n>基于数据库实现分布式锁 基于缓存（redis，memcached，tair）实现分布式锁 基于Zookeeper实现分布式锁\n\n在分析这几种实现方案之前我们先来想一下，我们需要的分布式锁应该是怎么样的？（这里以方法锁为例，资源锁同理）\n\n* 可以保证在分布式部署的应用集群中，同一个方法在同一时间只能被一台机器上的一个线程执行\n\n\n* 这把锁要是一把可重入锁（避免死锁）\n\n* 这把锁最好是一把阻塞锁（根据业务需求考虑要不要这条）\n\n* 有高可用的获取锁和释放锁功能\n\n* 获取锁和释放锁的性能要好\n\n## 基于数据库实现分布式锁\n### 基于数据库表\n要实现分布式锁，最简单的方式可能就是直接创建一张锁表，然后通过操作该表中的数据来实现了。\n\n当我们要锁住某个方法或资源时，我们就在该表中增加一条记录，想要释放锁的时候就删除这条记录。\n\n创建这样一张数据库表：\n\n```\nCREATE TABLE `methodLock` (\n  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',\n  `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',\n  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',\n  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间，自动生成',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';\n```\n\n\n当我们想要锁住某个方法时，执行以下SQL：\n\n```\ninsert into methodLock(method_name,desc) values (‘method_name’,‘desc’)\n```\n\n因为我们对method_name做了唯一性约束，这里如果有多个请求同时提交到数据库的话，数据库会保证只有一个操作可以成功，那么我们就可以认为操作成功的那个线程获得了该方法的锁，可以执行方法体内容。\n\n当方法执行完毕之后，想要释放锁的话，需要执行以下Sql:\n\n```\ndelete from methodLock where method_name ='method_name'\n```\n\n上面这种简单的实现有以下几个问题：\n\n1、这把锁强依赖数据库的可用性，数据库是一个单点，一旦数据库挂掉，会导致业务系统不可用。\n\n2、这把锁没有失效时间，一旦解锁操作失败，就会导致锁记录一直在数据库中，其他线程无法再获得到锁。\n\n3、这把锁只能是非阻塞的，因为数据的insert操作，一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列，要想再次获得锁就要再次触发获得锁操作。\n\n4、这把锁是非重入的，同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。\n\n当然，我们也可以有其他方式解决上面的问题。\n\n* 数据库是单点？搞两个数据库，数据之前双向同步。一旦挂掉快速切换到备库上。\n\n* 没有失效时间？只要做一个定时任务，每隔一定时间把数据库中的超时数据清理一遍。\n\n* 非阻塞的？搞一个while循环，直到insert成功再返回成功。\n\n* 非重入的？在数据库表中加个字段，记录当前获得锁的机器的主机信息和线程信息，那么下次再获取锁的时候先查询数据库，如果当前机器的主机信息和线程信息在数据库可以查到的话，直接把锁分配给他就可以了。\n\n### 基于数据库排他锁\n除了可以通过增删操作数据表中的记录以外，其实还可以借助数据中自带的锁来实现分布式的锁。\n\n我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎，可以使用以下方法来实现加锁操作：\n\n```\npublic boolean lock(){\n    connection.setAutoCommit(false)\n    while(true){\n        try{\n            result = select * from methodLock where method_name=xxx for update;\n            if(result==null){\n                return true;\n            }\n        }catch(Exception e){\n\n        }\n        sleep(1000);\n    }\n    return false;\n}\n```\n\n在查询语句后面增加for update，数据库会在查询过程中给数据库表增加排他锁（这里再多提一句，InnoDB引擎在加锁的时候，只有通过索引进行检索的时候才会使用行级锁，否则会使用表级锁。这里我们希望使用行级锁，就要给method_name添加索引，值得注意的是，这个索引一定要创建成唯一索引，否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。）。当某条记录被加上排他锁之后，其他线程无法再在该行记录上增加排他锁。\n\n我们可以认为获得排它锁的线程即可获得分布式锁，当获取到锁之后，可以执行方法的业务逻辑，执行完方法之后，再通过以下方法解锁：\n\n```\npublic void unlock(){\n    connection.commit();\n}\n```\n\n通过connection.commit()操作来释放锁。\n\n通过connection.commit()操作来释放锁。\n\n这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。\n阻塞锁？ for update语句会在执行成功后立即返回，在执行失败时一直处于阻塞状态，直到成功。\n锁定之后服务宕机，无法释放？使用这种方式，服务宕机之后数据库会自己把锁释放掉。\n但是还是无法直接解决数据库单点和可重入问题。\n\n这里还可能存在另外一个问题，虽然我们对method_name 使用了唯一索引，并且显示使用for update来使用行级锁。但是，MySql会对查询进行优化，即便在条件中使用了索引字段，但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的，如果 MySQL 认为全表扫效率更高，比如对一些很小的表，它就不会使用索引，这种情况下 InnoDB 将使用表锁，而不是行锁。如果发生这种情况就悲剧了。\n\n还有一个问题，就是我们要使用排他锁来进行分布式锁的lock，那么一个排他锁长时间不提交，就会占用数据库连接。一旦类似的连接变得多了，就可能把数据库连接池撑爆\n\n### 总结\n总结一下使用数据库来实现分布式锁的方式，这两种方式都是依赖数据库的一张表，一种是通过表中的记录的存在情况确定当前是否有锁存在，另外一种是通过数据库的排他锁来实现分布式锁。\n\n数据库实现分布式锁的优点:\n\n直接借助数据库，容易理解。\n\n数据库实现分布式锁的缺点:\n\n会有各种各样的问题，在解决问题的过程中会使整个方案变得越来越复杂。\n\n操作数据库需要一定的开销，性能问题需要考虑。\n\n使用数据库的行级锁并不一定靠谱，尤其是当我们的锁表并不大的时候。\n\n## 基于缓存实现分布式锁\n相比较于基于数据库实现分布式锁的方案来说，基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的，可以解决单点问题。\n\n目前有很多成熟的缓存产品，包括Redis，memcached以及阿里巴巴的Tair。\n\n这里以Tair为例来分析下使用缓存实现分布式锁的方案。关于Redis和memcached在网络上有很多相关的文章，并且也有一些成熟的框架及算法可以直接使用。\n\n基于Tair的实现分布式锁其实和Redis类似，其中主要的实现方式是使用TairManager.put方法来实现。\n\n```\npublic boolean trylock(String key) {\n    ResultCode code = ldbTairManager.put(NAMESPACE, key, \"This is a Lock.\", 2, 0);\n    if (ResultCode.SUCCESS.equals(code))\n        return true;\n    else\n        return false;\n}\npublic boolean unlock(String key) {\n    ldbTairManager.invalid(NAMESPACE, key);\n}\n```\n\n以上实现方式同样存在几个问题：\n\n* 1、这把锁没有失效时间，一旦解锁操作失败，就会导致锁记录一直在tair中，其他线程无法再获得到锁。\n \n* 2、这把锁只能是非阻塞的，无论成功还是失败都直接返回。\n \n* 3、这把锁是非重入的，一个线程获得锁之后，在释放锁之前，无法再次获得该锁，因为使用到的key在tair中已经存在。无法再执行put操作。\n\n当然，同样有方式可以解决。\n\n* 没有失效时间？tair的put方法支持传入失效时间，到达时间之后数据会自动删除。\n\n* 非阻塞？while重复执行。\n\n* 非可重入？在一个线程获取到锁之后，把当前主机信息和线程信息保存起来，下次再获取之前先检查自己是不是当前锁的拥有者。\n\n但是，失效时间我设置多长时间为好？如何设置的失效时间太短，方法没等执行完，锁就自动释放了，那么就会产生并发问题。如果设置的时间太长，其他获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁同样存在\n\n### 总结\n可以使用缓存来代替数据库来实现分布式锁，这个可以提供更好的性能，同时，很多缓存服务都是集群部署的，可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式锁的方法，比如Tair的put方法，redis的setnx方法等。并且，这些缓存服务也都提供了对数据的过期自动删除的支持，可以直接设置超时时间来控制锁的释放。\n\n使用缓存实现分布式锁的优点\n\n性能好，实现起来较为方便。\n\n使用缓存实现分布式锁的缺点\n\n通过超时时间来控制锁的失效时间并不是十分的靠谱。\n\n## 基于Zookeeper实现分布式锁\n基于zookeeper临时有序节点可以实现的分布式锁。\n\n大致思想即为：每个客户端对某个方法加锁时，在zookeeper上的与该方法对应的指定节点的目录下，生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单，只需要判断有序节点中序号最小的一个。 当释放锁的时候，只需将这个瞬时节点删除即可。同时，其可以避免服务宕机导致的锁无法释放，而产生的死锁问题。\n\n来看下Zookeeper能不能解决前面提到的问题。\n\n锁无法释放？使用Zookeeper可以有效的解决锁无法释放的问题，因为在创建锁的时候，客户端会在ZK中创建一个临时节点，一旦客户端获取到锁之后突然挂掉（Session连接断开），那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。\n\n非阻塞锁？使用Zookeeper可以实现阻塞的锁，客户端可以通过在ZK中创建顺序节点，并且在节点上绑定监听器，一旦节点有变化，Zookeeper会通知客户端，客户端可以检查自己创建的节点是不是当前所有节点中序号最小的，如果是，那么自己就获取到锁，便可以执行业务逻辑了。\n\n不可重入？使用Zookeeper也可以有效的解决不可重入的问题，客户端在创建节点的时候，把当前客户端的主机信息和线程信息直接写入到节点中，下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样，那么自己直接获取到锁，如果不一样就再创建一个临时的顺序节点，参与排队。\n\n单点问题？使用Zookeeper可以有效的解决单点问题，ZK是集群部署的，只要集群中有半数以上的机器存活，就可以对外提供服务。\n\n可以直接使用zookeeper第三方库Curator客户端，这个客户端中封装了一个可重入的锁服务。\n\n```\npublic boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {\n    try {\n        return interProcessMutex.acquire(timeout, unit);\n    } catch (Exception e) {\n        e.printStackTrace();\n    }\n    return true;\n}\npublic boolean unlock() {\n    try {\n        interProcessMutex.release();\n    } catch (Throwable e) {\n        log.error(e.getMessage(), e);\n    } finally {\n        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);\n    }\n    return true;\n}\n```\n\nCurator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁，release方法用于释放锁。\n\n使用ZK实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是，其实并不是，Zookeeper实现的分布式锁其实存在一个缺点，那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中，都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行，然后将数据同不到所有的Follower机器上。\n\n其实，使用Zookeeper也有可能带来并发问题，只是并不常见而已。考虑这样的情况，由于网络抖动，客户端可ZK集群的session连接断了，那么zk以为客户端挂了，就会删除临时节点，这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制，一旦zk集群检测不到客户端的心跳，就会重试，Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。（所以，选择一个合适的重试策略也比较重要，要在锁的粒度和并发之间找一个平衡。）\n\n### 总结\n使用Zookeeper实现分布式锁的优点\n\n有效的解决单点问题，不可重入问题，非阻塞问题以及锁无法释放的问题。实现起来较为简单。\n\n使用Zookeeper实现分布式锁的缺点\n\n性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。\n\n> *本文作者：Hollis*\n\n"
  },
  {
    "path": "分布式理论/分布式锁的解决方案.md",
    "content": "## 前言\n\n随着互联网技术的不断发展，数据量的不断增加，业务逻辑日趋复杂，在这种背景下，传统的集中式系统已经无法满足我们的业务需求，分布式系统被应用在更多的场景，而在分布式系统中访问共享资源就需要一种互斥机制，来防止彼此之间的互相干扰，以保证一致性，在这种情况下，我们就需要用到分布式锁。\n\n\n## 分布式一致性问题\n\n首先我们先来看一个小例子：\n\n假设某商城有一个商品库存剩10个，用户A想要买6个，用户B想要买5个，在理想状态下，用户A先买走了6了，库存减少6个还剩4个，此时用户B应该无法购买5个，给出数量不足的提示；而在真实情况下，用户A和B同时获取到商品剩10个，A买走6个，在A更新库存之前，B又买走了5个，此时B更新库存，商品还剩5个，这就是典型的电商“秒杀”活动。\n\n从上述例子不难看出，在高并发情况下，如果不做处理将会出现各种不可预知的后果。那么在这种高并发多线程的情况下，解决问题最有效最普遍的方法就是给共享资源或对共享资源的操作加一把锁，来保证对资源的访问互斥。在Java JDK已经为我们提供了这样的锁，利用ReentrantLcok或者synchronized，即可达到资源互斥访问的目的。但是在分布式系统中，由于分布式系统的分布性，即多线程和多进程并且分布在不同机器中，这两种锁将失去原有锁的效果，需要我们自己实现分布式锁——分布式锁。\n\n\n\n### 分布式锁需要具备哪些条件\n\n1. 获取锁和释放锁的性能要好\n\n2. 判断是否获得锁必须是原子性的，否则可能导致多个请求都获取到锁\n\n3. 网络中断或宕机无法释放锁时，锁必须被清楚，不然会发生死锁\n\n4. 可重入一个线程中可以多次获取同一把锁，比如一个线程在执行一个带锁的方法，该方法中又调用了另一个需要相同锁的方法，则该线程可以直接执行调用的方法，而无需重新获得锁；\n\n5.阻塞锁和非阻塞锁，阻塞锁即没有获取到锁，则继续等待获取锁；非阻塞锁即没有获取到锁后，不继续等待，直接返回锁失败。\n\n\n\n## 分布式锁实现方式\n\n### 一、数据库锁\n\n一般很少使用数据库锁，性能不好并且容易产生死锁。\n\n#### 1. 基于MySQL锁表\n\n该实现方式完全依靠数据库唯一索引来实现，当想要获得锁时，即向数据库中插入一条记录，释放锁时就删除这条记录。这种方式存在以下几个问题：\n\n(1) 锁没有失效时间，解锁失败会导致死锁，其他线程无法再获取到锁，因为唯一索引insert都会返回失败。\n\n(2) 只能是非阻塞锁，insert失败直接就报错了，无法进入队列进行重试\n\n(3) 不可重入，同一线程在没有释放锁之前无法再获取到锁\n\n#### 2. 采用乐观锁增加版本号\n\n根据版本号来判断更新之前有没有其他线程更新过，如果被更新过，则获取锁失败。\n\n### 二、缓存锁\n\n具体实例可以参考我讲述Redis的系列文章，里面有完整的Redis分布式锁实现方案\n\n这里我们主要介绍几种基于redis实现的分布式锁：\n\n#### 1. 基于setnx、expire两个命令来实现\n\n基于setnx（set if not exist）的特点，当缓存里key不存在时，才会去set，否则直接返回false。如果返回true则获取到锁，否则获取锁失败，为了防止死锁，我们再用expire命令对这个key设置一个超时时间来避免。但是这里看似完美，实则有缺陷，当我们setnx成功后，线程发生异常中断，expire还没来的及设置，那么就会产生死锁。\n\n解决上述问题有两种方案\n\n第一种是采用redis2.6.12版本以后的set，它提供了一系列选项\n\n* EX seconds – 设置键key的过期时间，单位时秒\n\n* PX milliseconds – 设置键key的过期时间，单位时毫秒\n\n* NX – 只有键key不存在的时候才会设置key的值\n\n* XX – 只有键key存在的时候才会设置key的值\n\n第二种采用setnx()，get()，getset()实现，大体的实现过程如下：\n\n(1) 线程Asetnx，值为超时的时间戳(t1)，如果返回true，获得锁。\n\n(2) 线程B用get 命令获取t1，与当前时间戳比较，判断是否超时，没超时false，如果已超时执行步骤3\n\n(3) 计算新的超时时间t2，使用getset命令返回t3(这个值可能其他线程已经修改过)，如果t1==t3,获得锁,如果t1!=t3说明锁被其他线程获取了\n\n(4) 获取锁后，处理完业务逻辑，再去判断锁是否超时，如果没超时删除锁，如果已超时，不用处理（防止删除其他线程的锁）\n\n#### 2. RedLock算法\n\nredlock算法是redis作者推荐的一种分布式锁实现方式，算法的内容如下：\n\n(1) 获取当前时间；\n\n(2) 尝试从5个相互独立redis客户端获取锁；\n\n(3) 计算获取所有锁消耗的时间，当且仅当客户端从多数节点获取锁，并且获取锁的时间小于锁的有效时间，认为获得锁；\n\n(4) 重新计算有效期时间，原有效时间减去获取锁消耗的时间；\n\n(5) 删除所有实例的锁\n\nredlock算法相对于单节点redis锁可靠性要更高，但是实现起来条件也较为苛刻。\n\n(1) 必须部署5个节点才能让Redlock的可靠性更强。\n\n(2) 需要请求5个节点才能获取到锁，通过Future的方式，先并发向5个节点请求，再一起获得响应结果，能缩短响应时间，不过还是比单节点redis锁要耗费更多时间。\n\n然后由于必须获取到5个节点中的3个以上，所以可能出现获取锁冲突，即大家都获得了1-2把锁，结果谁也不能获取到锁，这个问题，redis作者借鉴了raft算法的精髓，通过冲突后在随机时间开始，可以大大降低冲突时间，但是这问题并不能很好的避免，特别是在第一次获取锁的时候，所以获取锁的时间成本增加了。\n\n如果5个节点有2个宕机，此时锁的可用性会极大降低，首先必须等待这两个宕机节点的结果超时才能返回，另外只有3个节点，客户端必须获取到这全部3个节点的锁才能拥有锁，难度也加大了。\n\n如果出现网络分区，那么可能出现客户端永远也无法获取锁的情况，介于这种情况，下面我们来看一种更可靠的分布式锁zookeeper锁。\n\n\n\n### zookeeper分布式锁\n\n首先我们来了解一下zookeeper的特性，看看它为什么适合做分布式锁，\n\nzookeeper是一个为分布式应用提供一致性服务的软件，它内部是一个分层的文件系统目录树结构，规定统一个目录下只能有一个唯一文件名。\n\n数据模型：\n\n永久节点：节点创建后，不会因为会话失效而消失\n\n临时节点：与永久节点相反，如果客户端连接失效，则立即删除节点\n\n顺序节点：与上述两个节点特性类似，如果指定创建这类节点时，zk会自动在节点名后加一个数字后缀，并且是有序的。\n\n监视器（watcher）：\n\n当创建一个节点时，可以注册一个该节点的监视器，当节点状态发生改变时，watch被触发时，ZooKeeper将会向客户端发送且仅发送一条通知，因为watch只能被触发一次。\n\n根据zookeeper的这些特性，我们来看看如何利用这些特性来实现分布式锁：\n\n1. 创建一个锁目录lock\n\n2. 希望获得锁的线程A就在lock目录下，创建临时顺序节点\n\n3. 获取锁目录下所有的子节点，然后获取比自己小的兄弟节点，如果不存在，则说明当前线程顺序号最小，获得锁\n\n4. 线程B获取所有节点，判断自己不是最小节点，设置监听(watcher)比自己次小的节点（只关注比自己次小的节点是为了防止发生“羊群效应”）\n\n5. 线程A处理完，删除自己的节点，线程B监听到变更事件，判断自己是最小的节点，获得锁。\n\n\n\n小结\n\n在分布式系统中，共享资源互斥访问问题非常普遍，而针对访问共享资源的互斥问题，常用的解决方案就是使用分布式锁，这里只介绍了几种常用的分布式锁，分布式锁的实现方式还有有很多种，根据业务选择合适的分布式锁，下面对上述几种锁进行一下比较：\n\n数据库锁：\n\n优点：直接使用数据库，使用简单。\n\n缺点：分布式系统大多数瓶颈都在数据库，使用数据库锁会增加数据库负担。\n\n缓存锁：\n\n优点：性能高，实现起来较为方便，在允许偶发的锁失效情况，不影响系统正常使用，建议采用缓存锁。\n\n缺点：通过锁超时机制不是十分可靠，当线程获得锁后，处理时间过长导致锁超时，就失效了锁的作用。\n\nzookeeper锁：\n\n优点：不依靠超时时间释放锁；可靠性高；系统要求高可靠性时，建议采用zookeeper锁。\n\n缺点：性能比不上缓存锁，因为要频繁的创建节点删除节点。\n\n\n事实上，大家只要参考引入下面的代码：\nhttps://github.com/yujiasun/Distributed-Kit\n这个过程有基于redis和zookeeper分布式工具集-包括:分布式锁实现,分布式速率限制器,分布式ID生成器等。\n\n### 重要的事情说三遍：\n\n~~**不要去用一个没有经过严酷环境考验的自己写的分布式锁\n不要去用一个没有经过严酷环境考验的自己写的分布式锁\n不要去用一个没有经过严酷环境考验的自己写的分布式锁**~~"
  },
  {
    "path": "大数据框架学习/Azkaban_Flow_1.0_的使用.md",
    "content": "# Azkaban Flow 1.0 的使用\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二基本任务调度\">二、基本任务调度</a><br/>\n<a href=\"#三多任务调度\">三、多任务调度</a><br/>\n<a href=\"#四调度HDFS作业\">四、调度HDFS作业</a><br/>\n<a href=\"#五调度MR作业\">五、调度MR作业</a><br/>\n<a href=\"#六调度Hive作业\">六、调度Hive作业</a><br/>\n<a href=\"#七在线修改作业配置\">七、在线修改作业配置</a><br/>\n</nav>\n\n\n\n## 一、简介\n\nAzkaban 主要通过界面上传配置文件来进行任务的调度。它有两个重要的概念：\n\n- **Job**： 你需要执行的调度任务；\n- **Flow**：一个获取多个 Job 及它们之间的依赖关系所组成的图表叫做 Flow。\n\n目前 Azkaban 3.x 同时支持 Flow 1.0 和 Flow 2.0，本文主要讲解 Flow 1.0 的使用，下一篇文章会讲解 Flow 2.0 的使用。\n\n## 二、基本任务调度\n\n### 2.1 新建项目\n\n在 Azkaban 主界面可以创建对应的项目：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-create-project.png\"/> </div>\n\n### 2.2 任务配置\n\n新建任务配置文件 `Hello-Azkaban.job`，内容如下。这里的任务很简单，就是输出一句 `'Hello Azkaban!'` ：\n\n```shell\n#command.job\ntype=command\ncommand=echo 'Hello Azkaban!'\n```\n\n### 2.3 打包上传\n\n将 `Hello-Azkaban.job ` 打包为 `zip` 压缩文件：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-zip.png\"/> </div>\n\n通过 Web UI 界面上传：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-upload.png\"/> </div>\n\n上传成功后可以看到对应的 Flows：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-flows.png\"/> </div>\n\n### 2.4 执行任务\n\n点击页面上的 `Execute Flow` 执行任务：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-execute.png\"/> </div>\n\n### 2.5 执行结果\n\n点击 `detail` 可以查看到任务的执行日志：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-successed.png\"/> </div>\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-log.png\"/> </div>\n\n## 三、多任务调度\n\n### 3.1 依赖配置\n\n这里假设我们有五个任务（TaskA——TaskE）,D 任务需要在 A，B，C 任务执行完成后才能执行，而 E 任务则需要在 D 任务执行完成后才能执行，这种情况下需要使用 `dependencies` 属性定义其依赖关系。各任务配置如下：\n\n**Task-A.job**   :\n\n```shell\ntype=command\ncommand=echo 'Task A'\n```\n\n**Task-B.job**   :\n\n```shell\ntype=command\ncommand=echo 'Task B'\n```\n\n**Task-C.job**   :\n\n```shell\ntype=command\ncommand=echo 'Task C'\n```\n\n**Task-D.job**   : \n\n```shell\ntype=command\ncommand=echo 'Task D'\ndependencies=Task-A,Task-B,Task-C\n```\n\n**Task-E.job**   :\n\n```shell\ntype=command\ncommand=echo 'Task E'\ndependencies=Task-D\n```\n\n### 3.2 压缩上传\n\n压缩后进行上传，这里需要注意的是一个 Project 只能接收一个压缩包，这里我还沿用上面的 Project，默认后面的压缩包会覆盖前面的压缩包：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-task-abcde-zip.png\"/> </div>\n\n### 3.3 依赖关系\n\n多个任务存在依赖时，默认采用最后一个任务的文件名作为 Flow 的名称，其依赖关系如图：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-dependencies.png\"/> </div>\n\n### 3.4 执行结果\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-task-abcde.png\"/> </div>\n\n从这个案例可以看出，Flow1.0 无法通过一个 job 文件来完成多个任务的配置，但是 Flow 2.0 就很好的解决了这个问题。\n\n## 四、调度HDFS作业\n\n步骤与上面的步骤一致，这里以查看 HDFS 上的文件列表为例。命令建议采用完整路径，配置文件如下：\n\n```shell\ntype=command\ncommand=/usr/app/hadoop-2.6.0-cdh5.15.2/bin/hadoop fs -ls /\n```\n\n执行结果：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-hdfs.png\"/> </div>\n\n## 五、调度MR作业\n\nMR 作业配置：\n\n```shell\ntype=command\ncommand=/usr/app/hadoop-2.6.0-cdh5.15.2/bin/hadoop jar /usr/app/hadoop-2.6.0-cdh5.15.2/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3\n```\n\n执行结果：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-mr.png\"/> </div>\n\n## 六、调度Hive作业\n\n作业配置：\n\n```shell\ntype=command\ncommand=/usr/app/hive-1.1.0-cdh5.15.2/bin/hive -f 'test.sql'\n```\n\n其中 `test.sql` 内容如下，创建一张雇员表，然后查看其结构：\n\n```sql\nCREATE DATABASE IF NOT EXISTS hive;\nuse hive;\ndrop table if exists emp;\nCREATE TABLE emp(\nempno int,\nename string,\njob string,\nmgr int,\nhiredate string,\nsal double,\ncomm double,\ndeptno int\n) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\\t';\n-- 查看 emp 表的信息\ndesc emp;\n```\n\n打包的时候将 `job` 文件与 `sql` 文件一并进行打包：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-hive.png\"/> </div>\n\n执行结果如下：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-hive-result.png\"/> </div>\n\n## 七、在线修改作业配置\n\n在测试时，我们可能需要频繁修改配置，如果每次修改都要重新打包上传，这会比较麻烦。所以 Azkaban 支持配置的在线修改，点击需要修改的 Flow，就可以进入详情页面：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-project-edit.png\"/> </div>\n\n在详情页面点击 `Eidt` 按钮可以进入编辑页面：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-edit.png\"/> </div>\n\n在编辑页面可以新增配置或者修改配置：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-click-edit.png\"/> </div>\n\n## 附：可能出现的问题\n\n如果出现以下异常，多半是因为执行主机内存不足，Azkaban 要求执行主机的可用内存必须大于 3G 才能执行任务：\n\n```shell\nCannot request memory (Xms 0 kb, Xmx 0 kb) from system for job\n```\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-memory.png\"/> </div>\n\n如果你的执行主机没办法增大内存，那么可以通过修改 `plugins/jobtypes/` 目录下的 `commonprivate.properties` 文件来关闭内存检查，配置如下：\n\n```shell\nmemCheck.enabled=false\n```\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Azkaban_Flow_2.0_的使用.md",
    "content": "# Azkaban Flow 2.0的使用\n\n<nav>\n<a href=\"#一Flow-20-简介\">一、Flow 2.0 简介</a><br/>\n<a href=\"#二YAML语法\">二、YAML语法</a><br/>\n<a href=\"#三简单任务调度\">三、简单任务调度</a><br/>\n<a href=\"#四多任务调度\">四、多任务调度</a><br/>\n<a href=\"#五内嵌流\">五、内嵌流</a><br/>\n</nav>\n\n\n## 一、Flow 2.0 简介\n\n### 1.1 Flow 2.0 的产生\n\nAzkaban 目前同时支持 Flow 1.0 和 Flow2.0 ，但是官方文档上更推荐使用 Flow 2.0，因为 Flow 1.0 会在将来的版本被移除。Flow 2.0 的主要设计思想是提供 1.0 所没有的流级定义。用户可以将属于给定流的所有 `job / properties` 文件合并到单个流定义文件中，其内容采用 YAML 语法进行定义，同时还支持在流中再定义流，称为嵌入流或子流。\n\n### 1.2 基本结构\n\n项目 zip 将包含多个流 YAML 文件，一个项目 YAML 文件以及可选库和源代码。Flow YAML 文件的基本结构如下：\n\n+ 每个 Flow 都在单个 YAML 文件中定义；\n+ 流文件以流名称命名，如：`my-flow-name.flow`；\n+ 包含 DAG 中的所有节点；\n+  每个节点可以是作业或流程；\n+  每个节点 可以拥有 name, type, config, dependsOn 和 nodes sections 等属性；\n+  通过列出 dependsOn 列表中的父节点来指定节点依赖性；\n+ 包含与流相关的其他配置；\n+   当前 properties 文件中流的所有常见属性都将迁移到每个流 YAML 文件中的 config 部分。\n\n官方提供了一个比较完善的配置样例，如下：\n\n```yaml\nconfig:\n  user.to.proxy: azktest\n  param.hadoopOutData: /tmp/wordcounthadoopout\n  param.inData: /tmp/wordcountpigin\n  param.outData: /tmp/wordcountpigout\n\n# This section defines the list of jobs\n# A node can be a job or a flow\n# In this example, all nodes are jobs\nnodes:\n # Job definition\n # The job definition is like a YAMLified version of properties file\n # with one major difference. All custom properties are now clubbed together\n # in a config section in the definition.\n # The first line describes the name of the job\n - name: AZTest\n   type: noop\n   # The dependsOn section contains the list of parent nodes the current\n   # node depends on\n   dependsOn:\n     - hadoopWC1\n     - NoOpTest1\n     - hive2\n     - java1\n     - jobCommand2\n\n - name: pigWordCount1\n   type: pig\n   # The config section contains custom arguments or parameters which are\n   # required by the job\n   config:\n     pig.script: src/main/pig/wordCountText.pig\n\n - name: hadoopWC1\n   type: hadoopJava\n   dependsOn:\n     - pigWordCount1\n   config:\n     classpath: ./*\n     force.output.overwrite: true\n     input.path: ${param.inData}\n     job.class: com.linkedin.wordcount.WordCount\n     main.args: ${param.inData} ${param.hadoopOutData}\n     output.path: ${param.hadoopOutData}\n\n - name: hive1\n   type: hive\n   config:\n     hive.script: src/main/hive/showdb.q\n\n - name: NoOpTest1\n   type: noop\n\n - name: hive2\n   type: hive\n   dependsOn:\n     - hive1\n   config:\n     hive.script: src/main/hive/showTables.sql\n\n - name: java1\n   type: javaprocess\n   config:\n     Xms: 96M\n     java.class: com.linkedin.foo.HelloJavaProcessJob\n\n - name: jobCommand1\n   type: command\n   config:\n     command: echo \"hello world from job_command_1\"\n\n - name: jobCommand2\n   type: command\n   dependsOn:\n     - jobCommand1\n   config:\n     command: echo \"hello world from job_command_2\"\n```\n\n## 二、YAML语法\n\n想要使用 Flow 2.0 进行工作流的配置，首先需要了解 YAML 。YAML 是一种简洁的非标记语言，有着严格的格式要求的，如果你的格式配置失败，上传到 Azkaban 的时候就会抛出解析异常。\n\n### 2.1 基本规则\n\n1. 大小写敏感 ；\n2. 使用缩进表示层级关系 ；\n3. 缩进长度没有限制，只要元素对齐就表示这些元素属于一个层级； \n4. 使用#表示注释 ；\n5. 字符串默认不用加单双引号，但单引号和双引号都可以使用，双引号表示不需要对特殊字符进行转义；\n6. YAML 中提供了多种常量结构，包括：整数，浮点数，字符串，NULL，日期，布尔，时间。\n\n### 2.2 对象的写法\n\n```yaml\n# value 与 ： 符号之间必须要有一个空格\nkey: value\n```\n\n### 2.3 map的写法\n\n```yaml\n# 写法一 同一缩进的所有键值对属于一个map\nkey: \n    key1: value1\n    key2: value2\n\n# 写法二\n{key1: value1, key2: value2}\n```\n\n### 2.3 数组的写法\n\n```yaml\n# 写法一 使用一个短横线加一个空格代表一个数组项\n- a\n- b\n- c\n\n# 写法二\n[a,b,c]\n```\n\n### 2.5 单双引号\n\n支持单引号和双引号，但双引号不会对特殊字符进行转义：\n\n```yaml\ns1: '内容\\n 字符串'\ns2: \"内容\\n 字符串\"\n\n转换后：\n{ s1: '内容\\\\n 字符串', s2: '内容\\n 字符串' }\n```\n\n### 2.6 特殊符号\n\n一个 YAML 文件中可以包括多个文档，使用 `---` 进行分割。\n\n### 2.7 配置引用\n\nFlow 2.0 建议将公共参数定义在 `config` 下，并通过 `${}` 进行引用。\n\n\n\n## 三、简单任务调度\n\n### 3.1 任务配置\n\n新建 `flow` 配置文件：\n\n```yaml\nnodes:\n  - name: jobA\n    type: command\n    config:\n      command: echo \"Hello Azkaban Flow 2.0.\"\n```\n\n在当前的版本中，Azkaban 同时支持 Flow 1.0 和 Flow 2.0，如果你希望以 2.0 的方式运行，则需要新建一个 `project` 文件，指明是使用的是 Flow 2.0：\n\n```shell\nazkaban-flow-version: 2.0\n```\n\n### 3.2 打包上传\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-simple.png\"/> </div>\n\n\n\n### 3.3 执行结果\n\n由于在 1.0 版本中已经介绍过 Web UI 的使用，这里就不再赘述。对于 1.0 和 2.0 版本，只有配置方式有所不同，其他上传执行的方式都是相同的。执行结果如下：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-simle-result.png\"/> </div>\n\n## 四、多任务调度\n\n和 1.0 给出的案例一样，这里假设我们有五个任务（jobA——jobE）, D 任务需要在 A，B，C 任务执行完成后才能执行，而 E 任务则需要在 D 任务执行完成后才能执行，相关配置文件应如下。可以看到在 1.0 中我们需要分别定义五个配置文件，而在 2.0 中我们只需要一个配置文件即可完成配置。\n\n```yaml\nnodes:\n  - name: jobE\n    type: command\n    config:\n      command: echo \"This is job E\"\n    # jobE depends on jobD\n    dependsOn: \n      - jobD\n    \n  - name: jobD\n    type: command\n    config:\n      command: echo \"This is job D\"\n    # jobD depends on jobA、jobB、jobC\n    dependsOn:\n      - jobA\n      - jobB\n      - jobC\n\n  - name: jobA\n    type: command\n    config:\n      command: echo \"This is job A\"\n\n  - name: jobB\n    type: command\n    config:\n      command: echo \"This is job B\"\n\n  - name: jobC\n    type: command\n    config:\n      command: echo \"This is job C\"\n```\n\n## 五、内嵌流\n\nFlow2.0 支持在一个 Flow 中定义另一个 Flow，称为内嵌流或者子流。这里给出一个内嵌流的示例，其 `Flow` 配置如下：\n\n```yaml\nnodes:\n  - name: jobC\n    type: command\n    config:\n      command: echo \"This is job C\"\n    dependsOn:\n      - embedded_flow\n\n  - name: embedded_flow\n    type: flow\n    config:\n      prop: value\n    nodes:\n      - name: jobB\n        type: command\n        config:\n          command: echo \"This is job B\"\n        dependsOn:\n          - jobA\n\n      - name: jobA\n        type: command\n        config:\n          command: echo \"This is job A\"\n```\n\n内嵌流的 DAG 图如下：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-embeded-flow.png\"/> </div>\n\n执行情况如下：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-embeded-success.png\"/> </div>\n\n\n\n## 参考资料\n\n1. [Azkaban Flow 2.0 Design](https://github.com/azkaban/azkaban/wiki/Azkaban-Flow-2.0-Design)\n2. [Getting started with Azkaban Flow 2.0](https://github.com/azkaban/azkaban/wiki/Getting-started-with-Azkaban-Flow-2.0)\n\n"
  },
  {
    "path": "大数据框架学习/Azkaban简介.md",
    "content": "# Azkaban简介\n\n\n## 一、Azkaban 介绍\n\n#### 1.1 背景\n\n一个完整的大数据分析系统，必然由很多任务单元 (如数据收集、数据清洗、数据存储、数据分析等) 组成，所有的任务单元及其之间的依赖关系组成了复杂的工作流。复杂的工作流管理涉及到很多问题：\n\n- 如何定时调度某个任务？\n- 如何在某个任务执行完成后再去执行另一个任务？\n- 如何在任务失败时候发出预警？\n- ......\n\n面对这些问题，工作流调度系统应运而生。Azkaban 就是其中之一。\n\n#### 1.2 功能\n\nAzkaban 产生于 LinkedIn，并经过多年生产环境的检验，它具备以下功能：\n\n- 兼容任何版本的 Hadoop\n- 易于使用的 Web UI\n- 可以使用简单的 Web 页面进行工作流上传\n- 支持按项目进行独立管理\n- 定时任务调度\n- 模块化和可插入\n- 身份验证和授权\n- 跟踪用户操作\n- 支持失败和成功的电子邮件提醒\n- SLA 警报和自动查杀失败任务\n- 重试失败的任务\n\nAzkaban 的设计理念是在保证功能实现的基础上兼顾易用性，其页面风格清晰明朗，下面是其 WEB UI 界面：\n\n<div align=\"center\"> <img  src=\"../pictures/azkaban-web.png\"/> </div>\n\n## 二、Azkaban 和 Oozie\n\nAzkaban 和 Oozie 都是目前使用最为广泛的工作流调度程序，其主要区别如下：\n\n#### 功能对比\n\n- 两者均可以调度 Linux 命令、MapReduce、Spark、Pig、Java、Hive 等工作流任务；\n- 两者均可以定时执行工作流任务。\n\n#### 工作流定义\n\n- Azkaban 使用 Properties(Flow 1.0) 和 YAML(Flow 2.0) 文件定义工作流；\n- Oozie 使用 Hadoop 流程定义语言（hadoop process defination language，HPDL）来描述工作流，HPDL 是一种 XML 流程定义语言。\n\n#### 资源管理\n\n- Azkaban 有较严格的权限控制，如用户对工作流进行读/写/执行等操作；\n- Oozie 暂无严格的权限控制。\n\n#### 运行模式\n\n+ Azkaban 3.x 提供了两种运行模式：\n  + **solo server model(单服务模式)** ：元数据默认存放在内置的 H2 数据库（可以修改为 MySQL），该模式中 `webServer`(管理服务器) 和 `executorServer`(执行服务器) 运行在同一个进程中，进程名是 `AzkabanSingleServer`。该模式适用于小规模工作流的调度。\n  + **multiple-executor(分布式多服务模式)** ：存放元数据的数据库为 MySQL，MySQL 应采用主从模式进行备份和容错。这种模式下 `webServer` 和 `executorServer` 在不同进程中运行，彼此之间互不影响，适合用于生产环境。\n\n+ Oozie 使用 Tomcat 等 Web 容器来展示 Web 页面，默认使用 derby 存储工作流的元数据，由于 derby 过于轻量，实际使用中通常用 MySQL 代替。\n\n\n\n\n\n## 三、总结\n\n如果你的工作流不是特别复杂，推荐使用轻量级的 Azkaban，主要有以下原因：\n\n+ **安装方面**：Azkaban 3.0 之前都是提供安装包的，直接解压部署即可。Azkaban 3.0 之后的版本需要编译，这个编译是基于 gradle 的，自动化程度比较高；\n+ **页面设计**：所有任务的依赖关系、执行结果、执行日志都可以从界面上直观查看到；\n+ **配置方面**：Azkaban Flow 1.0 基于 Properties 文件来定义工作流，这个时候的限制可能会多一点。但是在 Flow 2.0 就支持了 YARM。YARM 语法更加灵活简单，著名的微服务框架 Spring Boot 就采用的 YAML 代替了繁重的 XML。\n\n \n"
  },
  {
    "path": "大数据框架学习/Flink_Data_Sink.md",
    "content": "# Flink Sink\n<nav>\n<a href=\"#一Data-Sinks\">一、Data Sinks</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-writeAsText\">1.1 writeAsText</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-writeAsCsv\">1.2 writeAsCsv</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-print--printToErr\">1.3 print  printToErr</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-writeUsingOutputFormat\">1.4 writeUsingOutputFormat</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-writeToSocket\">1.5 writeToSocket</a><br/>\n<a href=\"#二Streaming-Connectors\">二、Streaming Connectors</a><br/>\n<a href=\"#三整合-Kafka-Sink\">三、整合 Kafka Sink</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-addSink\">3.1 addSink</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-创建输出主题\">3.2 创建输出主题</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-启动消费者\">3.3 启动消费者</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-测试结果\">3.4 测试结果</a><br/>\n<a href=\"#四自定义-Sink\">四、自定义 Sink</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-导入依赖\">4.1 导入依赖</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-自定义-Sink\">4.2 自定义 Sink</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-使用自定义-Sink\">4.3 使用自定义 Sink</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44-测试结果\">4.4 测试结果</a><br/>\n</nav>\n\n\n\n## 一、Data Sinks\n\n在使用 Flink 进行数据处理时，数据经 Data Source 流入，然后通过系列 Transformations 的转化，最终可以通过 Sink 将计算结果进行输出，Flink Data Sinks 就是用于定义数据流最终的输出位置。Flink 提供了几个较为简单的 Sink API 用于日常的开发，具体如下：\n\n### 1.1 writeAsText\n\n`writeAsText` 用于将计算结果以文本的方式并行地写入到指定文件夹下，除了路径参数是必选外，该方法还可以通过指定第二个参数来定义输出模式，它有以下两个可选值：\n\n+ **WriteMode.NO_OVERWRITE**：当指定路径上不存在任何文件时，才执行写出操作；\n+ **WriteMode.OVERWRITE**：不论指定路径上是否存在文件，都执行写出操作；如果原来已有文件，则进行覆盖。\n\n使用示例如下：\n\n```java\n streamSource.writeAsText(\"D:\\\\out\", FileSystem.WriteMode.OVERWRITE);\n```\n\n以上写出是以并行的方式写出到多个文件，如果想要将输出结果全部写出到一个文件，需要设置其并行度为 1：\n\n```java\nstreamSource.writeAsText(\"D:\\\\out\", FileSystem.WriteMode.OVERWRITE).setParallelism(1);\n```\n\n### 1.2 writeAsCsv\n\n`writeAsCsv` 用于将计算结果以 CSV 的文件格式写出到指定目录，除了路径参数是必选外，该方法还支持传入输出模式，行分隔符，和字段分隔符三个额外的参数，其方法定义如下：\n\n```java\nwriteAsCsv(String path, WriteMode writeMode, String rowDelimiter, String fieldDelimiter) \n```\n\n### 1.3 print \\ printToErr\n\n`print \\ printToErr` 是测试当中最常用的方式，用于将计算结果以标准输出流或错误输出流的方式打印到控制台上。\n\n### 1.4 writeUsingOutputFormat\n\n采用自定义的输出格式将计算结果写出，上面介绍的 `writeAsText` 和 `writeAsCsv` 其底层调用的都是该方法，源码如下：\n\n```java\npublic DataStreamSink<T> writeAsText(String path, WriteMode writeMode) {\n    TextOutputFormat<T> tof = new TextOutputFormat<>(new Path(path));\n    tof.setWriteMode(writeMode);\n    return writeUsingOutputFormat(tof);\n}\n```\n\n### 1.5 writeToSocket\n\n`writeToSocket` 用于将计算结果以指定的格式写出到 Socket 中，使用示例如下：\n\n```shell\nstreamSource.writeToSocket(\"192.168.0.226\", 9999, new SimpleStringSchema());\n```\n\n\n\n## 二、Streaming Connectors\n\n除了上述 API 外，Flink 中还内置了系列的 Connectors 连接器，用于将计算结果输入到常用的存储系统或者消息中间件中，具体如下：\n\n- Apache Kafka (支持 source 和 sink)\n- Apache Cassandra (sink)\n- Amazon Kinesis Streams (source/sink)\n- Elasticsearch (sink)\n- Hadoop FileSystem (sink)\n- RabbitMQ (source/sink)\n- Apache NiFi (source/sink)\n- Google PubSub (source/sink)\n\n除了内置的连接器外，你还可以通过 Apache Bahir 的连接器扩展 Flink。Apache Bahir 旨在为分布式数据分析系统 (如 Spark，Flink) 等提供功能上的扩展，当前其支持的与 Flink Sink 相关的连接器如下：\n\n- Apache ActiveMQ (source/sink)\n- Apache Flume (sink)\n- Redis (sink)\n- Akka (sink)\n\n这里接着在 Data Sources 章节介绍的整合 Kafka Source 的基础上，将 Kafka Sink 也一并进行整合，具体步骤如下。\n\n\n\n## 三、整合 Kafka Sink\n\n### 3.1 addSink\n\nFlink 提供了 addSink 方法用来调用自定义的 Sink 或者第三方的连接器，想要将计算结果写出到 Kafka，需要使用该方法来调用 Kafka 的生产者 FlinkKafkaProducer，具体代码如下：\n\n```java\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n\n// 1.指定Kafka的相关配置属性\nProperties properties = new Properties();\nproperties.setProperty(\"bootstrap.servers\", \"192.168.200.0:9092\");\n\n// 2.接收Kafka上的数据\nDataStream<String> stream = env\n    .addSource(new FlinkKafkaConsumer<>(\"flink-stream-in-topic\", new SimpleStringSchema(), properties));\n\n// 3.定义计算结果到 Kafka ProducerRecord 的转换\nKafkaSerializationSchema<String> kafkaSerializationSchema = new KafkaSerializationSchema<String>() {\n    @Override\n    public ProducerRecord<byte[], byte[]> serialize(String element, @Nullable Long timestamp) {\n        return new ProducerRecord<>(\"flink-stream-out-topic\", element.getBytes());\n    }\n};\n// 4. 定义Flink Kafka生产者\nFlinkKafkaProducer<String> kafkaProducer = new FlinkKafkaProducer<>(\"flink-stream-out-topic\",\n                                                                    kafkaSerializationSchema,\n                                                                    properties,\n                                               FlinkKafkaProducer.Semantic.AT_LEAST_ONCE, 5);\n// 5. 将接收到输入元素*2后写出到Kafka\nstream.map((MapFunction<String, String>) value -> value + value).addSink(kafkaProducer);\nenv.execute(\"Flink Streaming\");\n```\n\n### 3.2 创建输出主题\n\n创建用于输出测试的主题：\n\n```shell\nbin/kafka-topics.sh --create \\\n                    --bootstrap-server hadoop001:9092 \\\n                    --replication-factor 1 \\\n                    --partitions 1  \\\n                    --topic flink-stream-out-topic\n\n# 查看所有主题\n bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092\n```\n\n### 3.3 启动消费者\n\n启动一个 Kafka 消费者，用于查看 Flink 程序的输出情况：\n\n```java\nbin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic flink-stream-out-topic\n```\n\n### 3.4 测试结果\n\n在 Kafka 生产者上发送消息到 Flink 程序，观察 Flink 程序转换后的输出情况，具体如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-kafka-producer-consumer.png\"/> </div>\n\n\n可以看到 Kafka 生成者发出的数据已经被 Flink 程序正常接收到，并经过转换后又输出到 Kafka 对应的 Topic 上。\n\n## 四、自定义 Sink\n\n除了使用内置的第三方连接器外，Flink 还支持使用自定义的 Sink 来满足多样化的输出需求。想要实现自定义的 Sink ，需要直接或者间接实现 SinkFunction 接口。通常情况下，我们都是实现其抽象类 RichSinkFunction，相比于 SinkFunction ，其提供了更多的与生命周期相关的方法。两者间的关系如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-richsink.png\"/> </div>\n\n\n这里我们以自定义一个 FlinkToMySQLSink 为例，将计算结果写出到 MySQL 数据库中，具体步骤如下：\n\n### 4.1 导入依赖\n\n首先需要导入 MySQL 相关的依赖：\n\n```xml\n<dependency>\n    <groupId>mysql</groupId>\n    <artifactId>mysql-connector-java</artifactId>\n    <version>8.0.16</version>\n</dependency>\n```\n\n### 4.2 自定义 Sink\n\n继承自 RichSinkFunction，实现自定义的 Sink ：\n\n```java\npublic class FlinkToMySQLSink extends RichSinkFunction<Employee> {\n\n    private PreparedStatement stmt;\n    private Connection conn;\n\n    @Override\n    public void open(Configuration parameters) throws Exception {\n        Class.forName(\"com.mysql.cj.jdbc.Driver\");\n        conn = DriverManager.getConnection(\"jdbc:mysql://192.168.0.229:3306/employees\" +\n                                           \"?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false\", \n                                           \"root\", \n                                           \"123456\");\n        String sql = \"insert into emp(name, age, birthday) values(?, ?, ?)\";\n        stmt = conn.prepareStatement(sql);\n    }\n\n    @Override\n    public void invoke(Employee value, Context context) throws Exception {\n        stmt.setString(1, value.getName());\n        stmt.setInt(2, value.getAge());\n        stmt.setDate(3, value.getBirthday());\n        stmt.executeUpdate();\n    }\n\n    @Override\n    public void close() throws Exception {\n        super.close();\n        if (stmt != null) {\n            stmt.close();\n        }\n        if (conn != null) {\n            conn.close();\n        }\n    }\n\n}\n```\n\n### 4.3 使用自定义 Sink\n\n想要使用自定义的 Sink，同样是需要调用 addSink 方法，具体如下：\n\n```java\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\nDate date = new Date(System.currentTimeMillis());\nDataStreamSource<Employee> streamSource = env.fromElements(\n    new Employee(\"hei\", 10, date),\n    new Employee(\"bai\", 20, date),\n    new Employee(\"ying\", 30, date));\nstreamSource.addSink(new FlinkToMySQLSink());\nenv.execute();\n```\n\n### 4.4 测试结果\n\n启动程序，观察数据库写入情况：\n\n<div align=\"center\"> <img src=\"../pictures/flink-mysql-sink.png\"/> </div>\n\n\n数据库成功写入，代表自定义 Sink 整合成功。\n\n> 以上所有用例的源码见本仓库：[flink-kafka-integration]( https://github.com/heibaiying/BigData-Notes/tree/master/code/Flink/flink-kafka-integration)\n\n\n\n## 参考资料\n\n1. data-sinks： https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/datastream_api.html#data-sinks \n2. Streaming Connectors：https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/index.html\n3. Apache Kafka Connector： https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/kafka.html \n\n"
  },
  {
    "path": "大数据框架学习/Flink_Data_Source.md",
    "content": "# Flink Data Source\n<nav>\n<a href=\"#一内置-Data-Source\">一、内置 Data Source</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-基于文件构建\">1.1 基于文件构建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-基于集合构建\">1.2 基于集合构建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13--基于-Socket-构建\">1.3  基于 Socket 构建</a><br/>\n<a href=\"#二自定义-Data-Source\">二、自定义 Data Source</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-SourceFunction\">2.1 SourceFunction</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-ParallelSourceFunction-和-RichParallelSourceFunction\">2.2 ParallelSourceFunction 和 RichParallelSourceFunction</a><br/>\n<a href=\"#三Streaming-Connectors\">三、Streaming Connectors</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-内置连接器\">3.1 内置连接器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-整合-Kakfa\">3.2 整合 Kakfa</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-整合测试\">3.3 整合测试</a><br/>\n</nav>\n\n\n\n## 一、内置 Data Source\n\nFlink Data Source 用于定义 Flink 程序的数据来源，Flink 官方提供了多种数据获取方法，用于帮助开发者简单快速地构建输入流，具体如下：\n\n### 1.1 基于文件构建\n\n**1. readTextFile(path)**：按照 TextInputFormat 格式读取文本文件，并将其内容以字符串的形式返回。示例如下：\n\n```java\nenv.readTextFile(filePath).print();\n```\n\n**2. readFile(fileInputFormat, path)** ：按照指定格式读取文件。\n\n**3. readFile(inputFormat, filePath, watchType, interval, typeInformation)**：按照指定格式周期性的读取文件。其中各个参数的含义如下：\n\n+ **inputFormat**：数据流的输入格式。\n+ **filePath**：文件路径，可以是本地文件系统上的路径，也可以是 HDFS 上的文件路径。\n+ **watchType**：读取方式，它有两个可选值，分别是 `FileProcessingMode.PROCESS_ONCE` 和 `FileProcessingMode.PROCESS_CONTINUOUSLY`：前者表示对指定路径上的数据只读取一次，然后退出；后者表示对路径进行定期地扫描和读取。需要注意的是如果 watchType 被设置为 `PROCESS_CONTINUOUSLY`，那么当文件被修改时，其所有的内容 (包含原有的内容和新增的内容) 都将被重新处理，因此这会打破 Flink 的 *exactly-once* 语义。\n+ **interval**：定期扫描的时间间隔。\n+ **typeInformation**：输入流中元素的类型。\n\n使用示例如下：\n\n```java\nfinal String filePath = \"D:\\\\log4j.properties\";\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\nenv.readFile(new TextInputFormat(new Path(filePath)),\n             filePath,\n             FileProcessingMode.PROCESS_ONCE,\n             1,\n             BasicTypeInfo.STRING_TYPE_INFO).print();\nenv.execute();\n```\n\n### 1.2 基于集合构建\n\n**1. fromCollection(Collection)**：基于集合构建，集合中的所有元素必须是同一类型。示例如下：\n\n```java\nenv.fromCollection(Arrays.asList(1,2,3,4,5)).print();\n```\n\n**2. fromElements(T ...)**： 基于元素构建，所有元素必须是同一类型。示例如下：\n\n```java\nenv.fromElements(1,2,3,4,5).print();\n```\n**3. generateSequence(from, to)**：基于给定的序列区间进行构建。示例如下：\n\n```java\nenv.generateSequence(0,100);\n```\n\n**4. fromCollection(Iterator, Class)**：基于迭代器进行构建。第一个参数用于定义迭代器，第二个参数用于定义输出元素的类型。使用示例如下：\n\n```java\nenv.fromCollection(new CustomIterator(), BasicTypeInfo.INT_TYPE_INFO).print();\n```\n\n其中 CustomIterator 为自定义的迭代器，这里以产生 1 到 100 区间内的数据为例，源码如下。需要注意的是自定义迭代器除了要实现 Iterator 接口外，还必须要实现序列化接口 Serializable ，否则会抛出序列化失败的异常：\n\n```java\nimport java.io.Serializable;\nimport java.util.Iterator;\n\npublic class CustomIterator implements Iterator<Integer>, Serializable {\n    private Integer i = 0;\n\n    @Override\n    public boolean hasNext() {\n        return i < 100;\n    }\n\n    @Override\n    public Integer next() {\n        i++;\n        return i;\n    }\n}\n```\n\n**5. fromParallelCollection(SplittableIterator, Class)**：方法接收两个参数，第二个参数用于定义输出元素的类型，第一个参数 SplittableIterator 是迭代器的抽象基类，它用于将原始迭代器的值拆分到多个不相交的迭代器中。\n\n### 1.3  基于 Socket 构建\n\nFlink 提供了 socketTextStream 方法用于构建基于 Socket 的数据流，socketTextStream 方法有以下四个主要参数：\n\n- **hostname**：主机名；\n- **port**：端口号，设置为 0 时，表示端口号自动分配；\n- **delimiter**：用于分隔每条记录的分隔符；\n- **maxRetry**：当 Socket 临时关闭时，程序的最大重试间隔，单位为秒。设置为 0 时表示不进行重试；设置为负值则表示一直重试。示例如下：\n\n```shell\n env.socketTextStream(\"192.168.0.229\", 9999, \"\\n\", 3).print();\n```\n\n\n\n## 二、自定义 Data Source\n\n### 2.1 SourceFunction\n\n除了内置的数据源外，用户还可以使用 `addSource` 方法来添加自定义的数据源。自定义的数据源必须要实现 SourceFunction 接口，这里以产生 [0 , 1000) 区间内的数据为例，代码如下：\n\n```java\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n\nenv.addSource(new SourceFunction<Long>() {\n    \n    private long count = 0L;\n    private volatile boolean isRunning = true;\n\n    public void run(SourceContext<Long> ctx) {\n        while (isRunning && count < 1000) {\n            // 通过collect将输入发送出去 \n            ctx.collect(count);\n            count++;\n        }\n    }\n\n    public void cancel() {\n        isRunning = false;\n    }\n\n}).print();\nenv.execute();\n```\n\n### 2.2 ParallelSourceFunction 和 RichParallelSourceFunction\n\n上面通过 SourceFunction 实现的数据源是不具有并行度的，即不支持在得到的 DataStream 上调用 `setParallelism(n)` 方法，此时会抛出如下的异常：\n\n```shell\nException in thread \"main\" java.lang.IllegalArgumentException: Source: 1 is not a parallel source\n```\n\n如果你想要实现具有并行度的输入流，则需要实现 ParallelSourceFunction 或 RichParallelSourceFunction 接口，其与 SourceFunction 的关系如下图： \n\n<div align=\"center\"> <img src=\"../pictures/flink-RichParallelSourceFunction.png\"/> </div>\nParallelSourceFunction 直接继承自 ParallelSourceFunction，具有并行度的功能。RichParallelSourceFunction 则继承自 AbstractRichFunction，同时实现了 ParallelSourceFunction 接口，所以其除了具有并行度的功能外，还提供了额外的与生命周期相关的方法，如 open() ，closen() 。\n\n## 三、Streaming Connectors\n\n### 3.1 内置连接器\n\n除了自定义数据源外， Flink 还内置了多种连接器，用于满足大多数的数据收集场景。当前内置连接器的支持情况如下：\n\n- Apache Kafka (支持 source 和 sink)\n- Apache Cassandra (sink)\n- Amazon Kinesis Streams (source/sink)\n- Elasticsearch (sink)\n- Hadoop FileSystem (sink)\n- RabbitMQ (source/sink)\n- Apache NiFi (source/sink)\n- Twitter Streaming API (source)\n- Google PubSub (source/sink)\n\n除了上述的连接器外，你还可以通过 Apache Bahir 的连接器扩展 Flink。Apache Bahir 旨在为分布式数据分析系统 (如 Spark，Flink) 等提供功能上的扩展，当前其支持的与 Flink 相关的连接器如下：\n\n- Apache ActiveMQ (source/sink)\n- Apache Flume (sink)\n- Redis (sink)\n- Akka (sink)\n- Netty (source)\n\n随着 Flink 的不断发展，可以预见到其会支持越来越多类型的连接器，关于连接器的后续发展情况，可以查看其官方文档：[Streaming Connectors]( https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/index.html) 。在所有 DataSource 连接器中，使用的广泛的就是 Kafka，所以这里我们以其为例，来介绍 Connectors 的整合步骤。\n\n### 3.2 整合 Kakfa\n\n#### 1. 导入依赖\n\n整合 Kafka 时，一定要注意所使用的 Kafka 的版本，不同版本间所需的 Maven 依赖和开发时所调用的类均不相同，具体如下：\n\n| Maven 依赖                      | Flink 版本 | Consumer and Producer 类的名称                   | Kafka 版本 |\n| :------------------------------ | :--------- | :----------------------------------------------- | :--------- |\n| flink-connector-kafka-0.8_2.11  | 1.0.0 +    | FlinkKafkaConsumer08 <br/>FlinkKafkaProducer08   | 0.8.x      |\n| flink-connector-kafka-0.9_2.11  | 1.0.0 +    | FlinkKafkaConsumer09<br/> FlinkKafkaProducer09   | 0.9.x      |\n| flink-connector-kafka-0.10_2.11 | 1.2.0 +    | FlinkKafkaConsumer010 <br/>FlinkKafkaProducer010 | 0.10.x     |\n| flink-connector-kafka-0.11_2.11 | 1.4.0 +    | FlinkKafkaConsumer011 <br/>FlinkKafkaProducer011 | 0.11.x     |\n| flink-connector-kafka_2.11      | 1.7.0 +    | FlinkKafkaConsumer <br/>FlinkKafkaProducer       | >= 1.0.0   |\n\n这里我使用的 Kafka 版本为 kafka_2.12-2.2.0，添加的依赖如下：\n\n```xml\n<dependency>\n    <groupId>org.apache.flink</groupId>\n    <artifactId>flink-connector-kafka_2.11</artifactId>\n    <version>1.9.0</version>\n</dependency>\n```\n\n#### 2. 代码开发\n\n这里以最简单的场景为例，接收 Kafka 上的数据并打印，代码如下：\n\n```java\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\nProperties properties = new Properties();\n// 指定Kafka的连接位置\nproperties.setProperty(\"bootstrap.servers\", \"hadoop001:9092\");\n// 指定监听的主题，并定义Kafka字节消息到Flink对象之间的转换规则\nDataStream<String> stream = env\n    .addSource(new FlinkKafkaConsumer<>(\"flink-stream-in-topic\", new SimpleStringSchema(), properties));\nstream.print();\nenv.execute(\"Flink Streaming\");\n```\n\n### 3.3 整合测试\n\n#### 1. 启动 Kakfa\n\nKafka 的运行依赖于 zookeeper，需要预先启动，可以启动 Kafka 内置的 zookeeper，也可以启动自己安装的：\n\n```shell\n# zookeeper启动命令\nbin/zkServer.sh start\n\n# 内置zookeeper启动命令\nbin/zookeeper-server-start.sh config/zookeeper.properties\n```\n\n启动单节点 kafka 用于测试：\n\n```shell\n# bin/kafka-server-start.sh config/server.properties\n```\n\n#### 2. 创建 Topic\n\n```shell\n# 创建用于测试主题\nbin/kafka-topics.sh --create \\\n                    --bootstrap-server hadoop001:9092 \\\n                    --replication-factor 1 \\\n                    --partitions 1  \\\n                    --topic flink-stream-in-topic\n\n# 查看所有主题\n bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092\n```\n\n#### 3. 启动 Producer\n\n这里 启动一个 Kafka 生产者，用于发送测试数据：\n\n```shell\nbin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic flink-stream-in-topic\n```\n\n#### 4. 测试结果\n\n在 Producer 上输入任意测试数据，之后观察程序控制台的输出：\n\n<div align=\"center\"> <img src=\"../pictures/flink-kafka-datasource-producer.png\"/> </div>\n程序控制台的输出如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-kafka-datasource-console.png\"/> </div>\n可以看到已经成功接收并打印出相关的数据。\n\n\n\n## 参考资料\n\n1. data-sources：https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/datastream_api.html#data-sources \n2. Streaming Connectors：https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/index.html\n3. Apache Kafka Connector： https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/kafka.html \n"
  },
  {
    "path": "大数据框架学习/Flink_Data_Transformation.md",
    "content": "# Flink Transformation\n<nav>\n<a href=\"#一Transformations-分类\">一、Transformations 分类</a><br/>\n<a href=\"#二DataStream-Transformations\">二、DataStream Transformations</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-Map-[DataStream-→-DataStream]\">2.1 Map [DataStream → DataStream] </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-FlatMap-[DataStream-→-DataStream]\">2.2 FlatMap [DataStream → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-Filter-[DataStream-→-DataStream]\">2.3 Filter [DataStream → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-KeyBy-和-Reduce\">2.4 KeyBy 和 Reduce</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-Aggregations-[KeyedStream-→-DataStream]\">2.5 Aggregations [KeyedStream → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-Union-[DataStream*-→-DataStream]\">2.6 Union [DataStream* → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-Connect-[DataStreamDataStream-→-ConnectedStreams]\">2.7 Connect [DataStream,DataStream → ConnectedStreams]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#28-Split-和-Select\">2.8 Split 和 Select</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#29-project-[DataStream-→-DataStream]\">2.9 project [DataStream → DataStream]</a><br/>\n<a href=\"#三物理分区\">三、物理分区</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-Random-partitioning-[DataStream-→-DataStream]\">3.1 Random partitioning [DataStream → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-Rebalancing-[DataStream-→-DataStream]\">3.2 Rebalancing [DataStream → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-Rescaling-[DataStream-→-DataStream]\">3.3 Rescaling [DataStream → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-Broadcasting-[DataStream-→-DataStream]\">3.4 Broadcasting [DataStream → DataStream]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-Custom-partitioning-[DataStream-→-DataStream]\">3.5 Custom partitioning [DataStream → DataStream]</a><br/>\n<a href=\"#四任务链和资源组\">四、任务链和资源组</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-startNewChain\">4.1 startNewChain</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-disableChaining\">4.2 disableChaining</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-slotSharingGroup\">4.3 slotSharingGroup</a><br/>\n</nav>\n\n\n\n## 一、Transformations 分类\n\nFlink 的 Transformations 操作主要用于将一个和多个 DataStream 按需转换成新的 DataStream。它主要分为以下三类：\n\n- **DataStream Transformations**：进行数据流相关转换操作；\n- **Physical partitioning**：物理分区。Flink 提供的底层 API ，允许用户定义数据的分区规则；\n- **Task chaining and resource groups**：任务链和资源组。允许用户进行任务链和资源组的细粒度的控制。\n\n以下分别对其主要 API 进行介绍：\n\n## 二、DataStream Transformations\n\n### 2.1 Map [DataStream → DataStream] \n\n对一个 DataStream 中的每个元素都执行特定的转换操作：\n\n```java\nDataStream<Integer> integerDataStream = env.fromElements(1, 2, 3, 4, 5);\nintegerDataStream.map((MapFunction<Integer, Object>) value -> value * 2).print();\n// 输出 2,4,6,8,10\n```\n\n### 2.2 FlatMap [DataStream → DataStream]\n\nFlatMap 与 Map 类似，但是 FlatMap 中的一个输入元素可以被映射成一个或者多个输出元素，示例如下：\n\n```java\nString string01 = \"one one one two two\";\nString string02 = \"third third third four\";\nDataStream<String> stringDataStream = env.fromElements(string01, string02);\nstringDataStream.flatMap(new FlatMapFunction<String, String>() {\n    @Override\n    public void flatMap(String value, Collector<String> out) throws Exception {\n        for (String s : value.split(\" \")) {\n            out.collect(s);\n        }\n    }\n}).print();\n// 输出每一个独立的单词，为节省排版，这里去掉换行，后文亦同\none one one two two third third third four\n```\n\n### 2.3 Filter [DataStream → DataStream]\n\n用于过滤符合条件的数据：\n\n```java\nenv.fromElements(1, 2, 3, 4, 5).filter(x -> x > 3).print();\n```\n\n### 2.4 KeyBy 和 Reduce\n\n- **KeyBy [DataStream → KeyedStream]** ：用于将相同 Key 值的数据分到相同的分区中；\n- **Reduce [KeyedStream → DataStream]** ：用于对数据执行归约计算。\n\n如下例子将数据按照 key 值分区后，滚动进行求和计算：\n\n```java\nDataStream<Tuple2<String, Integer>> tuple2DataStream = env.fromElements(new Tuple2<>(\"a\", 1),\n                                                                        new Tuple2<>(\"a\", 2), \n                                                                        new Tuple2<>(\"b\", 3), \n                                                                        new Tuple2<>(\"b\", 5));\nKeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = tuple2DataStream.keyBy(0);\nkeyedStream.reduce((ReduceFunction<Tuple2<String, Integer>>) (value1, value2) ->\n                   new Tuple2<>(value1.f0, value1.f1 + value2.f1)).print();\n\n// 持续进行求和计算，输出：\n(a,1)\n(a,3)\n(b,3)\n(b,8)\n```\n\nKeyBy 操作存在以下两个限制：\n\n- KeyBy 操作用于用户自定义的 POJOs 类型时，该自定义类型必须重写 hashCode 方法；\n- KeyBy 操作不能用于数组类型。\n\n### 2.5 Aggregations [KeyedStream → DataStream]\n\nAggregations 是官方提供的聚合算子，封装了常用的聚合操作，如上利用 Reduce 进行求和的操作也可以利用 Aggregations 中的 sum 算子重写为下面的形式：\n\n```java\ntuple2DataStream.keyBy(0).sum(1).print();\n```\n\n除了 sum 外，Flink 还提供了 min , max , minBy，maxBy 等常用聚合算子：\n\n```java\n// 滚动计算指定key的最小值，可以通过index或者fieldName来指定key\nkeyedStream.min(0);\nkeyedStream.min(\"key\");\n// 滚动计算指定key的最大值\nkeyedStream.max(0);\nkeyedStream.max(\"key\");\n// 滚动计算指定key的最小值，并返回其对应的元素\nkeyedStream.minBy(0);\nkeyedStream.minBy(\"key\");\n// 滚动计算指定key的最大值，并返回其对应的元素\nkeyedStream.maxBy(0);\nkeyedStream.maxBy(\"key\");\n\n```\n\n### 2.6 Union [DataStream* → DataStream]\n\n用于连接两个或者多个元素类型相同的 DataStream 。当然一个 DataStream 也可以与其本生进行连接，此时该 DataStream 中的每个元素都会被获取两次：\n\n```shell\nDataStreamSource<Tuple2<String, Integer>> streamSource01 = env.fromElements(new Tuple2<>(\"a\", 1), \n                                                                            new Tuple2<>(\"a\", 2));\nDataStreamSource<Tuple2<String, Integer>> streamSource02 = env.fromElements(new Tuple2<>(\"b\", 1), \n                                                                            new Tuple2<>(\"b\", 2));\nstreamSource01.union(streamSource02);\nstreamSource01.union(streamSource01,streamSource02);\n```\n\n### 2.7 Connect [DataStream,DataStream → ConnectedStreams]\n\nConnect 操作用于连接两个或者多个类型不同的 DataStream ，其返回的类型是 ConnectedStreams ，此时被连接的多个 DataStreams 可以共享彼此之间的数据状态。但是需要注意的是由于不同 DataStream 之间的数据类型是不同的，如果想要进行后续的计算操作，还需要通过 CoMap 或 CoFlatMap 将 ConnectedStreams  转换回 DataStream：\n\n```java\nDataStreamSource<Tuple2<String, Integer>> streamSource01 = env.fromElements(new Tuple2<>(\"a\", 3), \n                                                                            new Tuple2<>(\"b\", 5));\nDataStreamSource<Integer> streamSource02 = env.fromElements(2, 3, 9);\n// 使用connect进行连接\nConnectedStreams<Tuple2<String, Integer>, Integer> connect = streamSource01.connect(streamSource02);\nconnect.map(new CoMapFunction<Tuple2<String, Integer>, Integer, Integer>() {\n    @Override\n    public Integer map1(Tuple2<String, Integer> value) throws Exception {\n        return value.f1;\n    }\n\n    @Override\n    public Integer map2(Integer value) throws Exception {\n        return value;\n    }\n}).map(x -> x * 100).print();\n\n// 输出：\n300 500 200 900 300\n```\n\n### 2.8 Split 和 Select\n\n- **Split [DataStream → SplitStream]**：用于将一个 DataStream 按照指定规则进行拆分为多个 DataStream，需要注意的是这里进行的是逻辑拆分，即 Split 只是将数据贴上不同的类型标签，但最终返回的仍然只是一个 SplitStream；\n- **Select [SplitStream → DataStream]**：想要从逻辑拆分的 SplitStream 中获取真实的不同类型的 DataStream，需要使用 Select 算子，示例如下：\n\n```java\nDataStreamSource<Integer> streamSource = env.fromElements(1, 2, 3, 4, 5, 6, 7, 8);\n// 标记\nSplitStream<Integer> split = streamSource.split(new OutputSelector<Integer>() {\n    @Override\n    public Iterable<String> select(Integer value) {\n        List<String> output = new ArrayList<String>();\n        output.add(value % 2 == 0 ? \"even\" : \"odd\");\n        return output;\n    }\n});\n// 获取偶数数据集\nsplit.select(\"even\").print();\n// 输出 2,4,6,8\n```\n\n### 2.9 project [DataStream → DataStream]\n\nproject 主要用于获取 tuples 中的指定字段集，示例如下：\n\n```java\nDataStreamSource<Tuple3<String, Integer, String>> streamSource = env.fromElements(\n                                                                         new Tuple3<>(\"li\", 22, \"2018-09-23\"),\n                                                                         new Tuple3<>(\"ming\", 33, \"2020-09-23\"));\nstreamSource.project(0,2).print();\n\n// 输出\n(li,2018-09-23)\n(ming,2020-09-23)\n```\n\n## 三、物理分区\n\n物理分区 (Physical partitioning) 是 Flink 提供的底层的 API，允许用户采用内置的分区规则或者自定义的分区规则来对数据进行分区，从而避免数据在某些分区上过于倾斜，常用的分区规则如下：\n\n### 3.1 Random partitioning [DataStream → DataStream]\n\n随机分区 (Random partitioning) 用于随机的将数据分布到所有下游分区中，通过 shuffle 方法来进行实现：\n\n```java\ndataStream.shuffle();\n```\n\n### 3.2 Rebalancing [DataStream → DataStream]\n\nRebalancing 采用轮询的方式将数据进行分区，其适合于存在数据倾斜的场景下，通过 rebalance 方法进行实现：  \n\n```java\ndataStream.rebalance();\n```\n\n### 3.3 Rescaling [DataStream → DataStream]\n\n当采用 Rebalancing 进行分区平衡时，其实现的是全局性的负载均衡，数据会通过网络传输到其他节点上并完成分区数据的均衡。 而 Rescaling 则是低配版本的 rebalance，它不需要额外的网络开销，它只会对上下游的算子之间进行重新均衡，通过 rescale 方法进行实现：\n\n```java\ndataStream.rescale();\n```\n\nReScale 这个单词具有重新缩放的意义，其对应的操作也是如此，具体如下：如果上游 operation 并行度为 2，而下游的 operation 并行度为 6，则其中 1 个上游的 operation 会将元素分发到 3 个下游 operation，另 1 个上游 operation 则会将元素分发到另外 3 个下游 operation。反之亦然，如果上游的 operation 并行度为 6，而下游 operation 并行度为 2，则其中 3 个上游 operation 会将元素分发到 1 个下游 operation，另 3 个上游 operation 会将元素分发到另外 1 个下游operation：\n\n<div align=\"center\"> <img src=\"../pictures/flink-Rescaling.png\"/> </div>\n\n\n### 3.4 Broadcasting [DataStream → DataStream]\n\n将数据分发到所有分区上。通常用于小数据集与大数据集进行关联的情况下，此时可以将小数据集广播到所有分区上，避免频繁的跨分区关联，通过 broadcast 方法进行实现：\n\n```java\ndataStream.broadcast();\n```\n\n### 3.5 Custom partitioning [DataStream → DataStream]\n\nFlink 运行用户采用自定义的分区规则来实现分区，此时需要通过实现 Partitioner 接口来自定义分区规则，并指定对应的分区键，示例如下：\n\n```java\n DataStreamSource<Tuple2<String, Integer>> streamSource = env.fromElements(new Tuple2<>(\"Hadoop\", 1),\n                new Tuple2<>(\"Spark\", 1),\n                new Tuple2<>(\"Flink-streaming\", 2),\n                new Tuple2<>(\"Flink-batch\", 4),\n                new Tuple2<>(\"Storm\", 4),\n                new Tuple2<>(\"HBase\", 3));\nstreamSource.partitionCustom(new Partitioner<String>() {\n    @Override\n    public int partition(String key, int numPartitions) {\n        // 将第一个字段包含flink的Tuple2分配到同一个分区\n        return key.toLowerCase().contains(\"flink\") ? 0 : 1;\n    }\n}, 0).print();\n\n\n// 输出如下：\n1> (Flink-streaming,2)\n1> (Flink-batch,4)\n2> (Hadoop,1)\n2> (Spark,1)\n2> (Storm,4)\n2> (HBase,3)\n```\n\n\n\n## 四、任务链和资源组\n\n任务链和资源组 ( Task chaining and resource groups ) 也是 Flink 提供的底层 API，用于控制任务链和资源分配。默认情况下，如果操作允许 (例如相邻的两次 map 操作) ，则 Flink 会尝试将它们在同一个线程内进行，从而可以获取更好的性能。但是 Flink 也允许用户自己来控制这些行为，这就是任务链和资源组 API：\n\n### 4.1 startNewChain\n\nstartNewChain 用于基于当前 operation 开启一个新的任务链。如下所示，基于第一个 map 开启一个新的任务链，此时前一个 map 和 后一个 map 将处于同一个新的任务链中，但它们与 filter 操作则分别处于不同的任务链中：\n\n```java\nsomeStream.filter(...).map(...).startNewChain().map(...);\n```\n\n### 4.2 disableChaining\n\n disableChaining 操作用于禁止将其他操作与当前操作放置于同一个任务链中，示例如下：\n\n```java\nsomeStream.map(...).disableChaining();\n```\n\n### 4.3 slotSharingGroup\n\nslot 是任务管理器  (TaskManager) 所拥有资源的固定子集，每个操作 (operation) 的子任务 (sub task) 都需要获取 slot 来执行计算，但每个操作所需要资源的大小都是不相同的，为了更好地利用资源，Flink 允许不同操作的子任务被部署到同一 slot 中。slotSharingGroup 用于设置操作的 slot 共享组 (slot sharing group) ，Flink 会将具有相同 slot 共享组的操作放到同一个 slot 中 。示例如下：\n\n```java\nsomeStream.filter(...).slotSharingGroup(\"slotSharingGroupName\");\n```\n\n\n\n## 参考资料\n\nFlink Operators： https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/operators/ \n"
  },
  {
    "path": "大数据框架学习/Flink_Windows.md",
    "content": "# Flink Windows\n<nav>\n<a href=\"#一窗口概念\">一、窗口概念</a><br/>\n<a href=\"#二Time-Windows\">二、Time Windows</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-Tumbling-Windows\">2.1 Tumbling Windows</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-Sliding-Windows\">2.2 Sliding Windows</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-Session-Windows\">2.3 Session Windows</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-Global-Windows\">2.4 Global Windows</a><br/>\n<a href=\"#三Count-Windows\">三、Count Windows</a><br/>\n</nav>\n\n\n\n## 一、窗口概念\n\n在大多数场景下，我们需要统计的数据流都是无界的，因此我们无法等待整个数据流终止后才进行统计。通常情况下，我们只需要对某个时间范围或者数量范围内的数据进行统计分析：如每隔五分钟统计一次过去一小时内所有商品的点击量；或者每发生1000次点击后，都去统计一下每个商品点击率的占比。在 Flink 中，我们使用窗口 (Window) 来实现这类功能。按照统计维度的不同，Flink 中的窗口可以分为 时间窗口 (Time Windows) 和 计数窗口 (Count Windows) 。\n\n## 二、Time Windows\n\nTime Windows 用于以时间为维度来进行数据聚合，具体分为以下四类：\n\n### 2.1 Tumbling Windows\n\n滚动窗口 (Tumbling Windows) 是指彼此之间没有重叠的窗口。例如：每隔1小时统计过去1小时内的商品点击量，那么 1 天就只能分为 24 个窗口，每个窗口彼此之间是不存在重叠的，具体如下：\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/flink-tumbling-windows.png\"/> </div>\n\n\n这里我们以词频统计为例，给出一个具体的用例，代码如下：\n\n```java\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n// 接收socket上的数据输入\nDataStreamSource<String> streamSource = env.socketTextStream(\"hadoop001\", 9999, \"\\n\", 3);\nstreamSource.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {\n    @Override\n    public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {\n        String[] words = value.split(\"\\t\");\n        for (String word : words) {\n            out.collect(new Tuple2<>(word, 1L));\n        }\n    }\n}).keyBy(0).timeWindow(Time.seconds(3)).sum(1).print(); //每隔3秒统计一次每个单词出现的数量\nenv.execute(\"Flink Streaming\");\n```\n\n测试结果如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-window-word-count.png\"/> </div>\n\n\n\n\n### 2.2 Sliding Windows\n\n滑动窗口用于滚动进行聚合分析，例如：每隔 6 分钟统计一次过去一小时内所有商品的点击量，那么统计窗口彼此之间就是存在重叠的，即 1天可以分为 240 个窗口。图示如下：\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/flink-sliding-windows.png\"/> </div>\n\n\n可以看到 window 1 - 4 这四个窗口彼此之间都存在着时间相等的重叠部分。想要实现滑动窗口，只需要在使用 timeWindow 方法时额外传递第二个参数作为滚动时间即可，具体如下：\n\n```java\n// 每隔3秒统计一次过去1分钟内的数据\ntimeWindow(Time.minutes(1),Time.seconds(3))\n```\n\n### 2.3 Session Windows\n\n当用户在进行持续浏览时，可能每时每刻都会有点击数据，例如在活动区间内，用户可能频繁的将某类商品加入和移除购物车，而你只想知道用户本次浏览最终的购物车情况，此时就可以在用户持有的会话结束后再进行统计。想要实现这类统计，可以通过 Session Windows 来进行实现。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/flink-session-windows.png\"/> </div>\n\n\n具体的实现代码如下：\n\n```java\n// 以处理时间为衡量标准，如果10秒内没有任何数据输入，就认为会话已经关闭，此时触发统计\nwindow(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))\n// 以事件时间为衡量标准    \nwindow(EventTimeSessionWindows.withGap(Time.seconds(10)))\n```\n\n### 2.4 Global Windows\n\n最后一个窗口是全局窗口， 全局窗口会将所有 key 相同的元素分配到同一个窗口中，其通常配合触发器 (trigger) 进行使用。如果没有相应触发器，则计算将不会被执行。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/flink-non-windowed.png\"/> </div>\n\n\n这里继续以上面词频统计的案例为例，示例代码如下：\n\n```java\n// 当单词累计出现的次数每达到10次时，则触发计算，计算整个窗口内该单词出现的总数\nwindow(GlobalWindows.create()).trigger(CountTrigger.of(10)).sum(1).print();\n```\n\n## 三、Count Windows\n\nCount Windows 用于以数量为维度来进行数据聚合，同样也分为滚动窗口和滑动窗口，实现方式也和时间窗口完全一致，只是调用的 API 不同，具体如下：\n\n```java\n// 滚动计数窗口，每1000次点击则计算一次\ncountWindow(1000)\n// 滑动计数窗口，每10次点击发生后，则计算过去1000次点击的情况\ncountWindow(1000,10)\n```\n\n实际上计数窗口内部就是调用的我们上一部分介绍的全局窗口来实现的，其源码如下：\n\n```java\npublic WindowedStream<T, KEY, GlobalWindow> countWindow(long size) {\n    return window(GlobalWindows.create()).trigger(PurgingTrigger.of(CountTrigger.of(size)));\n}\n\n\npublic WindowedStream<T, KEY, GlobalWindow> countWindow(long size, long slide) {\n    return window(GlobalWindows.create())\n        .evictor(CountEvictor.of(size))\n        .trigger(CountTrigger.of(slide));\n}\n```\n\n\n\n## 参考资料\n\nFlink Windows： https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/operators/windows.html \n"
  },
  {
    "path": "大数据框架学习/Flink开发环境搭建.md",
    "content": "# Flink 开发环境搭建\n\n<nav>\n<a href=\"#一安装-Scala-插件\">一、安装 Scala 插件</a><br/>\n<a href=\"#二Flink-项目初始化\">二、Flink 项目初始化</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-使用官方脚本构建\">2.1 使用官方脚本构建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-使用-IDEA-构建\">2.2 使用 IDEA 构建</a><br/>\n<a href=\"#三项目结构\">三、项目结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-项目结构\">3.1 项目结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-主要依赖\">3.2 主要依赖</a><br/>\n<a href=\"#四词频统计案例\">四、词频统计案例</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-批处理示例\">4.1 批处理示例</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-流处理示例\">4.2 流处理示例</a><br/>\n<a href=\"#五使用-Scala-Shell\">五、使用 Scala Shell</a><br/>\n</nav>\n\n\n\n## 一、安装 Scala 插件\n\nFlink 分别提供了基于 Java 语言和 Scala 语言的 API ，如果想要使用 Scala 语言来开发 Flink 程序，可以通过在 IDEA 中安装 Scala 插件来提供语法提示，代码高亮等功能。打开 IDEA , 依次点击 `File => settings => plugins` 打开插件安装页面，搜索 Scala 插件并进行安装，安装完成后，重启 IDEA 即可生效。  \n\n<div align=\"center\"> <img src=\"../pictures/scala-plugin.png\"/> </div>\n\n## 二、Flink 项目初始化\n\n### 2.1 使用官方脚本构建\n\nFlink 官方支持使用 Maven 和 Gradle 两种构建工具来构建基于 Java 语言的 Flink 项目；支持使用 SBT 和 Maven 两种构建工具来构建基于 Scala 语言的 Flink 项目。 这里以 Maven 为例进行说明，因为其可以同时支持 Java 语言和 Scala 语言项目的构建。需要注意的是 Flink 1.9 只支持 Maven 3.0.4 以上的版本，Maven 安装完成后，可以通过以下两种方式来构建项目：\n\n**1. 直接基于 Maven Archetype 构建**\n\n直接使用下面的 mvn 语句来进行构建，然后根据交互信息的提示，依次输入 groupId , artifactId 以及包名等信息后等待初始化的完成： \n\n```bash\n$ mvn archetype:generate                               \\\n      -DarchetypeGroupId=org.apache.flink              \\\n      -DarchetypeArtifactId=flink-quickstart-java      \\\n      -DarchetypeVersion=1.9.0\n```\n\n> 注：如果想要创建基于 Scala 语言的项目，只需要将 flink-quickstart-java 换成 flink-quickstart-scala 即可，后文亦同。\n\n**2. 使用官方脚本快速构建**\n\n为了更方便的初始化项目，官方提供了快速构建脚本，可以直接通过以下命令来进行调用：\n\n```shell\n$ curl https://flink.apache.org/q/quickstart.sh | bash -s 1.9.0\n```\n\n该方式其实也是通过执行 maven archetype 命令来进行初始化，其脚本内容如下：\n\n```shell\nPACKAGE=quickstart\n\nmvn archetype:generate \\\n  -DarchetypeGroupId=org.apache.flink \\\n  -DarchetypeArtifactId=flink-quickstart-java \\\n  -DarchetypeVersion=${1:-1.8.0} \\\n  -DgroupId=org.myorg.quickstart \\\n  -DartifactId=$PACKAGE\t\\\n  -Dversion=0.1 \\\n  -Dpackage=org.myorg.quickstart \\\n  -DinteractiveMode=false\n```\n\n可以看到相比于第一种方式，该种方式只是直接指定好了 groupId ，artifactId ，version 等信息而已。\n\n### 2.2 使用 IDEA 构建\n\n如果你使用的是开发工具是 IDEA ，可以直接在项目创建页面选择 Maven Flink Archetype 进行项目初始化：\n\n<div align=\"center\"> <img src=\"../pictures/flink-maven.png\"/> </div>\n\n如果你的 IDEA 没有上述 Archetype， 可以通过点击右上角的 `ADD ARCHETYPE` ，来进行添加，依次填入所需信息，这些信息都可以从上述的 `archetype:generate ` 语句中获取。点击  `OK` 保存后，该 Archetype 就会一直存在于你的 IDEA 中，之后每次创建项目时，只需要直接选择该 Archetype 即可：\n\n<div align=\"center\"> <img src=\"../pictures/flink-maven-new.png\"/> </div>\n\n选中 Flink Archetype ，然后点击 `NEXT` 按钮，之后的所有步骤都和正常的 Maven 工程相同。\n\n## 三、项目结构\n\n### 3.1 项目结构\n\n创建完成后的自动生成的项目结构如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-basis-project.png\"/> </div>\n\n其中 BatchJob 为批处理的样例代码，源码如下：\n\n```scala\nimport org.apache.flink.api.scala._\n\nobject BatchJob {\n  def main(args: Array[String]) {\n    val env = ExecutionEnvironment.getExecutionEnvironment\n      ....\n    env.execute(\"Flink Batch Scala API Skeleton\")\n  }\n}\n```\n\ngetExecutionEnvironment 代表获取批处理的执行环境，如果是本地运行则获取到的就是本地的执行环境；如果在集群上运行，得到的就是集群的执行环境。如果想要获取流处理的执行环境，则只需要将 `ExecutionEnvironment` 替换为 `StreamExecutionEnvironment`， 对应的代码样例在 StreamingJob 中：\n\n```scala\nimport org.apache.flink.streaming.api.scala._\n\nobject StreamingJob {\n  def main(args: Array[String]) {\n    val env = StreamExecutionEnvironment.getExecutionEnvironment\n      ...\n    env.execute(\"Flink Streaming Scala API Skeleton\")\n  }\n}\n\n```\n\n需要注意的是对于流处理项目 `env.execute()` 这句代码是必须的，否则流处理程序就不会被执行，但是对于批处理项目则是可选的。\n\n### 3.2 主要依赖\n\n基于 Maven 骨架创建的项目主要提供了以下核心依赖：其中 `flink-scala` 用于支持开发批处理程序 ；`flink-streaming-scala` 用于支持开发流处理程序 ；`scala-library` 用于提供 Scala 语言所需要的类库。如果在使用 Maven 骨架创建时选择的是 Java 语言，则默认提供的则是 `flink-java` 和 `flink-streaming-java` 依赖。\n\n```xml\n<!-- Apache Flink dependencies -->\n<!-- These dependencies are provided, because they should not be packaged into the JAR file. -->\n<dependency>\n    <groupId>org.apache.flink</groupId>\n    <artifactId>flink-scala_${scala.binary.version}</artifactId>\n    <version>${flink.version}</version>\n    <scope>provided</scope>\n</dependency>\n<dependency>\n    <groupId>org.apache.flink</groupId>\n    <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>\n    <version>${flink.version}</version>\n    <scope>provided</scope>\n</dependency>\n\n<!-- Scala Library, provided by Flink as well. -->\n<dependency>\n    <groupId>org.scala-lang</groupId>\n    <artifactId>scala-library</artifactId>\n    <version>${scala.version}</version>\n    <scope>provided</scope>\n</dependency>\n```\n\n需要特别注意的以上依赖的 `scope` 标签全部被标识为 provided ，这意味着这些依赖都不会被打入最终的 JAR 包。因为 Flink 的安装包中已经提供了这些依赖，位于其 lib 目录下，名为  `flink-dist_*.jar`  ，它包含了 Flink 的所有核心类和依赖：\n\n<div align=\"center\"> <img src=\"../pictures/flink-lib.png\"/> </div>\n \n `scope` 标签被标识为 provided 会导致你在 IDEA 中启动项目时会抛出 ClassNotFoundException 异常。基于这个原因，在使用 IDEA 创建项目时还自动生成了以下 profile 配置：\n\n```xml\n<!-- This profile helps to make things run out of the box in IntelliJ -->\n<!-- Its adds Flink's core classes to the runtime class path. -->\n<!-- Otherwise they are missing in IntelliJ, because the dependency is 'provided' -->\n<profiles>\n    <profile>\n        <id>add-dependencies-for-IDEA</id>\n\n        <activation>\n            <property>\n                <name>idea.version</name>\n            </property>\n        </activation>\n\n        <dependencies>\n            <dependency>\n                <groupId>org.apache.flink</groupId>\n                <artifactId>flink-scala_${scala.binary.version}</artifactId>\n                <version>${flink.version}</version>\n                <scope>compile</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.flink</groupId>\n                <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>\n                <version>${flink.version}</version>\n                <scope>compile</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.scala-lang</groupId>\n                <artifactId>scala-library</artifactId>\n                <version>${scala.version}</version>\n                <scope>compile</scope>\n            </dependency>\n        </dependencies>\n    </profile>\n</profiles>\n```\n\n在 id 为 `add-dependencies-for-IDEA` 的 profile 中，所有的核心依赖都被标识为 compile，此时你可以无需改动任何代码，只需要在 IDEA 的 Maven 面板中勾选该 profile，即可直接在 IDEA 中运行 Flink 项目：\n\n<div align=\"center\"> <img src=\"../pictures/flink-maven-profile.png\"/> </div>\n\n## 四、词频统计案例\n\n项目创建完成后，可以先书写一个简单的词频统计的案例来尝试运行 Flink 项目，以下以 Scala 语言为例，分别介绍流处理程序和批处理程序的编程示例：\n\n### 4.1 批处理示例\n\n```scala\nimport org.apache.flink.api.scala._\n\nobject WordCountBatch {\n\n  def main(args: Array[String]): Unit = {\n    val benv = ExecutionEnvironment.getExecutionEnvironment\n    val dataSet = benv.readTextFile(\"D:\\\\wordcount.txt\")\n    dataSet.flatMap { _.toLowerCase.split(\",\")}\n            .filter (_.nonEmpty)\n            .map { (_, 1) }\n            .groupBy(0)\n            .sum(1)\n            .print()\n  }\n}\n```\n\n其中 `wordcount.txt` 中的内容如下：\n\n```shell\na,a,a,a,a\nb,b,b\nc,c\nd,d\n```\n\n本机不需要配置其他任何的 Flink 环境，直接运行 Main 方法即可，结果如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-word-count.png\"/> </div>\n\n### 4.2 流处理示例\n\n```scala\nimport org.apache.flink.streaming.api.scala._\nimport org.apache.flink.streaming.api.windowing.time.Time\n\nobject WordCountStreaming {\n\n  def main(args: Array[String]): Unit = {\n\n    val senv = StreamExecutionEnvironment.getExecutionEnvironment\n\n    val dataStream: DataStream[String] = senv.socketTextStream(\"192.168.0.229\", 9999, '\\n')\n    dataStream.flatMap { line => line.toLowerCase.split(\",\") }\n              .filter(_.nonEmpty)\n              .map { word => (word, 1) }\n              .keyBy(0)\n              .timeWindow(Time.seconds(3))\n              .sum(1)\n              .print()\n    senv.execute(\"Streaming WordCount\")\n  }\n}\n```\n\n这里以监听指定端口号上的内容为例，使用以下命令来开启端口服务：\n\n```shell\nnc -lk 9999\n```\n\n之后输入测试数据即可观察到流处理程序的处理情况。\n\n## 五、使用 Scala Shell\n\n对于日常的 Demo 项目，如果你不想频繁地启动 IDEA 来观察测试结果，可以像 Spark 一样，直接使用 Scala Shell 来运行程序，这对于日常的学习来说，效果更加直观，也更省时。Flink 安装包的下载地址如下：\n\n```shell\nhttps://flink.apache.org/downloads.html\n```\n\nFlink 大多数版本都提供有 Scala 2.11 和 Scala 2.12 两个版本的安装包可供下载：\n\n<div align=\"center\"> <img src=\"../pictures/flink-download.png\"/> </div>\n\n下载完成后进行解压即可，Scala Shell 位于安装目录的 bin 目录下，直接使用以下命令即可以本地模式启动：\n\n```shell\n./start-scala-shell.sh local\n```\n\n命令行启动完成后，其已经提供了批处理 （benv 和 btenv）和流处理（senv 和 stenv）的运行环境，可以直接运行 Scala Flink 程序，示例如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-scala-shell.png\"/> </div>\n\n最后解释一个常见的异常：这里我使用的 Flink 版本为 1.9.1，启动时会抛出如下异常。这里因为按照官方的说明，目前所有 Scala 2.12 版本的安装包暂时都不支持 Scala Shell，所以如果想要使用 Scala Shell，只能选择 Scala 2.11 版本的安装包。 \n\n```shell\n[root@hadoop001 bin]# ./start-scala-shell.sh local\n错误: 找不到或无法加载主类 org.apache.flink.api.scala.FlinkShell\n```\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Flink核心概念综述.md",
    "content": "# Flink 核心概念综述\n<nav>\n<a href=\"#一Flink-简介\">一、Flink 简介</a><br/>\n<a href=\"#二Flink-核心架构\">二、Flink 核心架构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-API--Libraries-层\">2.1 API & Libraries 层</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-Runtime-核心层\">2.2 Runtime 核心层</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-物理部署层\">2.3 物理部署层</a><br/>\n<a href=\"#三Flink-分层-API\">三、Flink 分层 API</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-SQL--Table-API\">3.1 SQL & Table API</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-DataStream--DataSet-API\">3.2 DataStream & DataSet API</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-Stateful-Stream-Processing\">3.3 Stateful Stream Processing</a><br/>\n<a href=\"#四Flink-集群架构\">四、Flink 集群架构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41--核心组件\">4.1  核心组件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42--Task--SubTask\">4.2  Task & SubTask</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43--资源管理\">4.3  资源管理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44-组件通讯\">4.4 组件通讯</a><br/>\n<a href=\"#五Flink-的优点\">五、Flink 的优点</a><br/>\n</nav>\n\n\n## 一、Flink 简介\n\nApache Flink 诞生于柏林工业大学的一个研究性项目，原名 StratoSphere 。2014 年，由 StratoSphere 项目孵化出 Flink，并于同年捐赠 Apache，之后成为 Apache 的顶级项目。2019 年 1 年，阿里巴巴收购了 Flink 的母公司 Data Artisans，并宣布开源内部的 Blink，Blink 是阿里巴巴基于 Flink 优化后的版本，增加了大量的新功能，并在性能和稳定性上进行了各种优化，经历过阿里内部多种复杂业务的挑战和检验。同时阿里巴巴也表示会逐步将这些新功能和特性 Merge 回社区版本的 Flink 中，因此 Flink 成为目前最为火热的大数据处理框架。\n\n简单来说，Flink 是一个分布式的流处理框架，它能够对有界和无界的数据流进行高效的处理。Flink 的核心是流处理，当然它也能支持批处理，Flink 将批处理看成是流处理的一种特殊情况，即数据流是有明确界限的。这和 Spark Streaming 的思想是完全相反的，Spark Streaming 的核心是批处理，它将流处理看成是批处理的一种特殊情况， 即把数据流进行极小粒度的拆分，拆分为多个微批处理。\n\nFlink 有界数据流和无界数据流：\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/flink-bounded-unbounded.png\"/> </div>\n\n\n\n\nSpark Streaming 数据流的拆分：\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/streaming-flow.png\"/> </div>\n\n\n\n\n## 二、Flink 核心架构\n\nFlink 采用分层的架构设计，从而保证各层在功能和职责上的清晰。如下图所示，由上而下分别是 API & Libraries 层、Runtime 核心层以及物理部署层：\n\n<div align=\"center\"> <img width=\"600px\"  src=\"../pictures/flink-stack.png\"/> </div>\n\n\n\n\n### 2.1 API & Libraries 层\n\n这一层主要提供了编程 API 和 顶层类库：\n\n+ 编程 API : 用于进行流处理的 DataStream API 和用于进行批处理的 DataSet API；\n+ 顶层类库：包括用于复杂事件处理的 CEP 库；用于结构化数据查询的 SQL & Table 库，以及基于批处理的机器学习库 FlinkML 和 图形处理库 Gelly。\n\n### 2.2 Runtime 核心层\n\n这一层是 Flink 分布式计算框架的核心实现层，包括作业转换，任务调度，资源分配，任务执行等功能，基于这一层的实现，可以在流式引擎下同时运行流处理程序和批处理程序。\n\n### 2.3 物理部署层\n\nFlink 的物理部署层，用于支持在不同平台上部署运行 Flink 应用。\n\n## 三、Flink 分层 API\n\n在上面介绍的 API & Libraries 这一层，Flink 又进行了更为具体的划分。具体如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-api-stack.png\"/> </div>\n\n\n\n\n按照如上的层次结构，API 的一致性由下至上依次递增，接口的表现能力由下至上依次递减，各层的核心功能如下：\n\n### 3.1 SQL & Table API\n\nSQL & Table API 同时适用于批处理和流处理，这意味着你可以对有界数据流和无界数据流以相同的语义进行查询，并产生相同的结果。除了基本查询外， 它还支持自定义的标量函数，聚合函数以及表值函数，可以满足多样化的查询需求。 \n\n### 3.2 DataStream & DataSet API\n\nDataStream &  DataSet API 是 Flink 数据处理的核心 API，支持使用 Java 语言或 Scala 语言进行调用，提供了数据读取，数据转换和数据输出等一系列常用操作的封装。\n\n### 3.3 Stateful Stream Processing\n\nStateful Stream Processing 是最低级别的抽象，它通过 Process Function 函数内嵌到 DataStream API 中。 Process Function 是 Flink 提供的最底层 API，具有最大的灵活性，允许开发者对于时间和状态进行细粒度的控制。\n\n## 四、Flink 集群架构\n\n### 4.1  核心组件\n\n按照上面的介绍，Flink 核心架构的第二层是 Runtime 层， 该层采用标准的 Master - Slave 结构， 其中，Master 部分又包含了三个核心组件：Dispatcher、ResourceManager 和 JobManager，而 Slave 则主要是 TaskManager 进程。它们的功能分别如下：\n\n- **JobManagers** (也称为 *masters*) ：JobManagers 接收由 Dispatcher 传递过来的执行程序，该执行程序包含了作业图 (JobGraph)，逻辑数据流图 (logical dataflow graph) 及其所有的 classes 文件以及第三方类库 (libraries) 等等 。紧接着 JobManagers 会将 JobGraph 转换为执行图 (ExecutionGraph)，然后向 ResourceManager 申请资源来执行该任务，一旦申请到资源，就将执行图分发给对应的 TaskManagers 。因此每个作业 (Job) 至少有一个 JobManager；高可用部署下可以有多个 JobManagers，其中一个作为 *leader*，其余的则处于 *standby* 状态。\n- **TaskManagers** (也称为 *workers*) : TaskManagers 负责实际的子任务 (subtasks) 的执行，每个 TaskManagers 都拥有一定数量的 slots。Slot 是一组固定大小的资源的合集 (如计算能力，存储空间)。TaskManagers 启动后，会将其所拥有的 slots 注册到 ResourceManager 上，由 ResourceManager 进行统一管理。\n- **Dispatcher**：负责接收客户端提交的执行程序，并传递给 JobManager 。除此之外，它还提供了一个 WEB UI 界面，用于监控作业的执行情况。\n- **ResourceManager** ：负责管理 slots 并协调集群资源。ResourceManager 接收来自 JobManager 的资源请求，并将存在空闲 slots 的 TaskManagers 分配给 JobManager 执行任务。Flink 基于不同的部署平台，如 YARN , Mesos，K8s 等提供了不同的资源管理器，当 TaskManagers 没有足够的 slots 来执行任务时，它会向第三方平台发起会话来请求额外的资源。\n\n<div align=\"center\"> <img src=\"../pictures/flink-application-submission.png\"/> </div>\n\n\n### 4.2  Task & SubTask\n\n上面我们提到：TaskManagers 实际执行的是 SubTask，而不是 Task，这里解释一下两者的区别：\n\n在执行分布式计算时，Flink 将可以链接的操作 (operators) 链接到一起，这就是 Task。之所以这样做， 是为了减少线程间切换和缓冲而导致的开销，在降低延迟的同时可以提高整体的吞吐量。 但不是所有的 operator 都可以被链接，如下 keyBy 等操作会导致网络 shuffle 和重分区，因此其就不能被链接，只能被单独作为一个 Task。  简单来说，一个 Task 就是一个可以链接的最小的操作链 (Operator Chains) 。如下图，source 和 map 算子被链接到一块，因此整个作业就只有三个 Task：\n\n<div align=\"center\"> <img src=\"../pictures/flink-task-subtask.png\"/> </div>\n\n\n解释完 Task ，我们在解释一下什么是 SubTask，其准确的翻译是： *A subtask is one parallel slice of a task*，即一个 Task 可以按照其并行度拆分为多个 SubTask。如上图，source & map 具有两个并行度，KeyBy 具有两个并行度，Sink 具有一个并行度，因此整个虽然只有 3 个 Task，但是却有 5 个 SubTask。Jobmanager 负责定义和拆分这些 SubTask，并将其交给 Taskmanagers 来执行，每个 SubTask 都是一个单独的线程。\n\n### 4.3  资源管理\n\n理解了 SubTasks ，我们再来看看其与 Slots 的对应情况。一种可能的分配情况如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-tasks-slots.png\"/> </div>\n\n\n\n\n这时每个 SubTask 线程运行在一个独立的 TaskSlot， 它们共享所属的 TaskManager 进程的TCP 连接（通过多路复用技术）和心跳信息 (heartbeat messages)，从而可以降低整体的性能开销。此时看似是最好的情况，但是每个操作需要的资源都是不尽相同的，这里假设该作业 keyBy 操作所需资源的数量比 Sink 多很多 ，那么此时 Sink 所在 Slot 的资源就没有得到有效的利用。\n\n基于这个原因，Flink 允许多个 subtasks 共享 slots，即使它们是不同 tasks 的 subtasks，但只要它们来自同一个 Job 就可以。假设上面 souce & map 和 keyBy 的并行度调整为 6，而 Slot 的数量不变，此时情况如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-subtask-slots.png\"/> </div>\n\n\n\n\n可以看到一个 Task Slot 中运行了多个 SubTask 子任务，此时每个子任务仍然在一个独立的线程中执行，只不过共享一组 Sot 资源而已。那么 Flink 到底如何确定一个 Job 至少需要多少个 Slot 呢？Flink 对于这个问题的处理很简单，默认情况一个 Job 所需要的 Slot 的数量就等于其 Operation 操作的最高并行度。如下， A，B，D 操作的并行度为 4，而 C，E 操作的并行度为 2，那么此时整个 Job 就需要至少四个 Slots 来完成。通过这个机制，Flink 就可以不必去关心一个 Job 到底会被拆分为多少个 Tasks 和 SubTasks。\n\n<div align=\"center\"> <img src=\"../pictures/flink-task-parallelism.png\"/> </div>\n\n\n\n\n \n\n### 4.4 组件通讯\n\nFlink 的所有组件都基于 Actor System 来进行通讯。Actor system是多种角色的 actor 的容器，它提供调度，配置，日志记录等多种服务，并包含一个可以启动所有 actor 的线程池，如果 actor 是本地的，则消息通过共享内存进行共享，但如果 actor 是远程的，则通过 RPC 的调用来传递消息。\n\n<div align=\"center\"> <img src=\"../pictures/flink-process.png\"/> </div>\n\n\n\n\n## 五、Flink 的优点\n\n最后基于上面的介绍，来总结一下 Flink 的优点：\n\n+ Flink 是基于事件驱动 (Event-driven) 的应用，能够同时支持流处理和批处理；\n+ 基于内存的计算，能够保证高吞吐和低延迟，具有优越的性能表现；\n+ 支持精确一次 (Exactly-once) 语意，能够完美地保证一致性和正确性；\n+ 分层 API ，能够满足各个层次的开发需求；\n+ 支持高可用配置，支持保存点机制，能够提供安全性和稳定性上的保证；\n+ 多样化的部署方式，支持本地，远端，云端等多种部署方案；\n+ 具有横向扩展架构，能够按照用户的需求进行动态扩容；\n+ 活跃度极高的社区和完善的生态圈的支持。\n\n\n\n## 参考资料\n\n+ [Dataflow Programming Model](https://ci.apache.org/projects/flink/flink-docs-release-1.9/concepts/programming-model.html)\n+ [Distributed Runtime Environment](https://ci.apache.org/projects/flink/flink-docs-release-1.9/concepts/runtime.html)\n+  [Component Stack](https://ci.apache.org/projects/flink/flink-docs-release-1.9/internals/components.html)\n+ Fabian Hueske , Vasiliki Kalavri . 《Stream Processing with Apache Flink》.  O'Reilly Media .  2019-4-30 \n\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Flink状态管理与检查点机制.md",
    "content": "# Flink 状态管理\n<nav>\n<a href=\"#一状态分类\">一、状态分类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-算子状态\">2.1 算子状态</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-键控状态\">2.2 键控状态</a><br/>\n<a href=\"#二状态编程\">二、状态编程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-键控状态\">2.1 键控状态</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-状态有效期\">2.2 状态有效期</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-算子状态\">2.3 算子状态</a><br/>\n<a href=\"#三检查点机制\">三、检查点机制</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-CheckPoints\">3.1 CheckPoints</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-开启检查点\">3.2 开启检查点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-保存点机制\">3.3 保存点机制</a><br/>\n<a href=\"#四状态后端\">四、状态后端</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-状态管理器分类\">4.1 状态管理器分类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-配置方式\">4.2 配置方式</a><br/>\n</nav>\n\n\n## 一、状态分类\n\n相对于其他流计算框架，Flink 一个比较重要的特性就是其支持有状态计算。即你可以将中间的计算结果进行保存，并提供给后续的计算使用：\n\n<div align=\"center\"> <img width=\"500px\" src=\"../pictures/flink-stateful-stream.png\"/> </div>\n\n\n\n具体而言，Flink 又将状态 (State) 分为 Keyed State 与 Operator State：\n\n### 2.1 算子状态\n\n算子状态 (Operator State)：顾名思义，状态是和算子进行绑定的，一个算子的状态不能被其他算子所访问到。官方文档上对 Operator State 的解释是：*each operator state is bound to one parallel operator instance*，所以更为确切的说一个算子状态是与一个并发的算子实例所绑定的，即假设算子的并行度是 2，那么其应有两个对应的算子状态：\n\n<div align=\"center\"> <img width=\"500px\" src=\"../pictures/flink-operator-state.png\"/> </div>\n\n\n\n### 2.2 键控状态\n\n键控状态 (Keyed State) ：是一种特殊的算子状态，即状态是根据 key 值进行区分的，Flink 会为每类键值维护一个状态实例。如下图所示，每个颜色代表不同 key 值，对应四个不同的状态实例。需要注意的是键控状态只能在 `KeyedStream` 上进行使用，我们可以通过 `stream.keyBy(...)` 来得到 `KeyedStream` 。\n\n<div align=\"center\"> <img src=\"../pictures/flink-keyed-state.png\"/> </div>\n\n\n\n## 二、状态编程\n\n### 2.1 键控状态\n\nFlink 提供了以下数据格式来管理和存储键控状态 (Keyed State)：\n\n- **ValueState**：存储单值类型的状态。可以使用  `update(T)` 进行更新，并通过 `T value()` 进行检索。 \n- **ListState**：存储列表类型的状态。可以使用 `add(T)` 或 `addAll(List)` 添加元素；并通过 `get()` 获得整个列表。\n- **ReducingState**：用于存储经过 ReduceFunction 计算后的结果，使用 `add(T)` 增加元素。\n- **AggregatingState**：用于存储经过 AggregatingState 计算后的结果，使用 `add(IN)` 添加元素。\n- **FoldingState**：已被标识为废弃，会在未来版本中移除，官方推荐使用 `AggregatingState` 代替。\n-  **MapState**：维护 Map 类型的状态。\n\n以上所有增删改查方法不必硬记，在使用时通过语法提示来调用即可。这里给出一个具体的使用示例：假设我们正在开发一个监控系统，当监控数据超过阈值一定次数后，需要发出报警信息。这里之所以要达到一定次数，是因为由于偶发原因，偶尔一次超过阈值并不能代表什么，故需要达到一定次数后才触发报警，这就需要使用到 Flink 的状态编程。相关代码如下：\n\n```java\npublic class ThresholdWarning extends \n    RichFlatMapFunction<Tuple2<String, Long>, Tuple2<String, List<Long>>> {\n\n    // 通过ListState来存储非正常数据的状态\n    private transient ListState<Long> abnormalData;\n    // 需要监控的阈值\n    private Long threshold;\n    // 触发报警的次数\n    private Integer numberOfTimes;\n\n    ThresholdWarning(Long threshold, Integer numberOfTimes) {\n        this.threshold = threshold;\n        this.numberOfTimes = numberOfTimes;\n    }\n\n    @Override\n    public void open(Configuration parameters) {\n        // 通过状态名称(句柄)获取状态实例，如果不存在则会自动创建\n        abnormalData = getRuntimeContext().getListState(\n            new ListStateDescriptor<>(\"abnormalData\", Long.class));\n    }\n\n    @Override\n    public void flatMap(Tuple2<String, Long> value, Collector<Tuple2<String, List<Long>>> out)\n        throws Exception {\n        Long inputValue = value.f1;\n        // 如果输入值超过阈值，则记录该次不正常的数据信息\n        if (inputValue >= threshold) {\n            abnormalData.add(inputValue);\n        }\n        ArrayList<Long> list = Lists.newArrayList(abnormalData.get().iterator());\n        // 如果不正常的数据出现达到一定次数，则输出报警信息\n        if (list.size() >= numberOfTimes) {\n            out.collect(Tuple2.of(value.f0 + \" 超过指定阈值 \", list));\n            // 报警信息输出后，清空状态\n            abnormalData.clear();\n        }\n    }\n}\n```\n\n调用自定义的状态监控，这里我们使用 a，b 来代表不同类型的监控数据，分别对其数据进行监控：\n\n```java\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\nDataStreamSource<Tuple2<String, Long>> tuple2DataStreamSource = env.fromElements(\n    Tuple2.of(\"a\", 50L), Tuple2.of(\"a\", 80L), Tuple2.of(\"a\", 400L),\n    Tuple2.of(\"a\", 100L), Tuple2.of(\"a\", 200L), Tuple2.of(\"a\", 200L),\n    Tuple2.of(\"b\", 100L), Tuple2.of(\"b\", 200L), Tuple2.of(\"b\", 200L),\n    Tuple2.of(\"b\", 500L), Tuple2.of(\"b\", 600L), Tuple2.of(\"b\", 700L));\ntuple2DataStreamSource\n    .keyBy(0)\n    .flatMap(new ThresholdWarning(100L, 3))  // 超过100的阈值3次后就进行报警\n    .printToErr();\nenv.execute(\"Managed Keyed State\");\n```\n\n输出如下结果如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-state-management.png\"/> </div>\n\n\n\n### 2.2 状态有效期\n\n以上任何类型的 keyed state 都支持配置有效期 (TTL) ，示例如下：\n\n```java\nStateTtlConfig ttlConfig = StateTtlConfig\n    // 设置有效期为 10 秒\n    .newBuilder(Time.seconds(10))  \n    // 设置有效期更新规则，这里设置为当创建和写入时，都重置其有效期到规定的10秒\n    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) \n    /*设置只要值过期就不可见，另外一个可选值是ReturnExpiredIfNotCleanedUp，\n     代表即使值过期了，但如果还没有被物理删除，就是可见的*/\n    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)\n    .build();\nListStateDescriptor<Long> descriptor = new ListStateDescriptor<>(\"abnormalData\", Long.class);\ndescriptor.enableTimeToLive(ttlConfig);\n```\n\n### 2.3 算子状态\n\n相比于键控状态，算子状态目前支持的存储类型只有以下三种：\n\n- **ListState**：存储列表类型的状态。\n- **UnionListState**：存储列表类型的状态，与 ListState 的区别在于：如果并行度发生变化，ListState 会将该算子的所有并发的状态实例进行汇总，然后均分给新的 Task；而 UnionListState 只是将所有并发的状态实例汇总起来，具体的划分行为则由用户进行定义。\n- **BroadcastState**：用于广播的算子状态。\n\n这里我们继续沿用上面的例子，假设此时我们不需要区分监控数据的类型，只要有监控数据超过阈值并达到指定的次数后，就进行报警，代码如下：\n\n```java\npublic class ThresholdWarning extends RichFlatMapFunction<Tuple2<String, Long>, \nTuple2<String, List<Tuple2<String, Long>>>> implements CheckpointedFunction {\n\n    // 非正常数据\n    private List<Tuple2<String, Long>> bufferedData;\n    // checkPointedState\n    private transient ListState<Tuple2<String, Long>> checkPointedState;\n    // 需要监控的阈值\n    private Long threshold;\n    // 次数\n    private Integer numberOfTimes;\n\n    ThresholdWarning(Long threshold, Integer numberOfTimes) {\n        this.threshold = threshold;\n        this.numberOfTimes = numberOfTimes;\n        this.bufferedData = new ArrayList<>();\n    }\n\n    @Override\n    public void initializeState(FunctionInitializationContext context) throws Exception {\n        // 注意这里获取的是OperatorStateStore\n        checkPointedState = context.getOperatorStateStore().\n            getListState(new ListStateDescriptor<>(\"abnormalData\",\n                TypeInformation.of(new TypeHint<Tuple2<String, Long>>() {\n                })));\n        // 如果发生重启，则需要从快照中将状态进行恢复\n        if (context.isRestored()) {\n            for (Tuple2<String, Long> element : checkPointedState.get()) {\n                bufferedData.add(element);\n            }\n        }\n    }\n\n    @Override\n    public void flatMap(Tuple2<String, Long> value, \n                        Collector<Tuple2<String, List<Tuple2<String, Long>>>> out) {\n        Long inputValue = value.f1;\n        // 超过阈值则进行记录\n        if (inputValue >= threshold) {\n            bufferedData.add(value);\n        }\n        // 超过指定次数则输出报警信息\n        if (bufferedData.size() >= numberOfTimes) {\n             // 顺便输出状态实例的hashcode\n             out.collect(Tuple2.of(checkPointedState.hashCode() + \"阈值警报！\", bufferedData));\n            bufferedData.clear();\n        }\n    }\n\n    @Override\n    public void snapshotState(FunctionSnapshotContext context) throws Exception {\n        // 在进行快照时，将数据存储到checkPointedState\n        checkPointedState.clear();\n        for (Tuple2<String, Long> element : bufferedData) {\n            checkPointedState.add(element);\n        }\n    }\n}\n```\n\n调用自定义算子状态，这里需要将并行度设置为 1：\n\n```java\nfinal StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();\n// 开启检查点机制\nenv.enableCheckpointing(1000);\n// 设置并行度为1\nDataStreamSource<Tuple2<String, Long>> tuple2DataStreamSource = env.setParallelism(1).fromElements(\n    Tuple2.of(\"a\", 50L), Tuple2.of(\"a\", 80L), Tuple2.of(\"a\", 400L),\n    Tuple2.of(\"a\", 100L), Tuple2.of(\"a\", 200L), Tuple2.of(\"a\", 200L),\n    Tuple2.of(\"b\", 100L), Tuple2.of(\"b\", 200L), Tuple2.of(\"b\", 200L),\n    Tuple2.of(\"b\", 500L), Tuple2.of(\"b\", 600L), Tuple2.of(\"b\", 700L));\ntuple2DataStreamSource\n    .flatMap(new ThresholdWarning(100L, 3))\n    .printToErr();\nenv.execute(\"Managed Keyed State\");\n}\n```\n\n此时输出如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-operator-state-para1.png\"/> </div>\n\n\n\n在上面的调用代码中，我们将程序的并行度设置为 1，可以看到三次输出中状态实例的 hashcode 全是一致的，证明它们都同一个状态实例。假设将并行度设置为 2，此时输出如下：\n\n<div align=\"center\"> <img src=\"../pictures/flink-operator-state-para2.png\"/> </div>\n\n\n\n可以看到此时两次输出中状态实例的 hashcode 是不一致的，代表它们不是同一个状态实例，这也就是上文提到的，一个算子状态是与一个并发的算子实例所绑定的。同时这里只输出两次，是因为在并发处理的情况下，线程 1 可能拿到 5 个非正常值，线程 2 可能拿到 4 个非正常值，因为要大于 3 次才能输出，所以在这种情况下就会出现只输出两条记录的情况，所以需要将程序的并行度设置为 1。\n\n## 三、检查点机制\n\n### 3.1 CheckPoints\n\n为了使 Flink 的状态具有良好的容错性，Flink 提供了检查点机制 (CheckPoints)  。通过检查点机制，Flink 定期在数据流上生成 checkpoint barrier ，当某个算子收到 barrier 时，即会基于当前状态生成一份快照，然后再将该 barrier 传递到下游算子，下游算子接收到该 barrier 后，也基于当前状态生成一份快照，依次传递直至到最后的 Sink 算子上。当出现异常后，Flink 就可以根据最近的一次的快照数据将所有算子恢复到先前的状态。\n\n<div align=\"center\"> <img src=\"../pictures/flink-stream-barriers.png\"/> </div>\n\n\n\n\n\n### 3.2 开启检查点\n\n默认情况下，检查点机制是关闭的，需要在程序中进行开启：\n\n```java\n// 开启检查点机制，并指定状态检查点之间的时间间隔\nenv.enableCheckpointing(1000); \n\n// 其他可选配置如下：\n// 设置语义\nenv.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);\n// 设置两个检查点之间的最小时间间隔\nenv.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);\n// 设置执行Checkpoint操作时的超时时间\nenv.getCheckpointConfig().setCheckpointTimeout(60000);\n// 设置最大并发执行的检查点的数量\nenv.getCheckpointConfig().setMaxConcurrentCheckpoints(1);\n// 将检查点持久化到外部存储\nenv.getCheckpointConfig().enableExternalizedCheckpoints(\n    ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);\n// 如果有更近的保存点时，是否将作业回退到该检查点\nenv.getCheckpointConfig().setPreferCheckpointForRecovery(true);\n```\n\n### 3.3 保存点机制\n\n保存点机制 (Savepoints) 是检查点机制的一种特殊的实现，它允许你通过手工的方式来触发 Checkpoint，并将结果持久化存储到指定路径中，主要用于避免 Flink 集群在重启或升级时导致状态丢失。示例如下：\n\n```shell\n# 触发指定id的作业的Savepoint，并将结果存储到指定目录下\nbin/flink savepoint :jobId [:targetDirectory]\n```\n\n更多命令和配置可以参考官方文档：[savepoints]( https://ci.apache.org/projects/flink/flink-docs-release-1.9/zh/ops/state/savepoints.html )\n\n## 四、状态后端\n\n### 4.1 状态管理器分类\n\n默认情况下，所有的状态都存储在 JVM 的堆内存中，在状态数据过多的情况下，这种方式很有可能导致内存溢出，因此 Flink 该提供了其它方式来存储状态数据，这些存储方式统一称为状态后端 (或状态管理器)：\n\n<div align=\"center\"> <img src=\"../pictures/flink-checkpoints-backend.png\"/> </div>\n\n\n\n主要有以下三种：\n\n#### 1. MemoryStateBackend\n\n默认的方式，即基于 JVM 的堆内存进行存储，主要适用于本地开发和调试。\n\n#### 2. FsStateBackend\n\n基于文件系统进行存储，可以是本地文件系统，也可以是 HDFS 等分布式文件系统。 需要注意而是虽然选择使用了 FsStateBackend ，但正在进行的数据仍然是存储在 TaskManager 的内存中的，只有在 checkpoint 时，才会将状态快照写入到指定文件系统上。\n\n#### 3. RocksDBStateBackend\n\nRocksDBStateBackend 是 Flink 内置的第三方状态管理器，采用嵌入式的 key-value 型数据库 RocksDB 来存储正在进行的数据。等到 checkpoint 时，再将其中的数据持久化到指定的文件系统中，所以采用 RocksDBStateBackend 时也需要配置持久化存储的文件系统。之所以这样做是因为 RocksDB 作为嵌入式数据库安全性比较低，但比起全文件系统的方式，其读取速率更快；比起全内存的方式，其存储空间更大，因此它是一种比较均衡的方案。\n\n### 4.2 配置方式\n\nFlink 支持使用两种方式来配置后端管理器：\n\n**第一种方式**：基于代码方式进行配置，只对当前作业生效：\n\n```java\n// 配置 FsStateBackend\nenv.setStateBackend(new FsStateBackend(\"hdfs://namenode:40010/flink/checkpoints\"));\n// 配置 RocksDBStateBackend\nenv.setStateBackend(new RocksDBStateBackend(\"hdfs://namenode:40010/flink/checkpoints\"));\n```\n\n配置 RocksDBStateBackend 时，需要额外导入下面的依赖：\n\n```xml\n<dependency>\n    <groupId>org.apache.flink</groupId>\n    <artifactId>flink-statebackend-rocksdb_2.11</artifactId>\n    <version>1.9.0</version>\n</dependency>\n```\n\n**第二种方式**：基于 `flink-conf.yaml` 配置文件的方式进行配置，对所有部署在该集群上的作业都生效：\n\n```yaml\nstate.backend: filesystem\nstate.checkpoints.dir: hdfs://namenode:40010/flink/checkpoints\n```\n\n\n\n> 注：本篇文章所有示例代码下载地址：[flink-state-management]( https://github.com/heibaiying/BigData-Notes/tree/master/code/Flink/flink-state-management)\n\n\n\n## 参考资料\n\n+ [Working with State](https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/state/state.html)\n+ [Checkpointing](https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/state/checkpointing.html)\n+ [Savepoints](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/state/savepoints.html#savepoints)\n+ [State Backends](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/state/state_backends.html)\n+ Fabian Hueske , Vasiliki Kalavri . 《Stream Processing with Apache Flink》.  O'Reilly Media .  2019-4-30 \n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Flume整合Kafka.md",
    "content": "# Flume 整合 Kafka\n\n<nav>\n<a href=\"#一背景\">一、背景</a><br/>\n<a href=\"#二整合流程\">二、整合流程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-启动Zookeeper和Kafka\">1. 启动Zookeeper和Kafka</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-创建主题\">2. 创建主题</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3-启动kafka消费者\">3. 启动kafka消费者</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#4-配置Flume\">4. 配置Flume</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#5-启动Flume\">5. 启动Flume</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#6-测试\">6. 测试</a><br/>\n</nav>\n\n## 一、背景\n\n先说一下，为什么要使用 Flume + Kafka？\n\n以实时流处理项目为例，由于采集的数据量可能存在峰值和峰谷，假设是一个电商项目，那么峰值通常出现在秒杀时，这时如果直接将 Flume 聚合后的数据输入到 Storm 等分布式计算框架中，可能就会超过集群的处理能力，这时采用 Kafka 就可以起到削峰的作用。Kafka 天生为大数据场景而设计，具有高吞吐的特性，能很好地抗住峰值数据的冲击。\n\n<div align=\"center\"> <img  src=\"../pictures/flume-kafka.png\"/> </div>\n\n\n\n## 二、整合流程\n\nFlume 发送数据到 Kafka 上主要是通过 `KafkaSink` 来实现的，主要步骤如下：\n\n### 1. 启动Zookeeper和Kafka\n\n这里启动一个单节点的 Kafka 作为测试：\n\n```shell\n# 启动Zookeeper\nzkServer.sh start\n\n# 启动kafka\nbin/kafka-server-start.sh config/server.properties\n```\n\n### 2. 创建主题\n\n创建一个主题 `flume-kafka`，之后 Flume 收集到的数据都会发到这个主题上：\n\n```shell\n# 创建主题\nbin/kafka-topics.sh --create \\\n--zookeeper hadoop001:2181 \\\n--replication-factor 1   \\\n--partitions 1 --topic flume-kafka\n\n# 查看创建的主题\nbin/kafka-topics.sh --zookeeper hadoop001:2181 --list\n```\n\n\n\n### 3. 启动kafka消费者\n\n启动一个消费者，监听我们刚才创建的 `flume-kafka` 主题：\n\n```shell\n# bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic flume-kafka\n```\n\n\n\n### 4. 配置Flume\n\n新建配置文件 `exec-memory-kafka.properties`，文件内容如下。这里我们监听一个名为 `kafka.log` 的文件，当文件内容有变化时，将新增加的内容发送到 Kafka 的 `flume-kafka` 主题上。\n\n```properties\na1.sources = s1\na1.channels = c1\na1.sinks = k1                                                                                         \n\na1.sources.s1.type=exec\na1.sources.s1.command=tail -F /tmp/kafka.log\na1.sources.s1.channels=c1 \n\n#设置Kafka接收器\na1.sinks.k1.type= org.apache.flume.sink.kafka.KafkaSink\n#设置Kafka地址\na1.sinks.k1.brokerList=hadoop001:9092\n#设置发送到Kafka上的主题\na1.sinks.k1.topic=flume-kafka\n#设置序列化方式\na1.sinks.k1.serializer.class=kafka.serializer.StringEncoder\na1.sinks.k1.channel=c1     \n\na1.channels.c1.type=memory\na1.channels.c1.capacity=10000\na1.channels.c1.transactionCapacity=100   \n```\n\n\n\n### 5. 启动Flume\n\n```shell\nflume-ng agent \\\n--conf conf \\\n--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/exec-memory-kafka.properties \\\n--name a1 -Dflume.root.logger=INFO,console\n```\n\n\n\n### 6. 测试\n\n向监听的 `/tmp/kafka.log     ` 文件中追加内容，查看 Kafka 消费者的输出：\n\n<div align=\"center\"> <img  src=\"../pictures/flume-kafka-01.png\"/> </div>\n\n可以看到 `flume-kafka` 主题的消费端已经收到了对应的消息：\n\n<div align=\"center\"> <img  src=\"../pictures/flume-kafka-2.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/Flume简介及基本使用.md",
    "content": "# Flume 简介及基本使用\n\n<nav>\n<a href=\"#一Flume简介\">一、Flume简介</a><br/>\n<a href=\"#二Flume架构和基本概念\">二、Flume架构和基本概念</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-基本架构\">2.1 基本架构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-基本概念\">2.2 基本概念</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-组件种类\">2.3 组件种类</a><br/>\n<a href=\"#三Flume架构模式\">三、Flume架构模式</a><br/>\n<a href=\"#四Flume配置格式\">四、Flume配置格式</a><br/>\n<a href=\"#五Flume的安装部署\">五、Flume安装部署</a><br/>\n<a href=\"#六Flume使用案例\">六、Flume使用案例</a><br/>\n</nav>\n\n\n## 一、Flume简介\n\nApache Flume 是一个分布式，高可用的数据收集系统。它可以从不同的数据源收集数据，经过聚合后发送到存储系统中，通常用于日志数据的收集。Flume 分为 NG 和 OG (1.0 之前) 两个版本，NG 在 OG 的基础上进行了完全的重构，是目前使用最为广泛的版本。下面的介绍均以 NG 为基础。\n\n## 二、Flume架构和基本概念\n\n下图为 Flume 的基本架构图：\n\n\n\n<div align=\"center\"> <img  src=\"../pictures/flume-architecture.png\"/> </div>\n\n### 2.1 基本架构\n\n外部数据源以特定格式向 Flume 发送 `events` (事件)，当 `source` 接收到 `events` 时，它将其存储到一个或多个 `channel`，`channe` 会一直保存 `events` 直到它被 `sink` 所消费。`sink` 的主要功能从 `channel` 中读取 `events`，并将其存入外部存储系统或转发到下一个 `source`，成功后再从 `channel` 中移除 `events`。\n\n\n\n### 2.2 基本概念\n\n**1. Event**\n\n`Event` 是 Flume NG 数据传输的基本单元。类似于 JMS 和消息系统中的消息。一个 `Event` 由标题和正文组成：前者是键/值映射，后者是任意字节数组。\n\n**2. Source** \n\n数据收集组件，从外部数据源收集数据，并存储到 Channel 中。\n\n**3. Channel**\n\n`Channel` 是源和接收器之间的管道，用于临时存储数据。可以是内存或持久化的文件系统：\n\n+ `Memory Channel` : 使用内存，优点是速度快，但数据可能会丢失 (如突然宕机)；\n+ `File Channel` : 使用持久化的文件系统，优点是能保证数据不丢失，但是速度慢。\n\n**4. Sink**\n\n`Sink` 的主要功能从 `Channel` 中读取 `Event`，并将其存入外部存储系统或将其转发到下一个 `Source`，成功后再从 `Channel` 中移除 `Event`。\n\n**5. Agent**\n\n是一个独立的 (JVM) 进程，包含 `Source`、 `Channel`、 `Sink` 等组件。\n\n\n\n### 2.3 组件种类\n\nFlume 中的每一个组件都提供了丰富的类型，适用于不同场景：\n\n- Source 类型 ：内置了几十种类型，如 `Avro Source`，`Thrift Source`，`Kafka Source`，`JMS Source`；\n\n- Sink 类型 ：`HDFS Sink`，`Hive Sink`，`HBaseSinks`，`Avro Sink` 等；\n\n- Channel 类型 ：`Memory Channel`，`JDBC Channel`，`Kafka Channel`，`File Channel` 等。\n\n对于 Flume 的使用，除非有特别的需求，否则通过组合内置的各种类型的 Source，Sink 和 Channel 就能满足大多数的需求。在 [Flume 官网](http://flume.apache.org/releases/content/1.9.0/FlumeUserGuide.html) 上对所有类型组件的配置参数均以表格的方式做了详尽的介绍，并附有配置样例；同时不同版本的参数可能略有所不同，所以使用时建议选取官网对应版本的 User Guide 作为主要参考资料。\n\n   \n\n## 三、Flume架构模式\n\nFlume 支持多种架构模式，分别介绍如下\n\n### 3.1 multi-agent flow\n\n\n\n<div align=\"center\"> <img  src=\"../pictures/flume-multi-agent-flow.png\"/> </div>\n\n<br/>\n\nFlume 支持跨越多个 Agent 的数据传递，这要求前一个 Agent 的 Sink 和下一个 Agent 的 Source 都必须是 `Avro` 类型，Sink 指向 Source 所在主机名 (或 IP 地址) 和端口（详细配置见下文案例三）。\n\n### 3.2 Consolidation\n\n<div align=\"center\"> <img  src=\"../pictures/flume-consolidation.png\"/> </div>\n\n\n\n<br/>\n\n日志收集中常常存在大量的客户端（比如分布式 web 服务），Flume 支持使用多个 Agent 分别收集日志，然后通过一个或者多个 Agent 聚合后再存储到文件系统中。\n\n### 3.3 Multiplexing the flow\n\n<div align=\"center\"> <img  src=\"../pictures/flume-multiplexing-the-flow.png\"/> </div>\n\nFlume 支持从一个 Source 向多个 Channel，也就是向多个 Sink 传递事件，这个操作称之为 `Fan Out`(扇出)。默认情况下 `Fan Out` 是向所有的 Channel 复制 `Event`，即所有 Channel 收到的数据都是相同的。同时 Flume 也支持在 `Source` 上自定义一个复用选择器 (multiplexing selector) 来实现自定义的路由规则。\n\n\n\n## 四、Flume配置格式\n\nFlume 配置通常需要以下两个步骤：\n\n1. 分别定义好 Agent 的 Sources，Sinks，Channels，然后将 Sources 和 Sinks 与通道进行绑定。需要注意的是一个 Source 可以配置多个 Channel，但一个 Sink 只能配置一个 Channel。基本格式如下：\n\n```shell\n<Agent>.sources = <Source>\n<Agent>.sinks = <Sink>\n<Agent>.channels = <Channel1> <Channel2>\n\n# set channel for source\n<Agent>.sources.<Source>.channels = <Channel1> <Channel2> ...\n\n# set channel for sink\n<Agent>.sinks.<Sink>.channel = <Channel1>\n```\n\n2. 分别定义 Source，Sink，Channel 的具体属性。基本格式如下：\n\n```shell\n\n<Agent>.sources.<Source>.<someProperty> = <someValue>\n\n# properties for channels\n<Agent>.channel.<Channel>.<someProperty> = <someValue>\n\n# properties for sinks\n<Agent>.sources.<Sink>.<someProperty> = <someValue>\n```\n\n\n\n## 五、Flume的安装部署\n\n为方便大家后期查阅，本仓库中所有软件的安装均单独成篇，Flume 的安装见：\n\n[Linux 环境下 Flume 的安装部署](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux%E4%B8%8BFlume%E7%9A%84%E5%AE%89%E8%A3%85.md)\n\n\n\n## 六、Flume使用案例\n\n介绍几个 Flume 的使用案例：\n\n+ 案例一：使用 Flume 监听文件内容变动，将新增加的内容输出到控制台。\n+ 案例二：使用 Flume 监听指定目录，将目录下新增加的文件存储到 HDFS。\n+ 案例三：使用 Avro 将本服务器收集到的日志数据发送到另外一台服务器。\n\n### 6.1 案例一\n\n需求： 监听文件内容变动，将新增加的内容输出到控制台。\n\n实现： 主要使用 `Exec Source` 配合 `tail` 命令实现。\n\n#### 1. 配置\n\n新建配置文件 `exec-memory-logger.properties`,其内容如下：\n\n```properties\n#指定agent的sources,sinks,channels\na1.sources = s1  \na1.sinks = k1  \na1.channels = c1  \n   \n#配置sources属性\na1.sources.s1.type = exec\na1.sources.s1.command = tail -F /tmp/log.txt\na1.sources.s1.shell = /bin/bash -c\n\n#将sources与channels进行绑定\na1.sources.s1.channels = c1\n   \n#配置sink \na1.sinks.k1.type = logger\n\n#将sinks与channels进行绑定  \na1.sinks.k1.channel = c1  \n   \n#配置channel类型\na1.channels.c1.type = memory\n```\n\n#### 2. 启动　\n\n```shell\nflume-ng agent \\\n--conf conf \\\n--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/exec-memory-logger.properties \\\n--name a1 \\\n-Dflume.root.logger=INFO,console\n```\n\n#### 3. 测试\n\n向文件中追加数据：\n\n<div align=\"center\"> <img  src=\"../pictures/flume-example-1.png\"/> </div>\n\n控制台的显示：\n\n<div align=\"center\"> <img  src=\"../pictures/flume-example-2.png\"/> </div>\n\n\n\n### 6.2 案例二\n\n需求： 监听指定目录，将目录下新增加的文件存储到 HDFS。\n\n实现：使用 `Spooling Directory Source` 和 `HDFS Sink`。\n\n#### 1. 配置\n\n```properties\n#指定agent的sources,sinks,channels\na1.sources = s1  \na1.sinks = k1  \na1.channels = c1  \n   \n#配置sources属性\na1.sources.s1.type =spooldir  \na1.sources.s1.spoolDir =/tmp/logs\na1.sources.s1.basenameHeader = true\na1.sources.s1.basenameHeaderKey = fileName \n#将sources与channels进行绑定  \na1.sources.s1.channels =c1 \n\n   \n#配置sink \na1.sinks.k1.type = hdfs\na1.sinks.k1.hdfs.path = /flume/events/%y-%m-%d/%H/\na1.sinks.k1.hdfs.filePrefix = %{fileName}\n#生成的文件类型，默认是Sequencefile，可用DataStream，则为普通文本\na1.sinks.k1.hdfs.fileType = DataStream  \na1.sinks.k1.hdfs.useLocalTimeStamp = true\n#将sinks与channels进行绑定  \na1.sinks.k1.channel = c1\n   \n#配置channel类型\na1.channels.c1.type = memory\n```\n\n#### 2. 启动\n\n```shell\nflume-ng agent \\\n--conf conf \\\n--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/spooling-memory-hdfs.properties \\\n--name a1 -Dflume.root.logger=INFO,console\n```\n\n#### 3. 测试\n\n拷贝任意文件到监听目录下，可以从日志看到文件上传到 HDFS 的路径：\n\n```shell\n# cp log.txt logs/\n```\n\n<div align=\"center\"> <img  src=\"../pictures/flume-example-3.png\"/> </div>\n\n查看上传到 HDFS 上的文件内容与本地是否一致：\n\n```shell\n# hdfs dfs -cat /flume/events/19-04-09/13/log.txt.1554788567801\n```\n\n<div align=\"center\"> <img  src=\"../pictures/flume-example-4.png\"/> </div>\n\n\n\n### 6.3 案例三\n\n需求： 将本服务器收集到的数据发送到另外一台服务器。\n\n实现：使用 `avro sources` 和 `avro Sink` 实现。\n\n#### 1. 配置日志收集Flume\n\n新建配置 `netcat-memory-avro.properties`，监听文件内容变化，然后将新的文件内容通过 `avro sink` 发送到 hadoop001 这台服务器的 8888 端口：\n\n```properties\n#指定agent的sources,sinks,channels\na1.sources = s1\na1.sinks = k1\na1.channels = c1\n\n#配置sources属性\na1.sources.s1.type = exec\na1.sources.s1.command = tail -F /tmp/log.txt\na1.sources.s1.shell = /bin/bash -c\na1.sources.s1.channels = c1\n\n#配置sink\na1.sinks.k1.type = avro\na1.sinks.k1.hostname = hadoop001\na1.sinks.k1.port = 8888\na1.sinks.k1.batch-size = 1\na1.sinks.k1.channel = c1\n\n#配置channel类型\na1.channels.c1.type = memory\na1.channels.c1.capacity = 1000\na1.channels.c1.transactionCapacity = 100\n```\n\n#### 2. 配置日志聚合Flume\n\n使用 `avro source` 监听 hadoop001 服务器的 8888 端口，将获取到内容输出到控制台：\n\n```properties\n#指定agent的sources,sinks,channels\na2.sources = s2\na2.sinks = k2\na2.channels = c2\n\n#配置sources属性\na2.sources.s2.type = avro\na2.sources.s2.bind = hadoop001\na2.sources.s2.port = 8888\n\n#将sources与channels进行绑定\na2.sources.s2.channels = c2\n\n#配置sink\na2.sinks.k2.type = logger\n\n#将sinks与channels进行绑定\na2.sinks.k2.channel = c2\n\n#配置channel类型\na2.channels.c2.type = memory\na2.channels.c2.capacity = 1000\na2.channels.c2.transactionCapacity = 100\n```\n\n#### 3. 启动\n\n启动日志聚集 Flume：\n\n```shell\nflume-ng agent \\\n--conf conf \\\n--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/avro-memory-logger.properties \\\n--name a2 -Dflume.root.logger=INFO,console\n```\n\n在启动日志收集 Flume:\n\n```shell\nflume-ng agent \\\n--conf conf \\\n--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/netcat-memory-avro.properties \\\n--name a1 -Dflume.root.logger=INFO,console\n```\n\n这里建议按以上顺序启动，原因是 `avro.source` 会先与端口进行绑定，这样 `avro sink` 连接时才不会报无法连接的异常。但是即使不按顺序启动也是没关系的，`sink` 会一直重试，直至建立好连接。\n\n<div align=\"center\"> <img  src=\"../pictures/flume-retry.png\"/> </div>\n\n#### 4.测试\n\n向文件 `tmp/log.txt` 中追加内容：\n\n<div align=\"center\"> <img  src=\"../pictures/flume-example-8.png\"/> </div>\n\n可以看到已经从 8888 端口监听到内容，并成功输出到控制台：\n\n<div align=\"center\"> <img  src=\"../pictures/flume-example-9.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/HDFS-Java-API.md",
    "content": "# HDFS Java API\n\n<nav>\n<a href=\"#一-简介\">一、 简介</a><br/>\n<a href=\"#二API的使用\">二、API的使用</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-FileSystem\">2.1 FileSystem</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-创建目录\">2.2 创建目录</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-创建指定权限的目录\">2.3 创建指定权限的目录</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-创建文件并写入内容\">2.4 创建文件，并写入内容</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-判断文件是否存在\">2.5 判断文件是否存在</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-查看文件内容\">2.6 查看文件内容</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-文件重命名\">2.7 文件重命名</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#28-删除目录或文件\">2.8 删除目录或文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#29-上传文件到HDFS\">2.9 上传文件到HDFS</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#210-上传大文件并显示上传进度\">2.10 上传大文件并显示上传进度</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#211-从HDFS上下载文件\">2.11 从HDFS上下载文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#212-查看指定目录下所有文件的信息\">2.12 查看指定目录下所有文件的信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#213-递归查看指定目录下所有文件的信息\">2.13 递归查看指定目录下所有文件的信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#214-查看文件的块信息\">2.14 查看文件的块信息</a><br/>\n</nav>\n\n## 一、 简介\n\n想要使用 HDFS API，需要导入依赖 `hadoop-client`。如果是 CDH 版本的 Hadoop，还需要额外指明其仓库地址：\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 \n         http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.heibaiying</groupId>\n    <artifactId>hdfs-java-api</artifactId>\n    <version>1.0</version>\n\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <hadoop.version>2.6.0-cdh5.15.2</hadoop.version>\n    </properties>\n\n\n    <!---配置 CDH 仓库地址-->\n    <repositories>\n        <repository>\n            <id>cloudera</id>\n            <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>\n        </repository>\n    </repositories>\n\n\n    <dependencies>\n        <!--Hadoop-client-->\n        <dependency>\n            <groupId>org.apache.hadoop</groupId>\n            <artifactId>hadoop-client</artifactId>\n            <version>${hadoop.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>4.12</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n</project>\n```\n\n\n\n## 二、API的使用\n\n### 2.1 FileSystem\n\nFileSystem 是所有 HDFS 操作的主入口。由于之后的每个单元测试都需要用到它，这里使用 `@Before` 注解进行标注。\n\n```java\nprivate static final String HDFS_PATH = \"hdfs://192.168.0.106:8020\";\nprivate static final String HDFS_USER = \"root\";\nprivate static FileSystem fileSystem;\n\n@Before\npublic void prepare() {\n    try {\n        Configuration configuration = new Configuration();\n        // 这里我启动的是单节点的 Hadoop,所以副本系数设置为 1,默认值为 3\n        configuration.set(\"dfs.replication\", \"1\");\n        fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration, HDFS_USER);\n    } catch (IOException e) {\n        e.printStackTrace();\n    } catch (InterruptedException e) {\n        e.printStackTrace();\n    } catch (URISyntaxException e) {\n        e.printStackTrace();\n    }\n}\n\n\n@After\npublic void destroy() {\n    fileSystem = null;\n}\n```\n\n\n\n### 2.2 创建目录\n\n支持递归创建目录：\n\n```java\n@Test\npublic void mkDir() throws Exception {\n    fileSystem.mkdirs(new Path(\"/hdfs-api/test0/\"));\n}\n```\n\n\n\n### 2.3 创建指定权限的目录\n\n`FsPermission(FsAction u, FsAction g, FsAction o)` 的三个参数分别对应：创建者权限，同组其他用户权限，其他用户权限，权限值定义在 `FsAction` 枚举类中。\n\n```java\n@Test\npublic void mkDirWithPermission() throws Exception {\n    fileSystem.mkdirs(new Path(\"/hdfs-api/test1/\"),\n            new FsPermission(FsAction.READ_WRITE, FsAction.READ, FsAction.READ));\n}\n```\n\n\n\n### 2.4 创建文件，并写入内容\n\n```java\n@Test\npublic void create() throws Exception {\n    // 如果文件存在，默认会覆盖, 可以通过第二个参数进行控制。第三个参数可以控制使用缓冲区的大小\n    FSDataOutputStream out = fileSystem.create(new Path(\"/hdfs-api/test/a.txt\"),\n                                               true, 4096);\n    out.write(\"hello hadoop!\".getBytes());\n    out.write(\"hello spark!\".getBytes());\n    out.write(\"hello flink!\".getBytes());\n    // 强制将缓冲区中内容刷出\n    out.flush();\n    out.close();\n}\n```\n\n\n\n### 2.5 判断文件是否存在\n\n```java\n@Test\npublic void exist() throws Exception {\n    boolean exists = fileSystem.exists(new Path(\"/hdfs-api/test/a.txt\"));\n    System.out.println(exists);\n}\n```\n\n\n\n### 2.6 查看文件内容\n\n查看小文本文件的内容，直接转换成字符串后输出：\n\n```java\n@Test\npublic void readToString() throws Exception {\n    FSDataInputStream inputStream = fileSystem.open(new Path(\"/hdfs-api/test/a.txt\"));\n    String context = inputStreamToString(inputStream, \"utf-8\");\n    System.out.println(context);\n}\n```\n\n`inputStreamToString` 是一个自定义方法，代码如下：\n\n```java\n/**\n * 把输入流转换为指定编码的字符\n *\n * @param inputStream 输入流\n * @param encode      指定编码类型\n */\nprivate static String inputStreamToString(InputStream inputStream, String encode) {\n    try {\n        if (encode == null || (\"\".equals(encode))) {\n            encode = \"utf-8\";\n        }\n        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, encode));\n        StringBuilder builder = new StringBuilder();\n        String str = \"\";\n        while ((str = reader.readLine()) != null) {\n            builder.append(str).append(\"\\n\");\n        }\n        return builder.toString();\n    } catch (IOException e) {\n        e.printStackTrace();\n    }\n    return null;\n}\n```\n\n\n\n### 2.7 文件重命名\n\n```java\n@Test\npublic void rename() throws Exception {\n    Path oldPath = new Path(\"/hdfs-api/test/a.txt\");\n    Path newPath = new Path(\"/hdfs-api/test/b.txt\");\n    boolean result = fileSystem.rename(oldPath, newPath);\n    System.out.println(result);\n}\n```\n\n\n\n### 2.8 删除目录或文件\n\n```java\npublic void delete() throws Exception {\n    /*\n     *  第二个参数代表是否递归删除\n     *    +  如果 path 是一个目录且递归删除为 true, 则删除该目录及其中所有文件;\n     *    +  如果 path 是一个目录但递归删除为 false,则会则抛出异常。\n     */\n    boolean result = fileSystem.delete(new Path(\"/hdfs-api/test/b.txt\"), true);\n    System.out.println(result);\n}\n```\n\n\n\n### 2.9 上传文件到HDFS\n\n```java\n@Test\npublic void copyFromLocalFile() throws Exception {\n    // 如果指定的是目录，则会把目录及其中的文件都复制到指定目录下\n    Path src = new Path(\"D:\\\\BigData-Notes\\\\notes\\\\installation\");\n    Path dst = new Path(\"/hdfs-api/test/\");\n    fileSystem.copyFromLocalFile(src, dst);\n}\n```\n\n\n\n### 2.10 上传大文件并显示上传进度\n\n```java\n@Test\n    public void copyFromLocalBigFile() throws Exception {\n\n        File file = new File(\"D:\\\\kafka.tgz\");\n        final float fileSize = file.length();\n        InputStream in = new BufferedInputStream(new FileInputStream(file));\n\n        FSDataOutputStream out = fileSystem.create(new Path(\"/hdfs-api/test/kafka5.tgz\"),\n                new Progressable() {\n                  long fileCount = 0;\n\n                  public void progress() {\n                     fileCount++;\n                     // progress 方法每上传大约 64KB 的数据后就会被调用一次\n                     System.out.println(\"上传进度：\" + (fileCount * 64 * 1024 / fileSize) * 100 + \" %\");\n                   }\n                });\n\n        IOUtils.copyBytes(in, out, 4096);\n\n    }\n```\n\n\n\n### 2.11 从HDFS上下载文件\n\n```java\n@Test\npublic void copyToLocalFile() throws Exception {\n    Path src = new Path(\"/hdfs-api/test/kafka.tgz\");\n    Path dst = new Path(\"D:\\\\app\\\\\");\n    /*\n     * 第一个参数控制下载完成后是否删除源文件,默认是 true,即删除;\n     * 最后一个参数表示是否将 RawLocalFileSystem 用作本地文件系统;\n     * RawLocalFileSystem 默认为 false,通常情况下可以不设置,\n     * 但如果你在执行时候抛出 NullPointerException 异常,则代表你的文件系统与程序可能存在不兼容的情况 (window 下常见),\n     * 此时可以将 RawLocalFileSystem 设置为 true\n     */\n    fileSystem.copyToLocalFile(false, src, dst, true);\n}\n```\n\n\n\n### 2.12 查看指定目录下所有文件的信息\n\n```java\npublic void listFiles() throws Exception {\n    FileStatus[] statuses = fileSystem.listStatus(new Path(\"/hdfs-api\"));\n    for (FileStatus fileStatus : statuses) {\n        //fileStatus 的 toString 方法被重写过，直接打印可以看到所有信息\n        System.out.println(fileStatus.toString());\n    }\n}\n```\n\n`FileStatus` 中包含了文件的基本信息，比如文件路径，是否是文件夹，修改时间，访问时间，所有者，所属组，文件权限，是否是符号链接等，输出内容示例如下：\n\n```properties\nFileStatus{\npath=hdfs://192.168.0.106:8020/hdfs-api/test; \nisDirectory=true; \nmodification_time=1556680796191; \naccess_time=0; \nowner=root; \ngroup=supergroup; \npermission=rwxr-xr-x; \nisSymlink=false\n}\n```\n\n\n\n### 2.13 递归查看指定目录下所有文件的信息\n\n```java\n@Test\npublic void listFilesRecursive() throws Exception {\n    RemoteIterator<LocatedFileStatus> files = fileSystem.listFiles(new Path(\"/hbase\"), true);\n    while (files.hasNext()) {\n        System.out.println(files.next());\n    }\n}\n```\n\n和上面输出类似，只是多了文本大小，副本系数，块大小信息。\n\n```properties\nLocatedFileStatus{\npath=hdfs://192.168.0.106:8020/hbase/hbase.version; \nisDirectory=false; \nlength=7; \nreplication=1; \nblocksize=134217728; \nmodification_time=1554129052916; \naccess_time=1554902661455; \nowner=root; group=supergroup;\npermission=rw-r--r--; \nisSymlink=false}\n```\n\n\n\n### 2.14 查看文件的块信息\n\n```java\n@Test\npublic void getFileBlockLocations() throws Exception {\n\n    FileStatus fileStatus = fileSystem.getFileStatus(new Path(\"/hdfs-api/test/kafka.tgz\"));\n    BlockLocation[] blocks = fileSystem.getFileBlockLocations(fileStatus, 0, fileStatus.getLen());\n    for (BlockLocation block : blocks) {\n        System.out.println(block);\n    }\n}\n```\n\n块输出信息有三个值，分别是文件的起始偏移量 (offset)，文件大小 (length)，块所在的主机名 (hosts)。\n\n```\n0,57028557,hadoop001\n```\n\n这里我上传的文件只有 57M(小于 128M)，且程序中设置了副本系数为 1，所有只有一个块信息。\n\n<br/>\n\n<br/>\n\n**以上所有测试用例下载地址**：[HDFS Java API](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hdfs-java-api)\n"
  },
  {
    "path": "大数据框架学习/HDFS常用Shell命令.md",
    "content": "# HDFS 常用 shell 命令\n\n**1. 显示当前目录结构**\n\n```shell\n# 显示当前目录结构\nhadoop fs -ls  <path>\n# 递归显示当前目录结构\nhadoop fs -ls  -R  <path>\n# 显示根目录下内容\nhadoop fs -ls  /\n```\n\n**2. 创建目录**\n\n```shell\n# 创建目录\nhadoop fs -mkdir  <path> \n# 递归创建目录\nhadoop fs -mkdir -p  <path>  \n```\n\n**3. 删除操作**\n\n```shell\n# 删除文件\nhadoop fs -rm  <path>\n# 递归删除目录和文件\nhadoop fs -rm -R  <path> \n```\n\n**4. 从本地加载文件到 HDFS**\n\n```shell\n# 二选一执行即可\nhadoop fs -put  [localsrc] [dst] \nhadoop fs - copyFromLocal [localsrc] [dst] \n```\n\n\n**5. 从 HDFS 导出文件到本地**\n\n```shell\n# 二选一执行即可\nhadoop fs -get  [dst] [localsrc] \nhadoop fs -copyToLocal [dst] [localsrc] \n```\n\n**6. 查看文件内容**\n\n```shell\n# 二选一执行即可\nhadoop fs -text  <path> \nhadoop fs -cat  <path>  \n```\n\n**7. 显示文件的最后一千字节**\n\n```shell\nhadoop fs -tail  <path> \n# 和Linux下一样，会持续监听文件内容变化 并显示文件的最后一千字节\nhadoop fs -tail -f  <path> \n```\n\n**8. 拷贝文件**\n\n```shell\nhadoop fs -cp [src] [dst]\n```\n\n**9. 移动文件**\n\n```shell\nhadoop fs -mv [src] [dst] \n```\n\n\n**10. 统计当前目录下各文件大小**  \n+ 默认单位字节  \n+ -s : 显示所有文件大小总和，\n+ -h : 将以更友好的方式显示文件大小（例如 64.0m 而不是 67108864）\n```shell\nhadoop fs -du  <path>  \n```\n\n**11. 合并下载多个文件**\n+ -nl  在每个文件的末尾添加换行符（LF）\n+ -skip-empty-file 跳过空文件\n\n```shell\nhadoop fs -getmerge\n# 示例 将HDFS上的hbase-policy.xml和hbase-site.xml文件合并后下载到本地的/usr/test.xml\nhadoop fs -getmerge -nl  /test/hbase-policy.xml /test/hbase-site.xml /usr/test.xml\n```\n\n**12. 统计文件系统的可用空间信息**\n\n```shell\nhadoop fs -df -h /\n```\n\n**13. 更改文件复制因子**\n```shell\nhadoop fs -setrep [-R] [-w] <numReplicas> <path>\n```\n+ 更改文件的复制因子。如果 path 是目录，则更改其下所有文件的复制因子\n+ -w : 请求命令是否等待复制完成\n\n```shell\n# 示例\nhadoop fs -setrep -w 3 /user/hadoop/dir1\n```\n\n**14. 权限控制**  \n```shell\n# 权限控制和Linux上使用方式一致\n# 变更文件或目录的所属群组。 用户必须是文件的所有者或超级用户。\nhadoop fs -chgrp [-R] GROUP URI [URI ...]\n# 修改文件或目录的访问权限  用户必须是文件的所有者或超级用户。\nhadoop fs -chmod [-R] <MODE[,MODE]... | OCTALMODE> URI [URI ...]\n# 修改文件的拥有者  用户必须是超级用户。\nhadoop fs -chown [-R] [OWNER][:[GROUP]] URI [URI ]\n```\n\n**15. 文件检测**\n```shell\nhadoop fs -test - [defsz]  URI\n```\n可选选项：\n+ -d：如果路径是目录，返回 0。\n+ -e：如果路径存在，则返回 0。\n+ -f：如果路径是文件，则返回 0。\n+ -s：如果路径不为空，则返回 0。\n+ -r：如果路径存在且授予读权限，则返回 0。\n+ -w：如果路径存在且授予写入权限，则返回 0。\n+ -z：如果文件长度为零，则返回 0。\n\n```shell\n# 示例\nhadoop fs -test -e filename\n```\n"
  },
  {
    "path": "大数据框架学习/Hadoop-HDFS.md",
    "content": "# Hadoop分布式文件系统——HDFS\n\n<nav>\n<a href=\"#一介绍\">一、介绍</a><br/>\n<a href=\"#二HDFS-设计原理\">二、HDFS 设计原理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-HDFS-架构\">2.1 HDFS 架构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-文件系统命名空间\">2.2 文件系统命名空间</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-数据复制\">2.3 数据复制</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-数据复制的实现原理\">2.4 数据复制的实现原理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25--副本的选择\">2.5  副本的选择</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-架构的稳定性\">2.6 架构的稳定性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-心跳机制和重新复制\">1. 心跳机制和重新复制</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-数据的完整性\">2. 数据的完整性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3元数据的磁盘故障\">3.元数据的磁盘故障</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#4支持快照\">4.支持快照</a><br/>\n<a href=\"#三HDFS-的特点\">三、HDFS 的特点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-高容错\">3.1 高容错</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-高吞吐量\">3.2 高吞吐量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33--大文件支持\">3.3  大文件支持</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-简单一致性模型\">3.3 简单一致性模型</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-跨平台移植性\">3.4 跨平台移植性</a><br/>\n<a href=\"#附图解HDFS存储原理\">附：图解HDFS存储原理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-HDFS写数据原理\">1. HDFS写数据原理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-HDFS读数据原理\">2. HDFS读数据原理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3-HDFS故障类型和其检测方法\">3. HDFS故障类型和其检测方法</a><br/>\n</nav>\n\n\n\n## 一、介绍\n\n**HDFS** （**Hadoop Distributed File System**）是 Hadoop 下的分布式文件系统，具有高容错、高吞吐量等特性，可以部署在低成本的硬件上。\n\n\n\n## 二、HDFS 设计原理\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/hdfsarchitecture.png\"/> </div>\n\n### 2.1 HDFS 架构\n\nHDFS 遵循主/从架构，由单个 NameNode(NN) 和多个 DataNode(DN) 组成：\n\n- **NameNode** : 负责执行有关 ` 文件系统命名空间 ` 的操作，例如打开，关闭、重命名文件和目录等。它同时还负责集群元数据的存储，记录着文件中各个数据块的位置信息。\n- **DataNode**：负责提供来自文件系统客户端的读写请求，执行块的创建，删除等操作。\n\n\n\n### 2.2 文件系统命名空间\n\nHDFS 的 ` 文件系统命名空间 ` 的层次结构与大多数文件系统类似 (如 Linux)， 支持目录和文件的创建、移动、删除和重命名等操作，支持配置用户和访问权限，但不支持硬链接和软连接。`NameNode` 负责维护文件系统名称空间，记录对名称空间或其属性的任何更改。\n\n\n\n### 2.3 数据复制\n\n由于 Hadoop 被设计运行在廉价的机器上，这意味着硬件是不可靠的，为了保证容错性，HDFS 提供了数据复制机制。HDFS 将每一个文件存储为一系列**块**，每个块由多个副本来保证容错，块的大小和复制因子可以自行配置（默认情况下，块大小是 128M，默认复制因子是 3）。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/hdfsdatanodes.png\"/> </div>\n\n### 2.4 数据复制的实现原理\n\n大型的 HDFS 实例在通常分布在多个机架的多台服务器上，不同机架上的两台服务器之间通过交换机进行通讯。在大多数情况下，同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。因此 HDFS 采用机架感知副本放置策略，对于常见情况，当复制因子为 3 时，HDFS 的放置策略是：\n\n在写入程序位于 `datanode` 上时，就优先将写入文件的一个副本放置在该 `datanode` 上，否则放在随机 `datanode` 上。之后在另一个远程机架上的任意一个节点上放置另一个副本，并在该机架上的另一个节点上放置最后一个副本。此策略可以减少机架间的写入流量，从而提高写入性能。\n\n<div align=\"center\"> <img src=\"../pictures/hdfs-机架.png\"/> </div>\n\n如果复制因子大于 3，则随机确定第 4 个和之后副本的放置位置，同时保持每个机架的副本数量低于上限，上限值通常为 `（复制系数 - 1）/机架数量 + 2`，需要注意的是不允许同一个 `dataNode` 上具有同一个块的多个副本。\n\n\n\n### 2.5  副本的选择\n\n为了最大限度地减少带宽消耗和读取延迟，HDFS 在执行读取请求时，优先读取距离读取器最近的副本。如果在与读取器节点相同的机架上存在副本，则优先选择该副本。如果 HDFS 群集跨越多个数据中心，则优先选择本地数据中心上的副本。\n\n\n\n### 2.6 架构的稳定性\n\n#### 1. 心跳机制和重新复制\n\n每个 DataNode 定期向 NameNode 发送心跳消息，如果超过指定时间没有收到心跳消息，则将 DataNode 标记为死亡。NameNode 不会将任何新的 IO 请求转发给标记为死亡的 DataNode，也不会再使用这些 DataNode 上的数据。 由于数据不再可用，可能会导致某些块的复制因子小于其指定值，NameNode 会跟踪这些块，并在必要的时候进行重新复制。\n\n#### 2. 数据的完整性\n\n由于存储设备故障等原因，存储在 DataNode 上的数据块也会发生损坏。为了避免读取到已经损坏的数据而导致错误，HDFS 提供了数据完整性校验机制来保证数据的完整性，具体操作如下：\n\n当客户端创建 HDFS 文件时，它会计算文件的每个块的 ` 校验和 `，并将 ` 校验和 ` 存储在同一 HDFS 命名空间下的单独的隐藏文件中。当客户端检索文件内容时，它会验证从每个 DataNode 接收的数据是否与存储在关联校验和文件中的 ` 校验和 ` 匹配。如果匹配失败，则证明数据已经损坏，此时客户端会选择从其他 DataNode 获取该块的其他可用副本。\n\n#### 3.元数据的磁盘故障\n\n`FsImage` 和 `EditLog` 是 HDFS 的核心数据，这些数据的意外丢失可能会导致整个 HDFS 服务不可用。为了避免这个问题，可以配置 NameNode 使其支持 `FsImage` 和 `EditLog` 多副本同步，这样 `FsImage` 或 `EditLog` 的任何改变都会引起每个副本 `FsImage` 和 `EditLog` 的同步更新。\n\n#### 4.支持快照\n\n快照支持在特定时刻存储数据副本，在数据意外损坏时，可以通过回滚操作恢复到健康的数据状态。\n\n\n\n## 三、HDFS 的特点\n\n### 3.1 高容错\n\n由于 HDFS 采用数据的多副本方案，所以部分硬件的损坏不会导致全部数据的丢失。\n\n### 3.2 高吞吐量\n\nHDFS 设计的重点是支持高吞吐量的数据访问，而不是低延迟的数据访问。\n\n### 3.3  大文件支持\n\nHDFS 适合于大文件的存储，文档的大小应该是 GB 到 TB 级别的。\n\n### 3.3 简单一致性模型\n\nHDFS 更适合于一次写入多次读取 (write-once-read-many) 的访问模型。支持将内容追加到文件末尾，但不支持数据的随机访问，不能从文件任意位置新增数据。\n\n### 3.4 跨平台移植性\n\nHDFS 具有良好的跨平台移植性，这使得其他大数据计算框架都将其作为数据持久化存储的首选方案。\n\n\n\n## 附：图解HDFS存储原理\n\n> 说明：以下图片引用自博客：[翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491)\n\n### 1. HDFS写数据原理\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-write-1.jpg\"/> </div>\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-write-2.jpg\"/> </div>\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-write-3.jpg\"/> </div>\n\n\n\n### 2. HDFS读数据原理\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-read-1.jpg\"/> </div>\n\n\n\n### 3. HDFS故障类型和其检测方法\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-tolerance-1.jpg\"/> </div>\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-tolerance-2.jpg\"/> </div>\n\n\n\n**第二部分：读写故障的处理**\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-tolerance-3.jpg\"/> </div>\n\n\n\n**第三部分：DataNode 故障处理**\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-tolerance-4.jpg\"/> </div>\n\n\n\n**副本布局策略**：\n\n<div align=\"center\"> <img  src=\"../pictures/hdfs-tolerance-5.jpg\"/> </div>\n\n\n\n## 参考资料\n\n1. [Apache Hadoop 2.9.2 > HDFS Architecture](http://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html)\n2. Tom White . hadoop 权威指南 [M] . 清华大学出版社 . 2017.\n3. [翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491)\n\n"
  },
  {
    "path": "大数据框架学习/Hadoop-MapReduce.md",
    "content": "# 分布式计算框架——MapReduce\n\n<nav>\n<a href=\"#一MapReduce概述\">一、MapReduce概述</a><br/>\n<a href=\"#二MapReduce编程模型简述\">二、MapReduce编程模型简述</a><br/>\n<a href=\"#三combiner--partitioner\">三、combiner & partitioner</a><br/>\n<a href=\"#四MapReduce词频统计案例\">四、MapReduce词频统计案例</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-项目简介\">4.1 项目简介</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-项目依赖\">4.2 项目依赖</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-WordCountMapper\">4.3 WordCountMapper</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44-WordCountReducer\">4.4 WordCountReducer</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44-WordCountApp\">4.4 WordCountApp</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#45-提交到服务器运行\">4.5 提交到服务器运行</a><br/>\n<a href=\"#五词频统计案例进阶之Combiner\">五、词频统计案例进阶之Combiner</a><br/>\n<a href=\"#六词频统计案例进阶之Partitioner\">六、词频统计案例进阶之Partitioner</a><br/>\n</nav>\n\n\n\n\n## 一、MapReduce概述\n\nHadoop MapReduce 是一个分布式计算框架，用于编写批处理应用程序。编写好的程序可以提交到 Hadoop 集群上用于并行处理大规模的数据集。\n\nMapReduce 作业通过将输入的数据集拆分为独立的块，这些块由 `map` 以并行的方式处理，框架对 `map` 的输出进行排序，然后输入到 `reduce` 中。MapReduce 框架专门用于 `<key，value>` 键值对处理，它将作业的输入视为一组 `<key，value>` 对，并生成一组 `<key，value>` 对作为输出。输出和输出的 `key` 和 `value` 都必须实现[Writable](http://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/Writable.html) 接口。\n\n```\n(input) <k1, v1> -> map -> <k2, v2> -> combine -> <k2, v2> -> reduce -> <k3, v3> (output)\n```\n\n\n\n## 二、MapReduce编程模型简述\n\n这里以词频统计为例进行说明，MapReduce 处理的流程如下：\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/mapreduceProcess.png\"/> </div>\n\n1. **input** : 读取文本文件；\n\n2. **splitting** : 将文件按照行进行拆分，此时得到的 `K1` 行数，`V1` 表示对应行的文本内容；\n\n3. **mapping** : 并行将每一行按照空格进行拆分，拆分得到的 `List(K2,V2)`，其中 `K2` 代表每一个单词，由于是做词频统计，所以 `V2` 的值为 1，代表出现 1 次；\n4. **shuffling**：由于 `Mapping` 操作可能是在不同的机器上并行处理的，所以需要通过 `shuffling` 将相同 `key` 值的数据分发到同一个节点上去合并，这样才能统计出最终的结果，此时得到 `K2` 为每一个单词，`List(V2)` 为可迭代集合，`V2` 就是 Mapping 中的 V2；\n5. **Reducing** : 这里的案例是统计单词出现的总次数，所以 `Reducing` 对 `List(V2)` 进行归约求和操作，最终输出。\n\nMapReduce 编程模型中 `splitting` 和 `shuffing` 操作都是由框架实现的，需要我们自己编程实现的只有 `mapping` 和 `reducing`，这也就是 MapReduce 这个称呼的来源。\n\n\n\n## 三、combiner & partitioner\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/Detailed-Hadoop-MapReduce-Data-Flow-14.png\"/> </div>\n\n### 3.1 InputFormat & RecordReaders \n\n`InputFormat` 将输出文件拆分为多个 `InputSplit`，并由 `RecordReaders` 将 `InputSplit` 转换为标准的<key，value>键值对，作为 map 的输出。这一步的意义在于只有先进行逻辑拆分并转为标准的键值对格式后，才能为多个 `map` 提供输入，以便进行并行处理。\n\n\n\n### 3.2 Combiner\n\n`combiner` 是 `map` 运算后的可选操作，它实际上是一个本地化的 `reduce` 操作，它主要是在 `map` 计算出中间文件后做一个简单的合并重复 `key` 值的操作。这里以词频统计为例：\n\n`map` 在遇到一个 hadoop 的单词时就会记录为 1，但是这篇文章里 hadoop 可能会出现 n 多次，那么 `map` 输出文件冗余就会很多，因此在 `reduce` 计算前对相同的 key 做一个合并操作，那么需要传输的数据量就会减少，传输效率就可以得到提升。\n\n但并非所有场景都适合使用 `combiner`，使用它的原则是 `combiner` 的输出不会影响到 `reduce` 计算的最终输入，例如：求总数，最大值，最小值时都可以使用 `combiner`，但是做平均值计算则不能使用 `combiner`。\n\n不使用 combiner 的情况：\n\n<div align=\"center\"> <img  width=\"600px\"  src=\"../pictures/mapreduce-without-combiners.png\"/> </div>\n\n使用 combiner 的情况：\n\n<div align=\"center\"> <img width=\"600px\"  src=\"../pictures/mapreduce-with-combiners.png\"/> </div>\n\n\n\n可以看到使用 combiner 的时候，需要传输到 reducer 中的数据由 12keys，降低到 10keys。降低的幅度取决于你 keys 的重复率，下文词频统计案例会演示用 combiner 降低数百倍的传输量。\n\n### 3.3 Partitioner\n\n`partitioner` 可以理解成分类器，将 `map` 的输出按照 key 值的不同分别分给对应的 `reducer`，支持自定义实现，下文案例会给出演示。\n\n\n\n## 四、MapReduce词频统计案例\n\n### 4.1 项目简介\n\n这里给出一个经典的词频统计的案例：统计如下样本数据中每个单词出现的次数。\n\n```properties\nSpark\tHBase\nHive\tFlink\tStorm\tHadoop\tHBase\tSpark\nFlink\nHBase\tStorm\nHBase\tHadoop\tHive\tFlink\nHBase\tFlink\tHive\tStorm\nHive\tFlink\tHadoop\nHBase\tHive\nHadoop\tSpark\tHBase\tStorm\nHBase\tHadoop\tHive\tFlink\nHBase\tFlink\tHive\tStorm\nHive\tFlink\tHadoop\nHBase\tHive\n```\n\n为方便大家开发，我在项目源码中放置了一个工具类 `WordCountDataUtils`，用于模拟产生词频统计的样本，生成的文件支持输出到本地或者直接写到 HDFS 上。\n\n> 项目完整源码下载地址：[hadoop-word-count](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hadoop-word-count)\n\n\n\n### 4.2 项目依赖\n\n想要进行 MapReduce 编程，需要导入 `hadoop-client` 依赖：\n\n```xml\n<dependency>\n    <groupId>org.apache.hadoop</groupId>\n    <artifactId>hadoop-client</artifactId>\n    <version>${hadoop.version}</version>\n</dependency>\n```\n\n### 4.3 WordCountMapper\n\n将每行数据按照指定分隔符进行拆分。这里需要注意在 MapReduce 中必须使用 Hadoop 定义的类型，因为 Hadoop 预定义的类型都是可序列化，可比较的，所有类型均实现了 `WritableComparable` 接口。\n\n```java\npublic class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {\n\n    @Override\n    protected void map(LongWritable key, Text value, Context context) throws IOException, \n                                                                      InterruptedException {\n        String[] words = value.toString().split(\"\\t\");\n        for (String word : words) {\n            context.write(new Text(word), new IntWritable(1));\n        }\n    }\n\n}\n```\n\n`WordCountMapper` 对应下图的 Mapping 操作：\n\n<div align=\"center\"> <img  src=\"../pictures/hadoop-code-mapping.png\"/> </div>\n\n\n\n`WordCountMapper` 继承自 `Mappe` 类，这是一个泛型类，定义如下：\n\n```java\nWordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>\n\npublic class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {\n   ......\n}\n```\n\n+ **KEYIN** : `mapping` 输入 key 的类型，即每行的偏移量 (每行第一个字符在整个文本中的位置)，`Long` 类型，对应 Hadoop 中的 `LongWritable` 类型；\n+ **VALUEIN** : `mapping` 输入 value 的类型，即每行数据；`String` 类型，对应 Hadoop 中 `Text` 类型；\n+ **KEYOUT** ：`mapping` 输出的 key 的类型，即每个单词；`String` 类型，对应 Hadoop 中 `Text` 类型；\n+ **VALUEOUT**：`mapping` 输出 value 的类型，即每个单词出现的次数；这里用 `int` 类型，对应 `IntWritable` 类型。\n\n\n\n### 4.4 WordCountReducer\n\n在 Reduce 中进行单词出现次数的统计：\n\n```java\npublic class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {\n\n    @Override\n    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, \n                                                                                  InterruptedException {\n        int count = 0;\n        for (IntWritable value : values) {\n            count += value.get();\n        }\n        context.write(key, new IntWritable(count));\n    }\n}\n```\n\n如下图，`shuffling` 的输出是 reduce 的输入。这里的 key 是每个单词，values 是一个可迭代的数据类型，类似 `(1,1,1,...)`。\n\n<div align=\"center\"> <img  src=\"../pictures/hadoop-code-reducer.png\"/> </div>\n\n### 4.4 WordCountApp\n\n组装 MapReduce 作业，并提交到服务器运行，代码如下：\n\n```java\n\n/**\n * 组装作业 并提交到集群运行\n */\npublic class WordCountApp {\n\n\n    // 这里为了直观显示参数 使用了硬编码，实际开发中可以通过外部传参\n    private static final String HDFS_URL = \"hdfs://192.168.0.107:8020\";\n    private static final String HADOOP_USER_NAME = \"root\";\n\n    public static void main(String[] args) throws Exception {\n\n        //  文件输入路径和输出路径由外部传参指定\n        if (args.length < 2) {\n            System.out.println(\"Input and output paths are necessary!\");\n            return;\n        }\n\n        // 需要指明 hadoop 用户名，否则在 HDFS 上创建目录时可能会抛出权限不足的异常\n        System.setProperty(\"HADOOP_USER_NAME\", HADOOP_USER_NAME);\n\n        Configuration configuration = new Configuration();\n        // 指明 HDFS 的地址\n        configuration.set(\"fs.defaultFS\", HDFS_URL);\n\n        // 创建一个 Job\n        Job job = Job.getInstance(configuration);\n\n        // 设置运行的主类\n        job.setJarByClass(WordCountApp.class);\n\n        // 设置 Mapper 和 Reducer\n        job.setMapperClass(WordCountMapper.class);\n        job.setReducerClass(WordCountReducer.class);\n\n        // 设置 Mapper 输出 key 和 value 的类型\n        job.setMapOutputKeyClass(Text.class);\n        job.setMapOutputValueClass(IntWritable.class);\n\n        // 设置 Reducer 输出 key 和 value 的类型\n        job.setOutputKeyClass(Text.class);\n        job.setOutputValueClass(IntWritable.class);\n\n        // 如果输出目录已经存在，则必须先删除，否则重复运行程序时会抛出异常\n        FileSystem fileSystem = FileSystem.get(new URI(HDFS_URL), configuration, HADOOP_USER_NAME);\n        Path outputPath = new Path(args[1]);\n        if (fileSystem.exists(outputPath)) {\n            fileSystem.delete(outputPath, true);\n        }\n\n        // 设置作业输入文件和输出文件的路径\n        FileInputFormat.setInputPaths(job, new Path(args[0]));\n        FileOutputFormat.setOutputPath(job, outputPath);\n\n        // 将作业提交到群集并等待它完成，参数设置为 true 代表打印显示对应的进度\n        boolean result = job.waitForCompletion(true);\n\n        // 关闭之前创建的 fileSystem\n        fileSystem.close();\n\n        // 根据作业结果,终止当前运行的 Java 虚拟机,退出程序\n        System.exit(result ? 0 : -1);\n\n    }\n}\n```\n\n需要注意的是：如果不设置 `Mapper` 操作的输出类型，则程序默认它和 `Reducer` 操作输出的类型相同。\n\n### 4.5 提交到服务器运行\n\n在实际开发中，可以在本机配置 hadoop 开发环境，直接在 IDE 中启动进行测试。这里主要介绍一下打包提交到服务器运行。由于本项目没有使用除 Hadoop 外的第三方依赖，直接打包即可：\n\n```shell\n# mvn clean package\n```\n\n使用以下命令提交作业：\n\n```shell\nhadoop jar /usr/appjar/hadoop-word-count-1.0.jar \\\ncom.heibaiying.WordCountApp \\\n/wordcount/input.txt /wordcount/output/WordCountApp\n```\n\n作业完成后查看 HDFS 上生成目录：\n\n```shell\n# 查看目录\nhadoop fs -ls /wordcount/output/WordCountApp\n\n# 查看统计结果\nhadoop fs -cat /wordcount/output/WordCountApp/part-r-00000\n```\n\n<div align=\"center\"> <img  src=\"../pictures/hadoop-wordcountapp.png\"/> </div>\n\n\n\n## 五、词频统计案例进阶之Combiner\n\n### 5.1 代码实现\n\n想要使用 `combiner` 功能只要在组装作业时，添加下面一行代码即可：\n\n```java\n// 设置 Combiner\njob.setCombinerClass(WordCountReducer.class);\n```\n\n### 5.2 执行结果\n\n加入 `combiner` 后统计结果是不会有变化的，但是可以从打印的日志看出 `combiner` 的效果：\n\n没有加入 `combiner` 的打印日志：\n\n<div align=\"center\"> <img  src=\"../pictures/hadoop-no-combiner.png\"/> </div>\n\n加入 `combiner` 后的打印日志如下：\n\n<div align=\"center\"> <img  src=\"../pictures/hadoop-combiner.png\"/> </div>\n\n这里我们只有一个输入文件并且小于 128M，所以只有一个 Map 进行处理。可以看到经过 combiner 后，records 由 `3519` 降低为 `6`(样本中单词种类就只有 6 种)，在这个用例中 combiner 就能极大地降低需要传输的数据量。\n\n\n\n## 六、词频统计案例进阶之Partitioner\n\n### 6.1  默认的Partitioner\n\n这里假设有个需求：将不同单词的统计结果输出到不同文件。这种需求实际上比较常见，比如统计产品的销量时，需要将结果按照产品种类进行拆分。要实现这个功能，就需要用到自定义 `Partitioner`。\n\n这里先介绍下 MapReduce 默认的分类规则：在构建 job 时候，如果不指定，默认的使用的是 `HashPartitioner`：对 key 值进行哈希散列并对 `numReduceTasks` 取余。其实现如下：\n\n```java\npublic class HashPartitioner<K, V> extends Partitioner<K, V> {\n\n  public int getPartition(K key, V value,\n                          int numReduceTasks) {\n    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;\n  }\n\n}\n```\n\n### 6.2 自定义Partitioner\n\n这里我们继承 `Partitioner` 自定义分类规则，这里按照单词进行分类：\n\n```java\npublic class CustomPartitioner extends Partitioner<Text, IntWritable> {\n\n    public int getPartition(Text text, IntWritable intWritable, int numPartitions) {\n        return WordCountDataUtils.WORD_LIST.indexOf(text.toString());\n    }\n}\n```\n\n在构建 `job` 时候指定使用我们自己的分类规则，并设置 `reduce` 的个数：\n\n```java\n// 设置自定义分区规则\njob.setPartitionerClass(CustomPartitioner.class);\n// 设置 reduce 个数\njob.setNumReduceTasks(WordCountDataUtils.WORD_LIST.size());\n```\n\n\n\n### 6.3  执行结果\n\n执行结果如下，分别生成 6 个文件，每个文件中为对应单词的统计结果：\n\n<div align=\"center\"> <img  src=\"../pictures/hadoop-wordcountcombinerpartition.png\"/> </div>\n\n\n\n\n\n## 参考资料\n\n1. [分布式计算框架 MapReduce](https://zhuanlan.zhihu.com/p/28682581)\n2. [Apache Hadoop 2.9.2 > MapReduce Tutorial](http://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html)\n3. [MapReduce - Combiners]( https://www.tutorialscampus.com/tutorials/map-reduce/combiners.htm)\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Hadoop-YARN.md",
    "content": "# 集群资源管理器——YARN\n\n<nav>\n<a href=\"#一hadoop-yarn-简介\">一、hadoop yarn 简介</a><br/>\n<a href=\"#二YARN架构\">二、YARN架构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-ResourceManager\">1. ResourceManager</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-NodeManager\">2. NodeManager</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3-ApplicationMaster\">3. ApplicationMaster </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#4-Contain\">4. Contain</a><br/>\n<a href=\"#三YARN工作原理简述\">三、YARN工作原理简述</a><br/>\n<a href=\"#四YARN工作原理详述\">四、YARN工作原理详述</a><br/>\n<a href=\"#五提交作业到YARN上运行\">五、提交作业到YARN上运行</a><br/>\n</nav>\n\n\n\n## 一、hadoop yarn 简介\n\n**Apache YARN** (Yet Another Resource Negotiator)  是 hadoop 2.0 引入的集群资源管理系统。用户可以将各种服务框架部署在 YARN 上，由 YARN 进行统一地管理和资源分配。\n\n<div align=\"center\"> <img width=\"600px\"  src=\"../pictures/yarn-base.png\"/> </div>\n\n\n\n## 二、YARN架构\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/Figure3Architecture-of-YARN.png\"/> </div>\n\n### 1. ResourceManager\n\n`ResourceManager` 通常在独立的机器上以后台进程的形式运行，它是整个集群资源的主要协调者和管理者。`ResourceManager` 负责给用户提交的所有应用程序分配资源，它根据应用程序优先级、队列容量、ACLs、数据位置等信息，做出决策，然后以共享的、安全的、多租户的方式制定分配策略，调度集群资源。\n\n### 2. NodeManager\n\n`NodeManager` 是 YARN 集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理，监视资源和跟踪节点健康。具体如下：\n\n- 启动时向 `ResourceManager` 注册并定时发送心跳消息，等待 `ResourceManager` 的指令；\n- 维护 `Container` 的生命周期，监控 `Container` 的资源使用情况；\n- 管理任务运行时的相关依赖，根据 `ApplicationMaster` 的需要，在启动 `Container` 之前将需要的程序及其依赖拷贝到本地。\n\n### 3. ApplicationMaster \n\n在用户提交一个应用程序时，YARN 会启动一个轻量级的进程 `ApplicationMaster`。`ApplicationMaster` 负责协调来自 `ResourceManager` 的资源，并通过 `NodeManager` 监视容器内资源的使用情况，同时还负责任务的监控与容错。具体如下：\n\n- 根据应用的运行状态来决定动态计算资源需求；\n- 向 `ResourceManager` 申请资源，监控申请的资源的使用情况；\n- 跟踪任务状态和进度，报告资源的使用情况和应用的进度信息；\n- 负责任务的容错。\n\n### 4. Container\n\n`Container` 是 YARN 中的资源抽象，它封装了某个节点上的多维度资源，如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时，RM 为 AM 返回的资源是用 `Container` 表示的。YARN 会为每个任务分配一个 `Container`，该任务只能使用该 `Container` 中描述的资源。`ApplicationMaster` 可在 `Container` 内运行任何类型的任务。例如，`MapReduce ApplicationMaster` 请求一个容器来启动 map 或 reduce 任务，而 `Giraph ApplicationMaster` 请求一个容器来运行 Giraph 任务。\n\n\n\n\n\n## 三、YARN工作原理简述\n\n<div align=\"center\"> <img src=\"../pictures/yarn工作原理简图.png\"/> </div>\n\n1. `Client` 提交作业到 YARN 上；\n\n2. `Resource Manager` 选择一个 `Node Manager`，启动一个 `Container` 并运行 `Application Master` 实例；\n\n3. `Application Master` 根据实际需要向 `Resource Manager` 请求更多的 `Container` 资源（如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务）；\n\n4. `Application Master` 通过获取到的 `Container` 资源执行分布式计算。\n\n   \n\n## 四、YARN工作原理详述\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/yarn工作原理.png\"/> </div>\n\n\n\n#### 1. 作业提交\n\nclient 调用 job.waitForCompletion 方法，向整个集群提交 MapReduce 作业 (第 1 步) 。新的作业 ID(应用 ID) 由资源管理器分配 (第 2 步)。作业的 client 核实作业的输出, 计算输入的 split, 将作业的资源 (包括 Jar 包，配置文件, split 信息) 拷贝给 HDFS(第 3 步)。 最后, 通过调用资源管理器的 submitApplication() 来提交作业 (第 4 步)。\n\n#### 2. 作业初始化\n\n当资源管理器收到 submitApplciation() 的请求时, 就将该请求发给调度器 (scheduler), 调度器分配 container, 然后资源管理器在该 container 内启动应用管理器进程, 由节点管理器监控 (第 5 步)。\n\nMapReduce 作业的应用管理器是一个主类为 MRAppMaster 的 Java 应用，其通过创造一些 bookkeeping 对象来监控作业的进度,  得到任务的进度和完成报告 (第 6 步)。然后其通过分布式文件系统得到由客户端计算好的输入 split(第 7 步)，然后为每个输入 split 创建一个 map 任务, 根据 mapreduce.job.reduces 创建 reduce 任务对象。\n\n#### 3. 任务分配\n\n如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务。\n\n如果不是小作业,  那么应用管理器向资源管理器请求 container 来运行所有的 map 和 reduce 任务 (第 8 步)。这些请求是通过心跳来传输的,  包括每个 map 任务的数据位置，比如存放输入 split 的主机名和机架 (rack)，调度器利用这些信息来调度任务，尽量将任务分配给存储数据的节点, 或者分配给和存放输入 split 的节点相同机架的节点。\n\n#### 4. 任务运行\n\n当一个任务由资源管理器的调度器分配给一个 container 后，应用管理器通过联系节点管理器来启动 container(第 9 步)。任务由一个主类为 YarnChild 的 Java 应用执行， 在运行任务之前首先本地化任务需要的资源，比如作业配置，JAR 文件,  以及分布式缓存的所有文件 (第 10 步。 最后, 运行 map 或 reduce 任务 (第 11 步)。\n\nYarnChild 运行在一个专用的 JVM 中, 但是 YARN 不支持 JVM 重用。\n\n#### 5. 进度和状态更新\n\nYARN 中的任务将其进度和状态 (包括 counter) 返回给应用管理器, 客户端每秒 (通 mapreduce.client.progressmonitor.pollinterval 设置) 向应用管理器请求进度更新, 展示给用户。\n\n#### 6. 作业完成\n\n除了向应用管理器请求作业进度外,  客户端每 5 分钟都会通过调用 waitForCompletion() 来检查作业是否完成，时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业完成之后,  应用管理器和 container 会清理工作状态， OutputCommiter 的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。\n\n\n\n## 五、提交作业到YARN上运行\n\n这里以提交 Hadoop Examples 中计算 Pi 的 MApReduce 程序为例，相关 Jar 包在 Hadoop 安装目录的 `share/hadoop/mapreduce` 目录下：\n\n```shell\n# 提交格式: hadoop jar jar包路径 主类名称 主类参数\n# hadoop jar hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3\n```\n\n\n\n## 参考资料\n\n1. [初步掌握 Yarn 的架构及原理](https://www.cnblogs.com/codeOfLife/p/5492740.html)\n\n2. [Apache Hadoop 2.9.2 > Apache Hadoop YARN](http://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html)\n\n   \n\n"
  },
  {
    "path": "大数据框架学习/Hbase_Java_API.md",
    "content": "# HBase Java API 的基本使用\n\n<nav>\n<a href=\"#一简述\">一、简述</a><br/>\n<a href=\"#二Java-API-1x-基本使用\">二、Java API 1.x 基本使用</a><br/>\n<a href=\"#三Java-API-2x-基本使用\">三、Java API 2.x 基本使用</a><br/>\n<a href=\"#四正确连接Hbase\">四、正确连接Hbase</a><br/>\n</nav>\n\n\n\n## 一、简述\n\n截至到目前 (2019.04)，HBase 有两个主要的版本，分别是 1.x 和 2.x ，两个版本的 Java API 有所不同，1.x 中某些方法在 2.x 中被标识为 `@deprecated` 过时。所以下面关于 API 的样例，我会分别给出 1.x 和 2.x 两个版本。完整的代码见本仓库：\n\n>+ [Java API 1.x Examples](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase/hbase-java-api-1.x)\n>\n>+ [Java API 2.x Examples](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase/hbase-java-api-2.x)\n\n同时你使用的客户端的版本必须与服务端版本保持一致，如果用 2.x 版本的客户端代码去连接 1.x 版本的服务端，会抛出 `NoSuchColumnFamilyException` 等异常。\n\n## 二、Java API 1.x 基本使用\n\n#### 2.1 新建Maven工程，导入项目依赖\n\n要使用 Java API 操作 HBase，需要引入 `hbase-client`。这里选取的 `HBase Client` 的版本为 `1.2.0`。\n\n```xml\n<dependency>\n    <groupId>org.apache.hbase</groupId>\n    <artifactId>hbase-client</artifactId>\n    <version>1.2.0</version>\n</dependency>\n```\n\n#### 2.2 API 基本使用\n\n```java\npublic class HBaseUtils {\n\n    private static Connection connection;\n\n    static {\n        Configuration configuration = HBaseConfiguration.create();\n        configuration.set(\"hbase.zookeeper.property.clientPort\", \"2181\");\n        // 如果是集群 则主机名用逗号分隔\n        configuration.set(\"hbase.zookeeper.quorum\", \"hadoop001\");\n        try {\n            connection = ConnectionFactory.createConnection(configuration);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * 创建 HBase 表\n     *\n     * @param tableName      表名\n     * @param columnFamilies 列族的数组\n     */\n    public static boolean createTable(String tableName, List<String> columnFamilies) {\n        try {\n            HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();\n            if (admin.tableExists(tableName)) {\n                return false;\n            }\n            HTableDescriptor tableDescriptor = new HTableDescriptor(TableName.valueOf(tableName));\n            columnFamilies.forEach(columnFamily -> {\n                HColumnDescriptor columnDescriptor = new HColumnDescriptor(columnFamily);\n                columnDescriptor.setMaxVersions(1);\n                tableDescriptor.addFamily(columnDescriptor);\n            });\n            admin.createTable(tableDescriptor);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 删除 hBase 表\n     *\n     * @param tableName 表名\n     */\n    public static boolean deleteTable(String tableName) {\n        try {\n            HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();\n            // 删除表前需要先禁用表\n            admin.disableTable(tableName);\n            admin.deleteTable(tableName);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n    /**\n     * 插入数据\n     *\n     * @param tableName        表名\n     * @param rowKey           唯一标识\n     * @param columnFamilyName 列族名\n     * @param qualifier        列标识\n     * @param value            数据\n     */\n    public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier,\n                                 String value) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Put put = new Put(Bytes.toBytes(rowKey));\n            put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value));\n            table.put(put);\n            table.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 插入数据\n     *\n     * @param tableName        表名\n     * @param rowKey           唯一标识\n     * @param columnFamilyName 列族名\n     * @param pairList         列标识和值的集合\n     */\n    public static boolean putRow(String tableName, String rowKey, String columnFamilyName, List<Pair<String, String>> pairList) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Put put = new Put(Bytes.toBytes(rowKey));\n            pairList.forEach(pair -> put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(pair.getKey()), Bytes.toBytes(pair.getValue())));\n            table.put(put);\n            table.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 根据 rowKey 获取指定行的数据\n     *\n     * @param tableName 表名\n     * @param rowKey    唯一标识\n     */\n    public static Result getRow(String tableName, String rowKey) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Get get = new Get(Bytes.toBytes(rowKey));\n            return table.get(get);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n\n    /**\n     * 获取指定行指定列 (cell) 的最新版本的数据\n     *\n     * @param tableName    表名\n     * @param rowKey       唯一标识\n     * @param columnFamily 列族\n     * @param qualifier    列标识\n     */\n    public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Get get = new Get(Bytes.toBytes(rowKey));\n            if (!get.isCheckExistenceOnly()) {\n                get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));\n                Result result = table.get(get);\n                byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));\n                return Bytes.toString(resultValue);\n            } else {\n                return null;\n            }\n\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n\n    /**\n     * 检索全表\n     *\n     * @param tableName 表名\n     */\n    public static ResultScanner getScanner(String tableName) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Scan scan = new Scan();\n            return table.getScanner(scan);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n\n    /**\n     * 检索表中指定数据\n     *\n     * @param tableName  表名\n     * @param filterList 过滤器\n     */\n\n    public static ResultScanner getScanner(String tableName, FilterList filterList) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Scan scan = new Scan();\n            scan.setFilter(filterList);\n            return table.getScanner(scan);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    /**\n     * 检索表中指定数据\n     *\n     * @param tableName   表名\n     * @param startRowKey 起始 RowKey\n     * @param endRowKey   终止 RowKey\n     * @param filterList  过滤器\n     */\n\n    public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey,\n                                           FilterList filterList) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Scan scan = new Scan();\n            scan.setStartRow(Bytes.toBytes(startRowKey));\n            scan.setStopRow(Bytes.toBytes(endRowKey));\n            scan.setFilter(filterList);\n            return table.getScanner(scan);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    /**\n     * 删除指定行记录\n     *\n     * @param tableName 表名\n     * @param rowKey    唯一标识\n     */\n    public static boolean deleteRow(String tableName, String rowKey) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Delete delete = new Delete(Bytes.toBytes(rowKey));\n            table.delete(delete);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 删除指定行的指定列\n     *\n     * @param tableName  表名\n     * @param rowKey     唯一标识\n     * @param familyName 列族\n     * @param qualifier  列标识\n     */\n    public static boolean deleteColumn(String tableName, String rowKey, String familyName,\n                                          String qualifier) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Delete delete = new Delete(Bytes.toBytes(rowKey));\n            delete.addColumn(Bytes.toBytes(familyName), Bytes.toBytes(qualifier));\n            table.delete(delete);\n            table.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n}\n```\n\n### 2.3 单元测试\n\n以单元测试的方式对上面封装的 API 进行测试。\n\n```java\npublic class HBaseUtilsTest {\n\n    private static final String TABLE_NAME = \"class\";\n    private static final String TEACHER = \"teacher\";\n    private static final String STUDENT = \"student\";\n\n    @Test\n    public void createTable() {\n        // 新建表\n        List<String> columnFamilies = Arrays.asList(TEACHER, STUDENT);\n        boolean table = HBaseUtils.createTable(TABLE_NAME, columnFamilies);\n        System.out.println(\"表创建结果:\" + table);\n    }\n\n    @Test\n    public void insertData() {\n        List<Pair<String, String>> pairs1 = Arrays.asList(new Pair<>(\"name\", \"Tom\"),\n                new Pair<>(\"age\", \"22\"),\n                new Pair<>(\"gender\", \"1\"));\n        HBaseUtils.putRow(TABLE_NAME, \"rowKey1\", STUDENT, pairs1);\n\n        List<Pair<String, String>> pairs2 = Arrays.asList(new Pair<>(\"name\", \"Jack\"),\n                new Pair<>(\"age\", \"33\"),\n                new Pair<>(\"gender\", \"2\"));\n        HBaseUtils.putRow(TABLE_NAME, \"rowKey2\", STUDENT, pairs2);\n\n        List<Pair<String, String>> pairs3 = Arrays.asList(new Pair<>(\"name\", \"Mike\"),\n                new Pair<>(\"age\", \"44\"),\n                new Pair<>(\"gender\", \"1\"));\n        HBaseUtils.putRow(TABLE_NAME, \"rowKey3\", STUDENT, pairs3);\n    }\n\n\n    @Test\n    public void getRow() {\n        Result result = HBaseUtils.getRow(TABLE_NAME, \"rowKey1\");\n        if (result != null) {\n            System.out.println(Bytes\n                    .toString(result.getValue(Bytes.toBytes(STUDENT), Bytes.toBytes(\"name\"))));\n        }\n\n    }\n\n    @Test\n    public void getCell() {\n        String cell = HBaseUtils.getCell(TABLE_NAME, \"rowKey2\", STUDENT, \"age\");\n        System.out.println(\"cell age :\" + cell);\n\n    }\n\n    @Test\n    public void getScanner() {\n        ResultScanner scanner = HBaseUtils.getScanner(TABLE_NAME);\n        if (scanner != null) {\n            scanner.forEach(result -> System.out.println(Bytes.toString(result.getRow()) + \"->\" + Bytes\n                    .toString(result.getValue(Bytes.toBytes(STUDENT), Bytes.toBytes(\"name\")))));\n            scanner.close();\n        }\n    }\n\n\n    @Test\n    public void getScannerWithFilter() {\n        FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);\n        SingleColumnValueFilter nameFilter = new SingleColumnValueFilter(Bytes.toBytes(STUDENT),\n                Bytes.toBytes(\"name\"), CompareOperator.EQUAL, Bytes.toBytes(\"Jack\"));\n        filterList.addFilter(nameFilter);\n        ResultScanner scanner = HBaseUtils.getScanner(TABLE_NAME, filterList);\n        if (scanner != null) {\n            scanner.forEach(result -> System.out.println(Bytes.toString(result.getRow()) + \"->\" + Bytes\n                    .toString(result.getValue(Bytes.toBytes(STUDENT), Bytes.toBytes(\"name\")))));\n            scanner.close();\n        }\n    }\n\n    @Test\n    public void deleteColumn() {\n        boolean b = HBaseUtils.deleteColumn(TABLE_NAME, \"rowKey2\", STUDENT, \"age\");\n        System.out.println(\"删除结果: \" + b);\n    }\n\n    @Test\n    public void deleteRow() {\n        boolean b = HBaseUtils.deleteRow(TABLE_NAME, \"rowKey2\");\n        System.out.println(\"删除结果: \" + b);\n    }\n\n    @Test\n    public void deleteTable() {\n        boolean b = HBaseUtils.deleteTable(TABLE_NAME);\n        System.out.println(\"删除结果: \" + b);\n    }\n}\n```\n\n\n\n## 三、Java API 2.x 基本使用\n\n#### 3.1 新建Maven工程，导入项目依赖\n\n这里选取的 `HBase Client` 的版本为最新的 `2.1.4`。\n\n```xml\n<dependency>\n    <groupId>org.apache.hbase</groupId>\n    <artifactId>hbase-client</artifactId>\n    <version>2.1.4</version>\n</dependency>\n```\n\n#### 3.2 API 的基本使用\n\n2.x 版本相比于 1.x 废弃了一部分方法，关于废弃的方法在源码中都会指明新的替代方法，比如，在 2.x 中创建表时：`HTableDescriptor` 和 `HColumnDescriptor` 等类都标识为废弃，取而代之的是使用 `TableDescriptorBuilder` 和 `ColumnFamilyDescriptorBuilder` 来定义表和列族。\n\n<div align=\"center\"> <img width=\"700px\"  src=\"../pictures/deprecated.png\"/> </div>\n\n\n\n以下为 HBase  2.x 版本 Java API 的使用示例：\n\n```java\npublic class HBaseUtils {\n\n    private static Connection connection;\n\n    static {\n        Configuration configuration = HBaseConfiguration.create();\n        configuration.set(\"hbase.zookeeper.property.clientPort\", \"2181\");\n        // 如果是集群 则主机名用逗号分隔\n        configuration.set(\"hbase.zookeeper.quorum\", \"hadoop001\");\n        try {\n            connection = ConnectionFactory.createConnection(configuration);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * 创建 HBase 表\n     *\n     * @param tableName      表名\n     * @param columnFamilies 列族的数组\n     */\n    public static boolean createTable(String tableName, List<String> columnFamilies) {\n        try {\n            HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();\n            if (admin.tableExists(TableName.valueOf(tableName))) {\n                return false;\n            }\n            TableDescriptorBuilder tableDescriptor = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));\n            columnFamilies.forEach(columnFamily -> {\n                ColumnFamilyDescriptorBuilder cfDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily));\n                cfDescriptorBuilder.setMaxVersions(1);\n                ColumnFamilyDescriptor familyDescriptor = cfDescriptorBuilder.build();\n                tableDescriptor.setColumnFamily(familyDescriptor);\n            });\n            admin.createTable(tableDescriptor.build());\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 删除 hBase 表\n     *\n     * @param tableName 表名\n     */\n    public static boolean deleteTable(String tableName) {\n        try {\n            HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();\n            // 删除表前需要先禁用表\n            admin.disableTable(TableName.valueOf(tableName));\n            admin.deleteTable(TableName.valueOf(tableName));\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n    /**\n     * 插入数据\n     *\n     * @param tableName        表名\n     * @param rowKey           唯一标识\n     * @param columnFamilyName 列族名\n     * @param qualifier        列标识\n     * @param value            数据\n     */\n    public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier,\n                                 String value) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Put put = new Put(Bytes.toBytes(rowKey));\n            put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value));\n            table.put(put);\n            table.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 插入数据\n     *\n     * @param tableName        表名\n     * @param rowKey           唯一标识\n     * @param columnFamilyName 列族名\n     * @param pairList         列标识和值的集合\n     */\n    public static boolean putRow(String tableName, String rowKey, String columnFamilyName, List<Pair<String, String>> pairList) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Put put = new Put(Bytes.toBytes(rowKey));\n            pairList.forEach(pair -> put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(pair.getKey()), Bytes.toBytes(pair.getValue())));\n            table.put(put);\n            table.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 根据 rowKey 获取指定行的数据\n     *\n     * @param tableName 表名\n     * @param rowKey    唯一标识\n     */\n    public static Result getRow(String tableName, String rowKey) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Get get = new Get(Bytes.toBytes(rowKey));\n            return table.get(get);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n\n    /**\n     * 获取指定行指定列 (cell) 的最新版本的数据\n     *\n     * @param tableName    表名\n     * @param rowKey       唯一标识\n     * @param columnFamily 列族\n     * @param qualifier    列标识\n     */\n    public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Get get = new Get(Bytes.toBytes(rowKey));\n            if (!get.isCheckExistenceOnly()) {\n                get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));\n                Result result = table.get(get);\n                byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));\n                return Bytes.toString(resultValue);\n            } else {\n                return null;\n            }\n\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n\n    /**\n     * 检索全表\n     *\n     * @param tableName 表名\n     */\n    public static ResultScanner getScanner(String tableName) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Scan scan = new Scan();\n            return table.getScanner(scan);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n\n    /**\n     * 检索表中指定数据\n     *\n     * @param tableName  表名\n     * @param filterList 过滤器\n     */\n\n    public static ResultScanner getScanner(String tableName, FilterList filterList) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Scan scan = new Scan();\n            scan.setFilter(filterList);\n            return table.getScanner(scan);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    /**\n     * 检索表中指定数据\n     *\n     * @param tableName   表名\n     * @param startRowKey 起始 RowKey\n     * @param endRowKey   终止 RowKey\n     * @param filterList  过滤器\n     */\n\n    public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey,\n                                           FilterList filterList) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Scan scan = new Scan();\n            scan.withStartRow(Bytes.toBytes(startRowKey));\n            scan.withStopRow(Bytes.toBytes(endRowKey));\n            scan.setFilter(filterList);\n            return table.getScanner(scan);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    /**\n     * 删除指定行记录\n     *\n     * @param tableName 表名\n     * @param rowKey    唯一标识\n     */\n    public static boolean deleteRow(String tableName, String rowKey) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Delete delete = new Delete(Bytes.toBytes(rowKey));\n            table.delete(delete);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n\n    /**\n     * 删除指定行指定列\n     *\n     * @param tableName  表名\n     * @param rowKey     唯一标识\n     * @param familyName 列族\n     * @param qualifier  列标识\n     */\n    public static boolean deleteColumn(String tableName, String rowKey, String familyName,\n                                          String qualifier) {\n        try {\n            Table table = connection.getTable(TableName.valueOf(tableName));\n            Delete delete = new Delete(Bytes.toBytes(rowKey));\n            delete.addColumn(Bytes.toBytes(familyName), Bytes.toBytes(qualifier));\n            table.delete(delete);\n            table.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n}\n```\n\n\n\n## 四、正确连接Hbase\n\n在上面的代码中，在类加载时就初始化了 Connection 连接，并且之后的方法都是复用这个 Connection，这时我们可能会考虑是否可以使用自定义连接池来获取更好的性能表现？实际上这是没有必要的。\n\n首先官方对于 `Connection` 的使用说明如下：\n\n```properties\nConnection Pooling For applications which require high-end multithreaded   \naccess (e.g., web-servers or  application servers  that may serve many   \napplication threads in a single JVM), you can pre-create a Connection,   \nas shown in the following example:\n\n对于高并发多线程访问的应用程序（例如，在单个 JVM 中存在的为多个线程服务的 Web 服务器或应用程序服务器），  \n您只需要预先创建一个 Connection。例子如下：\n\n// Create a connection to the cluster.\nConfiguration conf = HBaseConfiguration.create();\ntry (Connection connection = ConnectionFactory.createConnection(conf);\n     Table table = connection.getTable(TableName.valueOf(tablename))) {\n  // use table as needed, the table returned is lightweight\n}\n```\n\n之所以能这样使用，这是因为 Connection 并不是一个简单的 socket 连接，[接口文档](https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Connection.html) 中对 Connection 的表述是：\n\n```properties\nA cluster connection encapsulating lower level individual connections to actual servers and a  \nconnection to zookeeper.  Connections are instantiated through the ConnectionFactory class.  \nThe lifecycle of the connection is managed by the caller,  who has to close() the connection   \nto release the resources. \n\nConnection 是一个集群连接，封装了与多台服务器（Matser/Region Server）的底层连接以及与 zookeeper 的连接。  \n连接通过 ConnectionFactory  类实例化。连接的生命周期由调用者管理，调用者必须使用 close() 关闭连接以释放资源。\n```\n\n之所以封装这些连接，是因为 HBase 客户端需要连接三个不同的服务角色：\n\n+ **Zookeeper** ：主要用于获取 `meta` 表的位置信息，Master 的信息；\n+ **HBase Master** ：主要用于执行 HBaseAdmin 接口的一些操作，例如建表等；\n+ **HBase RegionServer** ：用于读、写数据。\n\n<div align=\"center\"> <img width=\"700px\"  src=\"../pictures/hbase-arc.png\"/> </div>\n\nConnection 对象和实际的 Socket 连接之间的对应关系如下图：\n\n<div align=\"center\"> <img width=\"700px\"   src=\"../pictures/hbase-connection.png\"/> </div>\n\n> 上面两张图片引用自博客：[连接 HBase 的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF)\n\n在 HBase 客户端代码中，真正对应 Socket 连接的是 `RpcConnection` 对象。HBase 使用 `PoolMap` 这种数据结构来存储客户端到 HBase 服务器之间的连接。`PoolMap` 的内部有一个 `ConcurrentHashMap` 实例，其 key 是 `ConnectionId`(封装了服务器地址和用户 ticket)，value 是一个 `RpcConnection` 对象的资源池。当 HBase 需要连接一个服务器时，首先会根据 `ConnectionId` 找到对应的连接池，然后从连接池中取出一个连接对象。\n\n```java\n@InterfaceAudience.Private\npublic class PoolMap<K, V> implements Map<K, V> {\n  private PoolType poolType;\n\n  private int poolMaxSize;\n\n  private Map<K, Pool<V>> pools = new ConcurrentHashMap<>();\n\n  public PoolMap(PoolType poolType) {\n    this.poolType = poolType;\n  }\n  .....\n```\n\nHBase 中提供了三种资源池的实现，分别是 `Reusable`，`RoundRobin` 和 `ThreadLocal`。具体实现可以通 `hbase.client.ipc.pool.type` 配置项指定，默认为 `Reusable`。连接池的大小也可以通过 `hbase.client.ipc.pool.size` 配置项指定，默认为 1，即每个 Server 1 个连接。也可以通过修改配置实现：\n\n```java\nconfig.set(\"hbase.client.ipc.pool.type\",...);\nconfig.set(\"hbase.client.ipc.pool.size\",...);\nconnection = ConnectionFactory.createConnection(config);\n```\n\n由此可以看出 HBase 中 Connection 类已经实现了对连接的管理功能，所以我们不必在 Connection 上在做额外的管理。\n\n另外，Connection 是线程安全的，但 Table 和 Admin 却不是线程安全的，因此正确的做法是一个进程共用一个 Connection 对象，而在不同的线程中使用单独的 Table 和 Admin 对象。Table 和 Admin 的获取操作 `getTable()` 和 `getAdmin()` 都是轻量级，所以不必担心性能的消耗，同时建议在使用完成后显示的调用 `close()` 方法来关闭它们。\n\n\n\n## 参考资料\n\n1. [连接 HBase 的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF)\n2. [Apache HBase ™ Reference Guide](http://hbase.apache.org/book.htm)\n\n"
  },
  {
    "path": "大数据框架学习/Hbase_Shell.md",
    "content": "# Hbase 常用 Shell 命令\n<nav>\n<a href=\"#一基本命令\">一、基本命令</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-获取帮助\">1.1 获取帮助</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-查看服务器状态\">1.2 查看服务器状态</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-查看版本信息\">1.3 查看版本信息</a><br/>\n<a href=\"#二关于表的操作\">二、关于表的操作</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-查看所有表\">2.1 查看所有表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-创建表\">2.2 创建表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-查看表的基本信息\">2.3 查看表的基本信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-表的启用禁用\">2.4 表的启用/禁用</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-检查表是否存在\">2.5 检查表是否存在</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-删除表\">2.6 删除表</a><br/>\n<a href=\"#三增删改\">三、增删改</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-添加列族\">3.1 添加列族</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-删除列族\">3.2 删除列族</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-更改列族存储版本的限制\">3.3 更改列族存储版本的限制</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-插入数据\">3.4 插入数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-获取指定行指定行中的列族列的信息\">3.5 获取指定行、指定行中的列族、列的信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#36-删除指定行指定行中的列\">3.6 删除指定行、指定行中的列</a><br/>\n<a href=\"#四查询\">四、查询</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41Get查询\">4.1Get查询</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-查询整表数据\">4.2 查询整表数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-查询指定列簇的数据\">4.3 查询指定列簇的数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44--条件查询\">4.4  条件查询</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#45--条件过滤\">4.5  条件过滤</a><br/>\n</nav>\n\n\n## 一、基本命令\n\n打开 Hbase Shell：\n\n```shell\n# hbase shell\n```\n\n#### 1.1 获取帮助\n\n```shell\n# 获取帮助\nhelp\n# 获取命令的详细信息\nhelp 'status'\n```\n\n#### 1.2 查看服务器状态\n\n```shell\nstatus\n```\n\n#### 1.3 查看版本信息\n```shell\nversion\n```\n\n\n\n## 二、关于表的操作\n\n\n#### 2.1 查看所有表\n\n```shell\nlist\n```\n\n#### 2.2 创建表\n\n **命令格式**： create '表名称', '列族名称 1','列族名称 2','列名称 N'\n\n```shell\n# 创建一张名为Student的表,包含基本信息（baseInfo）、学校信息（schoolInfo）两个列族\ncreate 'Student','baseInfo','schoolInfo'\n```\n\n#### 2.3 查看表的基本信息\n\n **命令格式**：desc '表名'\n\n```shell\ndescribe 'Student'\n```\n\n#### 2.4 表的启用/禁用\n\nenable 和 disable 可以启用/禁用这个表,is_enabled 和 is_disabled 来检查表是否被禁用\n\n```shell\n# 禁用表\ndisable 'Student'\n# 检查表是否被禁用\nis_disabled 'Student'\n# 启用表\nenable 'Student'\n# 检查表是否被启用\nis_enabled 'Student'\n```\n\n#### 2.5 检查表是否存在\n\n```shell\nexists 'Student'\n```\n\n#### 2.6 删除表\n\n```shell\n# 删除表前需要先禁用表\ndisable 'Student'\n# 删除表\ndrop 'Student'\n```\n\n\n\n## 三、增删改\n\n\n#### 3.1 添加列族\n\n **命令格式**： alter '表名', '列族名'\n\n```shell\nalter 'Student', 'teacherInfo'\n```\n\n#### 3.2 删除列族\n\n **命令格式**：alter '表名', {NAME => '列族名', METHOD => 'delete'}\n\n```shell\nalter 'Student', {NAME => 'teacherInfo', METHOD => 'delete'}\n```\n\n#### 3.3 更改列族存储版本的限制\n\n默认情况下，列族只存储一个版本的数据，如果需要存储多个版本的数据，则需要修改列族的属性。修改后可通过 `desc` 命令查看。\n\n```shell\nalter 'Student',{NAME=>'baseInfo',VERSIONS=>3}\n```\n\n#### 3.4 插入数据\n\n **命令格式**：put '表名', '行键','列族:列','值'\n\n**注意：如果新增数据的行键值、列族名、列名与原有数据完全相同，则相当于更新操作**\n\n```shell\nput 'Student', 'rowkey1','baseInfo:name','tom'\nput 'Student', 'rowkey1','baseInfo:birthday','1990-01-09'\nput 'Student', 'rowkey1','baseInfo:age','29'\nput 'Student', 'rowkey1','schoolInfo:name','Havard'\nput 'Student', 'rowkey1','schoolInfo:localtion','Boston'\n\nput 'Student', 'rowkey2','baseInfo:name','jack'\nput 'Student', 'rowkey2','baseInfo:birthday','1998-08-22'\nput 'Student', 'rowkey2','baseInfo:age','21'\nput 'Student', 'rowkey2','schoolInfo:name','yale'\nput 'Student', 'rowkey2','schoolInfo:localtion','New Haven'\n\nput 'Student', 'rowkey3','baseInfo:name','maike'\nput 'Student', 'rowkey3','baseInfo:birthday','1995-01-22'\nput 'Student', 'rowkey3','baseInfo:age','24'\nput 'Student', 'rowkey3','schoolInfo:name','yale'\nput 'Student', 'rowkey3','schoolInfo:localtion','New Haven'\n\nput 'Student', 'wrowkey4','baseInfo:name','maike-jack'\n```\n\n#### 3.5 获取指定行、指定行中的列族、列的信息\n\n```shell\n# 获取指定行中所有列的数据信息\nget 'Student','rowkey3'\n# 获取指定行中指定列族下所有列的数据信息\nget 'Student','rowkey3','baseInfo'\n# 获取指定行中指定列的数据信息\nget 'Student','rowkey3','baseInfo:name'\n```\n\n#### 3.6 删除指定行、指定行中的列\n\n```shell\n# 删除指定行\ndeleteall 'Student','rowkey3'\n# 删除指定行中指定列的数据\ndelete 'Student','rowkey3','baseInfo:name'\n```\n\n\n\n## 四、查询\n\nhbase 中访问数据有两种基本的方式：\n\n+ 按指定 rowkey 获取数据：get 方法；\n\n+ 按指定条件获取数据：scan 方法。\n\n`scan` 可以设置 begin 和 end 参数来访问一个范围内所有的数据。get 本质上就是 begin 和 end 相等的一种特殊的 scan。\n\n#### 4.1Get查询\n\n```shell\n# 获取指定行中所有列的数据信息\nget 'Student','rowkey3'\n# 获取指定行中指定列族下所有列的数据信息\nget 'Student','rowkey3','baseInfo'\n# 获取指定行中指定列的数据信息\nget 'Student','rowkey3','baseInfo:name'\n```\n\n#### 4.2 查询整表数据\n\n```shell\nscan 'Student'\n```\n\n#### 4.3 查询指定列簇的数据\n\n```shell\nscan 'Student', {COLUMN=>'baseInfo'}\n```\n\n#### 4.4  条件查询\n\n```shell\n# 查询指定列的数据\nscan 'Student', {COLUMNS=> 'baseInfo:birthday'}\n```\n\n除了列 `（COLUMNS）` 修饰词外，HBase 还支持 `Limit`（限制查询结果行数），`STARTROW`（`ROWKEY` 起始行，会先根据这个 `key` 定位到 `region`，再向后扫描）、`STOPROW`(结束行)、`TIMERANGE`（限定时间戳范围）、`VERSIONS`（版本数）、和 `FILTER`（按条件过滤行）等。\n\n如下代表从 `rowkey2` 这个 `rowkey` 开始，查找下两个行的最新 3 个版本的 name 列的数据：\n\n```shell\nscan 'Student', {COLUMNS=> 'baseInfo:name',STARTROW => 'rowkey2',STOPROW => 'wrowkey4',LIMIT=>2, VERSIONS=>3}\n```\n\n#### 4.5  条件过滤\n\nFilter 可以设定一系列条件来进行过滤。如我们要查询值等于 24 的所有数据：\n\n```shell\nscan 'Student', FILTER=>\"ValueFilter(=,'binary:24')\"\n```\n\n值包含 yale 的所有数据：\n\n```shell\nscan 'Student', FILTER=>\"ValueFilter(=,'substring:yale')\"\n```\n\n列名中的前缀为 birth 的：\n\n```shell\nscan 'Student', FILTER=>\"ColumnPrefixFilter('birth')\"\n```\n\nFILTER 中支持多个过滤条件通过括号、AND 和 OR 进行组合：\n\n```shell\n# 列名中的前缀为birth且列值中包含1998的数据\nscan 'Student', FILTER=>\"ColumnPrefixFilter('birth') AND ValueFilter ValueFilter(=,'substring:1998')\"\n```\n\n`PrefixFilter` 用于对 Rowkey 的前缀进行判断：\n\n```shell\nscan 'Student', FILTER=>\"PrefixFilter('wr')\"\n```\n\n\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Hbase协处理器详解.md",
    "content": "# Hbase 协处理器\n\n<nav>\n<a href=\"#一简述\">一、简述</a><br/>\n<a href=\"#二协处理器类型\">二、协处理器类型</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-Observer协处理器\">2.1 Observer协处理器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22--Endpoint协处理器\">2.2  Endpoint协处理器</a><br/>\n<a href=\"#三协处理的加载方式\">三、协处理的加载方式</a><br/>\n<a href=\"#四静态加载与卸载\">四、静态加载与卸载</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-静态加载\">4.1 静态加载</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-静态卸载\">4.2 静态卸载</a><br/>\n<a href=\"#五动态加载与卸载\">五、动态加载与卸载</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#51-HBase-Shell动态加载\">5.1 HBase Shell动态加载</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#52-HBase-Shell动态卸载\">5.2 HBase Shell动态卸载</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#53-Java-API-动态加载\">5.3 Java API 动态加载</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#54-Java-API-动态卸载\">5.4 Java API 动态卸载</a><br/>\n<a href=\"#六协处理器案例\">六、协处理器案例</a><br/>\n</nav>\n\n\n## 一、简述\n\n在使用 HBase 时，如果你的数据量达到了数十亿行或数百万列，此时能否在查询中返回大量数据将受制于网络的带宽，即便网络状况允许，但是客户端的计算处理也未必能够满足要求。在这种情况下，协处理器（Coprocessors）应运而生。它允许你将业务计算代码放入在 RegionServer 的协处理器中，将处理好的数据再返回给客户端，这可以极大地降低需要传输的数据量，从而获得性能上的提升。同时协处理器也允许用户扩展实现 HBase 目前所不具备的功能，如权限校验、二级索引、完整性约束等。\n\n\n\n## 二、协处理器类型\n\n### 2.1 Observer协处理器\n\n#### 1. 功能\n\nObserver 协处理器类似于关系型数据库中的触发器，当发生某些事件的时候这类协处理器会被 Server 端调用。通常可以用来实现下面功能：\n\n+ **权限校验**：在执行 `Get` 或 `Put` 操作之前，您可以使用 `preGet` 或 `prePut` 方法检查权限；\n+ **完整性约束**： HBase 不支持关系型数据库中的外键功能，可以通过触发器在插入或者删除数据的时候，对关联的数据进行检查；\n+ **二级索引**： 可以使用协处理器来维护二级索引。\n\n</br>\n\n#### 2. 类型\n\n当前 Observer 协处理器有以下四种类型：\n\n+ **RegionObserver** : \n  允许您观察 Region 上的事件，例如 Get 和 Put 操作。\n+ **RegionServerObserver** : \n  允许您观察与 RegionServer 操作相关的事件，例如启动，停止或执行合并，提交或回滚。\n+ **MasterObserver** : \n  允许您观察与 HBase Master 相关的事件，例如表创建，删除或 schema 修改。\n+ **WalObserver** : \n  允许您观察与预写日志（WAL）相关的事件。\n\n</br>\n\n####  3. 接口\n\n以上四种类型的 Observer 协处理器均继承自 `Coprocessor` 接口，这四个接口中分别定义了所有可用的钩子方法，以便在对应方法前后执行特定的操作。通常情况下，我们并不会直接实现上面接口，而是继承其 Base 实现类，Base 实现类只是简单空实现了接口中的方法，这样我们在实现自定义的协处理器时，就不必实现所有方法，只需要重写必要方法即可。\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-coprocessor.png\"/> </div>\n\n这里以 `RegionObservers ` 为例，其接口类中定义了所有可用的钩子方法，下面截取了部分方法的定义，多数方法都是成对出现的，有 `pre` 就有 `post`：\n\n<div align=\"center\"> <img  src=\"../pictures/RegionObserver.png\"/> </div>\n\n</br>\n\n#### 4. 执行流程\n\n<div align=\"center\"> <img  src=\"../pictures/RegionObservers-works.png\"/> </div>\n\n+ 客户端发出 put 请求\n+ 该请求被分派给合适的 RegionServer 和 region\n+ coprocessorHost 拦截该请求，然后在该表的每个 RegionObserver 上调用 prePut()\n+ 如果没有被 `prePut()` 拦截，该请求继续送到 region，然后进行处理\n+ region 产生的结果再次被 CoprocessorHost 拦截，调用 `postPut()`\n+ 假如没有 `postPut()` 拦截该响应，最终结果被返回给客户端\n\n如果大家了解 Spring，可以将这种执行方式类比于其 AOP 的执行原理即可，官方文档当中也是这样类比的：\n\n>If you are familiar with Aspect Oriented Programming (AOP), you can think of a coprocessor as applying advice by intercepting a request and then running some custom code,before passing the request on to its final destination (or even changing the destination).\n>\n>如果您熟悉面向切面编程（AOP），您可以将协处理器视为通过拦截请求然后运行一些自定义代码来使用 Advice，然后将请求传递到其最终目标（或者更改目标）。\n\n\n\n### 2.2  Endpoint协处理器\n\nEndpoint 协处理器类似于关系型数据库中的存储过程。客户端可以调用 Endpoint 协处理器在服务端对数据进行处理，然后再返回。\n\n以聚集操作为例，如果没有协处理器，当用户需要找出一张表中的最大数据，即 max 聚合操作，就必须进行全表扫描，然后在客户端上遍历扫描结果，这必然会加重了客户端处理数据的压力。利用 Coprocessor，用户可以将求最大值的代码部署到 HBase Server 端，HBase 将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内执行求最大值的代码，将每个 Region 的最大值在 Region Server 端计算出来，仅仅将该 max 值返回给客户端。之后客户端只需要将每个 Region 的最大值进行比较而找到其中最大的值即可。\n\n\n\n## 三、协处理的加载方式\n\n要使用我们自己开发的协处理器，必须通过静态（使用 HBase 配置）或动态（使用 HBase Shell 或 Java API）加载它。\n\n+ 静态加载的协处理器称之为 **System Coprocessor**（系统级协处理器）,作用范围是整个 HBase 上的所有表，需要重启 HBase 服务；\n+ 动态加载的协处理器称之为 **Table Coprocessor**（表处理器），作用于指定的表，不需要重启 HBase 服务。\n\n其加载和卸载方式分别介绍如下。\n\n\n\n## 四、静态加载与卸载\n\n### 4.1 静态加载\n\n静态加载分以下三步：\n\n1. 在 `hbase-site.xml` 定义需要加载的协处理器。\n\n```xml\n<property>\n    <name>hbase.coprocessor.region.classes</name>\n    <value>org.myname.hbase.coprocessor.endpoint.SumEndPoint</value>\n</property>\n```\n\n ` <name>` 标签的值必须是下面其中之一：\n\n  + RegionObservers 和 Endpoints 协处理器：`hbase.coprocessor.region.classes` \n  + WALObservers 协处理器： `hbase.coprocessor.wal.classes` \n  + MasterObservers 协处理器：`hbase.coprocessor.master.classes`\n\n  `<value>` 必须是协处理器实现类的全限定类名。如果为加载指定了多个类，则类名必须以逗号分隔。\n\n2. 将 jar(包含代码和所有依赖项) 放入 HBase 安装目录中的 `lib` 目录下；\n\n3. 重启 HBase。\n\n</br>\n\n### 4.2 静态卸载\n\n1. 从 hbase-site.xml 中删除配置的协处理器的\\<property>元素及其子元素；\n\n2. 从类路径或 HBase 的 lib 目录中删除协处理器的 JAR 文件（可选）；\n\n3. 重启 HBase。\n   \n\n\n\n## 五、动态加载与卸载\n\n使用动态加载协处理器，不需要重新启动 HBase。但动态加载的协处理器是基于每个表加载的，只能用于所指定的表。\n此外，在使用动态加载必须使表脱机（disable）以加载协处理器。动态加载通常有两种方式：Shell 和 Java API 。 \n\n> 以下示例基于两个前提：\n>\n> 1. coprocessor.jar 包含协处理器实现及其所有依赖项。\n> 2. JAR 包存放在 HDFS 上的路径为：hdfs：// \\<namenode>：\\<port> / user / \\<hadoop-user> /coprocessor.jar\n\n### 5.1 HBase Shell动态加载\n\n1. 使用 HBase Shell 禁用表\n\n```shell\nhbase > disable 'tableName'\n```\n\n2. 使用如下命令加载协处理器\n\n```shell\nhbase > alter 'tableName', METHOD => 'table_att', 'Coprocessor'=>'hdfs://<namenode>:<port>/\nuser/<hadoop-user>/coprocessor.jar| org.myname.hbase.Coprocessor.RegionObserverExample|1073741823|\narg1=1,arg2=2'\n```\n\n`Coprocessor` 包含由管道（|）字符分隔的四个参数，按顺序解释如下：\n\n+ **JAR 包路径**：通常为 JAR 包在 HDFS 上的路径。关于路径以下两点需要注意：\n+ 允许使用通配符，例如：`hdfs://<namenode>:<port>/user/<hadoop-user>/*.jar` 来添加指定的 JAR 包；\n  \n+ 可以使指定目录，例如：`hdfs://<namenode>:<port>/user/<hadoop-user>/` ，这会添加目录中的所有 JAR 包，但不会搜索子目录中的 JAR 包。\n  \n+ **类名**：协处理器的完整类名。\n+ **优先级**：协处理器的优先级，遵循数字的自然序，即值越小优先级越高。可以为空，在这种情况下，将分配默认优先级值。\n+ **可选参数** ：传递的协处理器的可选参数。\n\n3. 启用表\n\n```shell\nhbase > enable 'tableName'\n```\n\n4. 验证协处理器是否已加载\n\n```shell\nhbase > describe 'tableName'\n```\n\n协处理器出现在 `TABLE_ATTRIBUTES` 属性中则代表加载成功。\n\n</br>\n\n### 5.2 HBase Shell动态卸载\n\n1. 禁用表\n\n ```shell\nhbase> disable 'tableName'\n ```\n\n2. 移除表协处理器\n\n```shell\nhbase> alter 'tableName', METHOD => 'table_att_unset', NAME => 'coprocessor$1'\n```\n\n3. 启用表\n\n```shell\nhbase> enable 'tableName'\n```\n\n</br>\n\n### 5.3 Java API 动态加载\n\n```java\nTableName tableName = TableName.valueOf(\"users\");\nString path = \"hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar\";\nConfiguration conf = HBaseConfiguration.create();\nConnection connection = ConnectionFactory.createConnection(conf);\nAdmin admin = connection.getAdmin();\nadmin.disableTable(tableName);\nHTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);\nHColumnDescriptor columnFamily1 = new HColumnDescriptor(\"personalDet\");\ncolumnFamily1.setMaxVersions(3);\nhTableDescriptor.addFamily(columnFamily1);\nHColumnDescriptor columnFamily2 = new HColumnDescriptor(\"salaryDet\");\ncolumnFamily2.setMaxVersions(3);\nhTableDescriptor.addFamily(columnFamily2);\nhTableDescriptor.setValue(\"COPROCESSOR$1\", path + \"|\"\n+ RegionObserverExample.class.getCanonicalName() + \"|\"\n+ Coprocessor.PRIORITY_USER);\nadmin.modifyTable(tableName, hTableDescriptor);\nadmin.enableTable(tableName);\n```\n\n在 HBase 0.96 及其以后版本中，HTableDescriptor 的 addCoprocessor() 方法提供了一种更为简便的加载方法。\n\n```java\nTableName tableName = TableName.valueOf(\"users\");\nPath path = new Path(\"hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar\");\nConfiguration conf = HBaseConfiguration.create();\nConnection connection = ConnectionFactory.createConnection(conf);\nAdmin admin = connection.getAdmin();\nadmin.disableTable(tableName);\nHTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);\nHColumnDescriptor columnFamily1 = new HColumnDescriptor(\"personalDet\");\ncolumnFamily1.setMaxVersions(3);\nhTableDescriptor.addFamily(columnFamily1);\nHColumnDescriptor columnFamily2 = new HColumnDescriptor(\"salaryDet\");\ncolumnFamily2.setMaxVersions(3);\nhTableDescriptor.addFamily(columnFamily2);\nhTableDescriptor.addCoprocessor(RegionObserverExample.class.getCanonicalName(), path,\nCoprocessor.PRIORITY_USER, null);\nadmin.modifyTable(tableName, hTableDescriptor);\nadmin.enableTable(tableName);\n```\n\n\n\n### 5.4 Java API 动态卸载\n\n卸载其实就是重新定义表但不设置协处理器。这会删除所有表上的协处理器。\n\n```java\nTableName tableName = TableName.valueOf(\"users\");\nString path = \"hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar\";\nConfiguration conf = HBaseConfiguration.create();\nConnection connection = ConnectionFactory.createConnection(conf);\nAdmin admin = connection.getAdmin();\nadmin.disableTable(tableName);\nHTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);\nHColumnDescriptor columnFamily1 = new HColumnDescriptor(\"personalDet\");\ncolumnFamily1.setMaxVersions(3);\nhTableDescriptor.addFamily(columnFamily1);\nHColumnDescriptor columnFamily2 = new HColumnDescriptor(\"salaryDet\");\ncolumnFamily2.setMaxVersions(3);\nhTableDescriptor.addFamily(columnFamily2);\nadmin.modifyTable(tableName, hTableDescriptor);\nadmin.enableTable(tableName);\n```\n\n\n\n## 六、协处理器案例\n\n这里给出一个简单的案例，实现一个类似于 Redis 中 `append` 命令的协处理器，当我们对已有列执行 put 操作时候，HBase 默认执行的是 update 操作，这里我们修改为执行 append 操作。\n\n```shell\n# redis append 命令示例\nredis>  EXISTS mykey\n(integer) 0\nredis>  APPEND mykey \"Hello\"\n(integer) 5\nredis>  APPEND mykey \" World\"\n(integer) 11\nredis>  GET mykey \n\"Hello World\"\n```\n\n### 6.1 创建测试表\n\n```shell\n# 创建一张杂志表 有文章和图片两个列族\nhbase >  create 'magazine','article','picture'\n```\n\n### 6.2 协处理器编程\n\n> 完整代码可见本仓库：[hbase-observer-coprocessor](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase\\hbase-observer-coprocessor)\n\n新建 Maven 工程，导入下面依赖：\n\n```xml\n<dependency>\n    <groupId>org.apache.hbase</groupId>\n    <artifactId>hbase-common</artifactId>\n    <version>1.2.0</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.hbase</groupId>\n    <artifactId>hbase-server</artifactId>\n    <version>1.2.0</version>\n</dependency>\n```\n\n继承 `BaseRegionObserver` 实现我们自定义的 `RegionObserver`,对相同的 `article:content` 执行 put 命令时，将新插入的内容添加到原有内容的末尾，代码如下：\n\n```java\npublic class AppendRegionObserver extends BaseRegionObserver {\n\n    private byte[] columnFamily = Bytes.toBytes(\"article\");\n    private byte[] qualifier = Bytes.toBytes(\"content\");\n\n    @Override\n    public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit,\n                       Durability durability) throws IOException {\n        if (put.has(columnFamily, qualifier)) {\n            // 遍历查询结果，获取指定列的原值\n            Result rs = e.getEnvironment().getRegion().get(new Get(put.getRow()));\n            String oldValue = \"\";\n            for (Cell cell : rs.rawCells())\n                if (CellUtil.matchingColumn(cell, columnFamily, qualifier)) {\n                    oldValue = Bytes.toString(CellUtil.cloneValue(cell));\n                }\n\n            // 获取指定列新插入的值\n            List<Cell> cells = put.get(columnFamily, qualifier);\n            String newValue = \"\";\n            for (Cell cell : cells) {\n                if (CellUtil.matchingColumn(cell, columnFamily, qualifier)) {\n                    newValue = Bytes.toString(CellUtil.cloneValue(cell));\n                }\n            }\n\n            // Append 操作\n            put.addColumn(columnFamily, qualifier, Bytes.toBytes(oldValue + newValue));\n        }\n    }\n}\n```\n\n### 6.3 打包项目\n\n使用 maven 命令进行打包，打包后的文件名为 `hbase-observer-coprocessor-1.0-SNAPSHOT.jar`\n\n```shell\n# mvn clean package\n```\n\n### 6.4 上传JAR包到HDFS\n\n```shell\n# 上传项目到HDFS上的hbase目录\nhadoop fs -put /usr/app/hbase-observer-coprocessor-1.0-SNAPSHOT.jar /hbase\n# 查看上传是否成功\nhadoop fs -ls /hbase\n```\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-cp-hdfs.png\"/> </div>\n\n### 6.5 加载协处理器\n\n1. 加载协处理器前需要先禁用表\n\n```shell\nhbase >  disable 'magazine'\n```\n2. 加载协处理器\n\n```shell\nhbase >   alter 'magazine', METHOD => 'table_att', 'Coprocessor'=>'hdfs://hadoop001:8020/hbase/hbase-observer-coprocessor-1.0-SNAPSHOT.jar|com.heibaiying.AppendRegionObserver|1001|'\n```\n\n3. 启用表\n\n```shell\nhbase >  enable 'magazine'\n```\n\n4. 查看协处理器是否加载成功\n\n```shell\nhbase >  desc 'magazine'\n```\n\n协处理器出现在 `TABLE_ATTRIBUTES` 属性中则代表加载成功，如下图：\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-cp-load.png\"/> </div>\n\n### 6.6 测试加载结果\n\n插入一组测试数据：\n\n```shell\nhbase > put 'magazine', 'rowkey1','article:content','Hello'\nhbase > get 'magazine','rowkey1','article:content'\nhbase > put 'magazine', 'rowkey1','article:content','World'\nhbase > get 'magazine','rowkey1','article:content'\n```\n\n可以看到对于指定列的值已经执行了 append 操作：\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-cp-helloworld.png\"/> </div>\n\n插入一组对照数据：\n\n```shell\nhbase > put 'magazine', 'rowkey1','article:author','zhangsan'\nhbase > get 'magazine','rowkey1','article:author'\nhbase > put 'magazine', 'rowkey1','article:author','lisi'\nhbase > get 'magazine','rowkey1','article:author'\n```\n\n可以看到对于正常的列还是执行 update 操作:\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-cp-lisi.png\"/> </div>\n\n### 6.7 卸载协处理器\n1. 卸载协处理器前需要先禁用表\n\n```shell\nhbase >  disable 'magazine'\n```\n2. 卸载协处理器\n\n```shell\nhbase > alter 'magazine', METHOD => 'table_att_unset', NAME => 'coprocessor$1'\n```\n\n3. 启用表\n\n```shell\nhbase >  enable 'magazine'\n```\n\n4. 查看协处理器是否卸载成功\n\n```shell\nhbase >  desc 'magazine'\n```\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-co-unload.png\"/> </div>\n\n### 6.8 测试卸载结果\n\n依次执行下面命令可以测试卸载是否成功\n\n```shell\nhbase > get 'magazine','rowkey1','article:content'\nhbase > put 'magazine', 'rowkey1','article:content','Hello'\nhbase > get 'magazine','rowkey1','article:content'\n```\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-unload-test.png\"/> </div>\n\n\n\n## 参考资料\n\n1. [Apache HBase Coprocessors](http://hbase.apache.org/book.html#cp)\n2. [Apache HBase Coprocessor Introduction](https://blogs.apache.org/hbase/entry/coprocessor_introduction)\n3. [HBase 高階知識](https://www.itread01.com/content/1546245908.html)\n"
  },
  {
    "path": "大数据框架学习/Hbase容灾与备份.md",
    "content": "# Hbase容灾与备份\n\n<nav>\n<a href=\"#一前言\">一、前言</a><br/>\n<a href=\"#二CopyTable\">二、CopyTable</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-简介\">2.1 简介</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-命令格式\">2.2 命令格式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-常用命令\">2.3 常用命令</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-更多参数\">2.4 更多参数</a><br/>\n<a href=\"#三ExportImport\">三、Export/Import</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-简介\">3.1 简介</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-命令格式\">3.2 命令格式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-常用命令\">3.3 常用命令</a><br/>\n<a href=\"#四Snapshot\">四、Snapshot</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-简介\">4.1 简介</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-配置\">4.2 配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-常用命令\">4.3 常用命令</a><br/>\n</nav>\n\n## 一、前言\n\n本文主要介绍 Hbase 常用的三种简单的容灾备份方案，即**CopyTable**、**Export**/**Import**、**Snapshot**。分别介绍如下：\n\n\n\n## 二、CopyTable\n\n### 2.1 简介\n\n**CopyTable**可以将现有表的数据复制到新表中，具有以下特点：\n\n- 支持时间区间 、row 区间 、改变表名称 、改变列族名称 、以及是否 Copy 已被删除的数据等功能；\n- 执行命令前，需先创建与原表结构相同的新表；\n- `CopyTable` 的操作是基于 HBase Client API 进行的，即采用 `scan` 进行查询, 采用 `put` 进行写入。\n\n### 2.2 命令格式\n\n```shell\nUsage: CopyTable [general options] [--starttime=X] [--endtime=Y] [--new.name=NEW] [--peer.adr=ADR] <tablename>\n```\n\n### 2.3 常用命令\n\n1. 同集群下 CopyTable\n\n```shell\nhbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=tableCopy  tableOrig\n```\n\n2. 不同集群下 CopyTable\n\n```shell\n# 两表名称相同的情况\nhbase org.apache.hadoop.hbase.mapreduce.CopyTable \\\n--peer.adr=dstClusterZK:2181:/hbase tableOrig\n\n# 也可以指新的表名\nhbase org.apache.hadoop.hbase.mapreduce.CopyTable \\\n--peer.adr=dstClusterZK:2181:/hbase \\\n--new.name=tableCopy tableOrig\n```\n\n\n3. 下面是一个官方给的比较完整的例子，指定开始和结束时间，集群地址，以及只复制指定的列族：\n\n```shell\nhbase org.apache.hadoop.hbase.mapreduce.CopyTable \\\n--starttime=1265875194289 \\\n--endtime=1265878794289 \\\n--peer.adr=server1,server2,server3:2181:/hbase \\\n--families=myOldCf:myNewCf,cf2,cf3 TestTable\n```\n\n### 2.4 更多参数\n\n可以通过 `--help` 查看更多支持的参数\n\n```shell\n# hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help\n```\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-copy-table.png\"/> </div>\n\n\n\n## 三、Export/Import\n\n### 3.1 简介\n\n- `Export` 支持导出数据到 HDFS, `Import` 支持从 HDFS 导入数据。`Export` 还支持指定导出数据的开始时间和结束时间，因此可以用于增量备份。\n- `Export` 导出与 `CopyTable` 一样，依赖 HBase 的 `scan` 操作\n\n### 3.2 命令格式\n\n```shell\n# Export\nhbase org.apache.hadoop.hbase.mapreduce.Export <tablename> <outputdir> [<versions> [<starttime> [<endtime>]]]\n\n# Inport\nhbase org.apache.hadoop.hbase.mapreduce.Import <tablename> <inputdir>\n```\n\n+ 导出的 `outputdir` 目录可以不用预先创建，程序会自动创建。导出完成后，导出文件的所有权将由执行导出命令的用户所拥有。\n+ 默认情况下，仅导出给定 `Cell` 的最新版本，而不管历史版本。要导出多个版本，需要将 `<versions>` 参数替换为所需的版本数。\n\n### 3.3 常用命令\n\n1. 导出命令\n\n```shell\nhbase org.apache.hadoop.hbase.mapreduce.Export tableName  hdfs 路径/tableName.db\n```\n\n2. 导入命令\n\n```\nhbase org.apache.hadoop.hbase.mapreduce.Import tableName  hdfs 路径/tableName.db\n```\n\n\n\n## 四、Snapshot\n\n### 4.1 简介\n\nHBase 的快照 (Snapshot) 功能允许您获取表的副本 (包括内容和元数据)，并且性能开销很小。因为快照存储的仅仅是表的元数据和 HFiles 的信息。快照的 `clone` 操作会从该快照创建新表，快照的 `restore` 操作会将表的内容还原到快照节点。`clone` 和 `restore` 操作不需要复制任何数据，因为底层 HFiles(包含 HBase 表数据的文件) 不会被修改，修改的只是表的元数据信息。\n\n### 4.2 配置\n\nHBase 快照功能默认没有开启，如果要开启快照，需要在 `hbase-site.xml` 文件中添加如下配置项：\n\n```xml\n<property>\n    <name>hbase.snapshot.enabled</name>\n    <value>true</value>\n</property>\n```\n\n\n\n### 4.3 常用命令\n\n快照的所有命令都需要在 Hbase Shell 交互式命令行中执行。\n\n#### 1. Take a Snapshot\n\n```shell\n# 拍摄快照\nhbase> snapshot '表名', '快照名'\n```\n\n默认情况下拍摄快照之前会在内存中执行数据刷新。以保证内存中的数据包含在快照中。但是如果你不希望包含内存中的数据，则可以使用 `SKIP_FLUSH` 选项禁止刷新。\n\n```shell\n# 禁止内存刷新\nhbase> snapshot  '表名', '快照名', {SKIP_FLUSH => true}\n```\n\n#### 2. Listing Snapshots\n\n```shell\n# 获取快照列表\nhbase> list_snapshots\n```\n\n#### 3. Deleting Snapshots\n\n```shell\n# 删除快照\nhbase> delete_snapshot '快照名'\n```\n\n#### 4. Clone a table from snapshot\n\n```shell\n# 从现有的快照创建一张新表\nhbase>  clone_snapshot '快照名', '新表名'\n```\n\n#### 5. Restore a snapshot\n\n将表恢复到快照节点，恢复操作需要先禁用表\n\n```shell\nhbase> disable '表名'\nhbase> restore_snapshot '快照名'\n```\n\n这里需要注意的是：是如果 HBase 配置了基于 Replication 的主从复制，由于 Replication 在日志级别工作，而快照在文件系统级别工作，因此在还原之后，会出现副本与主服务器处于不同的状态的情况。这时候可以先停止同步，所有服务器还原到一致的数据点后再重新建立同步。\n\n\n\n## 参考资料\n\n1. [Online Apache HBase Backups with CopyTable](https://blog.cloudera.com/blog/2012/06/online-hbase-backups-with-copytable-2/)\n2. [Apache HBase ™ Reference Guide](http://hbase.apache.org/book.htm)\n"
  },
  {
    "path": "大数据框架学习/Hbase的SQL中间层_Phoenix.md",
    "content": "# Hbase的SQL中间层——Phoenix\n\n<nav>\n<a href=\"#一Phoenix简介\">一、Phoenix简介</a><br/>\n<a href=\"#二Phoenix安装\">二、Phoenix安装</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-下载并解压\">2.1 下载并解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-拷贝Jar包\">2.2 拷贝Jar包</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-重启-Region-Servers\">2.3 重启 Region Servers</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-启动Phoenix\">2.4 启动Phoenix</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-启动结果\">2.5 启动结果</a><br/>\n<a href=\"#三Phoenix-简单使用\">三、Phoenix 简单使用</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-创建表\">3.1 创建表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-插入数据\">3.2 插入数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-修改数据\">3.3 修改数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-删除数据\">3.4 删除数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-查询数据\">3.5 查询数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#36-退出命令\">3.6 退出命令</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#37-扩展\">3.7 扩展</a><br/>\n<a href=\"#四Phoenix-Java-API\">四、Phoenix Java API</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-引入Phoenix-core-JAR包\">4.1 引入Phoenix core JAR包</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-简单的Java-API实例\">4.2 简单的Java API实例</a><br/>\n</nav>\n\n## 一、Phoenix简介\n\n`Phoenix` 是 HBase 的开源 SQL 中间层，它允许你使用标准 JDBC 的方式来操作 HBase 上的数据。在 `Phoenix` 之前，如果你要访问 HBase，只能调用它的 Java API，但相比于使用一行 SQL 就能实现数据查询，HBase 的 API 还是过于复杂。`Phoenix` 的理念是 `we put sql SQL back in NOSQL`，即你可以使用标准的 SQL 就能完成对 HBase 上数据的操作。同时这也意味着你可以通过集成 `Spring Data  JPA` 或 `Mybatis` 等常用的持久层框架来操作 HBase。\n\n其次 `Phoenix` 的性能表现也非常优异，`Phoenix` 查询引擎会将 SQL 查询转换为一个或多个 HBase Scan，通过并行执行来生成标准的 JDBC 结果集。它通过直接使用 HBase API 以及协处理器和自定义过滤器，可以为小型数据查询提供毫秒级的性能，为千万行数据的查询提供秒级的性能。同时 Phoenix 还拥有二级索引等 HBase 不具备的特性，因为以上的优点，所以 `Phoenix` 成为了 HBase 最优秀的 SQL 中间层。\n\n<div align=\"center\"> <img width=\"600px\"  src=\"../pictures/Phoenix-hadoop.png\"/> </div>\n\n\n## 二、Phoenix安装\n\n> 我们可以按照官方安装说明进行安装，官方说明如下：\n>\n> - download and expand our installation tar\n> - copy the phoenix server jar that is compatible with your HBase installation into the lib directory of every region server\n> - restart the region servers\n> - add the phoenix client jar to the classpath of your HBase client\n> - download and setup SQuirrel as your SQL client so you can issue adhoc SQL against your HBase cluster\n\n### 2.1 下载并解压\n\n官方针对 Apache 版本和 CDH 版本的 HBase 均提供了安装包，按需下载即可。官方下载地址: http://phoenix.apache.org/download.html\n\n```shell\n# 下载\nwget http://mirror.bit.edu.cn/apache/phoenix/apache-phoenix-4.14.0-cdh5.14.2/bin/apache-phoenix-4.14.0-cdh5.14.2-bin.tar.gz\n# 解压\ntar tar apache-phoenix-4.14.0-cdh5.14.2-bin.tar.gz\n```\n\n### 2.2 拷贝Jar包\n\n按照官方文档的说明，需要将 `phoenix server jar` 添加到所有 `Region Servers` 的安装目录的 `lib` 目录下。\n\n这里由于我搭建的是 HBase 伪集群，所以只需要拷贝到当前机器的 HBase 的 lib 目录下。如果是真实集群，则使用 scp 命令分发到所有 `Region Servers` 机器上。\n\n```shell\ncp /usr/app/apache-phoenix-4.14.0-cdh5.14.2-bin/phoenix-4.14.0-cdh5.14.2-server.jar /usr/app/hbase-1.2.0-cdh5.15.2/lib\n```\n\n### 2.3 重启 Region Servers\n\n```shell\n# 停止Hbase\nstop-hbase.sh\n# 启动Hbase\nstart-hbase.sh\n```\n\n### 2.4 启动Phoenix\n\n在 Phoenix 解压目录下的 `bin` 目录下执行如下命令，需要指定 Zookeeper 的地址：\n\n+ 如果 HBase 采用 Standalone 模式或者伪集群模式搭建，则默认采用内置的 Zookeeper 服务，端口为 2181；\n+ 如果是 HBase 是集群模式并采用外置的 Zookeeper 集群，则按照自己的实际情况进行指定。\n\n```shell\n# ./sqlline.py hadoop001:2181\n```\n\n### 2.5 启动结果\n\n启动后则进入了 Phoenix 交互式 SQL 命令行，可以使用 `!table` 或 `!tables` 查看当前所有表的信息\n\n<div align=\"center\"> <img  src=\"../pictures/phoenix-shell.png\"/> </div>\n\n\n## 三、Phoenix 简单使用\n\n### 3.1 创建表\n\n```sql\nCREATE TABLE IF NOT EXISTS us_population (\n      state CHAR(2) NOT NULL,\n      city VARCHAR NOT NULL,\n      population BIGINT\n      CONSTRAINT my_pk PRIMARY KEY (state, city));\n```\n\n<div align=\"center\"> <img  src=\"../pictures/Phoenix-create-table.png\"/> </div>\n新建的表会按照特定的规则转换为 HBase 上的表，关于表的信息，可以通过 Hbase Web UI 进行查看：\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-web-ui-phoenix.png\"/> </div>\n### 3.2 插入数据\n\nPhoenix 中插入数据采用的是 `UPSERT` 而不是 `INSERT`,因为 Phoenix 并没有更新操作，插入相同主键的数据就视为更新，所以 `UPSERT` 就相当于 `UPDATE`+`INSERT`\n\n```shell\nUPSERT INTO us_population VALUES('NY','New York',8143197);\nUPSERT INTO us_population VALUES('CA','Los Angeles',3844829);\nUPSERT INTO us_population VALUES('IL','Chicago',2842518);\nUPSERT INTO us_population VALUES('TX','Houston',2016582);\nUPSERT INTO us_population VALUES('PA','Philadelphia',1463281);\nUPSERT INTO us_population VALUES('AZ','Phoenix',1461575);\nUPSERT INTO us_population VALUES('TX','San Antonio',1256509);\nUPSERT INTO us_population VALUES('CA','San Diego',1255540);\nUPSERT INTO us_population VALUES('TX','Dallas',1213825);\nUPSERT INTO us_population VALUES('CA','San Jose',912332);\n```\n\n### 3.3 修改数据\n\n```sql\n-- 插入主键相同的数据就视为更新\nUPSERT INTO us_population VALUES('NY','New York',999999);\n```\n\n<div align=\"center\"> <img  src=\"../pictures/Phoenix-update.png\"/> </div>\n### 3.4 删除数据\n\n```sql\nDELETE FROM us_population WHERE city='Dallas';\n```\n\n<div align=\"center\"> <img  src=\"../pictures/Phoenix-delete.png\"/> </div>\n### 3.5 查询数据\n\n```sql\nSELECT state as \"州\",count(city) as \"市\",sum(population) as \"热度\"\nFROM us_population\nGROUP BY state\nORDER BY sum(population) DESC;\n```\n\n<div align=\"center\"> <img  src=\"../pictures/Phoenix-select.png\"/> </div>\n\n\n### 3.6 退出命令\n\n```sql\n!quit\n```\n\n\n\n### 3.7 扩展\n\n从上面的操作中可以看出，Phoenix 支持大多数标准的 SQL 语法。关于 Phoenix 支持的语法、数据类型、函数、序列等详细信息，因为涉及内容很多，可以参考其官方文档，官方文档上有详细的说明：\n\n+ **语法 (Grammar)** ：https://phoenix.apache.org/language/index.html\n\n+ **函数 (Functions)** ：http://phoenix.apache.org/language/functions.html\n\n+ **数据类型 (Datatypes)** ：http://phoenix.apache.org/language/datatypes.html\n\n+ **序列 (Sequences)** :http://phoenix.apache.org/sequences.html\n\n+ **联结查询 (Joins)** ：http://phoenix.apache.org/joins.html\n\n\n\n## 四、Phoenix Java API\n\n因为 Phoenix 遵循 JDBC 规范，并提供了对应的数据库驱动 `PhoenixDriver`，这使得采用 Java 语言对其进行操作的时候，就如同对其他关系型数据库一样，下面给出基本的使用示例。\n\n### 4.1 引入Phoenix core JAR包\n\n如果是 maven 项目，直接在 maven 中央仓库找到对应的版本，导入依赖即可：\n\n```xml\n <!-- https://mvnrepository.com/artifact/org.apache.phoenix/phoenix-core -->\n    <dependency>\n      <groupId>org.apache.phoenix</groupId>\n      <artifactId>phoenix-core</artifactId>\n      <version>4.14.0-cdh5.14.2</version>\n    </dependency>\n```\n\n如果是普通项目，则可以从 Phoenix 解压目录下找到对应的 JAR 包，然后手动引入：\n\n<div align=\"center\"> <img  src=\"../pictures/phoenix-core-jar.png\"/> </div>\n### 4.2 简单的Java API实例\n\n```java\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\n\n\npublic class PhoenixJavaApi {\n\n    public static void main(String[] args) throws Exception {\n\n        // 加载数据库驱动\n        Class.forName(\"org.apache.phoenix.jdbc.PhoenixDriver\");\n\n        /*\n         * 指定数据库地址,格式为 jdbc:phoenix:Zookeeper 地址\n         * 如果 HBase 采用 Standalone 模式或者伪集群模式搭建，则 HBase 默认使用内置的 Zookeeper，默认端口为 2181\n         */\n        Connection connection = DriverManager.getConnection(\"jdbc:phoenix:192.168.200.226:2181\");\n\n        PreparedStatement statement = connection.prepareStatement(\"SELECT * FROM us_population\");\n\n        ResultSet resultSet = statement.executeQuery();\n\n        while (resultSet.next()) {\n            System.out.println(resultSet.getString(\"city\") + \" \"\n                    + resultSet.getInt(\"population\"));\n        }\n\n        statement.close();\n        connection.close();\n    }\n}\n```\n\n结果如下：\n\n<div align=\"center\"> <img  src=\"../pictures/Phoenix-java-api-result.png\"/> </div>\n\n\n实际的开发中我们通常都是采用第三方框架来操作数据库，如 `mybatis`，`Hibernate`，`Spring Data` 等。关于 Phoenix 与这些框架的整合步骤参见下一篇文章：[Spring/Spring Boot + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spring+Mybtais+Phoenix整合.md)\n\n# 参考资料\n\n1. http://phoenix.apache.org/\n"
  },
  {
    "path": "大数据框架学习/Hbase简介.md",
    "content": "# HBase简介\n\n<nav>\n<a href=\"#一Hadoop的局限\">一、Hadoop的局限</a><br/>\n<a href=\"#二HBase简介\">二、HBase简介</a><br/>\n<a href=\"#三HBase-Table\">三、HBase Table</a><br/>\n<a href=\"#四Phoenix\">四、Phoenix</a><br/>\n</nav>\n\n## 一、Hadoop的局限\n\nHBase 是一个构建在 Hadoop 文件系统之上的面向列的数据库管理系统。\n\n<div align=\"center\"> <img  src=\"../pictures/hbase.jpg\"/> </div>\n\n要想明白为什么产生 HBase，就需要先了解一下 Hadoop 存在的限制？Hadoop 可以通过 HDFS 来存储结构化、半结构甚至非结构化的数据，它是传统数据库的补充，是海量数据存储的最佳方法，它针对大文件的存储，批量访问和流式访问都做了优化，同时也通过多副本解决了容灾问题。\n\n但是 Hadoop 的缺陷在于它只能执行批处理，并且只能以顺序方式访问数据，这意味着即使是最简单的工作，也必须搜索整个数据集，无法实现对数据的随机访问。实现数据的随机访问是传统的关系型数据库所擅长的，但它们却不能用于海量数据的存储。在这种情况下，必须有一种新的方案来解决海量数据存储和随机访问的问题，HBase 就是其中之一 (HBase，Cassandra，couchDB，Dynamo 和 MongoDB 都能存储海量数据并支持随机访问)。\n\n> 注：数据结构分类：\n>\n> - 结构化数据：即以关系型数据库表形式管理的数据；\n> - 半结构化数据：非关系模型的，有基本固定结构模式的数据，例如日志文件、XML 文档、JSON 文档、Email 等；\n> - 非结构化数据：没有固定模式的数据，如 WORD、PDF、PPT、EXL，各种格式的图片、视频等。\n\n\n\n## 二、HBase简介\n\nHBase 是一个构建在 Hadoop 文件系统之上的面向列的数据库管理系统。\n\nHBase 是一种类似于 `Google’s Big Table` 的数据模型，它是 Hadoop 生态系统的一部分，它将数据存储在 HDFS 上，客户端可以通过 HBase 实现对 HDFS 上数据的随机访问。它具有以下特性：\n\n+ 不支持复杂的事务，只支持行级事务，即单行数据的读写都是原子性的；\n+ 由于是采用 HDFS 作为底层存储，所以和 HDFS 一样，支持结构化、半结构化和非结构化的存储；\n+ 支持通过增加机器进行横向扩展；\n+ 支持数据分片；\n+ 支持 RegionServers 之间的自动故障转移；\n+ 易于使用的 Java 客户端 API；\n+ 支持 BlockCache 和布隆过滤器；\n+ 过滤器支持谓词下推。\n\n\n\n## 三、HBase Table\n\nHBase 是一个面向 ` 列 ` 的数据库管理系统，这里更为确切的而说，HBase 是一个面向 ` 列族 ` 的数据库管理系统。表 schema 仅定义列族，表具有多个列族，每个列族可以包含任意数量的列，列由多个单元格（cell ）组成，单元格可以存储多个版本的数据，多个版本数据以时间戳进行区分。\n\n下图为 HBase 中一张表的：\n\n+ RowKey 为行的唯一标识，所有行按照 RowKey 的字典序进行排序；\n+ 该表具有两个列族，分别是 personal 和 office;\n+ 其中列族 personal 拥有 name、city、phone 三个列，列族 office 拥有 tel、addres 两个列。\n\n<div align=\"center\"> <img  src=\"../pictures/HBase_table-iteblog.png\"/> </div>\n\n> *图片引用自 : HBase 是列式存储数据库吗* *https://www.iteblog.com/archives/2498.html*\n\nHbase 的表具有以下特点：\n\n- 容量大：一个表可以有数十亿行，上百万列；\n\n- 面向列：数据是按照列存储，每一列都单独存放，数据即索引，在查询时可以只访问指定列的数据，有效地降低了系统的 I/O 负担；\n\n- 稀疏性：空 (null) 列并不占用存储空间，表可以设计的非常稀疏  ；\t\n\n- 数据多版本：每个单元中的数据可以有多个版本，按照时间戳排序，新的数据在最上面； \t\n\n- 存储类型：所有数据的底层存储格式都是字节数组 (byte[])。\n\n  \n\n## 四、Phoenix\n\n`Phoenix` 是 HBase 的开源 SQL 中间层，它允许你使用标准 JDBC 的方式来操作 HBase 上的数据。在 `Phoenix` 之前，如果你要访问 HBase，只能调用它的 Java API，但相比于使用一行 SQL 就能实现数据查询，HBase 的 API 还是过于复杂。`Phoenix` 的理念是 `we put sql SQL back in NOSQL`，即你可以使用标准的 SQL 就能完成对 HBase 上数据的操作。同时这也意味着你可以通过集成 `Spring Data  JPA` 或 `Mybatis` 等常用的持久层框架来操作 HBase。\n\n其次 `Phoenix` 的性能表现也非常优异，`Phoenix` 查询引擎会将 SQL 查询转换为一个或多个 HBase Scan，通过并行执行来生成标准的 JDBC 结果集。它通过直接使用 HBase API 以及协处理器和自定义过滤器，可以为小型数据查询提供毫秒级的性能，为千万行数据的查询提供秒级的性能。同时 Phoenix 还拥有二级索引等 HBase 不具备的特性，因为以上的优点，所以 `Phoenix` 成为了 HBase 最优秀的 SQL 中间层。\n\n\n\n\n\n## 参考资料\n\n1. [HBase - Overview](https://www.tutorialspoint.com/hbase/hbase_overview.htm)\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Hbase系统架构及数据结构.md",
    "content": "# Hbase系统架构及数据结构\n\n<nav>\n<a href=\"#一基本概念\">一、基本概念</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-Row-Key-行键\">1.1 Row Key (行键)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-Column-Family列族\">1.2 Column Family（列族）</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-Column-Qualifier-列限定符\">1.3 Column Qualifier (列限定符)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-Column列\">1.4 Column(列)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-Cell\">1.5 Cell</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-Timestamp时间戳\">1.6 Timestamp(时间戳)</a><br/>\n<a href=\"#二存储结构\">二、存储结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-Regions\">2.1 Regions</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-Region-Server\">2.2 Region Server</a><br/>\n<a href=\"#三Hbase系统架构\">三、Hbase系统架构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-系统架构\">3.1 系统架构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-组件间的协作\">3.2 组件间的协作</a><br/>\n<a href=\"#四数据的读写流程简述\">四、数据的读写流程简述</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-写入数据的流程\">4.1 写入数据的流程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-读取数据的流程\">4.2 读取数据的流程</a><br/>\n</nav>\n\n## 一、基本概念\n\n一个典型的 Hbase Table 表如下：\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-webtable.png\"/> </div>\n\n### 1.1 Row Key (行键)\n\n`Row Key` 是用来检索记录的主键。想要访问 HBase Table 中的数据，只有以下三种方式：\n\n+ 通过指定的 `Row Key` 进行访问；\n\n+ 通过 Row Key 的 range 进行访问，即访问指定范围内的行；\n\n+ 进行全表扫描。\n\n`Row Key` 可以是任意字符串，存储时数据按照 `Row Key` 的字典序进行排序。这里需要注意以下两点：\n\n+ 因为字典序对 Int 排序的结果是 1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。如果你使用整型的字符串作为行键，那么为了保持整型的自然序，行键必须用 0 作左填充。\n\n+ 行的一次读写操作时原子性的 (不论一次读写多少列)。\n\n\n\n### 1.2 Column Family（列族）\n\nHBase 表中的每个列，都归属于某个列族。列族是表的 Schema 的一部分，所以列族需要在创建表时进行定义。列族的所有列都以列族名作为前缀，例如 `courses:history`，`courses:math` 都属于 `courses` 这个列族。\n\n\n\n### 1.3 Column Qualifier (列限定符)\n\n列限定符，你可以理解为是具体的列名，例如 `courses:history`，`courses:math` 都属于 `courses` 这个列族，它们的列限定符分别是 `history` 和 `math`。需要注意的是列限定符不是表 Schema 的一部分，你可以在插入数据的过程中动态创建列。\n\n\n\n### 1.4 Column(列)\n\nHBase 中的列由列族和列限定符组成，它们由 `:`(冒号) 进行分隔，即一个完整的列名应该表述为 ` 列族名 ：列限定符 `。\n\n\n\n### 1.5 Cell\n\n`Cell` 是行，列族和列限定符的组合，并包含值和时间戳。你可以等价理解为关系型数据库中由指定行和指定列确定的一个单元格，但不同的是 HBase 中的一个单元格是由多个版本的数据组成的，每个版本的数据用时间戳进行区分。\n\n\n\n### 1.6 Timestamp(时间戳)\n\nHBase 中通过 `row key` 和 `column` 确定的为一个存储单元称为 `Cell`。每个 `Cell` 都保存着同一份数据的多个版本。版本通过时间戳来索引，时间戳的类型是 64 位整型，时间戳可以由 HBase 在数据写入时自动赋值，也可以由客户显式指定。每个 `Cell` 中，不同版本的数据按照时间戳倒序排列，即最新的数据排在最前面。\n\n\n\n## 二、存储结构\n\n### 2.1 Regions\n\nHBase Table 中的所有行按照 `Row Key` 的字典序排列。HBase Tables 通过行键的范围 (row key range) 被水平切分成多个 `Region`, 一个 `Region` 包含了在 start key 和 end key 之间的所有行。\n\n<div align=\"center\"> <img  src=\"../pictures/HBaseArchitecture-Blog-Fig2.png\"/> </div>\n\n每个表一开始只有一个 `Region`，随着数据不断增加，`Region` 会不断增大，当增大到一个阀值的时候，`Region` 就会等分为两个新的 `Region`。当 Table 中的行不断增多，就会有越来越多的 `Region`。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/hbase-region-splite.png\"/> </div>\n\n`Region` 是 HBase 中**分布式存储和负载均衡的最小单元**。这意味着不同的 `Region` 可以分布在不同的 `Region Server` 上。但一个 `Region` 是不会拆分到多个 Server 上的。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/hbase-region-dis.png\"/> </div>\n\n### 2.2 Region Server\n\n`Region Server` 运行在 HDFS 的 DataNode 上。它具有以下组件：\n\n- **WAL(Write Ahead Log，预写日志)**：用于存储尚未进持久化存储的数据记录，以便在发生故障时进行恢复。\n- **BlockCache**：读缓存。它将频繁读取的数据存储在内存中，如果存储不足，它将按照 ` 最近最少使用原则 ` 清除多余的数据。\n- **MemStore**：写缓存。它存储尚未写入磁盘的新数据，并会在数据写入磁盘之前对其进行排序。每个 Region 上的每个列族都有一个 MemStore。\n- **HFile** ：将行数据按照 Key\\Values 的形式存储在文件系统上。\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-Region-Server.png\"/> </div>\n\n\n\nRegion Server 存取一个子表时，会创建一个 Region 对象，然后对表的每个列族创建一个 `Store` 实例，每个 `Store` 会有 0 个或多个 `StoreFile` 与之对应，每个 `StoreFile` 则对应一个 `HFile`，HFile 就是实际存储在 HDFS 上的文件。\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-hadoop.png\"/> </div>\n\n\n\n## 三、Hbase系统架构\n\n### 3.1 系统架构\n\nHBase 系统遵循 Master/Salve 架构，由三种不同类型的组件组成：\n\n**Zookeeper**\n\n1. 保证任何时候，集群中只有一个 Master；\n\n2. 存贮所有 Region 的寻址入口；\n\n3. 实时监控 Region Server 的状态，将 Region Server 的上线和下线信息实时通知给 Master；\n\n4. 存储 HBase 的 Schema，包括有哪些 Table，每个 Table 有哪些 Column Family 等信息。\n\n**Master**\n\n1. 为 Region Server 分配 Region ；\n\n2. 负责 Region Server 的负载均衡 ；\n\n3. 发现失效的 Region Server 并重新分配其上的 Region； \n\n4. GFS 上的垃圾文件回收；\n\n5. 处理 Schema 的更新请求。\n\n**Region Server**\n\n1. Region Server 负责维护 Master 分配给它的 Region ，并处理发送到 Region 上的 IO 请求；\n\n2. Region Server 负责切分在运行过程中变得过大的 Region。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/HBaseArchitecture-Blog-Fig1.png\"/> </div>\n\n### 3.2 组件间的协作\n\n HBase 使用 ZooKeeper 作为分布式协调服务来维护集群中的服务器状态。 Zookeeper 负责维护可用服务列表，并提供服务故障通知等服务：\n\n+ 每个 Region Server 都会在 ZooKeeper 上创建一个临时节点，Master 通过 Zookeeper 的 Watcher 机制对节点进行监控，从而可以发现新加入的 Region Server 或故障退出的 Region Server；\n\n+ 所有 Masters 会竞争性地在 Zookeeper 上创建同一个临时节点，由于 Zookeeper 只能有一个同名节点，所以必然只有一个 Master 能够创建成功，此时该 Master 就是主 Master，主 Master 会定期向 Zookeeper 发送心跳。备用 Masters 则通过 Watcher 机制对主 HMaster 所在节点进行监听；\n\n+ 如果主 Master 未能定时发送心跳，则其持有的 Zookeeper 会话会过期，相应的临时节点也会被删除，这会触发定义在该节点上的 Watcher 事件，使得备用的 Master Servers 得到通知。所有备用的 Master Servers 在接到通知后，会再次去竞争性地创建临时节点，完成主 Master 的选举。\n\n<div align=\"center\"> <img  src=\"../pictures/HBaseArchitecture-Blog-Fig5.png\"/> </div>\n\n\n\n## 四、数据的读写流程简述\n\n### 4.1 写入数据的流程\n\n1. Client 向 Region Server 提交写请求；\n\n2. Region Server 找到目标 Region；\n\n3. Region 检查数据是否与 Schema 一致；\n\n4. 如果客户端没有指定版本，则获取当前系统时间作为数据版本；\n\n5. 将更新写入 WAL Log；\n\n6. 将更新写入 Memstore；\n\n7. 判断 Memstore 存储是否已满，如果存储已满则需要 flush 为 Store Hfile 文件。\n\n> 更为详细写入流程可以参考：[HBase － 数据写入流程解析](http://hbasefly.com/2016/03/23/hbase_writer/)\n\n\n\n### 4.2 读取数据的流程\n\n以下是客户端首次读写 HBase 上数据的流程：\n\n1. 客户端从 Zookeeper 获取 `META` 表所在的 Region Server；\n\n2. 客户端访问 `META` 表所在的 Region Server，从 `META` 表中查询到访问行键所在的 Region Server，之后客户端将缓存这些信息以及 `META` 表的位置；\n\n3. 客户端从行键所在的 Region Server 上获取数据。\n\n如果再次读取，客户端将从缓存中获取行键所在的 Region Server。这样客户端就不需要再次查询 `META` 表，除非 Region 移动导致缓存失效，这样的话，则将会重新查询并更新缓存。\n\n注：`META` 表是 HBase 中一张特殊的表，它保存了所有 Region 的位置信息，META 表自己的位置信息则存储在 ZooKeeper 上。\n\n<div align=\"center\"> <img  src=\"../pictures/HBaseArchitecture-Blog-Fig7.png\"/> </div>\n\n> 更为详细读取数据流程参考：\n>\n> [HBase 原理－数据读取流程解析](http://hbasefly.com/2016/12/21/hbase-getorscan/)\n>\n> [HBase 原理－迟到的‘数据读取流程部分细节](http://hbasefly.com/2017/06/11/hbase-scan-2/)\n\n\n\n\n\n## 参考资料\n\n本篇文章内容主要参考自官方文档和以下两篇博客，图片也主要引用自以下两篇博客：\n\n+ [HBase Architectural Components](https://mapr.com/blog/in-depth-look-hbase-architecture/#.VdMxvWSqqko)\n\n+ [Hbase 系统架构及数据结构](https://www.open-open.com/lib/view/open1346821084631.html)\n\n官方文档：\n\n+ [Apache HBase ™ Reference Guide](https://hbase.apache.org/2.1/book.html)\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Hbase过滤器详解.md",
    "content": "# Hbase 过滤器详解\n\n<nav>\n<a href=\"#一HBase过滤器简介\">一、HBase过滤器简介</a><br/>\n<a href=\"#二过滤器基础\">二、过滤器基础</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21--Filter接口和FilterBase抽象类\">2.1  Filter接口和FilterBase抽象类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-过滤器分类\">2.2 过滤器分类</a><br/>\n<a href=\"#三比较过滤器\">三、比较过滤器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-比较运算符\">3.1 比较运算符</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-比较器\">3.2 比较器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-比较过滤器种类\">3.3 比较过滤器种类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-DependentColumnFilter\">3.4 DependentColumnFilter </a><br/>\n<a href=\"#四专用过滤器\">四、专用过滤器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-单列列值过滤器-SingleColumnValueFilter\">4.1 单列列值过滤器 (SingleColumnValueFilter)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-单列列值排除器-SingleColumnValueExcludeFilter\">4.2 单列列值排除器 (SingleColumnValueExcludeFilter) </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-行键前缀过滤器-PrefixFilter\">4.3 行键前缀过滤器 (PrefixFilter)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44-列名前缀过滤器-ColumnPrefixFilter\">4.4 列名前缀过滤器 (ColumnPrefixFilter)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#45-分页过滤器-PageFilter\">4.5 分页过滤器 (PageFilter)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#46-时间戳过滤器-TimestampsFilter\">4.6 时间戳过滤器 (TimestampsFilter)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#47-首次行键过滤器-FirstKeyOnlyFilter\">4.7 首次行键过滤器 (FirstKeyOnlyFilter)</a><br/>\n<a href=\"#五包装过滤器\">五、包装过滤器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#51-SkipFilter过滤器\">5.1 SkipFilter过滤器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#52-WhileMatchFilter过滤器\">5.2 WhileMatchFilter过滤器</a><br/>\n<a href=\"#六FilterList\">六、FilterList</a><br/>\n</nav>\n\n\n\n## 一、HBase过滤器简介\n\nHbase 提供了种类丰富的过滤器（filter）来提高数据处理的效率，用户可以通过内置或自定义的过滤器来对数据进行过滤，所有的过滤器都在服务端生效，即谓词下推（predicate push down）。这样可以保证过滤掉的数据不会被传送到客户端，从而减轻网络传输和客户端处理的压力。\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-fliter.png\"/> </div>\n\n\n\n## 二、过滤器基础\n\n### 2.1  Filter接口和FilterBase抽象类\n\nFilter 接口中定义了过滤器的基本方法，FilterBase 抽象类实现了 Filter 接口。所有内置的过滤器则直接或者间接继承自 FilterBase 抽象类。用户只需要将定义好的过滤器通过 `setFilter` 方法传递给 `Scan` 或 `put` 的实例即可。\n\n```java\nsetFilter(Filter filter)\n```\n\n```java\n // Scan 中定义的 setFilter\n @Override\n  public Scan setFilter(Filter filter) {\n    super.setFilter(filter);\n    return this;\n  }\n```\n\n```java\n  // Get 中定义的 setFilter\n @Override\n  public Get setFilter(Filter filter) {\n    super.setFilter(filter);\n    return this;\n  }\n```\n\nFilterBase 的所有子类过滤器如下：<div align=\"center\"> <img  src=\"../pictures/hbase-filterbase-subclass.png\"/> </div>\n\n> 说明：上图基于当前时间点（2019.4）最新的 Hbase-2.1.4 ，下文所有说明均基于此版本。\n\n\n\n### 2.2 过滤器分类\n\nHBase 内置过滤器可以分为三类：分别是比较过滤器，专用过滤器和包装过滤器。分别在下面的三个小节中做详细的介绍。\n\n\n\n## 三、比较过滤器\n\n所有比较过滤器均继承自 `CompareFilter`。创建一个比较过滤器需要两个参数，分别是**比较运算符**和**比较器实例**。\n\n```java\n public CompareFilter(final CompareOp compareOp,final ByteArrayComparable comparator) {\n    this.compareOp = compareOp;\n    this.comparator = comparator;\n  }\n```\n\n### 3.1 比较运算符\n\n- LESS (<)\n- LESS_OR_EQUAL (<=)\n- EQUAL (=)\n- NOT_EQUAL (!=)\n- GREATER_OR_EQUAL (>=)\n- GREATER (>)\n- NO_OP (排除所有符合条件的值)\n\n比较运算符均定义在枚举类 `CompareOperator` 中\n\n```java\n@InterfaceAudience.Public\npublic enum CompareOperator {\n  LESS,\n  LESS_OR_EQUAL,\n  EQUAL,\n  NOT_EQUAL,\n  GREATER_OR_EQUAL,\n  GREATER,\n  NO_OP,\n}\n```\n\n> 注意：在 1.x 版本的 HBase 中，比较运算符定义在 `CompareFilter.CompareOp` 枚举类中，但在 2.0 之后这个类就被标识为 @deprecated ，并会在 3.0 移除。所以 2.0 之后版本的 HBase 需要使用 `CompareOperator` 这个枚举类。\n>\n\n### 3.2 比较器\n\n所有比较器均继承自 `ByteArrayComparable` 抽象类，常用的有以下几种：\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-bytearraycomparable.png\"/> </div>\n\n- **BinaryComparator**  : 使用 `Bytes.compareTo(byte []，byte [])` 按字典序比较指定的字节数组。\n- **BinaryPrefixComparator** : 按字典序与指定的字节数组进行比较，但只比较到这个字节数组的长度。\n- **RegexStringComparator** :  使用给定的正则表达式与指定的字节数组进行比较。仅支持 `EQUAL` 和 `NOT_EQUAL` 操作。\n- **SubStringComparator** : 测试给定的子字符串是否出现在指定的字节数组中，比较不区分大小写。仅支持 `EQUAL` 和 `NOT_EQUAL` 操作。\n- **NullComparator** ：判断给定的值是否为空。\n- **BitComparator** ：按位进行比较。\n\n`BinaryPrefixComparator` 和 `BinaryComparator` 的区别不是很好理解，这里举例说明一下：\n\n在进行 `EQUAL` 的比较时，如果比较器传入的是 `abcd` 的字节数组，但是待比较数据是 `abcdefgh`：\n\n+ 如果使用的是 `BinaryPrefixComparator` 比较器，则比较以 `abcd` 字节数组的长度为准，即 `efgh` 不会参与比较，这时候认为 `abcd` 与 `abcdefgh` 是满足 `EQUAL` 条件的；\n+ 如果使用的是 `BinaryComparator` 比较器，则认为其是不相等的。\n\n### 3.3 比较过滤器种类\n\n比较过滤器共有五个（Hbase 1.x 版本和 2.x 版本相同），见下图：\n\n<div align=\"center\"> <img  src=\"../pictures/hbase-compareFilter.png\"/> </div>\n\n+ **RowFilter** ：基于行键来过滤数据；\n+ **FamilyFilterr** ：基于列族来过滤数据；\n+ **QualifierFilterr** ：基于列限定符（列名）来过滤数据；\n+ **ValueFilterr** ：基于单元格 (cell) 的值来过滤数据；\n+ **DependentColumnFilter** ：指定一个参考列来过滤其他列的过滤器，过滤的原则是基于参考列的时间戳来进行筛选 。\n\n前四种过滤器的使用方法相同，均只要传递比较运算符和运算器实例即可构建，然后通过 `setFilter` 方法传递给 `scan`：\n\n```java\n Filter filter  = new RowFilter(CompareOperator.LESS_OR_EQUAL,\n                                new BinaryComparator(Bytes.toBytes(\"xxx\")));\n  scan.setFilter(filter);    \n```\n\n`DependentColumnFilter` 的使用稍微复杂一点，这里单独做下说明。\n\n### 3.4 DependentColumnFilter \n\n可以把 `DependentColumnFilter` 理解为**一个 valueFilter 和一个时间戳过滤器的组合**。`DependentColumnFilter` 有三个带参构造器，这里选择一个参数最全的进行说明：\n\n```java\nDependentColumnFilter(final byte [] family, final byte[] qualifier,\n                               final boolean dropDependentColumn, final CompareOperator op,\n                               final ByteArrayComparable valueComparator)\n```\n\n+ **family**  ：列族\n+ **qualifier** ：列限定符（列名）\n+ **dropDependentColumn** ：决定参考列是否被包含在返回结果内，为 true 时表示参考列被返回，为 false 时表示被丢弃\n+ **op** ：比较运算符\n+ **valueComparator** ：比较器\n\n这里举例进行说明：\n\n```java\nDependentColumnFilter dependentColumnFilter = new DependentColumnFilter( \n    Bytes.toBytes(\"student\"),\n    Bytes.toBytes(\"name\"),\n    false,\n    CompareOperator.EQUAL, \n    new BinaryPrefixComparator(Bytes.toBytes(\"xiaolan\")));\n```\n\n+ 首先会去查找 `student:name` 中值以 `xiaolan` 开头的所有数据获得 ` 参考数据集 `，这一步等同于 valueFilter 过滤器；\n\n+ 其次再用参考数据集中所有数据的时间戳去检索其他列，获得时间戳相同的其他列的数据作为 ` 结果数据集 `，这一步等同于时间戳过滤器；\n\n+ 最后如果 `dropDependentColumn` 为 true，则返回 ` 参考数据集 `+` 结果数据集 `，若为 false，则抛弃参考数据集，只返回 ` 结果数据集 `。\n\n\n\n## 四、专用过滤器\n\n专用过滤器通常直接继承自 `FilterBase`，适用于范围更小的筛选规则。\n\n### 4.1 单列列值过滤器 (SingleColumnValueFilter)\n\n基于某列（参考列）的值决定某行数据是否被过滤。其实例有以下方法：\n\n+ **setFilterIfMissing(boolean filterIfMissing)** ：默认值为 false，即如果该行数据不包含参考列，其依然被包含在最后的结果中；设置为 true 时，则不包含；\n+ **setLatestVersionOnly(boolean latestVersionOnly)** ：默认为 true，即只检索参考列的最新版本数据；设置为 false，则检索所有版本数据。\n\n```shell\nSingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(\n                \"student\".getBytes(), \n                \"name\".getBytes(), \n                CompareOperator.EQUAL, \n                new SubstringComparator(\"xiaolan\"));\nsingleColumnValueFilter.setFilterIfMissing(true);\nscan.setFilter(singleColumnValueFilter);\n```\n\n### 4.2 单列列值排除器 (SingleColumnValueExcludeFilter) \n\n`SingleColumnValueExcludeFilter` 继承自上面的 `SingleColumnValueFilter`，过滤行为与其相反。\n\n### 4.3 行键前缀过滤器 (PrefixFilter)\n\n基于 RowKey 值决定某行数据是否被过滤。\n\n```java\nPrefixFilter prefixFilter = new PrefixFilter(Bytes.toBytes(\"xxx\"));\nscan.setFilter(prefixFilter);\n```\n\n### 4.4 列名前缀过滤器 (ColumnPrefixFilter)\n\n基于列限定符（列名）决定某行数据是否被过滤。\n\n```java\nColumnPrefixFilter columnPrefixFilter = new ColumnPrefixFilter(Bytes.toBytes(\"xxx\"));\n scan.setFilter(columnPrefixFilter);\n```\n\n### 4.5 分页过滤器 (PageFilter)\n\n可以使用这个过滤器实现对结果按行进行分页，创建 PageFilter 实例的时候需要传入每页的行数。\n\n```java\npublic PageFilter(final long pageSize) {\n    Preconditions.checkArgument(pageSize >= 0, \"must be positive %s\", pageSize);\n    this.pageSize = pageSize;\n  }\n```\n\n下面的代码体现了客户端实现分页查询的主要逻辑，这里对其进行一下解释说明：\n\n客户端进行分页查询，需要传递 `startRow`(起始 RowKey)，知道起始 `startRow` 后，就可以返回对应的 pageSize 行数据。这里唯一的问题就是，对于第一次查询，显然 `startRow` 就是表格的第一行数据，但是之后第二次、第三次查询我们并不知道 `startRow`，只能知道上一次查询的最后一条数据的 RowKey（简单称之为 `lastRow`）。\n\n我们不能将 `lastRow` 作为新一次查询的 `startRow` 传入，因为 scan 的查询区间是[startRow，endRow) ，即前开后闭区间，这样 `startRow` 在新的查询也会被返回，这条数据就重复了。\n\n同时在不使用第三方数据库存储 RowKey 的情况下，我们是无法通过知道 `lastRow` 的下一个 RowKey 的，因为 RowKey 的设计可能是连续的也有可能是不连续的。\n\n由于 Hbase 的 RowKey 是按照字典序进行排序的。这种情况下，就可以在 `lastRow` 后面加上 `0` ，作为 `startRow` 传入，因为按照字典序的规则，某个值加上 `0` 后的新值，在字典序上一定是这个值的下一个值，对于 HBase 来说下一个 RowKey 在字典序上一定也是等于或者大于这个新值的。\n\n所以最后传入 `lastRow`+`0`，如果等于这个值的 RowKey 存在就从这个值开始 scan,否则从字典序的下一个 RowKey 开始 scan。\n\n> 25 个字母以及数字字符，字典排序如下:\n>\n> `'0' < '1' < '2' < ... < '9' < 'a' < 'b' < ... < 'z'`\n\n分页查询主要实现逻辑：\n\n```java\nbyte[] POSTFIX = new byte[] { 0x00 };\nFilter filter = new PageFilter(15);\n\nint totalRows = 0;\nbyte[] lastRow = null;\nwhile (true) {\n    Scan scan = new Scan();\n    scan.setFilter(filter);\n    if (lastRow != null) {\n        // 如果不是首行 则 lastRow + 0\n        byte[] startRow = Bytes.add(lastRow, POSTFIX);\n        System.out.println(\"start row: \" +\n                           Bytes.toStringBinary(startRow));\n        scan.withStartRow(startRow);\n    }\n    ResultScanner scanner = table.getScanner(scan);\n    int localRows = 0;\n    Result result;\n    while ((result = scanner.next()) != null) {\n        System.out.println(localRows++ + \": \" + result);\n        totalRows++;\n        lastRow = result.getRow();\n    }\n    scanner.close();\n    //最后一页，查询结束  \n    if (localRows == 0) break;\n}\nSystem.out.println(\"total rows: \" + totalRows);\n```\n\n>需要注意的是在多台 Regin Services 上执行分页过滤的时候，由于并行执行的过滤器不能共享它们的状态和边界，所以有可能每个过滤器都会在完成扫描前获取了 PageCount 行的结果，这种情况下会返回比分页条数更多的数据，分页过滤器就有失效的可能。\n\n\n\n### 4.6 时间戳过滤器 (TimestampsFilter)\n\n```java\nList<Long> list = new ArrayList<>();\nlist.add(1554975573000L);\nTimestampsFilter timestampsFilter = new TimestampsFilter(list);\nscan.setFilter(timestampsFilter);\n```\n\n### 4.7 首次行键过滤器 (FirstKeyOnlyFilter)\n\n`FirstKeyOnlyFilter` 只扫描每行的第一列，扫描完第一列后就结束对当前行的扫描，并跳转到下一行。相比于全表扫描，其性能更好，通常用于行数统计的场景，因为如果某一行存在，则行中必然至少有一列。\n\n```java\nFirstKeyOnlyFilter firstKeyOnlyFilter = new FirstKeyOnlyFilter();\nscan.set(firstKeyOnlyFilter);\n```\n\n## 五、包装过滤器\n\n包装过滤器就是通过包装其他过滤器以实现某些拓展的功能。\n\n### 5.1 SkipFilter过滤器\n\n`SkipFilter` 包装一个过滤器，当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时，则拓展过滤整行数据。下面是一个使用示例：\n\n```java\n// 定义 ValueFilter 过滤器\nFilter filter1 = new ValueFilter(CompareOperator.NOT_EQUAL,\n      new BinaryComparator(Bytes.toBytes(\"xxx\")));\n// 使用 SkipFilter 进行包装\nFilter filter2 = new SkipFilter(filter1);\n```\n\n\n\n### 5.2 WhileMatchFilter过滤器\n\n`WhileMatchFilter` 包装一个过滤器，当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时，`WhileMatchFilter` 则结束本次扫描，返回已经扫描到的结果。下面是其使用示例：\n\n```java\nFilter filter1 = new RowFilter(CompareOperator.NOT_EQUAL,\n                               new BinaryComparator(Bytes.toBytes(\"rowKey4\")));\n\nScan scan = new Scan();\nscan.setFilter(filter1);\nResultScanner scanner1 = table.getScanner(scan);\nfor (Result result : scanner1) {\n    for (Cell cell : result.listCells()) {\n        System.out.println(cell);\n    }\n}\nscanner1.close();\n\nSystem.out.println(\"--------------------\");\n\n// 使用 WhileMatchFilter 进行包装\nFilter filter2 = new WhileMatchFilter(filter1);\n\nscan.setFilter(filter2);\nResultScanner scanner2 = table.getScanner(scan);\nfor (Result result : scanner1) {\n    for (Cell cell : result.listCells()) {\n        System.out.println(cell);\n    }\n}\nscanner2.close();\n```\n\n```properties\nrowKey0/student:name/1555035006994/Put/vlen=8/seqid=0\nrowKey1/student:name/1555035007019/Put/vlen=8/seqid=0\nrowKey2/student:name/1555035007025/Put/vlen=8/seqid=0\nrowKey3/student:name/1555035007037/Put/vlen=8/seqid=0\nrowKey5/student:name/1555035007051/Put/vlen=8/seqid=0\nrowKey6/student:name/1555035007057/Put/vlen=8/seqid=0\nrowKey7/student:name/1555035007062/Put/vlen=8/seqid=0\nrowKey8/student:name/1555035007068/Put/vlen=8/seqid=0\nrowKey9/student:name/1555035007073/Put/vlen=8/seqid=0\n--------------------\nrowKey0/student:name/1555035006994/Put/vlen=8/seqid=0\nrowKey1/student:name/1555035007019/Put/vlen=8/seqid=0\nrowKey2/student:name/1555035007025/Put/vlen=8/seqid=0\nrowKey3/student:name/1555035007037/Put/vlen=8/seqid=0\n```\n\n可以看到被包装后，只返回了 `rowKey4` 之前的数据。\n\n## 六、FilterList\n\n以上都是讲解单个过滤器的作用，当需要多个过滤器共同作用于一次查询的时候，就需要使用 `FilterList`。`FilterList` 支持通过构造器或者 `addFilter` 方法传入多个过滤器。\n\n```java\n// 构造器传入\npublic FilterList(final Operator operator, final List<Filter> filters)\npublic FilterList(final List<Filter> filters)\npublic FilterList(final Filter... filters)\n\n// 方法传入\n public void addFilter(List<Filter> filters)\n public void addFilter(Filter filter)\n```\n\n多个过滤器组合的结果由 `operator` 参数定义 ，其可选参数定义在 `Operator` 枚举类中。只有 `MUST_PASS_ALL` 和 `MUST_PASS_ONE` 两个可选的值：\n\n+ **MUST_PASS_ALL** ：相当于 AND，必须所有的过滤器都通过才认为通过；\n+ **MUST_PASS_ONE** ：相当于 OR，只有要一个过滤器通过则认为通过。\n\n```java\n@InterfaceAudience.Public\n  public enum Operator {\n    /** !AND */\n    MUST_PASS_ALL,\n    /** !OR */\n    MUST_PASS_ONE\n  }\n```\n\n使用示例如下：\n\n```java\nList<Filter> filters = new ArrayList<Filter>();\n\nFilter filter1 = new RowFilter(CompareOperator.GREATER_OR_EQUAL,\n                               new BinaryComparator(Bytes.toBytes(\"XXX\")));\nfilters.add(filter1);\n\nFilter filter2 = new RowFilter(CompareOperator.LESS_OR_EQUAL,\n                               new BinaryComparator(Bytes.toBytes(\"YYY\")));\nfilters.add(filter2);\n\nFilter filter3 = new QualifierFilter(CompareOperator.EQUAL,\n                                     new RegexStringComparator(\"ZZZ\"));\nfilters.add(filter3);\n\nFilterList filterList = new FilterList(filters);\n\nScan scan = new Scan();\nscan.setFilter(filterList);\n```\n\n\n\n## 参考资料\n\n[HBase: The Definitive Guide _>  Chapter 4. Client API: Advanced Features](https://www.oreilly.com/library/view/hbase-the-definitive/9781449314682/ch04.html)\n"
  },
  {
    "path": "大数据框架学习/HiveCLI和Beeline命令行的基本使用.md",
    "content": "# Hive CLI和Beeline命令行的基本使用\n\n<nav>\n<a href=\"#一Hive-CLI\">一、Hive CLI</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-Help\">1.1 Help</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-交互式命令行\">1.2 交互式命令行</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-执行SQL命令\">1.3 执行SQL命令</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-执行SQL脚本\">1.4 执行SQL脚本</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-配置Hive变量\">1.5 配置Hive变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-配置文件启动\">1.6 配置文件启动</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#17-用户自定义变量\">1.7 用户自定义变量</a><br/>\n<a href=\"#二Beeline\">二、Beeline </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-HiveServer2\">2.1 HiveServer2</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-Beeline\">2.1 Beeline</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-常用参数\">2.3 常用参数</a><br/>\n<a href=\"#三Hive配置\">三、Hive配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-配置文件\">3.1 配置文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-hiveconf\">3.2 hiveconf</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-set\">3.3 set</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-配置优先级\">3.4 配置优先级</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-配置参数\">3.5 配置参数</a><br/>\n</nav>\n\n## 一、Hive CLI\n\n### 1.1 Help\n\n使用 `hive -H` 或者 `hive --help` 命令可以查看所有命令的帮助，显示如下：\n\n```\nusage: hive\n -d,--define <key=value>          Variable subsitution to apply to hive \n                                  commands. e.g. -d A=B or --define A=B  --定义用户自定义变量\n    --database <databasename>     Specify the database to use  -- 指定使用的数据库\n -e <quoted-query-string>         SQL from command line   -- 执行指定的 SQL\n -f <filename>                    SQL from files   --执行 SQL 脚本\n -H,--help                        Print help information  -- 打印帮助信息\n    --hiveconf <property=value>   Use value for given property    --自定义配置\n    --hivevar <key=value>         Variable subsitution to apply to hive  --自定义变量\n                                  commands. e.g. --hivevar A=B\n -i <filename>                    Initialization SQL file  --在进入交互模式之前运行初始化脚本\n -S,--silent                      Silent mode in interactive shell    --静默模式\n -v,--verbose                     Verbose mode (echo executed SQL to the  console)  --详细模式\n```\n\n### 1.2 交互式命令行\n\n直接使用 `Hive` 命令，不加任何参数，即可进入交互式命令行。\n\n### 1.3 执行SQL命令\n\n在不进入交互式命令行的情况下，可以使用 `hive -e ` 执行 SQL 命令。\n\n```sql\nhive -e 'select * from emp';\n```\n\n<div align=\"center\"> <img width='700px' src=\"../pictures/hive-e.png\"/> </div>\n\n\n\n### 1.4 执行SQL脚本\n\n用于执行的 sql 脚本可以在本地文件系统，也可以在 HDFS 上。\n\n```shell\n# 本地文件系统\nhive -f /usr/file/simple.sql;\n\n# HDFS文件系统\nhive -f hdfs://hadoop001:8020/tmp/simple.sql;\n```\n\n其中 `simple.sql` 内容如下：\n\n```sql\nselect * from emp;\n```\n\n### 1.5 配置Hive变量\n\n可以使用 `--hiveconf` 设置 Hive 运行时的变量。\n\n```sql\nhive -e 'select * from emp' \\\n--hiveconf hive.exec.scratchdir=/tmp/hive_scratch  \\\n--hiveconf mapred.reduce.tasks=4;\n```\n\n> hive.exec.scratchdir：指定 HDFS 上目录位置，用于存储不同 map/reduce 阶段的执行计划和这些阶段的中间输出结果。\n\n### 1.6 配置文件启动\n\n使用 `-i` 可以在进入交互模式之前运行初始化脚本，相当于指定配置文件启动。\n\n```shell\nhive -i /usr/file/hive-init.conf;\n```\n\n其中 `hive-init.conf` 的内容如下：\n\n```sql\nset hive.exec.mode.local.auto = true;\n```\n\n> hive.exec.mode.local.auto 默认值为 false，这里设置为 true ，代表开启本地模式。\n\n### 1.7 用户自定义变量\n\n`--define <key=value> ` 和 `--hivevar <key=value>  ` 在功能上是等价的，都是用来实现自定义变量，这里给出一个示例:\n\n定义变量：\n\n```sql\nhive  --define  n=ename --hiveconf  --hivevar j=job;\n```\n\n在查询中引用自定义变量：\n\n```sql\n# 以下两条语句等价\nhive > select ${n} from emp;\nhive >  select ${hivevar:n} from emp;\n\n# 以下两条语句等价\nhive > select ${j} from emp;\nhive >  select ${hivevar:j} from emp;\n```\n\n结果如下：\n\n<div align=\"center\"> <img width='700px' src=\"../pictures/hive-n-j.png\"/> </div>\n\n## 二、Beeline \n\n### 2.1 HiveServer2\n\nHive 内置了 HiveServer 和 HiveServer2 服务，两者都允许客户端使用多种编程语言进行连接，但是 HiveServer 不能处理多个客户端的并发请求，所以产生了 HiveServer2。\n\nHiveServer2（HS2）允许远程客户端可以使用各种编程语言向 Hive 提交请求并检索结果，支持多客户端并发访问和身份验证。HS2 是由多个服务组成的单个进程，其包括基于 Thrift 的 Hive 服务（TCP 或 HTTP）和用于 Web UI 的 Jetty Web 服务器。\n\n HiveServer2 拥有自己的 CLI(Beeline)，Beeline 是一个基于 SQLLine 的 JDBC 客户端。由于 HiveServer2 是 Hive 开发维护的重点 (Hive0.15 后就不再支持 hiveserver)，所以 Hive CLI 已经不推荐使用了，官方更加推荐使用 Beeline。\n\n### 2.1 Beeline\n\nBeeline 拥有更多可使用参数，可以使用 `beeline --help` 查看，完整参数如下：\n\n```properties\nUsage: java org.apache.hive.cli.beeline.BeeLine\n   -u <database url>               the JDBC URL to connect to\n   -r                              reconnect to last saved connect url (in conjunction with !save)\n   -n <username>                   the username to connect as\n   -p <password>                   the password to connect as\n   -d <driver class>               the driver class to use\n   -i <init file>                  script file for initialization\n   -e <query>                      query that should be executed\n   -f <exec file>                  script file that should be executed\n   -w (or) --password-file <password file>  the password file to read password from\n   --hiveconf property=value       Use value for given property\n   --hivevar name=value            hive variable name and value\n                                   This is Hive specific settings in which variables\n                                   can be set at session level and referenced in Hive\n                                   commands or queries.\n   --property-file=<property-file> the file to read connection properties (url, driver, user, password) from\n   --color=[true/false]            control whether color is used for display\n   --showHeader=[true/false]       show column names in query results\n   --headerInterval=ROWS;          the interval between which heades are displayed\n   --fastConnect=[true/false]      skip building table/column list for tab-completion\n   --autoCommit=[true/false]       enable/disable automatic transaction commit\n   --verbose=[true/false]          show verbose error messages and debug info\n   --showWarnings=[true/false]     display connection warnings\n   --showNestedErrs=[true/false]   display nested errors\n   --numberFormat=[pattern]        format numbers using DecimalFormat pattern\n   --force=[true/false]            continue running script even after errors\n   --maxWidth=MAXWIDTH             the maximum width of the terminal\n   --maxColumnWidth=MAXCOLWIDTH    the maximum width to use when displaying columns\n   --silent=[true/false]           be more silent\n   --autosave=[true/false]         automatically save preferences\n   --outputformat=[table/vertical/csv2/tsv2/dsv/csv/tsv]  format mode for result display\n   --incrementalBufferRows=NUMROWS the number of rows to buffer when printing rows on stdout,\n                                   defaults to 1000; only applicable if --incremental=true\n                                   and --outputformat=table\n   --truncateTable=[true/false]    truncate table column when it exceeds length\n   --delimiterForDSV=DELIMITER     specify the delimiter for delimiter-separated values output format (default: |)\n   --isolation=LEVEL               set the transaction isolation level\n   --nullemptystring=[true/false]  set to true to get historic behavior of printing null as empty string\n   --maxHistoryRows=MAXHISTORYROWS The maximum number of rows to store beeline history.\n   --convertBinaryArrayToString=[true/false]    display binary column data as string or as byte array\n   --help                          display this message\n\n```\n\n### 2.3 常用参数\n\n在 Hive CLI 中支持的参数，Beeline 都支持，常用的参数如下。更多参数说明可以参见官方文档 [Beeline Command Options](https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients#HiveServer2Clients-Beeline%E2%80%93NewCommandLineShell)\n\n| 参数 | 说明 |\n| -------------------------------------- | ------------------------------------------------------------ |\n| **-u \\<database URL>**              | 数据库地址                                             |\n| **-n \\<username>**                | 用户名                                                       |\n| **-p \\<password>**                  | 密码 |\n| **-d \\<driver class>**              | 驱动 (可选)                                                   |\n| **-e \\<query>**                     | 执行 SQL 命令 |\n| **-f \\<file>**                      | 执行 SQL 脚本 |\n| **-i  (or)--init  \\<file or files>** | 在进入交互模式之前运行初始化脚本                             |\n| **--property-file \\<file>** | 指定配置文件 |\n| **--hiveconf** *property**=**value* | 指定配置属性 |\n| **--hivevar** *name**=**value* | 用户自定义属性，在会话级别有效 |\n\n示例： 使用用户名和密码连接 Hive\n\n```shell\n$ beeline -u jdbc:hive2://localhost:10000  -n username -p password \n```\n\n​         \n\n## 三、Hive配置\n\n可以通过三种方式对 Hive 的相关属性进行配置，分别介绍如下：\n\n### 3.1 配置文件\n\n方式一为使用配置文件，使用配置文件指定的配置是永久有效的。Hive 有以下三个可选的配置文件：\n\n+ hive-site.xml ：Hive 的主要配置文件；\n\n+ hivemetastore-site.xml： 关于元数据的配置；\n+ hiveserver2-site.xml：关于 HiveServer2 的配置。\n\n示例如下,在 hive-site.xml 配置 `hive.exec.scratchdir`：\n\n```xml\n <property>\n    <name>hive.exec.scratchdir</name>\n    <value>/tmp/mydir</value>\n    <description>Scratch space for Hive jobs</description>\n  </property>\n```\n\n### 3.2 hiveconf\n\n方式二为在启动命令行 (Hive CLI / Beeline) 的时候使用 `--hiveconf` 指定配置，这种方式指定的配置作用于整个 Session。\n\n```\nhive --hiveconf hive.exec.scratchdir=/tmp/mydir\n```\n\n### 3.3 set\n\n方式三为在交互式环境下 (Hive CLI / Beeline)，使用 set 命令指定。这种设置的作用范围也是 Session 级别的，配置对于执行该命令后的所有命令生效。set 兼具设置参数和查看参数的功能。如下：\n\n```shell\n0: jdbc:hive2://hadoop001:10000> set hive.exec.scratchdir=/tmp/mydir;\nNo rows affected (0.025 seconds)\n0: jdbc:hive2://hadoop001:10000> set hive.exec.scratchdir;\n+----------------------------------+--+\n|               set                |\n+----------------------------------+--+\n| hive.exec.scratchdir=/tmp/mydir  |\n+----------------------------------+--+\n```\n\n### 3.4 配置优先级\n\n配置的优先顺序如下 (由低到高)：  \n`hive-site.xml` - >` hivemetastore-site.xml `- > `hiveserver2-site.xml` - >` -- hiveconf`- > `set`\n\n### 3.5 配置参数\n\nHive 可选的配置参数非常多，在用到时查阅官方文档即可[AdminManual Configuration](https://cwiki.apache.org/confluence/display/Hive/AdminManual+Configuration)\n\n\n\n## 参考资料\n\n1. [HiveServer2 Clients](https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients)\n2. [LanguageManual Cli](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Cli)\n3. [AdminManual Configuration](https://cwiki.apache.org/confluence/display/Hive/AdminManual+Configuration)\n"
  },
  {
    "path": "大数据框架学习/Hive分区表和分桶表.md",
    "content": "# Hive分区表和分桶表\n\n<nav>\n<a href=\"#一分区表\">一、分区表</a><br/>\n<a href=\"#二分桶表\">二、分桶表</a><br/>\n<a href=\"#三分区表和分桶表结合使用\">三、分区表和分桶表结合使用</a><br/>\n</nav>\n\n\n## 一、分区表\n\n### 1.1 概念\n\nHive 中的表对应为 HDFS 上的指定目录，在查询数据时候，默认会对全表进行扫描，这样时间和性能的消耗都非常大。\n\n**分区为 HDFS 上表目录的子目录**，数据按照分区存储在子目录中。如果查询的 `where` 字句的中包含分区条件，则直接从该分区去查找，而不是扫描整个表目录，合理的分区设计可以极大提高查询速度和性能。\n\n>这里说明一下分区表并 Hive 独有的概念，实际上这个概念非常常见。比如在我们常用的 Oracle 数据库中，当表中的数据量不断增大，查询数据的速度就会下降，这时也可以对表进行分区。表进行分区后，逻辑上表仍然是一张完整的表，只是将表中的数据存放到多个表空间（物理文件上），这样查询数据时，就不必要每次都扫描整张表，从而提升查询性能。\n\n### 1.2  使用场景\n\n通常，在管理大规模数据集的时候都需要进行分区，比如将日志文件按天进行分区，从而保证数据细粒度的划分，使得查询性能得到提升。\n\n### 1.3 创建分区表\n\n在 Hive 中可以使用 `PARTITIONED BY` 子句创建分区表。表可以包含一个或多个分区列，程序会为分区列中的每个不同值组合创建单独的数据目录。下面的我们创建一张雇员表作为测试：\n\n```shell\n CREATE EXTERNAL TABLE emp_partition(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2)\n    )\n    PARTITIONED BY (deptno INT)   -- 按照部门编号进行分区\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\"\n    LOCATION '/hive/emp_partition';\n```\n\n### 1.4 加载数据到分区表\n\n加载数据到分区表时候必须要指定数据所处的分区：\n\n```shell\n# 加载部门编号为20的数据到表中\nLOAD DATA LOCAL INPATH \"/usr/file/emp20.txt\" OVERWRITE INTO TABLE emp_partition PARTITION (deptno=20)\n# 加载部门编号为30的数据到表中\nLOAD DATA LOCAL INPATH \"/usr/file/emp30.txt\" OVERWRITE INTO TABLE emp_partition PARTITION (deptno=30)\n```\n\n### 1.5 查看分区目录\n\n这时候我们直接查看表目录，可以看到表目录下存在两个子目录，分别是 `deptno=20` 和 `deptno=30`,这就是分区目录，分区目录下才是我们加载的数据文件。\n\n```shell\n# hadoop fs -ls  hdfs://hadoop001:8020/hive/emp_partition/\n```\n\n这时候当你的查询语句的 `where` 包含 `deptno=20`，则就去对应的分区目录下进行查找，而不用扫描全表。\n\n<div align=\"center\"> <img  src=\"../pictures/hive-hadoop-partitation.png\"/> </div>\n\n\n\n## 二、分桶表\n\n### 1.1 简介\n\n分区提供了一个隔离数据和优化查询的可行方案，但是并非所有的数据集都可以形成合理的分区，分区的数量也不是越多越好，过多的分区条件可能会导致很多分区上没有数据。同时 Hive 会限制动态分区可以创建的最大分区数，用来避免过多分区文件对文件系统产生负担。鉴于以上原因，Hive 还提供了一种更加细粒度的数据拆分方案：分桶表 (bucket Table)。\n\n分桶表会将指定列的值进行哈希散列，并对 bucket（桶数量）取余，然后存储到对应的 bucket（桶）中。\n\n### 1.2 理解分桶表\n\n单从概念上理解分桶表可能会比较晦涩，其实和分区一样，分桶这个概念同样不是 Hive 独有的，对于 Java 开发人员而言，这可能是一个每天都会用到的概念，因为 Hive 中的分桶概念和 Java 数据结构中的 HashMap 的分桶概念是一致的。\n\n当调用 HashMap 的 put() 方法存储数据时，程序会先对 key 值调用 hashCode() 方法计算出 hashcode，然后对数组长度取模计算出 index，最后将数据存储在数组 index 位置的链表上，链表达到一定阈值后会转换为红黑树 (JDK1.8+)。下图为 HashMap 的数据结构图：\n\n<div align=\"center\"> <img width=\"600px\"  src=\"../pictures/HashMap-HashTable.png\"/> </div>\n\n> 图片引用自：[HashMap vs. Hashtable](http://www.itcuties.com/java/hashmap-hashtable/)\n\n### 1.3 创建分桶表\n\n在 Hive 中，我们可以通过 `CLUSTERED BY` 指定分桶列，并通过 `SORTED BY` 指定桶中数据的排序参考列。下面为分桶表建表语句示例：\n\n```sql\n  CREATE EXTERNAL TABLE emp_bucket(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2),\n    deptno INT)\n    CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS  --按照员工编号散列到四个 bucket 中\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\"\n    LOCATION '/hive/emp_bucket';\n```\n\n### 1.4 加载数据到分桶表\n\n这里直接使用 `Load` 语句向分桶表加载数据，数据时可以加载成功的，但是数据并不会分桶。\n\n这是由于分桶的实质是对指定字段做了 hash 散列然后存放到对应文件中，这意味着向分桶表中插入数据是必然要通过 MapReduce，且 Reducer 的数量必须等于分桶的数量。由于以上原因，分桶表的数据通常只能使用 CTAS(CREATE TABLE AS SELECT) 方式插入，因为 CTAS 操作会触发 MapReduce。加载数据步骤如下：\n\n#### 1. 设置强制分桶\n\n```sql\nset hive.enforce.bucketing = true; --Hive 2.x 不需要这一步\n```\n在 Hive 0.x and 1.x 版本，必须使用设置 `hive.enforce.bucketing = true`，表示强制分桶，允许程序根据表结构自动选择正确数量的 Reducer 和 cluster by  column 来进行分桶。\n\n#### 2. CTAS导入数据\n\n```sql\nINSERT INTO TABLE emp_bucket SELECT *  FROM emp;  --这里的 emp 表就是一张普通的雇员表\n```\n\n可以从执行日志看到 CTAS 触发 MapReduce 操作，且 Reducer 数量和建表时候指定 bucket 数量一致：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-hadoop-mapreducer.png\"/> </div>\n\n### 1.5 查看分桶文件\n\nbucket(桶) 本质上就是表目录下的具体文件：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-hadoop-bucket.png\"/> </div>\n\n\n\n## 三、分区表和分桶表结合使用\n\n分区表和分桶表的本质都是将数据按照不同粒度进行拆分，从而使得在查询时候不必扫描全表，只需要扫描对应的分区或分桶，从而提升查询效率。两者可以结合起来使用，从而保证表数据在不同粒度上都能得到合理的拆分。下面是 Hive 官方给出的示例：\n\n```sql\nCREATE TABLE page_view_bucketed(\n\tviewTime INT, \n    userid BIGINT,\n    page_url STRING, \n    referrer_url STRING,\n    ip STRING )\n PARTITIONED BY(dt STRING)\n CLUSTERED BY(userid) SORTED BY(viewTime) INTO 32 BUCKETS\n ROW FORMAT DELIMITED\n   FIELDS TERMINATED BY '\\001'\n   COLLECTION ITEMS TERMINATED BY '\\002'\n   MAP KEYS TERMINATED BY '\\003'\n STORED AS SEQUENCEFILE;\n```\n\n此时导入数据时需要指定分区：\n\n```shell\nINSERT OVERWRITE page_view_bucketed\nPARTITION (dt='2009-02-25')\nSELECT * FROM page_view WHERE dt='2009-02-25';\n```\n\n\n\n## 参考资料\n\n1. [LanguageManual DDL BucketedTables](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL+BucketedTables)\n"
  },
  {
    "path": "大数据框架学习/Hive常用DDL操作.md",
    "content": "# Hive常用DDL操作\n\n<nav>\n<a href=\"#一Database\">一、Database</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-查看数据列表\">1.1 查看数据列表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-使用数据库\">1.2 使用数据库</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-新建数据库\">1.3 新建数据库</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-查看数据库信息\">1.4 查看数据库信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-删除数据库\">1.5 删除数据库</a><br/>\n<a href=\"#二创建表\">二、创建表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-建表语法\">2.1 建表语法</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-内部表\">2.2 内部表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-外部表\">2.3 外部表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-分区表\">2.4 分区表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-分桶表\">2.5 分桶表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-倾斜表\">2.6 倾斜表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-临时表\">2.7 临时表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#28-CTAS创建表\">2.8 CTAS创建表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#29-复制表结构\">2.9 复制表结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#210-加载数据到表\">2.10 加载数据到表</a><br/>\n<a href=\"#三修改表\">三、修改表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-重命名表\">3.1 重命名表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-修改列\">3.2 修改列</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-新增列\">3.3 新增列</a><br/>\n<a href=\"#四清空表删除表\">四、清空表/删除表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-清空表\">4.1 清空表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-删除表\">4.2 删除表</a><br/>\n<a href=\"#五其他命令\">五、其他命令</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#51-Describe\">5.1 Describe</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#52-Show\">5.2 Show</a><br/>\n</nav>\n\n## 一、Database\n\n### 1.1 查看数据列表\n\n```sql\nshow databases;\n```\n\n<div align=\"center\"> <img width='700px' src=\"../pictures/hive-show-database.png\"/> </div>\n\n### 1.2 使用数据库\n\n```sql\nUSE database_name;\n```\n\n### 1.3 新建数据库\n\n语法：\n\n```sql\nCREATE (DATABASE|SCHEMA) [IF NOT EXISTS] database_name   --DATABASE|SCHEMA 是等价的\n  [COMMENT database_comment] --数据库注释\n  [LOCATION hdfs_path] --存储在 HDFS 上的位置\n  [WITH DBPROPERTIES (property_name=property_value, ...)]; --指定额外属性\n```\n\n示例：\n\n```sql\nCREATE DATABASE IF NOT EXISTS hive_test\n  COMMENT 'hive database for test'\n  WITH DBPROPERTIES ('create'='heibaiying');\n```\n\n\n\n### 1.4 查看数据库信息\n\n语法：\n\n```sql\nDESC DATABASE [EXTENDED] db_name; --EXTENDED 表示是否显示额外属性\n```\n\n示例：\n\n```sql\nDESC DATABASE  EXTENDED hive_test;\n```\n\n\n\n### 1.5 删除数据库\n\n语法：\n\n```sql\nDROP (DATABASE|SCHEMA) [IF EXISTS] database_name [RESTRICT|CASCADE];\n```\n\n+ 默认行为是 RESTRICT，如果数据库中存在表则删除失败。要想删除库及其中的表，可以使用 CASCADE 级联删除。\n\n示例：\n\n```sql\n  DROP DATABASE IF EXISTS hive_test CASCADE;\n```\n\n\n\n## 二、创建表\n\n### 2.1 建表语法\n\n```sql\nCREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name     --表名\n  [(col_name data_type [COMMENT col_comment],\n    ... [constraint_specification])]  --列名 列数据类型\n  [COMMENT table_comment]   --表描述\n  [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]  --分区表分区规则\n  [\n    CLUSTERED BY (col_name, col_name, ...) \n   [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS\n  ]  --分桶表分桶规则\n  [SKEWED BY (col_name, col_name, ...) ON ((col_value, col_value, ...), (col_value, col_value, ...), ...)  \n   [STORED AS DIRECTORIES] \n  ]  --指定倾斜列和值\n  [\n   [ROW FORMAT row_format]    \n   [STORED AS file_format]\n     | STORED BY 'storage.handler.class.name' [WITH SERDEPROPERTIES (...)]  \n  ]  -- 指定行分隔符、存储文件格式或采用自定义存储格式\n  [LOCATION hdfs_path]  -- 指定表的存储位置\n  [TBLPROPERTIES (property_name=property_value, ...)]  --指定表的属性\n  [AS select_statement];   --从查询结果创建表\n```\n\n### 2.2 内部表\n\n```sql\n  CREATE TABLE emp(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2),\n    deptno INT)\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n```\n\n### 2.3 外部表\n\n```sql\n  CREATE EXTERNAL TABLE emp_external(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2),\n    deptno INT)\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\"\n    LOCATION '/hive/emp_external';\n```\n\n使用 `desc format  emp_external` 命令可以查看表的详细信息如下：\n\n<div align=\"center\"> <img width='700px' src=\"../pictures/hive-external-table.png\"/> </div>\n\n### 2.4 分区表\n\n```sql\n  CREATE EXTERNAL TABLE emp_partition(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2)\n    )\n    PARTITIONED BY (deptno INT)   -- 按照部门编号进行分区\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\"\n    LOCATION '/hive/emp_partition';\n```\n\n### 2.5 分桶表\n\n```sql\n  CREATE EXTERNAL TABLE emp_bucket(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2),\n    deptno INT)\n    CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS  --按照员工编号散列到四个 bucket 中\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\"\n    LOCATION '/hive/emp_bucket';\n```\n\n### 2.6 倾斜表\n\n通过指定一个或者多个列经常出现的值（严重偏斜），Hive 会自动将涉及到这些值的数据拆分为单独的文件。在查询时，如果涉及到倾斜值，它就直接从独立文件中获取数据，而不是扫描所有文件，这使得性能得到提升。\n\n```sql\n  CREATE EXTERNAL TABLE emp_skewed(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2)\n    )\n    SKEWED BY (empno) ON (66,88,100)  --指定 empno 的倾斜值 66,88,100\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\"\n    LOCATION '/hive/emp_skewed';   \n```\n\n### 2.7 临时表\n\n临时表仅对当前 session 可见，临时表的数据将存储在用户的暂存目录中，并在会话结束后删除。如果临时表与永久表表名相同，则对该表名的任何引用都将解析为临时表，而不是永久表。临时表还具有以下两个限制：\n\n+ 不支持分区列；\n+ 不支持创建索引。\n\n```sql\n  CREATE TEMPORARY TABLE emp_temp(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2)\n    )\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n```\n\n### 2.8 CTAS创建表\n\n支持从查询语句的结果创建表：\n\n```sql\nCREATE TABLE emp_copy AS SELECT * FROM emp WHERE deptno='20';\n```\n\n### 2.9 复制表结构\n\n语法：\n\n```sql\nCREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name  --创建表表名\n   LIKE existing_table_or_view_name  --被复制表的表名\n   [LOCATION hdfs_path]; --存储位置\n```\n\n示例：\n\n```sql\nCREATE TEMPORARY EXTERNAL TABLE  IF NOT EXISTS  emp_co  LIKE emp\n```\n\n\n\n### 2.10 加载数据到表\n\n加载数据到表中属于 DML 操作，这里为了方便大家测试，先简单介绍一下加载本地数据到表中：\n\n```sql\n-- 加载数据到 emp 表中\nload data local inpath \"/usr/file/emp.txt\" into table emp;\n```\n\n其中 emp.txt 的内容如下，你可以直接复制使用，也可以到本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载：\n\n```txt\n7369\tSMITH\tCLERK\t7902\t1980-12-17 00:00:00\t800.00\t\t20\n7499\tALLEN\tSALESMAN\t7698\t1981-02-20 00:00:00\t1600.00\t300.00\t30\n7521\tWARD\tSALESMAN\t7698\t1981-02-22 00:00:00\t1250.00\t500.00\t30\n7566\tJONES\tMANAGER\t7839\t1981-04-02 00:00:00\t2975.00\t\t20\n7654\tMARTIN\tSALESMAN\t7698\t1981-09-28 00:00:00\t1250.00\t1400.00\t30\n7698\tBLAKE\tMANAGER\t7839\t1981-05-01 00:00:00\t2850.00\t\t30\n7782\tCLARK\tMANAGER\t7839\t1981-06-09 00:00:00\t2450.00\t\t10\n7788\tSCOTT\tANALYST\t7566\t1987-04-19 00:00:00\t1500.00\t\t20\n7839\tKING\tPRESIDENT\t\t1981-11-17 00:00:00\t5000.00\t\t10\n7844\tTURNER\tSALESMAN\t7698\t1981-09-08 00:00:00\t1500.00\t0.00\t30\n7876\tADAMS\tCLERK\t7788\t1987-05-23 00:00:00\t1100.00\t\t20\n7900\tJAMES\tCLERK\t7698\t1981-12-03 00:00:00\t950.00\t\t30\n7902\tFORD\tANALYST\t7566\t1981-12-03 00:00:00\t3000.00\t\t20\n7934\tMILLER\tCLERK\t7782\t1982-01-23 00:00:00\t1300.00\t\t10\n```\n\n加载后可查询表中数据：\n\n<div align=\"center\"> <img width='700px' src=\"../pictures/hive-select-emp.png\"/> </div>\n\n\n\n## 三、修改表\n\n### 3.1 重命名表\n\n语法：\n\n```sql\nALTER TABLE table_name RENAME TO new_table_name;\n```\n\n示例：\n\n```sql\nALTER TABLE emp_temp RENAME TO new_emp; --把 emp_temp 表重命名为 new_emp\n```\n\n\n\n### 3.2 修改列\n\n语法：\n\n```sql\nALTER TABLE table_name [PARTITION partition_spec] CHANGE [COLUMN] col_old_name col_new_name column_type\n  [COMMENT col_comment] [FIRST|AFTER column_name] [CASCADE|RESTRICT];\n```\n\n示例：\n\n```sql\n-- 修改字段名和类型\nALTER TABLE emp_temp CHANGE empno empno_new INT;\n \n-- 修改字段 sal 的名称 并将其放置到 empno 字段后\nALTER TABLE emp_temp CHANGE sal sal_new decimal(7,2)  AFTER ename;\n\n-- 为字段增加注释\nALTER TABLE emp_temp CHANGE mgr mgr_new INT COMMENT 'this is column mgr';\n```\n\n\n\n### 3.3 新增列\n\n示例：\n\n```sql\nALTER TABLE emp_temp ADD COLUMNS (address STRING COMMENT 'home address');\n```\n\n\n\n## 四、清空表/删除表\n\n### 4.1 清空表\n\n语法：\n\n```sql\n-- 清空整个表或表指定分区中的数据\nTRUNCATE TABLE table_name [PARTITION (partition_column = partition_col_value,  ...)];\n```\n\n+ 目前只有内部表才能执行 TRUNCATE 操作，外部表执行时会抛出异常 `Cannot truncate non-managed table XXXX`。\n\n示例：\n\n```sql\nTRUNCATE TABLE emp_mgt_ptn PARTITION (deptno=20);\n```\n\n\n\n### 4.2 删除表\n\n语法：\n\n```sql\nDROP TABLE [IF EXISTS] table_name [PURGE]; \n```\n\n+ 内部表：不仅会删除表的元数据，同时会删除 HDFS 上的数据；\n+ 外部表：只会删除表的元数据，不会删除 HDFS 上的数据；\n+ 删除视图引用的表时，不会给出警告（但视图已经无效了，必须由用户删除或重新创建）。\n\n\n\n## 五、其他命令\n\n### 5.1 Describe\n\n查看数据库：\n\n```sql\nDESCRIBE|Desc DATABASE [EXTENDED] db_name;  --EXTENDED 是否显示额外属性\n```\n\n查看表：\n\n```sql\nDESCRIBE|Desc [EXTENDED|FORMATTED] table_name --FORMATTED 以友好的展现方式查看表详情\n```\n\n\n\n### 5.2 Show\n\n**1. 查看数据库列表**\n\n```sql\n-- 语法\nSHOW (DATABASES|SCHEMAS) [LIKE 'identifier_with_wildcards'];\n\n-- 示例：\nSHOW DATABASES like 'hive*';\n```\n\nLIKE 子句允许使用正则表达式进行过滤，但是 SHOW 语句当中的 LIKE 子句只支持 `*`（通配符）和 `|`（条件或）两个符号。例如 `employees`，`emp *`，`emp * | * ees`，所有这些都将匹配名为 `employees` 的数据库。\n\n**2. 查看表的列表**\n\n```sql\n-- 语法\nSHOW TABLES [IN database_name] ['identifier_with_wildcards'];\n\n-- 示例\nSHOW TABLES IN default;\n```\n\n**3. 查看视图列表**\n\n```sql\nSHOW VIEWS [IN/FROM database_name] [LIKE 'pattern_with_wildcards'];   --仅支持 Hive 2.2.0 +\n```\n\n**4. 查看表的分区列表**\n\n```sql\nSHOW PARTITIONS table_name;\n```\n\n**5. 查看表/视图的创建语句**\n\n```sql\nSHOW CREATE TABLE ([db_name.]table_name|view_name);\n```\n\n\n\n## 参考资料\n\n[LanguageManual DDL](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL)\n"
  },
  {
    "path": "大数据框架学习/Hive常用DML操作.md",
    "content": "# Hive 常用DML操作\n\n<nav>\n<a href=\"#一加载文件数据到表\">一、加载文件数据到表</a><br/>\n<a href=\"#二查询结果插入到表\">二、查询结果插入到表</a><br/>\n<a href=\"#三使用SQL语句插入值\">三、使用SQL语句插入值</a><br/>\n<a href=\"#四更新和删除数据\">四、更新和删除数据</a><br/>\n<a href=\"#五查询结果写出到文件系统\">五、查询结果写出到文件系统</a><br/>\n</nav>\n\n\n## 一、加载文件数据到表\n\n### 1.1 语法\n\n```shell\nLOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] \nINTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)]\n```\n\n- `LOCAL` 关键字代表从本地文件系统加载文件，省略则代表从 HDFS 上加载文件：\n+ 从本地文件系统加载文件时， `filepath` 可以是绝对路径也可以是相对路径 (建议使用绝对路径)；\n  \n+ 从 HDFS 加载文件时候，`filepath` 为文件完整的 URL 地址：如 `hdfs://namenode:port/user/hive/project/ data1`\n  \n- `filepath` 可以是文件路径 (在这种情况下 Hive 会将文件移动到表中)，也可以目录路径 (在这种情况下，Hive 会将该目录中的所有文件移动到表中)；\n\n- 如果使用 OVERWRITE 关键字，则将删除目标表（或分区）的内容，使用新的数据填充；不使用此关键字，则数据以追加的方式加入；\n\n- 加载的目标可以是表或分区。如果是分区表，则必须指定加载数据的分区；\n\n- 加载文件的格式必须与建表时使用 ` STORED AS` 指定的存储格式相同。\n\n> 使用建议：\n>\n> **不论是本地路径还是 URL 都建议使用完整的**。虽然可以使用不完整的 URL 地址，此时 Hive 将使用 hadoop 中的 fs.default.name 配置来推断地址，但是为避免不必要的错误，建议使用完整的本地路径或 URL 地址；\n>\n> **加载对象是分区表时建议显示指定分区**。在 Hive 3.0 之后，内部将加载 (LOAD) 重写为 INSERT AS SELECT，此时如果不指定分区，INSERT AS SELECT 将假设最后一组列是分区列，如果该列不是表定义的分区，它将抛出错误。为避免错误，还是建议显示指定分区。\n\n### 1.2 示例\n\n新建分区表：\n\n```sql\n  CREATE TABLE emp_ptn(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2)\n    )\n    PARTITIONED BY (deptno INT)   -- 按照部门编号进行分区\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n```\n\n从 HDFS 上加载数据到分区表：\n\n```sql\nLOAD DATA  INPATH \"hdfs://hadoop001:8020/mydir/emp.txt\" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=20);\n```\n\n> emp.txt 文件可在本仓库的 resources 目录中下载\n\n加载后表中数据如下,分区列 deptno 全部赋值成 20：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-emp-ptn.png\"/> </div>\n\n## 二、查询结果插入到表\n\n### 2.1 语法\n\n```sql\nINSERT OVERWRITE TABLE tablename1 [PARTITION (partcol1=val1, partcol2=val2 ...) [IF NOT EXISTS]]   \nselect_statement1 FROM from_statement;\n\nINSERT INTO TABLE tablename1 [PARTITION (partcol1=val1, partcol2=val2 ...)] \nselect_statement1 FROM from_statement;\n```\n\n+ Hive 0.13.0 开始，建表时可以通过使用 TBLPROPERTIES（“immutable”=“true”）来创建不可变表 (immutable table) ，如果不可以变表中存在数据，则 INSERT INTO 失败。（注：INSERT OVERWRITE 的语句不受 `immutable` 属性的影响）;\n\n+ 可以对表或分区执行插入操作。如果表已分区，则必须通过指定所有分区列的值来指定表的特定分区；\n\n+ 从 Hive 1.1.0 开始，TABLE 关键字是可选的；\n\n+ 从 Hive 1.2.0 开始 ，可以采用 INSERT INTO tablename(z，x，c1) 指明插入列；\n\n+ 可以将 SELECT 语句的查询结果插入多个表（或分区），称为多表插入。语法如下：\n\n  ```sql\n  FROM from_statement\n  INSERT OVERWRITE TABLE tablename1 \n  [PARTITION (partcol1=val1, partcol2=val2 ...) [IF NOT EXISTS]] select_statement1\n  [INSERT OVERWRITE TABLE tablename2 [PARTITION ... [IF NOT EXISTS]] select_statement2]\n  [INSERT INTO TABLE tablename2 [PARTITION ...] select_statement2] ...;\n  ```\n\n### 2.2 动态插入分区\n\n```sql\nINSERT OVERWRITE TABLE tablename PARTITION (partcol1[=val1], partcol2[=val2] ...) \nselect_statement FROM from_statement;\n\nINSERT INTO TABLE tablename PARTITION (partcol1[=val1], partcol2[=val2] ...) \nselect_statement FROM from_statement;\n```\n\n在向分区表插入数据时候，分区列名是必须的，但是列值是可选的。如果给出了分区列值，我们将其称为静态分区，否则它是动态分区。动态分区列必须在 SELECT 语句的列中最后指定，并且与它们在 PARTITION() 子句中出现的顺序相同。\n\n注意：Hive 0.9.0 之前的版本动态分区插入是默认禁用的，而 0.9.0 之后的版本则默认启用。以下是动态分区的相关配置：\n\n| 配置                                       | 默认值   | 说明                                                         |\n| ------------------------------------------ | -------- | ------------------------------------------------------------ |\n| `hive.exec.dynamic.partition`              | `true`   | 需要设置为 true 才能启用动态分区插入                           |\n| `hive.exec.dynamic.partition.mode`         | `strict` | 在严格模式 (strict) 下，用户必须至少指定一个静态分区，以防用户意外覆盖所有分区，在非严格模式下，允许所有分区都是动态的 |\n| `hive.exec.max.dynamic.partitions.pernode` | 100      | 允许在每个 mapper/reducer 节点中创建的最大动态分区数           |\n| `hive.exec.max.dynamic.partitions`         | 1000     | 允许总共创建的最大动态分区数                                 |\n| `hive.exec.max.created.files`              | 100000   | 作业中所有 mapper/reducer 创建的 HDFS 文件的最大数量             |\n| `hive.error.on.empty.partition`            | `false`  | 如果动态分区插入生成空结果，是否抛出异常                     |\n\n### 2.3 示例\n\n1. 新建 emp 表，作为查询对象表\n\n```sql\nCREATE TABLE emp(\n    empno INT,\n    ename STRING,\n    job STRING,\n    mgr INT,\n    hiredate TIMESTAMP,\n    sal DECIMAL(7,2),\n    comm DECIMAL(7,2),\n    deptno INT)\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n    \n -- 加载数据到 emp 表中 这里直接从本地加载\nload data local inpath \"/usr/file/emp.txt\" into table emp;\n```\n​\t完成后 `emp` 表中数据如下：\n<div align=\"center\"> <img  src=\"../pictures/hive-emp.png\"/> </div>\n\n2. 为清晰演示，先清空 `emp_ptn` 表中加载的数据：\n\n```sql\nTRUNCATE TABLE emp_ptn;\n```\n\n3. 静态分区演示：从 `emp` 表中查询部门编号为 20 的员工数据，并插入 `emp_ptn` 表中，语句如下：\n\n```sql\nINSERT OVERWRITE TABLE emp_ptn PARTITION (deptno=20) \nSELECT empno,ename,job,mgr,hiredate,sal,comm FROM emp WHERE deptno=20;\n```\n\n​\t完成后 `emp_ptn` 表中数据如下：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-emp-deptno-20.png\"/> </div>\n\n4. 接着演示动态分区：\n\n```sql\n-- 由于我们只有一个分区，且还是动态分区，所以需要关闭严格默认。因为在严格模式下，用户必须至少指定一个静态分区\nset hive.exec.dynamic.partition.mode=nonstrict;\n\n-- 动态分区   此时查询语句的最后一列为动态分区列，即 deptno\nINSERT OVERWRITE TABLE emp_ptn PARTITION (deptno) \nSELECT empno,ename,job,mgr,hiredate,sal,comm,deptno FROM emp WHERE deptno=30;\n```\n\n​\t完成后 `emp_ptn` 表中数据如下：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-emp-deptno-20-30.png\"/> </div>\n\n\n\n## 三、使用SQL语句插入值\n\n```sql\nINSERT INTO TABLE tablename [PARTITION (partcol1[=val1], partcol2[=val2] ...)] \nVALUES ( value [, value ...] )\n```\n\n+ 使用时必须为表中的每个列都提供值。不支持只向部分列插入值（可以为缺省值的列提供空值来消除这个弊端）；\n+ 如果目标表表支持 ACID 及其事务管理器，则插入后自动提交；\n+ 不支持支持复杂类型 (array, map, struct, union) 的插入。\n\n\n\n## 四、更新和删除数据\n\n### 4.1 语法\n\n更新和删除的语法比较简单，和关系型数据库一致。需要注意的是这两个操作都只能在支持 ACID 的表，也就是事务表上才能执行。\n\n```sql\n-- 更新\nUPDATE tablename SET column = value [, column = value ...] [WHERE expression]\n\n--删除\nDELETE FROM tablename [WHERE expression]\n```\n\n### 4.2 示例\n\n**1. 修改配置**\n\n首先需要更改 `hive-site.xml`，添加如下配置，开启事务支持，配置完成后需要重启 Hive 服务。\n\n```xml\n<property>\n    <name>hive.support.concurrency</name>\n    <value>true</value>\n</property>\n<property>\n    <name>hive.enforce.bucketing</name>\n    <value>true</value>\n</property>\n<property>\n    <name>hive.exec.dynamic.partition.mode</name>\n    <value>nonstrict</value>\n</property>\n<property>\n    <name>hive.txn.manager</name>\n    <value>org.apache.hadoop.hive.ql.lockmgr.DbTxnManager</value>\n</property>\n<property>\n    <name>hive.compactor.initiator.on</name>\n    <value>true</value>\n</property>\n<property>\n    <name>hive.in.test</name>\n    <value>true</value>\n</property>\n```\n\n**2. 创建测试表**\n\n创建用于测试的事务表，建表时候指定属性 `transactional = true` 则代表该表是事务表。需要注意的是，按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/Hive+Transactions) 的说明，目前 Hive 中的事务表有以下限制：\n\n+ 必须是 buckets Table;\n+ 仅支持 ORC 文件格式；\n+ 不支持 LOAD DATA ...语句。\n\n```sql\nCREATE TABLE emp_ts(  \n  empno int,  \n  ename String\n)\nCLUSTERED BY (empno) INTO 2 BUCKETS STORED AS ORC\nTBLPROPERTIES (\"transactional\"=\"true\");\n```\n\n**3. 插入测试数据**\n\n```sql\nINSERT INTO TABLE emp_ts  VALUES (1,\"ming\"),(2,\"hong\");\n```\n\n插入数据依靠的是 MapReduce 作业，执行成功后数据如下：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-emp-ts.png\"/> </div>\n\n**4. 测试更新和删除**\n\n```sql\n--更新数据\nUPDATE emp_ts SET ename = \"lan\"  WHERE  empno=1;\n\n--删除数据\nDELETE FROM emp_ts WHERE empno=2;\n```\n\n更新和删除数据依靠的也是 MapReduce 作业，执行成功后数据如下：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-emp-ts-2.png\"/> </div>\n\n\n## 五、查询结果写出到文件系统\n\n### 5.1 语法\n\n```sql\nINSERT OVERWRITE [LOCAL] DIRECTORY directory1\n  [ROW FORMAT row_format] [STORED AS file_format] \n  SELECT ... FROM ...\n```\n\n+ OVERWRITE 关键字表示输出文件存在时，先删除后再重新写入；\n\n+ 和 Load 语句一样，建议无论是本地路径还是 URL 地址都使用完整的；\n\n+ 写入文件系统的数据被序列化为文本，其中列默认由^A 分隔，行由换行符分隔。如果列不是基本类型，则将其序列化为 JSON 格式。其中行分隔符不允许自定义，但列分隔符可以自定义，如下：\n\n  ```sql\n  -- 定义列分隔符为'\\t' \n  insert overwrite local directory './test-04' \n  row format delimited \n  FIELDS TERMINATED BY '\\t'\n  COLLECTION ITEMS TERMINATED BY ','\n  MAP KEYS TERMINATED BY ':'\n  select * from src;\n  ```\n\n### 5.2 示例\n\n这里我们将上面创建的 `emp_ptn` 表导出到本地文件系统，语句如下：\n\n```sql\nINSERT OVERWRITE LOCAL DIRECTORY '/usr/file/ouput'\nROW FORMAT DELIMITED\nFIELDS TERMINATED BY '\\t'\nSELECT * FROM emp_ptn;\n```\n\n导出结果如下：\n\n<div align=\"center\"> <img  src=\"../pictures/hive-ouput.png\"/> </div>\n\n\n\n\n\n## 参考资料\n\n1. [Hive Transactions](https://cwiki.apache.org/confluence/display/Hive/Hive+Transactions)\n2. [Hive Data Manipulation Language](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DML)\n"
  },
  {
    "path": "大数据框架学习/Hive数据查询详解.md",
    "content": "# Hive数据查询详解\n\n<nav>\n<a href=\"#一数据准备\">一、数据准备</a><br/>\n<a href=\"#二单表查询\">二、单表查询</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-SELECT\">2.1 SELECT</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-WHERE\">2.2 WHERE</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23--DISTINCT\">2.3  DISTINCT</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-分区查询\">2.4 分区查询</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-LIMIT\">2.5 LIMIT</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-GROUP-BY\">2.6 GROUP BY</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-ORDER-AND-SORT\">2.7 ORDER AND SORT</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#28-HAVING\">2.8 HAVING</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#29-DISTRIBUTE-BY\">2.9 DISTRIBUTE BY</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#210-CLUSTER-BY\">2.10 CLUSTER BY</a><br/>\n<a href=\"#三多表联结查询\">三、多表联结查询</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-INNER-JOIN\">3.1 INNER JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-LEFT-OUTER--JOIN\">3.2 LEFT OUTER  JOIN </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-RIGHT-OUTER--JOIN\">3.3 RIGHT OUTER  JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-FULL-OUTER--JOIN\">3.4 FULL OUTER  JOIN </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-LEFT-SEMI-JOIN\">3.5 LEFT SEMI JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#36-JOIN\">3.6 JOIN</a><br/>\n<a href=\"#四JOIN优化\">四、JOIN优化</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-STREAMTABLE\">4.1 STREAMTABLE</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-MAPJOIN\">4.2 MAPJOIN</a><br/>\n<a href=\"#五SELECT的其他用途\">五、SELECT的其他用途</a><br/>\n<a href=\"#六本地模式\">六、本地模式</a><br/>\n</nav>\n\n\n\n## 一、数据准备\n\n为了演示查询操作，这里需要预先创建三张表，并加载测试数据。\n\n> 数据文件 emp.txt 和 dept.txt 可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载。\n\n### 1.1 员工表\n\n```sql\n -- 建表语句\n CREATE TABLE emp(\n     empno INT,     -- 员工表编号\n     ename STRING,  -- 员工姓名\n     job STRING,    -- 职位类型\n     mgr INT,   \n     hiredate TIMESTAMP,  --雇佣日期\n     sal DECIMAL(7,2),  --工资\n     comm DECIMAL(7,2),\n     deptno INT)   --部门编号\n    ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n\n  --加载数据\nLOAD DATA LOCAL INPATH \"/usr/file/emp.txt\" OVERWRITE INTO TABLE emp;\n```\n\n### 1.2 部门表\n\n```sql\n -- 建表语句\n CREATE TABLE dept(\n     deptno INT,   --部门编号\n     dname STRING,  --部门名称\n     loc STRING    --部门所在的城市\n )\n ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n \n --加载数据\n LOAD DATA LOCAL INPATH \"/usr/file/dept.txt\" OVERWRITE INTO TABLE dept;\n```\n\n### 1.3 分区表\n\n这里需要额外创建一张分区表，主要是为了演示分区查询：\n\n```sql\nCREATE EXTERNAL TABLE emp_ptn(\n      empno INT,\n      ename STRING,\n      job STRING,\n      mgr INT,\n      hiredate TIMESTAMP,\n      sal DECIMAL(7,2),\n      comm DECIMAL(7,2)\n  )\n PARTITIONED BY (deptno INT)   -- 按照部门编号进行分区\n ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n\n\n--加载数据\nLOAD DATA LOCAL INPATH \"/usr/file/emp.txt\" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=20)\nLOAD DATA LOCAL INPATH \"/usr/file/emp.txt\" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=30)\nLOAD DATA LOCAL INPATH \"/usr/file/emp.txt\" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=40)\nLOAD DATA LOCAL INPATH \"/usr/file/emp.txt\" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=50)\n```\n\n\n\n## 二、单表查询\n\n### 2.1 SELECT\n\n```sql\n-- 查询表中全部数据\nSELECT * FROM emp;\n```\n\n\n\n### 2.2 WHERE\n\n```sql\n-- 查询 10 号部门中员工编号大于 7782 的员工信息 \nSELECT * FROM emp WHERE empno > 7782 AND deptno = 10;\n```\n\n\n\n### 2.3  DISTINCT\n\nHive 支持使用 DISTINCT 关键字去重。\n\n```sql\n-- 查询所有工作类型\nSELECT DISTINCT job FROM emp;\n```\n\n\n\n### 2.4 分区查询\n\n分区查询 (Partition Based Queries)，可以指定某个分区或者分区范围。\n\n```sql\n-- 查询分区表中部门编号在[20,40]之间的员工\nSELECT emp_ptn.* FROM emp_ptn\nWHERE emp_ptn.deptno >= 20 AND emp_ptn.deptno <= 40;\n```\n\n\n\n### 2.5 LIMIT\n\n```sql\n-- 查询薪资最高的 5 名员工\nSELECT * FROM emp ORDER BY sal DESC LIMIT 5;\n```\n\n\n\n### 2.6 GROUP BY\n\nHive 支持使用 GROUP BY 进行分组聚合操作。\n\n```sql\nset hive.map.aggr=true;\n\n-- 查询各个部门薪酬综合\nSELECT deptno,SUM(sal) FROM emp GROUP BY deptno;\n```\n\n`hive.map.aggr` 控制程序如何进行聚合。默认值为 false。如果设置为 true，Hive 会在 map 阶段就执行一次聚合。这可以提高聚合效率，但需要消耗更多内存。\n\n\n\n### 2.7 ORDER AND SORT\n\n可以使用 ORDER BY 或者 Sort BY 对查询结果进行排序，排序字段可以是整型也可以是字符串：如果是整型，则按照大小排序；如果是字符串，则按照字典序排序。ORDER BY 和 SORT BY 的区别如下：\n\n+ 使用 ORDER BY 时会有一个 Reducer 对全部查询结果进行排序，可以保证数据的全局有序性；\n+ 使用 SORT BY 时只会在每个 Reducer 中进行排序，这可以保证每个 Reducer 的输出数据是有序的，但不能保证全局有序。\n\n由于 ORDER BY 的时间可能很长，如果你设置了严格模式 (hive.mapred.mode = strict)，则其后面必须再跟一个 `limit` 子句。\n\n> 注 ：hive.mapred.mode 默认值是 nonstrict ，也就是非严格模式。\n\n```sql\n-- 查询员工工资，结果按照部门升序，按照工资降序排列\nSELECT empno, deptno, sal FROM emp ORDER BY deptno ASC, sal DESC;\n```\n\n\n\n### 2.8 HAVING\n\n可以使用 HAVING 对分组数据进行过滤。\n\n```sql\n-- 查询工资总和大于 9000 的所有部门\nSELECT deptno,SUM(sal) FROM emp GROUP BY deptno HAVING SUM(sal)>9000;\n```\n\n\n\n### 2.9 DISTRIBUTE BY\n\n默认情况下，MapReduce 程序会对 Map 输出结果的 Key 值进行散列，并均匀分发到所有 Reducer 上。如果想要把具有相同 Key 值的数据分发到同一个 Reducer 进行处理，这就需要使用 DISTRIBUTE BY 字句。\n\n需要注意的是，DISTRIBUTE BY 虽然能保证具有相同 Key 值的数据分发到同一个 Reducer，但是不能保证数据在 Reducer 上是有序的。情况如下：\n\n把以下 5 个数据发送到两个 Reducer 上进行处理：\n\n```properties\nk1\nk2\nk4\nk3\nk1\n```\n\nReducer1 得到如下乱序数据：\n\n```properties\nk1\nk2\nk1\n```\n\n\nReducer2 得到数据如下：\n\n```properties\nk4\nk3\n```\n\n如果想让 Reducer 上的数据时有序的，可以结合 `SORT BY` 使用 (示例如下)，或者使用下面我们将要介绍的 CLUSTER BY。\n\n```sql\n-- 将数据按照部门分发到对应的 Reducer 上处理\nSELECT empno, deptno, sal FROM emp DISTRIBUTE BY deptno SORT BY deptno ASC;\n```\n\n\n\n### 2.10 CLUSTER BY\n\n如果 `SORT BY` 和 `DISTRIBUTE BY` 指定的是相同字段，且 SORT BY 排序规则是 ASC，此时可以使用 `CLUSTER BY` 进行替换，同时 `CLUSTER BY` 可以保证数据在全局是有序的。\n\n```sql\nSELECT empno, deptno, sal FROM emp CLUSTER  BY deptno ;\n```\n\n\n\n## 三、多表联结查询\n\nHive 支持内连接，外连接，左外连接，右外连接，笛卡尔连接，这和传统数据库中的概念是一致的，可以参见下图。\n\n需要特别强调：JOIN 语句的关联条件必须用 ON 指定，不能用 WHERE 指定，否则就会先做笛卡尔积，再过滤，这会导致你得不到预期的结果 (下面的演示会有说明)。\n\n<div align=\"center\"> <img width=\"600px\"  src=\"../pictures/sql-join.jpg\"/> </div>\n\n### 3.1 INNER JOIN\n\n```sql\n-- 查询员工编号为 7369 的员工的详细信息\nSELECT e.*,d.* FROM \nemp e JOIN dept d\nON e.deptno = d.deptno \nWHERE empno=7369;\n\n--如果是三表或者更多表连接，语法如下\nSELECT a.val, b.val, c.val FROM a JOIN b ON (a.key = b.key1) JOIN c ON (c.key = b.key1)\n```\n\n### 3.2 LEFT OUTER  JOIN \n\nLEFT OUTER  JOIN 和 LEFT  JOIN 是等价的。 \n\n```sql\n-- 左连接\nSELECT e.*,d.*\nFROM emp e LEFT OUTER  JOIN  dept d\nON e.deptno = d.deptno;\n```\n\n### 3.3 RIGHT OUTER  JOIN\n\n```sql\n--右连接\nSELECT e.*,d.*\nFROM emp e RIGHT OUTER JOIN  dept d\nON e.deptno = d.deptno;\n```\n\n执行右连接后，由于 40 号部门下没有任何员工，所以此时员工信息为 NULL。这个查询可以很好的复述上面提到的——JOIN 语句的关联条件必须用 ON 指定，不能用 WHERE 指定。你可以把 ON 改成 WHERE，你会发现无论如何都查不出 40 号部门这条数据，因为笛卡尔运算不会有 (NULL, 40) 这种情况。\n\n<div align=\"center\"> <img width=\"700px\"   src=\"../pictures/hive-right-join.png\"/> </div>\n### 3.4 FULL OUTER  JOIN \n\n```sql\nSELECT e.*,d.*\nFROM emp e FULL OUTER JOIN  dept d\nON e.deptno = d.deptno;\n```\n\n### 3.5 LEFT SEMI JOIN\n\nLEFT SEMI JOIN （左半连接）是 IN/EXISTS 子查询的一种更高效的实现。\n\n+ JOIN 子句中右边的表只能在 ON 子句中设置过滤条件;\n+ 查询结果只包含左边表的数据，所以只能 SELECT 左表中的列。\n\n```sql\n-- 查询在纽约办公的所有员工信息\nSELECT emp.*\nFROM emp LEFT SEMI JOIN dept \nON emp.deptno = dept.deptno AND dept.loc=\"NEW YORK\";\n\n--上面的语句就等价于\nSELECT emp.* FROM emp\nWHERE emp.deptno IN (SELECT deptno FROM dept WHERE loc=\"NEW YORK\");\n```\n\n### 3.6 JOIN\n\n笛卡尔积连接，这个连接日常的开发中可能很少遇到，且性能消耗比较大，基于这个原因，如果在严格模式下 (hive.mapred.mode = strict)，Hive 会阻止用户执行此操作。\n\n```sql\nSELECT * FROM emp JOIN dept;\n```\n\n\n\n## 四、JOIN优化\n\n### 4.1 STREAMTABLE\n\n在多表进行联结的时候，如果每个 ON 字句都使用到共同的列（如下面的 `b.key`），此时 Hive 会进行优化，将多表 JOIN 在同一个 map / reduce 作业上进行。同时假定查询的最后一个表（如下面的 c 表）是最大的一个表，在对每行记录进行 JOIN 操作时，它将尝试将其他的表缓存起来，然后扫描最后那个表进行计算。因此用户需要保证查询的表的大小从左到右是依次增加的。\n\n```sql\n`SELECT a.val, b.val, c.val FROM a JOIN b ON (a.key = b.key) JOIN c ON (c.key = b.key)`\n```\n\n然后，用户并非需要总是把最大的表放在查询语句的最后面，Hive 提供了 `/*+ STREAMTABLE() */` 标志，用于标识最大的表，示例如下：\n\n```sql\nSELECT /*+ STREAMTABLE(d) */  e.*,d.* \nFROM emp e JOIN dept d\nON e.deptno = d.deptno\nWHERE job='CLERK';\n```\n\n\n\n### 4.2 MAPJOIN\n\n如果所有表中只有一张表是小表，那么 Hive 把这张小表加载到内存中。这时候程序会在 map 阶段直接拿另外一个表的数据和内存中表数据做匹配，由于在 map 就进行了 JOIN 操作，从而可以省略 reduce 过程，这样效率可以提升很多。Hive 中提供了 `/*+ MAPJOIN() */` 来标记小表，示例如下：\n\n```sql\nSELECT /*+ MAPJOIN(d) */ e.*,d.* \nFROM emp e JOIN dept d\nON e.deptno = d.deptno\nWHERE job='CLERK';\n```\n\n\n\n## 五、SELECT的其他用途\n\n查看当前数据库：\n\n```sql\nSELECT current_database()\n```\n\n\n\n## 六、本地模式\n\n在上面演示的语句中，大多数都会触发 MapReduce, 少部分不会触发，比如 `select * from emp limit 5` 就不会触发 MR，此时 Hive 只是简单的读取数据文件中的内容，然后格式化后进行输出。在需要执行 MapReduce 的查询中，你会发现执行时间可能会很长，这时候你可以选择开启本地模式。\n\n```sql\n--本地模式默认关闭，需要手动开启此功能\nSET hive.exec.mode.local.auto=true;\n```\n\n启用后，Hive 将分析查询中每个 map-reduce 作业的大小，如果满足以下条件，则可以在本地运行它：\n\n- 作业的总输入大小低于：hive.exec.mode.local.auto.inputbytes.max（默认为 128MB）；\n- map-tasks 的总数小于：hive.exec.mode.local.auto.tasks.max（默认为 4）；\n- 所需的 reduce 任务总数为 1 或 0。\n\n因为我们测试的数据集很小，所以你再次去执行上面涉及 MR 操作的查询，你会发现速度会有显著的提升。\n\n\n\n\n\n## 参考资料\n\n1. [LanguageManual Select](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Select)\n2. [LanguageManual Joins](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Joins)\n3. [LanguageManual GroupBy](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+GroupBy)\n4. [LanguageManual SortBy](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+SortBy)"
  },
  {
    "path": "大数据框架学习/Hive简介及核心概念.md",
    "content": "# Hive简介及核心概念\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二Hive的体系架构\">二、Hive的体系架构</a><br/>\n<a href=\"#三数据类型\">三、数据类型</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-基本数据类型\">3.1 基本数据类型</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-隐式转换\">3.2 隐式转换</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-复杂类型\">3.3 复杂类型</a><br/>\n<a href=\"#四内容格式\">四、内容格式</a><br/>\n<a href=\"#五存储格式\">五、存储格式</a><br/>\n<a href=\"#六内部表和外部表\">六、内部表和外部表</a><br/>\n</nav>\n\n\n## 一、简介\n\nHive 是一个构建在 Hadoop 之上的数据仓库，它可以将结构化的数据文件映射成表，并提供类 SQL 查询功能，用于查询的 SQL 语句会被转化为 MapReduce 作业，然后提交到 Hadoop 上运行。\n\n**特点**：\n\n1. 简单、容易上手 (提供了类似 sql 的查询语言 hql)，使得精通 sql 但是不了解 Java 编程的人也能很好地进行大数据分析；\n3. 灵活性高，可以自定义用户函数 (UDF) 和存储格式；\n4. 为超大的数据集设计的计算和存储能力，集群扩展容易;\n5. 统一的元数据管理，可与 presto／impala／sparksql 等共享数据；\n5. 执行延迟高，不适合做数据的实时处理，但适合做海量数据的离线处理。\n\n\n\n## 二、Hive的体系架构\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/hive体系架构.png\"/> </div>\n\n### 2.1 command-line shell & thrift/jdbc\n\n可以用 command-line shell 和 thrift／jdbc 两种方式来操作数据：\n\n+ **command-line shell**：通过 hive 命令行的方式来操作数据；\n+ **thrift／jdbc**：通过 thrift 协议按照标准的 JDBC 的方式操作数据。\n\n### 2.2 Metastore\n\n在 Hive 中，表名、表结构、字段名、字段类型、表的分隔符等统一被称为元数据。所有的元数据默认存储在 Hive 内置的 derby 数据库中，但由于 derby 只能有一个实例，也就是说不能有多个命令行客户端同时访问，所以在实际生产环境中，通常使用 MySQL 代替 derby。\n\nHive 进行的是统一的元数据管理，就是说你在 Hive 上创建了一张表，然后在 presto／impala／sparksql 中都是可以直接使用的，它们会从 Metastore 中获取统一的元数据信息，同样的你在 presto／impala／sparksql 中创建一张表，在 Hive 中也可以直接使用。\n\n### 2.3 HQL的执行流程\n\nHive 在执行一条 HQL 的时候，会经过以下步骤：\n\n1. 语法解析：Antlr 定义 SQL 的语法规则，完成 SQL 词法，语法解析，将 SQL 转化为抽象 语法树 AST Tree；\n2. 语义解析：遍历 AST Tree，抽象出查询的基本组成单元 QueryBlock；\n3. 生成逻辑执行计划：遍历 QueryBlock，翻译为执行操作树 OperatorTree；\n4. 优化逻辑执行计划：逻辑层优化器进行 OperatorTree 变换，合并不必要的 ReduceSinkOperator，减少 shuffle 数据量；\n5. 生成物理执行计划：遍历 OperatorTree，翻译为 MapReduce 任务；\n6. 优化物理执行计划：物理层优化器进行 MapReduce 任务的变换，生成最终的执行计划。\n\n> 关于 Hive SQL 的详细执行流程可以参考美团技术团队的文章：[Hive SQL 的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html)\n\n\n\n## 三、数据类型\n\n### 3.1 基本数据类型\n\nHive 表中的列支持以下基本数据类型：\n\n| 大类                                    | 类型                                                         |\n| --------------------------------------- | ------------------------------------------------------------ |\n| **Integers（整型）**                    | TINYINT—1 字节的有符号整数 <br/>SMALLINT—2 字节的有符号整数<br/> INT—4 字节的有符号整数<br/> BIGINT—8 字节的有符号整数 |\n| **Boolean（布尔型）**                   | BOOLEAN—TRUE/FALSE                                           |\n| **Floating point numbers（浮点型）**    | FLOAT— 单精度浮点型 <br/>DOUBLE—双精度浮点型                 |\n| **Fixed point numbers（定点数）**       | DECIMAL—用户自定义精度定点数，比如 DECIMAL(7,2)               |\n| **String types（字符串）**              | STRING—指定字符集的字符序列<br/> VARCHAR—具有最大长度限制的字符序列 <br/>CHAR—固定长度的字符序列 |\n| **Date and time types（日期时间类型）** | TIMESTAMP —  时间戳 <br/>TIMESTAMP WITH LOCAL TIME ZONE — 时间戳，纳秒精度<br/> DATE—日期类型 |\n| **Binary types（二进制类型）**          | BINARY—字节序列                                              |\n\n> TIMESTAMP 和 TIMESTAMP WITH LOCAL TIME ZONE 的区别如下：\n>\n> - **TIMESTAMP WITH LOCAL TIME ZONE**：用户提交时间给数据库时，会被转换成数据库所在的时区来保存。查询时则按照查询客户端的不同，转换为查询客户端所在时区的时间。\n> - **TIMESTAMP** ：提交什么时间就保存什么时间，查询时也不做任何转换。\n\n### 3.2 隐式转换\n\nHive 中基本数据类型遵循以下的层次结构，按照这个层次结构，子类型到祖先类型允许隐式转换。例如 INT 类型的数据允许隐式转换为 BIGINT 类型。额外注意的是：按照类型层次结构允许将 STRING 类型隐式转换为 DOUBLE 类型。\n\n<div align=\"center\"> <img  src=\"../pictures/hive-data-type.png\"/> </div>\n\n\n\n### 3.3 复杂类型\n\n| 类型       | 描述                                                         | 示例                                   |\n| ---------- | ------------------------------------------------------------ | -------------------------------------- |\n| **STRUCT** | 类似于对象，是字段的集合，字段的类型可以不同，可以使用 ` 名称.字段名 ` 方式进行访问 | STRUCT ('xiaoming', 12 , '2018-12-12') |\n| **MAP**    | 键值对的集合，可以使用 ` 名称[key]` 的方式访问对应的值          | map('a', 1, 'b', 2)                    |\n| **ARRAY**  | 数组是一组具有相同类型和名称的变量的集合，可以使用 ` 名称[index]` 访问对应的值 | ARRAY('a', 'b', 'c', 'd')              |\n\n\n\n### 3.4 示例\n\n如下给出一个基本数据类型和复杂数据类型的使用示例：\n\n```sql\nCREATE TABLE students(\n  name      STRING,   -- 姓名\n  age       INT,      -- 年龄\n  subject   ARRAY<STRING>,   --学科\n  score     MAP<STRING,FLOAT>,  --各个学科考试成绩\n  address   STRUCT<houseNumber:int, street:STRING, city:STRING, province：STRING>  --家庭居住地址\n) ROW FORMAT DELIMITED FIELDS TERMINATED BY \"\\t\";\n```\n\n\n\n## 四、内容格式\n\n当数据存储在文本文件中，必须按照一定格式区别行和列，如使用逗号作为分隔符的 CSV 文件 (Comma-Separated Values) 或者使用制表符作为分隔值的 TSV 文件 (Tab-Separated Values)。但此时也存在一个缺点，就是正常的文件内容中也可能出现逗号或者制表符。\n\n所以 Hive 默认使用了几个平时很少出现的字符，这些字符一般不会作为内容出现在文件中。Hive 默认的行和列分隔符如下表所示。\n\n| 分隔符          | 描述                                                         |\n| --------------- | ------------------------------------------------------------ |\n| **\\n**          | 对于文本文件来说，每行是一条记录，所以可以使用换行符来分割记录 |\n| **^A (Ctrl+A)** | 分割字段 (列)，在 CREATE TABLE 语句中也可以使用八进制编码 `\\001` 来表示 |\n| **^B**          | 用于分割 ARRAY 或者 STRUCT 中的元素，或者用于 MAP 中键值对之间的分割，<br/>在 CREATE TABLE 语句中也可以使用八进制编码 `\\002` 表示 |\n| **^C**          | 用于 MAP 中键和值之间的分割，在 CREATE TABLE 语句中也可以使用八进制编码 `\\003` 表示 |\n\n使用示例如下：\n\n```sql\nCREATE TABLE page_view(viewTime INT, userid BIGINT)\n ROW FORMAT DELIMITED\n   FIELDS TERMINATED BY '\\001'\n   COLLECTION ITEMS TERMINATED BY '\\002'\n   MAP KEYS TERMINATED BY '\\003'\n STORED AS SEQUENCEFILE;\n```\n\n\n\n## 五、存储格式\n\n### 5.1 支持的存储格式\n\nHive 会在 HDFS 为每个数据库上创建一个目录，数据库中的表是该目录的子目录，表中的数据会以文件的形式存储在对应的表目录下。Hive 支持以下几种文件存储格式：\n\n| 格式             | 说明                                                         |\n| ---------------- | ------------------------------------------------------------ |\n| **TextFile**     | 存储为纯文本文件。 这是 Hive 默认的文件存储格式。这种存储方式数据不做压缩，磁盘开销大，数据解析开销大。 |\n| **SequenceFile** | SequenceFile 是 Hadoop API 提供的一种二进制文件，它将数据以<key,value>的形式序列化到文件中。这种二进制文件内部使用 Hadoop 的标准的 Writable 接口实现序列化和反序列化。它与 Hadoop API 中的 MapFile 是互相兼容的。Hive 中的 SequenceFile 继承自 Hadoop API 的 SequenceFile，不过它的 key 为空，使用 value 存放实际的值，这样是为了避免 MR 在运行 map 阶段进行额外的排序操作。 |\n| **RCFile**       | RCFile 文件格式是 FaceBook 开源的一种 Hive 的文件存储格式，首先将表分为几个行组，对每个行组内的数据按列存储，每一列的数据都是分开存储。 |\n| **ORC Files**    | ORC 是在一定程度上扩展了 RCFile，是对 RCFile 的优化。            |\n| **Avro Files**   | Avro 是一个数据序列化系统，设计用于支持大批量数据交换的应用。它的主要特点有：支持二进制序列化方式，可以便捷，快速地处理大量数据；动态语言友好，Avro 提供的机制使动态语言可以方便地处理 Avro 数据。 |\n| **Parquet**      | Parquet 是基于 Dremel 的数据模型和算法实现的，面向分析型业务的列式存储格式。它通过按列进行高效压缩和特殊的编码技术，从而在降低存储空间的同时提高了 IO 效率。 |\n\n> 以上压缩格式中 ORC 和 Parquet 的综合性能突出，使用较为广泛，推荐使用这两种格式。\n\n### 5.2 指定存储格式\n\n通常在创建表的时候使用 `STORED AS` 参数指定：\n\n```sql\nCREATE TABLE page_view(viewTime INT, userid BIGINT)\n ROW FORMAT DELIMITED\n   FIELDS TERMINATED BY '\\001'\n   COLLECTION ITEMS TERMINATED BY '\\002'\n   MAP KEYS TERMINATED BY '\\003'\n STORED AS SEQUENCEFILE;\n```\n\n各个存储文件类型指定方式如下：\n\n- STORED AS TEXTFILE\n- STORED AS SEQUENCEFILE\n- STORED AS ORC\n- STORED AS PARQUET\n- STORED AS AVRO\n- STORED AS RCFILE\n\n\n\n## 六、内部表和外部表\n\n内部表又叫做管理表 (Managed/Internal Table)，创建表时不做任何指定，默认创建的就是内部表。想要创建外部表 (External Table)，则需要使用 External 进行修饰。 内部表和外部表主要区别如下：\n\n|              | 内部表                                                       | 外部表                                                       |\n| ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| 数据存储位置 | 内部表数据存储的位置由 hive.metastore.warehouse.dir 参数指定，默认情况下表的数据存储在 HDFS 的 `/user/hive/warehouse/数据库名.db/表名/`  目录下 | 外部表数据的存储位置创建表时由 `Location` 参数指定；           |\n| 导入数据     | 在导入数据到内部表，内部表将数据移动到自己的数据仓库目录下，数据的生命周期由 Hive 来进行管理 | 外部表不会将数据移动到自己的数据仓库目录下，只是在元数据中存储了数据的位置 |\n| 删除表       | 删除元数据（metadata）和文件                                 | 只删除元数据（metadata）                                     |\n\n\n\n## 参考资料\n\n1. [Hive Getting Started](https://cwiki.apache.org/confluence/display/Hive/GettingStarted)\n2. [Hive SQL 的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html)\n3. [LanguageManual DDL](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL)\n4. [LanguageManual Types](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Types)\n5. [Managed vs. External Tables](https://cwiki.apache.org/confluence/display/Hive/Managed+vs.+External+Tables)\n"
  },
  {
    "path": "大数据框架学习/Hive视图和索引.md",
    "content": "# Hive 视图和索引\n\n<nav>\n<a href=\"#一视图\">一、视图</a><br/>\n<a href=\"#二索引\">二、索引</a><br/>\n<a href=\"#三索引案例\">三、索引案例</a><br/>\n<a href=\"#四索引的缺陷\">四、索引的缺陷</a><br/>\n</nav>\n\n## 一、视图\n\n### 1.1 简介\n\nHive 中的视图和 RDBMS 中视图的概念一致，都是一组数据的逻辑表示，本质上就是一条 SELECT 语句的结果集。视图是纯粹的逻辑对象，没有关联的存储 (Hive 3.0.0 引入的物化视图除外)，当查询引用视图时，Hive 可以将视图的定义与查询结合起来，例如将查询中的过滤器推送到视图中。\n\n### 1.2 创建视图\n\n```sql\nCREATE VIEW [IF NOT EXISTS] [db_name.]view_name   -- 视图名称\n  [(column_name [COMMENT column_comment], ...) ]    --列名\n  [COMMENT view_comment]  --视图注释\n  [TBLPROPERTIES (property_name = property_value, ...)]  --额外信息\n  AS SELECT ...;\n```\n\n在 Hive 中可以使用 `CREATE VIEW` 创建视图，如果已存在具有相同名称的表或视图，则会抛出异常，建议使用 `IF NOT EXISTS` 预做判断。在使用视图时候需要注意以下事项：\n\n- 视图是只读的，不能用作 LOAD / INSERT / ALTER 的目标；\n\n- 在创建视图时候视图就已经固定，对基表的后续更改（如添加列）将不会反映在视图；\n\n- 删除基表并不会删除视图，需要手动删除视图；\n\n- 视图可能包含 ORDER BY 和 LIMIT 子句。如果引用视图的查询语句也包含这类子句，其执行优先级低于视图对应字句。例如，视图 `custom_view` 指定 LIMIT 5，查询语句为 `select * from custom_view  LIMIT 10`，此时结果最多返回 5 行。\n\n- 创建视图时，如果未提供列名，则将从 SELECT 语句中自动派生列名；\n\n- 创建视图时，如果 SELECT 语句中包含其他表达式，例如 x + y，则列名称将以\\_C0，\\_C1 等形式生成；\n\n  ```sql\n  CREATE VIEW  IF NOT EXISTS custom_view AS SELECT empno, empno+deptno , 1+2 FROM emp;\n  ```\n\n  <div align=\"center\"> <img  src=\"../pictures/hive-1-2-view.png\"/> </div>\n\n\n\n### 1.3 查看视图\n\n```sql\n-- 查看所有视图： 没有单独查看视图列表的语句，只能使用 show tables\nshow tables;\n-- 查看某个视图\ndesc view_name;\n-- 查看某个视图详细信息\ndesc formatted view_name;\n```\n\n\n\n### 1.4 删除视图\n\n```sql\nDROP VIEW [IF EXISTS] [db_name.]view_name;\n```\n\n删除视图时，如果被删除的视图被其他视图所引用，这时候程序不会发出警告，但是引用该视图其他视图已经失效，需要进行重建或者删除。\n\n\n\n### 1.5 修改视图\n\n```sql\nALTER VIEW [db_name.]view_name AS select_statement;\n```\n\n 被更改的视图必须存在，且视图不能具有分区，如果视图具有分区，则修改失败。  \n\n\n\n### 1.6 修改视图属性\n\n语法：\n\n```sql\nALTER VIEW [db_name.]view_name SET TBLPROPERTIES table_properties;\n \ntable_properties:\n  : (property_name = property_value, property_name = property_value, ...)\n```\n\n示例：\n\n```sql\nALTER VIEW custom_view SET TBLPROPERTIES ('create'='heibaiying','date'='2019-05-05');\n```\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/hive-view-properties.png\"/> </div>\n\n\n\n\n\n## 二、索引\n\n### 2.1 简介\n\nHive 在 0.7.0 引入了索引的功能，索引的设计目标是提高表某些列的查询速度。如果没有索引，带有谓词的查询（如'WHERE table1.column = 10'）会加载整个表或分区并处理所有行。但是如果 column 存在索引，则只需要加载和处理文件的一部分。\n\n### 2.2 索引原理\n\n在指定列上建立索引，会产生一张索引表（表结构如下），里面的字段包括：索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量。在查询涉及到索引字段时，首先到索引表查找索引列值对应的 HDFS 文件路径及偏移量，这样就避免了全表扫描。\n\n```properties\n+--------------+----------------+----------+--+\n|   col_name   |   data_type    | comment     |\n+--------------+----------------+----------+--+\n| empno        | int            |  建立索引的列  |   \n| _bucketname  | string         |  HDFS 文件路径  |\n| _offsets     | array<bigint>  |  偏移量       |\n+--------------+----------------+----------+--+\n```\n\n### 2.3 创建索引\n\n```sql\nCREATE INDEX index_name     --索引名称\n  ON TABLE base_table_name (col_name, ...)  --建立索引的列\n  AS index_type    --索引类型\n  [WITH DEFERRED REBUILD]    --重建索引\n  [IDXPROPERTIES (property_name=property_value, ...)]  --索引额外属性\n  [IN TABLE index_table_name]    --索引表的名字\n  [\n     [ ROW FORMAT ...] STORED AS ...  \n     | STORED BY ...\n  ]   --索引表行分隔符 、 存储格式\n  [LOCATION hdfs_path]  --索引表存储位置\n  [TBLPROPERTIES (...)]   --索引表表属性\n  [COMMENT \"index comment\"];  --索引注释\n```\n\n### 2.4 查看索引\n\n```sql\n--显示表上所有列的索引\nSHOW FORMATTED INDEX ON table_name;\n```\n\n### 2.4 删除索引\n\n删除索引会删除对应的索引表。\n\n```sql\nDROP INDEX [IF EXISTS] index_name ON table_name;\n```\n\n如果存在索引的表被删除了，其对应的索引和索引表都会被删除。如果被索引表的某个分区被删除了，那么分区对应的分区索引也会被删除。\n\n### 2.5 重建索引\n\n```sql\nALTER INDEX index_name ON table_name [PARTITION partition_spec] REBUILD;\n```\n\n重建索引。如果指定了 PARTITION，则仅重建该分区的索引。\n\n\n\n## 三、索引案例\n\n### 3.1 创建索引\n\n在 emp 表上针对 `empno` 字段创建名为 `emp_index`,索引数据存储在 `emp_index_table` 索引表中\n\n```sql\ncreate index emp_index on table emp(empno) as  \n'org.apache.hadoop.hive.ql.index.compact.CompactIndexHandler' \nwith deferred rebuild \nin table emp_index_table ;\n```\n\n此时索引表中是没有数据的，需要重建索引才会有索引的数据。\n\n### 3.2 重建索引\n\n```sql\nalter index emp_index on emp rebuild; \n```\n\nHive 会启动 MapReduce 作业去建立索引，建立好后查看索引表数据如下。三个表字段分别代表：索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量。\n\n<div align=\"center\"> <img width=\"700px\" src=\"../pictures/hive-index-table.png\"/> </div>\n\n### 3.3 自动使用索引\n\n默认情况下，虽然建立了索引，但是 Hive 在查询时候是不会自动去使用索引的，需要开启相关配置。开启配置后，涉及到索引列的查询就会使用索引功能去优化查询。\n\n```sql\nSET hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat;\nSET hive.optimize.index.filter=true;\nSET hive.optimize.index.filter.compact.minsize=0;\n```\n\n### 3.4 查看索引\n\n```sql\nSHOW INDEX ON emp;\n```\n\n<div align=\"center\"> <img  src=\"../pictures/hive-index-show.png\"/> </div>\n\n\n\n\n\n## 四、索引的缺陷\n\n索引表最主要的一个缺陷在于：索引表无法自动 rebuild，这也就意味着如果表中有数据新增或删除，则必须手动 rebuild，重新执行 MapReduce 作业，生成索引表数据。\n\n同时按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Indexing) 的说明，Hive 会从 3.0 开始移除索引功能，主要基于以下两个原因：\n\n- 具有自动重写的物化视图 (Materialized View) 可以产生与索引相似的效果（Hive 2.3.0 增加了对物化视图的支持，在 3.0 之后正式引入）。\n- 使用列式存储文件格式（Parquet，ORC）进行存储时，这些格式支持选择性扫描，可以跳过不需要的文件或块。\n\n> ORC 内置的索引功能可以参阅这篇文章：[Hive 性能优化之 ORC 索引–Row Group Index vs Bloom Filter Index](http://lxw1234.com/archives/2016/04/632.htm)\n\n\n\n\n\n## 参考资料\n\n1. [Create/Drop/Alter View](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-Create/Drop/AlterView)\n2. [Materialized views](https://cwiki.apache.org/confluence/display/Hive/Materialized+views)\n3. [Hive 索引](http://lxw1234.com/archives/2015/05/207.htm)\n4. [Overview of Hive Indexes](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Indexing)\n"
  },
  {
    "path": "大数据框架学习/Kafka消费者详解.md",
    "content": "# Kafka消费者详解\n\n<nav>\n<a href=\"#一消费者和消费者群组\">一、消费者和消费者群组</a><br/>\n<a href=\"#二分区再均衡\">二、分区再均衡</a><br/>\n<a href=\"#三创建Kafka消费者\">三、创建Kafka消费者</a><br/>\n<a href=\"#三-自动提交偏移量\">三、 自动提交偏移量</a><br/>\n<a href=\"#四手动提交偏移量\">四、手动提交偏移量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-同步提交\">4.1 同步提交</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-异步提交\">4.2 异步提交</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43--同步加异步提交\">4.3  同步加异步提交</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44-提交特定偏移量\">4.4 提交特定偏移量</a><br/>\n<a href=\"#五监听分区再均衡\">五、监听分区再均衡</a><br/>\n<a href=\"#六退出轮询\">六、退出轮询</a><br/>\n<a href=\"#七独立的消费者\">七、独立的消费者</a><br/>\n<a href=\"#附录--Kafka消费者可选属性\">附录 : Kafka消费者可选属性</a><br/>\n</nav>\n\n\n## 一、消费者和消费者群组\n\n在 Kafka 中，消费者通常是消费者群组的一部分，多个消费者群组共同读取同一个主题时，彼此之间互不影响。Kafka 之所以要引入消费者群组这个概念是因为 Kafka 消费者经常会做一些高延迟的操作，比如把数据写到数据库或 HDFS ，或者进行耗时的计算，在这些情况下，单个消费者无法跟上数据生成的速度。此时可以增加更多的消费者，让它们分担负载，分别处理部分分区的消息，这就是 Kafka 实现横向伸缩的主要手段。 \n\n<div align=\"center\"> <img  src=\"../pictures/kafka-consumer01.png\"/> </div>\n\n需要注意的是：同一个分区只能被同一个消费者群组里面的一个消费者读取，不可能存在同一个分区被同一个消费者群里多个消费者共同读取的情况，如图：\n\n<div align=\"center\"> <img  src=\"../pictures/kafka-consumer02.png\"/> </div>\n\n可以看到即便消费者 Consumer5 空闲了，但是也不会去读取任何一个分区的数据，这同时也提醒我们在使用时应该合理设置消费者的数量，以免造成闲置和额外开销。\n\n## 二、分区再均衡\n\n因为群组里的消费者共同读取主题的分区，所以当一个消费者被关闭或发生崩溃时，它就离开了群组，原本由它读取的分区将由群组里的其他消费者来读取。同时在主题发生变化时 ， 比如添加了新的分区，也会发生分区与消费者的重新分配，分区的所有权从一个消费者转移到另一个消费者，这样的行为被称为再均衡。正是因为再均衡，所以消费费者群组才能保证高可用性和伸缩性。\n\n消费者通过向群组协调器所在的 broker 发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。只要消费者以正常的时间间隔发送心跳，就被认为是活跃的，说明它还在读取分区里的消息。消费者会在轮询消息或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长，会话就会过期，群组协调器认为它已经死亡，就会触发再均衡。\n\n\n## 三、创建Kafka消费者\n\n在创建消费者的时候以下以下三个选项是必选的：\n\n- **bootstrap.servers** ：指定 broker 的地址清单，清单里不需要包含所有的 broker 地址，生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错；\n- **key.deserializer** ：指定键的反序列化器；\n- **value.deserializer** ：指定值的反序列化器。\n\n除此之外你还需要指明你需要想订阅的主题，可以使用如下两个 API :\n\n+  **consumer.subscribe(Collection\\<String> topics)**  ：指明需要订阅的主题的集合；\n+ **consumer.subscribe(Pattern pattern)**  ：使用正则来匹配需要订阅的集合。\n\n最后只需要通过轮询 API(`poll`) 向服务器定时请求数据。一旦消费者订阅了主题，轮询就会处理所有的细节，包括群组协调、分区再均衡、发送心跳和获取数据，这使得开发者只需要关注从分区返回的数据，然后进行业务处理。 示例如下：\n\n```scala\nString topic = \"Hello-Kafka\";\nString group = \"group1\";\nProperties props = new Properties();\nprops.put(\"bootstrap.servers\", \"hadoop001:9092\");\n/*指定分组 ID*/\nprops.put(\"group.id\", group);\nprops.put(\"key.deserializer\", \"org.apache.kafka.common.serialization.StringDeserializer\");\nprops.put(\"value.deserializer\", \"org.apache.kafka.common.serialization.StringDeserializer\");\nKafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);\n\n/*订阅主题 (s)*/\nconsumer.subscribe(Collections.singletonList(topic));\n\ntry {\n    while (true) {\n        /*轮询获取数据*/\n        ConsumerRecords<String, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n        for (ConsumerRecord<String, String> record : records) {\n            System.out.printf(\"topic = %s,partition = %d, key = %s, value = %s, offset = %d,\\n\",\n           record.topic(), record.partition(), record.key(), record.value(), record.offset());\n        }\n    }\n} finally {\n    consumer.close();\n}\n```\n\n> 本篇文章的所有示例代码可以从 Github 上进行下载：[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis)\n\n## 三、 自动提交偏移量\n\n### 3.1 偏移量的重要性\n\nKafka 的每一条消息都有一个偏移量属性，记录了其在分区中的位置，偏移量是一个单调递增的整数。消费者通过往一个叫作 `＿consumer_offset` 的特殊主题发送消息，消息里包含每个分区的偏移量。 如果消费者一直处于运行状态，那么偏移量就没有\n什么用处。不过，如果有消费者退出或者新分区加入，此时就会触发再均衡。完成再均衡之后，每个消费者可能分配到新的分区，而不是之前处理的那个。为了能够继续之前的工作，消费者需要读取每个分区最后一次提交的偏移量，然后从偏移量指定的地方继续处理。 因为这个原因，所以如果不能正确提交偏移量，就可能会导致数据丢失或者重复出现消费，比如下面情况：\n\n+ 如果提交的偏移量小于客户端处理的最后一个消息的偏移量 ，那么处于两个偏移量之间的消息就会被重复消费；\n+ 如果提交的偏移量大于客户端处理的最后一个消息的偏移量，那么处于两个偏移量之间的消息将会丢失。\n\n### 3.2 自动提交偏移量\n\nKafka 支持自动提交和手动提交偏移量两种方式。这里先介绍比较简单的自动提交：\n\n只需要将消费者的 `enable.auto.commit` 属性配置为 `true` 即可完成自动提交的配置。 此时每隔固定的时间，消费者就会把 `poll()` 方法接收到的最大偏移量进行提交，提交间隔由 `auto.commit.interval.ms` 属性进行配置，默认值是 5s。\n\n使用自动提交是存在隐患的，假设我们使用默认的 5s 提交时间间隔，在最近一次提交之后的 3s 发生了再均衡，再均衡之后，消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ，所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量，减小可能出现重复消息的时间窗，不过这种情况是无法完全避免的。基于这个原因，Kafka 也提供了手动提交偏移量的 API，使得用户可以更为灵活的提交偏移量。\n\n\n\n## 四、手动提交偏移量\n\n用户可以通过将 `enable.auto.commit` 设为 `false`，然后手动提交偏移量。基于用户需求手动提交偏移量可以分为两大类：\n\n+ 手动提交当前偏移量：即手动提交当前轮询的最大偏移量；\n+ 手动提交固定偏移量：即按照业务需求，提交某一个固定的偏移量。\n\n而按照 Kafka API，手动提交偏移量又可以分为同步提交和异步提交。\n\n### 4.1 同步提交\n\n通过调用 `consumer.commitSync()` 来进行同步提交，不传递任何参数时提交的是当前轮询的最大偏移量。\n\n```java\nwhile (true) {\n    ConsumerRecords<String, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n    for (ConsumerRecord<String, String> record : records) {\n        System.out.println(record);\n    }\n    /*同步提交*/\n    consumer.commitSync();\n}\n```\n\n如果某个提交失败，同步提交还会进行重试，这可以保证数据能够最大限度提交成功，但是同时也会降低程序的吞吐量。基于这个原因，Kafka 还提供了异步提交的 API。\n\n### 4.2 异步提交\n\n异步提交可以提高程序的吞吐量，因为此时你可以尽管请求数据，而不用等待 Broker 的响应。代码如下：\n\n```java\nwhile (true) {\n    ConsumerRecords<String, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n    for (ConsumerRecord<String, String> record : records) {\n        System.out.println(record);\n    }\n    /*异步提交并定义回调*/\n    consumer.commitAsync(new OffsetCommitCallback() {\n        @Override\n        public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {\n          if (exception != null) {\n             System.out.println(\"错误处理\");\n             offsets.forEach((x, y) -> System.out.printf(\"topic = %s,partition = %d, offset = %s \\n\",\n                                                            x.topic(), x.partition(), y.offset()));\n            }\n        }\n    });\n}\n```\n\n异步提交存在的问题是，在提交失败的时候不会进行自动重试，实际上也不能进行自动重试。假设程序同时提交了 200 和 300 的偏移量，此时 200 的偏移量失败的，但是紧随其后的 300 的偏移量成功了，此时如果重试就会存在 200 覆盖 300 偏移量的可能。同步提交就不存在这个问题，因为在同步提交的情况下，300 的提交请求必须等待服务器返回 200 提交请求的成功反馈后才会发出。基于这个原因，某些情况下，需要同时组合同步和异步两种提交方式。\n\n> 注：虽然程序不能在失败时候进行自动重试，但是我们是可以手动进行重试的，你可以通过一个 Map<TopicPartition, Integer> offsets 来维护你提交的每个分区的偏移量，然后当失败时候，你可以判断失败的偏移量是否小于你维护的同主题同分区的最后提交的偏移量，如果小于则代表你已经提交了更大的偏移量请求，此时不需要重试，否则就可以进行手动重试。\n\n### 4.3  同步加异步提交\n\n下面这种情况，在正常的轮询中使用异步提交来保证吞吐量，但是因为在最后即将要关闭消费者了，所以此时需要用同步提交来保证最大限度的提交成功。\n\n```scala\ntry {\n    while (true) {\n        ConsumerRecords<String, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n        for (ConsumerRecord<String, String> record : records) {\n            System.out.println(record);\n        }\n        // 异步提交\n        consumer.commitAsync();\n    }\n} catch (Exception e) {\n    e.printStackTrace();\n} finally {\n    try {\n        // 因为即将要关闭消费者，所以要用同步提交保证提交成功\n        consumer.commitSync();\n    } finally {\n        consumer.close();\n    }\n}\n```\n\n### 4.4 提交特定偏移量\n\n在上面同步和异步提交的 API 中，实际上我们都没有对 commit 方法传递参数，此时默认提交的是当前轮询的最大偏移量，如果你需要提交特定的偏移量，可以调用它们的重载方法。\n\n```java\n/*同步提交特定偏移量*/\ncommitSync(Map<TopicPartition, OffsetAndMetadata> offsets) \n/*异步提交特定偏移量*/    \ncommitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)\n```\n\n需要注意的是，因为你可以订阅多个主题，所以 `offsets` 中必须要包含所有主题的每个分区的偏移量，示例代码如下：\n\n```java\ntry {\n    while (true) {\n        ConsumerRecords<String, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n        for (ConsumerRecord<String, String> record : records) {\n            System.out.println(record);\n            /*记录每个主题的每个分区的偏移量*/\n            TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition());\n            OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset()+1, \"no metaData\");\n            /*TopicPartition 重写过 hashCode 和 equals 方法，所以能够保证同一主题和分区的实例不会被重复添加*/\n            offsets.put(topicPartition, offsetAndMetadata);\n        }\n        /*提交特定偏移量*/\n        consumer.commitAsync(offsets, null);\n    }\n} finally {\n    consumer.close();\n}\n```\n\n\n\n## 五、监听分区再均衡\n\n因为分区再均衡会导致分区与消费者的重新划分，有时候你可能希望在再均衡前执行一些操作：比如提交已经处理但是尚未提交的偏移量，关闭数据库连接等。此时可以在订阅主题时候，调用 `subscribe` 的重载方法传入自定义的分区再均衡监听器。\n\n```java\n /*订阅指定集合内的所有主题*/\nsubscribe(Collection<String> topics, ConsumerRebalanceListener listener)\n /*使用正则匹配需要订阅的主题*/    \nsubscribe(Pattern pattern, ConsumerRebalanceListener listener)    \n```\n\n代码示例如下：\n\n```java\nMap<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();\n\nconsumer.subscribe(Collections.singletonList(topic), new ConsumerRebalanceListener() {\n    /*该方法会在消费者停止读取消息之后，再均衡开始之前就调用*/\n    @Override\n    public void onPartitionsRevoked(Collection<TopicPartition> partitions) {\n        System.out.println(\"再均衡即将触发\");\n        // 提交已经处理的偏移量\n        consumer.commitSync(offsets);\n    }\n\n    /*该方法会在重新分配分区之后，消费者开始读取消息之前被调用*/\n    @Override\n    public void onPartitionsAssigned(Collection<TopicPartition> partitions) {\n\n    }\n});\n\ntry {\n    while (true) {\n        ConsumerRecords<String, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n        for (ConsumerRecord<String, String> record : records) {\n            System.out.println(record);\n            TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition());\n            OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset() + 1, \"no metaData\");\n            /*TopicPartition 重写过 hashCode 和 equals 方法，所以能够保证同一主题和分区的实例不会被重复添加*/\n            offsets.put(topicPartition, offsetAndMetadata);\n        }\n        consumer.commitAsync(offsets, null);\n    }\n} finally {\n    consumer.close();\n}\n```\n\n\n\n## 六 、退出轮询\n\nKafka 提供了 `consumer.wakeup()` 方法用于退出轮询，它通过抛出 `WakeupException` 异常来跳出循环。需要注意的是，在退出线程时最好显示的调用 `consumer.close()` , 此时消费者会提交任何还没有提交的东西，并向群组协调器发送消息，告知自己要离开群组，接下来就会触发再均衡 ，而不需要等待会话超时。 \n\n下面的示例代码为监听控制台输出，当输入 `exit` 时结束轮询，关闭消费者并退出程序：\n\n```java\n/*调用 wakeup 优雅的退出*/\nfinal Thread mainThread = Thread.currentThread();\nnew Thread(() -> {\n    Scanner sc = new Scanner(System.in);\n    while (sc.hasNext()) {\n        if (\"exit\".equals(sc.next())) {\n            consumer.wakeup();\n            try {\n                /*等待主线程完成提交偏移量、关闭消费者等操作*/\n                mainThread.join();\n                break;\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }\n    }\n}).start();\n\ntry {\n    while (true) {\n        ConsumerRecords<String, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n        for (ConsumerRecord<String, String> rd : records) {\n            System.out.printf(\"topic = %s,partition = %d, key = %s, value = %s, offset = %d,\\n\",\n                              rd.topic(), rd.partition(), rd.key(), rd.value(), rd.offset());\n        }\n    }\n} catch (WakeupException e) {\n    //对于 wakeup() 调用引起的 WakeupException 异常可以不必处理\n} finally {\n    consumer.close();\n    System.out.println(\"consumer 关闭\");\n}\n```\n\n\n\n## 七、独立的消费者\n\n因为 Kafka 的设计目标是高吞吐和低延迟，所以在 Kafka 中，消费者通常都是从属于某个群组的，这是因为单个消费者的处理能力是有限的。但是某些时候你的需求可能很简单，比如可能只需要一个消费者从一个主题的所有分区或者某个特定的分区读取数据，这个时候就不需要消费者群组和再均衡了， 只需要把主题或者分区分配给消费者，然后开始读取消息井提交偏移量即可。\n\n在这种情况下，就不需要订阅主题， 取而代之的是消费者为自己分配分区。 一个消费者可以订阅主题（井加入消费者群组），或者为自己分配分区，但不能同时做这两件事情。 分配分区的示例代码如下：\n\n```java\nList<TopicPartition> partitions = new ArrayList<>();\nList<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);\n\n/*可以指定读取哪些分区 如这里假设只读取主题的 0 分区*/\nfor (PartitionInfo partition : partitionInfos) {\n    if (partition.partition()==0){\n        partitions.add(new TopicPartition(partition.topic(), partition.partition()));\n    }\n}\n\n// 为消费者指定分区\nconsumer.assign(partitions);\n\n\nwhile (true) {\n    ConsumerRecords<Integer, String> records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS));\n    for (ConsumerRecord<Integer, String> record : records) {\n        System.out.printf(\"partition = %s, key = %d, value = %s\\n\",\n                          record.partition(), record.key(), record.value());\n    }\n    consumer.commitSync();\n}\n```\n\n\n\n## 附录 : Kafka消费者可选属性\n\n### 1. fetch.min.byte\n\n消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值，broker 会等待有足够的可用数据时才会把它返回给消费者。\n\n### 2. fetch.max.wait.ms\n\nbroker 返回给消费者数据的等待时间，默认是 500ms。\n\n### 3. max.partition.fetch.bytes\n\n该属性指定了服务器从每个分区返回给消费者的最大字节数，默认为 1MB。\n\n### 4. session.timeout.ms\n\n消费者在被认为死亡之前可以与服务器断开连接的时间，默认是 3s。\n\n### 5. auto.offset.reset\n\n该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理：\n\n- latest (默认值) ：在偏移量无效的情况下，消费者将从最新的记录开始读取数据（在消费者启动之后生成的最新记录）;\n- earliest ：在偏移量无效的情况下，消费者将从起始位置读取分区的记录。\n\n### 6. enable.auto.commit\n\n是否自动提交偏移量，默认值是 true。为了避免出现重复消费和数据丢失，可以把它设置为 false。\n\n### 7. client.id\n\n客户端 id，服务器用来识别消息的来源。\n\n### 8. max.poll.records\n\n单次调用 `poll()` 方法能够返回的记录数量。\n\n### 9. receive.buffer.bytes & send.buffer.byte\n\n这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小，-1 代表使用操作系统的默认值。\n\n\n\n## 参考资料\n\n1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26\n\n"
  },
  {
    "path": "大数据框架学习/Kafka深入理解分区副本机制.md",
    "content": "# 深入理解Kafka副本机制\n\n<nav>\n<a href=\"#一Kafka集群\">一、Kafka集群</a><br/>\n<a href=\"#二副本机制\">二、副本机制</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-分区和副本\">2.1 分区和副本</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-ISR机制\">2.2 ISR机制</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-不完全的首领选举\">2.3 不完全的首领选举</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-最少同步副本\">2.4 最少同步副本</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-发送确认\">2.5 发送确认</a><br/>\n<a href=\"#三数据请求\">三、数据请求</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-元数据请求机制\">3.1 元数据请求机制</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-数据可见性\">3.2 数据可见性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-零拷贝\">3.3 零拷贝</a><br/>\n<a href=\"#四物理存储\">四、物理存储</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-分区分配\">4.1 分区分配</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-分区数据保留规则\">4.2 分区数据保留规则</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-文件格式\">4.3 文件格式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#\"></a><br/>\n</nav>\n\n\n## 一、Kafka集群\n\nKafka 使用 Zookeeper 来维护集群成员 (brokers) 的信息。每个 broker 都有一个唯一标识 `broker.id`，用于标识自己在集群中的身份，可以在配置文件 `server.properties` 中进行配置，或者由程序自动生成。下面是 Kafka brokers 集群自动创建的过程：\n\n+ 每一个 broker 启动的时候，它会在 Zookeeper 的 `/brokers/ids` 路径下创建一个 ` 临时节点 `，并将自己的 `broker.id` 写入，从而将自身注册到集群；\n+ 当有多个 broker 时，所有 broker 会竞争性地在 Zookeeper 上创建 `/controller` 节点，由于 Zookeeper 上的节点不会重复，所以必然只会有一个 broker 创建成功，此时该 broker 称为 controller broker。它除了具备其他 broker 的功能外，**还负责管理主题分区及其副本的状态**。\n+ 当 broker 出现宕机或者主动退出从而导致其持有的 Zookeeper 会话超时时，会触发注册在 Zookeeper 上的 watcher 事件，此时 Kafka 会进行相应的容错处理；如果宕机的是 controller broker 时，还会触发新的 controller 选举。\n\n## 二、副本机制\n\n为了保证高可用，kafka 的分区是多副本的，如果一个副本丢失了，那么还可以从其他副本中获取分区数据。但是这要求对应副本的数据必须是完整的，这是 Kafka 数据一致性的基础，所以才需要使用 `controller broker` 来进行专门的管理。下面将详解介绍 Kafka 的副本机制。\n\n### 2.1 分区和副本\n\nKafka 的主题被分为多个分区 ，分区是 Kafka 最基本的存储单位。每个分区可以有多个副本 (可以在创建主题时使用 ` replication-factor` 参数进行指定)。其中一个副本是首领副本 (Leader replica)，所有的事件都直接发送给首领副本；其他副本是跟随者副本 (Follower replica)，需要通过复制来保持与首领副本数据一致，当首领副本不可用时，其中一个跟随者副本将成为新首领。 \n\n<div align=\"center\"> <img src=\"../pictures/kafka-cluster.png\"/> </div>\n\n### 2.2 ISR机制\n\n每个分区都有一个 ISR(in-sync Replica) 列表，用于维护所有同步的、可用的副本。首领副本必然是同步副本，而对于跟随者副本来说，它需要满足以下条件才能被认为是同步副本：\n\n+ 与 Zookeeper 之间有一个活跃的会话，即必须定时向 Zookeeper 发送心跳；\n+  在规定的时间内从首领副本那里低延迟地获取过消息。\n\n如果副本不满足上面条件的话，就会被从 ISR 列表中移除，直到满足条件才会被再次加入。\n\n这里给出一个主题创建的示例：使用 `--replication-factor` 指定副本系数为 3，创建成功后使用 `--describe ` 命令可以看到分区 0 的有 0,1,2 三个副本，且三个副本都在 ISR 列表中，其中 1 为首领副本。\n\n<div align=\"center\"> <img src=\"../pictures/kafka-分区副本.png\"/> </div>\n\n### 2.3 不完全的首领选举\n\n对于副本机制，在 broker 级别有一个可选的配置参数 `unclean.leader.election.enable`，默认值为 fasle，代表禁止不完全的首领选举。这是针对当首领副本挂掉且 ISR 中没有其他可用副本时，是否允许某个不完全同步的副本成为首领副本，这可能会导致数据丢失或者数据不一致，在某些对数据一致性要求较高的场景 (如金融领域)，这可能无法容忍的，所以其默认值为 false，如果你能够允许部分数据不一致的话，可以配置为 true。\n\n### 2.4 最少同步副本\n\nISR 机制的另外一个相关参数是 `min.insync.replicas` , 可以在 broker 或者主题级别进行配置，代表 ISR 列表中至少要有几个可用副本。这里假设设置为 2，那么当可用副本数量小于该值时，就认为整个分区处于不可用状态。此时客户端再向分区写入数据时候就会抛出异常 `org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。`\n\n### 2.5 发送确认\n\nKafka 在生产者上有一个可选的参数 ack，该参数指定了必须要有多少个分区副本收到消息，生产者才会认为消息写入成功：\n\n- **acks=0** ：消息发送出去就认为已经成功了，不会等待任何来自服务器的响应；\n- **acks=1** ： 只要集群的首领节点收到消息，生产者就会收到一个来自服务器成功响应；\n- **acks=all** ：只有当所有参与复制的节点全部收到消息时，生产者才会收到一个来自服务器的成功响应。\n\n## 三、数据请求\n\n### 3.1 元数据请求机制\n\n在所有副本中，只有领导副本才能进行消息的读写处理。由于不同分区的领导副本可能在不同的 broker 上，如果某个 broker 收到了一个分区请求，但是该分区的领导副本并不在该 broker 上，那么它就会向客户端返回一个 `Not a Leader for Partition` 的错误响应。 为了解决这个问题，Kafka 提供了元数据请求机制。\n\n首先集群中的每个 broker 都会缓存所有主题的分区副本信息，客户端会定期发送发送元数据请求，然后将获取的元数据进行缓存。定时刷新元数据的时间间隔可以通过为客户端配置 `metadata.max.age.ms` 来进行指定。有了元数据信息后，客户端就知道了领导副本所在的 broker，之后直接将读写请求发送给对应的 broker 即可。\n\n如果在定时请求的时间间隔内发生的分区副本的选举，则意味着原来缓存的信息可能已经过时了，此时还有可能会收到 `Not a Leader for Partition` 的错误响应，这种情况下客户端会再次求发出元数据请求，然后刷新本地缓存，之后再去正确的 broker 上执行对应的操作，过程如下图：\n\n<div align=\"center\"> <img src=\"../pictures/kafka-元数据请求.png\"/> </div>\n\n### 3.2 数据可见性\n\n需要注意的是，并不是所有保存在分区首领上的数据都可以被客户端读取到，为了保证数据一致性，只有被所有同步副本 (ISR 中所有副本) 都保存了的数据才能被客户端读取到。\n\n<div align=\"center\"> <img src=\"../pictures/kafka-数据可见性.png\"/> </div>\n\n### 3.3 零拷贝\n\nKafka 所有数据的写入和读取都是通过零拷贝来实现的。传统拷贝与零拷贝的区别如下：\n\n#### 传统模式下的四次拷贝与四次上下文切换\n\n以将磁盘文件通过网络发送为例。传统模式下，一般使用如下伪代码所示的方法先将文件数据读入内存，然后通过 Socket 将内存中的数据发送出去。\n\n```java\nbuffer = File.read\nSocket.send(buffer)\n```\n\n这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态 Buffer（DMA 拷贝），然后应用程序将内存态 Buffer 数据读入到用户态 Buffer（CPU 拷贝），接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer（CPU 拷贝），最后通过 DMA 拷贝将数据拷贝到 NIC Buffer。同时，还伴随着四次上下文切换，如下图所示：\n\n<div align=\"center\"> <img src=\"../pictures/kafka-BIO.png\"/> </div>\n\n#### sendfile和transferTo实现零拷贝\n\nLinux 2.4+ 内核通过 `sendfile` 系统调用，提供了零拷贝。数据通过 DMA 拷贝到内核态 Buffer 后，直接通过 DMA 拷贝到 NIC Buffer，无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外，因为整个读文件到网络发送由一个 `sendfile` 调用完成，整个过程只有两次上下文切换，因此大大提高了性能。零拷贝过程如下图所示：\n\n<div align=\"center\"> <img src=\"../pictures/kafka-零拷贝.png\"/> </div>\n\n从具体实现来看，Kafka 的数据传输通过 TransportLayer 来完成，其子类 `PlaintextTransportLayer` 的 `transferFrom` 方法通过调用 Java NIO 中 FileChannel 的 `transferTo` 方法实现零拷贝，如下所示：\n\n```java\n@Override\npublic long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {\n    return fileChannel.transferTo(position, count, socketChannel);\n}\n```\n\n**注：** `transferTo` 和 `transferFrom` 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关，如果操作系统提供 `sendfile` 这样的零拷贝系统调用，则这两个方法会通过这样的系统调用充分利用零拷贝的优势，否则并不能通过这两个方法本身实现零拷贝。\n\n## 四、物理存储\n\n### 4.1 分区分配\n\n在创建主题时，Kafka 会首先决定如何在 broker 间分配分区副本，它遵循以下原则：\n\n+ 在所有 broker 上均匀地分配分区副本；\n+ 确保分区的每个副本分布在不同的 broker 上；\n+ 如果使用了 `broker.rack` 参数为 broker 指定了机架信息，那么会尽可能的把每个分区的副本分配到不同机架的 broker 上，以避免一个机架不可用而导致整个分区不可用。\n\n基于以上原因，如果你在一个单节点上创建一个 3 副本的主题，通常会抛出下面的异常：\n\n```properties\nError while executing topic command : org.apache.kafka.common.errors.InvalidReplicationFactor   \nException: Replication factor: 3 larger than available brokers: 1.\n```\n\n### 4.2 分区数据保留规则\n\n保留数据是 Kafka 的一个基本特性， 但是 Kafka 不会一直保留数据，也不会等到所有消费者都读取了消息之后才删除消息。相反， Kafka 为每个主题配置了数据保留期限，规定数据被删除之前可以保留多长时间，或者清理数据之前可以保留的数据量大小。分别对应以下四个参数： \n\n- `log.retention.bytes` ：删除数据前允许的最大数据量；默认值-1，代表没有限制；\n- `log.retention.ms`：保存数据文件的毫秒数，如果未设置，则使用 `log.retention.minutes` 中的值，默认为 null；\n- `log.retention.minutes`：保留数据文件的分钟数，如果未设置，则使用 `log.retention.hours` 中的值，默认为 null；\n- `log.retention.hours`：保留数据文件的小时数，默认值为 168，也就是一周。\n\n因为在一个大文件里查找和删除消息是很费时的，也很容易出错，所以 Kafka 把分区分成若干个片段，当前正在写入数据的片段叫作活跃片段。活动片段永远不会被删除。如果按照默认值保留数据一周，而且每天使用一个新片段，那么你就会看到，在每天使用一个新片段的同时会删除一个最老的片段，所以大部分时间该分区会有 7 个片段存在。 \n\n### 4.3 文件格式\n\n通常保存在磁盘上的数据格式与生产者发送过来消息格式是一样的。 如果生产者发送的是压缩过的消息，那么同一个批次的消息会被压缩在一起，被当作“包装消息”进行发送 (格式如下所示) ，然后保存到磁盘上。之后消费者读取后再自己解压这个包装消息，获取每条消息的具体信息。\n\n<div align=\"center\"> <img src=\"../pictures/kafka-compress-message.png\"/> </div>\n\n\n\n## 参考资料\n\n1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26\n2. [Kafka 高性能架构之道](http://www.jasongj.com/kafka/high_throughput/)\n"
  },
  {
    "path": "大数据框架学习/Kafka生产者详解.md",
    "content": "# Kafka生产者详解\n\n<nav>\n<a href=\"#一生产者发送消息的过程\">一、生产者发送消息的过程</a><br/>\n<a href=\"#二创建生产者\">二、创建生产者</a><br/>\n<a href=\"#二发送消息\">二、发送消息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-同步发送\">2.1 同步发送</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-异步发送\">2.2 异步发送</a><br/>\n<a href=\"#三自定义分区器\">三、自定义分区器</a><br/>\n<a href=\"#四生产者其他属性\">四、生产者其他属性</a><br/>\n</nav>\n\n\n## 一、生产者发送消息的过程\n\n首先介绍一下 Kafka 生产者发送消息的过程：\n\n+ Kafka 会将发送消息包装为 ProducerRecord 对象， ProducerRecord 对象包含了目标主题和要发送的内容，同时还可以指定键和分区。在发送 ProducerRecord 对象前，生产者会先把键和值对象序列化成字节数组，这样它们才能够在网络上传输。\n+ 接下来，数据被传给分区器。如果之前已经在 ProducerRecord 对象里指定了分区，那么分区器就不会再做任何事情。如果没有指定分区 ，那么分区器会根据 ProducerRecord 对象的键来选择一个分区，紧接着，这条记录被添加到一个记录批次里，这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的 broker 上。\n+ 服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka，就返回一个 RecordMetaData 对象，它包含了主题和分区信息，以及记录在分区里的偏移量。如果写入失败，则会返回一个错误。生产者在收到错误之后会尝试重新发送消息，如果达到指定的重试次数后还没有成功，则直接抛出异常，不再重试。\n\n<div align=\"center\"> <img src=\"../pictures/kafka-send-messgaes.png\"/> </div>\n\n## 二、创建生产者\n\n### 2.1 项目依赖\n\n本项目采用 Maven 构建，想要调用 Kafka 生产者 API，需要导入 `kafka-clients` 依赖，如下：\n\n```xml\n<dependency>\n    <groupId>org.apache.kafka</groupId>\n    <artifactId>kafka-clients</artifactId>\n    <version>2.2.0</version>\n</dependency>\n```\n\n### 2.2 创建生产者\n\n创建 Kafka 生产者时，以下三个属性是必须指定的：\n\n+ **bootstrap.servers** ：指定 broker 的地址清单，清单里不需要包含所有的 broker 地址，生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错；\n+ **key.serializer** ：指定键的序列化器；\n+ **value.serializer** ：指定值的序列化器。\n\n创建的示例代码如下：\n\n```scala\npublic class SimpleProducer {\n\n    public static void main(String[] args) {\n\n        String topicName = \"Hello-Kafka\";\n\n        Properties props = new Properties();\n        props.put(\"bootstrap.servers\", \"hadoop001:9092\");\n        props.put(\"key.serializer\", \"org.apache.kafka.common.serialization.StringSerializer\");\n        props.put(\"value.serializer\", \"org.apache.kafka.common.serialization.StringSerializer\");\n        /*创建生产者*/\n        Producer<String, String> producer = new KafkaProducer<>(props);\n\n        for (int i = 0; i < 10; i++) {\n            ProducerRecord<String, String> record = new ProducerRecord<>(topicName, \"hello\" + i, \n                                                                         \"world\" + i);\n            /* 发送消息*/\n            producer.send(record);\n        }\n        /*关闭生产者*/\n        producer.close();\n    }\n}\n```\n\n> 本篇文章的所有示例代码可以从 Github 上进行下载：[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis)\n\n### 2.3 测试\n\n#### 1. 启动Kakfa\n\nKafka 的运行依赖于 zookeeper，需要预先启动，可以启动 Kafka 内置的 zookeeper，也可以启动自己安装的：\n\n```shell\n# zookeeper启动命令\nbin/zkServer.sh start\n\n# 内置zookeeper启动命令\nbin/zookeeper-server-start.sh config/zookeeper.properties\n```\n\n启动单节点 kafka 用于测试：\n\n```shell\n# bin/kafka-server-start.sh config/server.properties\n```\n\n#### 2. 创建topic\n\n```shell\n# 创建用于测试主题\nbin/kafka-topics.sh --create \\\n                    --bootstrap-server hadoop001:9092 \\\n                     --replication-factor 1 --partitions 1 \\\n                     --topic Hello-Kafka\n\n# 查看所有主题\n bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092\n```\n\n#### 3. 启动消费者\n\n 启动一个控制台消费者用于观察写入情况，启动命令如下：\n\n```shell\n# bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic Hello-Kafka --from-beginning\n```\n\n#### 4. 运行项目\n\n此时可以看到消费者控制台，输出如下，这里 `kafka-console-consumer` 只会打印出值信息，不会打印出键信息。\n\n<div align=\"center\"> <img src=\"../pictures/kafka-simple-producer.png\"/> </div>\n\n\n\n### 2.4 可能出现的问题\n\n在这里可能出现的一个问题是：生产者程序在启动后，一直处于等待状态。这通常出现在你使用默认配置启动 Kafka 的情况下，此时需要对 `server.properties` 文件中的 `listeners` 配置进行更改：\n\n```shell\n# hadoop001 为我启动kafka服务的主机名，你可以换成自己的主机名或者ip地址\nlisteners=PLAINTEXT://hadoop001:9092\n```\n\n\n\n## 二、发送消息\n\n上面的示例程序调用了 `send` 方法发送消息后没有做任何操作，在这种情况下，我们没有办法知道消息发送的结果。想要知道消息发送的结果，可以使用同步发送或者异步发送来实现。\n\n### 2.1 同步发送\n\n在调用 `send` 方法后可以接着调用 `get()` 方法，`send` 方法的返回值是一个 Future\\<RecordMetadata>对象，RecordMetadata 里面包含了发送消息的主题、分区、偏移量等信息。改写后的代码如下：\n\n```scala\nfor (int i = 0; i < 10; i++) {\n    try {\n        ProducerRecord<String, String> record = new ProducerRecord<>(topicName, \"k\" + i, \"world\" + i);\n        /*同步发送消息*/\n        RecordMetadata metadata = producer.send(record).get();\n        System.out.printf(\"topic=%s, partition=%d, offset=%s \\n\",\n                metadata.topic(), metadata.partition(), metadata.offset());\n    } catch (InterruptedException | ExecutionException e) {\n        e.printStackTrace();\n    }\n}\n```\n\n此时得到的输出如下：偏移量和调用次数有关，所有记录都分配到了 0 分区，这是因为在创建 `Hello-Kafka` 主题时候，使用 `--partitions` 指定其分区数为 1，即只有一个分区。\n\n```shell\ntopic=Hello-Kafka, partition=0, offset=40 \ntopic=Hello-Kafka, partition=0, offset=41 \ntopic=Hello-Kafka, partition=0, offset=42 \ntopic=Hello-Kafka, partition=0, offset=43 \ntopic=Hello-Kafka, partition=0, offset=44 \ntopic=Hello-Kafka, partition=0, offset=45 \ntopic=Hello-Kafka, partition=0, offset=46 \ntopic=Hello-Kafka, partition=0, offset=47 \ntopic=Hello-Kafka, partition=0, offset=48 \ntopic=Hello-Kafka, partition=0, offset=49 \n```\n\n### 2.2 异步发送\n\n通常我们并不关心发送成功的情况，更多关注的是失败的情况，因此 Kafka 提供了异步发送和回调函数。 代码如下：\n\n```scala\nfor (int i = 0; i < 10; i++) {\n    ProducerRecord<String, String> record = new ProducerRecord<>(topicName, \"k\" + i, \"world\" + i);\n    /*异步发送消息，并监听回调*/\n    producer.send(record, new Callback() {\n        @Override\n        public void onCompletion(RecordMetadata metadata, Exception exception) {\n            if (exception != null) {\n                System.out.println(\"进行异常处理\");\n            } else {\n                System.out.printf(\"topic=%s, partition=%d, offset=%s \\n\",\n                        metadata.topic(), metadata.partition(), metadata.offset());\n            }\n        }\n    });\n}\n```\n\n\n\n## 三、自定义分区器\n\nKafka 有着默认的分区机制：\n\n+ 如果键值为 null， 则使用轮询 (Round Robin) 算法将消息均衡地分布到各个分区上；\n+ 如果键值不为 null，那么 Kafka 会使用内置的散列算法对键进行散列，然后分布到各个分区上。\n\n某些情况下，你可能有着自己的分区需求，这时候可以采用自定义分区器实现。这里给出一个自定义分区器的示例：\n\n### 3.1 自定义分区器\n\n```java\n/**\n * 自定义分区器\n */\npublic class CustomPartitioner implements Partitioner {\n\n    private int passLine;\n\n    @Override\n    public void configure(Map<String, ?> configs) {\n        /*从生产者配置中获取分数线*/\n        passLine = (Integer) configs.get(\"pass.line\");\n    }\n\n    @Override\n    public int partition(String topic, Object key, byte[] keyBytes, Object value, \n                         byte[] valueBytes, Cluster cluster) {\n        /*key 值为分数，当分数大于分数线时候，分配到 1 分区，否则分配到 0 分区*/\n        return (Integer) key >= passLine ? 1 : 0;\n    }\n\n    @Override\n    public void close() {\n        System.out.println(\"分区器关闭\");\n    }\n}\n```\n\n需要在创建生产者时指定分区器，和分区器所需要的配置参数：\n\n```java\npublic class ProducerWithPartitioner {\n\n    public static void main(String[] args) {\n\n        String topicName = \"Kafka-Partitioner-Test\";\n\n        Properties props = new Properties();\n        props.put(\"bootstrap.servers\", \"hadoop001:9092\");\n        props.put(\"key.serializer\", \"org.apache.kafka.common.serialization.IntegerSerializer\");\n        props.put(\"value.serializer\", \"org.apache.kafka.common.serialization.StringSerializer\");\n\n        /*传递自定义分区器*/\n        props.put(\"partitioner.class\", \"com.heibaiying.producers.partitioners.CustomPartitioner\");\n        /*传递分区器所需的参数*/\n        props.put(\"pass.line\", 6);\n\n        Producer<Integer, String> producer = new KafkaProducer<>(props);\n\n        for (int i = 0; i <= 10; i++) {\n            String score = \"score:\" + i;\n            ProducerRecord<Integer, String> record = new ProducerRecord<>(topicName, i, score);\n            /*异步发送消息*/\n            producer.send(record, (metadata, exception) ->\n                    System.out.printf(\"%s, partition=%d, \\n\", score, metadata.partition()));\n        }\n\n        producer.close();\n    }\n}\n```\n\n### 3.2 测试\n\n需要创建一个至少有两个分区的主题：\n\n```shell\n bin/kafka-topics.sh --create \\\n                    --bootstrap-server hadoop001:9092 \\\n                     --replication-factor 1 --partitions 2 \\\n                     --topic Kafka-Partitioner-Test\n```\n\n此时输入如下，可以看到分数大于等于 6 分的都被分到 1 分区，而小于 6 分的都被分到了 0 分区。\n\n```shell\nscore:6, partition=1, \nscore:7, partition=1, \nscore:8, partition=1, \nscore:9, partition=1, \nscore:10, partition=1, \nscore:0, partition=0, \nscore:1, partition=0, \nscore:2, partition=0, \nscore:3, partition=0, \nscore:4, partition=0, \nscore:5, partition=0, \n分区器关闭\n```\n\n\n\n## 四、生产者其他属性\n\n上面生产者的创建都仅指定了服务地址，键序列化器、值序列化器，实际上 Kafka 的生产者还有很多可配置属性，如下：\n\n### 1. acks\n\nacks 参数指定了必须要有多少个分区副本收到消息，生产者才会认为消息写入是成功的：\n\n- **acks=0** ： 消息发送出去就认为已经成功了，不会等待任何来自服务器的响应；\n- **acks=1** ： 只要集群的首领节点收到消息，生产者就会收到一个来自服务器成功响应；\n- **acks=all** ：只有当所有参与复制的节点全部收到消息时，生产者才会收到一个来自服务器的成功响应。\n\n### 2. buffer.memory\n\n设置生产者内存缓冲区的大小。\n\n### 3. compression.type\n\n默认情况下，发送的消息不会被压缩。如果想要进行压缩，可以配置此参数，可选值有 snappy，gzip，lz4。\n\n### 4. retries\n\n发生错误后，消息重发的次数。如果达到设定值，生产者就会放弃重试并返回错误。\n\n### 5. batch.size\n\n当有多个消息需要被发送到同一个分区时，生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小，按照字节数计算。\n\n### 6. linger.ms\n\n该参数制定了生产者在发送批次之前等待更多消息加入批次的时间。\n\n### 7. clent.id\n\n客户端 id,服务器用来识别消息的来源。\n\n### 8. max.in.flight.requests.per.connection\n\n指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高，就会占用越多的内存，不过也会提升吞吐量，把它设置为 1 可以保证消息是按照发送的顺序写入服务器，即使发生了重试。\n\n### 9. timeout.ms, request.timeout.ms & metadata.fetch.timeout.ms\n\n- timeout.ms 指定了 borker 等待同步副本返回消息的确认时间；\n- request.timeout.ms 指定了生产者在发送数据时等待服务器返回响应的时间；\n- metadata.fetch.timeout.ms 指定了生产者在获取元数据（比如分区首领是谁）时等待服务器返回响应的时间。\n\n### 10. max.block.ms\n\n指定了在调用 `send()` 方法或使用 `partitionsFor()` 方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满，或者没有可用的元数据时，这些方法会阻塞。在阻塞时间达到 max.block.ms 时，生产者会抛出超时异常。\n\n### 11. max.request.size\n\n该参数用于控制生产者发送的请求大小。它可以指发送的单个消息的最大值，也可以指单个请求里所有消息总的大小。例如，假设这个值为 1000K ，那么可以发送的单个最大消息为 1000K ，或者生产者可以在单个请求里发送一个批次，该批次包含了 1000 个消息，每个消息大小为 1K。 \n\n### 12. receive.buffer.bytes & send.buffer.byte\n\n这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小，-1 代表使用操作系统的默认值。\n\n\n\n\n\n## 参考资料\n\n1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26\n"
  },
  {
    "path": "大数据框架学习/Kafka简介.md",
    "content": "# Kafka简介\n\n<nav>\n<a href=\"#一Kafka简介\">一、Kafka简介</a><br/>\n<a href=\"#二Kafka核心概念\">二、Kafka核心概念</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-Messages-And-Batches\">2.1 Messages And Batches</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-Topics-And-Partitions\">2.2 Topics And Partitions</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-Producers-And-Consumers\">2.3 Producers And Consumers</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-Brokers-And-Clusters\">2.4 Brokers And Clusters </a><br/>\n</nav>\n\n\n## 一、简介\n\nApacheKafka 是一个分布式的流处理平台。它具有以下特点：\n\n+ 支持消息的发布和订阅，类似于 RabbtMQ、ActiveMQ 等消息队列；\n+ 支持数据实时处理；\n+ 能保证消息的可靠性投递；\n+ 支持消息的持久化存储，并通过多副本分布式的存储方案来保证消息的容错；\n+ 高吞吐率，单 Broker 可以轻松处理数千个分区以及每秒百万级的消息量。\n\n## 二、基本概念\n\n### 2.1 Messages And Batches\n\nKafka 的基本数据单元被称为 message(消息)，为减少网络开销，提高效率，多个消息会被放入同一批次 (Batch) 中后再写入。\n\n### 2.2 Topics And Partitions\n\nKafka 的消息通过 Topics(主题) 进行分类，一个主题可以被分为若干个 Partitions(分区)，一个分区就是一个提交日志 (commit log)。消息以追加的方式写入分区，然后以先入先出的顺序读取。Kafka 通过分区来实现数据的冗余和伸缩性，分区可以分布在不同的服务器上，这意味着一个 Topic 可以横跨多个服务器，以提供比单个服务器更强大的性能。\n\n由于一个 Topic 包含多个分区，因此无法在整个 Topic 范围内保证消息的顺序性，但可以保证消息在单个分区内的顺序性。\n\n<div align=\"center\"> <img  src=\"../pictures/kafka-topic.png\"/> </div>\n\n### 2.3 Producers And Consumers\n\n#### 1. 生产者\n\n生产者负责创建消息。一般情况下，生产者在把消息均衡地分布到在主题的所有分区上，而并不关心消息会被写到哪个分区。如果我们想要把消息写到指定的分区，可以通过自定义分区器来实现。\n\n#### 2. 消费者\n\n消费者是消费者群组的一部分，消费者负责消费消息。消费者可以订阅一个或者多个主题，并按照消息生成的顺序来读取它们。消费者通过检查消息的偏移量 (offset) 来区分读取过的消息。偏移量是一个不断递增的数值，在创建消息时，Kafka 会把它添加到其中，在给定的分区里，每个消息的偏移量都是唯一的。消费者把每个分区最后读取的偏移量保存在 Zookeeper 或 Kafka 上，如果消费者关闭或者重启，它还可以重新获取该偏移量，以保证读取状态不会丢失。\n\n<div align=\"center\"> <img  src=\"../pictures/kafka-producer-consumer.png\"/> </div>\n\n一个分区只能被同一个消费者群组里面的一个消费者读取，但可以被不同消费者群组中所组成的多个消费者共同读取。多个消费者群组中消费者共同读取同一个主题时，彼此之间互不影响。\n\n<div align=\"center\"> <img  src=\"../pictures/kafka消费者.png\"/> </div>\n\n### 2.4 Brokers And Clusters \n\n一个独立的 Kafka 服务器被称为 Broker。Broker 接收来自生产者的消息，为消息设置偏移量，并提交消息到磁盘保存。Broker 为消费者提供服务，对读取分区的请求做出响应，返回已经提交到磁盘的消息。\n\nBroker 是集群 (Cluster) 的组成部分。每一个集群都会选举出一个 Broker 作为集群控制器 (Controller)，集群控制器负责管理工作，包括将分区分配给 Broker 和监控 Broker。\n\n在集群中，一个分区 (Partition) 从属一个 Broker，该 Broker 被称为分区的首领 (Leader)。一个分区可以分配给多个 Brokers，这个时候会发生分区复制。这种复制机制为分区提供了消息冗余，如果有一个 Broker 失效，其他 Broker 可以接管领导权。\n\n<div align=\"center\"> <img  src=\"../pictures/kafka-cluster.png\"/> </div>\n\n\n\n## 参考资料\n\nNeha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26\n"
  },
  {
    "path": "大数据框架学习/Scala函数和闭包.md",
    "content": "# 函数和闭包\n\n<nav>\n<a href=\"#一函数\">一、函数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-函数与方法\">1.1 函数与方法</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-函数类型\">1.2 函数类型</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-一等公民匿名函数\">1.3 一等公民&匿名函数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-特殊的函数表达式\">1.4 特殊的函数表达式</a><br/>\n<a href=\"#二闭包\">二、闭包</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-闭包的定义\">2.1 闭包的定义</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-修改自由变量\">2.2 修改自由变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-自由变量多副本\">2.3 自由变量多副本</a><br/>\n<a href=\"#三高阶函数\">三、高阶函数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-使用函数作为参数\">3.1 使用函数作为参数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-函数柯里化\">3.2 函数柯里化</a><br/>\n</nav>\n\n\n## 一、函数\n\n### 1.1 函数与方法\n\nScala 中函数与方法的区别非常小，如果函数作为某个对象的成员，这样的函数被称为方法，否则就是一个正常的函数。\n\n```scala\n// 定义方法\ndef multi1(x:Int) = {x * x}\n// 定义函数\nval multi2 = (x: Int) => {x * x}\n\nprintln(multi1(3)) //输出 9\nprintln(multi2(3)) //输出 9\n```\n\n也可以使用 `def` 定义函数：\n\n```scala\ndef multi3 = (x: Int) => {x * x}\nprintln(multi3(3))  //输出 9 \n```\n\n`multi2` 和 `multi3` 本质上没有区别，这是因为函数是一等公民，`val multi2 = (x: Int) => {x * x}` 这个语句相当于是使用 `def` 预先定义了函数，之后赋值给变量 `multi2`。\n\n### 1.2 函数类型\n\n上面我们说过 `multi2` 和 `multi3` 本质上是一样的，那么作为函数它们是什么类型的？两者的类型实际上都是 `Int => Int`，前面一个 Int 代表输入参数类型，后面一个 Int 代表返回值类型。\n\n```scala\nscala> val multi2 = (x: Int) => {x * x}\nmulti2: Int => Int = $$Lambda$1092/594363215@1dd1a777\n\nscala> def multi3 = (x: Int) => {x * x}\nmulti3: Int => Int\n\n// 如果有多个参数，则类型为：（参数类型，参数类型 ...）=>返回值类型\nscala> val multi4 = (x: Int,name: String) => {name + x * x }\nmulti4: (Int, String) => String = $$Lambda$1093/1039732747@2eb4fe7\n```\n\n### 1.3 一等公民&匿名函数\n\n在 Scala 中函数是一等公民，这意味着不仅可以定义函数并调用它们，还可以将它们作为值进行传递：\n\n```scala\nimport scala.math.ceil\nobject ScalaApp extends App {\n  // 将函数 ceil 赋值给变量 fun,使用下划线 (_) 指明是 ceil 函数但不传递参数\n  val fun = ceil _\n  println(fun(2.3456))  //输出 3.0\n\n}\n```\n\n在 Scala 中你不必给每一个函数都命名，如 `(x: Int) => 3 * x` 就是一个匿名函数：\n\n```scala\nobject ScalaApp extends App {\n  // 1.匿名函数\n  (x: Int) => 3 * x\n  // 2.具名函数\n  val fun = (x: Int) => 3 * x\n  // 3.直接使用匿名函数\n  val array01 = Array(1, 2, 3).map((x: Int) => 3 * x)  \n  // 4.使用占位符简写匿名函数\n  val array02 = Array(1, 2, 3).map(_ * 3)\n  // 5.使用具名函数\n  val array03 = Array(1, 2, 3).map(fun)\n  \n}\n```\n\n### 1.4 特殊的函数表达式\n\n#### 1. 可变长度参数列表\n\n在 Java 中如果你想要传递可变长度的参数，需要使用 `String ...args` 这种形式，Scala 中等效的表达为 `args: String*`。\n\n```scala\nobject ScalaApp extends App {\n  def echo(args: String*): Unit = {\n    for (arg <- args) println(arg)\n  }\n  echo(\"spark\",\"hadoop\",\"flink\")\n}\n// 输出\nspark\nhadoop\nflink\n```\n\n#### 2. 传递具名参数\n\n向函数传递参数时候可以指定具体的参数名。\n\n```scala\nobject ScalaApp extends App {\n  \n  def detail(name: String, age: Int): Unit = println(name + \":\" + age)\n  \n  // 1.按照参数定义的顺序传入\n  detail(\"heibaiying\", 12)\n  // 2.传递参数的时候指定具体的名称,则不必遵循定义的顺序\n  detail(age = 12, name = \"heibaiying\")\n\n}\n```\n\n#### 3. 默认值参数\n\n在定义函数时，可以为参数指定默认值。\n\n```scala\nobject ScalaApp extends App {\n\n  def detail(name: String, age: Int = 88): Unit = println(name + \":\" + age)\n\n  // 如果没有传递 age 值,则使用默认值\n  detail(\"heibaiying\")\n  detail(\"heibaiying\", 12)\n\n}\n```\n\n## 二、闭包\n\n### 2.1 闭包的定义\n\n```scala\nvar more = 10\n// addMore 一个闭包函数:因为其捕获了自由变量 more 从而闭合了该函数字面量\nval addMore = (x: Int) => x + more\n```\n\n如上函数 `addMore` 中有两个变量 x 和 more:\n\n+ **x** : 是一个绑定变量 (bound variable)，因为其是该函数的入参，在函数的上下文中有明确的定义；\n+ **more** : 是一个自由变量 (free variable)，因为函数字面量本生并没有给 more 赋予任何含义。\n\n按照定义：在创建函数时，如果需要捕获自由变量，那么包含指向被捕获变量的引用的函数就被称为闭包函数。\n\n### 2.2 修改自由变量\n\n这里需要注意的是，闭包捕获的是变量本身，即是对变量本身的引用，这意味着：\n\n+ 闭包外部对自由变量的修改，在闭包内部是可见的；\n+ 闭包内部对自由变量的修改，在闭包外部也是可见的。\n\n```scala\n// 声明 more 变量\nscala> var more = 10\nmore: Int = 10\n\n// more 变量必须已经被声明，否则下面的语句会报错\nscala> val addMore = (x: Int) => {x + more}\naddMore: Int => Int = $$Lambda$1076/1844473121@876c4f0\n\nscala> addMore(10)\nres7: Int = 20\n\n// 注意这里是给 more 变量赋值，而不是重新声明 more 变量\nscala> more=1000\nmore: Int = 1000\n\nscala> addMore(10)\nres8: Int = 1010\n```\n\n### 2.3 自由变量多副本\n\n自由变量可能随着程序的改变而改变，从而产生多个副本，但是闭包永远指向创建时候有效的那个变量副本。\n\n```scala\n// 第一次声明 more 变量\nscala> var more = 10\nmore: Int = 10\n\n// 创建闭包函数\nscala> val addMore10 = (x: Int) => {x + more}\naddMore10: Int => Int = $$Lambda$1077/1144251618@1bdaa13c\n\n// 调用闭包函数\nscala> addMore10(9)\nres9: Int = 19\n\n// 重新声明 more 变量\nscala> var more = 100\nmore: Int = 100\n\n// 创建新的闭包函数\nscala> val addMore100 = (x: Int) => {x + more}\naddMore100: Int => Int = $$Lambda$1078/626955849@4d0be2ac\n\n// 引用的是重新声明 more 变量\nscala> addMore100(9)\nres10: Int = 109\n\n// 引用的还是第一次声明的 more 变量\nscala> addMore10(9)\nres11: Int = 19\n\n// 对于全局而言 more 还是 100\nscala> more\nres12: Int = 100\n```\n\n从上面的示例可以看出重新声明 `more` 后，全局的 `more` 的值是 100，但是对于闭包函数 `addMore10` 还是引用的是值为 10 的 `more`，这是由虚拟机来实现的，虚拟机会保证 `more` 变量在重新声明后，原来的被捕获的变量副本继续在堆上保持存活。\n\n## 三、高阶函数\n\n### 3.1 使用函数作为参数\n\n定义函数时候支持传入函数作为参数，此时新定义的函数被称为高阶函数。\n\n```scala\nobject ScalaApp extends App {\n\n  // 1.定义函数\n  def square = (x: Int) => {\n    x * x\n  }\n\n  // 2.定义高阶函数: 第一个参数是类型为 Int => Int 的函数\n  def multi(fun: Int => Int, x: Int) = {\n    fun(x) * 100\n  }\n\n  // 3.传入具名函数\n  println(multi(square, 5)) // 输出 2500\n    \n  // 4.传入匿名函数\n  println(multi(_ * 100, 5)) // 输出 50000\n\n}\n```\n\n### 3.2 函数柯里化\n\n我们上面定义的函数都只支持一个参数列表，而柯里化函数则支持多个参数列表。柯里化指的是将原来接受两个参数的函数变成接受一个参数的函数的过程。新的函数以原有第二个参数作为参数。\n\n```scala\nobject ScalaApp extends App {\n  // 定义柯里化函数\n  def curriedSum(x: Int)(y: Int) = x + y\n  println(curriedSum(2)(3)) //输出 5\n}\n```\n\n这里当你调用 curriedSum 时候，实际上是连着做了两次传统的函数调用，实际执行的柯里化过程如下：\n\n+ 第一次调用接收一个名为 `x` 的 Int 型参数，返回一个用于第二次调用的函数，假设 `x` 为 2，则返回函数 `2+y`；\n+ 返回的函数接收参数 `y`，并计算并返回值 `2+3` 的值。\n\n想要获得柯里化的中间返回的函数其实也比较简单：\n\n```scala\nobject ScalaApp extends App {\n  // 定义柯里化函数\n  def curriedSum(x: Int)(y: Int) = x + y\n  println(curriedSum(2)(3)) //输出 5\n\n  // 获取传入值为 10 返回的中间函数 10 + y\n  val plus: Int => Int = curriedSum(10)_\n  println(plus(3)) //输出值 13\n}\n```\n\n柯里化支持多个参数列表，多个参数按照从左到右的顺序依次执行柯里化操作：\n\n```scala\nobject ScalaApp extends App {\n  // 定义柯里化函数\n  def curriedSum(x: Int)(y: Int)(z: String) = x + y + z\n  println(curriedSum(2)(3)(\"name\")) // 输出 5name\n  \n}\n```\n\n\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n\n\n\n\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Scala列表和集.md",
    "content": "# List & Set\n\n<nav>\n<a href=\"#一List字面量\">一、List字面量</a><br/>\n<a href=\"#二List类型\">二、List类型</a><br/>\n<a href=\"#三构建List\">三、构建List</a><br/>\n<a href=\"#四模式匹配\">四、模式匹配</a><br/>\n<a href=\"#五列表的基本操作\">五、列表的基本操作</a><br/>\n<a href=\"#六列表的高级操作\">六、列表的高级操作</a><br/>\n<a href=\"#七List对象的方法\">七、List对象的方法</a><br/>\n<a href=\"#八处理多个List\">八、处理多个List</a><br/>\n<a href=\"#九缓冲列表ListBuffer\">九、缓冲列表ListBuffer</a><br/>\n<a href=\"#十集Set\">十、集(Set)</a><br/>\n</nav>\n\n\n\n## 一、List字面量\n\nList 是 Scala 中非常重要的一个数据结构，其与 Array(数组) 非常类似，但是 List 是不可变的，和 Java 中的 List 一样，其底层实现是链表。\n\n```scala\nscala>  val list = List(\"hadoop\", \"spark\", \"storm\")\nlist: List[String] = List(hadoop, spark, storm)\n\n// List 是不可变\nscala> list(1) = \"hive\"\n<console>:9: error: value update is not a member of List[String]\n```\n\n## 二、List类型\n\nScala 中 List 具有以下两个特性：\n\n+ **同构 (homogeneous)**：同一个 List 中的所有元素都必须是相同的类型；\n+ **协变 (covariant)**：如果 S 是 T 的子类型，那么 `List[S]` 就是 `List[T]` 的子类型，例如 `List[String]` 是 `List[Object]` 的子类型。\n\n需要特别说明的是空列表的类型为 `List[Nothing]`：\n\n```scala\nscala> List()\nres1: List[Nothing] = List()\n```\n\n## 三、构建List\n\n所有 List 都由两个基本单元构成：`Nil` 和 `::`(读作\"cons\")。即列表要么是空列表 (Nil)，要么是由一个 head 加上一个 tail 组成，而 tail 又是一个 List。我们在上面使用的 `List(\"hadoop\", \"spark\", \"storm\")` 最终也是被解释为 ` \"hadoop\"::\"spark\":: \"storm\"::Nil`。\n\n```scala\nscala>  val list01 = \"hadoop\"::\"spark\":: \"storm\"::Nil\nlist01: List[String] = List(hadoop, spark, storm)\n\n// :: 操作符号是右结合的，所以上面的表达式和下面的等同\nscala> val list02 = \"hadoop\"::(\"spark\":: (\"storm\"::Nil))\nlist02: List[String] = List(hadoop, spark, storm)\n```\n\n## 四、模式匹配\n\nScala 支持展开列表以实现模式匹配。\n\n```scala\nscala>  val list = List(\"hadoop\", \"spark\", \"storm\")\nlist: List[String] = List(hadoop, spark, storm)\n\nscala> val List(a,b,c)=list\na: String = hadoop\nb: String = spark\nc: String = storm\n```\n\n如果只需要匹配部分内容，可以如下：\n\n```scala\nscala> val a::rest=list\na: String = hadoop\nrest: List[String] = List(spark, storm)\n```\n\n## 五、列表的基本操作\n\n### 5.1 常用方法\n\n```scala\nobject ScalaApp extends App {\n\n  val list = List(\"hadoop\", \"spark\", \"storm\")\n\n  // 1.列表是否为空\n  list.isEmpty\n\n  // 2.返回列表中的第一个元素\n  list.head\n\n  // 3.返回列表中除第一个元素外的所有元素 这里输出 List(spark, storm)\n  list.tail\n\n  // 4.tail 和 head 可以结合使用\n  list.tail.head\n\n  // 5.返回列表中的最后一个元素 与 head 相反\n  list.init\n\n  // 6.返回列表中除了最后一个元素之外的其他元素 与 tail 相反 这里输出 List(hadoop, spark)\n  list.last\n\n  // 7.使用下标访问元素\n  list(2)\n\n  // 8.获取列表长度\n  list.length\n\n  // 9. 反转列表\n  list.reverse\n\n}\n```\n\n### 5.2 indices\n\nindices 方法返回所有下标。\n\n```scala\nscala> list.indices\nres2: scala.collection.immutable.Range = Range(0, 1, 2)\n```\n\n### 5.3 take & drop & splitAt\n\n- **take**：获取前 n 个元素；\n- **drop**：删除前 n 个元素；\n- **splitAt**：从第几个位置开始拆分。\n\n```scala\nscala> list take 2\nres3: List[String] = List(hadoop, spark)\n\nscala> list drop 2\nres4: List[String] = List(storm)\n\nscala> list splitAt 2\nres5: (List[String], List[String]) = (List(hadoop, spark),List(storm))\n```\n\n### 5.4 flatten\n\nflatten 接收一个由列表组成的列表，并将其进行扁平化操作，返回单个列表。\n\n```scala\nscala>  List(List(1, 2), List(3), List(), List(4, 5)).flatten\nres6: List[Int] = List(1, 2, 3, 4, 5)\n```\n\n### 5.5 zip & unzip\n\n对两个 List 执行 `zip` 操作结果如下，返回对应位置元素组成的元组的列表，`unzip` 则执行反向操作。\n\n```scala\nscala> val list = List(\"hadoop\", \"spark\", \"storm\")\nscala> val score = List(10,20,30)\n\nscala> val zipped=list zip score\nzipped: List[(String, Int)] = List((hadoop,10), (spark,20), (storm,30))\n\nscala> zipped.unzip\nres7: (List[String], List[Int]) = (List(hadoop, spark, storm),List(10, 20, 30))\n```\n\n### 5.6 toString & mkString\n\ntoString 返回 List 的字符串表现形式。\n\n```scala\nscala> list.toString\nres8: String = List(hadoop, spark, storm)\n```\n\n如果想改变 List 的字符串表现形式，可以使用 mkString。mkString 有三个重载方法，方法定义如下：\n\n```scala\n// start：前缀  sep：分隔符  end:后缀\ndef mkString(start: String, sep: String, end: String): String =\n  addString(new StringBuilder(), start, sep, end).toString\n\n// seq 分隔符\ndef mkString(sep: String): String = mkString(\"\", sep, \"\")\n\n// 如果不指定分隔符 默认使用\"\"分隔\ndef mkString: String = mkString(\"\")\n```\n\n使用示例如下：\n\n```scala\nscala> list.mkString\nres9: String = hadoopsparkstorm\n\nscala>  list.mkString(\",\")\nres10: String = hadoop,spark,storm\n\nscala> list.mkString(\"{\",\",\",\"}\")\nres11: String = {hadoop,spark,storm}\n```\n\n### 5.7 iterator & toArray & copyToArray\n\niterator 方法返回的是迭代器，这和其他语言的使用是一样的。\n\n```scala\nobject ScalaApp extends App {\n\n  val list = List(\"hadoop\", \"spark\", \"storm\")\n\n  val iterator: Iterator[String] = list.iterator\n\n  while (iterator.hasNext) {\n    println(iterator.next)\n  }\n  \n}\n```\n\ntoArray 和 toList 用于 List 和数组之间的互相转换。\n\n```scala\nscala> val array = list.toArray\narray: Array[String] = Array(hadoop, spark, storm)\n\nscala> array.toList\nres13: List[String] = List(hadoop, spark, storm)\n```\n\ncopyToArray 将 List 中的元素拷贝到数组中指定位置。\n\n```scala\nobject ScalaApp extends App {\n\n  val list = List(\"hadoop\", \"spark\", \"storm\")\n  val array = Array(\"10\", \"20\", \"30\")\n\n  list.copyToArray(array,1)\n\n  println(array.toBuffer)\n}\n\n// 输出 ：ArrayBuffer(10, hadoop, spark)\n```\n\n## 六、列表的高级操作\n\n### 6.1 列表转换：map & flatMap & foreach\n\nmap 与 Java 8 函数式编程中的 map 类似，都是对 List 中每一个元素执行指定操作。\n\n```scala\nscala> List(1,2,3).map(_+10)\nres15: List[Int] = List(11, 12, 13)\n```\n\nflatMap 与 map 类似，但如果 List 中的元素还是 List，则会对其进行 flatten 操作。\n\n```scala\nscala> list.map(_.toList)\nres16: List[List[Char]] = List(List(h, a, d, o, o, p), List(s, p, a, r, k), List(s, t, o, r, m))\n\nscala> list.flatMap(_.toList)\nres17: List[Char] = List(h, a, d, o, o, p, s, p, a, r, k, s, t, o, r, m)\n```\n\nforeach 要求右侧的操作是一个返回值为 Unit 的函数，你也可以简单理解为执行一段没有返回值代码。\n\n```scala\nscala> var sum = 0\nsum: Int = 0\n\nscala> List(1, 2, 3, 4, 5) foreach (sum += _)\n\nscala> sum\nres19: Int = 15\n```\n\n### 6.2 列表过滤：filter & partition & find & takeWhile & dropWhile & span\n\nfilter 用于筛选满足条件元素，返回新的 List。\n\n```scala\nscala> List(1, 2, 3, 4, 5) filter (_ % 2 == 0)\nres20: List[Int] = List(2, 4)\n```\n\npartition 会按照筛选条件对元素进行分组，返回类型是 tuple(元组)。\n\n```scala\nscala> List(1, 2, 3, 4, 5) partition (_ % 2 == 0)\nres21: (List[Int], List[Int]) = (List(2, 4),List(1, 3, 5))\n```\n\nfind 查找第一个满足条件的值，由于可能并不存在这样的值，所以返回类型是 `Option`，可以通过 `getOrElse` 在不存在满足条件值的情况下返回默认值。\n\n```scala\nscala> List(1, 2, 3, 4, 5) find (_ % 2 == 0)\nres22: Option[Int] = Some(2)\n\nval result: Option[Int] = List(1, 2, 3, 4, 5) find (_ % 2 == 0)\nresult.getOrElse(10)\n```\n\ntakeWhile 遍历元素，直到遇到第一个不符合条件的值则结束遍历，返回所有遍历到的值。\n\n```scala\nscala> List(1, 2, 3, -4, 5) takeWhile (_ > 0)\nres23: List[Int] = List(1, 2, 3)\n```\n\ndropWhile 遍历元素，直到遇到第一个不符合条件的值则结束遍历，返回所有未遍历到的值。\n\n```scala\n// 第一个值就不满足条件,所以返回列表中所有的值\nscala> List(1, 2, 3, -4, 5) dropWhile  (_ < 0)\nres24: List[Int] = List(1, 2, 3, -4, 5)\n\n\nscala> List(1, 2, 3, -4, 5) dropWhile (_ < 3)\nres26: List[Int] = List(3, -4, 5)\n```\n\nspan 遍历元素，直到遇到第一个不符合条件的值则结束遍历，将遍历到的值和未遍历到的值分别放入两个 List 中返回，返回类型是 tuple(元组)。\n\n```scala\nscala> List(1, 2, 3, -4, 5) span (_ > 0)\nres27: (List[Int], List[Int]) = (List(1, 2, 3),List(-4, 5))\n```\n\n\n\n### 6.3 列表检查：forall & exists\n\nforall 检查 List 中所有元素，如果所有元素都满足条件，则返回 true。\n\n```scala\nscala> List(1, 2, 3, -4, 5) forall ( _ > 0 )\nres28: Boolean = false\n```\n\nexists 检查 List 中的元素，如果某个元素已经满足条件，则返回 true。\n\n```scala\nscala>  List(1, 2, 3, -4, 5) exists (_ > 0 )\nres29: Boolean = true\n```\n\n\n\n### 6.4 列表排序：sortWith\n\nsortWith 对 List 中所有元素按照指定规则进行排序，由于 List 是不可变的，所以排序返回一个新的 List。\n\n```scala\nscala> List(1, -3, 4, 2, 6) sortWith (_ < _)\nres30: List[Int] = List(-3, 1, 2, 4, 6)\n\nscala> val list = List( \"hive\",\"spark\",\"azkaban\",\"hadoop\")\nlist: List[String] = List(hive, spark, azkaban, hadoop)\n\nscala> list.sortWith(_.length>_.length)\nres33: List[String] = List(azkaban, hadoop, spark, hive)\n```\n\n\n\n## 七、List对象的方法\n\n上面介绍的所有方法都是 List 类上的方法，下面介绍的是 List 伴生对象中的方法。\n\n### 7.1 List.range\n\nList.range 可以产生指定的前闭后开区间内的值组成的 List，它有三个可选参数: start(开始值)，end(结束值，不包含)，step(步长)。\n\n```scala\nscala>  List.range(1, 5)\nres34: List[Int] = List(1, 2, 3, 4)\n\nscala> List.range(1, 9, 2)\nres35: List[Int] = List(1, 3, 5, 7)\n\nscala> List.range(9, 1, -3)\nres36: List[Int] = List(9, 6, 3)\n```\n\n### 7.2 List.fill\n\nList.fill 使用指定值填充 List。\n\n```scala\nscala> List.fill(3)(\"hello\")\nres37: List[String] = List(hello, hello, hello)\n\nscala> List.fill(2,3)(\"world\")\nres38: List[List[String]] = List(List(world, world, world), List(world, world, world))\n```\n\n### 7.3 List.concat\n\nList.concat 用于拼接多个 List。\n\n```scala\nscala> List.concat(List('a', 'b'), List('c'))\nres39: List[Char] = List(a, b, c)\n\nscala> List.concat(List(), List('b'), List('c'))\nres40: List[Char] = List(b, c)\n\nscala> List.concat()\nres41: List[Nothing] = List()\n```\n\n\n\n## 八、处理多个List\n\n当多个 List 被放入同一个 tuple 中时候，可以通过 zipped 对多个 List 进行关联处理。\n\n```scala\n// 两个 List 对应位置的元素相乘\nscala> (List(10, 20), List(3, 4, 5)).zipped.map(_ * _)\nres42: List[Int] = List(30, 80)\n\n// 三个 List 的操作也是一样的\nscala> (List(10, 20), List(3, 4, 5), List(100, 200)).zipped.map(_ * _ + _)\nres43: List[Int] = List(130, 280)\n\n// 判断第一个 List 中元素的长度与第二个 List 中元素的值是否相等\nscala>  (List(\"abc\", \"de\"), List(3, 2)).zipped.forall(_.length == _)\nres44: Boolean = true\n```\n\n\n\n## 九、缓冲列表ListBuffer\n\n上面介绍的 List，由于其底层实现是链表，这意味着能快速访问 List 头部元素，但对尾部元素的访问则比较低效，这时候可以采用 `ListBuffer`，ListBuffer 提供了在常量时间内往头部和尾部追加元素。\n\n```scala\nimport scala.collection.mutable.ListBuffer\n\nobject ScalaApp extends App {\n\n  val buffer = new ListBuffer[Int]\n  // 1.在尾部追加元素\n  buffer += 1\n  buffer += 2\n  // 2.在头部追加元素\n  3 +=: buffer\n  // 3. ListBuffer 转 List\n  val list: List[Int] = buffer.toList\n  println(list)\n}\n\n//输出：List(3, 1, 2)\n```\n\n\n\n## 十、集(Set)\n\nSet 是不重复元素的集合。分为可变 Set 和不可变 Set。\n\n### 10.1 可变Set\n\n```scala\nobject ScalaApp extends App {\n\n  // 可变 Set\n  val mutableSet = new collection.mutable.HashSet[Int]\n\n  // 1.添加元素\n  mutableSet.add(1)\n  mutableSet.add(2)\n  mutableSet.add(3)\n  mutableSet.add(3)\n  mutableSet.add(4)\n\n  // 2.移除元素\n  mutableSet.remove(2)\n  \n  // 3.调用 mkString 方法 输出 1,3,4\n  println(mutableSet.mkString(\",\"))\n\n  // 4. 获取 Set 中最小元素\n  println(mutableSet.min)\n\n  // 5. 获取 Set 中最大元素\n  println(mutableSet.max)\n\n}\n```\n\n### 10.2 不可变Set\n\n不可变 Set 没有 add 方法，可以使用 `+` 添加元素，但是此时会返回一个新的不可变 Set，原来的 Set 不变。\n\n```scala\nobject ScalaApp extends App {\n  \n  // 不可变 Set\n  val immutableSet = new collection.immutable.HashSet[Int]\n\n  val ints: HashSet[Int] = immutableSet+1\n\n  println(ints)\n\n}\n\n// 输出 Set(1)\n```\n\n### 10.3 Set间操作\n\n多个 Set 之间可以进行求交集或者合集等操作。\n\n```scala\nobject ScalaApp extends App {\n\n  // 声明有序 Set\n  val mutableSet = collection.mutable.SortedSet(1, 2, 3, 4, 5)\n  val immutableSet = collection.immutable.SortedSet(3, 4, 5, 6, 7)\n  \n  // 两个 Set 的合集  输出：TreeSet(1, 2, 3, 4, 5, 6, 7)\n  println(mutableSet ++ immutableSet)\n\n  // 两个 Set 的交集  输出：TreeSet(3, 4, 5)\n  println(mutableSet intersect immutableSet)\n\n}\n```\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n"
  },
  {
    "path": "大数据框架学习/Scala基本数据类型和运算符.md",
    "content": "# Scala基本数据类型和运算符\n\n<nav>\n<a href=\"#一数据类型\">一、数据类型</a><br/>\n<a href=\"#二字面量\">二、字面量</a><br/>\n<a href=\"#三运算符\">三、运算符</a><br/>\n</nav>\n\n## 一、数据类型\n\n### 1.1 类型支持\n\nScala 拥有下表所示的数据类型，其中 Byte、Short、Int、Long 和 Char 类型统称为整数类型，整数类型加上 Float 和 Double 统称为数值类型。Scala 数值类型的取值范围和 Java 对应类型的取值范围相同。\n\n| 数据类型 | 描述                                                         |\n| -------- | ------------------------------------------------------------ |\n| Byte     | 8 位有符号补码整数。数值区间为 -128 到 127                    |\n| Short    | 16 位有符号补码整数。数值区间为 -32768 到 32767               |\n| Int      | 32 位有符号补码整数。数值区间为 -2147483648 到 2147483647     |\n| Long     | 64 位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807 |\n| Float    | 32 位, IEEE 754 标准的单精度浮点数                           |\n| Double   | 64 位 IEEE 754 标准的双精度浮点数                            |\n| Char     | 16 位无符号 Unicode 字符, 区间值为 U+0000 到 U+FFFF             |\n| String   | 字符序列                                                     |\n| Boolean  | true 或 false                                                  |\n| Unit     | 表示无值，等同于 Java 中的 void。用作不返回任何结果的方法的结果类型。Unit 只有一个实例值，写成 ()。 |\n| Null     | null 或空引用                                                |\n| Nothing  | Nothing 类型在 Scala 的类层级的最低端；它是任何其他类型的子类型。 |\n| Any      | Any 是所有其他类的超类                                        |\n| AnyRef   | AnyRef 类是 Scala 里所有引用类 (reference class) 的基类           |\n\n### 1.2 定义变量\n\nScala 的变量分为两种，val 和 var，其区别如下：\n\n+ **val** ： 类似于 Java 中的 final 变量，一旦初始化就不能被重新赋值；\n+ **var** ：类似于 Java 中的非 final 变量，在整个声明周期内 var 可以被重新赋值；\n\n```scala\nscala> val a=1\na: Int = 1\n\nscala> a=2\n<console>:8: error: reassignment to val // 不允许重新赋值\n\nscala> var b=1\nb: Int = 1\n\nscala> b=2\nb: Int = 2\n```\n\n### 1.3 类型推断\n\n在上面的演示中，并没有声明 a 是 Int 类型，但是程序还是把 a 当做 Int 类型，这就是 Scala 的类型推断。在大多数情况下，你都无需指明变量的类型，程序会自动进行推断。如果你想显式的声明类型，可以在变量后面指定，如下：\n\n```scala\nscala>  val c:String=\"hello scala\"\nc: String = hello scala\n```\n\n### 1.4 Scala解释器\n\n在 scala 命令行中，如果没有对输入的值指定赋值的变量，则输入的值默认会赋值给 `resX`(其中 X 是一个从 0 开始递增的整数)，`res` 是 result 的缩写，这个变量可以在后面的语句中进行引用。\n\n```scala\nscala> 5\nres0: Int = 5\n\nscala> res0*6\nres1: Int = 30\n\nscala> println(res1)\n30\n```\n\n\n\n## 二、字面量\n\nScala 和 Java 字面量在使用上很多相似，比如都使用 F 或 f 表示浮点型，都使用 L 或 l 表示 Long 类型。下文主要介绍两者差异部分。\n\n```scala\nscala> 1.2\nres0: Double = 1.2\n\nscala> 1.2f\nres1: Float = 1.2\n\nscala> 1.4F\nres2: Float = 1.4\n\nscala> 1\nres3: Int = 1\n\nscala> 1l\nres4: Long = 1\n\nscala> 1L\nres5: Long = 1\n```\n\n### 2.1 整数字面量\n\nScala 支持 10 进制和 16 进制，但不支持八进制字面量和以 0 开头的整数字面量。\n\n```scala\nscala> 012\n<console>:1: error: Decimal integer literals may not have a leading zero. (Octal syntax is obsolete.)\n```\n\n### 2.2 字符串字面量\n\n#### 1. 字符字面量\n\n字符字面量由一对单引号和中间的任意 Unicode 字符组成。你可以显式的给出原字符、也可以使用字符的 Unicode 码来表示，还可以包含特殊的转义字符。\n\n```scala\nscala> '\\u0041'\nres0: Char = A\n\nscala> 'a'\nres1: Char = a\n\nscala> '\\n'\nres2: Char =\n```\n\n#### 2. 字符串字面量\n\n字符串字面量由双引号包起来的字符组成。\n\n```scala\nscala> \"hello world\"\nres3: String = hello world\n```\n\n#### 3.原生字符串\n\nScala 提供了 `\"\"\" ... \"\"\"` 语法，通过三个双引号来表示原生字符串和多行字符串，使用该种方式，原生字符串中的特殊字符不会被转义。\n\n```scala\nscala> \"hello \\tool\"\nres4: String = hello    ool\n\nscala> \"\"\"hello \\tool\"\"\"\nres5: String = hello \\tool\n\nscala> \"\"\"hello\n     | world\"\"\"\nres6: String =\nhello\nworld\n```\n\n### 2.3 符号字面量\n\n符号字面量写法为： `'标识符 ` ，这里 标识符可以是任何字母或数字的组合。符号字面量会被映射成 `scala.Symbol` 的实例，如:符号字面量 `'x ` 会被编译器翻译为 `scala.Symbol(\"x\")`。符号字面量可选方法很少，只能通过 `.name` 获取其名称。\n\n注意：具有相同 `name` 的符号字面量一定指向同一个 Symbol 对象，不同 `name` 的符号字面量一定指向不同的 Symbol 对象。\n\n```scala\nscala> val sym = 'ID008\nsym: Symbol = 'ID008\n\nscala> sym.name\nres12: String = ID008\n```\n\n### 2.4 插值表达式\n\nScala 支持插值表达式。\n\n```scala\nscala> val name=\"xiaoming\"\nname: String = xiaoming\n\nscala> println(s\"My name is $name,I'm ${2*9}.\")\nMy name is xiaoming,I'm 18.\n```\n\n## 三、运算符\n\nScala 和其他语言一样，支持大多数的操作运算符：\n\n- 算术运算符（+，-，*，/，%）\n- 关系运算符（==，!=，>，<，>=，<=）\n- 逻辑运算符 (&&，||，!，&，|)\n- 位运算符 (~，&，|，^，<<，>>，>>>)\n- 赋值运算符 (=，+=，-=，*=，/=，%=，<<=，>>=，&=，^=，|=)\n\n以上操作符的基本使用与 Java 类似，下文主要介绍差异部分和注意事项。\n\n### 3.1 运算符即方法\n\nScala 的面向对象比 Java 更加纯粹，在 Scala 中一切都是对象。所以对于 `1+2`,实际上是调用了 Int 类中名为 `+` 的方法，所以 1+2,也可以写成 `1.+(2)`。\n\n```scala\nscala> 1+2\nres14: Int = 3\n\nscala> 1.+(2)\nres15: Int = 3\n```\n\nInt 类中包含了多个重载的 `+` 方法，用于分别接收不同类型的参数。\n\n<div align=\"center\"> <img  src=\"../pictures/scala-int+.png\"/> </div>\n\n### 3.2 逻辑运算符\n\n和其他语言一样，在 Scala 中 `&&`，`||` 的执行是短路的，即如果左边的表达式能确定整个结果，右边的表达式就不会被执行，这满足大多数使用场景。但是如果你需要在无论什么情况下，都执行右边的表达式，则可以使用 `&` 或 `|` 代替。\n\n### 3.3 赋值运算符\n\n在 Scala 中没有 Java 中的 `++` 和 `--` 运算符，如果你想要实现类似的操作，只能使用 `+=1`，或者 `-=1`。\n\n```scala\nscala> var a=1\na: Int = 1\n\nscala> a+=1\n\nscala> a\nres8: Int = 2\n\nscala> a-=1\n\nscala> a\nres10: Int = 1\n```\n\n### 3.4 运算符优先级\n\n操作符的优先级如下：优先级由上至下，逐级递减。\n\n<div align=\"center\"> <img src=\"../pictures/scala-操作符优先级.png\"/> </div>\n\n在表格中某个字符的优先级越高，那么以这个字符打头的方法就拥有更高的优先级。如 `+` 的优先级大于 `<`，也就意味则 `+` 的优先级大于以 `<` 开头的 `<<`，所以 `2<<2+2` , 实际上等价于 `2<<(2+2)` :\n\n```scala\nscala> 2<<2+2\nres0: Int = 32\n\nscala> 2<<(2+2)\nres1: Int = 32\n```\n\n### 3.5 对象相等性\n\n如果想要判断两个对象是否相等，可以使用 `==` 和 `!=`,这两个操作符可以用于所有的对象，包括 null。\n\n```scala\nscala> 1==2\nres2: Boolean = false\n\nscala> List(1,2,3)==List(1,2,3)\nres3: Boolean = true\n\nscala> 1==1.0\nres4: Boolean = true\n\nscala> List(1,2,3)==null\nres5: Boolean = false\n\nscala> null==null\nres6: Boolean = true\n```\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 \n"
  },
  {
    "path": "大数据框架学习/Scala数组.md",
    "content": "# Scala 数组相关操作\n\n<nav>\n<a href=\"#一定长数组\">一、定长数组</a><br/>\n<a href=\"#二变长数组\">二、变长数组</a><br/>\n<a href=\"#三数组遍历\">三、数组遍历</a><br/>\n<a href=\"#四数组转换\">四、数组转换</a><br/>\n<a href=\"#五多维数组\">五、多维数组</a><br/>\n<a href=\"#六与Java互操作\">六、与Java互操作</a><br/>\n</nav>\n\n## 一、定长数组\n\n在 Scala 中，如果你需要一个长度不变的数组，可以使用 Array。但需要注意以下两点：\n\n- 在 Scala 中使用 `(index)` 而不是 `[index]` 来访问数组中的元素，因为访问元素，对于 Scala 来说是方法调用，`(index)` 相当于执行了 `.apply(index)` 方法。\n- Scala 中的数组与 Java 中的是等价的，`Array[Int]()` 在虚拟机层面就等价于 Java 的 `int[]`。\n\n```scala\n// 10 个整数的数组，所有元素初始化为 0\nscala> val nums=new Array[Int](10)\nnums: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)\n\n// 10 个元素的字符串数组，所有元素初始化为 null\nscala> val strings=new Array[String](10)\nstrings: Array[String] = Array(null, null, null, null, null, null, null, null, null, null)\n\n// 使用指定值初始化，此时不需要 new 关键字\nscala> val a=Array(\"hello\",\"scala\")\na: Array[String] = Array(hello, scala)\n\n// 使用 () 来访问元素\nscala> a(0)\nres3: String = hello\n```\n\n## 二、变长数组\n\n在 scala 中通过 ArrayBuffer 实现变长数组 (又称缓冲数组)。在构建 ArrayBuffer 时必须给出类型参数，但不必指定长度，因为 ArrayBuffer 会在需要的时候自动扩容和缩容。变长数组的构建方式及常用操作如下：\n\n```java\nimport scala.collection.mutable.ArrayBuffer\n\nobject ScalaApp {\n    \n  // 相当于 Java 中的 main 方法\n  def main(args: Array[String]): Unit = {\n    // 1.声明变长数组 (缓冲数组)\n    val ab = new ArrayBuffer[Int]()\n    // 2.在末端增加元素\n    ab += 1\n    // 3.在末端添加多个元素\n    ab += (2, 3, 4)\n    // 4.可以使用 ++=追加任何集合\n    ab ++= Array(5, 6, 7)\n    // 5.缓冲数组可以直接打印查看\n    println(ab)\n    // 6.移除最后三个元素\n    ab.trimEnd(3)\n    // 7.在第 1 个元素之后插入多个新元素\n    ab.insert(1, 8, 9)\n    // 8.从第 2 个元素开始,移除 3 个元素,不指定第二个参数的话,默认值为 1\n    ab.remove(2, 3)\n    // 9.缓冲数组转定长数组\n    val abToA = ab.toArray\n    // 10. 定长数组打印为其 hashcode 值\n    println(abToA)\n    // 11. 定长数组转缓冲数组\n    val aToAb = abToA.toBuffer\n  }\n}\n```\n\n需要注意的是：使用 `+= ` 在末尾插入元素是一个高效的操作，其时间复杂度是 O(1)。而使用 `insert` 随机插入元素的时间复杂度是 O(n)，因为在其插入位置之后的所有元素都要进行对应的后移，所以在 `ArrayBuffer` 中随机插入元素是一个低效的操作。\n\n## 三、数组遍历\n\n```scala\nobject ScalaApp extends App {\n\n  val a = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\n  // 1.方式一 相当于 Java 中的增强 for 循环\n  for (elem <- a) {\n    print(elem)\n  }\n\n  // 2.方式二\n  for (index <- 0 until a.length) {\n    print(a(index))\n  }\n\n  // 3.方式三, 是第二种方式的简写\n  for (index <- a.indices) {\n    print(a(index))\n  }\n\n  // 4.反向遍历\n  for (index <- a.indices.reverse) {\n    print(a(index))\n  }\n\n}\n```\n\n这里我们没有将代码写在 main 方法中，而是继承自 App.scala，这是 Scala 提供的一种简写方式，此时将代码写在类中，等价于写在 main 方法中，直接运行该类即可。\n\n\n\n## 四、数组转换\n\n数组转换是指由现有数组产生新的数组。假设当前拥有 a 数组，想把 a 中的偶数元素乘以 10 后产生一个新的数组，可以采用下面两种方式来实现：\n\n```scala\nobject ScalaApp extends App {\n\n  val a = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\n  // 1.方式一 yield 关键字\n  val ints1 = for (elem <- a if elem % 2 == 0) yield 10 * elem\n  for (elem <- ints1) {\n    println(elem)\n  }\n\n  // 2.方式二 采用函数式编程的方式,这和 Java 8 中的函数式编程是类似的，这里采用下划线标表示其中的每个元素\n  val ints2 = a.filter(_ % 2 == 0).map(_ * 10)\n  for (elem <- ints1) {\n    println(elem)\n  }\n}\n```\n\n\n\n## 五、多维数组\n\n和 Java 中一样，多维数组由单维数组组成。\n\n```scala\nobject ScalaApp extends App {\n\n  val matrix = Array(Array(11, 12, 13, 14, 15, 16, 17, 18, 19, 20),\n    Array(21, 22, 23, 24, 25, 26, 27, 28, 29, 30),\n    Array(31, 32, 33, 34, 35, 36, 37, 38, 39, 40))\n\n\n  for (elem <- matrix) {\n\n    for (elem <- elem) {\n      print(elem + \"-\")\n    }\n    println()\n  }\n\n}\n\n打印输出如下：\n11-12-13-14-15-16-17-18-19-20-\n21-22-23-24-25-26-27-28-29-30-\n31-32-33-34-35-36-37-38-39-40-\n```\n\n\n\n## 六、与Java互操作\n\n由于 Scala 的数组是使用 Java 的数组来实现的，所以两者之间可以相互转换。\n\n```scala\nimport java.util\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.collection.{JavaConverters, mutable}\n\nobject ScalaApp extends App {\n\n  val element = ArrayBuffer(\"hadoop\", \"spark\", \"storm\")\n  // Scala 转 Java\n  val javaList: util.List[String] = JavaConverters.bufferAsJavaList(element)\n  // Java 转 Scala\n  val scalaBuffer: mutable.Buffer[String] = JavaConverters.asScalaBuffer(javaList)\n  for (elem <- scalaBuffer) {\n    println(elem)\n  }\n}\n```\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n"
  },
  {
    "path": "大数据框架学习/Scala映射和元组.md",
    "content": "# Map & Tuple\n\n<nav>\n<a href=\"#一映射Map\">一、映射(Map)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-构造Map\">1.1 构造Map</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-获取值\">1.2 获取值</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-新增修改删除值\">1.3 新增/修改/删除值</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-遍历Map\">1.4 遍历Map</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-yield关键字\">1.5 yield关键字</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-其他Map结构\">1.6 其他Map结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#17-可选方法\">1.7 可选方法</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#18-与Java互操作\">1.8 与Java互操作</a><br/>\n<a href=\"#二元组Tuple\">二、元组(Tuple)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21--模式匹配\">2.1  模式匹配</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-zip方法\">2.2 zip方法</a><br/>\n</nav>\n\n\n\n## 一、映射(Map)\n\n### 1.1 构造Map\n\n```scala\n// 初始化一个空 map\nval scores01 = new HashMap[String, Int]\n\n// 从指定的值初始化 Map（方式一）\nval scores02 = Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n\n// 从指定的值初始化 Map（方式二）\nval scores03 = Map((\"hadoop\", 10), (\"spark\", 20), (\"storm\", 30))\n```\n\n采用上面方式得到的都是不可变 Map(immutable map)，想要得到可变 Map(mutable map)，则需要使用：\n\n```scala\nval scores04 = scala.collection.mutable.Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n```\n\n### 1.2 获取值\n\n```scala\nobject ScalaApp extends App {\n\n  val scores = Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n\n  // 1.获取指定 key 对应的值\n  println(scores(\"hadoop\"))\n\n  // 2. 如果对应的值不存在则使用默认值\n  println(scores.getOrElse(\"hadoop01\", 100))\n}\n```\n\n### 1.3 新增/修改/删除值\n\n可变 Map 允许进行新增、修改、删除等操作。\n\n```scala\nobject ScalaApp extends App {\n\n  val scores = scala.collection.mutable.Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n\n  // 1.如果 key 存在则更新\n  scores(\"hadoop\") = 100\n\n  // 2.如果 key 不存在则新增\n  scores(\"flink\") = 40\n\n  // 3.可以通过 += 来进行多个更新或新增操作\n  scores += (\"spark\" -> 200, \"hive\" -> 50)\n\n  // 4.可以通过 -= 来移除某个键和值\n  scores -= \"storm\"\n\n  for (elem <- scores) {println(elem)}\n}\n\n// 输出内容如下\n(spark,200)\n(hadoop,100)\n(flink,40)\n(hive,50)\n```\n\n不可变 Map 不允许进行新增、修改、删除等操作，但是允许由不可变 Map 产生新的 Map。\n\n```scala\nobject ScalaApp extends App {\n\n  val scores = Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n\n  val newScores = scores + (\"spark\" -> 200, \"hive\" -> 50)\n\n  for (elem <- scores) {println(elem)}\n\n}\n\n// 输出内容如下\n(hadoop,10)\n(spark,200)\n(storm,30)\n(hive,50)\n```\n\n### 1.4 遍历Map\n\n```java\nobject ScalaApp extends App {\n\n  val scores = Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n\n  // 1. 遍历键\n  for (key <- scores.keys) { println(key) }\n\n  // 2. 遍历值\n  for (value <- scores.values) { println(value) }\n\n  // 3. 遍历键值对\n  for ((key, value) <- scores) { println(key + \":\" + value) }\n\n}\n```\n\n### 1.5 yield关键字\n\n可以使用 `yield` 关键字从现有 Map 产生新的 Map。\n\n```scala\nobject ScalaApp extends App {\n\n  val scores = Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n\n  // 1.将 scores 中所有的值扩大 10 倍\n  val newScore = for ((key, value) <- scores) yield (key, value * 10)\n  for (elem <- newScore) { println(elem) }\n\n\n  // 2.将键和值互相调换\n  val reversalScore: Map[Int, String] = for ((key, value) <- scores) yield (value, key)\n  for (elem <- reversalScore) { println(elem) }\n\n}\n\n// 输出\n(hadoop,100)\n(spark,200)\n(storm,300)\n\n(10,hadoop)\n(20,spark)\n(30,storm)\n```\n\n### 1.6 其他Map结构\n\n在使用 Map 时候，如果不指定，默认使用的是 HashMap，如果想要使用 `TreeMap` 或者 `LinkedHashMap`，则需要显式的指定。\n\n```scala\nobject ScalaApp extends App {\n\n  // 1.使用 TreeMap,按照键的字典序进行排序\n  val scores01 = scala.collection.mutable.TreeMap(\"B\" -> 20, \"A\" -> 10, \"C\" -> 30)\n  for (elem <- scores01) {println(elem)}\n\n  // 2.使用 LinkedHashMap,按照键值对的插入顺序进行排序\n  val scores02 = scala.collection.mutable.LinkedHashMap(\"B\" -> 20, \"A\" -> 10, \"C\" -> 30)\n  for (elem <- scores02) {println(elem)}\n}\n\n// 输出\n(A,10)\n(B,20)\n(C,30)\n\n(B,20)\n(A,10)\n(C,30)\n```\n\n### 1.7 可选方法\n\n```scala\nobject ScalaApp extends App {\n\n  val scores = scala.collection.mutable.TreeMap(\"B\" -> 20, \"A\" -> 10, \"C\" -> 30)\n\n  // 1. 获取长度\n  println(scores.size)\n\n  // 2. 判断是否为空\n  println(scores.isEmpty)\n\n  // 3. 判断是否包含特定的 key\n  println(scores.contains(\"A\"))\n\n}\n```\n\n### 1.8 与Java互操作\n\n```scala\nimport java.util\nimport scala.collection.{JavaConverters, mutable}\n\nobject ScalaApp extends App {\n\n  val scores = Map(\"hadoop\" -> 10, \"spark\" -> 20, \"storm\" -> 30)\n\n  // scala map 转 java map\n  val javaMap: util.Map[String, Int] = JavaConverters.mapAsJavaMap(scores)\n\n  // java map 转 scala map\n  val scalaMap: mutable.Map[String, Int] = JavaConverters.mapAsScalaMap(javaMap)\n  \n  for (elem <- scalaMap) {println(elem)}\n}\n```\n\n\n\n## 二、元组(Tuple)\n\n元组与数组类似，但是数组中所有的元素必须是同一种类型，而元组则可以包含不同类型的元素。\n\n```scala\nscala> val tuple=(1,3.24f,\"scala\")\ntuple: (Int, Float, String) = (1,3.24,scala)\n```\n\n### 2.1  模式匹配\n\n可以通过模式匹配来获取元组中的值并赋予对应的变量：\n\n```scala\nscala> val (a,b,c)=tuple\na: Int = 1\nb: Float = 3.24\nc: String = scala\n```\n\n如果某些位置不需要赋值，则可以使用下划线代替：\n\n```scala\nscala> val (a,_,_)=tuple\na: Int = 1\n```\n\n### 2.2 zip方法\n\n```scala\nobject ScalaApp extends App {\n\n   val array01 = Array(\"hadoop\", \"spark\", \"storm\")\n  val array02 = Array(10, 20, 30)\n    \n  // 1.zip 方法得到的是多个 tuple 组成的数组\n  val tuples: Array[(String, Int)] = array01.zip(array02)\n  // 2.也可以在 zip 后调用 toMap 方法转换为 Map\n  val map: Map[String, Int] = array01.zip(array02).toMap\n    \n  for (elem <- tuples) { println(elem) }\n  for (elem <- map) {println(elem)}\n}\n\n// 输出\n(hadoop,10)\n(spark,20)\n(storm,30)\n\n(hadoop,10)\n(spark,20)\n(storm,30)\n```\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n"
  },
  {
    "path": "大数据框架学习/Scala模式匹配.md",
    "content": "# Scala模式匹配\n\n<nav>\n<a href=\"#一模式匹配\">一、模式匹配</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-更好的swith\">1.1 更好的swith</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-用作类型检查\">1.2 用作类型检查</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-匹配数据结构\">1.3 匹配数据结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-提取器\">1.4 提取器</a><br/>\n<a href=\"#二样例类\">二、样例类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-样例类\">2.1 样例类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-用于模式匹配\">2.3 用于模式匹配</a><br/>\n</nav>\n\n## 一、模式匹配\n\nScala 支持模式匹配机制，可以代替 swith 语句、执行类型检查、以及支持析构表达式等。\n\n### 1.1 更好的swith\n\nScala 不支持 swith，可以使用模式匹配 `match...case` 语法代替。但是 match 语句与 Java 中的 switch 有以下三点不同：\n\n- Scala 中的 case 语句支持任何类型；而 Java 中 case 语句仅支持整型、枚举和字符串常量；\n- Scala 中每个分支语句后面不需要写 break，因为在 case 语句中 break 是隐含的，默认就有；\n- 在 Scala 中 match 语句是有返回值的，而 Java 中 switch 语句是没有返回值的。如下：\n\n```scala\nobject ScalaApp extends App {\n\n  def matchTest(x: Int) = x match {\n    case 1 => \"one\"\n    case 2 => \"two\"\n    case _ if x > 9 && x < 100 => \"两位数\"   //支持条件表达式 这被称为模式守卫\n    case _ => \"other\"\n  }\n\n  println(matchTest(1))   //输出 one\n  println(matchTest(10))  //输出 两位数\n  println(matchTest(200)) //输出 other\n}\n```\n\n### 1.2 用作类型检查\n\n```scala\nobject ScalaApp extends App {\n\n  def matchTest[T](x: T) = x match {\n    case x: Int => \"数值型\"\n    case x: String => \"字符型\"\n    case x: Float => \"浮点型\"\n    case _ => \"other\"\n  }\n\n  println(matchTest(1))     //输出 数值型\n  println(matchTest(10.3f)) //输出 浮点型\n  println(matchTest(\"str\")) //输出 字符型\n  println(matchTest(2.1))   //输出 other\n}\n```\n\n### 1.3 匹配数据结构\n\n匹配元组示例：\n\n```scala\nobject ScalaApp extends App {\n\n  def matchTest(x: Any) = x match {\n    case (0, _, _) => \"匹配第一个元素为 0 的元组\"\n    case (a, b, c) => println(a + \"~\" + b + \"~\" + c)\n    case _ => \"other\"\n  }\n\n  println(matchTest((0, 1, 2)))             // 输出: 匹配第一个元素为 0 的元组\n  matchTest((1, 2, 3))                      // 输出: 1~2~3\n  println(matchTest(Array(10, 11, 12, 14))) // 输出: other\n}\n```\n\n匹配数组示例：\n\n```scala\nobject ScalaApp extends App {\n\n  def matchTest[T](x: Array[T]) = x match {\n    case Array(0) => \"匹配只有一个元素 0 的数组\"\n    case Array(a, b) => println(a + \"~\" + b)\n    case Array(10, _*) => \"第一个元素为 10 的数组\"\n    case _ => \"other\"\n  }\n\n  println(matchTest(Array(0)))          // 输出: 匹配只有一个元素 0 的数组\n  matchTest(Array(1, 2))                // 输出: 1~2\n  println(matchTest(Array(10, 11, 12))) // 输出: 第一个元素为 10 的数组\n  println(matchTest(Array(3, 2, 1)))    // 输出: other\n}\n```\n\n### 1.4 提取器\n\n数组、列表和元组能使用模式匹配，都是依靠提取器 (extractor) 机制，它们伴生对象中定义了 `unapply` 或 `unapplySeq` 方法：\n\n+ **unapply**：用于提取固定数量的对象；\n+ **unapplySeq**：用于提取一个序列；\n\n这里以数组为例，`Array.scala` 定义了 `unapplySeq` 方法：\n\n```scala\ndef unapplySeq[T](x : scala.Array[T]) : scala.Option[scala.IndexedSeq[T]] = { /* compiled code */ }\n```\n\n`unapplySeq` 返回一个序列，包含数组中的所有值，这样在模式匹配时，才能知道对应位置上的值。\n\n\n\n## 二、样例类\n\n### 2.1 样例类\n\n样例类是一种的特殊的类，它们被经过优化以用于模式匹配，样例类的声明比较简单，只需要在 `class` 前面加上关键字 `case`。下面给出一个样例类及其用于模式匹配的示例：\n\n```scala\n//声明一个抽象类\nabstract class Person{}\n```\n\n```scala\n// 样例类 Employee\ncase class Employee(name: String, age: Int, salary: Double) extends Person {}\n```\n\n```scala\n// 样例类 Student\ncase class Student(name: String, age: Int) extends Person {}\n```\n\n当你声明样例类后，编译器自动进行以下配置：\n\n- 构造器中每个参数都默认为 `val`；\n- 自动地生成 `equals, hashCode, toString, copy` 等方法；\n- 伴生对象中自动生成 `apply` 方法，使得可以不用 new 关键字就能构造出相应的对象；\n- 伴生对象中自动生成 `unapply` 方法，以支持模式匹配。\n\n除了上面的特征外，样例类和其他类相同，可以任意添加方法和字段，扩展它们。\n\n### 2.3 用于模式匹配\n\n样例的伴生对象中自动生成 `unapply` 方法，所以样例类可以支持模式匹配，使用如下：\n\n```scala\nobject ScalaApp extends App {\n\n  def matchTest(person: Person) = person match {\n    case Student(name, _) => \"student:\" + name\n    case Employee(_, _, salary) => \"employee salary:\" + salary\n    case _ => \"other\"\n  }\n\n  println(matchTest(Student(\"heibai\", 12)))        //输出: student:heibai\n  println(matchTest(Employee(\"ying\", 22, 999999))) //输出: employee salary:999999.0\n}\n```\n\n\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n\n"
  },
  {
    "path": "大数据框架学习/Scala流程控制语句.md",
    "content": "# 流程控制语句\n\n<nav>\n<a href=\"#一条件表达式if\">一、条件表达式if</a><br/>\n<a href=\"#二块表达式\">二、块表达式</a><br/>\n<a href=\"#三循环表达式while\">三、循环表达式while</a><br/>\n<a href=\"#四循环表达式for\">四、循环表达式for</a><br/>\n<a href=\"#五异常处理try\">五、异常处理try</a><br/>\n<a href=\"#六条件选择表达式match\">六、条件选择表达式match</a><br/>\n<a href=\"#七没有break和continue\">七、没有break和continue</a><br/>\n<a href=\"#八输入与输出\">八、输入与输出</a><br/>\n</nav>\n\n## 一、条件表达式if\n\nScala 中的 if/else 语法结构与 Java 中的一样，唯一不同的是，Scala 中的 if 表达式是有返回值的。\n\n```scala\nobject ScalaApp extends App {\n\n  val x = \"scala\"\n  val result = if (x.length == 5) \"true\" else \"false\"\n  print(result)\n  \n}\n```\n\n在 Java 中，每行语句都需要使用 `;` 表示结束，但是在 Scala 中并不需要。除非你在单行语句中写了多行代码。\n\n\n\n## 二、块表达式\n\n在 Scala 中，可以使用 `{}` 块包含一系列表达式，块中最后一个表达式的值就是块的值。\n\n```scala\nobject ScalaApp extends App {\n\n  val result = {\n    val a = 1 + 1; val b = 2 + 2; a + b\n  }\n  print(result)\n}\n\n// 输出： 6\n```\n\n如果块中的最后一个表达式没有返回值，则块的返回值是 Unit 类型。\n\n```scala\nscala> val result ={ val a = 1 + 1; val b = 2 + 2 }\nresult: Unit = ()\n```\n\n\n\n## 三、循环表达式while\n\nScala 和大多数语言一样，支持 `while` 和 `do ... while` 表达式。\n\n```scala\nobject ScalaApp extends App {\n\n  var n = 0\n\n  while (n < 10) {\n    n += 1\n    println(n)\n  }\n\n  // 循环至少要执行一次\n  do {\n    println(n)\n  } while (n > 10)\n}\n```\n\n\n\n## 四、循环表达式for\n\nfor 循环的基本使用如下：\n\n```scala\nobject ScalaApp extends App {\n\n  // 1.基本使用  输出[1,9)\n  for (n <- 1 until 10) {print(n)}\n\n  // 2.使用多个表达式生成器  输出: 11 12 13 21 22 23 31 32 33\n  for (i <- 1 to 3; j <- 1 to 3) print(f\"${10 * i + j}%3d\")\n\n  // 3.使用带条件的表达式生成器  输出: 12 13 21 23 31 32\n  for (i <- 1 to 3; j <- 1 to 3 if i != j) print(f\"${10 * i + j}%3d\")\n\n}\n```\n\n除了基本使用外，还可以使用 `yield` 关键字从 for 循环中产生 Vector，这称为 for 推导式。\n\n```scala\nscala> for (i <- 1 to 10) yield i * 6\nres1: scala.collection.immutable.IndexedSeq[Int] = Vector(6, 12, 18, 24, 30, 36, 42, 48, 54, 60)\n```\n\n\n\n## 五、异常处理try\n\n和 Java 中一样，支持 `try...catch...finally` 语句。\n\n```scala\nimport java.io.{FileNotFoundException, FileReader}\n\nobject ScalaApp extends App {\n\n  try {\n    val reader = new FileReader(\"wordCount.txt\")\n  } catch {\n    case ex: FileNotFoundException =>\n      ex.printStackTrace()\n      println(\"没有找到对应的文件!\")\n  } finally {\n    println(\"finally 语句一定会被执行！\")\n  }\n}\n```\n\n这里需要注意的是因为 finally 语句一定会被执行，所以不要在该语句中返回值，否则返回值会被作为整个 try 语句的返回值，如下：\n\n```scala\nscala> def g():Int = try return 1 finally  return  2\ng: ()Int\n\n// 方法 g() 总会返回 2\nscala> g()\nres3: Int = 2\n```\n\n\n\n## 六、条件选择表达式match\n\nmatch 类似于 java 中的 switch 语句。\n\n```scala\nobject ScalaApp extends App {\n\n  val elements = Array(\"A\", \"B\", \"C\", \"D\", \"E\")\n\n  for (elem <- elements) {\n    elem match {\n      case \"A\" => println(10)\n      case \"B\" => println(20)\n      case \"C\" => println(30)\n      case _ => println(50)\n    }\n  }\n}\n\n```\n\n但是与 Java 中的 switch 有以下三点不同：\n\n+ Scala 中的 case 语句支持任何类型；而 Java 中 case 语句仅支持整型、枚举和字符串常量；\n+ Scala 中每个分支语句后面不需要写 break，因为在 case 语句中 break 是隐含的，默认就有；\n+ 在 Scala 中 match 语句是有返回值的，而 Java 中 switch 语句是没有返回值的。如下：\n\n```scala\nobject ScalaApp extends App {\n\n  val elements = Array(\"A\", \"B\", \"C\", \"D\", \"E\")\n\n  for (elem <- elements) {\n    val score = elem match {\n      case \"A\" => 10\n      case \"B\" => 20\n      case \"C\" => 30\n      case _ => 50\n    }\n    print(elem + \":\" + score + \";\")\n  }\n}\n// 输出： A:10;B:20;C:30;D:50;E:50;\n```\n\n\n\n## 七、没有break和continue\n\n额外注意一下：Scala 中并不支持 Java 中的 break 和 continue 关键字。\n\n\n\n## 八、输入与输出\n\n在 Scala 中可以使用 print、println、printf 打印输出，这与 Java 中是一样的。如果需要从控制台中获取输入，则可以使用 `StdIn` 中定义的各种方法。\n\n```scala\nval name = StdIn.readLine(\"Your name: \")\nprint(\"Your age: \")\nval age = StdIn.readInt()\nprintln(s\"Hello, ${name}! Next year, you will be ${age + 1}.\")\n```\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n"
  },
  {
    "path": "大数据框架学习/Scala简介及开发环境配置.md",
    "content": "# Scala简介及开发环境配置\n\n<nav>\n<a href=\"#一Scala简介\">一、Scala简介</a><br/>\n<a href=\"#二配置IDEA开发环境\">二、配置IDEA开发环境</a><br/>\n</nav>\n\n\n## 一、Scala简介\n\n### 1.1 概念\n\nScala 全称为 Scalable Language，即“可伸缩的语言”，之所以这样命名，是因为它的设计目标是希望伴随着用户的需求一起成长。Scala 是一门综合了**面向对象**和**函数式编程概念**的**静态类型**的编程语言，它运行在标准的 Java 平台上，可以与所有的 Java 类库无缝协作。\n\n\n\n### 1.2 特点\n\n#### 1. Scala是面向对象的\n\nScala 是一种面向对象的语言，每个值都是对象，每个方法都是调用。举例来说，如果你执行 `1+2`，则对于 Scala 而言，实际是在调用 Int 类里定义的名为 `+` 的方法。\n\n#### 2. Scala是函数式的\n\nScala 不只是一门纯的面对对象的语言，它也是功能完整的函数式编程语言。函数式编程以两大核心理念为指导：\n\n+ 函数是一等公民；\n+ 程序中的操作应该将输入值映射成输出值，而不是当场修改数据。即方法不应该有副作用。\n\n\n\n### 1.3 Scala的优点\n\n#### 1. 与Java的兼容\n\nScala 可以与 Java 无缝对接，其在执行时会被编译成 JVM 字节码，这使得其性能与 Java 相当。Scala 可以直接调用 Java 中的方法、访问 Java 中的字段、继承 Java 类、实现 Java 接口。Scala 重度复用并包装了原生的 Java 类型，并支持隐式转换。\n\n#### 2. 精简的语法\n\nScala 的程序通常比较简洁，相比 Java 而言，代码行数会大大减少，这使得程序员对代码的阅读和理解更快，缺陷也更少。\n\n#### 3. 高级语言的特性\n\nScala 具有高级语言的特定，对代码进行了高级别的抽象，能够让你更好地控制程序的复杂度，保证开发的效率。\n\n#### 4. 静态类型\n\nScala 拥有非常先进的静态类型系统，Scala 不仅拥有与 Java 类似的允许嵌套类的类型系统，还支持使用泛型对类型进行参数化，用交集（intersection）来组合类型，以及使用抽象类型来进行隐藏类型的细节。通过这些特性，可以更快地设计出安全易用的程序和接口。\n\n\n\n\n\n## 二、配置IDEA开发环境\n\n### 2.1 前置条件\n\nScala 的运行依赖于 JDK，Scala 2.12.x 需要 JDK 1.8+。\n\n### 2.2 安装Scala插件\n\nIDEA 默认不支持 Scala 语言的开发，需要通过插件进行扩展。打开 IDEA，依次点击 **File** => **settings**=> **plugins** 选项卡，搜索 Scala 插件 (如下图)。找到插件后进行安装，并重启 IDEA 使得安装生效。\n\n<div align=\"center\"> <img width=\"700px\" src=\"../pictures/idea-scala-plugin.png\"/> </div>\n\n\n\n### 2.3 创建Scala项目\n\n在 IDEA 中依次点击 **File** => **New** => **Project** 选项卡，然后选择创建 `Scala—IDEA` 工程：\n\n<div align=\"center\"> <img  width=\"700px\"   src=\"../pictures/idea-newproject-scala.png\"/> </div>\n\n\n\n### 2.4 下载Scala SDK\n\n#### 1. 方式一\n\n此时看到 `Scala SDK` 为空，依次点击 `Create` => `Download` ，选择所需的版本后，点击 `OK` 按钮进行下载，下载完成点击 `Finish` 进入工程。\n\n<div align=\"center\"> <img  width=\"700px\"  src=\"../pictures/idea-scala-select.png\"/> </div>\n\n\n\n#### 2. 方式二\n\n方式一是 Scala 官方安装指南里使用的方式，但下载速度通常比较慢，且这种安装下并没有直接提供 Scala 命令行工具。所以个人推荐到官网下载安装包进行安装，下载地址：https://www.scala-lang.org/download/\n\n这里我的系统是 Windows，下载 msi 版本的安装包后，一直点击下一步进行安装，安装完成后会自动配置好环境变量。\n\n<div align=\"center\"> <img  width=\"700px\"   src=\"../pictures/scala-other-resources.png\"/> </div>\n\n\n\n由于安装时已经自动配置好环境变量，所以 IDEA 会自动选择对应版本的 SDK。\n\n<div align=\"center\"> <img  width=\"700px\"  src=\"../pictures/idea-scala-2.1.8.png\"/> </div>\n\n\n\n### 2.5 创建Hello World\n\n在工程 `src` 目录上右击 **New** => **Scala class** 创建 `Hello.scala`。输入代码如下，完成后点击运行按钮，成功运行则代表搭建成功。\n\n<div align=\"center\"> <img  width=\"700px\"   src=\"../pictures/scala-hello-world.png\"/> </div>\n\n\n\n\n\n### 2.6 切换Scala版本\n\n在日常的开发中，由于对应软件（如 Spark）的版本切换，可能导致需要切换 Scala 的版本，则可以在 `Project Structures` 中的 `Global Libraries` 选项卡中进行切换。\n\n<div align=\"center\"> <img  width=\"700px\"  src=\"../pictures/idea-scala-change.png\"/> </div>\n\n\n\n\n\n### 2.7 使用scala命令行\n\n采用 `msi` 方式安装，程序会自动配置好环境变量。此时可以直接使用命令行工具：\n\n<div align=\"center\"> <img  width=\"700px\"  src=\"../pictures/scala-shell.png\"/> </div>\n\n\n\n## 参考资料\n\n1. Martin Odersky(著)，高宇翔 (译) . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 \n2. https://www.scala-lang.org/download/\n"
  },
  {
    "path": "大数据框架学习/Scala类和对象.md",
    "content": "# 类和对象\n\n<nav>\n<a href=\"#一初识类和对象\">一、初识类和对象</a><br/>\n<a href=\"#二类\">二、类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-成员变量可见性\">2.1 成员变量可见性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-getter和setter属性\">2.2 getter和setter属性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-BeanProperty\">2.3 @BeanProperty</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-主构造器\">2.4 主构造器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-辅助构造器\">2.5 辅助构造器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-方法传参不可变\">2.6 方法传参不可变</a><br/>\n<a href=\"#三对象\">三、对象</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-工具类单例全局静态常量拓展特质\">3.1 工具类&单例&全局静态常量&拓展特质</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-伴生对象\">3.2 伴生对象</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-实现枚举类\">3.3 实现枚举类</a><br/>\n</nav>\n\n## 一、初识类和对象\n\nScala 的类与 Java 的类具有非常多的相似性，示例如下：\n\n```scala\n// 1. 在 scala 中，类不需要用 public 声明,所有的类都具有公共的可见性\nclass Person {\n\n  // 2. 声明私有变量,用 var 修饰的变量默认拥有 getter/setter 属性\n  private var age = 0\n\n  // 3.如果声明的变量不需要进行初始赋值，此时 Scala 就无法进行类型推断，所以需要显式指明类型\n  private var name: String = _\n\n\n  // 4. 定义方法,应指明传参类型。返回值类型不是必须的，Scala 可以自动推断出来，但是为了方便调用者，建议指明\n  def growUp(step: Int): Unit = {\n    age += step\n  }\n\n  // 5.对于改值器方法 (即改变对象状态的方法),即使不需要传入参数,也建议在声明中包含 ()\n  def growUpFix(): Unit = {\n    age += 10\n  }\n\n  // 6.对于取值器方法 (即不会改变对象状态的方法),不必在声明中包含 ()\n  def currentAge: Int = {\n    age\n  }\n\n  /**\n   * 7.不建议使用 return 关键字,默认方法中最后一行代码的计算结果为返回值\n   *   如果方法很简短，甚至可以写在同一行中\n   */\n  def getName: String = name\n\n}\n\n\n// 伴生对象\nobject Person {\n\n  def main(args: Array[String]): Unit = {\n    // 8.创建类的实例\n    val counter = new Person()\n    // 9.用 var 修饰的变量默认拥有 getter/setter 属性，可以直接对其进行赋值\n    counter.age = 12\n    counter.growUp(8)\n    counter.growUpFix()\n    // 10.用 var 修饰的变量默认拥有 getter/setter 属性，可以直接对其进行取值，输出: 30\n    println(counter.age)\n    // 输出: 30\n    println(counter.currentAge)\n    // 输出: null\n    println(counter.getName)\n  }\n\n}\n```\n\n<br/>\n\n## 二、类\n\n### 2.1 成员变量可见性\n\nScala 中成员变量的可见性默认都是 public，如果想要保证其不被外部干扰，可以声明为 private，并通过 getter 和 setter 方法进行访问。\n\n### 2.2 getter和setter属性\n\ngetter 和 setter 属性与声明变量时使用的关键字有关：\n\n+ 使用 var 关键字：变量同时拥有 getter 和 setter 属性；\n+ 使用 val 关键字：变量只拥有 getter 属性；\n+ 使用 private[this]：变量既没有 getter 属性、也没有 setter 属性，只能通过内部的方法访问；\n\n需要特别说明的是：假设变量名为 age,则其对应的 get 和 set 的方法名分别叫做 ` age` 和 `age_=`。\n\n```scala\nclass Person {\n\n  private val name = \"heibaiying\"\n  private var age = 12\n  private[this] var birthday = \"2019-08-08\"\n  // birthday 只能被内部方法所访问\n  def getBirthday: String = birthday\n}\n\nobject Person {\n  def main(args: Array[String]): Unit = {\n    val person = new Person\n    person.age = 30\n    println(person.name)\n    println(person.age)\n    println(person.getBirthday)\n  }\n}\n```\n\n> 解释说明：\n>\n> 示例代码中 `person.age=30` 在执行时内部实际是调用了方法 `person.age_=(30) `，而 `person.age` 内部执行时实际是调用了 `person.age()` 方法。想要证明这一点，可以对代码进行反编译。同时为了说明成员变量可见性的问题，我们对下面这段代码进行反编译：\n>\n> ```scala\n> class Person {\n> var name = \"\"\n> private var age = \"\"\n> }\n> ```\n>\n> 依次执行下面编译命令：\n>\n> ```shell\n> > scalac Person.scala\n> > javap -private Person\n> ```\n>\n> 编译结果如下，从编译结果可以看到实际的 get 和 set 的方法名 (因为 JVM 不允许在方法名中出现＝，所以它被翻译成$eq)，同时也验证了成员变量默认的可见性为 public。\n>\n> ```java\n> Compiled from \"Person.scala\"\n> public class Person {\n> private java.lang.String name;\n> private java.lang.String age;\n>  \n> public java.lang.String name();\n> public void name_$eq(java.lang.String);\n>  \n> private java.lang.String age();\n> private void age_$eq(java.lang.String);\n>  \n> public Person();\n> }\n> ```\n\n### 2.3 @BeanProperty\n\n在上面的例子中可以看到我们是使用 `.` 来对成员变量进行访问的，如果想要额外生成和 Java 中一样的 getXXX 和 setXXX 方法，则需要使用@BeanProperty 进行注解。\n\n```scala\nclass Person {\n  @BeanProperty var name = \"\"\n}\n\nobject Person {\n  def main(args: Array[String]): Unit = {\n    val person = new Person\n    person.setName(\"heibaiying\")\n    println(person.getName)\n  }\n}\n```\n\n\n\n### 2.4 主构造器\n\n和 Java 不同的是，Scala 类的主构造器直接写在类名后面，但注意以下两点：\n\n+ 主构造器传入的参数默认就是 val 类型的，即不可变，你没有办法在内部改变传参；\n+ 写在主构造器中的代码块会在类初始化的时候被执行，功能类似于 Java 的静态代码块 `static{}`\n\n```scala\nclass Person(val name: String, val age: Int) {\n\n  println(\"功能类似于 Java 的静态代码块 static{}\")\n\n  def getDetail: String = {\n    //name=\"heibai\" 无法通过编译\n    name + \":\" + age\n  }\n}\n\nobject Person {\n  def main(args: Array[String]): Unit = {\n    val person = new Person(\"heibaiying\", 20)\n    println(person.getDetail)\n  }\n}\n\n输出：\n功能类似于 Java 的静态代码块 static{}\nheibaiying:20\n```\n\n\n\n### 2.5 辅助构造器\n\n辅助构造器有两点硬性要求：\n\n+ 辅助构造器的名称必须为 this；\n+ 每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始。\n\n```scala\nclass Person(val name: String, val age: Int) {\n\n  private var birthday = \"\"\n\n  // 1.辅助构造器的名称必须为 this\n  def this(name: String, age: Int, birthday: String) {\n    // 2.每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始\n    this(name, age)\n    this.birthday = birthday\n  }\n\n  // 3.重写 toString 方法\n  override def toString: String = name + \":\" + age + \":\" + birthday\n}\n\nobject Person {\n  def main(args: Array[String]): Unit = {\n    println(new Person(\"heibaiying\", 20, \"2019-02-21\"))\n  }\n}\n```\n\n\n\n### 2.6 方法传参不可变\n\n在 Scala 中，方法传参默认是 val 类型，即不可变，这意味着你在方法体内部不能改变传入的参数。这和 Scala 的设计理念有关，Scala 遵循函数式编程理念，强调方法不应该有副作用。\n\n```scala\nclass Person() {\n\n  def low(word: String): String = {\n    word=\"word\" // 编译无法通过\n    word.toLowerCase\n  }\n}\n```\n\n<br/>\n\n## 三、对象\n\nScala 中的 object(对象) 主要有以下几个作用：\n\n+ 因为 object 中的变量和方法都是静态的，所以可以用于存放工具类；\n+ 可以作为单例对象的容器；\n+ 可以作为类的伴生对象；\n+ 可以拓展类或特质；\n+ 可以拓展 Enumeration 来实现枚举。\n\n### 3.1 工具类&单例&全局静态常量&拓展特质\n\n这里我们创建一个对象 `Utils`,代码如下：\n\n```scala\nobject Utils {\n\n  /*\n   *1. 相当于 Java 中的静态代码块 static,会在对象初始化时候被执行\n   *   这种方式实现的单例模式是饿汉式单例,即无论你的单例对象是否被用到，\n   *   都在一开始被初始化完成\n   */\n  val person = new Person\n  \n  // 2. 全局固定常量 等价于 Java 的 public static final \n  val CONSTANT = \"固定常量\"\n\n  // 3. 全局静态方法\n  def low(word: String): String = {\n    word.toLowerCase\n  }\n}\n```\n\n其中 Person 类代码如下：\n\n```scala\nclass Person() {\n  println(\"Person 默认构造器被调用\")\n}\n```\n\n新建测试类：\n\n```scala\n// 1.ScalaApp 对象扩展自 trait App\nobject ScalaApp extends App {\n\n  // 2.验证单例\n  println(Utils.person == Utils.person)\n\n  // 3.获取全局常量\n  println(Utils.CONSTANT)\n\n  // 4.调用工具类\n  println(Utils.low(\"ABCDEFG\"))\n  \n}\n\n// 输出如下：\nPerson 默认构造器被调用\ntrue\n固定常量\nabcdefg\n```\n\n### 3.2 伴生对象\n\n在 Java 中，你通常会用到既有实例方法又有静态方法的类，在 Scala 中，可以通过类和与类同名的伴生对象来实现。类和伴生对象必须存在与同一个文件中。\n\n```scala\nclass Person() {\n\n  private val name = \"HEIBAIYING\"\n\n  def getName: String = {\n    // 调用伴生对象的方法和属性\n    Person.toLow(Person.PREFIX + name)\n  }\n}\n\n// 伴生对象\nobject Person {\n\n  val PREFIX = \"prefix-\"\n\n  def toLow(word: String): String = {\n    word.toLowerCase\n  }\n\n  def main(args: Array[String]): Unit = {\n    val person = new Person\n    // 输出 prefix-heibaiying  \n    println(person.getName)\n  }\n\n}\n```\n\n\n\n### 3.3 实现枚举类\n\nScala 中没有直接提供枚举类，需要通过扩展 `Enumeration`，并调用其中的 Value 方法对所有枚举值进行初始化来实现。\n\n```scala\nobject Color extends Enumeration {\n\n  // 1.类型别名,建议声明,在 import 时有用\n  type Color = Value\n\n  // 2.调用 Value 方法\n  val GREEN = Value\n  // 3.只传入 id\n  val RED = Value(3)\n  // 4.只传入值\n  val BULE = Value(\"blue\")\n  // 5.传入 id 和值\n  val YELLOW = Value(5, \"yellow\")\n  // 6. 不传入 id 时,id 为上一个声明变量的 id+1,值默认和变量名相同\n  val PINK = Value\n \n}\n```\n\n使用枚举类：\n\n```scala\n// 1.使用类型别名导入枚举类\nimport com.heibaiying.Color.Color\n\nobject ScalaApp extends App {\n\n  // 2.使用枚举类型,这种情况下需要导入枚举类\n  def printColor(color: Color): Unit = {\n    println(color.toString)\n  }\n\n  // 3.判断传入值和枚举值是否相等\n  println(Color.YELLOW.toString == \"yellow\")\n  // 4.遍历枚举类和值\n  for (c <- Color.values) println(c.id + \":\" + c.toString)\n}\n\n//输出\ntrue\n0:GREEN\n3:RED\n4:blue\n5:yellow\n6:PINK\n```\n\n<br/>\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n\n"
  },
  {
    "path": "大数据框架学习/Scala类型参数.md",
    "content": "# 类型参数\n\n<nav>\n<a href=\"#一泛型\">一、泛型</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-泛型类\">1.1 泛型类</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-泛型方法\">1.2 泛型方法</a><br/>\n<a href=\"#二类型限定\">二、类型限定</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-类型上界限定\">2.1 类型上界限定</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-视图界定\">2.2 视图界定 </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-类型约束\">2.3 类型约束</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-上下文界定\">2.4 上下文界定</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-ClassTag上下文界定\">2.5 ClassTag上下文界定</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-类型下界限定\">2.6 类型下界限定</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-多重界定\">2.7 多重界定</a><br/>\n<a href=\"#三Ordering--Ordered\">三、Ordering & Ordered</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-Comparable\">3.1 Comparable</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-Comparator\">3.2 Comparator</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-上下文界定的优点\">3.3 上下文界定的优点</a><br/>\n<a href=\"#四通配符\">四、通配符</a><br/>\n</nav>\n\n\n## 一、泛型\n\nScala 支持类型参数化，使得我们能够编写泛型程序。\n\n### 1.1 泛型类\n\nJava 中使用 `<>` 符号来包含定义的类型参数，Scala 则使用 `[]`。\n\n```scala\nclass Pair[T, S](val first: T, val second: S) {\n  override def toString: String = first + \":\" + second\n}\n```\n\n```scala\nobject ScalaApp extends App {\n\n  // 使用时候你直接指定参数类型，也可以不指定，由程序自动推断\n  val pair01 = new Pair(\"heibai01\", 22)\n  val pair02 = new Pair[String,Int](\"heibai02\", 33)\n\n  println(pair01)\n  println(pair02)\n}\n```\n\n### 1.2 泛型方法\n\n函数和方法也支持类型参数。\n\n```scala\nobject Utils {\n  def getHalf[T](a: Array[T]): Int = a.length / 2\n}\n```\n\n## 二、类型限定\n\n### 2.1 类型上界限定\n\nScala 和 Java 一样，对于对象之间进行大小比较，要求被比较的对象实现 `java.lang.Comparable` 接口。所以如果想对泛型进行比较，需要限定类型上界为 `java.lang.Comparable`，语法为 ` S <: T`，代表类型 S 是类型 T 的子类或其本身。示例如下：\n\n```scala\n// 使用 <: 符号，限定 T 必须是 Comparable[T]的子类型\nclass Pair[T <: Comparable[T]](val first: T, val second: T) {\n  // 返回较小的值\n  def smaller: T = if (first.compareTo(second) < 0) first else second\n}\n```\n\n```scala\n// 测试代码\nval pair = new Pair(\"abc\", \"abcd\")\nprintln(pair.smaller) // 输出 abc\n```\n\n>扩展：如果你想要在 Java 中实现类型变量限定，需要使用关键字 extends 来实现，等价的 Java 代码如下：\n>\n>```java\n>public class Pair<T extends Comparable<T>> {\n>    private T first;\n>    private T second;\n>    Pair(T first, T second) {\n>        this.first = first;\n>        this.second = second;\n>    }\n>    public T smaller() {\n>        return first.compareTo(second) < 0 ? first : second;\n>     }\n>}\n>```\n\n### 2.2 视图界定 \n\n在上面的例子中，如果你使用 Int 类型或者 Double 等类型进行测试，点击运行后，你会发现程序根本无法通过编译：\n\n```scala\nval pair1 = new Pair(10, 12)\nval pair2 = new Pair(10.0, 12.0)\n```\n\n之所以出现这样的问题，是因为 Scala 中的 Int 类并没有实现 Comparable 接口。在 Scala 中直接继承 Comparable 接口的是特质 Ordered，它在继承 compareTo 方法的基础上，额外定义了关系符方法，源码如下:\n\n```scala\n// 除了 compareTo 方法外，还提供了额外的关系符方法\ntrait Ordered[A] extends Any with java.lang.Comparable[A] {\n  def compare(that: A): Int\n  def <  (that: A): Boolean = (this compare that) <  0\n  def >  (that: A): Boolean = (this compare that) >  0\n  def <= (that: A): Boolean = (this compare that) <= 0\n  def >= (that: A): Boolean = (this compare that) >= 0\n  def compareTo(that: A): Int = compare(that)\n}\n```\n\n之所以在日常的编程中之所以你能够执行 `3>2` 这样的判断操作，是因为程序执行了定义在 `Predef` 中的隐式转换方法 `intWrapper(x: Int) `，将 Int 类型转换为 RichInt 类型，而 RichInt 间接混入了 Ordered 特质，所以能够进行比较。\n\n```scala\n// Predef.scala\n@inline implicit def intWrapper(x: Int)   = new runtime.RichInt(x)\n```\n\n<div align=\"center\"> <img src=\"../pictures/scala-richInt.png\"/> </div>\n\n要想解决传入数值无法进行比较的问题，可以使用视图界定。语法为 `T <% U`，代表 T 能够通过隐式转换转为 U，即允许 Int 型参数在无法进行比较的时候转换为 RichInt 类型。示例如下：\n\n```scala\n// 视图界定符号 <%\nclass Pair[T <% Comparable[T]](val first: T, val second: T) {\n  // 返回较小的值\n  def smaller: T = if (first.compareTo(second) < 0) first else second\n}\n```\n\n> 注：由于直接继承 Java 中 Comparable 接口的是特质 Ordered，所以如下的视图界定和上面是等效的：\n>\n> ```scala\n> // 隐式转换为 Ordered[T]\n>    class Pair[T <% Ordered[T]](val first: T, val second: T) {\n> def smaller: T = if (first.compareTo(second) < 0) first else second\n>    }\n> ```\n\n### 2.3 类型约束\n\n如果你用的 Scala 是 2.11+，会发现视图界定已被标识为废弃。官方推荐使用类型约束 (type constraint) 来实现同样的功能，其本质是使用隐式参数进行隐式转换，示例如下：\n\n```scala\n // 1.使用隐式参数隐式转换为 Comparable[T]\nclass Pair[T](val first: T, val second: T)(implicit ev: T => Comparable[T]) \n  def smaller: T = if (first.compareTo(second) < 0) first else second\n}\n\n// 2.由于直接继承 Java 中 Comparable 接口的是特质 Ordered，所以也可以隐式转换为 Ordered[T]\nclass Pair[T](val first: T, val second: T)(implicit ev: T => Ordered[T]) {\n  def smaller: T = if (first.compareTo(second) < 0) first else second\n}\n```\n\n当然，隐式参数转换也可以运用在具体的方法上：\n\n```scala\nobject PairUtils{\n  def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b\n}\n```\n\n### 2.4 上下文界定\n\n上下文界定的形式为 `T:M`，其中 M 是一个泛型，它要求必须存在一个类型为 M[T]的隐式值，当你声明一个带隐式参数的方法时，需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写：\n\n```scala\nclass Pair[T](val first: T, val second: T) {\n  // 请注意 这个地方用的是 Ordering[T]，而上面视图界定和类型约束，用的是 Ordered[T]，两者的区别会在后文给出解释\n  def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second \n}\n\n// 测试\nval pair= new Pair(88, 66)\nprintln(pair.smaller)  //输出：66\n```\n\n在上面的示例中，我们无需手动添加隐式默认值就可以完成转换，这是因为 Scala 自动引入了 Ordering[Int]这个隐式值。为了更好的说明上下文界定，下面给出一个自定义类型的比较示例：\n\n```scala\n// 1.定义一个人员类\nclass Person(val name: String, val age: Int) {\n  override def toString: String = name + \":\" + age\n}\n\n// 2.继承 Ordering[T],实现自定义比较器,按照自己的规则重写比较方法\nclass PersonOrdering extends Ordering[Person] {\n  override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1\n}\n\nclass Pair[T](val first: T, val second: T) {\n  def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second\n}\n\n\nobject ScalaApp extends App {\n\n  val pair = new Pair(new Person(\"hei\", 88), new Person(\"bai\", 66))\n  // 3.定义隐式默认值,如果不定义,则下一行代码无法通过编译\n  implicit val ImpPersonOrdering = new PersonOrdering\n  println(pair.smaller) //输出： bai:66\n}\n```\n\n### 2.5 ClassTag上下文界定\n\n这里先看一个例子：下面这段代码，没有任何语法错误，但是在运行时会抛出异常：`Error: cannot find class tag for element type T`, 这是由于 Scala 和 Java 一样，都存在类型擦除，即**泛型信息只存在于代码编译阶段，在进入 JVM 之前，与泛型相关的信息会被擦除掉**。对于下面的代码，在运行阶段创建 Array 时，你必须明确指明其类型，但是此时泛型信息已经被擦除，导致出现找不到类型的异常。\n\n```scala\nobject ScalaApp extends App {\n  def makePair[T](first: T, second: T) = {\n    // 创建以一个数组 并赋值\n    val r = new Array[T](2); r(0) = first; r(1) = second; r\n  }\n}\n```\n\nScala 针对这个问题，提供了 ClassTag 上下文界定，即把泛型的信息存储在 ClassTag 中，这样在运行阶段需要时，只需要从 ClassTag 中进行获取即可。其语法为 `T : ClassTag`，示例如下：\n\n```scala\nimport scala.reflect._\nobject ScalaApp extends App {\n  def makePair[T : ClassTag](first: T, second: T) = {\n    val r = new Array[T](2); r(0) = first; r(1) = second; r\n  }\n}\n```\n\n### 2.6 类型下界限定\n\n2.1 小节介绍了类型上界的限定，Scala 同时也支持下界的限定，语法为：`U >: T`，即 U 必须是类型 T 的超类或本身。\n\n```scala\n// 首席执行官\nclass CEO\n\n// 部门经理\nclass Manager extends CEO\n\n// 本公司普通员工\nclass Employee extends Manager\n\n// 其他公司人员\nclass OtherCompany\n\nobject ScalaApp extends App {\n\n  // 限定：只有本公司部门经理以上人员才能获取权限\n  def Check[T >: Manager](t: T): T = {\n    println(\"获得审核权限\")\n    t\n  }\n\n  // 错误写法: 省略泛型参数后,以下所有人都能获得权限,显然这是不正确的\n  Check(new CEO)\n  Check(new Manager)\n  Check(new Employee)\n  Check(new OtherCompany)\n\n\n  // 正确写法,传入泛型参数\n  Check[CEO](new CEO)\n  Check[Manager](new Manager)\n  /*\n   * 以下两条语句无法通过编译,异常信息为: \n   * do not conform to method Check's type parameter bounds(不符合方法 Check 的类型参数边界)\n   * 这种情况就完成了下界限制，即只有本公司经理及以上的人员才能获得审核权限\n   */\n  Check[Employee](new Employee)\n  Check[OtherCompany](new OtherCompany)\n}\n```\n\n### 2.7 多重界定\n\n+ 类型变量可以同时有上界和下界。 写法为 ：`T > : Lower <: Upper`；\n\n+ 不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质，写法为 :\n\n  `T < : Comparable[T] with Serializable with Cloneable`；\n\n+ 你可以有多个上下文界定，写法为 `T : Ordering : ClassTag` 。\n\n\n\n## 三、Ordering & Ordered\n\n上文中使用到 Ordering 和 Ordered 特质，它们最主要的区别在于分别继承自不同的 Java 接口：Comparable 和 Comparator：\n\n+ **Comparable**：可以理解为内置的比较器，实现此接口的对象可以与自身进行比较；\n+ **Comparator**：可以理解为外置的比较器；当对象自身并没有定义比较规则的时候，可以传入外部比较器进行比较。\n\n为什么 Java 中要同时给出这两个比较接口，这是因为你要比较的对象不一定实现了 Comparable 接口，而你又想对其进行比较，这时候当然你可以修改代码实现 Comparable，但是如果这个类你无法修改 (如源码中的类)，这时候就可以使用外置的比较器。同样的问题在 Scala 中当然也会出现，所以 Scala 分别使用了 Ordering 和 Ordered 来继承它们。\n\n<div align=\"center\"> <img src=\"../pictures/scala-ordered-ordering.png\"/> </div>\n\n\n\n下面分别给出 Java 中 Comparable 和 Comparator 接口的使用示例：\n\n### 3.1 Comparable\n\n```java\nimport java.util.Arrays;\n// 实现 Comparable 接口\npublic class Person implements Comparable<Person> {\n\n    private String name;\n    private int age;\n\n    Person(String name,int age) {this.name=name;this.age=age;}\n    @Override\n    public String toString() { return name+\":\"+age; }\n\n    // 核心的方法是重写比较规则，按照年龄进行排序\n    @Override\n    public int compareTo(Person person) {\n        return this.age - person.age;\n    }\n\n    public static void main(String[] args) {\n        Person[] peoples= {new Person(\"hei\", 66), new Person(\"bai\", 55), new Person(\"ying\", 77)};\n        Arrays.sort(peoples);\n        Arrays.stream(peoples).forEach(System.out::println);\n    }\n}\n\n输出：\nbai:55\nhei:66\nying:77\n```\n\n### 3.2 Comparator\n\n```java\nimport java.util.Arrays;\nimport java.util.Comparator;\n\npublic class Person {\n\n    private String name;\n    private int age;\n\n    Person(String name,int age) {this.name=name;this.age=age;}\n    @Override\n    public String toString() { return name+\":\"+age; }\n\n    public static void main(String[] args) {\n        Person[] peoples= {new Person(\"hei\", 66), new Person(\"bai\", 55), new Person(\"ying\", 77)};\n        // 这里为了直观直接使用匿名内部类,实现 Comparator 接口\n        //如果是 Java8 你也可以写成 Arrays.sort(peoples, Comparator.comparingInt(o -> o.age));\n        Arrays.sort(peoples, new Comparator<Person>() {\n            @Override\n            public int compare(Person o1, Person o2) {\n                return o1.age-o2.age;\n            }\n        });\n        Arrays.stream(peoples).forEach(System.out::println);\n    }\n}\n```\n\n使用外置比较器还有一个好处，就是你可以随时定义其排序规则：\n\n```scala\n// 按照年龄大小排序\nArrays.sort(peoples, Comparator.comparingInt(o -> o.age));\nArrays.stream(peoples).forEach(System.out::println);\n// 按照名字长度倒序排列\nArrays.sort(peoples, Comparator.comparingInt(o -> -o.name.length()));\nArrays.stream(peoples).forEach(System.out::println);\n```\n\n### 3.3 上下文界定的优点\n\n这里再次给出上下文界定中的示例代码作为回顾：\n\n```scala\n// 1.定义一个人员类\nclass Person(val name: String, val age: Int) {\n  override def toString: String = name + \":\" + age\n}\n\n// 2.继承 Ordering[T],实现自定义比较器,这个比较器就是一个外置比较器\nclass PersonOrdering extends Ordering[Person] {\n  override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1\n}\n\nclass Pair[T](val first: T, val second: T) {\n  def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second\n}\n\n\nobject ScalaApp extends App {\n\n  val pair = new Pair(new Person(\"hei\", 88), new Person(\"bai\", 66))\n  // 3.在当前上下文定义隐式默认值,这就相当于传入了外置比较器\n  implicit val ImpPersonOrdering = new PersonOrdering\n  println(pair.smaller) //输出： bai:66\n}\n```\n\n使用上下文界定和 Ordering 带来的好处是：传入 `Pair` 中的参数不一定需要可比较，只要在比较时传入外置比较器即可。\n\n需要注意的是由于隐式默认值二义性的限制，你不能像上面 Java 代码一样，在同一个上下文作用域中传入两个外置比较器，即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值，即使用不同的外置比较器。\n\n```scala\nimplicit val ImpPersonOrdering = new PersonOrdering\nprintln(pair.smaller) \nimplicit val ImpPersonOrdering2 = new PersonOrdering\nprintln(pair.smaller)\n```\n\n\n\n## 四、通配符\n\n在实际编码中，通常需要把泛型限定在某个范围内，比如限定为某个类及其子类。因此 Scala 和 Java 一样引入了通配符这个概念，用于限定泛型的范围。不同的是 Java 使用 `?` 表示通配符，Scala 使用 `_` 表示通配符。\n\n```scala\nclass Ceo(val name: String) {\n  override def toString: String = name\n}\n\nclass Manager(name: String) extends Ceo(name)\n\nclass Employee(name: String) extends Manager(name)\n\nclass Pair[T](val first: T, val second: T) {\n  override def toString: String = \"first:\" + first + \", second: \" + second\n}\n\nobject ScalaApp extends App {\n  // 限定部门经理及以下的人才可以组队\n  def makePair(p: Pair[_ <: Manager]): Unit = {println(p)}\n  makePair(new Pair(new Employee(\"heibai\"), new Manager(\"ying\")))\n}\n```\n\n目前 Scala 中的通配符在某些复杂情况下还不完善，如下面的语句在 Scala 2.12 中并不能通过编译：\n\n```scala\ndef min[T <: Comparable[_ >: T]](p: Pair[T]) ={}\n```\n\n可以使用以下语法代替：\n\n```scala\ntype SuperComparable[T] = Comparable[_ >: T]\ndef min[T <: SuperComparable[T]](p: Pair[T]) = {}\n```\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n\n"
  },
  {
    "path": "大数据框架学习/Scala继承和特质.md",
    "content": "# 继承和特质\n\n<nav>\n<a href=\"#一继承\">一、继承</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-Scala中的继承结构\">1.1 Scala中的继承结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-extends--override\">1.2 extends & override</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-调用超类构造器\">1.3 调用超类构造器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-类型检查和转换\">1.4 类型检查和转换</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-构造顺序和提前定义\">1.5 构造顺序和提前定义</a><br/>\n<a href=\"#二抽象类\">二、抽象类</a><br/>\n<a href=\"#三特质\">三、特质</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-trait--with\">3.1 trait & with</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-特质中的字段\">3.2 特质中的字段</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-带有特质的对象\">3.3 带有特质的对象</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-特质构造顺序\">3.4 特质构造顺序</a><br/>\n</nav>\n\n\n## 一、继承\n\n### 1.1 Scala中的继承结构\n\nScala 中继承关系如下图：\n\n+ Any 是整个继承关系的根节点；\n+ AnyRef 包含 Scala Classes 和 Java Classes，等价于 Java 中的 java.lang.Object；\n+ AnyVal 是所有值类型的一个标记；\n+ Null 是所有引用类型的子类型，唯一实例是 null，可以将 null 赋值给除了值类型外的所有类型的变量;\n+ Nothing 是所有类型的子类型。\n\n<div align=\"center\"> <img src=\"../pictures/scala继承层次.png\"/> </div>\n\n### 1.2 extends & override\n\nScala 的集成机制和 Java 有很多相似之处，比如都使用 `extends` 关键字表示继承，都使用 `override` 关键字表示重写父类的方法或成员变量。示例如下：\n\n```scala\n//父类\nclass Person {\n\n  var name = \"\"\n  // 1.不加任何修饰词,默认为 public,能被子类和外部访问\n  var age = 0\n  // 2.使用 protected 修饰的变量能子类访问，但是不能被外部访问\n  protected var birthday = \"\"\n  // 3.使用 private 修饰的变量不能被子类和外部访问\n  private var sex = \"\"\n    \n  def setSex(sex: String): Unit = {\n    this.sex = sex\n  }\n  // 4.重写父类的方法建议使用 override 关键字修饰\n  override def toString: String = name + \":\" + age + \":\" + birthday + \":\" + sex\n\n}\n```\n\n使用 `extends` 关键字实现继承：\n\n```scala\n// 1.使用 extends 关键字实现继承\nclass Employee extends Person {\n\n  override def toString: String = \"Employee~\" + super.toString\n\n  // 2.使用 public 或 protected 关键字修饰的变量能被子类访问\n  def setBirthday(date: String): Unit = {\n    birthday = date\n  }\n\n}\n```\n\n测试继承：\n\n```scala\n\nobject ScalaApp extends App {\n\n  val employee = new Employee\n\n  employee.name = \"heibaiying\"\n  employee.age = 20\n  employee.setBirthday(\"2019-03-05\")\n  employee.setSex(\"男\")\n\n  println(employee)\n}\n\n// 输出： Employee~heibaiying:20:2019-03-05:男\n```\n\n### 1.3 调用超类构造器\n\n在 Scala 的类中，每个辅助构造器都必须首先调用其他构造器或主构造器，这样就导致了子类的辅助构造器永远无法直接调用超类的构造器，只有主构造器才能调用超类的构造器。所以想要调用超类的构造器，代码示例如下：\n\n```scala\nclass Employee(name:String,age:Int,salary:Double) extends Person(name:String,age:Int) {\n    .....\n}\n```\n\n### 1.4 类型检查和转换\n\n想要实现类检查可以使用 `isInstanceOf`，判断一个实例是否来源于某个类或者其子类，如果是，则可以使用 `asInstanceOf` 进行强制类型转换。\n\n```scala\nobject ScalaApp extends App {\n\n  val employee = new Employee\n  val person = new Person\n\n  // 1. 判断一个实例是否来源于某个类或者其子类 输出 true \n  println(employee.isInstanceOf[Person])\n  println(person.isInstanceOf[Person])\n\n  // 2. 强制类型转换\n  var p: Person = employee.asInstanceOf[Person]\n\n  // 3. 判断一个实例是否来源于某个类 (而不是其子类)\n  println(employee.getClass == classOf[Employee])\n\n}\n```\n\n### 1.5 构造顺序和提前定义\n\n#### **1. 构造顺序**\n\n在 Scala 中还有一个需要注意的问题，如果你在子类中重写父类的 val 变量，并且超类的构造器中使用了该变量，那么可能会产生不可预期的错误。下面给出一个示例：\n\n```scala\n// 父类\nclass Person {\n  println(\"父类的默认构造器\")\n  val range: Int = 10\n  val array: Array[Int] = new Array[Int](range)\n}\n\n//子类\nclass Employee extends Person {\n  println(\"子类的默认构造器\")\n  override val range = 2\n}\n\n//测试\nobject ScalaApp extends App {\n  val employee = new Employee\n  println(employee.array.mkString(\"(\", \",\", \")\"))\n\n}\n```\n\n这里初始化 array 用到了变量 range，这里你会发现实际上 array 既不会被初始化 Array(10)，也不会被初始化为 Array(2)，实际的输出应该如下：\n\n```properties\n父类的默认构造器\n子类的默认构造器\n()\n```\n\n可以看到 array 被初始化为 Array(0)，主要原因在于父类构造器的执行顺序先于子类构造器，这里给出实际的执行步骤：\n\n1. 父类的构造器被调用，执行 `new Array[Int](range)` 语句;\n2. 这里想要得到 range 的值，会去调用子类 range() 方法，因为 `override val` 重写变量的同时也重写了其 get 方法；\n3. 调用子类的 range() 方法，自然也是返回子类的 range 值，但是由于子类的构造器还没有执行，这也就意味着对 range 赋值的 `range = 2` 语句还没有被执行，所以自然返回 range 的默认值，也就是 0。\n\n这里可能比较疑惑的是为什么 `val range = 2` 没有被执行，却能使用 range 变量，这里因为在虚拟机层面，是先对成员变量先分配存储空间并赋给默认值，之后才赋予给定的值。想要证明这一点其实也比较简单，代码如下:\n\n```scala\nclass Person {\n  // val range: Int = 10 正常代码 array 为 Array(10)\n  val array: Array[Int] = new Array[Int](range)\n  val range: Int = 10  //如果把变量的声明放在使用之后，此时数据 array 为 array(0)\n}\n\nobject Person {\n  def main(args: Array[String]): Unit = {\n    val person = new Person\n    println(person.array.mkString(\"(\", \",\", \")\"))\n  }\n}\n```\n\n#### **2. 提前定义**\n\n想要解决上面的问题，有以下几种方法：\n\n(1) . 将变量用 final 修饰，代表不允许被子类重写，即 `final val range: Int = 10 `；\n\n(2) . 将变量使用 lazy 修饰，代表懒加载，即只有当你实际使用到 array 时候，才去进行初始化；\n\n```scala\nlazy val array: Array[Int] = new Array[Int](range)\n```\n\n(3) . 采用提前定义，代码如下，代表 range 的定义优先于超类构造器。\n\n```scala\nclass Employee extends {\n  //这里不能定义其他方法\n  override val range = 2\n} with Person {\n  // 定义其他变量或者方法\n  def pr(): Unit = {println(\"Employee\")}\n}\n```\n\n但是这种语法也有其限制：你只能在上面代码块中重写已有的变量，而不能定义新的变量和方法，定义新的变量和方法只能写在下面代码块中。\n\n>**注意事项**：类的继承和下文特质 (trait) 的继承都存在这个问题，也同样可以通过提前定义来解决。虽然如此，但还是建议合理设计以规避该类问题。\n\n<br/>\n\n## 二、抽象类\n\nScala 中允许使用 `abstract` 定义抽象类，并且通过 `extends` 关键字继承它。\n\n定义抽象类：\n\n```scala\nabstract class Person {\n  // 1.定义字段\n  var name: String\n  val age: Int\n\n  // 2.定义抽象方法\n  def geDetail: String\n\n  // 3. scala 的抽象类允许定义具体方法\n  def print(): Unit = {\n    println(\"抽象类中的默认方法\")\n  }\n}\n```\n\n继承抽象类：\n\n```scala\nclass Employee extends Person {\n  // 覆盖抽象类中变量\n  override var name: String = \"employee\"\n  override val age: Int = 12\n\n  // 覆盖抽象方法\n  def geDetail: String = name + \":\" + age\n}\n\n```\n\n<br/>\n\n## 三、特质\n\n### 3.1 trait & with\n\nScala 中没有 interface 这个关键字，想要实现类似的功能，可以使用特质 (trait)。trait 等价于 Java 8 中的接口，因为 trait 中既能定义抽象方法，也能定义具体方法，这和 Java 8 中的接口是类似的。\n\n```scala\n// 1.特质使用 trait 关键字修饰\ntrait Logger {\n\n  // 2.定义抽象方法\n  def log(msg: String)\n\n  // 3.定义具体方法\n  def logInfo(msg: String): Unit = {\n    println(\"INFO:\" + msg)\n  }\n}\n```\n\n想要使用特质，需要使用 `extends` 关键字，而不是 `implements` 关键字，如果想要添加多个特质，可以使用 `with` 关键字。\n\n```scala\n// 1.使用 extends 关键字,而不是 implements,如果想要添加多个特质，可以使用 with 关键字\nclass ConsoleLogger extends Logger with Serializable with Cloneable {\n\n  // 2. 实现特质中的抽象方法\n  def log(msg: String): Unit = {\n    println(\"CONSOLE:\" + msg)\n  }\n}\n```\n\n### 3.2 特质中的字段\n\n和方法一样，特质中的字段可以是抽象的，也可以是具体的：\n\n+ 如果是抽象字段，则混入特质的类需要重写覆盖该字段；\n+ 如果是具体字段，则混入特质的类获得该字段，但是并非是通过继承关系得到，而是在编译时候，简单将该字段加入到子类。\n\n```scala\ntrait Logger {\n  // 抽象字段\n  var LogLevel:String\n  // 具体字段\n  var LogType = \"FILE\"\n}\n```\n\n覆盖抽象字段：\n\n```scala\nclass InfoLogger extends Logger {\n  // 覆盖抽象字段\n  override var LogLevel: String = \"INFO\"\n}\n```\n\n### 3.3 带有特质的对象\n\nScala 支持在类定义的时混入 ` 父类 trait`，而在类实例化为具体对象的时候指明其实际使用的 ` 子类 trait`。示例如下：\n\n<div align=\"center\"> <img  src=\"../pictures/scala带有特质的对象.png\"/> </div>\n\ntrait Logger：\n\n```scala\n// 父类\ntrait Logger {\n  // 定义空方法 日志打印\n  def log(msg: String) {}\n}\n```\n\ntrait ErrorLogger：\n\n```scala\n// 错误日志打印，继承自 Logger\ntrait ErrorLogger extends Logger {\n  // 覆盖空方法\n  override def log(msg: String): Unit = {\n    println(\"Error:\" + msg)\n  }\n}\n```\n\ntrait InfoLogger：\n\n```scala\n// 通知日志打印，继承自 Logger\ntrait InfoLogger extends Logger {\n\n  // 覆盖空方法\n  override def log(msg: String): Unit = {\n    println(\"INFO:\" + msg)\n  }\n}\n```\n\n具体的使用类：\n\n```scala\n// 混入 trait Logger\nclass Person extends Logger {\n  // 调用定义的抽象方法\n  def printDetail(detail: String): Unit = {\n    log(detail)\n  }\n}\n```\n\n这里通过 main 方法来测试：\n\n```scala\nobject ScalaApp extends App {\n\n  // 使用 with 指明需要具体使用的 trait  \n  val person01 = new Person with InfoLogger\n  val person02 = new Person with ErrorLogger\n  val person03 = new  Person with InfoLogger with ErrorLogger\n  person01.log(\"scala\")  //输出 INFO:scala\n  person02.log(\"scala\")  //输出 Error:scala\n  person03.log(\"scala\")  //输出 Error:scala\n\n}\n```\n\n这里前面两个输出比较明显，因为只指明了一个具体的 `trait`，这里需要说明的是第三个输出，**因为 trait 的调用是由右到左开始生效的**，所以这里打印出 `Error:scala`。\n\n### 3.4 特质构造顺序\n\n`trait` 有默认的无参构造器，但是不支持有参构造器。一个类混入多个特质后初始化顺序应该如下：\n\n```scala\n// 示例\nclass Employee extends Person with InfoLogger with ErrorLogger {...}\n```\n\n1. 超类首先被构造，即 Person 的构造器首先被执行；\n2. 特质的构造器在超类构造器之前，在类构造器之后；特质由左到右被构造；每个特质中，父特质首先被构造；\n   + Logger 构造器执行（Logger 是 InfoLogger 的父类）；\n   + InfoLogger 构造器执行；\n   + ErrorLogger 构造器执行;\n3. 所有超类和特质构造完毕，子类才会被构造。\n\n<br/>\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Scala隐式转换和隐式参数.md",
    "content": "# 隐式转换和隐式参数\n\n<nav>\n<a href=\"#一隐式转换\">一、隐式转换</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-使用隐式转换\">1.1 使用隐式转换</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-隐式转换规则\">1.2 隐式转换规则</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-引入隐式转换\">1.3 引入隐式转换</a><br/>\n<a href=\"#二隐式参数\">二、隐式参数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-使用隐式参数\">2.1 使用隐式参数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-引入隐式参数\">2.2 引入隐式参数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-利用隐式参数进行隐式转换\">2.3 利用隐式参数进行隐式转换</a><br/>\n</nav>\n\n\n## 一、隐式转换\n\n### 1.1 使用隐式转换\n\n隐式转换指的是以 `implicit` 关键字声明带有单个参数的转换函数，它将值从一种类型转换为另一种类型，以便使用之前类型所没有的功能。示例如下：\n\n```scala\n// 普通人\nclass Person(val name: String)\n\n// 雷神\nclass Thor(val name: String) {\n  // 正常情况下只有雷神才能举起雷神之锤\n  def hammer(): Unit = {\n    println(name + \"举起雷神之锤\")\n  }\n}\n\nobject Thor extends App {\n  // 定义隐式转换方法 将普通人转换为雷神 通常建议方法名使用 source2Target,即：被转换对象 To 转换对象\n  implicit def person2Thor(p: Person): Thor = new Thor(p.name)\n  // 这样普通人也能举起雷神之锤\n  new Person(\"普通人\").hammer()\n}\n\n输出： 普通人举起雷神之锤\n```\n\n\n\n### 1.2 隐式转换规则\n\n并不是你使用 `implicit` 转换后，隐式转换就一定会发生，比如上面如果不调用 `hammer()` 方法的时候，普通人就还是普通人。通常程序会在以下情况下尝试执行隐式转换：\n\n+ 当对象访问一个不存在的成员时，即调用的方法不存在或者访问的成员变量不存在；\n+ 当对象调用某个方法，该方法存在，但是方法的声明参数与传入参数不匹配时。\n\n而在以下三种情况下编译器不会尝试执行隐式转换：\n\n+ 如果代码能够在不使用隐式转换的前提下通过编译，则不会使用隐式转换；\n+ 编译器不会尝试同时执行多个转换，比如 `convert1(convert2(a))*b`；\n+ 转换存在二义性，也不会发生转换。\n\n这里首先解释一下二义性，上面的代码进行如下修改，由于两个隐式转换都是生效的，所以就存在了二义性：\n\n```scala\n//两个隐式转换都是有效的\nimplicit def person2Thor(p: Person): Thor = new Thor(p.name)\nimplicit def person2Thor2(p: Person): Thor = new Thor(p.name)\n// 此时下面这段语句无法通过编译\nnew Person(\"普通人\").hammer()\n```\n\n其次再解释一下多个转换的问题：\n\n```scala\nclass ClassA {\n  override def toString = \"This is Class A\"\n}\n\nclass ClassB {\n  override def toString = \"This is Class B\"\n  def printB(b: ClassB): Unit = println(b)\n}\n\nclass ClassC\n\nclass ClassD\n\nobject ImplicitTest extends App {\n  implicit def A2B(a: ClassA): ClassB = {\n    println(\"A2B\")\n    new ClassB\n  }\n\n  implicit def C2B(c: ClassC): ClassB = {\n    println(\"C2B\")\n    new ClassB\n  }\n\n  implicit def D2C(d: ClassD): ClassC = {\n    println(\"D2C\")\n    new ClassC\n  }\n\n  // 这行代码无法通过编译，因为要调用到 printB 方法，需要执行两次转换 C2B(D2C(ClassD))\n  new ClassD().printB(new ClassA)\n    \n  /*\n   *  下面的这一行代码虽然也进行了两次隐式转换，但是两次的转换对象并不是一个对象,所以它是生效的:\n   *  转换流程如下:\n   *  1. ClassC 中并没有 printB 方法,因此隐式转换为 ClassB,然后调用 printB 方法;\n   *  2. 但是 printB 参数类型为 ClassB,然而传入的参数类型是 ClassA,所以需要将参数 ClassA 转换为 ClassB,这是第二次;\n   *  即: C2B(ClassC) -> ClassB.printB(ClassA) -> ClassB.printB(A2B(ClassA)) -> ClassB.printB(ClassB)\n   *  转换过程 1 的对象是 ClassC,而转换过程 2 的转换对象是 ClassA,所以虽然是一行代码两次转换，但是仍然是有效转换\n   */\n  new ClassC().printB(new ClassA)\n}\n\n// 输出：\nC2B\nA2B\nThis is Class B\n```\n\n\n\n### 1.3 引入隐式转换\n\n隐式转换的可以定义在以下三个地方：\n\n+ 定义在原类型的伴生对象中；\n+ 直接定义在执行代码的上下文作用域中；\n+ 统一定义在一个文件中，在使用时候导入。\n\n上面我们使用的方法相当于直接定义在执行代码的作用域中，下面分别给出其他两种定义的代码示例：\n\n**定义在原类型的伴生对象中**：\n\n```scala\nclass Person(val name: String)\n// 在伴生对象中定义隐式转换函数\nobject Person{\n  implicit def person2Thor(p: Person): Thor = new Thor(p.name)\n}\n```\n\n```scala\nclass Thor(val name: String) {\n  def hammer(): Unit = {\n    println(name + \"举起雷神之锤\")\n  }\n}\n```\n\n```scala\n// 使用示例\nobject ScalaApp extends App {\n  new Person(\"普通人\").hammer()\n}\n```\n\n**定义在一个公共的对象中**：\n\n```scala\nobject Convert {\n  implicit def person2Thor(p: Person): Thor = new Thor(p.name)\n}\n```\n\n```scala\n// 导入 Convert 下所有的隐式转换函数\nimport com.heibaiying.Convert._\n\nobject ScalaApp extends App {\n  new Person(\"普通人\").hammer()\n}\n```\n\n> 注：Scala 自身的隐式转换函数大部分定义在 `Predef.scala` 中，你可以打开源文件查看，也可以在 Scala 交互式命令行中采用 `:implicit -v` 查看全部隐式转换函数。\n\n<br/>\n\n## 二、隐式参数\n\n### 2.1 使用隐式参数\n\n在定义函数或方法时可以使用标记为 `implicit` 的参数，这种情况下，编译器将会查找默认值，提供给函数调用。\n\n```scala\n// 定义分隔符类\nclass Delimiters(val left: String, val right: String)\n\nobject ScalaApp extends App {\n  \n    // 进行格式化输出\n  def formatted(context: String)(implicit deli: Delimiters): Unit = {\n    println(deli.left + context + deli.right)\n  }\n    \n  // 定义一个隐式默认值 使用左右中括号作为分隔符\n  implicit val bracket = new Delimiters(\"(\", \")\")\n  formatted(\"this is context\") // 输出: (this is context)\n}\n```\n\n关于隐式参数，有两点需要注意：\n\n1.我们上面定义 `formatted` 函数的时候使用了柯里化，如果你不使用柯里化表达式，按照通常习惯只有下面两种写法：\n\n```scala\n// 这种写法没有语法错误，但是无法通过编译\ndef formatted(implicit context: String, deli: Delimiters): Unit = {\n  println(deli.left + context + deli.right)\n} \n// 不存在这种写法，IDEA 直接会直接提示语法错误\ndef formatted( context: String,  implicit deli: Delimiters): Unit = {\n  println(deli.left + context + deli.right)\n} \n```\n\n上面第一种写法编译的时候会出现下面所示 `error` 信息,从中也可以看出 `implicit` 是作用于参数列表中每个参数的，这显然不是我们想要到达的效果，所以上面的写法采用了柯里化。\n\n```\nnot enough arguments for method formatted: \n(implicit context: String, implicit deli: com.heibaiying.Delimiters)\n```\n\n2.第二个问题和隐式函数一样，隐式默认值不能存在二义性，否则无法通过编译，示例如下：\n\n```scala\nimplicit val bracket = new Delimiters(\"(\", \")\")\nimplicit val brace = new Delimiters(\"{\", \"}\")\nformatted(\"this is context\")\n```\n\n上面代码无法通过编译，出现错误提示 `ambiguous implicit values`，即隐式值存在冲突。\n\n\n\n### 2.2 引入隐式参数\n\n引入隐式参数和引入隐式转换函数方法是一样的，有以下三种方式：\n\n- 定义在隐式参数对应类的伴生对象中；\n- 直接定义在执行代码的上下文作用域中；\n- 统一定义在一个文件中，在使用时候导入。\n\n我们上面示例程序相当于直接定义执行代码的上下文作用域中，下面给出其他两种方式的示例：\n\n**定义在隐式参数对应类的伴生对象中**；\n\n```scala\nclass Delimiters(val left: String, val right: String)\n\nobject Delimiters {\n  implicit val bracket = new Delimiters(\"(\", \")\")\n}\n```\n\n```scala\n// 此时执行代码的上下文中不用定义\nobject ScalaApp extends App {\n\n  def formatted(context: String)(implicit deli: Delimiters): Unit = {\n    println(deli.left + context + deli.right)\n  }\n  formatted(\"this is context\") \n}\n```\n\n**统一定义在一个文件中，在使用时候导入**：\n\n```scala\nobject Convert {\n  implicit val bracket = new Delimiters(\"(\", \")\")\n}\n```\n\n```scala\n// 在使用的时候导入\nimport com.heibaiying.Convert.bracket\n\nobject ScalaApp extends App {\n  def formatted(context: String)(implicit deli: Delimiters): Unit = {\n    println(deli.left + context + deli.right)\n  }\n  formatted(\"this is context\") // 输出: (this is context)\n}\n```\n\n\n\n### 2.3 利用隐式参数进行隐式转换\n\n```scala\ndef smaller[T] (a: T, b: T) = if (a < b) a else b\n```\n\n在 Scala 中如果定义了一个如上所示的比较对象大小的泛型方法，你会发现无法通过编译。对于对象之间进行大小比较，Scala 和 Java 一样，都要求被比较的对象需要实现 java.lang.Comparable 接口。在 Scala 中，直接继承 Java 中 Comparable 接口的是特质 Ordered，它在继承 compareTo 方法的基础上，额外定义了关系符方法，源码如下:\n\n```scala\ntrait Ordered[A] extends Any with java.lang.Comparable[A] {\n  def compare(that: A): Int\n  def <  (that: A): Boolean = (this compare that) <  0\n  def >  (that: A): Boolean = (this compare that) >  0\n  def <= (that: A): Boolean = (this compare that) <= 0\n  def >= (that: A): Boolean = (this compare that) >= 0\n  def compareTo(that: A): Int = compare(that)\n}\n```\n\n所以要想在泛型中解决这个问题，有两种方法：\n\n#### 1. 使用视图界定\n\n```scala\nobject Pair extends App {\n\n // 视图界定\n  def smaller[T<% Ordered[T]](a: T, b: T) = if (a < b) a else b\n \n  println(smaller(1,2)) //输出 1\n}\n```\n\n视图限定限制了 T 可以通过隐式转换 `Ordered[T]`，即对象一定可以进行大小比较。在上面的代码中 `smaller(1,2)` 中参数 `1` 和 `2` 实际上是通过定义在 `Predef` 中的隐式转换方法 `intWrapper` 转换为 `RichInt`。\n\n```scala\n// Predef.scala\n@inline implicit def intWrapper(x: Int)   = new runtime.RichInt(x)\n```\n\n为什么要这么麻烦执行隐式转换，原因是 Scala 中的 Int 类型并不能直接进行比较，因为其没有实现 `Ordered` 特质，真正实现 `Ordered` 特质的是 `RichInt`。\n\n<div align=\"center\"> <img  src=\"../pictures/scala-richInt.png\"/> </div>\n\n\n\n#### 2. 利用隐式参数进行隐式转换\n\nScala2.11+ 后，视图界定被标识为废弃，官方推荐使用类型限定来解决上面的问题，本质上就是使用隐式参数进行隐式转换。\n\n```scala\nobject Pair extends App {\n\n   // order 既是一个隐式参数也是一个隐式转换，即如果 a 不存在 < 方法，则转换为 order(a)<b\n  def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b\n\n  println(smaller(1,2)) //输出 1\n}\n```\n\n\n\n## 参考资料\n\n1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1  \n2. 凯.S.霍斯特曼  . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Scala集合类型.md",
    "content": "# 集合\n\n<nav>\n<a href=\"#一集合简介\">一、集合简介</a><br/>\n<a href=\"#二集合结构\">二、集合结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-scalacollection\">3.1 scala.collection</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-scalacollectionmutable\">3.2 scala.collection.mutable</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-scalacollectionimmutable\">3.2 scala.collection.immutable</a><br/>\n<a href=\"#三Trait-Traversable\">三、Trait Traversable</a><br/>\n<a href=\"#四Trait-Iterable\">四、Trait Iterable</a><br/>\n<a href=\"#五修改集合\">五、修改集合</a><br/>\n</nav>\n\n## 一、集合简介\n\nScala 中拥有多种集合类型，主要分为可变的和不可变的集合两大类：\n\n+ **可变集合**： 可以被修改。即可以更改，添加，删除集合中的元素；\n\n+ **不可变集合类**：不能被修改。对集合执行更改，添加或删除操作都会返回一个新的集合，而不是修改原来的集合。\n\n## 二、集合结构\n\nScala 中的大部分集合类都存在三类变体，分别位于 `scala.collection`, `scala.collection.immutable`, `scala.collection.mutable` 包中。还有部分集合类位于 `scala.collection.generic` 包下。\n\n- **scala.collection.immutable** ：包是中的集合是不可变的；\n- **scala.collection.mutable** ：包中的集合是可变的；\n- **scala.collection** ：包中的集合，既可以是可变的，也可以是不可变的。\n\n```scala\nval sortSet = scala.collection.SortedSet(1, 2, 3, 4, 5)\nval mutableSet = collection.mutable.SortedSet(1, 2, 3, 4, 5)\nval immutableSet = collection.immutable.SortedSet(1, 2, 3, 4, 5)\n```\n\n如果你仅写了 `Set` 而没有加任何前缀也没有进行任何 `import`，则 Scala 默认采用不可变集合类。\n\n```scala\nscala> Set(1,2,3,4,5)\nres0: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)\n```\n\n### 3.1 scala.collection\n\nscala.collection 包中所有集合如下图：\n\n<div align=\"center\"> <img  src=\"../pictures/scala-collection.png\"/> </div>\n\n### 3.2 scala.collection.mutable\n\nscala.collection.mutable 包中所有集合如下图：\n\n<div align=\"center\"> <img  src=\"../pictures/scala-collection-m.png\"/> </div>\n\n### 3.2 scala.collection.immutable\n\nscala.collection.immutable 包中所有集合如下图：\n\n<div align=\"center\"> <img  src=\"../pictures/scala-collection-imm.png\"/> </div>\n\n## 三、Trait Traversable\n\nScala 中所有集合的顶层实现是 `Traversable` 。它唯一的抽象方法是 `foreach`：\n\n```scala\ndef foreach[U](f: Elem => U)\n```\n\n实现 `Traversable` 的集合类只需要实现这个抽象方法，其他方法可以从 `Traversable` 继承。`Traversable` 中的所有可用方法如下：\n\n| **方法**                            | **作用**                                                     |\n| ----------------------------------- | ------------------------------------------------------------ |\n| **Abstract Method:**                |                                                              |\n| `xs foreach f`                      | 为 xs 的每个元素执行函数 f                                   |\n| **Addition:**                       |                                                              |\n| `xs ++ ys`                          | 一个包含 xs 和 ys 中所有元素的新的集合。 ys 是一个 Traversable 或 Iterator。 |\n| **Maps:**                           |                                                              |\n| `xs map f`                          | 对 xs 中每一个元素应用函数 f，并返回一个新的集合             |\n| `xs flatMap f`                      | 对 xs 中每一个元素应用函数 f，最后将结果合并成一个新的集合   |\n| `xs collect f`                      | 对 xs 中每一个元素调用偏函数 f，并返回一个新的集合           |\n| **Conversions:**                    |                                                              |\n| `xs.toArray`                        | 将集合转化为一个 Array                                       |\n| `xs.toList`                         | 将集合转化为一个 List                                        |\n| `xs.toIterable`                     | 将集合转化为一个 Iterable                                    |\n| `xs.toSeq`                          | 将集合转化为一个 Seq                                         |\n| `xs.toIndexedSeq`                   | 将集合转化为一个 IndexedSeq                                  |\n| `xs.toStream`                       | 将集合转化为一个延迟计算的流                                 |\n| `xs.toSet`                          | 将集合转化为一个 Set                                         |\n| `xs.toMap`                          | 将一个（key, value）对的集合转化为一个 Map。 如果当前集合的元素类型不是（key, value）对形式， 则报静态类型错误。 |\n| **Copying:**                        |                                                              |\n| `xs copyToBuffer buf`               | 拷贝集合中所有元素到缓存 buf                                 |\n| `xs copyToArray(arr,s,n)`           | 从索引 s 开始，将集合中最多 n 个元素复制到数组 arr。 最后两个参数是可选的。 |\n| **Size info:**                      |                                                              |\n| `xs.isEmpty`                        | 判断集合是否为空                                             |\n| `xs.nonEmpty`                       | 判断集合是否包含元素                                         |\n| `xs.size`                           | 返回集合中元素的个数                                         |\n| `xs.hasDefiniteSize`                | 如果 xs 具有有限大小，则为真。                               |\n| **Element Retrieval:**              |                                                              |\n| `xs.head`                           | 返回集合中的第一个元素（如果无序，则随机返回）               |\n| `xs.headOption`                     | 以 Option 的方式返回集合中的第一个元素， 如果集合为空则返回 None |\n| `xs.last`                           | 返回集合中的最后一个元素（如果无序，则随机返回）             |\n| `xs.lastOption`                     | 以 Option 的方式返回集合中的最后一个元素， 如果集合为空则返回 None |\n| `xs find p`                         | 以 Option 的方式返回满足条件 p 的第一个元素， 如果都不满足则返回 None |\n| **Subcollection:**                  |                                                              |\n| `xs.tail`                           | 除了第一个元素之外的其他元素组成的集合                       |\n| `xs.init`                           | 除了最后一个元素之外的其他元素组成的集合                     |\n| `xs slice (from, to)`               | 返回给定索引范围之内的元素组成的集合 （包含 from 位置的元素但不包含 to 位置的元素） |\n| `xs take n`                         | 返回 xs 的前 n 个元素组成的集合（如果无序，则返回任意 n 个元素） |\n| `xs drop n`                         | 返回 xs 的后 n 个元素组成的集合（如果无序，则返回任意 n 个元素） |\n| `xs takeWhile p`                    | 从第一个元素开始查找满足条件 p 的元素， 直到遇到一个不满足条件的元素，返回所有遍历到的值。 |\n| `xs dropWhile p`                    | 从第一个元素开始查找满足条件 p 的元素， 直到遇到一个不满足条件的元素，返回所有未遍历到的值。 |\n| `xs filter p`                       | 返回满足条件 p 的所有元素的集合                              |\n| `xs withFilter p`                   | 集合的非严格的过滤器。后续对 xs 调用方法 map、flatMap 以及 withFilter 都只用作于满足条件 p 的元素，而忽略其他元素 |\n| `xs filterNot p`                    | 返回不满足条件 p 的所有元素组成的集合                        |\n| **Subdivisions:**                   |                                                              |\n| `xs splitAt n`                      | 在给定位置拆分集合，返回一个集合对 (xs take n, xs drop n)    |\n| `xs span p`                         | 根据给定条件拆分集合，返回一个集合对 (xs takeWhile p, xs dropWhile p)。即遍历元素，直到遇到第一个不符合条件的值则结束遍历，将遍历到的值和未遍历到的值分别放入两个集合返回。 |\n| `xs partition p`                    | 按照筛选条件对元素进行分组                                   |\n| `xs groupBy f`                      | 根据鉴别器函数 f 将 xs 划分为集合映射                        |\n| **Element Conditions:**             |                                                              |\n| `xs forall p`                       | 判断集合中所有的元素是否都满足条件 p                         |\n| `xs exists p`                       | 判断集合中是否存在一个元素满足条件 p                         |\n| `xs count p`                        | xs 中满足条件 p 的元素的个数                                 |\n| **Folds:**                          |                                                              |\n| `(z /: xs) (op)`                    | 以 z 为初始值，从左到右对 xs 中的元素执行操作为 op 的归约操作 |\n| `(xs :\\ z) (op)`                    | 以 z 为初始值，从右到左对 xs 中的元素执行操作为 op 的归约操作 |\n| `xs.foldLeft(z) (op)`               | 同 (z /: xs) (op)                                            |\n| `xs.foldRight(z) (op)`              | 同 (xs :\\ z) (op)                                            |\n| `xs reduceLeft op`                  | 从左到右对 xs 中的元素执行操作为 op 的归约操作               |\n| `xs reduceRight op`                 | 从右到左对 xs 中的元素执行操作为 op 的归约操作               |\n| **Specific Folds:**                 |                                                              |\n| `xs.sum`                            | 累计求和                                                     |\n| `xs.product`                        | 累计求积                                                     |\n| `xs.min`                            | xs 中的最小值                                                |\n| `xs.max`                            | xs 中的最大值                                                |\n| **String:**                         |                                                              |\n| `xs addString (b, start, sep, end)` | 向 StringBuilder  b 中添加一个字符串， 该字符串包含 xs 的所有元素。start、seq 和 end  都是可选的，seq 为分隔符，start 为开始符号，end 为结束符号。 |\n| `xs mkString (start, seq, end)`     | 将集合转化为一个字符串。start、seq 和 end  都是可选的，seq 为分隔符，start 为开始符号，end 为结束符号。 |\n| `xs.stringPrefix`                   | 返回 xs.toString 字符串开头的集合名称                        |\n| **Views:**                          |                                                              |\n| `xs.view`                           | 生成 xs 的视图                                               |\n| `xs view (from, to)`                | 生成 xs 上指定索引范围内元素的视图                            |\n\n\n\n下面为部分方法的使用示例：\n\n```scala\nscala> List(1, 2, 3, 4, 5, 6).collect { case i if i % 2 == 0 => i * 10 }\nres0: List[Int] = List(20, 40, 60)\n\nscala> List(1, 2, 3, 4, 5, 6).withFilter(_ % 2 == 0).map(_ * 10)\nres1: List[Int] = List(20, 40, 60)\n\nscala> (10 /: List(1, 2, 3)) (_ + _)\nres2: Int = 16\n\nscala> List(1, 2, 3, -4, 5) takeWhile (_ > 0)\nres3: List[Int] = List(1, 2, 3)\n\nscala> List(1, 2, 3, -4, 5) span (_ > 0)\nres4: (List[Int], List[Int]) = (List(1, 2, 3),List(-4, 5))\n\nscala> List(1, 2, 3).mkString(\"[\",\"-\",\"]\")\nres5: String = [1-2-3]\n```\n\n\n\n## 四、Trait Iterable\n\nScala 中所有的集合都直接或者间接实现了 `Iterable` 特质，`Iterable` 拓展自 `Traversable`，并额外定义了部分方法：\n\n| **方法**               | **作用**                                                     |\n| ---------------------- | ------------------------------------------------------------ |\n| **Abstract Method:**   |                                                              |\n| `xs.iterator`          | 返回一个迭代器，用于遍历 xs 中的元素， 与 foreach 遍历元素的顺序相同。 |\n| **Other Iterators:**   |                                                              |\n| `xs grouped size`      | 返回一个固定大小的迭代器                                     |\n| `xs sliding size`      | 返回一个固定大小的滑动窗口的迭代器                           |\n| **Subcollections:**    |                                                              |\n| `xs takeRigtht n`      | 返回 xs 中最后 n 个元素组成的集合（如果无序，则返回任意 n 个元素组成的集合） |\n| `xs dropRight n`       | 返回 xs 中除了最后 n 个元素外的部分                          |\n| **Zippers:**           |                                                              |\n| `xs zip ys`            | 返回 xs 和 ys 的对应位置上的元素对组成的集合                 |\n| `xs zipAll (ys, x, y)` | 返回 xs 和 ys 的对应位置上的元素对组成的集合。其中较短的序列通过附加元素 x 或 y 来扩展以匹配较长的序列。 |\n| `xs.zipWithIndex`      | 返回一个由 xs 中元素及其索引所组成的元素对的集合             |\n| **Comparison:**        |                                                              |\n| `xs sameElements ys`   | 测试 xs 和 ys 是否包含相同顺序的相同元素                     |\n\n所有方法示例如下：\n\n```scala\nscala> List(1, 2, 3).iterator.reduce(_ * _ * 10)\nres0: Int = 600\n\nscala> List(\"a\",\"b\",\"c\",\"d\",\"e\") grouped 2 foreach println\nList(a, b)\nList(c, d)\nList(e)\n\nscala> List(\"a\",\"b\",\"c\",\"d\",\"e\") sliding 2 foreach println\nList(a, b)\nList(b, c)\nList(c, d)\nList(d, e)\n\nscala>  List(\"a\",\"b\",\"c\",\"d\",\"e\").takeRight(3)\nres1: List[String] = List(c, d, e)\n\nscala> List(\"a\",\"b\",\"c\",\"d\",\"e\").dropRight(3)\nres2: List[String] = List(a, b)\n\nscala> List(\"a\",\"b\",\"c\").zip(List(1,2,3))\nres3: List[(String, Int)] = List((a,1), (b,2), (c,3))\n\nscala> List(\"a\",\"b\",\"c\",\"d\").zipAll(List(1,2,3),\"\",4)\nres4: List[(String, Int)] = List((a,1), (b,2), (c,3), (d,4))\n\nscala> List(\"a\",\"b\",\"c\").zipAll(List(1,2,3,4),\"d\",\"\")\nres5: List[(String, Any)] = List((a,1), (b,2), (c,3), (d,4))\n\nscala> List(\"a\", \"b\", \"c\").zipWithIndex\nres6: List[(String, Int)] = List((a,0), (b,1), (c,2))\n\nscala> List(\"a\", \"b\") sameElements List(\"a\", \"b\")\nres7: Boolean = true\n\nscala> List(\"a\", \"b\") sameElements List(\"b\", \"a\")\nres8: Boolean = false\n```\n\n\n\n## 五、修改集合\n\n当你想对集合添加或者删除元素，需要根据不同的集合类型选择不同的操作符号：\n\n| 操作符                                                       | 描述                                              | 集合类型              |\n| ------------------------------------------------------------ | ------------------------------------------------- | --------------------- |\n| coll(k)<br/>即 coll.apply(k)                                  | 获取指定位置的元素                                | Seq, Map              |\n| coll :+ elem<br/>elem +: coll                                | 向集合末尾或者集合头增加元素                      | Seq                   |\n| coll + elem<br/>coll + (e1, e2, ...)                         | 追加元素                                          | Seq, Map              |\n| coll - elem<br/>coll - (e1, e2, ...)                         | 删除元素                                          | Set, Map, ArrayBuffer |\n| coll ++ coll2<br/>coll2 ++: coll                             | 合并集合                                          | Iterable              |\n| coll -- coll2                                                | 移除 coll 中包含的 coll2 中的元素                     | Set, Map, ArrayBuffer |\n| elem :: lst<br/>lst2 :: lst                                  | 把指定列表 (lst2) 或者元素 (elem) 添加到列表 (lst) 头部 | List                  |\n| list ::: list2                                               | 合并 List                                          | List                  |\n| set \\| set2<br/>set & set2<br/>set &~ set2                   | 并集、交集、差集                                  | Set                   |\n| coll += elem<br/>coll += (e1, e2, ...)<br/>coll ++= coll2<br/>coll -= elem<br/>coll -= (e1, e2, ...)<br/>coll --= coll2 | 添加或者删除元素，并将修改后的结果赋值给集合本身  | 可变集合              |\n| elem +=: coll<br/>coll2 ++=: coll                            | 在集合头部追加元素或集合                          | ArrayBuffer           |\n\n\n\n## 参考资料\n\n1. https://docs.scala-lang.org/overviews/collections/overview.html\n2. https://docs.scala-lang.org/overviews/collections/trait-traversable.html\n3. https://docs.scala-lang.org/overviews/collections/trait-iterable.html\n"
  },
  {
    "path": "大数据框架学习/SparkSQL_Dataset和DataFrame简介.md",
    "content": "# DataFrame和Dataset简介\n\n<nav>\n<a href=\"#一Spark-SQL简介\">一、Spark SQL简介</a><br/>\n<a href=\"#二DataFrame--DataSet\">二、DataFrame & DataSet </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-DataFrame\">2.1 DataFrame  </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-DataFrame-对比-RDDs\">2.2 DataFrame 对比 RDDs</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-DataSet\">2.3 DataSet</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-静态类型与运行时类型安全\">2.4 静态类型与运行时类型安全</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-Untyped--Typed\">2.5 Untyped & Typed </a><br/>\n<a href=\"#三DataFrame--DataSet---RDDs-总结\">三、DataFrame & DataSet  & RDDs 总结</a><br/>\n<a href=\"#四Spark-SQL的运行原理\">四、Spark SQL的运行原理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-逻辑计划Logical-Plan\">4.1 逻辑计划(Logical Plan)</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-物理计划Physical-Plan\">4.2 物理计划(Physical Plan) </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-执行\">4.3 执行</a><br/>\n</nav>\n\n## 一、Spark SQL简介\n\nSpark SQL 是 Spark 中的一个子模块，主要用于操作结构化数据。它具有以下特点：\n\n+  能够将 SQL 查询与 Spark 程序无缝混合，允许您使用 SQL 或 DataFrame API 对结构化数据进行查询；\n+  支持多种开发语言；\n+ 支持多达上百种的外部数据源，包括 Hive，Avro，Parquet，ORC，JSON 和 JDBC 等；\n+ 支持 HiveQL 语法以及 Hive SerDes 和 UDF，允许你访问现有的 Hive 仓库；\n+ 支持标准的 JDBC 和 ODBC 连接；\n+ 支持优化器，列式存储和代码生成等特性；\n+ 支持扩展并能保证容错。\n\n<div align=\"center\"> <img src=\"../pictures/sql-hive-arch.png\"/> </div>\n\n## 二、DataFrame & DataSet \n\n### 2.1 DataFrame  \n\n为了支持结构化数据的处理，Spark SQL 提供了新的数据结构 DataFrame。DataFrame 是一个由具名列组成的数据集。它在概念上等同于关系数据库中的表或 R/Python 语言中的 `data frame`。 由于 Spark SQL 支持多种语言的开发，所以每种语言都定义了 `DataFrame` 的抽象，主要如下：\n\n| 语言   | 主要抽象                                     |\n| ------ | -------------------------------------------- |\n| Scala  | Dataset[T] & DataFrame (Dataset[Row] 的别名) |\n| Java   | Dataset[T]                                   |\n| Python | DataFrame                                    |\n| R      | DataFrame                                    |\n\n### 2.2 DataFrame 对比 RDDs\n\nDataFrame 和 RDDs 最主要的区别在于一个面向的是结构化数据，一个面向的是非结构化数据，它们内部的数据结构如下：\n\n<div align=\"center\"> <img src=\"../pictures/spark-dataFrame+RDDs.png\"/> </div>\n\nDataFrame 内部的有明确 Scheme 结构，即列名、列字段类型都是已知的，这带来的好处是可以减少数据读取以及更好地优化执行计划，从而保证查询效率。\n\n**DataFrame 和 RDDs 应该如何选择？**\n\n+ 如果你想使用函数式编程而不是 DataFrame API，则使用 RDDs；\n+ 如果你的数据是非结构化的 (比如流媒体或者字符流)，则使用 RDDs，\n+ 如果你的数据是结构化的 (如 RDBMS 中的数据) 或者半结构化的 (如日志)，出于性能上的考虑，应优先使用 DataFrame。\n\n### 2.3 DataSet\n\nDataset 也是分布式的数据集合，在 Spark 1.6 版本被引入，它集成了 RDD 和 DataFrame 的优点，具备强类型的特点，同时支持 Lambda 函数，但只能在 Scala 和 Java 语言中使用。在 Spark 2.0 后，为了方便开发者，Spark 将 DataFrame 和 Dataset 的 API 融合到一起，提供了结构化的 API(Structured API)，即用户可以通过一套标准的 API 就能完成对两者的操作。\n\n> 这里注意一下：DataFrame 被标记为 Untyped API，而 DataSet 被标记为 Typed API，后文会对两者做出解释。\n\n\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/spark-unifed.png\"/> </div>\n\n### 2.4 静态类型与运行时类型安全\n\n静态类型 (Static-typing) 与运行时类型安全 (runtime type-safety) 主要表现如下:\n\n在实际使用中，如果你用的是 Spark SQL 的查询语句，则直到运行时你才会发现有语法错误，而如果你用的是 DataFrame 和 Dataset，则在编译时就可以发现错误 (这节省了开发时间和整体代价)。DataFrame 和 Dataset 主要区别在于：\n\n在 DataFrame 中，当你调用了 API 之外的函数，编译器就会报错，但如果你使用了一个不存在的字段名字，编译器依然无法发现。而 Dataset 的 API 都是用 Lambda 函数和 JVM 类型对象表示的，所有不匹配的类型参数在编译时就会被发现。\n\n以上这些最终都被解释成关于类型安全图谱，对应开发中的语法和分析错误。在图谱中，Dataset 最严格，但对于开发者来说效率最高。\n\n<div align=\"center\"> <img  width=\"600px\"  src=\"../pictures/spark-运行安全.png\"/> </div>\n\n上面的描述可能并没有那么直观，下面的给出一个 IDEA 中代码编译的示例：\n\n<div align=\"center\"> <img src=\"../pictures/spark-运行时类型安全.png\"/> </div>\n\n这里一个可能的疑惑是 DataFrame 明明是有确定的 Scheme 结构 (即列名、列字段类型都是已知的)，但是为什么还是无法对列名进行推断和错误判断，这是因为 DataFrame 是 Untyped 的。\n\n### 2.5 Untyped & Typed \n\n在上面我们介绍过 DataFrame API 被标记为 `Untyped API`，而 DataSet API 被标记为 `Typed API`。DataFrame 的 `Untyped` 是相对于语言或 API 层面而言，它确实有明确的 Scheme 结构，即列名，列类型都是确定的，但这些信息完全由 Spark 来维护，Spark 只会在运行时检查这些类型和指定类型是否一致。这也就是为什么在 Spark 2.0 之后，官方推荐把 DataFrame 看做是 `DatSet[Row]`，Row 是 Spark 中定义的一个 `trait`，其子类中封装了列字段的信息。\n\n相对而言，DataSet 是 `Typed` 的，即强类型。如下面代码，DataSet 的类型由 Case Class(Scala) 或者 Java Bean(Java) 来明确指定的，在这里即每一行数据代表一个 `Person`，这些信息由 JVM 来保证正确性，所以字段名错误和类型错误在编译的时候就会被 IDE 所发现。\n\n```scala\ncase class Person(name: String, age: Long)\nval dataSet: Dataset[Person] = spark.read.json(\"people.json\").as[Person]\n```\n\n\n\n## 三、DataFrame & DataSet  & RDDs 总结\n\n这里对三者做一下简单的总结：\n\n+ RDDs 适合非结构化数据的处理，而 DataFrame & DataSet 更适合结构化数据和半结构化的处理；\n+ DataFrame & DataSet 可以通过统一的 Structured API 进行访问，而 RDDs 则更适合函数式编程的场景；\n+ 相比于 DataFrame 而言，DataSet 是强类型的 (Typed)，有着更为严格的静态类型检查；\n+ DataSets、DataFrames、SQL 的底层都依赖了 RDDs API，并对外提供结构化的访问接口。\n\n<div align=\"center\"> <img  width=\"600px\"  src=\"../pictures/spark-structure-api.png\"/> </div>\n\n\n\n## 四、Spark SQL的运行原理\n\nDataFrame、DataSet 和 Spark SQL 的实际执行流程都是相同的：\n\n1. 进行 DataFrame/Dataset/SQL 编程；\n2. 如果是有效的代码，即代码没有编译错误，Spark 会将其转换为一个逻辑计划；\n3. Spark 将此逻辑计划转换为物理计划，同时进行代码优化；\n4. Spark 然后在集群上执行这个物理计划 (基于 RDD 操作) 。\n\n### 4.1 逻辑计划(Logical Plan)\n\n执行的第一个阶段是将用户代码转换成一个逻辑计划。它首先将用户代码转换成 `unresolved logical plan`(未解决的逻辑计划)，之所以这个计划是未解决的，是因为尽管您的代码在语法上是正确的，但是它引用的表或列可能不存在。 Spark 使用 `analyzer`(分析器) 基于 `catalog`(存储的所有表和 `DataFrames` 的信息) 进行解析。解析失败则拒绝执行，解析成功则将结果传给 `Catalyst` 优化器 (`Catalyst Optimizer`)，优化器是一组规则的集合，用于优化逻辑计划，通过谓词下推等方式进行优化，最终输出优化后的逻辑执行计划。\n\n<div align=\"center\"> <img src=\"../pictures/spark-Logical-Planning.png\"/> </div>\n\n\n\n### 4.2 物理计划(Physical Plan) \n\n得到优化后的逻辑计划后，Spark 就开始了物理计划过程。 它通过生成不同的物理执行策略，并通过成本模型来比较它们，从而选择一个最优的物理计划在集群上面执行的。物理规划的输出结果是一系列的 RDDs 和转换关系 (transformations)。\n\n<div align=\"center\"> <img src=\"../pictures/spark-Physical-Planning.png\"/> </div>\n\n### 4.3 执行\n\n在选择一个物理计划后，Spark 运行其 RDDs 代码，并在运行时执行进一步的优化，生成本地 Java 字节码，最后将运行结果返回给用户。 \n\n\n\n## 参考资料\n\n1.  Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 \n2. [Spark SQL, DataFrames and Datasets Guide](https://spark.apache.org/docs/latest/sql-programming-guide.html)\n3. [且谈 Apache Spark 的 API 三剑客：RDD、DataFrame 和 Dataset(译文)](https://www.infoq.cn/article/three-apache-spark-apis-rdds-dataframes-and-datasets)\n4. [A Tale of Three Apache Spark APIs: RDDs vs DataFrames and Datasets(原文)](https://databricks.com/blog/2016/07/14/a-tale-of-three-apache-spark-apis-rdds-dataframes-and-datasets.html)\n"
  },
  {
    "path": "大数据框架学习/SparkSQL外部数据源.md",
    "content": "# Spark SQL 外部数据源\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-多数据源支持\">1.1 多数据源支持</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-读数据格式\">1.2 读数据格式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-写数据格式\">1.3 写数据格式</a><br/>\n<a href=\"#二CSV\">二、CSV</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-读取CSV文件\">2.1 读取CSV文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-写入CSV文件\">2.2 写入CSV文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-可选配置\">2.3 可选配置</a><br/>\n<a href=\"#三JSON\">三、JSON</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-读取JSON文件\">3.1 读取JSON文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-写入JSON文件\">3.2 写入JSON文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-可选配置\">3.3 可选配置</a><br/>\n<a href=\"#四Parquet\">四、Parquet</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-读取Parquet文件\">4.1 读取Parquet文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-写入Parquet文件\">2.2 写入Parquet文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-可选配置\">2.3 可选配置</a><br/>\n<a href=\"#五ORC\">五、ORC </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#51-读取ORC文件\">5.1 读取ORC文件 </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-写入ORC文件\">4.2 写入ORC文件</a><br/>\n<a href=\"#六SQL-Databases\">六、SQL Databases </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#61-读取数据\">6.1 读取数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#62-写入数据\">6.2 写入数据</a><br/>\n<a href=\"#七Text\">七、Text </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#71-读取Text数据\">7.1 读取Text数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#72-写入Text数据\">7.2 写入Text数据</a><br/>\n<a href=\"#八数据读写高级特性\">八、数据读写高级特性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#81-并行读\">8.1 并行读</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#82-并行写\">8.2 并行写</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#83-分区写入\">8.3 分区写入</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#83-分桶写入\">8.3 分桶写入</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#85-文件大小管理\">8.5 文件大小管理</a><br/>\n<a href=\"#九可选配置附录\">九、可选配置附录</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#91-CSV读写可选配置\">9.1 CSV读写可选配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#92-JSON读写可选配置\">9.2 JSON读写可选配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#93-数据库读写可选配置\">9.3 数据库读写可选配置</a><br/>\n</nav>\n\n## 一、简介\n\n### 1.1 多数据源支持\n\nSpark 支持以下六个核心数据源，同时 Spark 社区还提供了多达上百种数据源的读取方式，能够满足绝大部分使用场景。\n\n- CSV\n- JSON\n- Parquet\n- ORC\n- JDBC/ODBC connections\n- Plain-text files\n\n> 注：以下所有测试文件均可从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录进行下载\n\n### 1.2 读数据格式\n\n所有读取 API 遵循以下调用格式：\n\n```scala\n// 格式\nDataFrameReader.format(...).option(\"key\", \"value\").schema(...).load()\n\n// 示例\nspark.read.format(\"csv\")\n.option(\"mode\", \"FAILFAST\")          // 读取模式\n.option(\"inferSchema\", \"true\")       // 是否自动推断 schema\n.option(\"path\", \"path/to/file(s)\")   // 文件路径\n.schema(someSchema)                  // 使用预定义的 schema      \n.load()\n```\n\n读取模式有以下三种可选项：\n\n| 读模式          | 描述                                                         |\n| --------------- | ------------------------------------------------------------ |\n| `permissive`    | 当遇到损坏的记录时，将其所有字段设置为 null，并将所有损坏的记录放在名为 _corruption t_record 的字符串列中 |\n| `dropMalformed` | 删除格式不正确的行                                           |\n| `failFast`      | 遇到格式不正确的数据时立即失败                               |\n\n### 1.3 写数据格式\n\n```scala\n// 格式\nDataFrameWriter.format(...).option(...).partitionBy(...).bucketBy(...).sortBy(...).save()\n\n//示例\ndataframe.write.format(\"csv\")\n.option(\"mode\", \"OVERWRITE\")         //写模式\n.option(\"dateFormat\", \"yyyy-MM-dd\")  //日期格式\n.option(\"path\", \"path/to/file(s)\")\n.save()\n```\n\n写数据模式有以下四种可选项：\n\n| Scala/Java               | 描述                                                         |\n| :----------------------- | :----------------------------------------------------------- |\n| `SaveMode.ErrorIfExists` | 如果给定的路径已经存在文件，则抛出异常，这是写数据默认的模式 |\n| `SaveMode.Append`        | 数据以追加的方式写入                                         |\n| `SaveMode.Overwrite`     | 数据以覆盖的方式写入                                         |\n| `SaveMode.Ignore`        | 如果给定的路径已经存在文件，则不做任何操作 |\n\n<br/>\n\n## 二、CSV\n\nCSV 是一种常见的文本文件格式，其中每一行表示一条记录，记录中的每个字段用逗号分隔。\n\n### 2.1 读取CSV文件\n\n自动推断类型读取读取示例：\n\n```scala\nspark.read.format(\"csv\")\n.option(\"header\", \"false\")        // 文件中的第一行是否为列的名称\n.option(\"mode\", \"FAILFAST\")      // 是否快速失败\n.option(\"inferSchema\", \"true\")   // 是否自动推断 schema\n.load(\"/usr/file/csv/dept.csv\")\n.show()\n```\n\n使用预定义类型：\n\n```scala\nimport org.apache.spark.sql.types.{StructField, StructType, StringType,LongType}\n//预定义数据格式\nval myManualSchema = new StructType(Array(\n    StructField(\"deptno\", LongType, nullable = false),\n    StructField(\"dname\", StringType,nullable = true),\n    StructField(\"loc\", StringType,nullable = true)\n))\nspark.read.format(\"csv\")\n.option(\"mode\", \"FAILFAST\")\n.schema(myManualSchema)\n.load(\"/usr/file/csv/dept.csv\")\n.show()\n```\n\n### 2.2 写入CSV文件\n\n```scala\ndf.write.format(\"csv\").mode(\"overwrite\").save(\"/tmp/csv/dept2\")\n```\n\n也可以指定具体的分隔符：\n\n```scala\ndf.write.format(\"csv\").mode(\"overwrite\").option(\"sep\", \"\\t\").save(\"/tmp/csv/dept2\")\n```\n\n### 2.3 可选配置\n\n为节省主文篇幅，所有读写配置项见文末 9.1 小节。\n\n<br/>\n\n## 三、JSON\n\n### 3.1 读取JSON文件\n\n```json\nspark.read.format(\"json\").option(\"mode\", \"FAILFAST\").load(\"/usr/file/json/dept.json\").show(5)\n```\n\n需要注意的是：默认不支持一条数据记录跨越多行 (如下)，可以通过配置 `multiLine` 为 `true` 来进行更改，其默认值为 `false`。\n\n```json\n// 默认支持单行\n{\"DEPTNO\": 10,\"DNAME\": \"ACCOUNTING\",\"LOC\": \"NEW YORK\"}\n\n//默认不支持多行\n{\n  \"DEPTNO\": 10,\n  \"DNAME\": \"ACCOUNTING\",\n  \"LOC\": \"NEW YORK\"\n}\n```\n\n### 3.2 写入JSON文件\n\n```scala\ndf.write.format(\"json\").mode(\"overwrite\").save(\"/tmp/spark/json/dept\")\n```\n\n### 3.3 可选配置\n\n为节省主文篇幅，所有读写配置项见文末 9.2 小节。\n\n<br/>\n\n## 四、Parquet\n\n Parquet 是一个开源的面向列的数据存储，它提供了多种存储优化，允许读取单独的列非整个文件，这不仅节省了存储空间而且提升了读取效率，它是 Spark 是默认的文件格式。\n\n### 4.1 读取Parquet文件\n\n```scala\nspark.read.format(\"parquet\").load(\"/usr/file/parquet/dept.parquet\").show(5)\n```\n\n### 2.2 写入Parquet文件\n\n```scala\ndf.write.format(\"parquet\").mode(\"overwrite\").save(\"/tmp/spark/parquet/dept\")\n```\n\n### 2.3 可选配置\n\nParquet 文件有着自己的存储规则，因此其可选配置项比较少，常用的有如下两个：\n\n| 读写操作 | 配置项               | 可选值                                                       | 默认值                                      | 描述                                                         |\n| -------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------- | ------------------------------------------------------------ |\n| Write    | compression or codec | None,<br/>uncompressed,<br/>bzip2,<br/>deflate, gzip,<br/>lz4, or snappy | None                                        | 压缩文件格式                                                 |\n| Read     | mergeSchema          | true, false                                                  | 取决于配置项 `spark.sql.parquet.mergeSchema` | 当为真时，Parquet 数据源将所有数据文件收集的 Schema 合并在一起，否则将从摘要文件中选择 Schema，如果没有可用的摘要文件，则从随机数据文件中选择 Schema。 |\n\n> 更多可选配置可以参阅官方文档：https://spark.apache.org/docs/latest/sql-data-sources-parquet.html\n\n<br/>\n\n## 五、ORC \n\nORC 是一种自描述的、类型感知的列文件格式，它针对大型数据的读写进行了优化，也是大数据中常用的文件格式。\n\n### 5.1 读取ORC文件 \n\n```scala\nspark.read.format(\"orc\").load(\"/usr/file/orc/dept.orc\").show(5)\n```\n\n### 4.2 写入ORC文件\n\n```scala\ncsvFile.write.format(\"orc\").mode(\"overwrite\").save(\"/tmp/spark/orc/dept\")\n```\n\n<br/>\n\n## 六、SQL Databases \n\nSpark 同样支持与传统的关系型数据库进行数据读写。但是 Spark 程序默认是没有提供数据库驱动的，所以在使用前需要将对应的数据库驱动上传到安装目录下的 `jars` 目录中。下面示例使用的是 Mysql 数据库，使用前需要将对应的 `mysql-connector-java-x.x.x.jar` 上传到 `jars` 目录下。\n\n### 6.1 读取数据\n\n读取全表数据示例如下，这里的 `help_keyword` 是 mysql 内置的字典表，只有 `help_keyword_id` 和 `name` 两个字段。\n\n```scala\nspark.read\n.format(\"jdbc\")\n.option(\"driver\", \"com.mysql.jdbc.Driver\")            //驱动\n.option(\"url\", \"jdbc:mysql://127.0.0.1:3306/mysql\")   //数据库地址\n.option(\"dbtable\", \"help_keyword\")                    //表名\n.option(\"user\", \"root\").option(\"password\",\"root\").load().show(10)\n```\n\n从查询结果读取数据：\n\n```scala\nval pushDownQuery = \"\"\"(SELECT * FROM help_keyword WHERE help_keyword_id <20) AS help_keywords\"\"\"\nspark.read.format(\"jdbc\")\n.option(\"url\", \"jdbc:mysql://127.0.0.1:3306/mysql\")\n.option(\"driver\", \"com.mysql.jdbc.Driver\")\n.option(\"user\", \"root\").option(\"password\", \"root\")\n.option(\"dbtable\", pushDownQuery)\n.load().show()\n\n//输出\n+---------------+-----------+\n|help_keyword_id|       name|\n+---------------+-----------+\n|              0|         <>|\n|              1|     ACTION|\n|              2|        ADD|\n|              3|AES_DECRYPT|\n|              4|AES_ENCRYPT|\n|              5|      AFTER|\n|              6|    AGAINST|\n|              7|  AGGREGATE|\n|              8|  ALGORITHM|\n|              9|        ALL|\n|             10|      ALTER|\n|             11|    ANALYSE|\n|             12|    ANALYZE|\n|             13|        AND|\n|             14|    ARCHIVE|\n|             15|       AREA|\n|             16|         AS|\n|             17|   ASBINARY|\n|             18|        ASC|\n|             19|     ASTEXT|\n+---------------+-----------+\n```\n\n也可以使用如下的写法进行数据的过滤：\n\n```scala\nval props = new java.util.Properties\nprops.setProperty(\"driver\", \"com.mysql.jdbc.Driver\")\nprops.setProperty(\"user\", \"root\")\nprops.setProperty(\"password\", \"root\")\nval predicates = Array(\"help_keyword_id < 10  OR name = 'WHEN'\")   //指定数据过滤条件\nspark.read.jdbc(\"jdbc:mysql://127.0.0.1:3306/mysql\", \"help_keyword\", predicates, props).show() \n\n//输出：\n+---------------+-----------+\n|help_keyword_id|       name|\n+---------------+-----------+\n|              0|         <>|\n|              1|     ACTION|\n|              2|        ADD|\n|              3|AES_DECRYPT|\n|              4|AES_ENCRYPT|\n|              5|      AFTER|\n|              6|    AGAINST|\n|              7|  AGGREGATE|\n|              8|  ALGORITHM|\n|              9|        ALL|\n|            604|       WHEN|\n+---------------+-----------+\n```\n\n可以使用 `numPartitions` 指定读取数据的并行度：\n\n```scala\noption(\"numPartitions\", 10)\n```\n\n在这里，除了可以指定分区外，还可以设置上界和下界，任何小于下界的值都会被分配在第一个分区中，任何大于上界的值都会被分配在最后一个分区中。\n\n```scala\nval colName = \"help_keyword_id\"   //用于判断上下界的列\nval lowerBound = 300L    //下界\nval upperBound = 500L    //上界\nval numPartitions = 10   //分区综述\nval jdbcDf = spark.read.jdbc(\"jdbc:mysql://127.0.0.1:3306/mysql\",\"help_keyword\",\n                             colName,lowerBound,upperBound,numPartitions,props)\n```\n\n想要验证分区内容，可以使用 `mapPartitionsWithIndex` 这个算子，代码如下：\n\n```scala\njdbcDf.rdd.mapPartitionsWithIndex((index, iterator) => {\n    val buffer = new ListBuffer[String]\n    while (iterator.hasNext) {\n        buffer.append(index + \"分区:\" + iterator.next())\n    }\n    buffer.toIterator\n}).foreach(println)\n```\n\n执行结果如下：`help_keyword` 这张表只有 600 条左右的数据，本来数据应该均匀分布在 10 个分区，但是 0 分区里面却有 319 条数据，这是因为设置了下限，所有小于 300 的数据都会被限制在第一个分区，即 0 分区。同理所有大于 500 的数据被分配在 9 分区，即最后一个分区。\n\n<div align=\"center\"> <img src=\"../pictures/spark-mysql-分区上下限.png\"/> </div>\n\n### 6.2 写入数据\n\n```scala\nval df = spark.read.format(\"json\").load(\"/usr/file/json/emp.json\")\ndf.write\n.format(\"jdbc\")\n.option(\"url\", \"jdbc:mysql://127.0.0.1:3306/mysql\")\n.option(\"user\", \"root\").option(\"password\", \"root\")\n.option(\"dbtable\", \"emp\")\n.save()\n```\n\n<br/>\n\n## 七、Text \n\nText 文件在读写性能方面并没有任何优势，且不能表达明确的数据结构，所以其使用的比较少，读写操作如下：\n\n### 7.1 读取Text数据\n\n```scala\nspark.read.textFile(\"/usr/file/txt/dept.txt\").show()\n```\n\n### 7.2 写入Text数据\n\n```scala\ndf.write.text(\"/tmp/spark/txt/dept\")\n```\n\n<br/>\n\n## 八、数据读写高级特性\n\n### 8.1 并行读\n\n多个 Executors 不能同时读取同一个文件，但它们可以同时读取不同的文件。这意味着当您从一个包含多个文件的文件夹中读取数据时，这些文件中的每一个都将成为 DataFrame 中的一个分区，并由可用的 Executors 并行读取。\n\n### 8.2 并行写\n\n写入的文件或数据的数量取决于写入数据时 DataFrame 拥有的分区数量。默认情况下，每个数据分区写一个文件。\n\n### 8.3 分区写入\n\n分区和分桶这两个概念和 Hive 中分区表和分桶表是一致的。都是将数据按照一定规则进行拆分存储。需要注意的是 `partitionBy` 指定的分区和 RDD 中分区不是一个概念：这里的**分区表现为输出目录的子目录**，数据分别存储在对应的子目录中。\n\n```scala\nval df = spark.read.format(\"json\").load(\"/usr/file/json/emp.json\")\ndf.write.mode(\"overwrite\").partitionBy(\"deptno\").save(\"/tmp/spark/partitions\")\n```\n\n输出结果如下：可以看到输出被按照部门编号分为三个子目录，子目录中才是对应的输出文件。\n\n<div align=\"center\"> <img src=\"../pictures/spark-分区.png\"/> </div>\n\n### 8.3 分桶写入\n\n分桶写入就是将数据按照指定的列和桶数进行散列，目前分桶写入只支持保存为表，实际上这就是 Hive 的分桶表。\n\n```scala\nval numberBuckets = 10\nval columnToBucketBy = \"empno\"\ndf.write.format(\"parquet\").mode(\"overwrite\")\n.bucketBy(numberBuckets, columnToBucketBy).saveAsTable(\"bucketedFiles\")\n```\n\n### 8.5 文件大小管理\n\n如果写入产生小文件数量过多，这时会产生大量的元数据开销。Spark 和 HDFS 一样，都不能很好的处理这个问题，这被称为“small file problem”。同时数据文件也不能过大，否则在查询时会有不必要的性能开销，因此要把文件大小控制在一个合理的范围内。\n\n在上文我们已经介绍过可以通过分区数量来控制生成文件的数量，从而间接控制文件大小。Spark 2.2 引入了一种新的方法，以更自动化的方式控制文件大小，这就是 `maxRecordsPerFile` 参数，它允许你通过控制写入文件的记录数来控制文件大小。\n\n```scala\n // Spark 将确保文件最多包含 5000 条记录\ndf.write.option(“maxRecordsPerFile”, 5000)\n```\n\n<br>\n\n## 九、可选配置附录\n\n### 9.1 CSV读写可选配置\n\n| 读\\写操作 | 配置项                      | 可选值                                                       | 默认值                     | 描述                                                         |\n| --------- | --------------------------- | ------------------------------------------------------------ | -------------------------- | ------------------------------------------------------------ |\n| Both      | seq                         | 任意字符                                                     | `,`(逗号)                  | 分隔符                                                       |\n| Both      | header                      | true, false                                                  | false                      | 文件中的第一行是否为列的名称。                               |\n| Read      | escape                      | 任意字符                                                     | \\                          | 转义字符                                                     |\n| Read      | inferSchema                 | true, false                                                  | false                      | 是否自动推断列类型                                           |\n| Read      | ignoreLeadingWhiteSpace     | true, false                                                  | false                      | 是否跳过值前面的空格                                         |\n| Both      | ignoreTrailingWhiteSpace    | true, false                                                  | false                      | 是否跳过值后面的空格                                         |\n| Both      | nullValue                   | 任意字符                                                     | “”                         | 声明文件中哪个字符表示空值                                   |\n| Both      | nanValue                    | 任意字符                                                     | NaN                        | 声明哪个值表示 NaN 或者缺省值                                  |\n| Both      | positiveInf                 | 任意字符                                                     | Inf                        | 正无穷                                                       |\n| Both      | negativeInf                 | 任意字符                                                     | -Inf                       | 负无穷                                                       |\n| Both      | compression or codec        | None,<br/>uncompressed,<br/>bzip2, deflate,<br/>gzip, lz4, or<br/>snappy | none                       | 文件压缩格式                                                 |\n| Both      | dateFormat                  | 任何能转换为 Java 的 <br/>SimpleDataFormat 的字符串            | yyyy-MM-dd                 | 日期格式                                                     |\n| Both      | timestampFormat             | 任何能转换为 Java 的 <br/>SimpleDataFormat 的字符串            | yyyy-MMdd’T’HH:mm:ss.SSSZZ | 时间戳格式                                                   |\n| Read      | maxColumns                  | 任意整数                                                     | 20480                      | 声明文件中的最大列数                                         |\n| Read      | maxCharsPerColumn           | 任意整数                                                     | 1000000                    | 声明一个列中的最大字符数。                                   |\n| Read      | escapeQuotes                | true, false                                                  | true                       | 是否应该转义行中的引号。                                     |\n| Read      | maxMalformedLogPerPartition | 任意整数                                                     | 10                         | 声明每个分区中最多允许多少条格式错误的数据，超过这个值后格式错误的数据将不会被读取 |\n| Write     | quoteAll                    | true, false                                                  | false                      | 指定是否应该将所有值都括在引号中，而不只是转义具有引号字符的值。 |\n| Read      | multiLine                   | true, false                                                  | false                      | 是否允许每条完整记录跨域多行                                 |\n\n### 9.2 JSON读写可选配置\n\n| 读\\写操作 | 配置项                             | 可选值                                                       | 默认值                           |\n| --------- | ---------------------------------- | ------------------------------------------------------------ | -------------------------------- |\n| Both      | compression or codec               | None,<br/>uncompressed,<br/>bzip2, deflate,<br/>gzip, lz4, or<br/>snappy | none                             |\n| Both      | dateFormat                         | 任何能转换为 Java 的 SimpleDataFormat 的字符串                 | yyyy-MM-dd                       |\n| Both      | timestampFormat                    | 任何能转换为 Java 的 SimpleDataFormat 的字符串                 | yyyy-MMdd’T’HH:mm:ss.SSSZZ       |\n| Read      | primitiveAsString                  | true, false                                                  | false                            |\n| Read      | allowComments                      | true, false                                                  | false                            |\n| Read      | allowUnquotedFieldNames            | true, false                                                  | false                            |\n| Read      | allowSingleQuotes                  | true, false                                                  | true                             |\n| Read      | allowNumericLeadingZeros           | true, false                                                  | false                            |\n| Read      | allowBackslashEscapingAnyCharacter | true, false                                                  | false                            |\n| Read      | columnNameOfCorruptRecord          | true, false                                                  | Value of spark.sql.column&NameOf |\n| Read      | multiLine                          | true, false                                                  | false                            |\n\n### 9.3 数据库读写可选配置\n\n| 属性名称                                   | 含义                                                         |\n| ------------------------------------------ | ------------------------------------------------------------ |\n| url                                        | 数据库地址                                                   |\n| dbtable                                    | 表名称                                                       |\n| driver                                     | 数据库驱动                                                   |\n| partitionColumn,<br/>lowerBound, upperBoun | 分区总数，上界，下界                                         |\n| numPartitions                              | 可用于表读写并行性的最大分区数。如果要写的分区数量超过这个限制，那么可以调用 coalesce(numpartition) 重置分区数。 |\n| fetchsize                                  | 每次往返要获取多少行数据。此选项仅适用于读取数据。           |\n| batchsize                                  | 每次往返插入多少行数据，这个选项只适用于写入数据。默认值是 1000。 |\n| isolationLevel                             | 事务隔离级别：可以是 NONE，READ_COMMITTED, READ_UNCOMMITTED，REPEATABLE_READ 或 SERIALIZABLE，即标准事务隔离级别。<br/>默认值是 READ_UNCOMMITTED。这个选项只适用于数据读取。 |\n| createTableOptions                         | 写入数据时自定义创建表的相关配置                             |\n| createTableColumnTypes                     | 写入数据时自定义创建列的列类型                               |\n\n> 数据库读写更多配置可以参阅官方文档：https://spark.apache.org/docs/latest/sql-data-sources-jdbc.html\n\n\n\n## 参考资料\n\n1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 \n2. https://spark.apache.org/docs/latest/sql-data-sources.html\n\n"
  },
  {
    "path": "大数据框架学习/SparkSQL常用聚合函数.md",
    "content": "# 聚合函数Aggregations\n\n<nav>\n<a href=\"#一简单聚合\">一、简单聚合</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-数据准备\">1.1 数据准备</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-count\">1.2 count</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-countDistinct\">1.3 countDistinct</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-approx_count_distinct\">1.4 approx_count_distinct </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-first--last\">1.5 first & last </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-min--max\">1.6 min & max</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#17-sum--sumDistinct\">1.7 sum & sumDistinct</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#18-avg\">1.8 avg</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#19-数学函数\">1.9 数学函数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#110-聚合数据到集合\">1.10 聚合数据到集合</a><br/>\n<a href=\"#二分组聚合\">二、分组聚合</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-简单分组\">2.1 简单分组</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-分组聚合\">2.2 分组聚合</a><br/>\n<a href=\"#三自定义聚合函数\">三、自定义聚合函数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-有类型的自定义函数\">3.1 有类型的自定义函数</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-无类型的自定义聚合函数\">3.2 无类型的自定义聚合函数</a><br/>\n</nav>\n\n\n## 一、简单聚合\n\n### 1.1 数据准备\n\n```scala\n// 需要导入 spark sql 内置的函数包\nimport org.apache.spark.sql.functions._\n\nval spark = SparkSession.builder().appName(\"aggregations\").master(\"local[2]\").getOrCreate()\nval empDF = spark.read.json(\"/usr/file/json/emp.json\")\n// 注册为临时视图，用于后面演示 SQL 查询\nempDF.createOrReplaceTempView(\"emp\")\nempDF.show()\n```\n\n> 注：emp.json 可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载。\n\n### 1.2 count\n\n```scala\n// 计算员工人数\nempDF.select(count(\"ename\")).show()\n```\n\n### 1.3 countDistinct\n\n```scala\n// 计算姓名不重复的员工人数\nempDF.select(countDistinct(\"deptno\")).show()\n```\n\n### 1.4 approx_count_distinct \n\n通常在使用大型数据集时，你可能关注的只是近似值而不是准确值，这时可以使用 approx_count_distinct 函数，并可以使用第二个参数指定最大允许误差。\n\n```scala\nempDF.select(approx_count_distinct (\"ename\",0.1)).show()\n```\n\n### 1.5 first & last \n\n获取 DataFrame 中指定列的第一个值或者最后一个值。\n\n```scala\nempDF.select(first(\"ename\"),last(\"job\")).show()\n```\n\n### 1.6 min & max\n\n获取 DataFrame 中指定列的最小值或者最大值。\n\n```scala\nempDF.select(min(\"sal\"),max(\"sal\")).show()\n```\n\n### 1.7 sum & sumDistinct\n\n求和以及求指定列所有不相同的值的和。\n\n```scala\nempDF.select(sum(\"sal\")).show()\nempDF.select(sumDistinct(\"sal\")).show()\n```\n\n### 1.8 avg\n\n内置的求平均数的函数。\n\n```scala\nempDF.select(avg(\"sal\")).show()\n```\n\n### 1.9 数学函数\n\nSpark SQL 中还支持多种数学聚合函数，用于通常的数学计算，以下是一些常用的例子：\n\n```scala\n// 1.计算总体方差、均方差、总体标准差、样本标准差\nempDF.select(var_pop(\"sal\"), var_samp(\"sal\"), stddev_pop(\"sal\"), stddev_samp(\"sal\")).show()\n\n// 2.计算偏度和峰度\nempDF.select(skewness(\"sal\"), kurtosis(\"sal\")).show()\n\n// 3. 计算两列的皮尔逊相关系数、样本协方差、总体协方差。(这里只是演示，员工编号和薪资两列实际上并没有什么关联关系)\nempDF.select(corr(\"empno\", \"sal\"), covar_samp(\"empno\", \"sal\"),covar_pop(\"empno\", \"sal\")).show()\n```\n\n### 1.10 聚合数据到集合\n\n```scala\nscala>  empDF.agg(collect_set(\"job\"), collect_list(\"ename\")).show()\n\n输出：\n+--------------------+--------------------+\n|    collect_set(job)| collect_list(ename)|\n+--------------------+--------------------+\n|[MANAGER, SALESMA...|[SMITH, ALLEN, WA...|\n+--------------------+--------------------+\n```\n\n\n\n## 二、分组聚合\n\n### 2.1 简单分组\n\n```scala\nempDF.groupBy(\"deptno\", \"job\").count().show()\n//等价 SQL\nspark.sql(\"SELECT deptno, job, count(*) FROM emp GROUP BY deptno, job\").show()\n\n输出：\n+------+---------+-----+\n|deptno|      job|count|\n+------+---------+-----+\n|    10|PRESIDENT|    1|\n|    30|    CLERK|    1|\n|    10|  MANAGER|    1|\n|    30|  MANAGER|    1|\n|    20|    CLERK|    2|\n|    30| SALESMAN|    4|\n|    20|  ANALYST|    2|\n|    10|    CLERK|    1|\n|    20|  MANAGER|    1|\n+------+---------+-----+\n```\n\n### 2.2 分组聚合\n\n```scala\nempDF.groupBy(\"deptno\").agg(count(\"ename\").alias(\"人数\"), sum(\"sal\").alias(\"总工资\")).show()\n// 等价语法\nempDF.groupBy(\"deptno\").agg(\"ename\"->\"count\",\"sal\"->\"sum\").show()\n// 等价 SQL\nspark.sql(\"SELECT deptno, count(ename) ,sum(sal) FROM emp GROUP BY deptno\").show()\n\n输出：\n+------+----+------+\n|deptno|人数|总工资|\n+------+----+------+\n|    10|   3|8750.0|\n|    30|   6|9400.0|\n|    20|   5|9375.0|\n+------+----+------+\n```\n\n\n\n## 三、自定义聚合函数\n\nScala 提供了两种自定义聚合函数的方法，分别如下：\n\n- 有类型的自定义聚合函数，主要适用于 DataSet；\n- 无类型的自定义聚合函数，主要适用于 DataFrame。\n\n以下分别使用两种方式来自定义一个求平均值的聚合函数，这里以计算员工平均工资为例。两种自定义方式分别如下：\n\n### 3.1 有类型的自定义函数\n\n```scala\nimport org.apache.spark.sql.expressions.Aggregator\nimport org.apache.spark.sql.{Encoder, Encoders, SparkSession, functions}\n\n// 1.定义员工类,对于可能存在 null 值的字段需要使用 Option 进行包装\ncase class Emp(ename: String, comm: scala.Option[Double], deptno: Long, empno: Long,\n               hiredate: String, job: String, mgr: scala.Option[Long], sal: Double)\n\n// 2.定义聚合操作的中间输出类型\ncase class SumAndCount(var sum: Double, var count: Long)\n\n/* 3.自定义聚合函数\n * @IN  聚合操作的输入类型\n * @BUF reduction 操作输出值的类型\n * @OUT 聚合操作的输出类型\n */\nobject MyAverage extends Aggregator[Emp, SumAndCount, Double] {\n    \n    // 4.用于聚合操作的初始零值\n    override def zero: SumAndCount = SumAndCount(0, 0)\n    \n    // 5.同一分区中的 reduce 操作\n    override def reduce(avg: SumAndCount, emp: Emp): SumAndCount = {\n        avg.sum += emp.sal\n        avg.count += 1\n        avg\n    }\n\n    // 6.不同分区中的 merge 操作\n    override def merge(avg1: SumAndCount, avg2: SumAndCount): SumAndCount = {\n        avg1.sum += avg2.sum\n        avg1.count += avg2.count\n        avg1\n    }\n\n    // 7.定义最终的输出类型\n    override def finish(reduction: SumAndCount): Double = reduction.sum / reduction.count\n\n    // 8.中间类型的编码转换\n    override def bufferEncoder: Encoder[SumAndCount] = Encoders.product\n\n    // 9.输出类型的编码转换\n    override def outputEncoder: Encoder[Double] = Encoders.scalaDouble\n}\n\nobject SparkSqlApp {\n\n    // 测试方法\n    def main(args: Array[String]): Unit = {\n\n        val spark = SparkSession.builder().appName(\"Spark-SQL\").master(\"local[2]\").getOrCreate()\n        import spark.implicits._\n        val ds = spark.read.json(\"file/emp.json\").as[Emp]\n\n        // 10.使用内置 avg() 函数和自定义函数分别进行计算，验证自定义函数是否正确\n        val myAvg = ds.select(MyAverage.toColumn.name(\"average_sal\")).first()\n        val avg = ds.select(functions.avg(ds.col(\"sal\"))).first().get(0)\n\n        println(\"自定义 average 函数 : \" + myAvg)\n        println(\"内置的 average 函数 : \" + avg)\n    }\n}\n```\n\n自定义聚合函数需要实现的方法比较多，这里以绘图的方式来演示其执行流程，以及每个方法的作用：\n\n<div align=\"center\"> <img src=\"../pictures/spark-sql-自定义函数.png\"/> </div>\n\n\n\n关于 `zero`,`reduce`,`merge`,`finish` 方法的作用在上图都有说明，这里解释一下中间类型和输出类型的编码转换，这个写法比较固定，基本上就是两种情况：\n\n- 自定义类型 Case Class 或者元组就使用 `Encoders.product` 方法；\n- 基本类型就使用其对应名称的方法，如 `scalaByte `，`scalaFloat`，`scalaShort` 等，示例如下：\n\n```scala\noverride def bufferEncoder: Encoder[SumAndCount] = Encoders.product\noverride def outputEncoder: Encoder[Double] = Encoders.scalaDouble\n```\n\n\n\n### 3.2 无类型的自定义聚合函数\n\n理解了有类型的自定义聚合函数后，无类型的定义方式也基本相同，代码如下：\n\n```scala\nimport org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}\nimport org.apache.spark.sql.types._\nimport org.apache.spark.sql.{Row, SparkSession}\n\nobject MyAverage extends UserDefinedAggregateFunction {\n  // 1.聚合操作输入参数的类型,字段名称可以自定义\n  def inputSchema: StructType = StructType(StructField(\"MyInputColumn\", LongType) :: Nil)\n\n  // 2.聚合操作中间值的类型,字段名称可以自定义\n  def bufferSchema: StructType = {\n    StructType(StructField(\"sum\", LongType) :: StructField(\"MyCount\", LongType) :: Nil)\n  }\n\n  // 3.聚合操作输出参数的类型\n  def dataType: DataType = DoubleType\n\n  // 4.此函数是否始终在相同输入上返回相同的输出,通常为 true\n  def deterministic: Boolean = true\n\n  // 5.定义零值\n  def initialize(buffer: MutableAggregationBuffer): Unit = {\n    buffer(0) = 0L\n    buffer(1) = 0L\n  }\n\n  // 6.同一分区中的 reduce 操作\n  def update(buffer: MutableAggregationBuffer, input: Row): Unit = {\n    if (!input.isNullAt(0)) {\n      buffer(0) = buffer.getLong(0) + input.getLong(0)\n      buffer(1) = buffer.getLong(1) + 1\n    }\n  }\n\n  // 7.不同分区中的 merge 操作\n  def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {\n    buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)\n    buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)\n  }\n\n  // 8.计算最终的输出值\n  def evaluate(buffer: Row): Double = buffer.getLong(0).toDouble / buffer.getLong(1)\n}\n\nobject SparkSqlApp {\n\n  // 测试方法\n  def main(args: Array[String]): Unit = {\n\n    val spark = SparkSession.builder().appName(\"Spark-SQL\").master(\"local[2]\").getOrCreate()\n    // 9.注册自定义的聚合函数\n    spark.udf.register(\"myAverage\", MyAverage)\n\n    val df = spark.read.json(\"file/emp.json\")\n    df.createOrReplaceTempView(\"emp\")\n\n    // 10.使用自定义函数和内置函数分别进行计算\n    val myAvg = spark.sql(\"SELECT myAverage(sal) as avg_sal FROM emp\").first()\n    val avg = spark.sql(\"SELECT avg(sal) as avg_sal FROM emp\").first()\n\n    println(\"自定义 average 函数 : \" + myAvg)\n    println(\"内置的 average 函数 : \" + avg)\n  }\n}\n```\n\n\n\n## 参考资料\n\n1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 \n"
  },
  {
    "path": "大数据框架学习/SparkSQL联结操作.md",
    "content": "# Spark SQL JOIN\n\n<nav>\n<a href=\"#一-数据准备\">一、 数据准备</a><br/>\n<a href=\"#二连接类型\">二、连接类型</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-INNER-JOIN\">2.1 INNER JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-FULL-OUTER-JOIN\">2.2 FULL OUTER JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-LEFT-OUTER-JOIN\"> 2.3 LEFT OUTER JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-RIGHT-OUTER-JOIN\">2.4 RIGHT OUTER JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-LEFT-SEMI-JOIN\">2.5 LEFT SEMI JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-LEFT-ANTI-JOIN\">2.6 LEFT ANTI JOIN </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-CROSS-JOIN\">2.7 CROSS JOIN</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#28-NATURAL-JOIN\">2.8 NATURAL JOIN</a><br/>\n<a href=\"#三连接的执行\">三、连接的执行</a><br/>\n</nav>\n\n## 一、 数据准备\n\n本文主要介绍 Spark SQL 的多表连接，需要预先准备测试数据。分别创建员工和部门的 Datafame，并注册为临时视图，代码如下：\n\n```scala\nval spark = SparkSession.builder().appName(\"aggregations\").master(\"local[2]\").getOrCreate()\n\nval empDF = spark.read.json(\"/usr/file/json/emp.json\")\nempDF.createOrReplaceTempView(\"emp\")\n\nval deptDF = spark.read.json(\"/usr/file/json/dept.json\")\ndeptDF.createOrReplaceTempView(\"dept\")\n```\n\n两表的主要字段如下：\n\n```properties\nemp 员工表\n |-- ENAME: 员工姓名\n |-- DEPTNO: 部门编号\n |-- EMPNO: 员工编号\n |-- HIREDATE: 入职时间\n |-- JOB: 职务\n |-- MGR: 上级编号\n |-- SAL: 薪资\n |-- COMM: 奖金  \n```\n\n```properties\ndept 部门表\n |-- DEPTNO: 部门编号\n |-- DNAME:  部门名称\n |-- LOC:    部门所在城市\n```\n\n> 注：emp.json，dept.json 可以在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录进行下载。\n\n\n\n## 二、连接类型\n\nSpark 中支持多种连接类型：\n\n+ **Inner Join** : 内连接；\n+ **Full Outer Join** :  全外连接；\n+ **Left Outer Join** :  左外连接；\n+ **Right Outer Join** :  右外连接；\n+ **Left Semi Join** :  左半连接；\n+ **Left Anti Join** :  左反连接；\n+ **Natural Join** :  自然连接；\n+ **Cross (or Cartesian) Join** :  交叉 (或笛卡尔) 连接。\n\n其中内，外连接，笛卡尔积均与普通关系型数据库中的相同，如下图所示：\n\n<div align=\"center\"> <img src=\"../pictures/sql-join.jpg\"/> </div>\n\n这里解释一下左半连接和左反连接，这两个连接等价于关系型数据库中的 `IN` 和 `NOT IN` 字句：\n\n```sql\n-- LEFT SEMI JOIN\nSELECT * FROM emp LEFT SEMI JOIN dept ON emp.deptno = dept.deptno\n-- 等价于如下的 IN 语句\nSELECT * FROM emp WHERE deptno IN (SELECT deptno FROM dept)\n\n-- LEFT ANTI JOIN\nSELECT * FROM emp LEFT ANTI JOIN dept ON emp.deptno = dept.deptno\n-- 等价于如下的 IN 语句\nSELECT * FROM emp WHERE deptno NOT IN (SELECT deptno FROM dept)\n```\n\n所有连接类型的示例代码如下：\n\n### 2.1 INNER JOIN\n\n```scala\n// 1.定义连接表达式\nval joinExpression = empDF.col(\"deptno\") === deptDF.col(\"deptno\")\n// 2.连接查询 \nempDF.join(deptDF,joinExpression).select(\"ename\",\"dname\").show()\n\n// 等价 SQL 如下：\nspark.sql(\"SELECT ename,dname FROM emp JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n### 2.2 FULL OUTER JOIN\n\n```scala\nempDF.join(deptDF, joinExpression, \"outer\").show()\nspark.sql(\"SELECT * FROM emp FULL OUTER JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n###  2.3 LEFT OUTER JOIN\n\n```scala\nempDF.join(deptDF, joinExpression, \"left_outer\").show()\nspark.sql(\"SELECT * FROM emp LEFT OUTER JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n### 2.4 RIGHT OUTER JOIN\n\n```scala\nempDF.join(deptDF, joinExpression, \"right_outer\").show()\nspark.sql(\"SELECT * FROM emp RIGHT OUTER JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n### 2.5 LEFT SEMI JOIN\n\n```scala\nempDF.join(deptDF, joinExpression, \"left_semi\").show()\nspark.sql(\"SELECT * FROM emp LEFT SEMI JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n### 2.6 LEFT ANTI JOIN \n\n```scala\nempDF.join(deptDF, joinExpression, \"left_anti\").show()\nspark.sql(\"SELECT * FROM emp LEFT ANTI JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n### 2.7 CROSS JOIN\n\n```scala\nempDF.join(deptDF, joinExpression, \"cross\").show()\nspark.sql(\"SELECT * FROM emp CROSS JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n### 2.8 NATURAL JOIN\n\n自然连接是在两张表中寻找那些数据类型和列名都相同的字段，然后自动地将他们连接起来，并返回所有符合条件的结果。\n\n```scala\nspark.sql(\"SELECT * FROM emp NATURAL JOIN dept\").show()\n```\n\n以下是一个自然连接的查询结果，程序自动推断出使用两张表都存在的 dept 列进行连接，其实际等价于：\n\n```sql\nspark.sql(\"SELECT * FROM emp JOIN dept ON emp.deptno = dept.deptno\").show()\n```\n\n<div align=\"center\"> <img src=\"../pictures/spark-sql-NATURAL-JOIN.png\"/> </div>\n\n由于自然连接常常会产生不可预期的结果，所以并不推荐使用。\n\n\n\n## 三、连接的执行\n\n在对大表与大表之间进行连接操作时，通常都会触发 `Shuffle Join`，两表的所有分区节点会进行 `All-to-All` 的通讯，这种查询通常比较昂贵，会对网络 IO 会造成比较大的负担。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/spark-Big-table–to–big-table.png\"/> </div>\n\n\n\n而对于大表和小表的连接操作，Spark 会在一定程度上进行优化，如果小表的数据量小于 Worker Node 的内存空间，Spark 会考虑将小表的数据广播到每一个 Worker Node，在每个工作节点内部执行连接计算，这可以降低网络的 IO，但会加大每个 Worker Node 的 CPU 负担。\n\n<div align=\"center\"> <img  width=\"600px\" src=\"../pictures/spark-Big-table–to–small-table.png\"/> </div>\n\n是否采用广播方式进行 `Join` 取决于程序内部对小表的判断，如果想明确使用广播方式进行 `Join`，则可以在 DataFrame API 中使用 `broadcast` 方法指定需要广播的小表：\n\n```scala\nempDF.join(broadcast(deptDF), joinExpression).show()\n```\n\n\n\n## 参考资料\n\n1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 \n"
  },
  {
    "path": "大数据框架学习/Spark_RDD.md",
    "content": "\n\n# 弹性式数据集RDDs\n\n<nav>\n<a href=\"#一RDD简介\">一、RDD简介</a><br/>\n<a href=\"#二创建RDD\">二、创建RDD</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-由现有集合创建\">2.1 由现有集合创建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-引用外部存储系统中的数据集\">2.2 引用外部存储系统中的数据集</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-textFile--wholeTextFiles\">2.3 textFile & wholeTextFiles</a><br/>\n<a href=\"#三操作RDD\">三、操作RDD</a><br/>\n<a href=\"#四缓存RDD\">四、缓存RDD</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-缓存级别\">4.1 缓存级别</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-使用缓存\">4.2 使用缓存</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-移除缓存\">4.3 移除缓存</a><br/>\n<a href=\"#五理解shuffle\">五、理解shuffle</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#51-shuffle介绍\">5.1 shuffle介绍</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#52-Shuffle的影响\">5.2 Shuffle的影响</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#53-导致Shuffle的操作\">5.3 导致Shuffle的操作</a><br/>\n<a href=\"#五宽依赖和窄依赖\">五、宽依赖和窄依赖</a><br/>\n<a href=\"#六DAG的生成\">六、DAG的生成</a><br/>\n</nav>\n\n## 一、RDD简介\n\n`RDD` 全称为 Resilient Distributed Datasets，是 Spark 最基本的数据抽象，它是只读的、分区记录的集合，支持并行操作，可以由外部数据集或其他 RDD 转换而来，它具有以下特性：\n\n+ 一个 RDD 由一个或者多个分区（Partitions）组成。对于 RDD 来说，每个分区会被一个计算任务所处理，用户可以在创建 RDD 时指定其分区个数，如果没有指定，则默认采用程序所分配到的 CPU 的核心数；\n+ RDD 拥有一个用于计算分区的函数 compute；\n+ RDD 会保存彼此间的依赖关系，RDD 的每次转换都会生成一个新的依赖关系，这种 RDD 之间的依赖关系就像流水线一样。在部分分区数据丢失后，可以通过这种依赖关系重新计算丢失的分区数据，而不是对 RDD 的所有分区进行重新计算；\n+ Key-Value 型的 RDD 还拥有 Partitioner(分区器)，用于决定数据被存储在哪个分区中，目前 Spark 中支持 HashPartitioner(按照哈希分区) 和 RangeParationer(按照范围进行分区)；\n+ 一个优先位置列表 (可选)，用于存储每个分区的优先位置 (prefered location)。对于一个 HDFS 文件来说，这个列表保存的就是每个分区所在的块的位置，按照“移动数据不如移动计算“的理念，Spark 在进行任务调度的时候，会尽可能的将计算任务分配到其所要处理数据块的存储位置。\n\n`RDD[T]` 抽象类的部分相关代码如下：\n\n```scala\n// 由子类实现以计算给定分区\ndef compute(split: Partition, context: TaskContext): Iterator[T]\n\n// 获取所有分区\nprotected def getPartitions: Array[Partition]\n\n// 获取所有依赖关系\nprotected def getDependencies: Seq[Dependency[_]] = deps\n\n// 获取优先位置列表\nprotected def getPreferredLocations(split: Partition): Seq[String] = Nil\n\n// 分区器 由子类重写以指定它们的分区方式\n@transient val partitioner: Option[Partitioner] = None\n```\n\n\n\n## 二、创建RDD\n\nRDD 有两种创建方式，分别介绍如下：\n\n### 2.1 由现有集合创建\n\n这里使用 `spark-shell` 进行测试，启动命令如下：\n\n```shell\nspark-shell --master local[4]\n```\n\n启动 `spark-shell` 后，程序会自动创建应用上下文，相当于执行了下面的 Scala 语句：\n\n```scala\nval conf = new SparkConf().setAppName(\"Spark shell\").setMaster(\"local[4]\")\nval sc = new SparkContext(conf)\n```\n\n由现有集合创建 RDD，你可以在创建时指定其分区个数，如果没有指定，则采用程序所分配到的 CPU 的核心数：\n\n```scala\nval data = Array(1, 2, 3, 4, 5)\n// 由现有集合创建 RDD,默认分区数为程序所分配到的 CPU 的核心数\nval dataRDD = sc.parallelize(data) \n// 查看分区数\ndataRDD.getNumPartitions\n// 明确指定分区数\nval dataRDD = sc.parallelize(data,2)\n```\n\n执行结果如下：\n\n<div align=\"center\"> <img src=\"../pictures/scala-分区数.png\"/> </div>\n### 2.2 引用外部存储系统中的数据集\n\n引用外部存储系统中的数据集，例如本地文件系统，HDFS，HBase 或支持 Hadoop InputFormat 的任何数据源。\n\n```scala\nval fileRDD = sc.textFile(\"/usr/file/emp.txt\")\n// 获取第一行文本\nfileRDD.take(1)\n```\n\n使用外部存储系统时需要注意以下两点：\n\n+ 如果在集群环境下从本地文件系统读取数据，则要求该文件必须在集群中所有机器上都存在，且路径相同；\n+ 支持目录路径，支持压缩文件，支持使用通配符。\n\n### 2.3 textFile & wholeTextFiles\n\n两者都可以用来读取外部文件，但是返回格式是不同的：\n\n+ **textFile**：其返回格式是 `RDD[String]` ，返回的是就是文件内容，RDD 中每一个元素对应一行数据；\n+ **wholeTextFiles**：其返回格式是 `RDD[(String, String)]`，元组中第一个参数是文件路径，第二个参数是文件内容；\n+ 两者都提供第二个参数来控制最小分区数；\n+ 从 HDFS 上读取文件时，Spark 会为每个块创建一个分区。\n\n```scala\ndef textFile(path: String,minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {...}\ndef wholeTextFiles(path: String,minPartitions: Int = defaultMinPartitions): RDD[(String, String)]={..}\n```\n\n\n\n## 三、操作RDD\n\nRDD 支持两种类型的操作：*transformations*（转换，从现有数据集创建新数据集）和 *actions*（在数据集上运行计算后将值返回到驱动程序）。RDD 中的所有转换操作都是惰性的，它们只是记住这些转换操作，但不会立即执行，只有遇到 *action* 操作后才会真正的进行计算，这类似于函数式编程中的惰性求值。\n\n```scala\nval list = List(1, 2, 3)\n// map 是一个 transformations 操作，而 foreach 是一个 actions 操作\nsc.parallelize(list).map(_ * 10).foreach(println)\n// 输出： 10 20 30\n```\n\n\n\n## 四、缓存RDD\n\n### 4.1 缓存级别\n\nSpark 速度非常快的一个原因是 RDD 支持缓存。成功缓存后，如果之后的操作使用到了该数据集，则直接从缓存中获取。虽然缓存也有丢失的风险，但是由于 RDD 之间的依赖关系，如果某个分区的缓存数据丢失，只需要重新计算该分区即可。\n\nSpark 支持多种缓存级别 ：\n\n| Storage Level<br/>（存储级别）                 | Meaning（含义）                                              |\n| ---------------------------------------------- | ------------------------------------------------------------ |\n| `MEMORY_ONLY`                                  | 默认的缓存级别，将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够，则部分分区数据将不再缓存。 |\n| `MEMORY_AND_DISK`                              | 将 RDD 以反序列化的 Java 对象的形式存储 JVM 中。如果内存空间不够，将未缓存的分区数据存储到磁盘，在需要使用这些分区时从磁盘读取。 |\n| `MEMORY_ONLY_SER`<br/>     | 将 RDD 以序列化的 Java 对象的形式进行存储（每个分区为一个 byte 数组）。这种方式比反序列化对象节省存储空间，但在读取时会增加 CPU 的计算负担。仅支持 Java 和 Scala 。  |\n| `MEMORY_AND_DISK_SER`<br/> | 类似于 `MEMORY_ONLY_SER`，但是溢出的分区数据会存储到磁盘，而不是在用到它们时重新计算。仅支持 Java 和 Scala。 |\n| `DISK_ONLY`                                    | 只在磁盘上缓存 RDD                                            |\n| `MEMORY_ONLY_2`, <br/>`MEMORY_AND_DISK_2`, etc | 与上面的对应级别功能相同，但是会为每个分区在集群中的两个节点上建立副本。 |\n| `OFF_HEAP`                                     | 与 `MEMORY_ONLY_SER` 类似，但将数据存储在堆外内存中。这需要启用堆外内存。 |\n\n> 启动堆外内存需要配置两个参数：\n>\n> + **spark.memory.offHeap.enabled** ：是否开启堆外内存，默认值为 false，需要设置为 true；\n> + **spark.memory.offHeap.size** : 堆外内存空间的大小，默认值为 0，需要设置为正值。\n\n### 4.2 使用缓存\n\n缓存数据的方法有两个：`persist` 和 `cache` 。`cache` 内部调用的也是 `persist`，它是 `persist` 的特殊化形式，等价于 `persist(StorageLevel.MEMORY_ONLY)`。示例如下：\n\n```scala\n// 所有存储级别均定义在 StorageLevel 对象中\nfileRDD.persist(StorageLevel.MEMORY_AND_DISK)\nfileRDD.cache()\n```\n\n### 4.3 移除缓存\n\nSpark 会自动监视每个节点上的缓存使用情况，并按照最近最少使用（LRU）的规则删除旧数据分区。当然，你也可以使用 `RDD.unpersist()` 方法进行手动删除。\n\n\n\n## 五、理解shuffle\n\n### 5.1 shuffle介绍\n\n在 Spark 中，一个任务对应一个分区，通常不会跨分区操作数据。但如果遇到 `reduceByKey` 等操作，Spark 必须从所有分区读取数据，并查找所有键的所有值，然后汇总在一起以计算每个键的最终结果 ，这称为 `Shuffle`。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/spark-reducebykey.png\"/> </div>\n\n\n### 5.2 Shuffle的影响\n\nShuffle 是一项昂贵的操作，因为它通常会跨节点操作数据，这会涉及磁盘 I/O，网络 I/O，和数据序列化。某些 Shuffle 操作还会消耗大量的堆内存，因为它们使用堆内存来临时存储需要网络传输的数据。Shuffle 还会在磁盘上生成大量中间文件，从 Spark 1.3 开始，这些文件将被保留，直到相应的 RDD 不再使用并进行垃圾回收，这样做是为了避免在计算时重复创建 Shuffle 文件。如果应用程序长期保留对这些 RDD 的引用，则垃圾回收可能在很长一段时间后才会发生，这意味着长时间运行的 Spark 作业可能会占用大量磁盘空间，通常可以使用 `spark.local.dir` 参数来指定这些临时文件的存储目录。\n\n### 5.3 导致Shuffle的操作\n\n由于 Shuffle 操作对性能的影响比较大，所以需要特别注意使用，以下操作都会导致 Shuffle：\n\n+ **涉及到重新分区操作**： 如 `repartition` 和 `coalesce`；\n+ **所有涉及到 ByKey 的操作**：如 `groupByKey` 和 `reduceByKey`，但 `countByKey` 除外；\n+ **联结操作**：如 `cogroup` 和 `join`。\n\n\n\n## 六、宽依赖和窄依赖\n\nRDD 和它的父 RDD(s) 之间的依赖关系分为两种不同的类型：\n\n- **窄依赖 (narrow dependency)**：父 RDDs 的一个分区最多被子 RDDs 一个分区所依赖；\n- **宽依赖 (wide dependency)**：父 RDDs 的一个分区可以被子 RDDs 的多个子分区所依赖。\n\n如下图，每一个方框表示一个 RDD，带有颜色的矩形表示分区：\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/spark-窄依赖和宽依赖.png\"/> </div>\n\n\n区分这两种依赖是非常有用的：\n\n+ 首先，窄依赖允许在一个集群节点上以流水线的方式（pipeline）对父分区数据进行计算，例如先执行 map 操作，然后执行 filter 操作。而宽依赖则需要计算好所有父分区的数据，然后再在节点之间进行 Shuffle，这与 MapReduce 类似。\n+ 窄依赖能够更有效地进行数据恢复，因为只需重新对丢失分区的父分区进行计算，且不同节点之间可以并行计算；而对于宽依赖而言，如果数据丢失，则需要对所有父分区数据进行计算并再次 Shuffle。\n\n\n\n## 七、DAG的生成\n\nRDD(s) 及其之间的依赖关系组成了 DAG(有向无环图)，DAG 定义了这些 RDD(s) 之间的 Lineage(血统) 关系，通过血统关系，如果一个 RDD 的部分或者全部计算结果丢失了，也可以重新进行计算。那么 Spark 是如何根据 DAG 来生成计算任务呢？主要是根据依赖关系的不同将 DAG 划分为不同的计算阶段 (Stage)：\n\n+ 对于窄依赖，由于分区的依赖关系是确定的，其转换操作可以在同一个线程执行，所以可以划分到同一个执行阶段；\n+ 对于宽依赖，由于 Shuffle 的存在，只能在父 RDD(s) 被 Shuffle 处理完成后，才能开始接下来的计算，因此遇到宽依赖就需要重新划分阶段。\n\n<div align=\"center\"> <img width=\"600px\" height=\"600px\" src=\"../pictures/spark-DAG.png\"/> </div>\n\n\n\n\n## 参考资料\n\n1. 张安站 . Spark 技术内幕：深入解析 Spark 内核架构设计与实现原理[M] . 机械工业出版社 . 2015-09-01\n2. [RDD Programming Guide](https://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide)\n3. [RDD：基于内存的集群计算容错抽象](http://shiyanjun.cn/archives/744.html)\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Spark_Streaming与流处理.md",
    "content": "# Spark Streaming与流处理\n\n<nav>\n<a href=\"#一流处理\">一、流处理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-静态数据处理\">1.1 静态数据处理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-流处理\">1.2 流处理</a><br/>\n<a href=\"#二Spark-Streaming\">二、Spark Streaming</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-简介\">2.1 简介</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-DStream\">2.2 DStream</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-Spark--Storm--Flink\">2.3 Spark & Storm & Flink</a><br/>\n</nav>\n\n## 一、流处理\n\n### 1.1 静态数据处理\n\n在流处理之前，数据通常存储在数据库，文件系统或其他形式的存储系统中。应用程序根据需要查询数据或计算数据。这就是传统的静态数据处理架构。Hadoop 采用 HDFS 进行数据存储，采用 MapReduce 进行数据查询或分析，这就是典型的静态数据处理架构。\n\n<div align=\"center\"> <img  src=\"../pictures/01_data_at_rest_infrastructure.png\"/> </div>\n\n\n\n### 1.2 流处理\n\n而流处理则是直接对运动中的数据的处理，在接收数据时直接计算数据。\n\n大多数数据都是连续的流：传感器事件，网站上的用户活动，金融交易等等 ，所有这些数据都是随着时间的推移而创建的。\n\n接收和发送数据流并执行应用程序或分析逻辑的系统称为**流处理器**。流处理器的基本职责是确保数据有效流动，同时具备可扩展性和容错能力，Storm 和 Flink 就是其代表性的实现。\n\n<div align=\"center\"> <img  src=\"../pictures/02_stream_processing_infrastructure.png\"/> </div>\n\n\n\n流处理带来了静态数据处理所不具备的众多优点：\n\n \n\n- **应用程序立即对数据做出反应**：降低了数据的滞后性，使得数据更具有时效性，更能反映对未来的预期；\n- **流处理可以处理更大的数据量**：直接处理数据流，并且只保留数据中有意义的子集，并将其传送到下一个处理单元，逐级过滤数据，降低需要处理的数据量，从而能够承受更大的数据量；\n- **流处理更贴近现实的数据模型**：在实际的环境中，一切数据都是持续变化的，要想能够通过过去的数据推断未来的趋势，必须保证数据的不断输入和模型的不断修正，典型的就是金融市场、股票市场，流处理能更好的应对这些数据的连续性的特征和及时性的需求；\n- **流处理分散和分离基础设施**：流式处理减少了对大型数据库的需求。相反，每个流处理程序通过流处理框架维护了自己的数据和状态，这使得流处理程序更适合微服务架构。\n\n\n\n## 二、Spark Streaming\n\n### 2.1 简介\n\nSpark Streaming 是 Spark 的一个子模块，用于快速构建可扩展，高吞吐量，高容错的流处理程序。具有以下特点：\n\n+ 通过高级 API 构建应用程序，简单易用；\n+ 支持多种语言，如 Java，Scala 和 Python；\n+ 良好的容错性，Spark Streaming 支持快速从失败中恢复丢失的操作状态；\n+ 能够和 Spark 其他模块无缝集成，将流处理与批处理完美结合；\n+ Spark Streaming 可以从 HDFS，Flume，Kafka，Twitter 和 ZeroMQ 读取数据，也支持自定义数据源。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/spark-streaming-arch.png\"/> </div>\n\n### 2.2 DStream\n\nSpark Streaming 提供称为离散流 (DStream) 的高级抽象，用于表示连续的数据流。 DStream 可以从来自 Kafka，Flume 和 Kinesis 等数据源的输入数据流创建，也可以由其他 DStream 转化而来。**在内部，DStream 表示为一系列 RDD**。\n\n<div align=\"center\"> <img width=\"600px\"   src=\"../pictures/spark-streaming-flow.png\"/> </div>\n\n\n\n### 2.3 Spark & Storm & Flink\n\nstorm 和 Flink 都是真正意义上的流计算框架，但 Spark Streaming 只是将数据流进行极小粒度的拆分，拆分为多个批处理，使得其能够得到接近于流处理的效果，但其本质上还是批处理（或微批处理）。\n\n\n\n\n\n## 参考资料\n\n1. [Spark Streaming Programming Guide](https://spark.apache.org/docs/latest/streaming-programming-guide.html)\n2. [What is stream processing?](https://www.ververica.com/what-is-stream-processing)\n"
  },
  {
    "path": "大数据框架学习/Spark_Streaming基本操作.md",
    "content": "# Spark Streaming 基本操作\n\n<nav>\n<a href=\"#一案例引入\">一、案例引入</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-StreamingContext\">3.1 StreamingContext</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-数据源\">3.2 数据源</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-服务的启动与停止\">3.3 服务的启动与停止</a><br/>\n<a href=\"#二Transformation\">二、Transformation</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-DStream与RDDs\">2.1 DStream与RDDs</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-updateStateByKey\">2.2 updateStateByKey</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-启动测试\">2.3 启动测试</a><br/>\n<a href=\"#三输出操作\">三、输出操作</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-输出API\">3.1 输出API</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-foreachRDD\">3.1 foreachRDD</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-代码说明\">3.3 代码说明</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-启动测试\">3.4 启动测试</a><br/>\n</nav>\n\n## 一、案例引入\n\n这里先引入一个基本的案例来演示流的创建：获取指定端口上的数据并进行词频统计。项目依赖和代码实现如下：\n\n```xml\n<dependency>\n    <groupId>org.apache.spark</groupId>\n    <artifactId>spark-streaming_2.12</artifactId>\n    <version>2.4.3</version>\n</dependency>\n```\n\n```scala\nimport org.apache.spark.SparkConf\nimport org.apache.spark.streaming.{Seconds, StreamingContext}\n\nobject NetworkWordCount {\n\n  def main(args: Array[String]) {\n\n    /*指定时间间隔为 5s*/\n    val sparkConf = new SparkConf().setAppName(\"NetworkWordCount\").setMaster(\"local[2]\")\n    val ssc = new StreamingContext(sparkConf, Seconds(5))\n\n    /*创建文本输入流,并进行词频统计*/\n    val lines = ssc.socketTextStream(\"hadoop001\", 9999)\n    lines.flatMap(_.split(\" \")).map(x => (x, 1)).reduceByKey(_ + _).print()\n\n    /*启动服务*/\n    ssc.start()\n    /*等待服务结束*/\n    ssc.awaitTermination()\n  }\n}\n```\n\n使用本地模式启动 Spark 程序，然后使用 `nc -lk 9999` 打开端口并输入测试数据：\n\n```shell\n[root@hadoop001 ~]#  nc -lk 9999\nhello world hello spark hive hive hadoop\nstorm storm flink azkaban\n```\n\n此时控制台输出如下，可以看到已经接收到数据并按行进行了词频统计。\n\n<div align=\"center\"> <img  src=\"../pictures/spark-streaming-word-count-v1.png\"/> </div>\n<br/>\n\n下面针对示例代码进行讲解：\n\n### 3.1 StreamingContext\n\nSpark Streaming 编程的入口类是 StreamingContext，在创建时候需要指明 `sparkConf` 和 `batchDuration`(批次时间)，Spark 流处理本质是将流数据拆分为一个个批次，然后进行微批处理，`batchDuration` 就是批次拆分的时间间隔。这个时间可以根据业务需求和服务器性能进行指定，如果业务要求低延迟并且服务器性能也允许，则这个时间可以指定得很短。\n\n这里需要注意的是：示例代码使用的是本地模式，配置为 `local[2]`，这里不能配置为 `local[1]`。这是因为对于流数据的处理，Spark 必须有一个独立的 Executor 来接收数据，然后再由其他的 Executors 来处理，所以为了保证数据能够被处理，至少要有 2 个 Executors。这里我们的程序只有一个数据流，在并行读取多个数据流的时候，也需要保证有足够的 Executors 来接收和处理数据。\n\n### 3.2 数据源\n\n在示例代码中使用的是 `socketTextStream` 来创建基于 Socket 的数据流，实际上 Spark 还支持多种数据源，分为以下两类：\n\n+ **基本数据源**：包括文件系统、Socket 连接等；\n+ **高级数据源**：包括 Kafka，Flume，Kinesis 等。\n\n在基本数据源中，Spark 支持监听 HDFS 上指定目录，当有新文件加入时，会获取其文件内容作为输入流。创建方式如下：\n\n```scala\n// 对于文本文件，指明监听目录即可\nstreamingContext.textFileStream(dataDirectory)\n// 对于其他文件，需要指明目录，以及键的类型、值的类型、和输入格式\nstreamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)\n```\n\n被监听的目录可以是具体目录，如 `hdfs://host:8040/logs/`；也可以使用通配符，如 `hdfs://host:8040/logs/2017/*`。\n\n> 关于高级数据源的整合单独整理至：[Spark Streaming 整合 Flume](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Flume.md) 和 [Spark Streaming 整合 Kafka](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Kafka.md)\n\n### 3.3 服务的启动与停止\n\n在示例代码中，使用 `streamingContext.start()` 代表启动服务，此时还要使用 `streamingContext.awaitTermination()` 使服务处于等待和可用的状态，直到发生异常或者手动使用 `streamingContext.stop()` 进行终止。\n\n\n\n## 二、Transformation\n\n### 2.1 DStream与RDDs\n\nDStream 是 Spark Streaming 提供的基本抽象。它表示连续的数据流。在内部，DStream 由一系列连续的 RDD 表示。所以从本质上而言，应用于 DStream 的任何操作都会转换为底层 RDD 上的操作。例如，在示例代码中 flatMap 算子的操作实际上是作用在每个 RDDs 上 (如下图)。因为这个原因，所以 DStream 能够支持 RDD 大部分的*transformation*算子。\n\n<div align=\"center\"> <img  src=\"../pictures/spark-streaming-dstream-ops.png\"/> </div>\n\n### 2.2 updateStateByKey\n\n除了能够支持 RDD 的算子外，DStream 还有部分独有的*transformation*算子，这当中比较常用的是 `updateStateByKey`。文章开头的词频统计程序，只能统计每一次输入文本中单词出现的数量，想要统计所有历史输入中单词出现的数量，可以使用 `updateStateByKey` 算子。代码如下：\n\n```scala\nobject NetworkWordCountV2 {\n\n\n  def main(args: Array[String]) {\n\n    /*\n     * 本地测试时最好指定 hadoop 用户名,否则会默认使用本地电脑的用户名,\n     * 此时在 HDFS 上创建目录时可能会抛出权限不足的异常\n     */\n    System.setProperty(\"HADOOP_USER_NAME\", \"root\")\n      \n    val sparkConf = new SparkConf().setAppName(\"NetworkWordCountV2\").setMaster(\"local[2]\")\n    val ssc = new StreamingContext(sparkConf, Seconds(5))\n    /*必须要设置检查点*/\n    ssc.checkpoint(\"hdfs://hadoop001:8020/spark-streaming\")\n    val lines = ssc.socketTextStream(\"hadoop001\", 9999)\n    lines.flatMap(_.split(\" \")).map(x => (x, 1))\n      .updateStateByKey[Int](updateFunction _)   //updateStateByKey 算子\n      .print()\n\n    ssc.start()\n    ssc.awaitTermination()\n  }\n\n  /**\n    * 累计求和\n    *\n    * @param currentValues 当前的数据\n    * @param preValues     之前的数据\n    * @return 相加后的数据\n    */\n  def updateFunction(currentValues: Seq[Int], preValues: Option[Int]): Option[Int] = {\n    val current = currentValues.sum\n    val pre = preValues.getOrElse(0)\n    Some(current + pre)\n  }\n}\n```\n\n使用 `updateStateByKey` 算子，你必须使用 `ssc.checkpoint()` 设置检查点，这样当使用 `updateStateByKey` 算子时，它会去检查点中取出上一次保存的信息，并使用自定义的 `updateFunction` 函数将上一次的数据和本次数据进行相加，然后返回。\n\n### 2.3 启动测试\n\n在监听端口输入如下测试数据：\n\n```shell\n[root@hadoop001 ~]#  nc -lk 9999\nhello world hello spark hive hive hadoop\nstorm storm flink azkaban\nhello world hello spark hive hive hadoop\nstorm storm flink azkaban\n```\n\n此时控制台输出如下，所有输入都被进行了词频累计：\n\n<div align=\"center\"> <img  src=\"../pictures/spark-streaming-word-count-v2.png\"/> </div>\n同时在输出日志中还可以看到检查点操作的相关信息：\n\n```shell\n# 保存检查点信息\n19/05/27 16:21:05 INFO CheckpointWriter: Saving checkpoint for time 1558945265000 ms \nto file 'hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000'\n\n# 删除已经无用的检查点信息\n19/05/27 16:21:30 INFO CheckpointWriter: \nDeleting hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000\n```\n\n## 三、输出操作\n\n### 3.1 输出API\n\nSpark Streaming 支持以下输出操作：\n\n| Output Operation                            | Meaning                                                      |\n| :------------------------------------------ | :----------------------------------------------------------- |\n| **print**()                                 | 在运行流应用程序的 driver 节点上打印 DStream 中每个批次的前十个元素。用于开发调试。 |\n| **saveAsTextFiles**(*prefix*, [*suffix*])   | 将 DStream 的内容保存为文本文件。每个批处理间隔的文件名基于前缀和后缀生成：“prefix-TIME_IN_MS [.suffix]”。 |\n| **saveAsObjectFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容序列化为 Java 对象，并保存到 SequenceFiles。每个批处理间隔的文件名基于前缀和后缀生成：“prefix-TIME_IN_MS [.suffix]”。 |\n| **saveAsHadoopFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容保存为 Hadoop 文件。每个批处理间隔的文件名基于前缀和后缀生成：“prefix-TIME_IN_MS [.suffix]”。 |\n| **foreachRDD**(*func*)                      | 最通用的输出方式，它将函数 func 应用于从流生成的每个 RDD。此函数应将每个 RDD 中的数据推送到外部系统，例如将 RDD 保存到文件，或通过网络将其写入数据库。 |\n\n前面的四个 API 都是直接调用即可，下面主要讲解通用的输出方式 `foreachRDD(func)`，通过该 API 你可以将数据保存到任何你需要的数据源。\n\n### 3.1 foreachRDD\n\n这里我们使用 Redis 作为客户端，对文章开头示例程序进行改变，把每一次词频统计的结果写入到 Redis，并利用 Redis 的 `HINCRBY` 命令来进行词频统计。这里需要导入 Jedis 依赖：\n\n```xml\n<dependency>\n    <groupId>redis.clients</groupId>\n    <artifactId>jedis</artifactId>\n    <version>2.9.0</version>\n</dependency>\n```\n\n具体实现代码如下:\n\n```scala\nimport org.apache.spark.SparkConf\nimport org.apache.spark.streaming.dstream.DStream\nimport org.apache.spark.streaming.{Seconds, StreamingContext}\nimport redis.clients.jedis.Jedis\n\nobject NetworkWordCountToRedis {\n  \n    def main(args: Array[String]) {\n\n    val sparkConf = new SparkConf().setAppName(\"NetworkWordCountToRedis\").setMaster(\"local[2]\")\n    val ssc = new StreamingContext(sparkConf, Seconds(5))\n\n    /*创建文本输入流,并进行词频统计*/\n    val lines = ssc.socketTextStream(\"hadoop001\", 9999)\n    val pairs: DStream[(String, Int)] = lines.flatMap(_.split(\" \")).map(x => (x, 1)).reduceByKey(_ + _)\n     /*保存数据到 Redis*/\n    pairs.foreachRDD { rdd =>\n      rdd.foreachPartition { partitionOfRecords =>\n        var jedis: Jedis = null\n        try {\n          jedis = JedisPoolUtil.getConnection\n          partitionOfRecords.foreach(record => jedis.hincrBy(\"wordCount\", record._1, record._2))\n        } catch {\n          case ex: Exception =>\n            ex.printStackTrace()\n        } finally {\n          if (jedis != null) jedis.close()\n        }\n      }\n    }\n    ssc.start()\n    ssc.awaitTermination()\n  }\n}\n\n```\n\n其中 `JedisPoolUtil` 的代码如下：\n\n```java\nimport redis.clients.jedis.Jedis;\nimport redis.clients.jedis.JedisPool;\nimport redis.clients.jedis.JedisPoolConfig;\n\npublic class JedisPoolUtil {\n\n    /* 声明为 volatile 防止指令重排序 */\n    private static volatile JedisPool jedisPool = null;\n    private static final String HOST = \"localhost\";\n    private static final int PORT = 6379;\n\n    /* 双重检查锁实现懒汉式单例 */\n    public static Jedis getConnection() {\n        if (jedisPool == null) {\n            synchronized (JedisPoolUtil.class) {\n                if (jedisPool == null) {\n                    JedisPoolConfig config = new JedisPoolConfig();\n                    config.setMaxTotal(30);\n                    config.setMaxIdle(10);\n                    jedisPool = new JedisPool(config, HOST, PORT);\n                }\n            }\n        }\n        return jedisPool.getResource();\n    }\n}\n```\n\n### 3.3 代码说明\n\n这里将上面保存到 Redis 的代码单独抽取出来，并去除异常判断的部分。精简后的代码如下：\n\n```scala\npairs.foreachRDD { rdd =>\n  rdd.foreachPartition { partitionOfRecords =>\n    val jedis = JedisPoolUtil.getConnection\n    partitionOfRecords.foreach(record => jedis.hincrBy(\"wordCount\", record._1, record._2))\n    jedis.close()\n  }\n}\n```\n\n这里可以看到一共使用了三次循环，分别是循环 RDD，循环分区，循环每条记录，上面我们的代码是在循环分区的时候获取连接，也就是为每一个分区获取一个连接。但是这里大家可能会有疑问：为什么不在循环 RDD 的时候，为每一个 RDD 获取一个连接，这样所需要的连接数会更少。实际上这是不可行的，如果按照这种情况进行改写，如下：\n\n```scala\npairs.foreachRDD { rdd =>\n    val jedis = JedisPoolUtil.getConnection\n    rdd.foreachPartition { partitionOfRecords =>\n        partitionOfRecords.foreach(record => jedis.hincrBy(\"wordCount\", record._1, record._2))\n    }\n    jedis.close()\n}\n```\n\n此时在执行时候就会抛出 `Caused by: java.io.NotSerializableException: redis.clients.jedis.Jedis`，这是因为在实际计算时，Spark 会将对 RDD 操作分解为多个 Task，Task 运行在具体的 Worker Node 上。在执行之前，Spark 会对任务进行闭包，之后闭包被序列化并发送给每个 Executor，而 `Jedis` 显然是不能被序列化的，所以会抛出异常。\n\n第二个需要注意的是 ConnectionPool 最好是一个静态，惰性初始化连接池 。这是因为 Spark 的转换操作本身就是惰性的，且没有数据流时不会触发写出操作，所以出于性能考虑，连接池应该是惰性的，因此上面 `JedisPool` 在初始化时采用了懒汉式单例进行惰性初始化。\n\n### 3.4 启动测试\n\n在监听端口输入如下测试数据：\n\n```shell\n[root@hadoop001 ~]#  nc -lk 9999\nhello world hello spark hive hive hadoop\nstorm storm flink azkaban\nhello world hello spark hive hive hadoop\nstorm storm flink azkaban\n```\n\n使用 Redis Manager 查看写入结果 (如下图),可以看到与使用 `updateStateByKey` 算子得到的计算结果相同。\n\n<div align=\"center\"> <img  src=\"../pictures/spark-streaming-word-count-v3.png\"/> </div>  \n<br/>\n\n> 本片文章所有源码见本仓库：[spark-streaming-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-basis)\n\n\n\n## 参考资料\n\nSpark 官方文档：http://spark.apache.org/docs/latest/streaming-programming-guide.html\n"
  },
  {
    "path": "大数据框架学习/Spark_Streaming整合Flume.md",
    "content": "# Spark Streaming 整合 Flume\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二推送式方法\">二、推送式方法</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-配置日志收集Flume\">2.1 配置日志收集Flume</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-项目依赖\">2.2 项目依赖</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-Spark-Streaming接收日志数据\">2.3 Spark Streaming接收日志数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-项目打包\">2.4 项目打包</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-启动服务和提交作业\">2.5 启动服务和提交作业</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-测试\">2.6 测试</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-注意事项\">2.7 注意事项</a><br/>\n<a href=\"#三拉取式方法\">三、拉取式方法</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31--配置日志收集Flume\">3.1  配置日志收集Flume</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-新增依赖\">2.2 新增依赖</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-Spark-Streaming接收日志数据\">2.3 Spark Streaming接收日志数据</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-启动测试\">2.4 启动测试</a><br/>\n</nav>\n\n\n## 一、简介\n\nApache Flume 是一个分布式，高可用的数据收集系统，可以从不同的数据源收集数据，经过聚合后发送到分布式计算框架或者存储系统中。Spark Straming 提供了以下两种方式用于 Flume 的整合。\n\n## 二、推送式方法\n\n在推送式方法 (Flume-style Push-based Approach) 中，Spark Streaming 程序需要对某台服务器的某个端口进行监听，Flume 通过 `avro Sink` 将数据源源不断推送到该端口。这里以监听日志文件为例，具体整合方式如下：\n\n### 2.1 配置日志收集Flume\n\n新建配置 `netcat-memory-avro.properties`，使用 `tail` 命令监听文件内容变化，然后将新的文件内容通过 `avro sink` 发送到 hadoop001 这台服务器的 8888 端口：\n\n```properties\n#指定agent的sources,sinks,channels\na1.sources = s1\na1.sinks = k1\na1.channels = c1\n\n#配置sources属性\na1.sources.s1.type = exec\na1.sources.s1.command = tail -F /tmp/log.txt\na1.sources.s1.shell = /bin/bash -c\na1.sources.s1.channels = c1\n\n#配置sink\na1.sinks.k1.type = avro\na1.sinks.k1.hostname = hadoop001\na1.sinks.k1.port = 8888\na1.sinks.k1.batch-size = 1\na1.sinks.k1.channel = c1\n\n#配置channel类型\na1.channels.c1.type = memory\na1.channels.c1.capacity = 1000\na1.channels.c1.transactionCapacity = 100\n```\n\n### 2.2 项目依赖\n\n项目采用 Maven 工程进行构建，主要依赖为 `spark-streaming` 和 `spark-streaming-flume`。\n\n```xml\n<properties>\n    <scala.version>2.11</scala.version>\n    <spark.version>2.4.0</spark.version>\n</properties>\n\n<dependencies>\n    <!-- Spark Streaming-->\n    <dependency>\n        <groupId>org.apache.spark</groupId>\n        <artifactId>spark-streaming_${scala.version}</artifactId>\n        <version>${spark.version}</version>\n    </dependency>\n    <!-- Spark Streaming 整合 Flume 依赖-->\n    <dependency>\n        <groupId>org.apache.spark</groupId>\n        <artifactId>spark-streaming-flume_${scala.version}</artifactId>\n        <version>2.4.3</version>\n    </dependency>\n</dependencies>\n\n```\n\n### 2.3 Spark Streaming接收日志数据\n\n调用 FlumeUtils 工具类的 `createStream` 方法，对 hadoop001 的 8888 端口进行监听，获取到流数据并进行打印：\n\n```scala\nimport org.apache.spark.SparkConf\nimport org.apache.spark.streaming.{Seconds, StreamingContext}\nimport org.apache.spark.streaming.flume.FlumeUtils\n\nobject PushBasedWordCount {\n    \n  def main(args: Array[String]): Unit = {\n    val sparkConf = new SparkConf()\n    val ssc = new StreamingContext(sparkConf, Seconds(5))\n    // 1.获取输入流\n    val flumeStream = FlumeUtils.createStream(ssc, \"hadoop001\", 8888)\n    // 2.打印输入流的数据\n    flumeStream.map(line => new String(line.event.getBody.array()).trim).print()\n\n    ssc.start()\n    ssc.awaitTermination()\n  }\n}\n```\n\n### 2.4 项目打包\n\n因为 Spark 安装目录下是不含有 `spark-streaming-flume` 依赖包的，所以在提交到集群运行时候必须提供该依赖包，你可以在提交命令中使用 `--jar` 指定上传到服务器的该依赖包，或者使用 `--packages org.apache.spark:spark-streaming-flume_2.12:2.4.3` 指定依赖包的完整名称，这样程序在启动时会先去中央仓库进行下载。\n\n这里我采用的是第三种方式：使用 `maven-shade-plugin` 插件进行 `ALL IN ONE` 打包，把所有依赖的 Jar 一并打入最终包中。需要注意的是 `spark-streaming` 包在 Spark 安装目录的 `jars` 目录中已经提供，所以不需要打入。插件配置如下：\n\n\n```xml\n<build>\n    <plugins>\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-compiler-plugin</artifactId>\n            <configuration>\n                <source>8</source>\n                <target>8</target>\n            </configuration>\n        </plugin>\n        <!--使用 shade 进行打包-->\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-shade-plugin</artifactId>\n            <configuration>\n                <createDependencyReducedPom>true</createDependencyReducedPom>\n                <filters>\n                    <filter>\n                        <artifact>*:*</artifact>\n                        <excludes>\n                            <exclude>META-INF/*.SF</exclude>\n                            <exclude>META-INF/*.sf</exclude>\n                            <exclude>META-INF/*.DSA</exclude>\n                            <exclude>META-INF/*.dsa</exclude>\n                            <exclude>META-INF/*.RSA</exclude>\n                            <exclude>META-INF/*.rsa</exclude>\n                            <exclude>META-INF/*.EC</exclude>\n                            <exclude>META-INF/*.ec</exclude>\n                            <exclude>META-INF/MSFTSIG.SF</exclude>\n                            <exclude>META-INF/MSFTSIG.RSA</exclude>\n                        </excludes>\n                    </filter>\n                </filters>\n                <artifactSet>\n                    <excludes>\n                        <exclude>org.apache.spark:spark-streaming_${scala.version}</exclude>\n                        <exclude>org.scala-lang:scala-library</exclude>\n                        <exclude>org.apache.commons:commons-lang3</exclude>\n                    </excludes>\n                </artifactSet>\n            </configuration>\n            <executions>\n                <execution>\n                    <phase>package</phase>\n                    <goals>\n                        <goal>shade</goal>\n                    </goals>\n                    <configuration>\n                        <transformers>\n                            <transformer \n                              implementation=\"org.apache.maven.plugins.shade.resource.ServicesResourceTransformer\"/>\n                            <transformer \n                              implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                            </transformer>\n                        </transformers>\n                    </configuration>\n                </execution>\n            </executions>\n        </plugin>\n        <!--打包.scala 文件需要配置此插件-->\n        <plugin>\n            <groupId>org.scala-tools</groupId>\n            <artifactId>maven-scala-plugin</artifactId>\n            <version>2.15.1</version>\n            <executions>\n                <execution>\n                    <id>scala-compile</id>\n                    <goals>\n                        <goal>compile</goal>\n                    </goals>\n                    <configuration>\n                        <includes>\n                            <include>**/*.scala</include>\n                        </includes>\n                    </configuration>\n                </execution>\n                <execution>\n                    <id>scala-test-compile</id>\n                    <goals>\n                        <goal>testCompile</goal>\n                    </goals>\n                </execution>\n            </executions>\n        </plugin>\n    </plugins>\n</build>\n```\n> 本项目完整源码见：[spark-streaming-flume](https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-flume)\n\n使用 `mvn clean package` 命令打包后会生产以下两个 Jar 包，提交 ` 非 original` 开头的 Jar 即可。\n\n<div align=\"center\"> <img src=\"../pictures/spark-streaming-flume-jar.png\"/> </div>\n\n### 2.5 启动服务和提交作业\n\n 启动 Flume 服务：\n\n```shell\nflume-ng agent \\\n--conf conf \\\n--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/netcat-memory-avro.properties \\\n--name a1 -Dflume.root.logger=INFO,console\n```\n\n提交 Spark Streaming 作业：\n\n```shell\nspark-submit \\\n--class com.heibaiying.flume.PushBasedWordCount \\\n--master local[4] \\\n/usr/appjar/spark-streaming-flume-1.0.jar\n```\n\n### 2.6 测试\n\n这里使用 `echo` 命令模拟日志产生的场景，往日志文件中追加数据，然后查看程序的输出：\n\n<div align=\"center\"> <img src=\"../pictures/spark-flume-input.png\"/> </div>\n\nSpark Streaming 程序成功接收到数据并打印输出：\n\n<div align=\"center\"> <img src=\"../pictures/spark-flume-console.png\"/> </div>\n\n### 2.7 注意事项\n\n#### 1. 启动顺序\n\n这里需要注意的，不论你先启动 Spark 程序还是 Flume 程序，由于两者的启动都需要一定的时间，此时先启动的程序会短暂地抛出端口拒绝连接的异常，此时不需要进行任何操作，等待两个程序都启动完成即可。\n\n<div align=\"center\"> <img src=\"../pictures/flume-retry.png\"/> </div>\n\n#### 2. 版本一致\n\n最好保证用于本地开发和编译的 Scala 版本和 Spark 的 Scala 版本一致，至少保证大版本一致，如都是 `2.11`。\n\n<br/>\n\n## 三、拉取式方法\n\n拉取式方法 (Pull-based Approach using a Custom Sink) 是将数据推送到 `SparkSink` 接收器中，此时数据会保持缓冲状态，Spark Streaming 定时从接收器中拉取数据。这种方式是基于事务的，即只有在 Spark Streaming 接收和复制数据完成后，才会删除缓存的数据。与第一种方式相比，具有更强的可靠性和容错保证。整合步骤如下：\n\n### 3.1  配置日志收集Flume\n\n新建 Flume 配置文件 `netcat-memory-sparkSink.properties`，配置和上面基本一致，只是把 `a1.sinks.k1.type` 的属性修改为 `org.apache.spark.streaming.flume.sink.SparkSink`，即采用 Spark 接收器。\n\n```properties\n#指定agent的sources,sinks,channels\na1.sources = s1\na1.sinks = k1\na1.channels = c1\n\n#配置sources属性\na1.sources.s1.type = exec\na1.sources.s1.command = tail -F /tmp/log.txt\na1.sources.s1.shell = /bin/bash -c\na1.sources.s1.channels = c1\n\n#配置sink\na1.sinks.k1.type = org.apache.spark.streaming.flume.sink.SparkSink\na1.sinks.k1.hostname = hadoop001\na1.sinks.k1.port = 8888\na1.sinks.k1.batch-size = 1\na1.sinks.k1.channel = c1\n\n#配置channel类型\na1.channels.c1.type = memory\na1.channels.c1.capacity = 1000\na1.channels.c1.transactionCapacity = 100\n```\n\n### 2.2 新增依赖\n\n使用拉取式方法需要额外添加以下两个依赖：\n\n```xml\n<dependency>\n    <groupId>org.scala-lang</groupId>\n    <artifactId>scala-library</artifactId>\n    <version>2.12.8</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.commons</groupId>\n    <artifactId>commons-lang3</artifactId>\n    <version>3.5</version>\n</dependency>\n```\n\n注意：添加这两个依赖只是为了本地测试，Spark 的安装目录下已经提供了这两个依赖，所以在最终打包时需要进行排除。\n\n### 2.3 Spark Streaming接收日志数据\n\n这里和上面推送式方法的代码基本相同，只是将调用方法改为 `createPollingStream`。\n\n```scala\nimport org.apache.spark.SparkConf\nimport org.apache.spark.streaming.{Seconds, StreamingContext}\nimport org.apache.spark.streaming.flume.FlumeUtils\n\nobject PullBasedWordCount {\n\n  def main(args: Array[String]): Unit = {\n\n    val sparkConf = new SparkConf()\n    val ssc = new StreamingContext(sparkConf, Seconds(5))\n    // 1.获取输入流\n    val flumeStream = FlumeUtils.createPollingStream(ssc, \"hadoop001\", 8888)\n    // 2.打印输入流中的数据\n    flumeStream.map(line => new String(line.event.getBody.array()).trim).print()\n    ssc.start()\n    ssc.awaitTermination()\n  }\n}\n```\n\n### 2.4 启动测试\n\n启动和提交作业流程与上面相同，这里给出执行脚本，过程不再赘述。\n\n启动 Flume 进行日志收集：\n\n```shell\nflume-ng agent \\\n--conf conf \\\n--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/netcat-memory-sparkSink.properties \\\n--name a1 -Dflume.root.logger=INFO,console\n```\n\n提交 Spark Streaming 作业：\n\n```shel\nspark-submit \\\n--class com.heibaiying.flume.PullBasedWordCount \\\n--master local[4] \\\n/usr/appjar/spark-streaming-flume-1.0.jar\n```\n\n\n\n## 参考资料\n\n- [streaming-flume-integration](https://spark.apache.org/docs/latest/streaming-flume-integration.html)\n- 关于大数据应用常用的打包方式可以参见：[大数据应用常用打包方式](https://github.com/heibaiying/BigData-Notes/blob/master/notes/大数据应用常用打包方式.md)\n"
  },
  {
    "path": "大数据框架学习/Spark_Streaming整合Kafka.md",
    "content": "# Spark Streaming 整合 Kafka\n\n<nav>\n<a href=\"#一版本说明\">一、版本说明</a><br/>\n<a href=\"#二项目依赖\">二、项目依赖</a><br/>\n<a href=\"#三整合Kafka\">三、整合Kafka</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-ConsumerRecord\">3.1 ConsumerRecord</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-生产者属性\">3.2 生产者属性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-位置策略\">3.3 位置策略</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-订阅方式\">3.4 订阅方式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-提交偏移量\">3.5 提交偏移量</a><br/>\n<a href=\"#四启动测试\">四、启动测试</a><br/>\n</nav>\n\n\n## 一、版本说明\n\nSpark 针对 Kafka 的不同版本，提供了两套整合方案：`spark-streaming-kafka-0-8` 和 `spark-streaming-kafka-0-10`，其主要区别如下：\n\n|                                               | [spark-streaming-kafka-0-8](https://spark.apache.org/docs/latest/streaming-kafka-0-8-integration.html) | [spark-streaming-kafka-0-10](https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html) |\n| :-------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |\n| Kafka 版本                                     | 0.8.2.1 or higher                                            | 0.10.0 or higher                                             |\n| AP 状态                                        | Deprecated<br/>从 Spark 2.3.0 版本开始，Kafka 0.8 支持已被弃用  | Stable(稳定版)                                               |\n| 语言支持                                      | Scala, Java, Python                                          | Scala, Java                                                  |\n| Receiver DStream                              | Yes                                                          | No                                                           |\n| Direct DStream                                | Yes                                                          | Yes                                                          |\n| SSL / TLS Support                             | No                                                           | Yes                                                          |\n| Offset Commit API(偏移量提交)                 | No                                                           | Yes                                                          |\n| Dynamic Topic Subscription<br/>(动态主题订阅) | No                                                           | Yes                                                          |\n\n本文使用的 Kafka 版本为 `kafka_2.12-2.2.0`，故采用第二种方式进行整合。\n\n## 二、项目依赖\n\n项目采用 Maven 进行构建，主要依赖如下：\n\n```xml\n<properties>\n    <scala.version>2.12</scala.version>\n</properties>\n\n<dependencies>\n    <!-- Spark Streaming-->\n    <dependency>\n        <groupId>org.apache.spark</groupId>\n        <artifactId>spark-streaming_${scala.version}</artifactId>\n        <version>${spark.version}</version>\n    </dependency>\n    <!-- Spark Streaming 整合 Kafka 依赖-->\n    <dependency>\n        <groupId>org.apache.spark</groupId>\n        <artifactId>spark-streaming-kafka-0-10_${scala.version}</artifactId>\n        <version>2.4.3</version>\n    </dependency>\n</dependencies>\n```\n\n> 完整源码见本仓库：[spark-streaming-kafka](https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-kafka)\n\n## 三、整合Kafka\n\n通过调用 `KafkaUtils` 对象的 `createDirectStream` 方法来创建输入流，完整代码如下：\n\n```scala\nimport org.apache.kafka.common.serialization.StringDeserializer\nimport org.apache.spark.SparkConf\nimport org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe\nimport org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent\nimport org.apache.spark.streaming.kafka010._\nimport org.apache.spark.streaming.{Seconds, StreamingContext}\n\n/**\n  * spark streaming 整合 kafka\n  */\nobject KafkaDirectStream {\n\n  def main(args: Array[String]): Unit = {\n\n    val sparkConf = new SparkConf().setAppName(\"KafkaDirectStream\").setMaster(\"local[2]\")\n    val streamingContext = new StreamingContext(sparkConf, Seconds(5))\n\n    val kafkaParams = Map[String, Object](\n      /*\n       * 指定 broker 的地址清单，清单里不需要包含所有的 broker 地址，生产者会从给定的 broker 里查找其他 broker 的信息。\n       * 不过建议至少提供两个 broker 的信息作为容错。\n       */\n      \"bootstrap.servers\" -> \"hadoop001:9092\",\n      /*键的序列化器*/\n      \"key.deserializer\" -> classOf[StringDeserializer],\n      /*值的序列化器*/\n      \"value.deserializer\" -> classOf[StringDeserializer],\n      /*消费者所在分组的 ID*/\n      \"group.id\" -> \"spark-streaming-group\",\n      /*\n       * 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:\n       * latest: 在偏移量无效的情况下，消费者将从最新的记录开始读取数据（在消费者启动之后生成的记录）\n       * earliest: 在偏移量无效的情况下，消费者将从起始位置读取分区的记录\n       */\n      \"auto.offset.reset\" -> \"latest\",\n      /*是否自动提交*/\n      \"enable.auto.commit\" -> (true: java.lang.Boolean)\n    )\n    \n    /*可以同时订阅多个主题*/\n    val topics = Array(\"spark-streaming-topic\")\n    val stream = KafkaUtils.createDirectStream[String, String](\n      streamingContext,\n      /*位置策略*/\n      PreferConsistent,\n      /*订阅主题*/\n      Subscribe[String, String](topics, kafkaParams)\n    )\n\n    /*打印输入流*/\n    stream.map(record => (record.key, record.value)).print()\n\n    streamingContext.start()\n    streamingContext.awaitTermination()\n  }\n}\n```\n\n### 3.1 ConsumerRecord\n\n这里获得的输入流中每一个 Record 实际上是 `ConsumerRecord<K, V> ` 的实例，其包含了 Record 的所有可用信息，源码如下：\n\n```scala\npublic class ConsumerRecord<K, V> {\n    \n    public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;\n    public static final int NULL_SIZE = -1;\n    public static final int NULL_CHECKSUM = -1;\n    \n    /*主题名称*/\n    private final String topic;\n    /*分区编号*/\n    private final int partition;\n    /*偏移量*/\n    private final long offset;\n    /*时间戳*/\n    private final long timestamp;\n    /*时间戳代表的含义*/\n    private final TimestampType timestampType;\n    /*键序列化器*/\n    private final int serializedKeySize;\n    /*值序列化器*/\n    private final int serializedValueSize;\n    /*值序列化器*/\n    private final Headers headers;\n    /*键*/\n    private final K key;\n    /*值*/\n    private final V value;\n    .....   \n}\n```\n\n### 3.2 生产者属性\n\n在示例代码中 `kafkaParams` 封装了 Kafka 消费者的属性，这些属性和 Spark Streaming 无关，是 Kafka 原生 API 中就有定义的。其中服务器地址、键序列化器和值序列化器是必选的，其他配置是可选的。其余可选的配置项如下：\n\n#### 1. fetch.min.byte\n\n消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值，broker 会等待有足够的可用数据时才会把它返回给消费者。\n\n#### 2. fetch.max.wait.ms\n\nbroker 返回给消费者数据的等待时间。\n\n#### 3. max.partition.fetch.bytes\n\n分区返回给消费者的最大字节数。\n\n#### 4. session.timeout.ms\n\n消费者在被认为死亡之前可以与服务器断开连接的时间。\n\n#### 5. auto.offset.reset\n\n该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理：\n\n- latest(默认值) ：在偏移量无效的情况下，消费者将从其启动之后生成的最新的记录开始读取数据；\n- earliest ：在偏移量无效的情况下，消费者将从起始位置读取分区的记录。\n\n#### 6. enable.auto.commit\n\n是否自动提交偏移量，默认值是 true,为了避免出现重复数据和数据丢失，可以把它设置为 false。\n\n#### 7. client.id\n\n客户端 id，服务器用来识别消息的来源。\n\n#### 8. max.poll.records\n\n单次调用 `poll()` 方法能够返回的记录数量。\n\n#### 9. receive.buffer.bytes 和 send.buffer.byte\n\n这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小，-1 代表使用操作系统的默认值。\n\n\n\n### 3.3 位置策略\n\nSpark Streaming 中提供了如下三种位置策略，用于指定 Kafka 主题分区与 Spark 执行程序 Executors 之间的分配关系：\n\n+ **PreferConsistent** : 它将在所有的 Executors 上均匀分配分区；\n\n+ **PreferBrokers** : 当 Spark 的 Executor 与 Kafka Broker 在同一机器上时可以选择该选项，它优先将该 Broker 上的首领分区分配给该机器上的 Executor；\n+ **PreferFixed** : 可以指定主题分区与特定主机的映射关系，显示地将分区分配到特定的主机，其构造器如下：\n\n```scala\n@Experimental\ndef PreferFixed(hostMap: collection.Map[TopicPartition, String]): LocationStrategy =\n  new PreferFixed(new ju.HashMap[TopicPartition, String](hostMap.asJava))\n\n@Experimental\ndef PreferFixed(hostMap: ju.Map[TopicPartition, String]): LocationStrategy =\n  new PreferFixed(hostMap)\n```\n\n\n\n### 3.4 订阅方式\n\nSpark Streaming 提供了两种主题订阅方式，分别为 `Subscribe` 和 `SubscribePattern`。后者可以使用正则匹配订阅主题的名称。其构造器分别如下：\n\n```scala\n/**\n  * @param 需要订阅的主题的集合\n  * @param Kafka 消费者参数\n  * @param offsets(可选): 在初始启动时开始的偏移量。如果没有，则将使用保存的偏移量或 auto.offset.reset 属性的值\n  */\ndef Subscribe[K, V](\n    topics: ju.Collection[jl.String],\n    kafkaParams: ju.Map[String, Object],\n    offsets: ju.Map[TopicPartition, jl.Long]): ConsumerStrategy[K, V] = { ... }\n\n/**\n  * @param 需要订阅的正则\n  * @param Kafka 消费者参数\n  * @param offsets(可选): 在初始启动时开始的偏移量。如果没有，则将使用保存的偏移量或 auto.offset.reset 属性的值\n  */\ndef SubscribePattern[K, V](\n    pattern: ju.regex.Pattern,\n    kafkaParams: collection.Map[String, Object],\n    offsets: collection.Map[TopicPartition, Long]): ConsumerStrategy[K, V] = { ... }\n```\n\n在示例代码中，我们实际上并没有指定第三个参数 `offsets`，所以程序默认采用的是配置的 `auto.offset.reset` 属性的值 latest，即在偏移量无效的情况下，消费者将从其启动之后生成的最新的记录开始读取数据。\n\n### 3.5 提交偏移量\n\n在示例代码中，我们将 `enable.auto.commit` 设置为 true，代表自动提交。在某些情况下，你可能需要更高的可靠性，如在业务完全处理完成后再提交偏移量，这时候可以使用手动提交。想要进行手动提交，需要调用 Kafka 原生的 API :\n\n+ `commitSync`:  用于异步提交；\n+ `commitAsync`：用于同步提交。\n\n具体提交方式可以参见：[Kafka 消费者详解](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka 消费者详解.md)\n\n\n\n## 四、启动测试\n\n### 4.1 创建主题\n\n#### 1. 启动Kakfa\n\nKafka 的运行依赖于 zookeeper，需要预先启动，可以启动 Kafka 内置的 zookeeper，也可以启动自己安装的：\n\n```shell\n# zookeeper启动命令\nbin/zkServer.sh start\n\n# 内置zookeeper启动命令\nbin/zookeeper-server-start.sh config/zookeeper.properties\n```\n\n启动单节点 kafka 用于测试：\n\n```shell\n# bin/kafka-server-start.sh config/server.properties\n```\n\n#### 2. 创建topic\n\n```shell\n# 创建用于测试主题\nbin/kafka-topics.sh --create \\\n                    --bootstrap-server hadoop001:9092 \\\n                    --replication-factor 1 \\\n                    --partitions 1  \\\n                    --topic spark-streaming-topic\n\n# 查看所有主题\n bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092\n```\n\n#### 3. 创建生产者\n\n这里创建一个 Kafka 生产者，用于发送测试数据：\n\n```shell\nbin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic spark-streaming-topic\n```\n\n### 4.2 本地模式测试\n\n这里我直接使用本地模式启动 Spark Streaming 程序。启动后使用生产者发送数据，从控制台查看结果。\n\n从控制台输出中可以看到数据流已经被成功接收，由于采用 `kafka-console-producer.sh` 发送的数据默认是没有 key 的，所以 key 值为 null。同时从输出中也可以看到在程序中指定的 `groupId` 和程序自动分配的 `clientId`。\n\n<div align=\"center\"> <img  src=\"../pictures/spark-straming-kafka-console.png\"/> </div>\n\n\n\n\n\n## 参考资料\n\n1. https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html\n"
  },
  {
    "path": "大数据框架学习/Spark_Structured_API的基本使用.md",
    "content": "# Structured API基本使用\n\n<nav>\n<a href=\"#一创建DataFrame和Dataset\">一、创建DataFrame和Dataset</a><br/>\n<a href=\"#二Columns列操作\">二、Columns列操作</a><br/>\n<a href=\"#三使用Structured-API进行基本查询\">三、使用Structured API进行基本查询</a><br/>\n<a href=\"#四使用Spark-SQL进行基本查询\">四、使用Spark SQL进行基本查询</a><br/>\n</nav>\n\n\n## 一、创建DataFrame和Dataset\n\n### 1.1 创建DataFrame\n\nSpark 中所有功能的入口点是 `SparkSession`，可以使用 `SparkSession.builder()` 创建。创建后应用程序就可以从现有 RDD，Hive 表或 Spark 数据源创建 DataFrame。示例如下：\n\n```scala\nval spark = SparkSession.builder().appName(\"Spark-SQL\").master(\"local[2]\").getOrCreate()\nval df = spark.read.json(\"/usr/file/json/emp.json\")\ndf.show()\n\n// 建议在进行 spark SQL 编程前导入下面的隐式转换，因为 DataFrames 和 dataSets 中很多操作都依赖了隐式转换\nimport spark.implicits._\n```\n\n可以使用 `spark-shell` 进行测试，需要注意的是 `spark-shell` 启动后会自动创建一个名为 `spark` 的 `SparkSession`，在命令行中可以直接引用即可：\n\n<div align=\"center\"> <img src=\"../pictures/spark-sql-shell.png\"/> </div>\n<br/>\n\n### 1.2 创建Dataset\n\nSpark 支持由内部数据集和外部数据集来创建 DataSet，其创建方式分别如下：\n\n####  1. 由外部数据集创建\n\n```scala\n// 1.需要导入隐式转换\nimport spark.implicits._\n\n// 2.创建 case class,等价于 Java Bean\ncase class Emp(ename: String, comm: Double, deptno: Long, empno: Long, \n               hiredate: String, job: String, mgr: Long, sal: Double)\n\n// 3.由外部数据集创建 Datasets\nval ds = spark.read.json(\"/usr/file/emp.json\").as[Emp]\nds.show()\n```\n\n#### 2. 由内部数据集创建\n\n```scala\n// 1.需要导入隐式转换\nimport spark.implicits._\n\n// 2.创建 case class,等价于 Java Bean\ncase class Emp(ename: String, comm: Double, deptno: Long, empno: Long, \n               hiredate: String, job: String, mgr: Long, sal: Double)\n\n// 3.由内部数据集创建 Datasets\nval caseClassDS = Seq(Emp(\"ALLEN\", 300.0, 30, 7499, \"1981-02-20 00:00:00\", \"SALESMAN\", 7698, 1600.0),\n                      Emp(\"JONES\", 300.0, 30, 7499, \"1981-02-20 00:00:00\", \"SALESMAN\", 7698, 1600.0))\n                    .toDS()\ncaseClassDS.show()\n```\n\n<br/>\n\n### 1.3 由RDD创建DataFrame\n\nSpark 支持两种方式把 RDD 转换为 DataFrame，分别是使用反射推断和指定 Schema 转换：\n\n#### 1. 使用反射推断\n\n```scala\n// 1.导入隐式转换\nimport spark.implicits._\n\n// 2.创建部门类\ncase class Dept(deptno: Long, dname: String, loc: String)\n\n// 3.创建 RDD 并转换为 dataSet\nval rddToDS = spark.sparkContext\n  .textFile(\"/usr/file/dept.txt\")\n  .map(_.split(\"\\t\"))\n  .map(line => Dept(line(0).trim.toLong, line(1), line(2)))\n  .toDS()  // 如果调用 toDF() 则转换为 dataFrame \n```\n\n#### 2. 以编程方式指定Schema\n\n```scala\nimport org.apache.spark.sql.Row\nimport org.apache.spark.sql.types._\n\n\n// 1.定义每个列的列类型\nval fields = Array(StructField(\"deptno\", LongType, nullable = true),\n                   StructField(\"dname\", StringType, nullable = true),\n                   StructField(\"loc\", StringType, nullable = true))\n\n// 2.创建 schema\nval schema = StructType(fields)\n\n// 3.创建 RDD\nval deptRDD = spark.sparkContext.textFile(\"/usr/file/dept.txt\")\nval rowRDD = deptRDD.map(_.split(\"\\t\")).map(line => Row(line(0).toLong, line(1), line(2)))\n\n\n// 4.将 RDD 转换为 dataFrame\nval deptDF = spark.createDataFrame(rowRDD, schema)\ndeptDF.show()\n```\n\n<br/>\n\n### 1.4  DataFrames与Datasets互相转换\n\nSpark 提供了非常简单的转换方法用于 DataFrame 与 Dataset 间的互相转换，示例如下：\n\n```shell\n# DataFrames转Datasets\nscala> df.as[Emp]\nres1: org.apache.spark.sql.Dataset[Emp] = [COMM: double, DEPTNO: bigint ... 6 more fields]\n\n# Datasets转DataFrames\nscala> ds.toDF()\nres2: org.apache.spark.sql.DataFrame = [COMM: double, DEPTNO: bigint ... 6 more fields]\n```\n\n<br/>\n\n## 二、Columns列操作\n\n### 2.1 引用列\n\nSpark 支持多种方法来构造和引用列，最简单的是使用 `col() ` 或 `column() ` 函数。\n\n```scala\ncol(\"colName\")\ncolumn(\"colName\")\n\n// 对于 Scala 语言而言，还可以使用$\"myColumn\"和'myColumn 这两种语法糖进行引用。\ndf.select($\"ename\", $\"job\").show()\ndf.select('ename, 'job).show()\n```\n\n### 2.2 新增列\n\n```scala\n// 基于已有列值新增列\ndf.withColumn(\"upSal\",$\"sal\"+1000)\n// 基于固定值新增列\ndf.withColumn(\"intCol\",lit(1000))\n```\n\n### 2.3 删除列\n\n```scala\n// 支持删除多个列\ndf.drop(\"comm\",\"job\").show()\n```\n\n### 2.4 重命名列\n\n```scala\ndf.withColumnRenamed(\"comm\", \"common\").show()\n```\n\n需要说明的是新增，删除，重命名列都会产生新的 DataFrame，原来的 DataFrame 不会被改变。\n\n<br/>\n\n## 三、使用Structured API进行基本查询\n\n```scala\n// 1.查询员工姓名及工作\ndf.select($\"ename\", $\"job\").show()\n\n// 2.filter 查询工资大于 2000 的员工信息\ndf.filter($\"sal\" > 2000).show()\n\n// 3.orderBy 按照部门编号降序，工资升序进行查询\ndf.orderBy(desc(\"deptno\"), asc(\"sal\")).show()\n\n// 4.limit 查询工资最高的 3 名员工的信息\ndf.orderBy(desc(\"sal\")).limit(3).show()\n\n// 5.distinct 查询所有部门编号\ndf.select(\"deptno\").distinct().show()\n\n// 6.groupBy 分组统计部门人数\ndf.groupBy(\"deptno\").count().show()\n```\n\n<br/>\n\n## 四、使用Spark SQL进行基本查询\n\n### 4.1 Spark  SQL基本使用\n\n```scala\n// 1.首先需要将 DataFrame 注册为临时视图\ndf.createOrReplaceTempView(\"emp\")\n\n// 2.查询员工姓名及工作\nspark.sql(\"SELECT ename,job FROM emp\").show()\n\n// 3.查询工资大于 2000 的员工信息\nspark.sql(\"SELECT * FROM emp where sal > 2000\").show()\n\n// 4.orderBy 按照部门编号降序，工资升序进行查询\nspark.sql(\"SELECT * FROM emp ORDER BY deptno DESC,sal ASC\").show()\n\n// 5.limit  查询工资最高的 3 名员工的信息\nspark.sql(\"SELECT * FROM emp ORDER BY sal DESC LIMIT 3\").show()\n\n// 6.distinct 查询所有部门编号\nspark.sql(\"SELECT DISTINCT(deptno) FROM emp\").show()\n\n// 7.分组统计部门人数\nspark.sql(\"SELECT deptno,count(ename) FROM emp group by deptno\").show()\n```\n\n### 4.2 全局临时视图\n\n上面使用 `createOrReplaceTempView` 创建的是会话临时视图，它的生命周期仅限于会话范围，会随会话的结束而结束。\n\n你也可以使用 `createGlobalTempView` 创建全局临时视图，全局临时视图可以在所有会话之间共享，并直到整个 Spark 应用程序终止后才会消失。全局临时视图被定义在内置的 `global_temp` 数据库下，需要使用限定名称进行引用，如 `SELECT * FROM global_temp.view1`。\n\n```scala\n// 注册为全局临时视图\ndf.createGlobalTempView(\"gemp\")\n\n// 使用限定名称进行引用\nspark.sql(\"SELECT ename,job FROM global_temp.gemp\").show()\n```\n\n\n\n## 参考资料\n\n[Spark SQL, DataFrames and Datasets Guide > Getting Started](https://spark.apache.org/docs/latest/sql-getting-started.html)\n"
  },
  {
    "path": "大数据框架学习/Spark_Transformation和Action算子.md",
    "content": "# Transformation 和 Action 常用算子\n\n<nav>\n<a href=\"#一Transformation\">一、Transformation</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-map\">1.1 map</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-filter\">1.2 filter</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-flatMap\">1.3 flatMap</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-mapPartitions\">1.4 mapPartitions</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-mapPartitionsWithIndex\">1.5 mapPartitionsWithIndex</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-sample\">1.6 sample</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#17-union\">1.7 union</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#18-intersection\">1.8 intersection</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#19-distinct\">1.9 distinct</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#110-groupByKey\">1.10 groupByKey</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#111-reduceByKey\">1.11 reduceByKey</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#112-sortBy--sortByKey\">1.12 sortBy & sortByKey </a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#113-join\">1.13 join</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#114-cogroup\">1.14 cogroup</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#115-cartesian\">1.15 cartesian</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#116-aggregateByKey\">1.16 aggregateByKey</a><br/>\n<a href=\"#二Action\">二、Action</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-reduce\">2.1 reduce</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-takeOrdered\">2.2 takeOrdered</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-countByKey\">2.3 countByKey</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-saveAsTextFile\">2.4 saveAsTextFile</a><br/>\n</nav>\n\n## 一、Transformation\n\nspark 常用的 Transformation 算子如下表：\n\n| Transformation 算子                                           | Meaning（含义）                                              |\n| ------------------------------------------------------------ | ------------------------------------------------------------ |\n| **map**(*func*)                                              | 对原 RDD 中每个元素运用 *func* 函数，并生成新的 RDD             |\n| **filter**(*func*)                                           | 对原 RDD 中每个元素使用*func* 函数进行过滤，并生成新的 RDD      |\n| **flatMap**(*func*)                                          | 与 map 类似，但是每一个输入的 item 被映射成 0 个或多个输出的 items（ *func* 返回类型需要为 Seq ）。 |\n| **mapPartitions**(*func*)                                    | 与 map 类似，但函数单独在 RDD 的每个分区上运行， *func*函数的类型为  Iterator\\<T> => Iterator\\<U> ，其中 T 是 RDD 的类型，即 RDD[T] |\n| **mapPartitionsWithIndex**(*func*)                           | 与 mapPartitions 类似，但 *func* 类型为 (Int, Iterator\\<T>) => Iterator\\<U> ，其中第一个参数为分区索引 |\n| **sample**(*withReplacement*, *fraction*, *seed*)            | 数据采样，有三个可选参数：设置是否放回（withReplacement）、采样的百分比（*fraction*）、随机数生成器的种子（seed）； |\n| **union**(*otherDataset*)                                    | 合并两个 RDD                                                  |\n| **intersection**(*otherDataset*)                             | 求两个 RDD 的交集                                              |\n| **distinct**([*numTasks*]))                                  | 去重                                                         |\n| **groupByKey**([*numTasks*])                                 | 按照 key 值进行分区，即在一个 (K, V) 对的 dataset 上调用时，返回一个 (K, Iterable\\<V>) <br/>**Note:** 如果分组是为了在每一个 key 上执行聚合操作（例如，sum 或 average)，此时使用 `reduceByKey` 或 `aggregateByKey` 性能会更好<br>**Note:** 默认情况下，并行度取决于父 RDD 的分区数。可以传入 `numTasks` 参数进行修改。 |\n| **reduceByKey**(*func*, [*numTasks*])                        | 按照 key 值进行分组，并对分组后的数据执行归约操作。            |\n| **aggregateByKey**(*zeroValue*,*numPartitions*)(*seqOp*, *combOp*, [*numTasks*]) | 当调用（K，V）对的数据集时，返回（K，U）对的数据集，其中使用给定的组合函数和 zeroValue 聚合每个键的值。与 groupByKey 类似，reduce 任务的数量可通过第二个参数进行配置。 |\n| **sortByKey**([*ascending*], [*numTasks*])                   | 按照 key 进行排序，其中的 key 需要实现 Ordered 特质，即可比较      |\n| **join**(*otherDataset*, [*numTasks*])                       | 在一个 (K, V) 和 (K, W) 类型的 dataset 上调用时，返回一个 (K, (V, W)) pairs 的 dataset，等价于内连接操作。如果想要执行外连接，可以使用 `leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。 |\n| **cogroup**(*otherDataset*, [*numTasks*])                    | 在一个 (K, V) 对的 dataset 上调用时，返回一个 (K, (Iterable\\<V>, Iterable\\<W>)) tuples 的 dataset。 |\n| **cartesian**(*otherDataset*)                                | 在一个 T 和 U 类型的 dataset 上调用时，返回一个 (T, U) 类型的 dataset（即笛卡尔积）。 |\n| **coalesce**(*numPartitions*)                                | 将 RDD 中的分区数减少为 numPartitions。                         |\n| **repartition**(*numPartitions*)                             | 随机重新调整 RDD 中的数据以创建更多或更少的分区，并在它们之间进行平衡。 |\n| **repartitionAndSortWithinPartitions**(*partitioner*)        | 根据给定的 partitioner（分区器）对 RDD 进行重新分区，并对分区中的数据按照 key 值进行排序。这比调用 `repartition` 然后再 sorting（排序）效率更高，因为它可以将排序过程推送到 shuffle 操作所在的机器。 |\n\n下面分别给出这些算子的基本使用示例：\n\n### 1.1 map\n\n```scala\nval list = List(1,2,3)\nsc.parallelize(list).map(_ * 10).foreach(println)\n\n// 输出结果： 10 20 30 （这里为了节省篇幅去掉了换行,后文亦同）\n```\n\n### 1.2 filter\n\n```scala\nval list = List(3, 6, 9, 10, 12, 21)\nsc.parallelize(list).filter(_ >= 10).foreach(println)\n\n// 输出： 10 12 21\n```\n\n### 1.3 flatMap\n\n`flatMap(func)` 与 `map` 类似，但每一个输入的 item 会被映射成 0 个或多个输出的 items（ *func* 返回类型需要为 `Seq`）。\n\n```scala\nval list = List(List(1, 2), List(3), List(), List(4, 5))\nsc.parallelize(list).flatMap(_.toList).map(_ * 10).foreach(println)\n\n// 输出结果 ： 10 20 30 40 50\n```\n\nflatMap 这个算子在日志分析中使用概率非常高，这里进行一下演示：拆分输入的每行数据为单个单词，并赋值为 1，代表出现一次，之后按照单词分组并统计其出现总次数，代码如下：\n\n```scala\nval lines = List(\"spark flume spark\",\n                 \"hadoop flume hive\")\nsc.parallelize(lines).flatMap(line => line.split(\" \")).\nmap(word=>(word,1)).reduceByKey(_+_).foreach(println)\n\n// 输出：\n(spark,2)\n(hive,1)\n(hadoop,1)\n(flume,2)\n```\n\n### 1.4 mapPartitions\n\n与 map 类似，但函数单独在 RDD 的每个分区上运行， *func*函数的类型为 `Iterator<T> => Iterator<U>` (其中 T 是 RDD 的类型)，即输入和输出都必须是可迭代类型。\n\n```scala\nval list = List(1, 2, 3, 4, 5, 6)\nsc.parallelize(list, 3).mapPartitions(iterator => {\n  val buffer = new ListBuffer[Int]\n  while (iterator.hasNext) {\n    buffer.append(iterator.next() * 100)\n  }\n  buffer.toIterator\n}).foreach(println)\n//输出结果\n100 200 300 400 500 600\n```\n\n### 1.5 mapPartitionsWithIndex\n\n  与 mapPartitions 类似，但 *func* 类型为 `(Int, Iterator<T>) => Iterator<U>` ，其中第一个参数为分区索引。\n\n```scala\nval list = List(1, 2, 3, 4, 5, 6)\nsc.parallelize(list, 3).mapPartitionsWithIndex((index, iterator) => {\n  val buffer = new ListBuffer[String]\n  while (iterator.hasNext) {\n    buffer.append(index + \"分区:\" + iterator.next() * 100)\n  }\n  buffer.toIterator\n}).foreach(println)\n//输出\n0 分区:100\n0 分区:200\n1 分区:300\n1 分区:400\n2 分区:500\n2 分区:600\n```\n\n### 1.6 sample\n\n  数据采样。有三个可选参数：设置是否放回 (withReplacement)、采样的百分比 (fraction)、随机数生成器的种子 (seed) ：\n\n```scala\nval list = List(1, 2, 3, 4, 5, 6)\nsc.parallelize(list).sample(withReplacement = false, fraction = 0.5).foreach(println)\n```\n\n### 1.7 union\n\n合并两个 RDD：\n\n```scala\nval list1 = List(1, 2, 3)\nval list2 = List(4, 5, 6)\nsc.parallelize(list1).union(sc.parallelize(list2)).foreach(println)\n// 输出: 1 2 3 4 5 6\n```\n\n### 1.8 intersection\n\n求两个 RDD 的交集：\n\n```scala\nval list1 = List(1, 2, 3, 4, 5)\nval list2 = List(4, 5, 6)\nsc.parallelize(list1).intersection(sc.parallelize(list2)).foreach(println)\n// 输出:  4 5\n```\n\n### 1.9 distinct\n\n去重：\n\n```scala\nval list = List(1, 2, 2, 4, 4)\nsc.parallelize(list).distinct().foreach(println)\n// 输出: 4 1 2\n```\n\n### 1.10 groupByKey\n\n按照键进行分组：\n\n```scala\nval list = List((\"hadoop\", 2), (\"spark\", 3), (\"spark\", 5), (\"storm\", 6), (\"hadoop\", 2))\nsc.parallelize(list).groupByKey().map(x => (x._1, x._2.toList)).foreach(println)\n\n//输出：\n(spark,List(3, 5))\n(hadoop,List(2, 2))\n(storm,List(6))\n```\n\n### 1.11 reduceByKey\n\n按照键进行归约操作：\n\n```scala\nval list = List((\"hadoop\", 2), (\"spark\", 3), (\"spark\", 5), (\"storm\", 6), (\"hadoop\", 2))\nsc.parallelize(list).reduceByKey(_ + _).foreach(println)\n\n//输出\n(spark,8)\n(hadoop,4)\n(storm,6)\n```\n\n### 1.12 sortBy & sortByKey \n\n按照键进行排序：\n\n```scala\nval list01 = List((100, \"hadoop\"), (90, \"spark\"), (120, \"storm\"))\nsc.parallelize(list01).sortByKey(ascending = false).foreach(println)\n// 输出\n(120,storm)\n(100,hadoop)\n(90,spark)\n```\n\n按照指定元素进行排序：\n\n```scala\nval list02 = List((\"hadoop\",100), (\"spark\",90), (\"storm\",120))\nsc.parallelize(list02).sortBy(x=>x._2,ascending=false).foreach(println)\n// 输出\n(storm,120)\n(hadoop,100)\n(spark,90)\n```\n\n### 1.13 join\n\n在一个 (K, V) 和 (K, W) 类型的 Dataset 上调用时，返回一个 (K, (V, W)) 的 Dataset，等价于内连接操作。如果想要执行外连接，可以使用 `leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。\n\n```scala\nval list01 = List((1, \"student01\"), (2, \"student02\"), (3, \"student03\"))\nval list02 = List((1, \"teacher01\"), (2, \"teacher02\"), (3, \"teacher03\"))\nsc.parallelize(list01).join(sc.parallelize(list02)).foreach(println)\n\n// 输出\n(1,(student01,teacher01))\n(3,(student03,teacher03))\n(2,(student02,teacher02))\n```\n\n### 1.14 cogroup\n\n在一个 (K, V) 对的 Dataset 上调用时，返回多个类型为 (K, (Iterable\\<V>, Iterable\\<W>)) 的元组所组成的 Dataset。\n\n```scala\nval list01 = List((1, \"a\"),(1, \"a\"), (2, \"b\"), (3, \"e\"))\nval list02 = List((1, \"A\"), (2, \"B\"), (3, \"E\"))\nval list03 = List((1, \"[ab]\"), (2, \"[bB]\"), (3, \"eE\"),(3, \"eE\"))\nsc.parallelize(list01).cogroup(sc.parallelize(list02),sc.parallelize(list03)).foreach(println)\n\n// 输出： 同一个 RDD 中的元素先按照 key 进行分组，然后再对不同 RDD 中的元素按照 key 进行分组\n(1,(CompactBuffer(a, a),CompactBuffer(A),CompactBuffer([ab])))\n(3,(CompactBuffer(e),CompactBuffer(E),CompactBuffer(eE, eE)))\n(2,(CompactBuffer(b),CompactBuffer(B),CompactBuffer([bB])))\n\n```\n\n### 1.15 cartesian\n\n计算笛卡尔积：\n\n```scala\nval list1 = List(\"A\", \"B\", \"C\")\nval list2 = List(1, 2, 3)\nsc.parallelize(list1).cartesian(sc.parallelize(list2)).foreach(println)\n\n//输出笛卡尔积\n(A,1)\n(A,2)\n(A,3)\n(B,1)\n(B,2)\n(B,3)\n(C,1)\n(C,2)\n(C,3)\n```\n\n### 1.16 aggregateByKey\n\n当调用（K，V）对的数据集时，返回（K，U）对的数据集，其中使用给定的组合函数和 zeroValue 聚合每个键的值。与 `groupByKey` 类似，reduce 任务的数量可通过第二个参数 `numPartitions` 进行配置。示例如下：\n\n```scala\n// 为了清晰，以下所有参数均使用具名传参\nval list = List((\"hadoop\", 3), (\"hadoop\", 2), (\"spark\", 4), (\"spark\", 3), (\"storm\", 6), (\"storm\", 8))\nsc.parallelize(list,numSlices = 2).aggregateByKey(zeroValue = 0,numPartitions = 3)(\n      seqOp = math.max(_, _),\n      combOp = _ + _\n    ).collect.foreach(println)\n//输出结果：\n(hadoop,3)\n(storm,8)\n(spark,7)\n```\n\n这里使用了 `numSlices = 2` 指定 aggregateByKey 父操作 parallelize 的分区数量为 2，其执行流程如下：\n\n<div align=\"center\"> <img src=\"../pictures/spark-aggregateByKey.png\"/> </div>\n\n基于同样的执行流程，如果 `numSlices = 1`，则意味着只有输入一个分区，则其最后一步 combOp 相当于是无效的，执行结果为：\n\n```properties\n(hadoop,3)\n(storm,8)\n(spark,4)\n```\n\n同样的，如果每个单词对一个分区，即 `numSlices = 6`，此时相当于求和操作，执行结果为：\n\n```properties\n(hadoop,5)\n(storm,14)\n(spark,7)\n```\n\n`aggregateByKey(zeroValue = 0,numPartitions = 3)` 的第二个参数 `numPartitions` 决定的是输出 RDD 的分区数量，想要验证这个问题，可以对上面代码进行改写，使用 `getNumPartitions` 方法获取分区数量：\n\n```scala\nsc.parallelize(list,numSlices = 6).aggregateByKey(zeroValue = 0,numPartitions = 3)(\n  seqOp = math.max(_, _),\n  combOp = _ + _\n).getNumPartitions\n```\n\n<div align=\"center\"> <img src=\"../pictures/spark-getpartnum.png\"/> </div>\n\n## 二、Action\n\nSpark 常用的 Action 算子如下：\n\n| Action（动作）                                     | Meaning（含义）                                              |\n| -------------------------------------------------- | ------------------------------------------------------------ |\n| **reduce**(*func*)                                 | 使用函数*func*执行归约操作                                   |\n| **collect**()                                      | 以一个 array 数组的形式返回 dataset 的所有元素，适用于小结果集。 |\n| **count**()                                        | 返回 dataset 中元素的个数。                                  |\n| **first**()                                        | 返回 dataset 中的第一个元素，等价于 take(1)。                |\n| **take**(*n*)                                      | 将数据集中的前 *n* 个元素作为一个 array 数组返回。           |\n| **takeSample**(*withReplacement*, *num*, [*seed*]) | 对一个 dataset 进行随机抽样                                  |\n| **takeOrdered**(*n*, *[ordering]*)                 | 按自然顺序（natural order）或自定义比较器（custom comparator）排序后返回前 *n* 个元素。只适用于小结果集，因为所有数据都会被加载到驱动程序的内存中进行排序。 |\n| **saveAsTextFile**(*path*)                         | 将 dataset 中的元素以文本文件的形式写入本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。Spark 将对每个元素调用 toString 方法，将元素转换为文本文件中的一行记录。 |\n| **saveAsSequenceFile**(*path*)                     | 将 dataset 中的元素以 Hadoop SequenceFile 的形式写入到本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。该操作要求 RDD 中的元素需要实现 Hadoop 的 Writable 接口。对于 Scala 语言而言，它可以将 Spark 中的基本数据类型自动隐式转换为对应 Writable 类型。(目前仅支持 Java and Scala) |\n| **saveAsObjectFile**(*path*)                       | 使用 Java 序列化后存储，可以使用 `SparkContext.objectFile()` 进行加载。(目前仅支持 Java and Scala) |\n| **countByKey**()                                   | 计算每个键出现的次数。                                       |\n| **foreach**(*func*)                                | 遍历 RDD 中每个元素，并对其执行*fun*函数                       |\n\n### 2.1 reduce\n\n使用函数*func*执行归约操作：\n\n```scala\n val list = List(1, 2, 3, 4, 5)\nsc.parallelize(list).reduce((x, y) => x + y)\nsc.parallelize(list).reduce(_ + _)\n\n// 输出 15\n```\n\n### 2.2 takeOrdered\n\n按自然顺序（natural order）或自定义比较器（custom comparator）排序后返回前 *n* 个元素。需要注意的是 `takeOrdered` 使用隐式参数进行隐式转换，以下为其源码。所以在使用自定义排序时，需要继承 `Ordering[T]` 实现自定义比较器，然后将其作为隐式参数引入。\n\n```scala\ndef takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {\n  .........\n}\n```\n\n自定义规则排序：\n\n```scala\n// 继承 Ordering[T],实现自定义比较器，按照 value 值的长度进行排序\nclass CustomOrdering extends Ordering[(Int, String)] {\n    override def compare(x: (Int, String), y: (Int, String)): Int\n    = if (x._2.length > y._2.length) 1 else -1\n}\n\nval list = List((1, \"hadoop\"), (1, \"storm\"), (1, \"azkaban\"), (1, \"hive\"))\n//  引入隐式默认值\nimplicit val implicitOrdering = new CustomOrdering\nsc.parallelize(list).takeOrdered(5)\n\n// 输出： Array((1,hive), (1,storm), (1,hadoop), (1,azkaban)\n```\n\n### 2.3 countByKey\n\n计算每个键出现的次数：\n\n```scala\nval list = List((\"hadoop\", 10), (\"hadoop\", 10), (\"storm\", 3), (\"storm\", 3), (\"azkaban\", 1))\nsc.parallelize(list).countByKey()\n\n// 输出： Map(hadoop -> 2, storm -> 2, azkaban -> 1)\n```\n\n### 2.4 saveAsTextFile\n\n将 dataset 中的元素以文本文件的形式写入本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。Spark 将对每个元素调用 toString 方法，将元素转换为文本文件中的一行记录。\n\n```scala\nval list = List((\"hadoop\", 10), (\"hadoop\", 10), (\"storm\", 3), (\"storm\", 3), (\"azkaban\", 1))\nsc.parallelize(list).saveAsTextFile(\"/usr/file/temp\")\n```\n\n\n\n\n\n## 参考资料\n\n[RDD Programming Guide](http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide)\n\n"
  },
  {
    "path": "大数据框架学习/Spark简介.md",
    "content": "# Spark简介\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二特点\">二、特点</a><br/>\n<a href=\"#三集群架构\">三、集群架构</a><br/>\n<a href=\"#四核心组件\">四、核心组件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-Spark--SQL\">3.1 Spark  SQL</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-Spark-Streaming\">3.2 Spark Streaming</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-MLlib\">3.3 MLlib</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-Graphx\">3.4 Graphx</a><br/>\n<a href=\"#\">  </a><br/>\n</nav>\n\n## 一、简介\n\nSpark 于 2009 年诞生于加州大学伯克利分校 AMPLab，2013 年被捐赠给 Apache 软件基金会，2014 年 2 月成为 Apache 的顶级项目。相对于 MapReduce 的批处理计算，Spark 可以带来上百倍的性能提升，因此它成为继 MapReduce 之后，最为广泛使用的分布式计算框架。\n\n## 二、特点\n\nApache Spark 具有以下特点：\n\n+ 使用先进的 DAG 调度程序，查询优化器和物理执行引擎，以实现性能上的保证；\n+ 多语言支持，目前支持的有 Java，Scala，Python 和 R；\n+ 提供了 80 多个高级 API，可以轻松地构建应用程序；\n+ 支持批处理，流处理和复杂的业务分析；\n+ 丰富的类库支持：包括 SQL，MLlib，GraphX 和 Spark Streaming 等库，并且可以将它们无缝地进行组合；  \n+ 丰富的部署模式：支持本地模式和自带的集群模式，也支持在 Hadoop，Mesos，Kubernetes 上运行；\n+ 多数据源支持：支持访问 HDFS，Alluxio，Cassandra，HBase，Hive 以及数百个其他数据源中的数据。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/future-of-spark.png\"/> </div>\n\n## 三、集群架构\n\n| Term（术语）    | Meaning（含义）                                              |\n| --------------- | ------------------------------------------------------------ |\n| Application     | Spark 应用程序，由集群上的一个 Driver 节点和多个 Executor 节点组成。 |\n| Driver program  | 主运用程序，该进程运行应用的 main() 方法并且创建  SparkContext |\n| Cluster manager | 集群资源管理器（例如，Standlone Manager，Mesos，YARN）       |\n| Worker node     | 执行计算任务的工作节点                                       |\n| Executor        | 位于工作节点上的应用进程，负责执行计算任务并且将输出数据保存到内存或者磁盘中 |\n| Task            | 被发送到 Executor 中的工作单元                                 |\n\n<div align=\"center\"> <img src=\"../pictures/spark-集群模式.png\"/> </div>\n\n**执行过程**：\n\n1. 用户程序创建 SparkContext 后，它会连接到集群资源管理器，集群资源管理器会为用户程序分配计算资源，并启动 Executor；\n2. Dirver 将计算程序划分为不同的执行阶段和多个 Task，之后将 Task 发送给 Executor；\n3. Executor 负责执行 Task，并将执行状态汇报给 Driver，同时也会将当前节点资源的使用情况汇报给集群资源管理器。\n\n## 四、核心组件\n\nSpark 基于 Spark Core 扩展了四个核心组件，分别用于满足不同领域的计算需求。\n\n<div align=\"center\"> <img  width=\"600px\" src=\"../pictures/spark-stack.png\"/> </div>\n\n### 3.1 Spark  SQL\n\nSpark SQL 主要用于结构化数据的处理。其具有以下特点：\n\n- 能够将 SQL 查询与 Spark 程序无缝混合，允许您使用 SQL 或 DataFrame API 对结构化数据进行查询；\n- 支持多种数据源，包括 Hive，Avro，Parquet，ORC，JSON 和 JDBC；\n- 支持 HiveQL 语法以及用户自定义函数 (UDF)，允许你访问现有的 Hive 仓库；\n- 支持标准的 JDBC 和 ODBC 连接；\n- 支持优化器，列式存储和代码生成等特性，以提高查询效率。\n\n### 3.2 Spark Streaming\n\nSpark Streaming 主要用于快速构建可扩展，高吞吐量，高容错的流处理程序。支持从 HDFS，Flume，Kafka，Twitter 和 ZeroMQ 读取数据，并进行处理。\n\n<div align=\"center\"> <img width=\"600px\" src=\"../pictures/spark-streaming-arch.png\"/> </div>\n\n Spark Streaming 的本质是微批处理，它将数据流进行极小粒度的拆分，拆分为多个批处理，从而达到接近于流处理的效果。\n\n<div align=\"center\"> <img width=\"600px\"   src=\"../pictures/spark-streaming-flow.png\"/> </div>\n\n\n\n### 3.3 MLlib\n\nMLlib 是 Spark 的机器学习库。其设计目标是使得机器学习变得简单且可扩展。它提供了以下工具：\n\n+ **常见的机器学习算法**：如分类，回归，聚类和协同过滤；\n+ **特征化**：特征提取，转换，降维和选择；\n+ **管道**：用于构建，评估和调整 ML 管道的工具；\n+ **持久性**：保存和加载算法，模型，管道数据；\n+ **实用工具**：线性代数，统计，数据处理等。\n\n### 3.4 Graphx\n\nGraphX 是 Spark 中用于图形计算和图形并行计算的新组件。在高层次上，GraphX 通过引入一个新的图形抽象来扩展 RDD(一种具有附加到每个顶点和边缘的属性的定向多重图形)。为了支持图计算，GraphX 提供了一组基本运算符（如： subgraph，joinVertices 和 aggregateMessages）以及优化后的 Pregel API。此外，GraphX 还包括越来越多的图形算法和构建器，以简化图形分析任务。\n\n##   \n"
  },
  {
    "path": "大数据框架学习/Spark累加器与广播变量.md",
    "content": "# Spark 累加器与广播变量\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二累加器\">二、累加器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-理解闭包\">2.1 理解闭包</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-使用累加器\">2.2 使用累加器</a><br/>\n<a href=\"#三广播变量\">三、广播变量</a><br/>\n</nav>\n\n## 一、简介\n\n在 Spark 中，提供了两种类型的共享变量：累加器 (accumulator) 与广播变量 (broadcast variable)：\n\n+ **累加器**：用来对信息进行聚合，主要用于累计计数等场景；\n+ **广播变量**：主要用于在节点间高效分发大对象。\n\n## 二、累加器\n\n这里先看一个具体的场景，对于正常的累计求和，如果在集群模式中使用下面的代码进行计算，会发现执行结果并非预期：\n\n```scala\nvar counter = 0\nval data = Array(1, 2, 3, 4, 5)\nsc.parallelize(data).foreach(x => counter += x)\n println(counter)\n```\n\ncounter 最后的结果是 0，导致这个问题的主要原因是闭包。\n\n<div align=\"center\"> <img src=\"../pictures/spark-累加器1.png\"/> </div>\n\n\n\n### 2.1 理解闭包\n\n**1. Scala 中闭包的概念**\n\n这里先介绍一下 Scala 中关于闭包的概念：\n\n```\nvar more = 10\nval addMore = (x: Int) => x + more\n```\n\n如上函数 `addMore` 中有两个变量 x 和 more:\n\n- **x** : 是一个绑定变量 (bound variable)，因为其是该函数的入参，在函数的上下文中有明确的定义；\n- **more** : 是一个自由变量 (free variable)，因为函数字面量本生并没有给 more 赋予任何含义。\n\n按照定义：在创建函数时，如果需要捕获自由变量，那么包含指向被捕获变量的引用的函数就被称为闭包函数。\n\n**2. Spark 中的闭包**\n\n在实际计算时，Spark 会将对 RDD 操作分解为 Task，Task 运行在 Worker Node 上。在执行之前，Spark 会对任务进行闭包，如果闭包内涉及到自由变量，则程序会进行拷贝，并将副本变量放在闭包中，之后闭包被序列化并发送给每个执行者。因此，当在 foreach 函数中引用 `counter` 时，它将不再是 Driver 节点上的 `counter`，而是闭包中的副本 `counter`，默认情况下，副本 `counter` 更新后的值不会回传到 Driver，所以 `counter` 的最终值仍然为零。\n\n需要注意的是：在 Local 模式下，有可能执行 `foreach` 的 Worker Node 与 Diver 处在相同的 JVM，并引用相同的原始 `counter`，这时候更新可能是正确的，但是在集群模式下一定不正确。所以在遇到此类问题时应优先使用累加器。\n\n累加器的原理实际上很简单：就是将每个副本变量的最终值传回 Driver，由 Driver 聚合后得到最终值，并更新原始变量。\n\n\n<div align=\"center\"> <img src=\"../pictures/spark-集群模式.png\"/> </div>\n\n### 2.2 使用累加器\n\n`SparkContext` 中定义了所有创建累加器的方法，需要注意的是：被中横线划掉的累加器方法在 Spark 2.0.0 之后被标识为废弃。\n\n<div align=\"center\"> <img src=\"../pictures/spark-累加器方法.png\"/> </div>\n\n使用示例和执行结果分别如下：\n\n```scala\nval data = Array(1, 2, 3, 4, 5)\n// 定义累加器\nval accum = sc.longAccumulator(\"My Accumulator\")\nsc.parallelize(data).foreach(x => accum.add(x))\n// 获取累加器的值\naccum.value\n```\n\n<div align=\"center\"> <img src=\"../pictures/spark-累加器2.png\"/> </div>\n\n\n\n## 三、广播变量\n\n在上面介绍中闭包的过程中我们说道每个 Task 任务的闭包都会持有自由变量的副本，如果变量很大且 Task 任务很多的情况下，这必然会对网络 IO 造成压力，为了解决这个情况，Spark 提供了广播变量。\n\n广播变量的做法很简单：就是不把副本变量分发到每个 Task 中，而是将其分发到每个 Executor，Executor 中的所有 Task 共享一个副本变量。\n\n```scala\n// 把一个数组定义为一个广播变量\nval broadcastVar = sc.broadcast(Array(1, 2, 3, 4, 5))\n// 之后用到该数组时应优先使用广播变量，而不是原值\nsc.parallelize(broadcastVar.value).map(_ * 10).collect()\n```\n\n\n\n\n\n## 参考资料\n\n[RDD Programming Guide](http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide)\n\n"
  },
  {
    "path": "大数据框架学习/Spark部署模式与作业提交.md",
    "content": "# Spark部署模式与作业提交\n\n<nav>\n<a href=\"#一作业提交\">一、作业提交</a><br/>\n<a href=\"#二Local模式\">二、Local模式</a><br/>\n<a href=\"#三Standalone模式\">三、Standalone模式</a><br/>\n<a href=\"#三Spark-on-Yarn模式\">三、Spark on Yarn模式</a><br/>\n</nav>\n\n\n## 一、作业提交\n\n### 1.1  spark-submit\n\nSpark 所有模式均使用 `spark-submit` 命令提交作业，其格式如下：\n\n```shell\n./bin/spark-submit \\\n  --class <main-class> \\        # 应用程序主入口类\n  --master <master-url> \\       # 集群的 Master Url\n  --deploy-mode <deploy-mode> \\ # 部署模式\n  --conf <key>=<value> \\        # 可选配置       \n  ... # other options    \n  <application-jar> \\           # Jar 包路径 \n  [application-arguments]       #传递给主入口类的参数  \n```\n\n需要注意的是：在集群环境下，`application-jar` 必须能被集群中所有节点都能访问，可以是 HDFS 上的路径；也可以是本地文件系统路径，如果是本地文件系统路径，则要求集群中每一个机器节点上的相同路径都存在该 Jar 包。\n\n### 1.2 deploy-mode\n\ndeploy-mode 有 `cluster` 和 `client` 两个可选参数，默认为 `client`。这里以 Spark On Yarn 模式对两者进行说明 ：\n\n+ 在 cluster 模式下，Spark Drvier 在应用程序的 Master 进程内运行，该进程由群集上的 YARN 管理，提交作业的客户端可以在启动应用程序后关闭；\n+ 在 client 模式下，Spark Drvier 在提交作业的客户端进程中运行，Master 进程仅用于从 YARN 请求资源。\n\n### 1.3 master-url\n\nmaster-url 的所有可选参数如下表所示：\n\n| Master URL                        | Meaning                                                      |\n| --------------------------------- | ------------------------------------------------------------ |\n| `local`                           | 使用一个线程本地运行 Spark                                    |\n| `local[K]`                        | 使用 K 个 worker 线程本地运行 Spark                          |\n| `local[K,F]`                      | 使用 K 个 worker 线程本地运行 , 第二个参数为 Task 的失败重试次数 |\n| `local[*]`                        | 使用与 CPU 核心数一样的线程数在本地运行 Spark                   |\n| `local[*,F]`                      | 使用与 CPU 核心数一样的线程数在本地运行 Spark<br/>第二个参数为 Task 的失败重试次数 |\n| `spark://HOST:PORT`               | 连接至指定的 standalone 集群的 master 节点。端口号默认是 7077。 |\n| `spark://HOST1:PORT1,HOST2:PORT2` | 如果 standalone 集群采用 Zookeeper 实现高可用，则必须包含由 zookeeper 设置的所有 master 主机地址。 |\n| `mesos://HOST:PORT`               | 连接至给定的 Mesos 集群。端口默认是 5050。对于使用了 ZooKeeper 的 Mesos cluster 来说，使用 `mesos://zk://...` 来指定地址，使用 `--deploy-mode cluster` 模式来提交。 |\n| `yarn`                            | 连接至一个 YARN 集群，集群由配置的 `HADOOP_CONF_DIR` 或者 `YARN_CONF_DIR` 来决定。使用 `--deploy-mode` 参数来配置 `client` 或 `cluster` 模式。 |\n\n下面主要介绍三种常用部署模式及对应的作业提交方式。\n\n## 二、Local模式\n\nLocal 模式下提交作业最为简单，不需要进行任何配置，提交命令如下：\n\n```shell\n# 本地模式提交应用\nspark-submit \\\n--class org.apache.spark.examples.SparkPi \\\n--master local[2] \\\n/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \\\n100   # 传给 SparkPi 的参数\n```\n\n`spark-examples_2.11-2.4.0.jar` 是 Spark 提供的测试用例包，`SparkPi` 用于计算 Pi 值，执行结果如下：\n\n<div align=\"center\"> <img src=\"../pictures/spark-pi.png\"/> </div>\n\n\n\n## 三、Standalone模式\n\nStandalone 是 Spark 提供的一种内置的集群模式，采用内置的资源管理器进行管理。下面按照如图所示演示 1 个 Mater 和 2 个 Worker 节点的集群配置，这里使用两台主机进行演示：\n\n+ hadoop001： 由于只有两台主机，所以 hadoop001 既是 Master 节点，也是 Worker 节点;\n+ hadoop002 ： Worker 节点。\n\n\n\n\n\n<div align=\"center\"> <img src=\"../pictures/spark-集群模式.png\"/> </div>\n\n### 3.1 环境配置\n\n首先需要保证 Spark 已经解压在两台主机的相同路径上。然后进入 hadoop001 的 `${SPARK_HOME}/conf/` 目录下，拷贝配置样本并进行相关配置：\n\n```shell\n# cp spark-env.sh.template spark-env.sh\n```\n\n在 `spark-env.sh` 中配置 JDK 的目录，完成后将该配置使用 scp 命令分发到 hadoop002 上：\n\n```shell\n# JDK安装位置\nJAVA_HOME=/usr/java/jdk1.8.0_201\n```\n\n### 3.2 集群配置\n\n在 `${SPARK_HOME}/conf/` 目录下，拷贝集群配置样本并进行相关配置：\n\n```\n# cp slaves.template slaves\n```\n\n指定所有 Worker 节点的主机名：\n\n```shell\n# A Spark Worker will be started on each of the machines listed below.\nhadoop001\nhadoop002\n```\n\n这里需要注意以下三点：\n\n+ 主机名与 IP 地址的映射必须在 `/etc/hosts` 文件中已经配置，否则就直接使用 IP 地址；\n+ 每个主机名必须独占一行；\n+ Spark 的 Master 主机是通过 SSH 访问所有的 Worker 节点，所以需要预先配置免密登录。\n\n### 3.3 启动\n\n使用 `start-all.sh` 代表启动 Master 和所有 Worker 服务。\n\n```shell\n./sbin/start-master.sh \n```\n\n访问 8080 端口，查看 Spark 的 Web-UI 界面,，此时应该显示有两个有效的工作节点：\n\n<div align=\"center\"> <img src=\"../pictures/spark-Standalone-web-ui.png\"/> </div>\n\n### 3.4 提交作业\n\n```shell\n# 以client模式提交到standalone集群 \nspark-submit \\\n--class org.apache.spark.examples.SparkPi \\\n--master spark://hadoop001:7077 \\\n--executor-memory 2G \\\n--total-executor-cores 10 \\\n/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \\\n100\n\n# 以cluster模式提交到standalone集群 \nspark-submit \\\n--class org.apache.spark.examples.SparkPi \\\n--master spark://207.184.161.138:7077 \\\n--deploy-mode cluster \\\n--supervise \\  # 配置此参数代表开启监督，如果主应用程序异常退出，则自动重启 Driver\n--executor-memory 2G \\\n--total-executor-cores 10 \\\n/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \\\n100\n```\n\n### 3.5 可选配置\n\n在虚拟机上提交作业时经常出现一个的问题是作业无法申请到足够的资源：\n\n```properties\nInitial job has not accepted any resources; \ncheck your cluster UI to ensure that workers are registered and have sufficient resources\n```\n\n<div align=\"center\"> <img src=\"../pictures/spark-内存不足2.png\"/> </div>\n\n<br/>\n\n这时候可以查看 Web UI，我这里是内存空间不足：提交命令中要求作业的 `executor-memory` 是 2G，但是实际的工作节点的 `Memory` 只有 1G，这时候你可以修改 `--executor-memory`，也可以修改 Woker 的 `Memory`，其默认值为主机所有可用内存值减去 1G。\n\n<div align=\"center\"> <img src=\"../pictures/spark-内存不足.png\"/> </div>   \n\n<br/>\n\n关于 Master 和 Woker 节点的所有可选配置如下，可以在 `spark-env.sh` 中进行对应的配置：    \n\n| Environment Variable（环境变量） | Meaning（含义）                                              |\n| -------------------------------- | ------------------------------------------------------------ |\n| `SPARK_MASTER_HOST`              | master 节点地址                                              |\n| `SPARK_MASTER_PORT`              | master 节点地址端口（默认：7077）                            |\n| `SPARK_MASTER_WEBUI_PORT`        | master 的 web UI 的端口（默认：8080）                        |\n| `SPARK_MASTER_OPTS`              | 仅用于 master 的配置属性，格式是 \"-Dx=y\"（默认：none）,所有属性可以参考官方文档：[spark-standalone-mode](https://spark.apache.org/docs/latest/spark-standalone.html#spark-standalone-mode) |\n| `SPARK_LOCAL_DIRS`               | spark 的临时存储的目录，用于暂存 map 的输出和持久化存储 RDDs。多个目录用逗号分隔 |\n| `SPARK_WORKER_CORES`             | spark worker 节点可以使用 CPU Cores 的数量。（默认：全部可用）  |\n| `SPARK_WORKER_MEMORY`            | spark worker 节点可以使用的内存数量（默认：全部的内存减去 1GB）； |\n| `SPARK_WORKER_PORT`              | spark worker 节点的端口（默认： random（随机））              |\n| `SPARK_WORKER_WEBUI_PORT`        | worker 的 web UI 的 Port（端口）（默认：8081）               |\n| `SPARK_WORKER_DIR`               | worker 运行应用程序的目录，这个目录中包含日志和暂存空间（default：SPARK_HOME/work） |\n| `SPARK_WORKER_OPTS`              | 仅用于 worker 的配置属性，格式是 \"-Dx=y\"（默认：none）。所有属性可以参考官方文档：[spark-standalone-mode](https://spark.apache.org/docs/latest/spark-standalone.html#spark-standalone-mode) |\n| `SPARK_DAEMON_MEMORY`            | 分配给 spark master 和 worker 守护进程的内存。（默认： 1G）  |\n| `SPARK_DAEMON_JAVA_OPTS`         | spark master 和 worker 守护进程的 JVM 选项，格式是 \"-Dx=y\"（默认：none） |\n| `SPARK_PUBLIC_DNS`               | spark master 和 worker 的公开 DNS 名称。（默认：none）       |\n\n\n\n## 三、Spark on Yarn模式\n\nSpark 支持将作业提交到 Yarn 上运行，此时不需要启动 Master 节点，也不需要启动 Worker 节点。\n\n### 3.1 配置\n\n在 `spark-env.sh` 中配置 hadoop 的配置目录的位置，可以使用 `YARN_CONF_DIR` 或 `HADOOP_CONF_DIR` 进行指定：\n\n```properties\nYARN_CONF_DIR=/usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop\n# JDK安装位置\nJAVA_HOME=/usr/java/jdk1.8.0_201\n```\n\n### 3.2 启动\n\n必须要保证 Hadoop 已经启动，这里包括 YARN 和 HDFS 都需要启动，因为在计算过程中 Spark 会使用 HDFS 存储临时文件，如果 HDFS 没有启动，则会抛出异常。\n\n```shell\n# start-yarn.sh\n# start-dfs.sh\n```\n\n### 3.3 提交应用\n\n```shell\n#  以client模式提交到yarn集群 \nspark-submit \\\n--class org.apache.spark.examples.SparkPi \\\n--master yarn \\\n--deploy-mode client \\\n--executor-memory 2G \\\n--num-executors 10 \\\n/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \\\n100\n\n#  以cluster模式提交到yarn集群 \nspark-submit \\\n--class org.apache.spark.examples.SparkPi \\\n--master yarn \\\n--deploy-mode cluster \\\n--executor-memory 2G \\\n--num-executors 10 \\\n/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \\\n100\n```\n\n\n\n"
  },
  {
    "path": "大数据框架学习/Spring+Mybtais+Phoenix整合.md",
    "content": "# Spring/Spring Boot 整合 Mybatis + Phoenix\n\n<nav>\n<a href=\"#一前言\">一、前言</a><br/>\n<a href=\"#二Spring-+-Mybatis-+-Phoenix\">二、Spring + Mybatis + Phoenix</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-项目结构\">2.1 项目结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-主要依赖\">2.2 主要依赖</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23--数据库配置文件\">2.3  数据库配置文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24--配置数据源和会话工厂\">2.4  配置数据源和会话工厂</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-Mybtais参数配置\">2.5 Mybtais参数配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-查询接口\">2.6 查询接口</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#27-单元测试\">2.7 单元测试</a><br/>\n<a href=\"#三SpringBoot-+-Mybatis-+-Phoenix\">三、SpringBoot + Mybatis + Phoenix</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-项目结构\">3.1 项目结构</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-主要依赖\">3.2 主要依赖</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-配置数据源\">3.3 配置数据源</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-新建查询接口\">3.4 新建查询接口</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-单元测试\">3.5 单元测试</a><br/>\n<a href=\"#附建表语句\">附：建表语句</a><br/>\n</nav>\n\n## 一、前言\n\n使用 Spring+Mybatis 操作 Phoenix 和操作其他的关系型数据库（如 Mysql，Oracle）在配置上是基本相同的，下面会分别给出 Spring/Spring Boot 整合步骤，完整代码见本仓库：\n\n+ [Spring + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/tree/master/code/Phoenix/spring-mybatis-phoenix)\n+ [SpringBoot + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/tree/master/code/Phoenix/spring-boot-mybatis-phoenix)\n\n## 二、Spring + Mybatis + Phoenix\n\n### 2.1 项目结构\n\n<div align=\"center\"> <img  src=\"../pictures/spring-mybatis-phoenix.png\"/> </div>\n\n### 2.2 主要依赖\n\n除了 Spring 相关依赖外，还需要导入 `phoenix-core` 和对应的 Mybatis 依赖包\n\n```xml\n<!--mybatis 依赖包-->\n<dependency>\n    <groupId>org.mybatis</groupId>\n    <artifactId>mybatis-spring</artifactId>\n    <version>1.3.2</version>\n</dependency>\n<dependency>\n    <groupId>org.mybatis</groupId>\n    <artifactId>mybatis</artifactId>\n    <version>3.4.6</version>\n</dependency>\n<!--phoenix core-->\n<dependency>\n    <groupId>org.apache.phoenix</groupId>\n    <artifactId>phoenix-core</artifactId>\n    <version>4.14.0-cdh5.14.2</version>\n</dependency>\n```\n\n### 2.3  数据库配置文件\n\n在数据库配置文件 `jdbc.properties`  中配置数据库驱动和 zookeeper 地址\n\n```properties\n# 数据库驱动\nphoenix.driverClassName=org.apache.phoenix.jdbc.PhoenixDriver\n# zookeeper地址\nphoenix.url=jdbc:phoenix:192.168.0.105:2181\n```\n\n### 2.4  配置数据源和会话工厂\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<beans xmlns=\"http://www.springframework.org/schema/beans\"\n       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n       xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:tx=\"http://www.springframework.org/schema/tx\"\n       xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\n        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd\">\n\n    <!-- 开启注解包扫描-->\n    <context:component-scan base-package=\"com.heibaiying.*\"/>\n\n    <!--指定配置文件的位置-->\n    <context:property-placeholder location=\"classpath:jdbc.properties\"/>\n\n    <!--配置数据源-->\n    <bean id=\"dataSource\" class=\"org.springframework.jdbc.datasource.DriverManagerDataSource\">\n        <!--Phoenix 配置-->\n        <property name=\"driverClassName\" value=\"${phoenix.driverClassName}\"/>\n        <property name=\"url\" value=\"${phoenix.url}\"/>\n    </bean>\n\n    <!--配置 mybatis 会话工厂 -->\n    <bean id=\"sqlSessionFactory\" class=\"org.mybatis.spring.SqlSessionFactoryBean\">\n        <property name=\"dataSource\" ref=\"dataSource\"/>\n        <!--指定 mapper 文件所在的位置-->\n        <property name=\"mapperLocations\" value=\"classpath*:/mappers/**/*.xml\"/>\n        <property name=\"configLocation\" value=\"classpath:mybatisConfig.xml\"/>\n    </bean>\n\n    <!--扫描注册接口 -->\n    <!--作用:从接口的基础包开始递归搜索，并将它们注册为 MapperFactoryBean(只有至少一种方法的接口才会被注册;, 具体类将被忽略)-->\n    <bean class=\"org.mybatis.spring.mapper.MapperScannerConfigurer\">\n        <!--指定会话工厂 -->\n        <property name=\"sqlSessionFactoryBeanName\" value=\"sqlSessionFactory\"/>\n        <!-- 指定 mybatis 接口所在的包 -->\n        <property name=\"basePackage\" value=\"com.heibaiying.dao\"/>\n    </bean>\n\n</beans>\n```\n\n### 2.5 Mybtais参数配置\n\n新建 mybtais 配置文件，按照需求配置额外参数， 更多 settings 配置项可以参考[官方文档](http://www.mybatis.org/mybatis-3/zh/configuration.html)\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE configuration\n        PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-config.dtd\">\n\n<!-- mybatis 配置文件 -->\n<configuration>\n    <settings>\n        <!-- 开启驼峰命名 -->\n        <setting name=\"mapUnderscoreToCamelCase\" value=\"true\"/>\n        <!-- 打印查询 sql -->\n        <setting name=\"logImpl\" value=\"STDOUT_LOGGING\"/>\n    </settings>\n</configuration>\n```\n\n### 2.6 查询接口\n\n```java\npublic interface PopulationDao {\n\n    List<USPopulation> queryAll();\n\n    void save(USPopulation USPopulation);\n\n    USPopulation queryByStateAndCity(@Param(\"state\") String state, @Param(\"city\") String city);\n\n    void deleteByStateAndCity(@Param(\"state\") String state, @Param(\"city\") String city);\n}\n```\n\n```xml\n<!DOCTYPE mapper\n        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n\n<mapper namespace=\"com.heibaiying.dao.PopulationDao\">\n\n\n    <select id=\"queryAll\" resultType=\"com.heibaiying.bean.USPopulation\">\n        SELECT * FROM us_population\n    </select>\n\n    <insert id=\"save\">\n        UPSERT INTO us_population VALUES( #{state}, #{city}, #{population} )\n    </insert>\n\n    <select id=\"queryByStateAndCity\" resultType=\"com.heibaiying.bean.USPopulation\">\n        SELECT * FROM us_population WHERE state=#{state} AND city = #{city}\n    </select>\n\n    <delete id=\"deleteByStateAndCity\">\n        DELETE FROM us_population WHERE state=#{state} AND city = #{city}\n    </delete>\n\n</mapper>\n```\n\n### 2.7 单元测试\n\n```java\n@RunWith(SpringRunner.class)\n@ContextConfiguration({\"classpath:springApplication.xml\"})\npublic class PopulationDaoTest {\n\n    @Autowired\n    private PopulationDao populationDao;\n\n    @Test\n    public void queryAll() {\n        List<USPopulation> USPopulationList = populationDao.queryAll();\n        if (USPopulationList != null) {\n            for (USPopulation USPopulation : USPopulationList) {\n                System.out.println(USPopulation.getCity() + \" \" + USPopulation.getPopulation());\n            }\n        }\n    }\n\n    @Test\n    public void save() {\n        populationDao.save(new USPopulation(\"TX\", \"Dallas\", 66666));\n        USPopulation usPopulation = populationDao.queryByStateAndCity(\"TX\", \"Dallas\");\n        System.out.println(usPopulation);\n    }\n\n    @Test\n    public void update() {\n        populationDao.save(new USPopulation(\"TX\", \"Dallas\", 99999));\n        USPopulation usPopulation = populationDao.queryByStateAndCity(\"TX\", \"Dallas\");\n        System.out.println(usPopulation);\n    }\n\n\n    @Test\n    public void delete() {\n        populationDao.deleteByStateAndCity(\"TX\", \"Dallas\");\n        USPopulation usPopulation = populationDao.queryByStateAndCity(\"TX\", \"Dallas\");\n        System.out.println(usPopulation);\n    }\n}\n```\n\n## 三、SpringBoot + Mybatis + Phoenix\n\n### 3.1 项目结构\n\n<div align=\"center\"> <img  src=\"../pictures/spring-boot-mybatis-phoenix.png\"/> </div>\n\n### 3.2 主要依赖\n\n```xml\n<!--spring 1.5 x 以上版本对应 mybatis 1.3.x (1.3.1)\n        关于更多 spring-boot 与 mybatis 的版本对应可以参见 <a href=\"http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/\">-->\n<dependency>\n    <groupId>org.mybatis.spring.boot</groupId>\n    <artifactId>mybatis-spring-boot-starter</artifactId>\n    <version>1.3.2</version>\n</dependency>\n<!--phoenix core-->\n<dependency>\n    <groupId>org.apache.phoenix</groupId>\n    <artifactId>phoenix-core</artifactId>\n    <version>4.14.0-cdh5.14.2</version>\n</dependency>\n<dependency>\n```\n\nspring boot 与 mybatis 版本的对应关系：\n\n| MyBatis-Spring-Boot-Starter 版本 | MyBatis-Spring 版本 | Spring Boot 版本 |\n| -------------------------------- | ------------------- | ---------------- |\n| **1.3.x (1.3.1)**                | 1.3 or higher       | 1.5 or higher    |\n| **1.2.x (1.2.1)**                | 1.3 or higher       | 1.4 or higher    |\n| **1.1.x (1.1.1)**                | 1.3 or higher       | 1.3 or higher    |\n| **1.0.x (1.0.2)**                | 1.2 or higher       | 1.3 or higher    |\n\n### 3.3 配置数据源\n\n在 application.yml 中配置数据源，spring boot 2.x 版本默认采用 Hikari 作为数据库连接池，Hikari 是目前 java 平台性能最好的连接池，性能好于 druid。\n\n```yaml\nspring:\n  datasource:\n    #zookeeper 地址\n    url: jdbc:phoenix:192.168.0.105:2181\n    driver-class-name: org.apache.phoenix.jdbc.PhoenixDriver\n\n    # 如果不想配置对数据库连接池做特殊配置的话,以下关于连接池的配置就不是必须的\n    # spring-boot 2.X 默认采用高性能的 Hikari 作为连接池 更多配置可以参考 https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby\n    type: com.zaxxer.hikari.HikariDataSource\n    hikari:\n      # 池中维护的最小空闲连接数\n      minimum-idle: 10\n      # 池中最大连接数，包括闲置和使用中的连接\n      maximum-pool-size: 20\n      # 此属性控制从池返回的连接的默认自动提交行为。默认为 true\n      auto-commit: true\n      # 允许最长空闲时间\n      idle-timeout: 30000\n      # 此属性表示连接池的用户定义名称，主要显示在日志记录和 JMX 管理控制台中，以标识池和池配置。 默认值：自动生成\n      pool-name: custom-hikari\n      #此属性控制池中连接的最长生命周期，值 0 表示无限生命周期，默认 1800000 即 30 分钟\n      max-lifetime: 1800000\n      # 数据库连接超时时间,默认 30 秒，即 30000\n      connection-timeout: 30000\n      # 连接测试 sql 这个地方需要根据数据库方言差异而配置 例如 oracle 就应该写成  select 1 from dual\n      connection-test-query: SELECT 1\n\n# mybatis 相关配置\nmybatis:\n  configuration:\n    # 是否打印 sql 语句 调试的时候可以开启\n    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\n```\n\n### 3.4 新建查询接口\n\n上面 Spring+Mybatis 我们使用了 XML 的方式来写 SQL，为了体现 Mybatis 支持多种方式，这里使用注解的方式来写 SQL。\n\n```java\n@Mapper\npublic interface PopulationDao {\n\n    @Select(\"SELECT * from us_population\")\n    List<USPopulation> queryAll();\n\n    @Insert(\"UPSERT INTO us_population VALUES( #{state}, #{city}, #{population} )\")\n    void save(USPopulation USPopulation);\n\n    @Select(\"SELECT * FROM us_population WHERE state=#{state} AND city = #{city}\")\n    USPopulation queryByStateAndCity(String state, String city);\n\n\n    @Delete(\"DELETE FROM us_population WHERE state=#{state} AND city = #{city}\")\n    void deleteByStateAndCity(String state, String city);\n}\n```\n\n### 3.5 单元测试\n\n```java\n@RunWith(SpringRunner.class)\n@SpringBootTest\npublic class PopulationTest {\n\n    @Autowired\n    private PopulationDao populationDao;\n\n    @Test\n    public void queryAll() {\n        List<USPopulation> USPopulationList = populationDao.queryAll();\n        if (USPopulationList != null) {\n            for (USPopulation USPopulation : USPopulationList) {\n                System.out.println(USPopulation.getCity() + \" \" + USPopulation.getPopulation());\n            }\n        }\n    }\n\n    @Test\n    public void save() {\n        populationDao.save(new USPopulation(\"TX\", \"Dallas\", 66666));\n        USPopulation usPopulation = populationDao.queryByStateAndCity(\"TX\", \"Dallas\");\n        System.out.println(usPopulation);\n    }\n\n    @Test\n    public void update() {\n        populationDao.save(new USPopulation(\"TX\", \"Dallas\", 99999));\n        USPopulation usPopulation = populationDao.queryByStateAndCity(\"TX\", \"Dallas\");\n        System.out.println(usPopulation);\n    }\n\n\n    @Test\n    public void delete() {\n        populationDao.deleteByStateAndCity(\"TX\", \"Dallas\");\n        USPopulation usPopulation = populationDao.queryByStateAndCity(\"TX\", \"Dallas\");\n        System.out.println(usPopulation);\n    }\n\n}\n\n```\n\n\n\n## 附：建表语句\n\n上面单元测试涉及到的测试表的建表语句如下：\n\n```sql\nCREATE TABLE IF NOT EXISTS us_population (\n      state CHAR(2) NOT NULL,\n      city VARCHAR NOT NULL,\n      population BIGINT\n      CONSTRAINT my_pk PRIMARY KEY (state, city));\n      \n-- 测试数据\nUPSERT INTO us_population VALUES('NY','New York',8143197);\nUPSERT INTO us_population VALUES('CA','Los Angeles',3844829);\nUPSERT INTO us_population VALUES('IL','Chicago',2842518);\nUPSERT INTO us_population VALUES('TX','Houston',2016582);\nUPSERT INTO us_population VALUES('PA','Philadelphia',1463281);\nUPSERT INTO us_population VALUES('AZ','Phoenix',1461575);\nUPSERT INTO us_population VALUES('TX','San Antonio',1256509);\nUPSERT INTO us_population VALUES('CA','San Diego',1255540);\nUPSERT INTO us_population VALUES('CA','San Jose',912332);\n```\n\n"
  },
  {
    "path": "大数据框架学习/Sqoop基本使用.md",
    "content": "# Sqoop基本使用\n\n<nav>\n<a href=\"#一Sqoop-基本命令\">一、Sqoop 基本命令</a><br/>\n<a href=\"#二Sqoop-与-MySQL\">二、Sqoop 与 MySQL</a><br/>\n<a href=\"#三Sqoop-与-HDFS\">三、Sqoop 与 HDFS</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-MySQL数据导入到HDFS\">3.1 MySQL数据导入到HDFS</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-HDFS数据导出到MySQL\">3.2 HDFS数据导出到MySQL</a><br/>\n<a href=\"#四Sqoop-与-Hive\">四、Sqoop 与 Hive</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-MySQL数据导入到Hive\">4.1 MySQL数据导入到Hive</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-Hive-导出数据到MySQL\">4.2 Hive 导出数据到MySQL</a><br/>\n<a href=\"#五Sqoop-与-HBase\">五、Sqoop 与 HBase</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#51-MySQL导入数据到HBase\">5.1 MySQL导入数据到HBase</a><br/>\n<a href=\"#六全库导出\">六、全库导出</a><br/>\n<a href=\"#七Sqoop-数据过滤\">七、Sqoop 数据过滤</a><br/>\n<a href=\"#八类型支持\">八、类型支持</a><br/>\n</nav>\n\n\n## 一、Sqoop 基本命令\n\n### 1. 查看所有命令\n\n```shell\n# sqoop help\n```\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-help.png\"/> </div>\n\n<br/>\n\n### 2. 查看某条命令的具体使用方法\n\n```shell\n# sqoop help 命令名\n```\n\n\n\n## 二、Sqoop 与 MySQL\n\n### 1. 查询MySQL所有数据库\n\n通常用于 Sqoop 与 MySQL 连通测试：\n\n```shell\nsqoop list-databases \\\n--connect jdbc:mysql://hadoop001:3306/ \\\n--username root \\\n--password root\n```\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-list-databases.png\"/> </div>\n\n<br/>\n\n### 2. 查询指定数据库中所有数据表\n\n```shell\nsqoop list-tables \\\n--connect jdbc:mysql://hadoop001:3306/mysql \\\n--username root \\\n--password root\n```\n\n\n\n## 三、Sqoop 与 HDFS\n\n### 3.1 MySQL数据导入到HDFS\n\n#### 1. 导入命令\n\n示例：导出 MySQL 数据库中的 `help_keyword` 表到 HDFS 的 `/sqoop` 目录下，如果导入目录存在则先删除再导入，使用 3 个 `map tasks` 并行导入。\n\n> 注：help_keyword 是 MySQL 内置的一张字典表，之后的示例均使用这张表。\n\n```shell\nsqoop import \\\n--connect jdbc:mysql://hadoop001:3306/mysql \\     \n--username root \\\n--password root \\\n--table help_keyword \\           # 待导入的表\n--delete-target-dir \\            # 目标目录存在则先删除\n--target-dir /sqoop \\            # 导入的目标目录\n--fields-terminated-by '\\t'  \\   # 指定导出数据的分隔符\n-m 3                             # 指定并行执行的 map tasks 数量\n```\n\n日志输出如下，可以看到输入数据被平均 `split` 为三份，分别由三个 `map task` 进行处理。数据默认以表的主键列作为拆分依据，如果你的表没有主键，有以下两种方案：\n\n+ 添加 `-- autoreset-to-one-mapper` 参数，代表只启动一个 `map task`，即不并行执行；\n+ 若仍希望并行执行，则可以使用 `--split-by <column-name>` 指明拆分数据的参考列。\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-map-task.png\"/> </div>\n\n#### 2. 导入验证\n\n```shell\n# 查看导入后的目录\nhadoop fs -ls  -R /sqoop\n# 查看导入内容\nhadoop fs -text  /sqoop/part-m-00000\n```\n\n查看 HDFS 导入目录,可以看到表中数据被分为 3 部分进行存储，这是由指定的并行度决定的。\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop_hdfs_ls.png\"/> </div>\n\n<br/>\n\n### 3.2 HDFS数据导出到MySQL\n\n```shell\nsqoop export  \\\n    --connect jdbc:mysql://hadoop001:3306/mysql \\\n    --username root \\\n    --password root \\\n    --table help_keyword_from_hdfs \\        # 导出数据存储在 MySQL 的 help_keyword_from_hdf 的表中\n    --export-dir /sqoop  \\\n    --input-fields-terminated-by '\\t'\\\n    --m 3 \n```\n\n表必须预先创建，建表语句如下：\n\n```sql\nCREATE TABLE help_keyword_from_hdfs LIKE help_keyword ;\n```\n\n\n\n## 四、Sqoop 与 Hive\n\n### 4.1 MySQL数据导入到Hive\n\nSqoop 导入数据到 Hive 是通过先将数据导入到 HDFS 上的临时目录，然后再将数据从 HDFS 上 `Load` 到 Hive 中，最后将临时目录删除。可以使用 `target-dir` 来指定临时目录。\n\n#### 1. 导入命令\n\n```shell\nsqoop import \\\n  --connect jdbc:mysql://hadoop001:3306/mysql \\\n  --username root \\\n  --password root \\\n  --table help_keyword \\        # 待导入的表     \n  --delete-target-dir \\         # 如果临时目录存在删除\n  --target-dir /sqoop_hive  \\   # 临时目录位置\n  --hive-database sqoop_test \\  # 导入到 Hive 的 sqoop_test 数据库，数据库需要预先创建。不指定则默认为 default 库\n  --hive-import \\               # 导入到 Hive\n  --hive-overwrite \\            # 如果 Hive 表中有数据则覆盖，这会清除表中原有的数据，然后再写入\n  -m 3                          # 并行度\n```\n\n导入到 Hive 中的 `sqoop_test` 数据库需要预先创建，不指定则默认使用 Hive 中的 `default` 库。\n\n```shell\n # 查看 hive 中的所有数据库\n hive>  SHOW DATABASES;\n # 创建 sqoop_test 数据库\n hive>  CREATE DATABASE sqoop_test;\n```\n\n#### 2. 导入验证\n\n```shell\n# 查看 sqoop_test 数据库的所有表\n hive>  SHOW  TABLES  IN  sqoop_test;\n# 查看表中数据\n hive> SELECT * FROM sqoop_test.help_keyword;\n```\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop_hive_tables.png\"/> </div>\n\n#### 3. 可能出现的问题\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop_hive_error.png\"/> </div>\n\n<br/>\n\n如果执行报错 `java.io.IOException: java.lang.ClassNotFoundException: org.apache.hadoop.hive.conf.HiveConf`，则需将 Hive 安装目录下 `lib` 下的 `hive-exec-**.jar` 放到 sqoop 的 `lib` 。\n\n```shell\n[root@hadoop001 lib]# ll hive-exec-*\n-rw-r--r--. 1 1106 4001 19632031 11 月 13 21:45 hive-exec-1.1.0-cdh5.15.2.jar\n[root@hadoop001 lib]# cp hive-exec-1.1.0-cdh5.15.2.jar  ${SQOOP_HOME}/lib\n```\n\n<br/>\n\n### 4.2 Hive 导出数据到MySQL\n\n由于 Hive 的数据是存储在 HDFS 上的，所以 Hive 导入数据到 MySQL，实际上就是 HDFS 导入数据到 MySQL。\n\n#### 1. 查看Hive表在HDFS的存储位置\n\n```shell\n# 进入对应的数据库\nhive> use sqoop_test;\n# 查看表信息\nhive> desc formatted help_keyword;\n```\n\n`Location` 属性为其存储位置：\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-hive-location.png\"/> </div>\n\n这里可以查看一下这个目录，文件结构如下：\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-hive-hdfs.png\"/> </div>\n\n#### 3.2 执行导出命令\n\n```shell\nsqoop export  \\\n    --connect jdbc:mysql://hadoop001:3306/mysql \\\n    --username root \\\n    --password root \\\n    --table help_keyword_from_hive \\\n    --export-dir /user/hive/warehouse/sqoop_test.db/help_keyword  \\\n    -input-fields-terminated-by '\\001' \\             # 需要注意的是 hive 中默认的分隔符为 \\001\n    --m 3 \n```\nMySQL 中的表需要预先创建：\n\n```sql\nCREATE TABLE help_keyword_from_hive LIKE help_keyword ;\n```\n\n\n\n## 五、Sqoop 与 HBase\n\n> 本小节只讲解从 RDBMS 导入数据到 HBase，因为暂时没有命令能够从 HBase 直接导出数据到 RDBMS。\n\n### 5.1 MySQL导入数据到HBase\n\n#### 1. 导入数据\n\n将 `help_keyword` 表中数据导入到 HBase 上的 `help_keyword_hbase` 表中，使用原表的主键 `help_keyword_id` 作为 `RowKey`，原表的所有列都会在 `keywordInfo` 列族下，目前只支持全部导入到一个列族下，不支持分别指定列族。\n\n```shell\nsqoop import \\\n    --connect jdbc:mysql://hadoop001:3306/mysql \\\n    --username root \\\n    --password root \\\n    --table help_keyword \\              # 待导入的表\n    --hbase-table help_keyword_hbase \\  # hbase 表名称，表需要预先创建\n    --column-family keywordInfo \\       # 所有列导入到 keywordInfo 列族下 \n    --hbase-row-key help_keyword_id     # 使用原表的 help_keyword_id 作为 RowKey\n```\n\n导入的 HBase 表需要预先创建：\n\n```shell\n# 查看所有表\nhbase> list\n# 创建表\nhbase> create 'help_keyword_hbase', 'keywordInfo'\n# 查看表信息\nhbase> desc 'help_keyword_hbase'\n```\n\n#### 2. 导入验证\n\n使用 `scan` 查看表数据：\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop_hbase.png\"/> </div>\n\n\n\n\n\n## 六、全库导出\n\nSqoop 支持通过 `import-all-tables` 命令进行全库导出到 HDFS/Hive，但需要注意有以下两个限制：\n\n+ 所有表必须有主键；或者使用 `--autoreset-to-one-mapper`，代表只启动一个 `map task`;\n+ 你不能使用非默认的分割列，也不能通过 WHERE 子句添加任何限制。\n\n> 第二点解释得比较拗口，这里列出官方原本的说明：\n>\n> + You must not intend to use non-default splitting column, nor impose any conditions via a `WHERE` clause.\n\n全库导出到 HDFS：\n\n```shell\nsqoop import-all-tables \\\n    --connect jdbc:mysql://hadoop001:3306/数据库名 \\\n    --username root \\\n    --password root \\\n    --warehouse-dir  /sqoop_all \\     # 每个表会单独导出到一个目录，需要用此参数指明所有目录的父目录\n    --fields-terminated-by '\\t'  \\\n    -m 3\n```\n\n全库导出到 Hive：\n\n```shell\nsqoop import-all-tables -Dorg.apache.sqoop.splitter.allow_text_splitter=true \\\n  --connect jdbc:mysql://hadoop001:3306/数据库名 \\\n  --username root \\\n  --password root \\\n  --hive-database sqoop_test \\         # 导出到 Hive 对应的库   \n  --hive-import \\\n  --hive-overwrite \\\n  -m 3\n```\n\n\n\n## 七、Sqoop 数据过滤\n\n### 7.1 query参数\n\nSqoop 支持使用 `query` 参数定义查询 SQL，从而可以导出任何想要的结果集。使用示例如下：\n\n```shell\nsqoop import \\\n  --connect jdbc:mysql://hadoop001:3306/mysql \\\n  --username root \\\n  --password root \\\n  --query 'select * from help_keyword where  $CONDITIONS and  help_keyword_id < 50' \\  \n  --delete-target-dir \\            \n  --target-dir /sqoop_hive  \\ \n  --hive-database sqoop_test \\           # 指定导入目标数据库 不指定则默认使用 Hive 中的 default 库\n  --hive-table filter_help_keyword \\     # 指定导入目标表\n  --split-by help_keyword_id \\           # 指定用于 split 的列      \n  --hive-import \\                        # 导入到 Hive\n  --hive-overwrite \\                     、\n  -m 3                                  \n```\n\n在使用 `query` 进行数据过滤时，需要注意以下三点：\n\n+ 必须用 `--hive-table` 指明目标表；\n+ 如果并行度 `-m` 不为 1 或者没有指定 `--autoreset-to-one-mapper`，则需要用 ` --split-by ` 指明参考列；\n+ SQL 的 `where` 字句必须包含 `$CONDITIONS`，这是固定写法，作用是动态替换。\n\n  \n\n### 7.2 增量导入\n\n```shell\nsqoop import \\\n    --connect jdbc:mysql://hadoop001:3306/mysql \\\n    --username root \\\n    --password root \\\n    --table help_keyword \\\n    --target-dir /sqoop_hive  \\\n    --hive-database sqoop_test \\         \n    --incremental  append  \\             # 指明模式\n    --check-column  help_keyword_id \\    # 指明用于增量导入的参考列\n    --last-value 300  \\                  # 指定参考列上次导入的最大值\n    --hive-import \\   \n    -m 3  \n```\n\n`incremental` 参数有以下两个可选的选项：\n\n+ **append**：要求参考列的值必须是递增的，所有大于 `last-value` 的值都会被导入；\n+ **lastmodified**：要求参考列的值必须是 `timestamp` 类型，且插入数据时候要在参考列插入当前时间戳，更新数据时也要更新参考列的时间戳，所有时间晚于 ``last-value`` 的数据都会被导入。\n\n通过上面的解释我们可以看出来，其实 Sqoop 的增量导入并没有太多神器的地方，就是依靠维护的参考列来判断哪些是增量数据。当然我们也可以使用上面介绍的 `query` 参数来进行手动的增量导出，这样反而更加灵活。\n\n\n\n## 八、类型支持\n\nSqoop 默认支持数据库的大多数字段类型，但是某些特殊类型是不支持的。遇到不支持的类型，程序会抛出异常 `Hive does not support the SQL type for column xxx` 异常，此时可以通过下面两个参数进行强制类型转换：\n\n+ **--map-column-java\\<mapping>**   ：重写 SQL 到 Java 类型的映射；\n+  **--map-column-hive \\<mapping>** ： 重写 Hive 到 Java 类型的映射。\n\n示例如下，将原先 `id` 字段强制转为 String 类型，`value` 字段强制转为 Integer 类型：\n\n```\n$ sqoop import ... --map-column-java id=String,value=Integer\n```\n\n\n\n\n\n## 参考资料\n\n[Sqoop User Guide (v1.4.7)](http://sqoop.apache.org/docs/1.4.7/SqoopUserGuide.html)\n"
  },
  {
    "path": "大数据框架学习/Sqoop简介与安装.md",
    "content": "# Sqoop 简介与安装\n\n<nav>\n<a href=\"#一Sqoop-简介\">一、Sqoop 简介</a><br/>\n<a href=\"#二安装\">二、安装</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-下载并解压\">2.1 下载并解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-配置环境变量\">2.2 配置环境变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-修改配置\">2.3 修改配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-拷贝数据库驱动\">2.4 拷贝数据库驱动</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-验证\">2.5 验证</a><br/>\n</nav>\n\n\n## 一、Sqoop 简介\n\nSqoop 是一个常用的数据迁移工具，主要用于在不同存储系统之间实现数据的导入与导出：\n\n+ 导入数据：从 MySQL，Oracle 等关系型数据库中导入数据到 HDFS、Hive、HBase 等分布式文件存储系统中；\n\n+ 导出数据：从 分布式文件系统中导出数据到关系数据库中。\n\n其原理是将执行命令转化成 MapReduce 作业来实现数据的迁移，如下图：\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-tool.png\"/> </div>\n\n## 二、安装\n\n版本选择：目前 Sqoop 有 Sqoop 1 和 Sqoop 2 两个版本，但是截至到目前，官方并不推荐使用 Sqoop 2，因为其与 Sqoop 1 并不兼容，且功能还没有完善，所以这里优先推荐使用 Sqoop 1。\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-version-selected.png\"/> </div>\n\n\n\n### 2.1 下载并解压\n\n下载所需版本的 Sqoop ，这里我下载的是 `CDH` 版本的 Sqoop 。下载地址为：http://archive.cloudera.com/cdh5/cdh/5/\n\n```shell\n# 下载后进行解压\ntar -zxvf  sqoop-1.4.6-cdh5.15.2.tar.gz\n```\n\n### 2.2 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport SQOOP_HOME=/usr/app/sqoop-1.4.6-cdh5.15.2\nexport PATH=$SQOOP_HOME/bin:$PATH\n```\n\n使得配置的环境变量立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 2.3 修改配置\n\n进入安装目录下的 `conf/` 目录，拷贝 Sqoop 的环境配置模板 `sqoop-env.sh.template`\n\n```shell\n# cp sqoop-env-template.sh sqoop-env.sh\n```\n\n修改 `sqoop-env.sh`，内容如下 (以下配置中 `HADOOP_COMMON_HOME` 和 `HADOOP_MAPRED_HOME` 是必选的，其他的是可选的)：\n\n```shell\n# Set Hadoop-specific environment variables here.\n#Set path to where bin/hadoop is available\nexport HADOOP_COMMON_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2\n\n#Set path to where hadoop-*-core.jar is available\nexport HADOOP_MAPRED_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2\n\n#set the path to where bin/hbase is available\nexport HBASE_HOME=/usr/app/hbase-1.2.0-cdh5.15.2\n\n#Set the path to where bin/hive is available\nexport HIVE_HOME=/usr/app/hive-1.1.0-cdh5.15.2\n\n#Set the path for where zookeper config dir is\nexport ZOOCFGDIR=/usr/app/zookeeper-3.4.13/conf\n\n```\n\n### 2.4 拷贝数据库驱动\n\n将 MySQL 驱动包拷贝到 Sqoop 安装目录的 `lib` 目录下, 驱动包的下载地址为 https://dev.mysql.com/downloads/connector/j/  。在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下我也上传了一份，有需要的话可以自行下载。\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-mysql-jar.png\"/> </div>\n\n\n\n### 2.5 验证\n\n由于已经将 sqoop 的 `bin` 目录配置到环境变量，直接使用以下命令验证是否配置成功：\n\n```shell\n# sqoop version\n```\n\n出现对应的版本信息则代表配置成功：\n\n<div align=\"center\"> <img  src=\"../pictures/sqoop-version.png\"/> </div>\n\n这里出现的两个 `Warning` 警告是因为我们本身就没有用到 `HCatalog` 和 `Accumulo`，忽略即可。Sqoop 在启动时会去检查环境变量中是否有配置这些软件，如果想去除这些警告，可以修改 `bin/configure-sqoop`，注释掉不必要的检查。\n\n```shell\n# Check: If we can't find our dependencies, give up here.\nif [ ! -d \"${HADOOP_COMMON_HOME}\" ]; then\n  echo \"Error: $HADOOP_COMMON_HOME does not exist!\"\n  echo 'Please set $HADOOP_COMMON_HOME to the root of your Hadoop installation.'\n  exit 1\nfi\nif [ ! -d \"${HADOOP_MAPRED_HOME}\" ]; then\n  echo \"Error: $HADOOP_MAPRED_HOME does not exist!\"\n  echo 'Please set $HADOOP_MAPRED_HOME to the root of your Hadoop MapReduce installation.'\n  exit 1\nfi\n\n## Moved to be a runtime check in sqoop.\nif [ ! -d \"${HBASE_HOME}\" ]; then\n  echo \"Warning: $HBASE_HOME does not exist! HBase imports will fail.\"\n  echo 'Please set $HBASE_HOME to the root of your HBase installation.'\nfi\n\n## Moved to be a runtime check in sqoop.\nif [ ! -d \"${HCAT_HOME}\" ]; then\n  echo \"Warning: $HCAT_HOME does not exist! HCatalog jobs will fail.\"\n  echo 'Please set $HCAT_HOME to the root of your HCatalog installation.'\nfi\n\nif [ ! -d \"${ACCUMULO_HOME}\" ]; then\n  echo \"Warning: $ACCUMULO_HOME does not exist! Accumulo imports will fail.\"\n  echo 'Please set $ACCUMULO_HOME to the root of your Accumulo installation.'\nfi\nif [ ! -d \"${ZOOKEEPER_HOME}\" ]; then\n  echo \"Warning: $ZOOKEEPER_HOME does not exist! Accumulo imports will fail.\"\n  echo 'Please set $ZOOKEEPER_HOME to the root of your Zookeeper installation.'\nfi\n```\n\n"
  },
  {
    "path": "大数据框架学习/Storm三种打包方式对比分析.md",
    "content": "# Storm三种打包方式对比分析\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二mvn-package\">二、mvn package</a><br/>\n<a href=\"#三maven-assembly-plugin插件\">三、maven-assembly-plugin插件</a><br/>\n<a href=\"#四maven-shade-plugin插件\">四、maven-shade-plugin插件</a><br/>\n<a href=\"#五结论\">五、结论</a><br/>\n<a href=\"#六打包注意事项\">六、打包注意事项</a><br/>\n</nav>\n\n\n## 一、简介\n\n在将 Storm Topology 提交到服务器集群运行时，需要先将项目进行打包。本文主要对比分析各种打包方式，并将打包过程中需要注意的事项进行说明。主要打包方式有以下三种：\n\n+ 第一种：不加任何插件，直接使用 mvn package 打包；\n+ 第二种：使用 maven-assembly-plugin 插件进行打包；\n+ 第三种：使用 maven-shade-plugin 进行打包。\n\n以下分别进行详细的说明。\n\n\n\n## 二、mvn package\n\n### 2.1 mvn package的局限\n\n不在 POM 中配置任何插件，直接使用 `mvn package` 进行项目打包，这对于没有使用外部依赖包的项目是可行的。\n\n但如果项目中使用了第三方 JAR 包，就会出现问题，因为 `mvn package` 打包后的 JAR 中是不含有依赖包的，如果此时你提交到服务器上运行，就会出现找不到第三方依赖的异常。\n\n如果你想采用这种方式进行打包，但是又使用了第三方 JAR，有没有解决办法？答案是有的，这一点在官方文档的[Command Line Client](http://storm.apache.org/releases/2.0.0-SNAPSHOT/Command-line-client.html) 章节有所讲解，主要解决办法如下。\n\n### 2.2 解决办法\n\n在使用 `storm jar` 提交 Topology 时，可以使用如下方式指定第三方依赖：\n\n+ 如果第三方 JAR 包在本地，可以使用 `--jars` 指定；\n+ 如果第三方 JAR 包在远程中央仓库，可以使用 `--artifacts` 指定，此时如果想要排除某些依赖，可以使用 `^` 符号。指定后 Storm 会自动到中央仓库进行下载，然后缓存到本地；\n+ 如果第三方 JAR 包在其他仓库，还需要使用 `--artifactRepositories` 指明仓库地址，库名和地址使用 `^` 符号分隔。\n\n以下是一个包含上面三种情况的命令示例：\n\n```shell\n./bin/storm jar example/storm-starter/storm-starter-topologies-*.jar \\\norg.apache.storm.starter.RollingTopWords blobstore-remote2 remote  \\\n--jars \"./external/storm-redis/storm-redis-1.1.0.jar,./external/storm-kafka/storm-kafka-1.1.0.jar\" \\\n--artifacts \"redis.clients:jedis:2.9.0,org.apache.kafka:kafka_2.10:0.8.2.2^org.slf4j:slf4j-log4j12\" \\\n--artifactRepositories \"jboss-repository^http://repository.jboss.com/maven2, \\\nHDPRepo^http://repo.hortonworks.com/content/groups/public/\"\n```\n\n这种方式是建立在你能够连接到外网的情况下，如果你的服务器不能连接外网，或者你希望能把项目直接打包成一个 `ALL IN ONE` 的 JAR，即包含所有相关依赖，此时可以采用下面介绍的两个插件。\n\n## 三、maven-assembly-plugin插件\n\nmaven-assembly-plugin 是官方文档中介绍的打包方法，来源于官方文档：[Running Topologies on a Production Cluster](http://storm.apache.org/releases/2.0.0-SNAPSHOT/Running-topologies-on-a-production-cluster.html)\n\n> If you're using Maven, the [Maven Assembly Plugin](http://maven.apache.org/plugins/maven-assembly-plugin/) can do the packaging for you. Just add this to your pom.xml:\n>\n> ```xml\n> <plugin>\n> <artifactId>maven-assembly-plugin</artifactId>\n> <configuration>\n>  <descriptorRefs>  \n>    <descriptorRef>jar-with-dependencies</descriptorRef>\n>  </descriptorRefs>\n>  <archive>\n>    <manifest>\n>      <mainClass>com.path.to.main.Class</mainClass>\n>    </manifest>\n>  </archive>\n> </configuration>\n> </plugin>\n> ```\n>\n> Then run mvn assembly:assembly to get an appropriately packaged jar. Make sure you [exclude](http://maven.apache.org/plugins/maven-assembly-plugin/examples/single/including-and-excluding-artifacts.html) the Storm jars since the cluster already has Storm on the classpath.\n\n官方文档主要说明了以下几点：\n\n- 使用 maven-assembly-plugin 可以把所有的依赖一并打入到最后的 JAR 中；\n- 需要排除掉 Storm 集群环境中已经提供的 Storm jars；\n- 通过 `  <mainClass>` 标签指定主入口类；\n- 通过 `<descriptorRef>` 标签指定打包相关配置。\n\n`jar-with-dependencies` 是 Maven[预定义](http://maven.apache.org/plugins/maven-assembly-plugin/descriptor-refs.html#jar-with-dependencies) 的一种最基本的打包配置，其 XML 文件如下：\n\n```xml\n<assembly xmlns=\"http://maven.apache.org/ASSEMBLY/2.0.0\"\n          xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://maven.apache.org/ASSEMBLY/2.0.0\n                              http://maven.apache.org/xsd/assembly-2.0.0.xsd\">\n    <id>jar-with-dependencies</id>\n    <formats>\n        <format>jar</format>\n    </formats>\n    <includeBaseDirectory>false</includeBaseDirectory>\n    <dependencySets>\n        <dependencySet>\n            <outputDirectory>/</outputDirectory>\n            <useProjectArtifact>true</useProjectArtifact>\n            <unpack>true</unpack>\n            <scope>runtime</scope>\n        </dependencySet>\n    </dependencySets>\n</assembly>\n```\n\n我们可以通过对该配置文件进行拓展，从而实现更多的功能，比如排除指定的 JAR 等。使用示例如下：\n\n### 1. 引入插件\n\n在 POM.xml 中引入插件，并指定打包格式的配置文件为 `assembly.xml`(名称可自定义)：\n\n```xml\n<build>\n    <plugins>\n        <plugin>\n            <artifactId>maven-assembly-plugin</artifactId>\n            <configuration>\n                <descriptors>\n                    <descriptor>src/main/resources/assembly.xml</descriptor>\n                </descriptors>\n                <archive>\n                    <manifest>\n                        <mainClass>com.heibaiying.wordcount.ClusterWordCountApp</mainClass>\n                    </manifest>\n                </archive>\n            </configuration>\n        </plugin>\n    </plugins>\n</build>\n```\n\n`assembly.xml` 拓展自 `jar-with-dependencies.xml`，使用了 `<excludes>` 标签排除 Storm jars，具体内容如下：\n\n```xml\n<assembly xmlns=\"http://maven.apache.org/ASSEMBLY/2.0.0\"\n          xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://maven.apache.org/ASSEMBLY/2.0.0 \n                              http://maven.apache.org/xsd/assembly-2.0.0.xsd\">\n    \n    <id>jar-with-dependencies</id>\n\n    <!--指明打包方式-->\n    <formats>\n        <format>jar</format>\n    </formats>\n\n    <includeBaseDirectory>false</includeBaseDirectory>\n    <dependencySets>\n        <dependencySet>\n            <outputDirectory>/</outputDirectory>\n            <useProjectArtifact>true</useProjectArtifact>\n            <unpack>true</unpack>\n            <scope>runtime</scope>\n            <!--排除 storm 环境中已经提供的 storm-core-->\n            <excludes>\n                <exclude>org.apache.storm:storm-core</exclude>\n            </excludes>\n        </dependencySet>\n    </dependencySets>\n</assembly>\n```\n\n>在配置文件中不仅可以排除依赖，还可以排除指定的文件，更多的配置规则可以参考官方文档：[Descriptor Format](http://maven.apache.org/plugins/maven-assembly-plugin/assembly.html#)\n\n### 2.  打包命令\n\n采用 maven-assembly-plugin 进行打包时命令如下：\n\n```shell\n# mvn assembly:assembly \n```\n\n打包后会同时生成两个 JAR 包，其中后缀为 `jar-with-dependencies` 是含有第三方依赖的 JAR 包，后缀是由 `assembly.xml` 中 `<id>` 标签指定的，可以自定义修改。提交该 JAR 到集群环境即可直接使用。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-jar.png\"/> </div>\n\n\n\n## 四、maven-shade-plugin插件\n\n### 4.1 官方文档说明\n\n第三种方式是使用 maven-shade-plugin，既然已经有了 maven-assembly-plugin，为什么还需要 maven-shade-plugin，这一点在官方文档中也是有所说明的，来自于官方对 HDFS 整合讲解的章节[Storm HDFS Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html)，原文如下：\n\n>When packaging your topology, it's important that you use the [maven-shade-plugin](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html) as opposed to the [maven-assembly-plugin](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html).\n>\n>The shade plugin provides facilities for merging JAR manifest entries, which the hadoop client leverages for URL scheme resolution.\n>\n>If you experience errors such as the following:\n>\n>```\n>java.lang.RuntimeException: Error preparing HdfsBolt: No FileSystem for scheme: hdfs\n>```\n>\n>it's an indication that your topology jar file isn't packaged properly.\n>\n>If you are using maven to create your topology jar, you should use the following `maven-shade-plugin` configuration to create your topology jar。\n\n这里第一句就说的比较清晰，在集成 HDFS 时候，你必须使用 maven-shade-plugin 来代替 maven-assembly-plugin，否则会抛出 RuntimeException 异常。\n\n采用 maven-shade-plugin 打包有很多好处，比如你的工程依赖很多的 JAR 包，而被依赖的 JAR 又会依赖其他的 JAR 包，这样,当工程中依赖到不同的版本的 JAR 时，并且 JAR 中具有相同名称的资源文件时，shade 插件会尝试将所有资源文件打包在一起时，而不是和 assembly 一样执行覆盖操作。\n\n### 4.2 配置\n\n采用 `maven-shade-plugin` 进行打包时候，配置示例如下：\n\n```xml\n<plugin>\n    <groupId>org.apache.maven.plugins</groupId>\n    <artifactId>maven-shade-plugin</artifactId>\n    <configuration>\n        <createDependencyReducedPom>true</createDependencyReducedPom>\n        <filters>\n            <filter>\n                <artifact>*:*</artifact>\n                <excludes>\n                    <exclude>META-INF/*.SF</exclude>\n                    <exclude>META-INF/*.sf</exclude>\n                    <exclude>META-INF/*.DSA</exclude>\n                    <exclude>META-INF/*.dsa</exclude>\n                    <exclude>META-INF/*.RSA</exclude>\n                    <exclude>META-INF/*.rsa</exclude>\n                    <exclude>META-INF/*.EC</exclude>\n                    <exclude>META-INF/*.ec</exclude>\n                    <exclude>META-INF/MSFTSIG.SF</exclude>\n                    <exclude>META-INF/MSFTSIG.RSA</exclude>\n                </excludes>\n            </filter>\n        </filters>\n        <artifactSet>\n            <excludes>\n                <exclude>org.apache.storm:storm-core</exclude>\n            </excludes>\n        </artifactSet>\n    </configuration>\n    <executions>\n        <execution>\n            <phase>package</phase>\n            <goals>\n                <goal>shade</goal>\n            </goals>\n            <configuration>\n                <transformers>\n                    <transformer\n                       implementation=\"org.apache.maven.plugins.shade.resource.ServicesResourceTransformer\"/>\n                    <transformer\n                       implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                    </transformer>\n                </transformers>\n            </configuration>\n        </execution>\n    </executions>\n</plugin>\n```\n\n以上配置示例来源于 Storm Github，这里做一下说明：\n\n在上面的配置中，排除了部分文件，这是因为有些 JAR 包生成时，会使用 jarsigner 生成文件签名（完成性校验），分为两个文件存放在 META-INF 目录下：\n\n+ a signature file, with a .SF extension；\n+ a signature block file, with a .DSA, .RSA, or .EC extension；\n\n如果某些包的存在重复引用，这可能会导致在打包时候出现 `Invalid signature file digest for Manifest main attributes` 异常，所以在配置中排除这些文件。\n\n### 4.3 打包命令\n\n使用 maven-shade-plugin 进行打包的时候，打包命令和普通的一样：\n\n```shell\n# mvn  package\n```\n\n打包后会生成两个 JAR 包，提交到服务器集群时使用 ` 非 original` 开头的 JAR。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-jar2.png\"/> </div>\n\n## 五、结论\n\n通过以上三种打包方式的详细介绍，这里给出最后的结论：**建议使用 maven-shade-plugin 插件进行打包**，因为其通用性最强，操作最简单，并且 Storm Github 中所有[examples](https://github.com/apache/storm/tree/master/examples) 都是采用该方式进行打包。\n\n\n\n## 六、打包注意事项\n\n无论采用任何打包方式，都必须排除集群环境中已经提供的 storm jars。这里比较典型的是 storm-core，其在安装目录的 lib 目录下已经存在。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-lib.png\"/> </div>\n\n\n\n如果你不排除 storm-core，通常会抛出下面的异常：\n\n```properties\nCaused by: java.lang.RuntimeException: java.io.IOException: Found multiple defaults.yaml resources.   \nYou're probably bundling the Storm jars with your topology jar.   \n[jar:file:/usr/app/apache-storm-1.2.2/lib/storm-core-1.2.2.jar!/defaults.yaml,   \njar:file:/usr/appjar/storm-hdfs-integration-1.0.jar!/defaults.yaml]\n        at org.apache.storm.utils.Utils.findAndReadConfigFile(Utils.java:384)\n        at org.apache.storm.utils.Utils.readDefaultConfig(Utils.java:428)\n        at org.apache.storm.utils.Utils.readStormConfig(Utils.java:464)\n        at org.apache.storm.utils.Utils.<clinit>(Utils.java:178)\n        ... 39 more\n```\n\n<div align=\"center\"> <img  src=\"../pictures/storm-jar-complie-error.png\"/> </div>\n\n\n\n## 参考资料\n\n关于 maven-shade-plugin 的更多配置可以参考： [maven-shade-plugin 入门指南](https://www.jianshu.com/p/7a0e20b30401)\n"
  },
  {
    "path": "大数据框架学习/Storm和流处理简介.md",
    "content": "# Storm和流处理简介\n\n<nav>\n<a href=\"#一Storm\">一、Storm</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-简介\">1.1 简介</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-Storm-与-Hadoop对比\">1.2 Storm 与 Hadoop对比</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-Storm-与Spark-Streaming对比\">1.3 Storm 与  Spark Streaming对比</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-Storm-与-Flink对比\">1.4 Storm 与 Flink对比</a><br/>\n<a href=\"#二流处理\">二、流处理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-静态数据处理\">2.1 静态数据处理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-流处理\">2.2 流处理</a><br/>\n</nav>\n\n\n## 一、Storm\n\n#### 1.1 简介\n\nStorm 是一个开源的分布式实时计算框架，可以以简单、可靠的方式进行大数据流的处理。通常用于实时分析，在线机器学习、持续计算、分布式 RPC、ETL 等场景。Storm 具有以下特点：\n\n+ 支持水平横向扩展；\n+ 具有高容错性，通过 ACK 机制每个消息都不丢失；\n+ 处理速度非常快，每个节点每秒能处理超过一百万个 tuples ；\n+ 易于设置和操作，并可以与任何编程语言一起使用；\n+ 支持本地模式运行，对于开发人员来说非常友好；\n+ 支持图形化管理界面。\n\n\n\n#### 1.2 Storm 与 Hadoop对比\n\nHadoop 采用 MapReduce 处理数据，而 MapReduce 主要是对数据进行批处理，这使得 Hadoop 更适合于海量数据离线处理的场景。而 Strom 的设计目标是对数据进行实时计算，这使得其更适合实时数据分析的场景。\n\n\n\n#### 1.3 Storm 与 Spark Streaming对比\n\nSpark Streaming 并不是真正意义上的流处理框架。 Spark Streaming 接收实时输入的数据流，并将数据拆分为一系列批次，然后进行微批处理。只不过 Spark Streaming 能够将数据流进行极小粒度的拆分，使得其能够得到接近于流处理的效果，但其本质上还是批处理（或微批处理）。\n\n<div align=\"center\"> <img  src=\"../pictures/streaming-flow.png\"/> </div>\n\n#### 1.4 Strom 与 Flink对比\n\nstorm 和 Flink 都是真正意义上的实时计算框架。其对比如下：\n\n|          | storm                                                        | flink                                                        |\n| -------- | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| 状态管理 | 无状态                                                       | 有状态                                                       |\n| 窗口支持 | 对事件窗口支持较弱，缓存整个窗口的所有数据，窗口结束时一起计算 | 窗口支持较为完善，自带一些窗口聚合方法，<br>并且会自动管理窗口状态 |\n| 消息投递 | At Most  Once<br/>At Least Once                              | At Most  Once<br/>At Least Once<br/>**Exactly Once**         |\n| 容错方式 | ACK 机制：对每个消息进行全链路跟踪，失败或者超时时候进行重发  | 检查点机制：通过分布式一致性快照机制，<br/>对数据流和算子状态进行保存。在发生错误时，使系统能够进行回滚。 |\n\n\n> 注  :  对于消息投递，一般有以下三种方案：\n> + At Most Once : 保证每个消息会被投递 0 次或者 1 次，在这种机制下消息很有可能会丢失；\n> + At Least Once : 保证了每个消息会被默认投递多次，至少保证有一次被成功接收，信息可能有重复，但是不会丢失；\n> + Exactly Once  :  每个消息对于接收者而言正好被接收一次，保证即不会丢失也不会重复。\n\n\n\n## 二、流处理\n\n#### 2.1 静态数据处理\n\n在流处理之前，数据通常存储在数据库或文件系统中，应用程序根据需要查询或计算数据，这就是传统的静态数据处理架构。Hadoop 采用 HDFS 进行数据存储，采用 MapReduce 进行数据查询或分析，这就是典型的静态数据处理架构。\n\n<div align=\"center\"> <img  src=\"../pictures/01_data_at_rest_infrastructure.png\"/> </div>\n\n\n\n#### 2.2 流处理\n\n而流处理则是直接对运动中数据的处理，在接收数据的同时直接计算数据。实际上，在真实世界中的大多数数据都是连续的流，如传感器数据，网站用户活动数据，金融交易数据等等 ，所有这些数据都是随着时间的推移而源源不断地产生。\n\n接收和发送数据流并执行应用程序或分析逻辑的系统称为**流处理器**。流处理器的基本职责是确保数据有效流动，同时具备可扩展性和容错能力，Storm 和 Flink 就是其代表性的实现。\n\n<div align=\"center\"> <img  src=\"../pictures/02_stream_processing_infrastructure.png\"/> </div>\n\n\n\n流处理带来了很多优点：\n\n- **可以立即对数据做出反应**：降低了数据的滞后性，使得数据更具有时效性，更能反映对未来的预期；\n\n- **可以处理更大的数据量**：直接处理数据流，并且只保留数据中有意义的子集，然后将其传送到下一个处理单元，通过逐级过滤数据，从而降低实际需要处理的数据量；\n\n- **更贴近现实的数据模型**：在实际的环境中，一切数据都是持续变化的，想要通过历史数据推断未来的趋势，必须保证数据的不断输入和模型的持续修正，典型的就是金融市场、股票市场，流处理能更好地处理这些场景下对数据连续性和及时性的需求；\n\n- **分散和分离基础设施**：流式处理减少了对大型数据库的需求。每个流处理程序通过流处理框架维护了自己的数据和状态，这使其更适合于当下最流行的微服务架构。\n\n\n\n\n\n## 参考资料\n\n1.  [What is stream processing?](https://www.ververica.com/what-is-stream-processing)\n2. [流计算框架 Flink 与 Storm 的性能对比](http://bigdata.51cto.com/art/201711/558416.htm)\n"
  },
  {
    "path": "大数据框架学习/Storm核心概念详解.md",
    "content": "# Storm 核心概念详解\n\n<nav>\n<a href=\"#一storm核心概念\">一、Storm核心概念</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11--Topologies拓扑\">1.1  Topologies（拓扑）</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12--Streams流\">1.2  Streams（流）</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-Spouts\">1.3 Spouts</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-Bolts\"> 1.4 Bolts</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-Stream-groupings分组策略\">1.5 Stream groupings（分组策略）</a><br/>\n<a href=\"#二storm架构详解\">二、Storm架构详解</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-nimbus进程\">2.1 nimbus进程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-supervisor进程\">2.2 supervisor进程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-zookeeper的作用\">2.3 zookeeper的作用</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-worker进程\">2.4 worker进程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-executor线程\">2.5 executor线程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-并行度\">2.6 并行度</a><br/>\n</nav>\n\n## 一、Storm核心概念\n\n<div align=\"center\"> <img  src=\"../pictures/spout-bolt.png\"/> </div>\n\n### 1.1  Topologies（拓扑）\n\n一个完整的 Storm 流处理程序被称为 Storm topology(拓扑)。它是一个是由 `Spouts` 和 `Bolts` 通过 `Stream` 连接起来的有向无环图，Storm 会保持每个提交到集群的 topology 持续地运行，从而处理源源不断的数据流，直到你将主动其杀死 (kill) 为止。\n\n### 1.2  Streams（流）\n\n`Stream` 是 Storm 中的核心概念。一个 `Stream` 是一个无界的、以分布式方式并行创建和处理的 `Tuple` 序列。Tuple 可以包含大多数基本类型以及自定义类型的数据。简单来说，Tuple 就是流数据的实际载体，而 Stream 就是一系列 Tuple。\n\n### 1.3 Spouts\n\n`Spouts` 是流数据的源头，一个 Spout 可以向不止一个 `Streams` 中发送数据。`Spout` 通常分为**可靠**和**不可靠**两种：可靠的 ` Spout` 能够在失败时重新发送 Tuple, 不可靠的 `Spout` 一旦把 Tuple 发送出去就置之不理了。\n\n### 1.4 Bolts\n\n`Bolts` 是流数据的处理单元，它可以从一个或者多个 `Streams` 中接收数据，处理完成后再发射到新的 `Streams` 中。`Bolts` 可以执行过滤 (filtering)，聚合 (aggregations)，连接 (joins) 等操作，并能与文件系统或数据库进行交互。\n\n### 1.5 Stream groupings（分组策略）\n\n<div align=\"center\"> <img width=\"400px\" src=\"../pictures/topology-tasks.png\"/> </div>\n\n`spouts` 和 `bolts` 在集群上执行任务时，是由多个 Task 并行执行 (如上图，每一个圆圈代表一个 Task)。当一个 Tuple 需要从 Bolt A 发送给 Bolt B 执行的时候，程序如何知道应该发送给 Bolt B 的哪一个 Task 执行呢？\n\n这是由 Stream groupings 分组策略来决定的，Storm 中一共有如下 8 个内置的 Stream Grouping。当然你也可以通过实现 `CustomStreamGrouping` 接口来实现自定义 Stream 分组策略。\n\n1. **Shuffle grouping**\n\n   Tuples 随机的分发到每个 Bolt 的每个 Task 上，每个 Bolt 获取到等量的 Tuples。\n\n2. **Fields grouping** \n\n    Streams 通过 grouping 指定的字段 (field) 来分组。假设通过 `user-id` 字段进行分区，那么具有相同 `user-id` 的 Tuples 就会发送到同一个 Task。\n\n3. **Partial Key grouping**\n\n   Streams 通过 grouping 中指定的字段 (field) 来分组，与 `Fields Grouping` 相似。但是对于两个下游的 Bolt 来说是负载均衡的，可以在输入数据不平均的情况下提供更好的优化。\n\n4. **All grouping** \n\n   Streams 会被所有的 Bolt 的 Tasks 进行复制。由于存在数据重复处理，所以需要谨慎使用。\n\n5. **Global grouping**  \n\n   整个 Streams 会进入 Bolt 的其中一个 Task，通常会进入 id 最小的 Task。\n\n6. **None grouping**\n\n   当前 None grouping 和 Shuffle grouping 等价，都是进行随机分发。\n\n7. **Direct grouping**\n\n   Direct grouping 只能被用于 direct streams 。使用这种方式需要由 Tuple 的生产者直接指定由哪个 Task 进行处理。\n\n8. **Local or shuffle grouping** \n\n   如果目标 Bolt 有 Tasks 和当前 Bolt 的 Tasks 处在同一个 Worker 进程中，那么则优先将 Tuple Shuffled 到处于同一个进程的目标 Bolt 的 Tasks 上，这样可以最大限度地减少网络传输。否则，就和普通的 `Shuffle Grouping` 行为一致。\n\n\n\n## 二、Storm架构详解\n\n<div align=\"center\"> <img  src=\"../pictures/Internal-Working-of-Apache-Storm.png\"/> </div>\n\n### 2.1 Nimbus进程\n\n 也叫做 Master Node，是 Storm 集群工作的全局指挥官。主要功能如下：\n\n1. 通过 Thrift 接口，监听并接收 Client 提交的 Topology；\n2. 根据集群 Workers 的资源情况，将 Client 提交的 Topology 进行任务分配，分配结果写入 Zookeeper;\n3. 通过 Thrift 接口，监听 Supervisor 的下载 Topology 代码的请求，并提供下载 ;\n4. 通过 Thrift 接口，监听 UI 对统计信息的读取，从 Zookeeper 上读取统计信息，返回给 UI;\n5. 若进程退出后，立即在本机重启，则不影响集群运行。 \n\n\n\n### 2.2 Supervisor进程\n\n也叫做 Worker Node , 是 Storm 集群的资源管理者，按需启动 Worker 进程。主要功能如下：\n\n1. 定时从 Zookeeper 检查是否有新 Topology 代码未下载到本地 ，并定时删除旧 Topology 代码 ;\n2. 根据 Nimbus 的任务分配计划，在本机按需启动 1 个或多个 Worker 进程，并监控所有的 Worker 进程的情况；\n3. 若进程退出，立即在本机重启，则不影响集群运行。 \n\n\n\n### 2.3 zookeeper的作用\n\nNimbus 和 Supervisor 进程都被设计为**快速失败**（遇到任何意外情况时进程自毁）和**无状态**（所有状态保存在 Zookeeper 或磁盘上）。  这样设计的好处就是如果它们的进程被意外销毁，那么在重新启动后，就只需要从 Zookeeper 上获取之前的状态数据即可，并不会造成任何数据丢失。\n\n\n\n### 2.4 Worker进程\n\nStorm 集群的任务构造者 ，构造 Spoult 或 Bolt 的 Task 实例，启动 Executor 线程。主要功能如下： \n\n1. 根据 Zookeeper 上分配的 Task，在本进程中启动 1 个或多个 Executor 线程，将构造好的 Task 实例交给 Executor 去运行；\n2. 向 Zookeeper 写入心跳 ；\n3. 维持传输队列，发送 Tuple 到其他的 Worker ；\n4. 若进程退出，立即在本机重启，则不影响集群运行。 \n\n   \n\n### 2.5 Executor线程\n\nStorm 集群的任务执行者 ，循环执行 Task 代码。主要功能如下：\n\n1. 执行 1 个或多个 Task；\n2. 执行 Acker 机制，负责发送 Task 处理状态给对应 Spout 所在的 worker。\n\n\n\n### 2.6 并行度\n\n<div align=\"center\"> <img  src=\"../pictures/relationships-worker-processes-executors-tasks.png\"/> </div>\n\n1 个 Worker 进程执行的是 1 个 Topology 的子集，不会出现 1 个 Worker 为多个 Topology 服务的情况，因此 1 个运行中的 Topology 就是由集群中多台物理机上的多个 Worker 进程组成的。1 个 Worker 进程会启动 1 个或多个 Executor 线程来执行 1 个 Topology 的 Component(组件，即 Spout 或 Bolt)。\n\nExecutor 是 1 个被 Worker 进程启动的单独线程。每个 Executor 会运行 1 个 Component 中的一个或者多个 Task。\n\nTask 是组成 Component 的代码单元。Topology 启动后，1 个 Component 的 Task 数目是固定不变的，但该 Component 使用的 Executor 线程数可以动态调整（例如：1 个 Executor 线程可以执行该 Component 的 1 个或多个 Task 实例）。这意味着，对于 1 个 Component 来说，`#threads<=#tasks`（线程数小于等于 Task 数目）这样的情况是存在的。默认情况下 Task 的数目等于 Executor 线程数，即 1 个 Executor 线程只运行 1 个 Task。  \n\n**总结如下：**\n\n+ 一个运行中的 Topology 由集群中的多个 Worker 进程组成的；\n+ 在默认情况下，每个 Worker 进程默认启动一个 Executor 线程；\n+ 在默认情况下，每个 Executor 默认启动一个 Task 线程；\n+ Task 是组成 Component 的代码单元。\n\n\n\n## 参考资料\n\n1. [storm documentation -> Concepts](http://storm.apache.org/releases/1.2.2/Concepts.html)\n\n2. [Internal Working of Apache Storm](https://www.spritle.com/blogs/2016/04/04/apache-storm/)\n3. [Understanding the Parallelism of a Storm Topology](http://storm.apache.org/releases/1.2.2/Understanding-the-parallelism-of-a-Storm-topology.html)\n4. [Storm nimbus 单节点宕机的处理](https://blog.csdn.net/daiyutage/article/details/52049519)\n\n"
  },
  {
    "path": "大数据框架学习/Storm编程模型详解.md",
    "content": "# Storm 编程模型\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二IComponent接口\">二、IComponent接口</a><br/>\n<a href=\"#三Spout\">三、Spout</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-ISpout接口\">3.1 ISpout接口</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-BaseRichSpout抽象类\">3.2 BaseRichSpout抽象类</a><br/>\n<a href=\"#四Bolt\">四、Bolt</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-IBolt-接口\">4.1 IBolt 接口</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-BaseRichBolt抽象类\">4.2 BaseRichBolt抽象类</a><br/>\n<a href=\"#五词频统计案例\">五、词频统计案例</a><br/>\n<a href=\"#六提交到服务器集群运行\">六、提交到服务器集群运行</a><br/>\n<a href=\"#七关于项目打包的扩展说明\">七、关于项目打包的扩展说明</a><br/>\n</nav>\n\n\n\n\n\n## 一、简介\n\n下图为 Strom 的运行流程图，在开发 Storm 流处理程序时，我们需要采用内置或自定义实现 `spout`(数据源) 和 `bolt`(处理单元)，并通过 `TopologyBuilder` 将它们之间进行关联，形成 `Topology`。\n\n<div align=\"center\"> <img  src=\"../pictures/spout-bolt.png\"/> </div>\n\n## 二、IComponent接口\n\n`IComponent` 接口定义了 Topology 中所有组件 (spout/bolt) 的公共方法，自定义的 spout 或 bolt 必须直接或间接实现这个接口。\n\n```java\npublic interface IComponent extends Serializable {\n\n    /**\n     * 声明此拓扑的所有流的输出模式。\n     * @param declarer 这用于声明输出流 id，输出字段以及每个输出流是否是直接流（direct stream）\n     */\n    void declareOutputFields(OutputFieldsDeclarer declarer);\n\n    /**\n     * 声明此组件的配置。\n     *\n     */\n    Map<String, Object> getComponentConfiguration();\n\n}\n```\n\n## 三、Spout\n\n### 3.1 ISpout接口\n\n自定义的 spout 需要实现 `ISpout` 接口，它定义了 spout 的所有可用方法：\n\n```java\npublic interface ISpout extends Serializable {\n    /**\n     * 组件初始化时候被调用\n     *\n     * @param conf ISpout 的配置\n     * @param context 应用上下文，可以通过其获取任务 ID 和组件 ID，输入和输出信息等。\n     * @param collector  用来发送 spout 中的 tuples，它是线程安全的，建议保存为此 spout 对象的实例变量\n     */\n    void open(Map conf, TopologyContext context, SpoutOutputCollector collector);\n\n    /**\n     * ISpout 将要被关闭的时候调用。但是其不一定会被执行，如果在集群环境中通过 kill -9 杀死进程时其就无法被执行。\n     */\n    void close();\n    \n    /**\n     * 当 ISpout 从停用状态激活时被调用\n     */\n    void activate();\n    \n    /**\n     * 当 ISpout 停用时候被调用\n     */\n    void deactivate();\n\n    /**\n     * 这是一个核心方法，主要通过在此方法中调用 collector 将 tuples 发送给下一个接收器，这个方法必须是非阻塞的。     \n     * nextTuple/ack/fail/是在同一个线程中执行的，所以不用考虑线程安全方面。当没有 tuples 发出时应该让\n     * nextTuple 休眠 (sleep) 一下，以免浪费 CPU。\n     */\n    void nextTuple();\n\n    /**\n     * 通过 msgId 进行 tuples 处理成功的确认，被确认后的 tuples 不会再次被发送\n     */\n    void ack(Object msgId);\n\n    /**\n     * 通过 msgId 进行 tuples 处理失败的确认，被确认后的 tuples 会再次被发送进行处理\n     */\n    void fail(Object msgId);\n}\n```\n\n### 3.2 BaseRichSpout抽象类\n\n**通常情况下，我们实现自定义的 Spout 时不会直接去实现 `ISpout` 接口，而是继承 `BaseRichSpout`。**`BaseRichSpout` 继承自 `BaseCompont`，同时实现了 `IRichSpout` 接口。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-baseRichSpout.png\"/> </div>\n\n`IRichSpout` 接口继承自 `ISpout` 和 `IComponent`,自身并没有定义任何方法：\n\n```java\npublic interface IRichSpout extends ISpout, IComponent {\n\n}\n```\n\n`BaseComponent` 抽象类空实现了 `IComponent` 中 `getComponentConfiguration` 方法：\n\n```java\npublic abstract class BaseComponent implements IComponent {\n    @Override\n    public Map<String, Object> getComponentConfiguration() {\n        return null;\n    }    \n}\n```\n\n`BaseRichSpout` 继承自 `BaseCompont` 类并实现了 `IRichSpout` 接口，并且空实现了其中部分方法：\n\n```java\npublic abstract class BaseRichSpout extends BaseComponent implements IRichSpout {\n    @Override\n    public void close() {}\n\n    @Override\n    public void activate() {}\n\n    @Override\n    public void deactivate() {}\n\n    @Override\n    public void ack(Object msgId) {}\n\n    @Override\n    public void fail(Object msgId) {}\n}\n```\n\n通过这样的设计，我们在继承 `BaseRichSpout` 实现自定义 spout 时，就只有三个方法必须实现：\n\n+ **open** ： 来源于 ISpout，可以通过此方法获取用来发送 tuples 的 `SpoutOutputCollector`；\n+ **nextTuple** ：来源于 ISpout，必须在此方法内部发送 tuples；\n+ **declareOutputFields** ：来源于 IComponent，声明发送的 tuples 的名称，这样下一个组件才能知道如何接受。\n\n\n\n## 四、Bolt\n\nbolt 接口的设计与 spout 的类似：\n\n### 4.1 IBolt 接口\n\n```java\n /**\n  * 在客户端计算机上创建的 IBolt 对象。会被被序列化到 topology 中（使用 Java 序列化）,并提交给集群的主机（Nimbus）。  \n  * Nimbus 启动 workers 反序列化对象，调用 prepare，然后开始处理 tuples。\n */\n\npublic interface IBolt extends Serializable {\n    /**\n     * 组件初始化时候被调用\n     *\n     * @param conf storm 中定义的此 bolt 的配置\n     * @param context 应用上下文，可以通过其获取任务 ID 和组件 ID，输入和输出信息等。\n     * @param collector  用来发送 spout 中的 tuples，它是线程安全的，建议保存为此 spout 对象的实例变量\n     */\n    void prepare(Map stormConf, TopologyContext context, OutputCollector collector);\n\n    /**\n     * 处理单个 tuple 输入。\n     * \n     * @param Tuple 对象包含关于它的元数据（如来自哪个组件/流/任务）\n     */\n    void execute(Tuple input);\n\n    /**\n     * IBolt 将要被关闭的时候调用。但是其不一定会被执行，如果在集群环境中通过 kill -9 杀死进程时其就无法被执行。\n     */\n    void cleanup();\n```\n\n\n\n### 4.2 BaseRichBolt抽象类\n\n同样的，在实现自定义 bolt 时，通常是继承 `BaseRichBolt` 抽象类来实现。`BaseRichBolt` 继承自 `BaseComponent` 抽象类并实现了 `IRichBolt` 接口。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-baseRichbolt.png\"/> </div>\n\n`IRichBolt` 接口继承自 `IBolt` 和 `IComponent`,自身并没有定义任何方法：\n\n```\npublic interface IRichBolt extends IBolt, IComponent {\n\n}\n```\n\n通过这样的设计，在继承 `BaseRichBolt` 实现自定义 bolt 时，就只需要实现三个必须的方法：\n\n- **prepare**： 来源于 IBolt，可以通过此方法获取用来发送 tuples 的 `OutputCollector`；\n- **execute**：来源于 IBolt，处理 tuples 和发送处理完成的 tuples；\n- **declareOutputFields** ：来源于 IComponent，声明发送的 tuples 的名称，这样下一个组件才能知道如何接收。\n\n\n\n## 五、词频统计案例\n\n### 5.1 案例简介\n\n这里我们使用自定义的 `DataSourceSpout` 产生词频数据，然后使用自定义的 `SplitBolt` 和 `CountBolt` 来进行词频统计。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-word-count-p.png\"/> </div>\n\n> 案例源码下载地址：[storm-word-count](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-word-count)\n\n### 5.2 代码实现\n\n#### 1. 项目依赖\n\n```xml\n<dependency>\n    <groupId>org.apache.storm</groupId>\n    <artifactId>storm-core</artifactId>\n    <version>1.2.2</version>\n</dependency>\n```\n\n#### 2. DataSourceSpout\n\n```java\npublic class DataSourceSpout extends BaseRichSpout {\n\n    private List<String> list = Arrays.asList(\"Spark\", \"Hadoop\", \"HBase\", \"Storm\", \"Flink\", \"Hive\");\n\n    private SpoutOutputCollector spoutOutputCollector;\n\n    @Override\n    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {\n        this.spoutOutputCollector = spoutOutputCollector;\n    }\n\n    @Override\n    public void nextTuple() {\n        // 模拟产生数据\n        String lineData = productData();\n        spoutOutputCollector.emit(new Values(lineData));\n        Utils.sleep(1000);\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {\n        outputFieldsDeclarer.declare(new Fields(\"line\"));\n    }\n\n\n    /**\n     * 模拟数据\n     */\n    private String productData() {\n        Collections.shuffle(list);\n        Random random = new Random();\n        int endIndex = random.nextInt(list.size()) % (list.size()) + 1;\n        return StringUtils.join(list.toArray(), \"\\t\", 0, endIndex);\n    }\n\n}\n```\n\n上面类使用 `productData` 方法来产生模拟数据，产生数据的格式如下：\n\n```properties\nSpark\tHBase\nHive\tFlink\tStorm\tHadoop\tHBase\tSpark\nFlink\nHBase\tStorm\nHBase\tHadoop\tHive\tFlink\nHBase\tFlink\tHive\tStorm\nHive\tFlink\tHadoop\nHBase\tHive\nHadoop\tSpark\tHBase\tStorm\n```\n\n#### 3. SplitBolt\n\n```java\npublic class SplitBolt extends BaseRichBolt {\n\n    private OutputCollector collector;\n\n    @Override\n    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {\n        this.collector=collector;\n    }\n\n    @Override\n    public void execute(Tuple input) {\n        String line = input.getStringByField(\"line\");\n        String[] words = line.split(\"\\t\");\n        for (String word : words) {\n            collector.emit(new Values(word));\n        }\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n        declarer.declare(new Fields(\"word\"));\n    }\n}\n```\n\n#### 4. CountBolt\n\n```java\npublic class CountBolt extends BaseRichBolt {\n\n    private Map<String, Integer> counts = new HashMap<>();\n\n    @Override\n    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {\n\n    }\n\n    @Override\n    public void execute(Tuple input) {\n        String word = input.getStringByField(\"word\");\n        Integer count = counts.get(word);\n        if (count == null) {\n            count = 0;\n        }\n        count++;\n        counts.put(word, count);\n        // 输出\n        System.out.print(\"当前实时统计结果:\");\n        counts.forEach((key, value) -> System.out.print(key + \":\" + value + \"; \"));\n        System.out.println();\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n\n    }\n}\n```\n\n#### 5.  LocalWordCountApp\n\n通过 TopologyBuilder 将上面定义好的组件进行串联形成 Topology，并提交到本地集群（LocalCluster）运行。通常在开发中，可先用本地模式进行测试，测试完成后再提交到服务器集群运行。\n\n```java\npublic class LocalWordCountApp {\n\n    public static void main(String[] args) {\n        TopologyBuilder builder = new TopologyBuilder();\n        \n        builder.setSpout(\"DataSourceSpout\", new DataSourceSpout());\n        \n        // 指明将 DataSourceSpout 的数据发送到 SplitBolt 中处理\n        builder.setBolt(\"SplitBolt\", new SplitBolt()).shuffleGrouping(\"DataSourceSpout\");\n        \n        //  指明将 SplitBolt 的数据发送到 CountBolt 中 处理\n        builder.setBolt(\"CountBolt\", new CountBolt()).shuffleGrouping(\"SplitBolt\");\n\n        // 创建本地集群用于测试 这种模式不需要本机安装 storm,直接运行该 Main 方法即可\n        LocalCluster cluster = new LocalCluster();\n        cluster.submitTopology(\"LocalWordCountApp\",\n                new Config(), builder.createTopology());\n    }\n\n}\n```\n\n\n\n#### 6. 运行结果\n\n启动 `WordCountApp` 的 main 方法即可运行，采用本地模式 Storm 会自动在本地搭建一个集群，所以启动的过程会稍慢一点，启动成功后即可看到输出日志。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-word-count-console.png\"/> </div>\n\n\n## 六、提交到服务器集群运行\n\n### 6.1 代码更改\n\n提交到服务器的代码和本地代码略有不同，提交到服务器集群时需要使用 `StormSubmitter` 进行提交。主要代码如下：\n\n> 为了结构清晰，这里新建 ClusterWordCountApp 类来演示集群模式的提交。实际开发中可以将两种模式的代码写在同一个类中，通过外部传参来决定启动何种模式。\n\n```java\npublic class ClusterWordCountApp {\n\n    public static void main(String[] args) {\n        TopologyBuilder builder = new TopologyBuilder();\n        \n        builder.setSpout(\"DataSourceSpout\", new DataSourceSpout());\n        \n        // 指明将 DataSourceSpout 的数据发送到 SplitBolt 中处理\n        builder.setBolt(\"SplitBolt\", new SplitBolt()).shuffleGrouping(\"DataSourceSpout\");\n        \n        //  指明将 SplitBolt 的数据发送到 CountBolt 中 处理\n        builder.setBolt(\"CountBolt\", new CountBolt()).shuffleGrouping(\"SplitBolt\");\n\n        // 使用 StormSubmitter 提交 Topology 到服务器集群\n        try {\n            StormSubmitter.submitTopology(\"ClusterWordCountApp\",  new Config(), builder.createTopology());\n        } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {\n            e.printStackTrace();\n        }\n    }\n\n}\n```\n\n### 6.2 打包上传\n\n打包后上传到服务器任意位置，这里我打包后的名称为 `storm-word-count-1.0.jar`\n\n```shell\n# mvn clean package -Dmaven.test.skip=true\n```\n\n### 6.3 提交Topology\n\n使用以下命令提交 Topology 到集群：\n\n```shell\n# 命令格式: storm jar jar包位置 主类的全路径 ...可选传参\nstorm jar /usr/appjar/storm-word-count-1.0.jar  com.heibaiying.wordcount.ClusterWordCountApp\n```\n\n出现 `successfully` 则代表提交成功：\n\n<div align=\"center\"> <img  src=\"../pictures/storm-submit-success.png\"/> </div>\n\n### 6.4 查看Topology与停止Topology（命令行方式）\n\n```shell\n# 查看所有Topology\nstorm list\n\n# 停止  storm kill topology-name [-w wait-time-secs]\nstorm kill ClusterWordCountApp -w 3\n```\n\n<div align=\"center\"> <img  src=\"../pictures/storm-list-kill.png\"/> </div>\n\n### 6.5 查看Topology与停止Topology（界面方式）\n\n使用 UI 界面同样也可进行停止操作，进入 WEB UI 界面（8080 端口），在 `Topology Summary` 中点击对应 Topology 即可进入详情页面进行操作。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-ui-actions.png\"/> </div>\n\n\n\n\n\n\n\n\n## 七、关于项目打包的扩展说明\n\n### mvn package的局限性\n\n在上面的步骤中，我们没有在 POM 中配置任何插件，就直接使用 `mvn package` 进行项目打包，这对于没有使用外部依赖包的项目是可行的。但如果项目中使用了第三方 JAR 包，就会出现问题，因为 `package` 打包后的 JAR 中是不含有依赖包的，如果此时你提交到服务器上运行，就会出现找不到第三方依赖的异常。\n\n这时候可能大家会有疑惑，在我们的项目中不是使用了 `storm-core` 这个依赖吗？其实上面之所以我们能运行成功，是因为在 Storm 的集群环境中提供了这个 JAR 包，在安装目录的 lib 目录下：\n\n<div align=\"center\"> <img  src=\"../pictures/storm-lib.png\"/> </div>\n为了说明这个问题我在 Maven 中引入了一个第三方的 JAR 包，并修改产生数据的方法：\n\n```xml\n<dependency>\n    <groupId>org.apache.commons</groupId>\n    <artifactId>commons-lang3</artifactId>\n    <version>3.8.1</version>\n</dependency>\n```\n\n`StringUtils.join()` 这个方法在 `commons.lang3` 和 `storm-core` 中都有，原来的代码无需任何更改，只需要在 `import` 时指明使用 `commons.lang3`。\n\n```java\nimport org.apache.commons.lang3.StringUtils;\n\nprivate String productData() {\n    Collections.shuffle(list);\n    Random random = new Random();\n    int endIndex = random.nextInt(list.size()) % (list.size()) + 1;\n    return StringUtils.join(list.toArray(), \"\\t\", 0, endIndex);\n}\n```\n\n此时直接使用 `mvn clean package` 打包运行，就会抛出下图的异常。因此这种直接打包的方式并不适用于实际的开发，因为实际开发中通常都是需要第三方的 JAR 包。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-package-error.png\"/> </div>\n\n\n想把依赖包一并打入最后的 JAR 中，maven 提供了两个插件来实现，分别是 `maven-assembly-plugin` 和 `maven-shade-plugin`。鉴于本篇文章篇幅已经比较长，且关于 Storm 打包还有很多需要说明的地方，所以关于 Storm 的打包方式单独整理至下一篇文章：\n\n[Storm 三种打包方式对比分析](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Storm三种打包方式对比分析.md)\n\n## 参考资料\n\n1. [Running Topologies on a Production Cluster](http://storm.apache.org/releases/2.0.0-SNAPSHOT/Running-topologies-on-a-production-cluster.html)\n2. [Pre-defined Descriptor Files](http://maven.apache.org/plugins/maven-assembly-plugin/descriptor-refs.html)\n"
  },
  {
    "path": "大数据框架学习/Storm集成HBase和HDFS.md",
    "content": "# Storm集成HDFS和HBase\n\n<nav>\n<a href=\"#一Storm集成HDFS\">一、Storm集成HDFS</a><br/>\n<a href=\"#二Storm集成HBase\">二、Storm集成HBase</a><br/>\n</nav>\n\n## 一、Storm集成HDFS\n\n### 1.1 项目结构\n\n<div align=\"center\"> <img  src=\"../pictures/datasourcetohdfs.png\"/> </div>\n\n> 本用例源码下载地址：[storm-hdfs-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-hdfs-integration)\n\n### 1.2 项目主要依赖\n\n项目主要依赖如下，有两个地方需要注意：\n\n+ 这里由于我服务器上安装的是 CDH 版本的 Hadoop，在导入依赖时引入的也是 CDH 版本的依赖，需要使用 `<repository>` 标签指定 CDH 的仓库地址；\n+ `hadoop-common`、`hadoop-client`、`hadoop-hdfs` 均需要排除 `slf4j-log4j12` 依赖，原因是 `storm-core` 中已经有该依赖，不排除的话有 JAR 包冲突的风险；\n\n```xml\n<properties>\n    <storm.version>1.2.2</storm.version>\n</properties>\n\n<repositories>\n    <repository>\n        <id>cloudera</id>\n        <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>\n    </repository>\n</repositories>\n\n<dependencies>\n    <dependency>\n        <groupId>org.apache.storm</groupId>\n        <artifactId>storm-core</artifactId>\n        <version>${storm.version}</version>\n    </dependency>\n    <!--Storm 整合 HDFS 依赖-->\n    <dependency>\n        <groupId>org.apache.storm</groupId>\n        <artifactId>storm-hdfs</artifactId>\n        <version>${storm.version}</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.hadoop</groupId>\n        <artifactId>hadoop-common</artifactId>\n        <version>2.6.0-cdh5.15.2</version>\n        <exclusions>\n            <exclusion>\n                <groupId>org.slf4j</groupId>\n                <artifactId>slf4j-log4j12</artifactId>\n            </exclusion>\n        </exclusions>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.hadoop</groupId>\n        <artifactId>hadoop-client</artifactId>\n        <version>2.6.0-cdh5.15.2</version>\n        <exclusions>\n            <exclusion>\n                <groupId>org.slf4j</groupId>\n                <artifactId>slf4j-log4j12</artifactId>\n            </exclusion>\n        </exclusions>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.hadoop</groupId>\n        <artifactId>hadoop-hdfs</artifactId>\n        <version>2.6.0-cdh5.15.2</version>\n        <exclusions>\n            <exclusion>\n                <groupId>org.slf4j</groupId>\n                <artifactId>slf4j-log4j12</artifactId>\n            </exclusion>\n        </exclusions>\n    </dependency>\n</dependencies>\n```\n\n### 1.3 DataSourceSpout\n\n```java\n/**\n * 产生词频样本的数据源\n */\npublic class DataSourceSpout extends BaseRichSpout {\n\n    private List<String> list = Arrays.asList(\"Spark\", \"Hadoop\", \"HBase\", \"Storm\", \"Flink\", \"Hive\");\n\n    private SpoutOutputCollector spoutOutputCollector;\n\n    @Override\n    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {\n        this.spoutOutputCollector = spoutOutputCollector;\n    }\n\n    @Override\n    public void nextTuple() {\n        // 模拟产生数据\n        String lineData = productData();\n        spoutOutputCollector.emit(new Values(lineData));\n        Utils.sleep(1000);\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {\n        outputFieldsDeclarer.declare(new Fields(\"line\"));\n    }\n\n\n    /**\n     * 模拟数据\n     */\n    private String productData() {\n        Collections.shuffle(list);\n        Random random = new Random();\n        int endIndex = random.nextInt(list.size()) % (list.size()) + 1;\n        return StringUtils.join(list.toArray(), \"\\t\", 0, endIndex);\n    }\n\n}\n```\n\n产生的模拟数据格式如下：\n\n```properties\nSpark\tHBase\nHive\tFlink\tStorm\tHadoop\tHBase\tSpark\nFlink\nHBase\tStorm\nHBase\tHadoop\tHive\tFlink\nHBase\tFlink\tHive\tStorm\nHive\tFlink\tHadoop\nHBase\tHive\nHadoop\tSpark\tHBase\tStorm\n```\n\n### 1.4 将数据存储到HDFS\n\n这里 HDFS 的地址和数据存储路径均使用了硬编码，在实际开发中可以通过外部传参指定，这样程序更为灵活。\n\n```java\npublic class DataToHdfsApp {\n\n    private static final String DATA_SOURCE_SPOUT = \"dataSourceSpout\";\n    private static final String HDFS_BOLT = \"hdfsBolt\";\n\n    public static void main(String[] args) {\n\n        // 指定 Hadoop 的用户名 如果不指定,则在 HDFS 创建目录时候有可能抛出无权限的异常 (RemoteException: Permission denied)\n        System.setProperty(\"HADOOP_USER_NAME\", \"root\");\n\n        // 定义输出字段 (Field) 之间的分隔符\n        RecordFormat format = new DelimitedRecordFormat()\n                .withFieldDelimiter(\"|\");\n\n        // 同步策略: 每 100 个 tuples 之后就会把数据从缓存刷新到 HDFS 中\n        SyncPolicy syncPolicy = new CountSyncPolicy(100);\n\n        // 文件策略: 每个文件大小上限 1M,超过限定时,创建新文件并继续写入\n        FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(1.0f, Units.MB);\n\n        // 定义存储路径\n        FileNameFormat fileNameFormat = new DefaultFileNameFormat()\n                .withPath(\"/storm-hdfs/\");\n\n        // 定义 HdfsBolt\n        HdfsBolt hdfsBolt = new HdfsBolt()\n                .withFsUrl(\"hdfs://hadoop001:8020\")\n                .withFileNameFormat(fileNameFormat)\n                .withRecordFormat(format)\n                .withRotationPolicy(rotationPolicy)\n                .withSyncPolicy(syncPolicy);\n\n\n        // 构建 Topology\n        TopologyBuilder builder = new TopologyBuilder();\n        builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout());\n        // save to HDFS\n        builder.setBolt(HDFS_BOLT, hdfsBolt, 1).shuffleGrouping(DATA_SOURCE_SPOUT);\n\n\n        // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动\n        if (args.length > 0 && args[0].equals(\"cluster\")) {\n            try {\n                StormSubmitter.submitTopology(\"ClusterDataToHdfsApp\", new Config(), builder.createTopology());\n            } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {\n                e.printStackTrace();\n            }\n        } else {\n            LocalCluster cluster = new LocalCluster();\n            cluster.submitTopology(\"LocalDataToHdfsApp\",\n                    new Config(), builder.createTopology());\n        }\n    }\n}\n```\n\n### 1.5 启动测试\n\n可以用直接使用本地模式运行，也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包，打包命令如下：\n\n```shell\n# mvn clean package -D maven.test.skip=true\n```\n\n运行后，数据会存储到 HDFS 的 `/storm-hdfs` 目录下。使用以下命令可以查看目录内容：\n\n```shell\n# 查看目录内容\nhadoop fs -ls /storm-hdfs\n# 监听文内容变化\nhadoop fs -tail -f /strom-hdfs/文件名\n```\n\n\n\n<div align=\"center\"> <img  src=\"../pictures/storm-hdfs-result.png\"/> </div>\n\n\n\n## 二、Storm集成HBase\n\n### 2.1 项目结构\n\n集成用例： 进行词频统计并将最后的结果存储到 HBase，项目主要结构如下：\n\n<div align=\"center\"> <img  src=\"../pictures/WordCountToHBaseApp.png\"/> </div>\n\n> 本用例源码下载地址：[storm-hbase-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-hbase-integration)\n\n### 2.2 项目主要依赖\n\n```xml\n <properties>\n        <storm.version>1.2.2</storm.version>\n    </properties>\n\n\n    <dependencies>\n        <dependency>\n            <groupId>org.apache.storm</groupId>\n            <artifactId>storm-core</artifactId>\n            <version>${storm.version}</version>\n        </dependency>\n        <!--Storm 整合 HBase 依赖-->\n        <dependency>\n            <groupId>org.apache.storm</groupId>\n            <artifactId>storm-hbase</artifactId>\n            <version>${storm.version}</version>\n        </dependency>\n    </dependencies>\n```\n\n### 2.3 DataSourceSpout\n\n```java\n/**\n * 产生词频样本的数据源\n */\npublic class DataSourceSpout extends BaseRichSpout {\n\n    private List<String> list = Arrays.asList(\"Spark\", \"Hadoop\", \"HBase\", \"Storm\", \"Flink\", \"Hive\");\n\n    private SpoutOutputCollector spoutOutputCollector;\n\n    @Override\n    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {\n        this.spoutOutputCollector = spoutOutputCollector;\n    }\n\n    @Override\n    public void nextTuple() {\n        // 模拟产生数据\n        String lineData = productData();\n        spoutOutputCollector.emit(new Values(lineData));\n        Utils.sleep(1000);\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {\n        outputFieldsDeclarer.declare(new Fields(\"line\"));\n    }\n\n\n    /**\n     * 模拟数据\n     */\n    private String productData() {\n        Collections.shuffle(list);\n        Random random = new Random();\n        int endIndex = random.nextInt(list.size()) % (list.size()) + 1;\n        return StringUtils.join(list.toArray(), \"\\t\", 0, endIndex);\n    }\n\n}\n```\n\n产生的模拟数据格式如下：\n\n```properties\nSpark\tHBase\nHive\tFlink\tStorm\tHadoop\tHBase\tSpark\nFlink\nHBase\tStorm\nHBase\tHadoop\tHive\tFlink\nHBase\tFlink\tHive\tStorm\nHive\tFlink\tHadoop\nHBase\tHive\nHadoop\tSpark\tHBase\tStorm\n```\n\n\n\n### 2.4 SplitBolt\n\n```java\n/**\n * 将每行数据按照指定分隔符进行拆分\n */\npublic class SplitBolt extends BaseRichBolt {\n\n    private OutputCollector collector;\n\n    @Override\n    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {\n        this.collector = collector;\n    }\n\n    @Override\n    public void execute(Tuple input) {\n        String line = input.getStringByField(\"line\");\n        String[] words = line.split(\"\\t\");\n        for (String word : words) {\n            collector.emit(tuple(word, 1));\n        }\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n        declarer.declare(new Fields(\"word\", \"count\"));\n    }\n}\n```\n\n### 2.5 CountBolt\n\n```java\n/**\n * 进行词频统计\n */\npublic class CountBolt extends BaseRichBolt {\n\n    private Map<String, Integer> counts = new HashMap<>();\n\n    private OutputCollector collector;\n\n\n    @Override\n    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {\n            this.collector=collector;\n    }\n\n    @Override\n    public void execute(Tuple input) {\n        String word = input.getStringByField(\"word\");\n        Integer count = counts.get(word);\n        if (count == null) {\n            count = 0;\n        }\n        count++;\n        counts.put(word, count);\n        // 输出\n        collector.emit(new Values(word, String.valueOf(count)));\n\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n        declarer.declare(new Fields(\"word\", \"count\"));\n    }\n}\n```\n\n### 2.6 WordCountToHBaseApp\n\n```java\n/**\n * 进行词频统计 并将统计结果存储到 HBase 中\n */\npublic class WordCountToHBaseApp {\n\n    private static final String DATA_SOURCE_SPOUT = \"dataSourceSpout\";\n    private static final String SPLIT_BOLT = \"splitBolt\";\n    private static final String COUNT_BOLT = \"countBolt\";\n    private static final String HBASE_BOLT = \"hbaseBolt\";\n\n    public static void main(String[] args) {\n\n        // storm 的配置\n        Config config = new Config();\n\n        // HBase 的配置\n        Map<String, Object> hbConf = new HashMap<>();\n        hbConf.put(\"hbase.rootdir\", \"hdfs://hadoop001:8020/hbase\");\n        hbConf.put(\"hbase.zookeeper.quorum\", \"hadoop001:2181\");\n\n        // 将 HBase 的配置传入 Storm 的配置中\n        config.put(\"hbase.conf\", hbConf);\n\n        // 定义流数据与 HBase 中数据的映射\n        SimpleHBaseMapper mapper = new SimpleHBaseMapper()\n                .withRowKeyField(\"word\")\n                .withColumnFields(new Fields(\"word\",\"count\"))\n                .withColumnFamily(\"info\");\n\n        /*\n         * 给 HBaseBolt 传入表名、数据映射关系、和 HBase 的配置信息\n         * 表需要预先创建: create 'WordCount','info'\n         */\n        HBaseBolt hbase = new HBaseBolt(\"WordCount\", mapper)\n                .withConfigKey(\"hbase.conf\");\n\n        // 构建 Topology\n        TopologyBuilder builder = new TopologyBuilder();\n        builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout(),1);\n        // split\n        builder.setBolt(SPLIT_BOLT, new SplitBolt(), 1).shuffleGrouping(DATA_SOURCE_SPOUT);\n        // count\n        builder.setBolt(COUNT_BOLT, new CountBolt(),1).shuffleGrouping(SPLIT_BOLT);\n        // save to HBase\n        builder.setBolt(HBASE_BOLT, hbase, 1).shuffleGrouping(COUNT_BOLT);\n\n\n        // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动\n        if (args.length > 0 && args[0].equals(\"cluster\")) {\n            try {\n                StormSubmitter.submitTopology(\"ClusterWordCountToRedisApp\", config, builder.createTopology());\n            } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {\n                e.printStackTrace();\n            }\n        } else {\n            LocalCluster cluster = new LocalCluster();\n            cluster.submitTopology(\"LocalWordCountToRedisApp\",\n                    config, builder.createTopology());\n        }\n    }\n}\n```\n\n### 2.7 启动测试\n\n可以用直接使用本地模式运行，也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包，打包命令如下：\n\n```shell\n# mvn clean package -D maven.test.skip=true\n```\n\n运行后，数据会存储到 HBase 的 `WordCount` 表中。使用以下命令查看表的内容：\n\n```shell\nhbase >  scan 'WordCount'\n```\n\n<div align=\"center\"> <img  src=\"../pictures/storm-hbase-result.png\"/> </div>\n\n\n\n### 2.8 withCounterFields\n\n在上面的用例中我们是手动编码来实现词频统计，并将最后的结果存储到 HBase 中。其实也可以在构建 `SimpleHBaseMapper` 的时候通过 `withCounterFields` 指定 count 字段，被指定的字段会自动进行累加操作，这样也可以实现词频统计。需要注意的是 withCounterFields 指定的字段必须是 Long 类型，不能是 String 类型。\n\n```java\nSimpleHBaseMapper mapper = new SimpleHBaseMapper() \n        .withRowKeyField(\"word\")\n        .withColumnFields(new Fields(\"word\"))\n        .withCounterFields(new Fields(\"count\"))\n        .withColumnFamily(\"cf\");\n```\n\n\n\n## 参考资料\n\n1. [Apache HDFS Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html)\n2. [Apache HBase Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hbase.html)\n"
  },
  {
    "path": "大数据框架学习/Storm集成Kakfa.md",
    "content": "# Storm集成Kafka\n\n<nav>\n<a href=\"#一整合说明\">一、整合说明</a><br/>\n<a href=\"#二写入数据到Kafka\">二、写入数据到Kafka</a><br/>\n<a href=\"#三从Kafka中读取数据\">三、从Kafka中读取数据</a><br/>\n</nav>\n\n\n## 一、整合说明\n\nStorm 官方对 Kafka 的整合分为两个版本，官方说明文档分别如下：\n\n+ [Storm Kafka Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-kafka.html) : 主要是针对 0.8.x 版本的 Kafka 提供整合支持；\n+  [Storm Kafka Integration (0.10.x+)]() : 包含 Kafka 新版本的 consumer API，主要对 Kafka 0.10.x + 提供整合支持。\n\n这里我服务端安装的 Kafka 版本为 2.2.0(Released Mar 22, 2019) ，按照官方 0.10.x+ 的整合文档进行整合，不适用于 0.8.x 版本的 Kafka。\n\n## 二、写入数据到Kafka\n\n### 2.1 项目结构\n\n<div align=\"center\"> <img  src=\"../pictures/writetokafka.png\"/> </div>\n\n### 2.2 项目主要依赖\n\n```xml\n<properties>\n    <storm.version>1.2.2</storm.version>\n    <kafka.version>2.2.0</kafka.version>\n</properties>\n\n<dependencies>\n    <dependency>\n        <groupId>org.apache.storm</groupId>\n        <artifactId>storm-core</artifactId>\n        <version>${storm.version}</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.storm</groupId>\n        <artifactId>storm-kafka-client</artifactId>\n        <version>${storm.version}</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.kafka</groupId>\n        <artifactId>kafka-clients</artifactId>\n        <version>${kafka.version}</version>\n    </dependency>\n</dependencies>\n```\n\n### 2.3 DataSourceSpout\n\n```java\n/**\n * 产生词频样本的数据源\n */\npublic class DataSourceSpout extends BaseRichSpout {\n\n    private List<String> list = Arrays.asList(\"Spark\", \"Hadoop\", \"HBase\", \"Storm\", \"Flink\", \"Hive\");\n\n    private SpoutOutputCollector spoutOutputCollector;\n\n    @Override\n    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {\n        this.spoutOutputCollector = spoutOutputCollector;\n    }\n\n    @Override\n    public void nextTuple() {\n        // 模拟产生数据\n        String lineData = productData();\n        spoutOutputCollector.emit(new Values(lineData));\n        Utils.sleep(1000);\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {\n        outputFieldsDeclarer.declare(new Fields(\"line\"));\n    }\n\n\n    /**\n     * 模拟数据\n     */\n    private String productData() {\n        Collections.shuffle(list);\n        Random random = new Random();\n        int endIndex = random.nextInt(list.size()) % (list.size()) + 1;\n        return StringUtils.join(list.toArray(), \"\\t\", 0, endIndex);\n    }\n\n}\n```\n\n产生的模拟数据格式如下：\n\n```properties\nSpark\tHBase\nHive\tFlink\tStorm\tHadoop\tHBase\tSpark\nFlink\nHBase\tStorm\nHBase\tHadoop\tHive\tFlink\nHBase\tFlink\tHive\tStorm\nHive\tFlink\tHadoop\nHBase\tHive\nHadoop\tSpark\tHBase\tStorm\n```\n\n### 2.4 WritingToKafkaApp\n\n```java\n/**\n * 写入数据到 Kafka 中\n */\npublic class WritingToKafkaApp {\n\n    private static final String BOOTSTRAP_SERVERS = \"hadoop001:9092\";\n    private static final String TOPIC_NAME = \"storm-topic\";\n\n    public static void main(String[] args) {\n\n\n        TopologyBuilder builder = new TopologyBuilder();\n\n        // 定义 Kafka 生产者属性\n        Properties props = new Properties();\n        /*\n         * 指定 broker 的地址清单，清单里不需要包含所有的 broker 地址，生产者会从给定的 broker 里查找其他 broker 的信息。\n         * 不过建议至少要提供两个 broker 的信息作为容错。\n         */\n        props.put(\"bootstrap.servers\", BOOTSTRAP_SERVERS);\n        /*\n         * acks 参数指定了必须要有多少个分区副本收到消息，生产者才会认为消息写入是成功的。\n         * acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。\n         * acks=1 : 只要集群的首领节点收到消息，生产者就会收到一个来自服务器成功响应。\n         * acks=all : 只有当所有参与复制的节点全部收到消息时，生产者才会收到一个来自服务器的成功响应。\n         */\n        props.put(\"acks\", \"1\");\n        props.put(\"key.serializer\", \"org.apache.kafka.common.serialization.StringSerializer\");\n        props.put(\"value.serializer\", \"org.apache.kafka.common.serialization.StringSerializer\");\n\n        KafkaBolt bolt = new KafkaBolt<String, String>()\n                .withProducerProperties(props)\n                .withTopicSelector(new DefaultTopicSelector(TOPIC_NAME))\n                .withTupleToKafkaMapper(new FieldNameBasedTupleToKafkaMapper<>());\n\n        builder.setSpout(\"sourceSpout\", new DataSourceSpout(), 1);\n        builder.setBolt(\"kafkaBolt\", bolt, 1).shuffleGrouping(\"sourceSpout\");\n\n\n        if (args.length > 0 && args[0].equals(\"cluster\")) {\n            try {\n                StormSubmitter.submitTopology(\"ClusterWritingToKafkaApp\", new Config(), builder.createTopology());\n            } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {\n                e.printStackTrace();\n            }\n        } else {\n            LocalCluster cluster = new LocalCluster();\n            cluster.submitTopology(\"LocalWritingToKafkaApp\",\n                    new Config(), builder.createTopology());\n        }\n    }\n}\n```\n\n### 2.5 测试准备工作\n\n进行测试前需要启动 Kakfa：\n\n#### 1. 启动Kakfa\n\nKafka 的运行依赖于 zookeeper，需要预先启动，可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的：\n\n```shell\n# zookeeper启动命令\nbin/zkServer.sh start\n\n# 内置zookeeper启动命令\nbin/zookeeper-server-start.sh config/zookeeper.properties\n```\n\n启动单节点 kafka 用于测试：\n\n```shell\n# bin/kafka-server-start.sh config/server.properties\n```\n\n#### 2. 创建topic\n\n```shell\n# 创建用于测试主题\nbin/kafka-topics.sh --create --bootstrap-server hadoop001:9092 --replication-factor 1 --partitions 1 --topic storm-topic\n\n# 查看所有主题\n bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092\n```\n\n#### 3. 启动消费者\n\n 启动一个消费者用于观察写入情况，启动命令如下：\n\n```shell\n# bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic storm-topic --from-beginning\n```\n\n### 2.6 测试\n\n可以用直接使用本地模式运行，也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包，打包命令如下：\n\n```shell\n# mvn clean package -D maven.test.skip=true\n```\n\n启动后，消费者监听情况如下：\n\n<div align=\"center\"> <img  src=\"../pictures/strom-kafka-consumer.png\"/> </div>\n\n\n\n## 三、从Kafka中读取数据\n\n### 3.1 项目结构\n\n<div align=\"center\"> <img  src=\"../pictures/readfromkafka.png\"/> </div>\n\n### 3.2  ReadingFromKafkaApp\n\n```java\n/**\n * 从 Kafka 中读取数据\n */\npublic class ReadingFromKafkaApp {\n\n    private static final String BOOTSTRAP_SERVERS = \"hadoop001:9092\";\n    private static final String TOPIC_NAME = \"storm-topic\";\n\n    public static void main(String[] args) {\n\n        final TopologyBuilder builder = new TopologyBuilder();\n        builder.setSpout(\"kafka_spout\", new KafkaSpout<>(getKafkaSpoutConfig(BOOTSTRAP_SERVERS, TOPIC_NAME)), 1);\n        builder.setBolt(\"bolt\", new LogConsoleBolt()).shuffleGrouping(\"kafka_spout\");\n\n        // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动\n        if (args.length > 0 && args[0].equals(\"cluster\")) {\n            try {\n                StormSubmitter.submitTopology(\"ClusterReadingFromKafkaApp\", new Config(), builder.createTopology());\n            } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {\n                e.printStackTrace();\n            }\n        } else {\n            LocalCluster cluster = new LocalCluster();\n            cluster.submitTopology(\"LocalReadingFromKafkaApp\",\n                    new Config(), builder.createTopology());\n        }\n    }\n\n    private static KafkaSpoutConfig<String, String> getKafkaSpoutConfig(String bootstrapServers, String topic) {\n        return KafkaSpoutConfig.builder(bootstrapServers, topic)\n                // 除了分组 ID,以下配置都是可选的。分组 ID 必须指定,否则会抛出 InvalidGroupIdException 异常\n                .setProp(ConsumerConfig.GROUP_ID_CONFIG, \"kafkaSpoutTestGroup\")\n                // 定义重试策略\n                .setRetry(getRetryService())\n                // 定时提交偏移量的时间间隔,默认是 15s\n                .setOffsetCommitPeriodMs(10_000)\n                .build();\n    }\n\n    // 定义重试策略\n    private static KafkaSpoutRetryService getRetryService() {\n        return new KafkaSpoutRetryExponentialBackoff(TimeInterval.microSeconds(500),\n                TimeInterval.milliSeconds(2), Integer.MAX_VALUE, TimeInterval.seconds(10));\n    }\n}\n\n```\n\n### 3.3 LogConsoleBolt\n\n```java\n/**\n * 打印从 Kafka 中获取的数据\n */\npublic class LogConsoleBolt extends BaseRichBolt {\n\n\n    private OutputCollector collector;\n\n    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {\n        this.collector=collector;\n    }\n\n    public void execute(Tuple input) {\n        try {\n            String value = input.getStringByField(\"value\");\n            System.out.println(\"received from kafka : \"+ value);\n            // 必须 ack,否则会重复消费 kafka 中的消息\n            collector.ack(input);\n        }catch (Exception e){\n            e.printStackTrace();\n            collector.fail(input);\n        }\n\n    }\n\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n\n    }\n}\n```\n\n这里从 `value` 字段中获取 kafka 输出的值数据。\n\n在开发中，我们可以通过继承 `RecordTranslator` 接口定义了 Kafka 中 Record 与输出流之间的映射关系，可以在构建 `KafkaSpoutConfig` 的时候通过构造器或者 `setRecordTranslator()` 方法传入，并最后传递给具体的 `KafkaSpout`。\n\n默认情况下使用内置的 `DefaultRecordTranslator`，其源码如下，`FIELDS` 中 定义了 tuple 中所有可用的字段：主题，分区，偏移量，消息键，值。\n\n```java\npublic class DefaultRecordTranslator<K, V> implements RecordTranslator<K, V> {\n    private static final long serialVersionUID = -5782462870112305750L;\n    public static final Fields FIELDS = new Fields(\"topic\", \"partition\", \"offset\", \"key\", \"value\");\n    @Override\n    public List<Object> apply(ConsumerRecord<K, V> record) {\n        return new Values(record.topic(),\n                record.partition(),\n                record.offset(),\n                record.key(),\n                record.value());\n    }\n\n    @Override\n    public Fields getFieldsFor(String stream) {\n        return FIELDS;\n    }\n\n    @Override\n    public List<String> streams() {\n        return DEFAULT_STREAM;\n    }\n}\n```\n\n### 3.4 启动测试\n\n这里启动一个生产者用于发送测试数据，启动命令如下：\n\n```shell\n# bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic storm-topic\n```\n\n<div align=\"center\"> <img  src=\"../pictures/storm-kafka-producer.png\"/> </div>\n\n本地运行的项目接收到从 Kafka 发送过来的数据：\n\n<div align=\"center\"> <img  src=\"../pictures/storm-kafka-receiver.png\"/> </div>\n\n\n\n<br>\n\n> 用例源码下载地址：[storm-kafka-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-kafka-integration)\n\n\n\n## 参考资料\n\n1. [Storm Kafka Integration (0.10.x+)](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-kafka-client.html)\n"
  },
  {
    "path": "大数据框架学习/Storm集成Redis详解.md",
    "content": "# Storm 集成 Redis 详解\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二集成案例\">二、集成案例</a><br/>\n<a href=\"#三storm-redis-实现原理\">三、storm-redis 实现原理</a><br/>\n<a href=\"#四自定义RedisBolt实现词频统计\">四、自定义RedisBolt实现词频统计</a><br/>\n</nav>\n\n\n## 一、简介\n\nStorm-Redis 提供了 Storm 与 Redis 的集成支持，你只需要引入对应的依赖即可使用：\n\n```xml\n<dependency>\n    <groupId>org.apache.storm</groupId>\n    <artifactId>storm-redis</artifactId>\n    <version>${storm.version}</version>\n    <type>jar</type>\n</dependency> \n```\n\nStorm-Redis 使用 Jedis 为 Redis 客户端，并提供了如下三个基本的 Bolt 实现：\n\n+ **RedisLookupBolt**：从 Redis 中查询数据；\n+ **RedisStoreBolt**：存储数据到 Redis；\n+ **RedisFilterBolt** : 查询符合条件的数据；\n\n`RedisLookupBolt`、`RedisStoreBolt`、`RedisFilterBolt ` 均继承自 `AbstractRedisBolt` 抽象类。我们可以通过继承该抽象类，实现自定义 RedisBolt，进行功能的拓展。\n\n\n\n## 二、集成案例\n\n### 2.1 项目结构\n\n这里首先给出一个集成案例：进行词频统计并将最后的结果存储到 Redis。项目结构如下：\n\n<div align=\"center\"> <img  src=\"../pictures/storm-wordcounttoredis.png\"/> </div>\n\n> 用例源码下载地址：[storm-redis-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-redis-integration)\n\n### 2.2 项目依赖\n\n项目主要依赖如下：\n\n```xml\n<properties>\n    <storm.version>1.2.2</storm.version>\n</properties>\n\n<dependencies>\n    <dependency>\n        <groupId>org.apache.storm</groupId>\n        <artifactId>storm-core</artifactId>\n        <version>${storm.version}</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.storm</groupId>\n        <artifactId>storm-redis</artifactId>\n        <version>${storm.version}</version>\n    </dependency>\n</dependencies>\n```\n\n### 2.3 DataSourceSpout\n\n```java\n/**\n * 产生词频样本的数据源\n */\npublic class DataSourceSpout extends BaseRichSpout {\n\n    private List<String> list = Arrays.asList(\"Spark\", \"Hadoop\", \"HBase\", \"Storm\", \"Flink\", \"Hive\");\n\n    private SpoutOutputCollector spoutOutputCollector;\n\n    @Override\n    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {\n        this.spoutOutputCollector = spoutOutputCollector;\n    }\n\n    @Override\n    public void nextTuple() {\n        // 模拟产生数据\n        String lineData = productData();\n        spoutOutputCollector.emit(new Values(lineData));\n        Utils.sleep(1000);\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {\n        outputFieldsDeclarer.declare(new Fields(\"line\"));\n    }\n\n\n    /**\n     * 模拟数据\n     */\n    private String productData() {\n        Collections.shuffle(list);\n        Random random = new Random();\n        int endIndex = random.nextInt(list.size()) % (list.size()) + 1;\n        return StringUtils.join(list.toArray(), \"\\t\", 0, endIndex);\n    }\n\n}\n```\n\n产生的模拟数据格式如下：\n\n```properties\nSpark\tHBase\nHive\tFlink\tStorm\tHadoop\tHBase\tSpark\nFlink\nHBase\tStorm\nHBase\tHadoop\tHive\tFlink\nHBase\tFlink\tHive\tStorm\nHive\tFlink\tHadoop\nHBase\tHive\nHadoop\tSpark\tHBase\tStorm\n```\n\n### 2.4 SplitBolt\n\n```java\n/**\n * 将每行数据按照指定分隔符进行拆分\n */\npublic class SplitBolt extends BaseRichBolt {\n\n    private OutputCollector collector;\n\n    @Override\n    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {\n        this.collector = collector;\n    }\n\n    @Override\n    public void execute(Tuple input) {\n        String line = input.getStringByField(\"line\");\n        String[] words = line.split(\"\\t\");\n        for (String word : words) {\n            collector.emit(new Values(word, String.valueOf(1)));\n        }\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n        declarer.declare(new Fields(\"word\", \"count\"));\n    }\n}\n```\n\n### 2.5 CountBolt\n\n```java\n/**\n * 进行词频统计\n */\npublic class CountBolt extends BaseRichBolt {\n\n    private Map<String, Integer> counts = new HashMap<>();\n\n    private OutputCollector collector;\n\n\n    @Override\n    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {\n            this.collector=collector;\n    }\n\n    @Override\n    public void execute(Tuple input) {\n        String word = input.getStringByField(\"word\");\n        Integer count = counts.get(word);\n        if (count == null) {\n            count = 0;\n        }\n        count++;\n        counts.put(word, count);\n        // 输出\n        collector.emit(new Values(word, String.valueOf(count)));\n\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n        declarer.declare(new Fields(\"word\", \"count\"));\n    }\n}\n```\n\n### 2.6 WordCountStoreMapper\n\n实现 RedisStoreMapper 接口，定义 tuple 与 Redis 中数据的映射关系：即需要指定 tuple 中的哪个字段为 key，哪个字段为 value，并且存储到 Redis 的何种数据结构中。\n\n```java\n/**\n * 定义 tuple 与 Redis 中数据的映射关系\n */\npublic class  WordCountStoreMapper implements RedisStoreMapper {\n    private RedisDataTypeDescription description;\n    private final String hashKey = \"wordCount\";\n\n    public WordCountStoreMapper() {\n        description = new RedisDataTypeDescription(\n                RedisDataTypeDescription.RedisDataType.HASH, hashKey);\n    }\n\n    @Override\n    public RedisDataTypeDescription getDataTypeDescription() {\n        return description;\n    }\n\n    @Override\n    public String getKeyFromTuple(ITuple tuple) {\n        return tuple.getStringByField(\"word\");\n    }\n\n    @Override\n    public String getValueFromTuple(ITuple tuple) {\n        return tuple.getStringByField(\"count\");\n    }\n}\n```\n\n### 2.7 WordCountToRedisApp\n\n```java\n/**\n * 进行词频统计 并将统计结果存储到 Redis 中\n */\npublic class WordCountToRedisApp {\n\n    private static final String DATA_SOURCE_SPOUT = \"dataSourceSpout\";\n    private static final String SPLIT_BOLT = \"splitBolt\";\n    private static final String COUNT_BOLT = \"countBolt\";\n    private static final String STORE_BOLT = \"storeBolt\";\n\n    //在实际开发中这些参数可以将通过外部传入 使得程序更加灵活\n    private static final String REDIS_HOST = \"192.168.200.226\";\n    private static final int REDIS_PORT = 6379;\n\n    public static void main(String[] args) {\n        TopologyBuilder builder = new TopologyBuilder();\n        builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout());\n        // split\n        builder.setBolt(SPLIT_BOLT, new SplitBolt()).shuffleGrouping(DATA_SOURCE_SPOUT);\n        // count\n        builder.setBolt(COUNT_BOLT, new CountBolt()).shuffleGrouping(SPLIT_BOLT);\n        // save to redis\n        JedisPoolConfig poolConfig = new JedisPoolConfig.Builder()\n                .setHost(REDIS_HOST).setPort(REDIS_PORT).build();\n        RedisStoreMapper storeMapper = new WordCountStoreMapper();\n        RedisStoreBolt storeBolt = new RedisStoreBolt(poolConfig, storeMapper);\n        builder.setBolt(STORE_BOLT, storeBolt).shuffleGrouping(COUNT_BOLT);\n\n        // 如果外部传参 cluster 则代表线上环境启动否则代表本地启动\n        if (args.length > 0 && args[0].equals(\"cluster\")) {\n            try {\n                StormSubmitter.submitTopology(\"ClusterWordCountToRedisApp\", new Config(), builder.createTopology());\n            } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {\n                e.printStackTrace();\n            }\n        } else {\n            LocalCluster cluster = new LocalCluster();\n            cluster.submitTopology(\"LocalWordCountToRedisApp\",\n                    new Config(), builder.createTopology());\n        }\n    }\n}\n```\n\n### 2.8 启动测试\n\n可以用直接使用本地模式运行，也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包，打包命令如下：\n\n```shell\n# mvn clean package -D maven.test.skip=true\n```\n\n启动后，查看 Redis 中的数据：\n\n<div align=\"center\"> <img  src=\"../pictures/store-redis-manager.png\"/> </div>\n\n\n\n## 三、storm-redis 实现原理\n\n### 3.1 AbstractRedisBolt\n\n`RedisLookupBolt`、`RedisStoreBolt`、`RedisFilterBolt ` 均继承自 `AbstractRedisBolt` 抽象类，和我们自定义实现 Bolt 一样，`AbstractRedisBolt` 间接继承自 `BaseRichBolt`。\n\n\n\n<div align=\"center\"> <img  src=\"../pictures/storm-abstractRedisBolt.png\"/> </div>\n\n`AbstractRedisBolt` 中比较重要的是 prepare 方法，在该方法中通过外部传入的 jedis 连接池配置 ( jedisPoolConfig/jedisClusterConfig) 创建用于管理 Jedis 实例的容器 `JedisCommandsInstanceContainer`。\n\n```java\npublic abstract class AbstractRedisBolt extends BaseTickTupleAwareRichBolt {\n    protected OutputCollector collector;\n\n    private transient JedisCommandsInstanceContainer container;\n\n    private JedisPoolConfig jedisPoolConfig;\n    private JedisClusterConfig jedisClusterConfig;\n\n   ......\n   \n    @Override\n    public void prepare(Map map, TopologyContext topologyContext, OutputCollector collector) {\n        // FIXME: stores map (stormConf), topologyContext and expose these to derived classes\n        this.collector = collector;\n\n        if (jedisPoolConfig != null) {\n            this.container = JedisCommandsContainerBuilder.build(jedisPoolConfig);\n        } else if (jedisClusterConfig != null) {\n            this.container = JedisCommandsContainerBuilder.build(jedisClusterConfig);\n        } else {\n            throw new IllegalArgumentException(\"Jedis configuration not found\");\n        }\n    }\n\n  .......\n}\n```\n\n`JedisCommandsInstanceContainer` 的 `build()` 方法如下，实际上就是创建 JedisPool 或 JedisCluster 并传入容器中。\n\n```java\npublic static JedisCommandsInstanceContainer build(JedisPoolConfig config) {\n        JedisPool jedisPool = new JedisPool(DEFAULT_POOL_CONFIG, config.getHost(), config.getPort(), config.getTimeout(), config.getPassword(), config.getDatabase());\n        return new JedisContainer(jedisPool);\n    }\n\n public static JedisCommandsInstanceContainer build(JedisClusterConfig config) {\n        JedisCluster jedisCluster = new JedisCluster(config.getNodes(), config.getTimeout(), config.getTimeout(), config.getMaxRedirections(), config.getPassword(), DEFAULT_POOL_CONFIG);\n        return new JedisClusterContainer(jedisCluster);\n    }\n```\n\n### 3.2 RedisStoreBolt和RedisLookupBolt\n\n`RedisStoreBolt` 中比较重要的是 process 方法，该方法主要从 storeMapper 中获取传入 key/value 的值，并按照其存储类型 `dataType` 调用 jedisCommand 的对应方法进行存储。\n\nRedisLookupBolt 的实现基本类似，从 lookupMapper 中获取传入的 key 值，并进行查询操作。\n\n```java\npublic class RedisStoreBolt extends AbstractRedisBolt {\n    private final RedisStoreMapper storeMapper;\n    private final RedisDataTypeDescription.RedisDataType dataType;\n    private final String additionalKey;\n\n   public RedisStoreBolt(JedisPoolConfig config, RedisStoreMapper storeMapper) {\n        super(config);\n        this.storeMapper = storeMapper;\n\n        RedisDataTypeDescription dataTypeDescription = storeMapper.getDataTypeDescription();\n        this.dataType = dataTypeDescription.getDataType();\n        this.additionalKey = dataTypeDescription.getAdditionalKey();\n    }\n\n    public RedisStoreBolt(JedisClusterConfig config, RedisStoreMapper storeMapper) {\n        super(config);\n        this.storeMapper = storeMapper;\n\n        RedisDataTypeDescription dataTypeDescription = storeMapper.getDataTypeDescription();\n        this.dataType = dataTypeDescription.getDataType();\n        this.additionalKey = dataTypeDescription.getAdditionalKey();\n    }\n       \n  \n    @Override\n    public void process(Tuple input) {\n        String key = storeMapper.getKeyFromTuple(input);\n        String value = storeMapper.getValueFromTuple(input);\n\n        JedisCommands jedisCommand = null;\n        try {\n            jedisCommand = getInstance();\n\n            switch (dataType) {\n                case STRING:\n                    jedisCommand.set(key, value);\n                    break;\n\n                case LIST:\n                    jedisCommand.rpush(key, value);\n                    break;\n\n                case HASH:\n                    jedisCommand.hset(additionalKey, key, value);\n                    break;\n\n                case SET:\n                    jedisCommand.sadd(key, value);\n                    break;\n\n                case SORTED_SET:\n                    jedisCommand.zadd(additionalKey, Double.valueOf(value), key);\n                    break;\n\n                case HYPER_LOG_LOG:\n                    jedisCommand.pfadd(key, value);\n                    break;\n\n                case GEO:\n                    String[] array = value.split(\":\");\n                    if (array.length != 2) {\n                        throw new IllegalArgumentException(\"value structure should be longitude:latitude\");\n                    }\n\n                    double longitude = Double.valueOf(array[0]);\n                    double latitude = Double.valueOf(array[1]);\n                    jedisCommand.geoadd(additionalKey, longitude, latitude, key);\n                    break;\n\n                default:\n                    throw new IllegalArgumentException(\"Cannot process such data type: \" + dataType);\n            }\n\n            collector.ack(input);\n        } catch (Exception e) {\n            this.collector.reportError(e);\n            this.collector.fail(input);\n        } finally {\n            returnInstance(jedisCommand);\n        }\n    }\n\n     .........\n}\n\n```\n\n### 3.3 JedisCommands\n\nJedisCommands 接口中定义了所有的 Redis 客户端命令，它有以下三个实现类，分别是 Jedis、JedisCluster、ShardedJedis。Strom 中主要使用前两种实现类，具体调用哪一个实现类来执行命令，由传入的是 jedisPoolConfig 还是 jedisClusterConfig 来决定。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-jedicCommands.png\"/> </div>\n\n### 3.4 RedisMapper 和 TupleMapper\n\nRedisMapper 和 TupleMapper 定义了 tuple 和 Redis 中的数据如何进行映射转换。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-Redis-Mapper.png\"/> </div>\n\n#### 1. TupleMapper \n\nTupleMapper 主要定义了两个方法：\n\n+ getKeyFromTuple(ITuple tuple)： 从 tuple 中获取那个字段作为 Key；\n\n+ getValueFromTuple(ITuple tuple)：从 tuple 中获取那个字段作为 Value；\n\n#### 2. RedisMapper\n\n定义了获取数据类型的方法 `getDataTypeDescription()`,RedisDataTypeDescription 中 RedisDataType 枚举类定义了所有可用的 Redis 数据类型：\n\n```java\npublic class RedisDataTypeDescription implements Serializable { \n\n    public enum RedisDataType { STRING, HASH, LIST, SET, SORTED_SET, HYPER_LOG_LOG, GEO }\n     ......\n    }\n```\n\n#### 3. RedisStoreMapper\n\nRedisStoreMapper 继承 TupleMapper 和 RedisMapper 接口，用于数据存储时，没有定义额外方法。\n\n#### 4. RedisLookupMapper\n\nRedisLookupMapper 继承 TupleMapper 和 RedisMapper 接口：\n\n+ 定义了 declareOutputFields 方法，声明输出的字段。\n+ 定义了 toTuple 方法，将查询结果组装为 Storm 的 Values 的集合，并用于发送。\n\n下面的例子表示从输入 `Tuple` 的获取 `word` 字段作为 key，使用 `RedisLookupBolt` 进行查询后，将 key 和查询结果 value 组装为 values 并发送到下一个处理单元。\n\n```java\nclass WordCountRedisLookupMapper implements RedisLookupMapper {\n    private RedisDataTypeDescription description;\n    private final String hashKey = \"wordCount\";\n\n    public WordCountRedisLookupMapper() {\n        description = new RedisDataTypeDescription(\n                RedisDataTypeDescription.RedisDataType.HASH, hashKey);\n    }\n\n    @Override\n    public List<Values> toTuple(ITuple input, Object value) {\n        String member = getKeyFromTuple(input);\n        List<Values> values = Lists.newArrayList();\n        values.add(new Values(member, value));\n        return values;\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n        declarer.declare(new Fields(\"wordName\", \"count\"));\n    }\n\n    @Override\n    public RedisDataTypeDescription getDataTypeDescription() {\n        return description;\n    }\n\n    @Override\n    public String getKeyFromTuple(ITuple tuple) {\n        return tuple.getStringByField(\"word\");\n    }\n\n    @Override\n    public String getValueFromTuple(ITuple tuple) {\n        return null;\n    }\n} \n```\n\n#### 5. RedisFilterMapper\n\nRedisFilterMapper 继承 TupleMapper 和 RedisMapper 接口，用于查询数据时，定义了 declareOutputFields 方法，声明输出的字段。如下面的实现：\n\n```java\n@Override\npublic void declareOutputFields(OutputFieldsDeclarer declarer) {\n    declarer.declare(new Fields(\"wordName\", \"count\"));\n}\n\n```\n\n## 四、自定义RedisBolt实现词频统计\n\n### 4.1 实现原理\n\n自定义 RedisBolt：主要利用 Redis 中哈希结构的 `hincrby key field` 命令进行词频统计。在 Redis 中 `hincrby` 的执行效果如下。hincrby 可以将字段按照指定的值进行递增，如果该字段不存在的话，还会新建该字段，并赋值为 0。通过这个命令可以非常轻松的实现词频统计功能。\n\n```shell\nredis>  HSET myhash field 5\n(integer) 1\nredis>  HINCRBY myhash field 1\n(integer) 6\nredis>  HINCRBY myhash field -1\n(integer) 5\nredis>  HINCRBY myhash field -10\n(integer) -5\nredis> \n```\n\n### 4.2 项目结构\n\n<div align=\"center\"> <img  src=\"../pictures/CustomRedisCountApp.png\"/> </div>\n\n### 4.3 自定义RedisBolt的代码实现\n\n```java\n/**\n * 自定义 RedisBolt 利用 Redis 的哈希数据结构的 hincrby key field 命令进行词频统计\n */\npublic class RedisCountStoreBolt extends AbstractRedisBolt {\n\n    private final RedisStoreMapper storeMapper;\n    private final RedisDataTypeDescription.RedisDataType dataType;\n    private final String additionalKey;\n\n    public RedisCountStoreBolt(JedisPoolConfig config, RedisStoreMapper storeMapper) {\n        super(config);\n        this.storeMapper = storeMapper;\n        RedisDataTypeDescription dataTypeDescription = storeMapper.getDataTypeDescription();\n        this.dataType = dataTypeDescription.getDataType();\n        this.additionalKey = dataTypeDescription.getAdditionalKey();\n    }\n\n    @Override\n    protected void process(Tuple tuple) {\n        String key = storeMapper.getKeyFromTuple(tuple);\n        String value = storeMapper.getValueFromTuple(tuple);\n\n        JedisCommands jedisCommand = null;\n        try {\n            jedisCommand = getInstance();\n            if (dataType == RedisDataTypeDescription.RedisDataType.HASH) {\n                jedisCommand.hincrBy(additionalKey, key, Long.valueOf(value));\n            } else {\n                throw new IllegalArgumentException(\"Cannot process such data type for Count: \" + dataType);\n            }\n\n            collector.ack(tuple);\n        } catch (Exception e) {\n            this.collector.reportError(e);\n            this.collector.fail(tuple);\n        } finally {\n            returnInstance(jedisCommand);\n        }\n    }\n\n    @Override\n    public void declareOutputFields(OutputFieldsDeclarer declarer) {\n\n    }\n}\n```\n\n### 4.4 CustomRedisCountApp\n\n```java\n/**\n * 利用自定义的 RedisBolt 实现词频统计\n */\npublic class CustomRedisCountApp {\n\n    private static final String DATA_SOURCE_SPOUT = \"dataSourceSpout\";\n    private static final String SPLIT_BOLT = \"splitBolt\";\n    private static final String STORE_BOLT = \"storeBolt\";\n\n    private static final String REDIS_HOST = \"192.168.200.226\";\n    private static final int REDIS_PORT = 6379;\n\n    public static void main(String[] args) {\n        TopologyBuilder builder = new TopologyBuilder();\n        builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout());\n        // split\n        builder.setBolt(SPLIT_BOLT, new SplitBolt()).shuffleGrouping(DATA_SOURCE_SPOUT);\n        // save to redis and count\n        JedisPoolConfig poolConfig = new JedisPoolConfig.Builder()\n                .setHost(REDIS_HOST).setPort(REDIS_PORT).build();\n        RedisStoreMapper storeMapper = new WordCountStoreMapper();\n        RedisCountStoreBolt countStoreBolt = new RedisCountStoreBolt(poolConfig, storeMapper);\n        builder.setBolt(STORE_BOLT, countStoreBolt).shuffleGrouping(SPLIT_BOLT);\n\n        // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动\n        if (args.length > 0 && args[0].equals(\"cluster\")) {\n            try {\n                StormSubmitter.submitTopology(\"ClusterCustomRedisCountApp\", new Config(), builder.createTopology());\n            } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {\n                e.printStackTrace();\n            }\n        } else {\n            LocalCluster cluster = new LocalCluster();\n            cluster.submitTopology(\"LocalCustomRedisCountApp\",\n                    new Config(), builder.createTopology());\n        }\n    }\n}\n```\n\n\n\n## 参考资料\n\n1. [Storm Redis Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-redis.html)\n"
  },
  {
    "path": "大数据框架学习/Zookeeper_ACL权限控制.md",
    "content": "# Zookeeper ACL\n\n<nav>\n<a href=\"#一前言\">一、前言</a><br/>\n<a href=\"#二使用Shell进行权限管理\">二、使用Shell进行权限管理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-设置与查看权限\">2.1 设置与查看权限</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-权限组成\">2.2 权限组成</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-添加认证信息\">2.3 添加认证信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-权限设置示例\">2.4 权限设置示例</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-world模式\">1. world模式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-auth模式\">2. auth模式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3-digest模式\">3. digest模式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#4-ip模式\">4. ip模式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#5-super模式\">5. super模式</a><br/>\n<a href=\"#三使用Java客户端进行权限管理\">三、使用Java客户端进行权限管理</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-主要依赖\">3.1 主要依赖</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-权限管理API\">3.2 权限管理API</a><br/>\n</nav>\n\n\n## 一、前言\n\n为了避免存储在 Zookeeper 上的数据被其他程序或者人为误修改，Zookeeper 提供了 ACL(Access Control Lists) 进行权限控制。只有拥有对应权限的用户才可以对节点进行增删改查等操作。下文分别介绍使用原生的 Shell 命令和 Apache Curator 客户端进行权限设置。\n\n## 二、使用Shell进行权限管理\n\n### 2.1 设置与查看权限\n\n想要给某个节点设置权限 (ACL)，有以下两个可选的命令：\n\n```shell\n # 1.给已有节点赋予权限\n setAcl path acl\n \n # 2.在创建节点时候指定权限\n create [-s] [-e] path data acl\n```\n\n查看指定节点的权限命令如下：\n\n```shell\ngetAcl path\n```\n\n### 2.2 权限组成\n\nZookeeper 的权限由[scheme : id :permissions]三部分组成，其中 Schemes 和 Permissions 内置的可选项分别如下：\n\n**Permissions 可选项**：\n\n- **CREATE**：允许创建子节点；\n- **READ**：允许从节点获取数据并列出其子节点；\n- **WRITE**：允许为节点设置数据；\n- **DELETE**：允许删除子节点；\n- **ADMIN**：允许为节点设置权限。  \n\n**Schemes 可选项**：\n\n- **world**：默认模式，所有客户端都拥有指定的权限。world 下只有一个 id 选项，就是 anyone，通常组合写法为 `world:anyone:[permissons]`；\n- **auth**：只有经过认证的用户才拥有指定的权限。通常组合写法为 `auth:user:password:[permissons]`，使用这种模式时，你需要先进行登录，之后采用 auth 模式设置权限时，`user` 和 `password` 都将使用登录的用户名和密码；\n- **digest**：只有经过认证的用户才拥有指定的权限。通常组合写法为 `auth:user:BASE64(SHA1(password)):[permissons]`，这种形式下的密码必须通过 SHA1 和 BASE64 进行双重加密；\n- **ip**：限制只有特定 IP 的客户端才拥有指定的权限。通常组成写法为 `ip:182.168.0.168:[permissions]`；\n- **super**：代表超级管理员，拥有所有的权限，需要修改 Zookeeper 启动脚本进行配置。\n\n\n\n### 2.3 添加认证信息\n\n可以使用如下所示的命令为当前 Session 添加用户认证信息，等价于登录操作。\n\n```shell\n# 格式\naddauth scheme auth \n\n#示例：添加用户名为heibai,密码为root的用户认证信息\naddauth digest heibai:root \n```\n\n\n\n### 2.4 权限设置示例\n\n#### 1. world模式\n\nworld 是一种默认的模式，即创建时如果不指定权限，则默认的权限就是 world。\n\n```shell\n[zk: localhost:2181(CONNECTED) 32] create /hadoop 123\nCreated /hadoop\n[zk: localhost:2181(CONNECTED) 33] getAcl /hadoop\n'world,'anyone    #默认的权限\n: cdrwa\n[zk: localhost:2181(CONNECTED) 34] setAcl /hadoop world:anyone:cwda   # 修改节点，不允许所有客户端读\n....\n[zk: localhost:2181(CONNECTED) 35] get /hadoop\nAuthentication is not valid : /hadoop     # 权限不足\n\n```\n\n#### 2. auth模式\n\n```shell\n[zk: localhost:2181(CONNECTED) 36] addauth digest heibai:heibai  # 登录\n[zk: localhost:2181(CONNECTED) 37] setAcl /hadoop auth::cdrwa    # 设置权限\n[zk: localhost:2181(CONNECTED) 38] getAcl /hadoop                # 获取权限\n'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=   #用户名和密码 (密码经过加密处理)，注意返回的权限类型是 digest\n: cdrwa\n\n#用户名和密码都是使用登录的用户名和密码，即使你在创建权限时候进行指定也是无效的\n[zk: localhost:2181(CONNECTED) 39] setAcl /hadoop auth:root:root:cdrwa    #指定用户名和密码为 root\n[zk: localhost:2181(CONNECTED) 40] getAcl /hadoop\n'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=  #无效，使用的用户名和密码依然还是 heibai\n: cdrwa\n\n```\n\n#### 3. digest模式\n\n```shell\n[zk:44] create /spark \"spark\" digest:heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=:cdrwa  #指定用户名和加密后的密码\n[zk:45] getAcl /spark  #获取权限\n'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=   # 返回的权限类型是 digest\n: cdrwa\n```\n\n到这里你可以发现使用 `auth` 模式设置的权限和使用 `digest` 模式设置的权限，在最终结果上，得到的权限模式都是 `digest`。某种程度上，你可以把 `auth` 模式理解成是 `digest` 模式的一种简便实现。因为在 `digest` 模式下，每次设置都需要书写用户名和加密后的密码，这是比较繁琐的，采用 `auth` 模式就可以避免这种麻烦。\n\n#### 4. ip模式\n\n限定只有特定的 ip 才能访问。\n\n```shell\n[zk: localhost:2181(CONNECTED) 46] create  /hive \"hive\" ip:192.168.0.108:cdrwa  \n[zk: localhost:2181(CONNECTED) 47] get /hive\nAuthentication is not valid : /hive  # 当前主机已经不能访问\n```\n\n这里可以看到当前主机已经不能访问，想要能够再次访问，可以使用对应 IP 的客户端，或使用下面介绍的 `super` 模式。\n\n#### 5. super模式\n\n需要修改启动脚本 `zkServer.sh`，并在指定位置添加超级管理员账户和密码信息：\n\n```shell\n\"-Dzookeeper.DigestAuthenticationProvider.superDigest=heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=\" \n```\n\n<div align=\"center\"> <img  src=\"../pictures/zookeeper-super.png\"/> </div>\n\n修改完成后需要使用 `zkServer.sh restart` 重启服务，此时再次访问限制 IP 的节点：\n\n```shell\n[zk: localhost:2181(CONNECTED) 0] get /hive  #访问受限\nAuthentication is not valid : /hive\n[zk: localhost:2181(CONNECTED) 1] addauth digest heibai:heibai  # 登录 (添加认证信息)\n[zk: localhost:2181(CONNECTED) 2] get /hive  #成功访问\nhive\ncZxid = 0x158\nctime = Sat May 25 09:11:29 CST 2019\nmZxid = 0x158\nmtime = Sat May 25 09:11:29 CST 2019\npZxid = 0x158\ncversion = 0\ndataVersion = 0\naclVersion = 0\nephemeralOwner = 0x0\ndataLength = 4\nnumChildren = 0\n```\n\n## 三、使用Java客户端进行权限管理\n\n### 3.1 主要依赖\n\n这里以 Apache Curator 为例，使用前需要导入相关依赖，完整依赖如下：\n\n```xml\n<dependencies>\n    <!--Apache Curator 相关依赖-->\n    <dependency>\n        <groupId>org.apache.curator</groupId>\n        <artifactId>curator-framework</artifactId>\n        <version>4.0.0</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.curator</groupId>\n        <artifactId>curator-recipes</artifactId>\n        <version>4.0.0</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.zookeeper</groupId>\n        <artifactId>zookeeper</artifactId>\n        <version>3.4.13</version>\n    </dependency>\n    <!--单元测试相关依赖-->\n    <dependency>\n        <groupId>junit</groupId>\n        <artifactId>junit</artifactId>\n        <version>4.12</version>\n    </dependency>\n</dependencies>\n```\n\n### 3.2 权限管理API\n\n Apache Curator 权限设置的示例如下：\n\n```java\npublic class AclOperation {\n\n    private CuratorFramework client = null;\n    private static final String zkServerPath = \"192.168.0.226:2181\";\n    private static final String nodePath = \"/hadoop/hdfs\";\n\n    @Before\n    public void prepare() {\n        RetryPolicy retryPolicy = new RetryNTimes(3, 5000);\n        client = CuratorFrameworkFactory.builder()\n                .authorization(\"digest\", \"heibai:123456\".getBytes()) //等价于 addauth 命令\n                .connectString(zkServerPath)\n                .sessionTimeoutMs(10000).retryPolicy(retryPolicy)\n                .namespace(\"workspace\").build();\n        client.start();\n    }\n\n    /**\n     * 新建节点并赋予权限\n     */\n    @Test\n    public void createNodesWithAcl() throws Exception {\n        List<ACL> aclList = new ArrayList<>();\n        // 对密码进行加密\n        String digest1 = DigestAuthenticationProvider.generateDigest(\"heibai:123456\");\n        String digest2 = DigestAuthenticationProvider.generateDigest(\"ying:123456\");\n        Id user01 = new Id(\"digest\", digest1);\n        Id user02 = new Id(\"digest\", digest2);\n        // 指定所有权限\n        aclList.add(new ACL(Perms.ALL, user01));\n        // 如果想要指定权限的组合，中间需要使用 | ,这里的|代表的是位运算中的 按位或\n        aclList.add(new ACL(Perms.DELETE | Perms.CREATE, user02));\n\n        // 创建节点\n        byte[] data = \"abc\".getBytes();\n        client.create().creatingParentsIfNeeded()\n                .withMode(CreateMode.PERSISTENT)\n                .withACL(aclList, true)\n                .forPath(nodePath, data);\n    }\n\n\n    /**\n     * 给已有节点设置权限,注意这会删除所有原来节点上已有的权限设置\n     */\n    @Test\n    public void SetAcl() throws Exception {\n        String digest = DigestAuthenticationProvider.generateDigest(\"admin:admin\");\n        Id user = new Id(\"digest\", digest);\n        client.setACL()\n                .withACL(Collections.singletonList(new ACL(Perms.READ | Perms.DELETE, user)))\n                .forPath(nodePath);\n    }\n\n    /**\n     * 获取权限\n     */\n    @Test\n    public void getAcl() throws Exception {\n        List<ACL> aclList = client.getACL().forPath(nodePath);\n        ACL acl = aclList.get(0);\n        System.out.println(acl.getId().getId() \n                           + \"是否有删读权限:\" + (acl.getPerms() == (Perms.READ | Perms.DELETE)));\n    }\n\n    @After\n    public void destroy() {\n        if (client != null) {\n            client.close();\n        }\n    }\n}\n```\n\n> 完整源码见本仓库： https://github.com/heibaiying/BigData-Notes/tree/master/code/Zookeeper/curator\n"
  },
  {
    "path": "大数据框架学习/Zookeeper_Java客户端Curator.md",
    "content": "# Zookeeper Java 客户端 ——Apache Curator\n\n<nav>\n<a href=\"#一基本依赖\">一、基本依赖</a><br/>\n<a href=\"#二客户端相关操作\">二、客户端相关操作</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-创建客户端实例\">2.1 创建客户端实例</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-重试策略\">2.2 重试策略</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-判断服务状态\">2.3 判断服务状态</a><br/>\n<a href=\"#三节点增删改查\">三、节点增删改查</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-创建节点\">3.1 创建节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-获取节点信息\">2.2 获取节点信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-获取子节点列表\">2.3 获取子节点列表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-更新节点\">2.4 更新节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-删除节点\">2.5 删除节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#26-判断节点是否存在\">2.6 判断节点是否存在</a><br/>\n<a href=\"#三监听事件\">三、监听事件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-创建一次性监听\">3.1 创建一次性监听</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-创建永久监听\">3.2 创建永久监听</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-监听子节点\">3.3 监听子节点</a><br/>\n</nav>\n\n## 一、基本依赖\n\nCurator 是 Netflix 公司开源的一个 Zookeeper 客户端，目前由 Apache 进行维护。与 Zookeeper 原生客户端相比，Curator 的抽象层次更高，功能也更加丰富，是目前 Zookeeper 使用范围最广的 Java 客户端。本篇文章主要讲解其基本使用，项目采用 Maven 构建，以单元测试的方法进行讲解，相关依赖如下：\n\n```xml\n<dependencies>\n    <!--Curator 相关依赖-->\n    <dependency>\n        <groupId>org.apache.curator</groupId>\n        <artifactId>curator-framework</artifactId>\n        <version>4.0.0</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.curator</groupId>\n        <artifactId>curator-recipes</artifactId>\n        <version>4.0.0</version>\n    </dependency>\n    <dependency>\n        <groupId>org.apache.zookeeper</groupId>\n        <artifactId>zookeeper</artifactId>\n        <version>3.4.13</version>\n    </dependency>\n    <!--单元测试相关依赖-->\n    <dependency>\n        <groupId>junit</groupId>\n        <artifactId>junit</artifactId>\n        <version>4.12</version>\n    </dependency>\n</dependencies>\n```\n\n> 完整源码见本仓库： https://github.com/heibaiying/BigData-Notes/tree/master/code/Zookeeper/curator\n\n\n\n## 二、客户端相关操作\n\n### 2.1 创建客户端实例\n\n这里使用 `@Before` 在单元测试执行前创建客户端实例，并使用 `@After` 在单元测试后关闭客户端连接。\n\n```java\npublic class BasicOperation {\n\n    private CuratorFramework client = null;\n    private static final String zkServerPath = \"192.168.0.226:2181\";\n    private static final String nodePath = \"/hadoop/yarn\";\n\n    @Before\n    public void prepare() {\n        // 重试策略\n        RetryPolicy retryPolicy = new RetryNTimes(3, 5000);\n        client = CuratorFrameworkFactory.builder()\n        .connectString(zkServerPath)\n        .sessionTimeoutMs(10000).retryPolicy(retryPolicy)\n        .namespace(\"workspace\").build();  //指定命名空间后，client 的所有路径操作都会以/workspace 开头\n        client.start();\n    }\n\n    @After\n    public void destroy() {\n        if (client != null) {\n            client.close();\n        }\n    }\n}\n```\n\n### 2.2 重试策略\n\n在连接 Zookeeper 时，Curator 提供了多种重试策略以满足各种需求，所有重试策略均继承自 `RetryPolicy` 接口，如下图：\n\n<div align=\"center\"> <img  src=\"../pictures/curator-retry-policy.png\"/> </div>\n\n这些重试策略类主要分为以下两类：\n\n+ **RetryForever** ：代表一直重试，直到连接成功；\n+ **SleepingRetry** ： 基于一定间隔时间的重试。这里以其子类 `ExponentialBackoffRetry` 为例说明，其构造器如下：\n\n```java\n/**\n * @param baseSleepTimeMs 重试之间等待的初始时间\n * @param maxRetries 最大重试次数\n * @param maxSleepMs 每次重试间隔的最长睡眠时间（毫秒）\n */\nExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs)    \n```\n### 2.3 判断服务状态\n\n```scala\n@Test\npublic void getStatus() {\n    CuratorFrameworkState state = client.getState();\n    System.out.println(\"服务是否已经启动:\" + (state == CuratorFrameworkState.STARTED));\n}\n```\n\n\n\n## 三、节点增删改查\n\n### 3.1 创建节点\n\n```java\n@Test\npublic void createNodes() throws Exception {\n    byte[] data = \"abc\".getBytes();\n    client.create().creatingParentsIfNeeded()\n            .withMode(CreateMode.PERSISTENT)      //节点类型\n            .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)\n            .forPath(nodePath, data);\n}\n```\n\n创建时可以指定节点类型，这里的节点类型和 Zookeeper 原生的一致，全部类型定义在枚举类 `CreateMode` 中：\n\n```java\npublic enum CreateMode {\n    // 永久节点\n    PERSISTENT (0, false, false),\n    //永久有序节点\n    PERSISTENT_SEQUENTIAL (2, false, true),\n    // 临时节点\n    EPHEMERAL (1, true, false),\n    // 临时有序节点\n    EPHEMERAL_SEQUENTIAL (3, true, true);\n    ....\n}\n```\n\n### 2.2 获取节点信息\n\n```scala\n@Test\npublic void getNode() throws Exception {\n    Stat stat = new Stat();\n    byte[] data = client.getData().storingStatIn(stat).forPath(nodePath);\n    System.out.println(\"节点数据:\" + new String(data));\n    System.out.println(\"节点信息:\" + stat.toString());\n}\n```\n\n如上所示，节点信息被封装在 `Stat` 类中，其主要属性如下：\n\n```java\npublic class Stat implements Record {\n    private long czxid;\n    private long mzxid;\n    private long ctime;\n    private long mtime;\n    private int version;\n    private int cversion;\n    private int aversion;\n    private long ephemeralOwner;\n    private int dataLength;\n    private int numChildren;\n    private long pzxid;\n    ...\n}\n```\n\n每个属性的含义如下：\n\n| **状态属性**   | **说明**                                                     |\n| -------------- | ------------------------------------------------------------ |\n| czxid          | 数据节点创建时的事务 ID                                       |\n| ctime          | 数据节点创建时的时间                                         |\n| mzxid          | 数据节点最后一次更新时的事务 ID                               |\n| mtime          | 数据节点最后一次更新时的时间                                 |\n| pzxid          | 数据节点的子节点最后一次被修改时的事务 ID                     |\n| cversion       | 子节点的更改次数                                             |\n| version        | 节点数据的更改次数                                           |\n| aversion       | 节点的 ACL 的更改次数                                          |\n| ephemeralOwner | 如果节点是临时节点，则表示创建该节点的会话的 SessionID；如果节点是持久节点，则该属性值为 0 |\n| dataLength     | 数据内容的长度                                               |\n| numChildren    | 数据节点当前的子节点个数                                     |\n\n### 2.3 获取子节点列表\n\n```java\n@Test\npublic void getChildrenNodes() throws Exception {\n    List<String> childNodes = client.getChildren().forPath(\"/hadoop\");\n    for (String s : childNodes) {\n        System.out.println(s);\n    }\n}\n```\n\n### 2.4 更新节点\n\n更新时可以传入版本号也可以不传入，如果传入则类似于乐观锁机制，只有在版本号正确的时候才会被更新。\n\n```scala\n@Test\npublic void updateNode() throws Exception {\n    byte[] newData = \"defg\".getBytes();\n    client.setData().withVersion(0)     // 传入版本号，如果版本号错误则拒绝更新操作,并抛出 BadVersion 异常\n            .forPath(nodePath, newData);\n}\n```\n\n### 2.5 删除节点\n\n```java\n@Test\npublic void deleteNodes() throws Exception {\n    client.delete()\n            .guaranteed()                // 如果删除失败，那么在会继续执行，直到成功\n            .deletingChildrenIfNeeded()  // 如果有子节点，则递归删除\n            .withVersion(0)              // 传入版本号，如果版本号错误则拒绝删除操作,并抛出 BadVersion 异常\n            .forPath(nodePath);\n}\n```\n\n### 2.6 判断节点是否存在\n\n```java\n@Test\npublic void existNode() throws Exception {\n    // 如果节点存在则返回其状态信息如果不存在则为 null\n    Stat stat = client.checkExists().forPath(nodePath + \"aa/bb/cc\");\n    System.out.println(\"节点是否存在:\" + !(stat == null));\n}\n```\n\n\n\n## 三、监听事件\n\n### 3.1 创建一次性监听\n\n和 Zookeeper 原生监听一样，使用 `usingWatcher` 注册的监听是一次性的，即监听只会触发一次，触发后就销毁。示例如下：\n\n```java\n@Test\npublic void DisposableWatch() throws Exception {\n    client.getData().usingWatcher(new CuratorWatcher() {\n        public void process(WatchedEvent event) {\n            System.out.println(\"节点\" + event.getPath() + \"发生了事件:\" + event.getType());\n        }\n    }).forPath(nodePath);\n    Thread.sleep(1000 * 1000);  //休眠以观察测试效果\n}\n```\n\n### 3.2 创建永久监听\n\nCurator 还提供了创建永久监听的 API，其使用方式如下：\n\n```java\n@Test\npublic void permanentWatch() throws Exception {\n    // 使用 NodeCache 包装节点，对其注册的监听作用于节点，且是永久性的\n    NodeCache nodeCache = new NodeCache(client, nodePath);\n    // 通常设置为 true, 代表创建 nodeCache 时,就去获取对应节点的值并缓存\n    nodeCache.start(true);\n    nodeCache.getListenable().addListener(new NodeCacheListener() {\n        public void nodeChanged() {\n            ChildData currentData = nodeCache.getCurrentData();\n            if (currentData != null) {\n                System.out.println(\"节点路径：\" + currentData.getPath() +\n                        \"数据：\" + new String(currentData.getData()));\n            }\n        }\n    });\n    Thread.sleep(1000 * 1000);  //休眠以观察测试效果\n}\n```\n\n### 3.3 监听子节点\n\n这里以监听 `/hadoop` 下所有子节点为例，实现方式如下：\n\n```scala\n@Test\npublic void permanentChildrenNodesWatch() throws Exception {\n\n    // 第三个参数代表除了节点状态外，是否还缓存节点内容\n    PathChildrenCache childrenCache = new PathChildrenCache(client, \"/hadoop\", true);\n    /*\n         * StartMode 代表初始化方式:\n         *    NORMAL: 异步初始化\n         *    BUILD_INITIAL_CACHE: 同步初始化\n         *    POST_INITIALIZED_EVENT: 异步并通知,初始化之后会触发 INITIALIZED 事件\n         */\n    childrenCache.start(StartMode.POST_INITIALIZED_EVENT);\n\n    List<ChildData> childDataList = childrenCache.getCurrentData();\n    System.out.println(\"当前数据节点的子节点列表：\");\n    childDataList.forEach(x -> System.out.println(x.getPath()));\n\n    childrenCache.getListenable().addListener(new PathChildrenCacheListener() {\n        public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) {\n            switch (event.getType()) {\n                case INITIALIZED:\n                System.out.println(\"childrenCache 初始化完成\");\n                break;\n                case CHILD_ADDED:\n                // 需要注意的是: 即使是之前已经存在的子节点，也会触发该监听，因为会把该子节点加入 childrenCache 缓存中\n                System.out.println(\"增加子节点:\" + event.getData().getPath());\n                break;\n                case CHILD_REMOVED:\n                System.out.println(\"删除子节点:\" + event.getData().getPath());\n                break;\n                case CHILD_UPDATED:\n                System.out.println(\"被修改的子节点的路径:\" + event.getData().getPath());\n                System.out.println(\"修改后的数据:\" + new String(event.getData().getData()));\n                break;\n            }\n        }\n    });\n    Thread.sleep(1000 * 1000); //休眠以观察测试效果\n}\n```\n"
  },
  {
    "path": "大数据框架学习/Zookeeper常用Shell命令.md",
    "content": "# Zookeeper常用Shell命令\n\n<nav>\n<a href=\"#一节点增删改查\">一、节点增删改查</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-启动服务和连接服务\">1.1 启动服务和连接服务</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-help命令\">1.2 help命令</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-查看节点列表\">1.3 查看节点列表</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-新增节点\">1.4 新增节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-查看节点\">1.5 查看节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-更新节点\">1.6 更新节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#17-删除节点\">1.7 删除节点</a><br/>\n<a href=\"#二监听器\">二、监听器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-get-path-[watch]\">2.1 get path [watch]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-stat-path-[watch]\">2.2 stat path [watch]</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-lsls2-path--[watch]\">2.3 ls\\ls2 path  [watch]</a><br/>\n<a href=\"#三-zookeeper-四字命令\">三、 zookeeper 四字命令</a><br/>\n</nav>\n\n\n## 一、节点增删改查\n\n### 1.1 启动服务和连接服务\n\n```shell\n# 启动服务\nbin/zkServer.sh start\n\n#连接服务 不指定服务地址则默认连接到localhost:2181\nzkCli.sh -server hadoop001:2181\n```\n\n### 1.2 help命令\n\n使用 `help` 可以查看所有命令及格式。\n\n```\nZooKeeper -server host:port -client-configuration properties-file cmd args\n        addWatch [-m mode] path # optional mode is one of [PERSISTENT, PERSISTENT_RECURSIVE] - default is PERSISTENT_RECURSIVE\n        addauth scheme auth\n        close\n        config [-c] [-w] [-s]\n        connect host:port\n        create [-s] [-e] [-c] [-t ttl] path [data] [acl]\n        delete [-v version] path\n        deleteall path [-b batch size]\n        delquota [-n|-b] path\n        get [-s] [-w] path\n        getAcl [-s] path\n        getAllChildrenNumber path\n        getEphemerals path\n        history\n        listquota path\n        ls [-s] [-w] [-R] path\n        printwatches on|off\n        quit\n        reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]\n        redo cmdno\n        removewatches path [-c|-d|-a] [-l]\n        set [-s] [-v version] path data\n        setAcl [-s] [-v version] [-R] path acl\n        setquota -n|-b val path\n        stat [-w] path\n        sync path\n        version\n```\n\n\n### 1.3 查看节点列表\n\n查看节点列表有 `ls path` 和 `ls -s path` 两个命令，后者是前者的增强，不仅可以查看指定路径下的所有节点，还可以查看当前节点的信息。\n\n```shell\n[zk: localhost:2181(CONNECTED) 0] ls /\n[zk: localhost:2181(CONNECTED) 1] ls -s /\n[a0000000001, b0000000002, c0000000003, hadoop, zookeeper]\ncZxid = 0x0\nctime = Thu Jan 01 08:00:00 CST 1970\nmZxid = 0x0\nmtime = Thu Jan 01 08:00:00 CST 1970\npZxid = 0x300000011\ncversion = 9\ndataVersion = 0\naclVersion = 0\nephemeralOwner = 0x0\ndataLength = 0\nnumChildren = 7\n```\n\n### 1.4 新增节点\n\n```shell\ncreate [-s] [-e] [-c] [-t ttl] path [data] [acl]   #其中-s 为有序节点，-e 临时节点\n```\n\n创建节点并写入数据：\n\n```shell\ncreate /hadoop 123456\n```\n\n创建有序节点，此时创建的节点名为指定节点名 + 自增序号：\n\n```shell\n[zk: localhost:2181(CONNECTED) 23] create -s /a  \"aaa\"\nCreated /a0000000022\n[zk: localhost:2181(CONNECTED) 24] create -s /b  \"bbb\"\nCreated /b0000000023\n[zk: localhost:2181(CONNECTED) 25] create -s /c  \"ccc\"\nCreated /c0000000024\n```\n\n创建临时节点，临时节点会在会话过期后被删除：\n\n```shell\n[zk: localhost:2181(CONNECTED) 26] create -e /tmp  \"tmp\"\nCreated /tmp\n```\n\n### 1.5 查看节点\n\n#### 1. 获取节点数据\n\n```shell\n# 格式\n# get [-s] [-w] path\n[zk: localhost:2181(CONNECTED) 24] get -s -w /hadoop\n123456\ncZxid = 0x200000002\nctime = Sat Nov 12 17:08:09 CST 2020\nmZxid = 0x200000002\nmtime = Sat Nov 12 17:08:09 CST 2020\npZxid = 0x200000002\ncversion = 0\ndataVersion = 0\naclVersion = 0\nephemeralOwner = 0x0\ndataLength = 6\nnumChildren = 0\n\n[zk: localhost:2181(CONNECTED) 25] get -s -w /tmp\ntmp\ncZxid = 0x200000006\nctime = Sat Nov 12 17:10:45 CST 2021\nmZxid = 0x200000006\nmtime = Sat Nov 12 17:10:45 CST 2021\npZxid = 0x200000006\ncversion = 0\ndataVersion = 0\naclVersion = 0\nephemeralOwner = 0x1000251b9bb0000\ndataLength = 3\nnumChildren = 0\n```\n\n节点各个属性如下表。其中一个重要的概念是 Zxid(ZooKeeper Transaction  Id)，ZooKeeper 节点的每一次更改都具有唯一的 Zxid，如果 Zxid1 小于 Zxid2，则 Zxid1 的更改发生在 Zxid2 更改之前。\n\n| **状态属性**   | **说明**                                                     |\n| -------------- | ------------------------------------------------------------ |\n| cZxid          | 数据节点创建时的事务 ID                                       |\n| ctime          | 数据节点创建时的时间                                         |\n| mZxid          | 数据节点最后一次更新时的事务 ID                               |\n| mtime          | 数据节点最后一次更新时的时间                                 |\n| pZxid          | 数据节点的子节点最后一次被修改时的事务 ID                     |\n| cversion       | 子节点的更改次数                                             |\n| dataVersion    | 节点数据的更改次数                                           |\n| aclVersion     | 节点的 ACL 的更改次数                                          |\n| ephemeralOwner | 如果节点是临时节点，则表示创建该节点的会话的 SessionID；如果节点是持久节点，则该属性值为 0 |\n| dataLength     | 数据内容的长度                                               |\n| numChildren    | 数据节点当前的子节点个数                                     |\n\n#### 2. 查看节点状态\n\n可以使用 `stat` 命令查看节点状态，它的返回值和 `get` 命令类似，但不会返回节点数据。\n\n```shell\n[zk: localhost:2181(CONNECTED) 32] stat /hadoop\ncZxid = 0x14b\nctime = Fri May 24 17:03:06 CST 2019\nmZxid = 0x14b\nmtime = Fri May 24 17:03:06 CST 2019\npZxid = 0x14b\ncversion = 0\ndataVersion = 0\naclVersion = 0\nephemeralOwner = 0x0\ndataLength = 6\nnumChildren = 0\n```\n\n### 1.6 更新节点\n\n更新节点的命令是 `set`，可以直接进行修改，如下：\n\n```shell\n[zk: localhost:2181(CONNECTED) 33] set /hadoop 345\ncZxid = 0x14b\nctime = Fri May 24 17:03:06 CST 2019\nmZxid = 0x14c\nmtime = Fri May 24 17:13:05 CST 2019\npZxid = 0x14b\ncversion = 0\ndataVersion = 1  # 注意更改后此时版本号为 1，默认创建时为 0\naclVersion = 0\nephemeralOwner = 0x0\ndataLength = 3\nnumChildren = 0\n```\n\n也可以基于版本号进行更改，此时类似于乐观锁机制，当你传入的数据版本号 (dataVersion) 和当前节点的数据版本号不符合时，zookeeper 会拒绝本次修改：\n\n```shell\n[zk: localhost:2181(CONNECTED) 34] set  -v 0 /hadoop 678\nversion No is not valid : /hadoop  #无效的版本号\n```\n\n### 1.7 删除节点\n\n删除节点的语法如下：\n\n```shell\ndelete path -v [version]\n```\n\n和更新节点数据一样，也可以传入版本号，当你传入的数据版本号 (dataVersion) 和当前节点的数据版本号不符合时，zookeeper 不会执行删除操作。\n\n```\n[zk: localhost:2181(CONNECTED) 35] delete /hadoop -v 0\nversion No is not valid : /hadoop\n[zk: localhost:2181(CONNECTED) 36] delete /hadoop\n```\n\n要想删除某个节点及其所有后代节点，可以使用递归删除，命令为 `rmr path`。\n\n## 二、监听器\n\n### 2.1 get [-w] path\n\n使用 `get [-w] path` 注册的监听器能够在节点内容发生改变的时候，向客户端发出通知。需要注意的是 zookeeper 的触发器是一次性的 (One-time trigger)，即触发一次后就会立即失效。\n\n```shell\n[zk: localhost:2181(CONNECTED) 4] get -w /hadoop\n[zk: localhost:2181(CONNECTED) 5] set /hadoop 456\n\nWATCHER::\n\nWatchedEvent state:SyncConnected type:NodeDataChanged path:/hadoop\n```\n\n### 2.2 stat [-w] path \n\n使用 `stat [-w] path` 注册的监听器能够在节点状态发生改变的时候，向客户端发出通知。\n\n```shell\n[zk: localhost:2181(CONNECTED) 7] stat -w /hadoop\n[zk: localhost:2181(CONNECTED) 8] set /hadoop 112233\nWATCHER::\n\nWatchedEvent state:SyncConnected type:NodeDataChanged path:/hadoop\n```\n\n### 2.3 ls [-s] [-w] [-R] path\n\n使用 `ls [-s] [-w] -R path` 注册的监听器能够监听该节点下所有**子节点**的增加和删除操作。\n\n```shell\n[zk: localhost:2181(CONNECTED) 9] ls -w -R /hadoop\n[]\n[zk: localhost:2181(CONNECTED) 10] create  /hadoop/yarn \"aaa\"\nWATCHER::\n\nWatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hadoop\n```\n\n\n\n## 三、 zookeeper 四字命令\n\n| 命令 | 功能描述|\n| ---- | ------------------------------------------------------------ |\n| conf | 打印有关服务配置的详细信息。|\n| cons | 列出连接到此服务器的所有客户端的完整连接/会话详细信息。包括有关接收/发送的数据包数量、会话 ID、操作延迟、上次执行的操作等信息...|\n| crst | 重置所有连接的连接/会话统计信息。|\n| dump | 列出未完成的会话和临时节点。|\n| envi | 打印有关服务环境的详细信息|\n| ruok | 测试服务器是否在非错误状态下运行。如果服务器正在运行，它将以 imok 响应。否则它根本不会响应。“imok”响应并不一定表示服务器已加入仲裁，只是服务器进程处于活动状态并绑定到指定的客户端端口。使用“stat”获取有关状态wrt quorum和客户端连接信息的详细信息。|\n| srst | 重置服务器统计信息。|\n| srvr | 列出服务器的完整详细信息。|\n| stat | 列出服务器和连接客户端的简要详细信息。|\n| wchs | 列出有关服务器监视的简要信息。|\n| wchc | 按会话列出有关服务器监视的详细信息。这将输出具有关联手表（路径）的会话（连接）列表。请注意，根据观察次数，此操作可能会很昂贵（即影响服务器性能），请谨慎使用。|\n| dirs | 以字节为单位显示快照和日志文件的总大小|\n| wchp | 按路径列出有关服务器监视的详细信息。这将输出具有关联会话的路径（znode）列表。请注意，根据观察次数，此操作可能会很昂贵（即影响服务器性能），请谨慎使用。|\n| mntr | 输出可用于监控集群健康状况的变量列表。|\n| isro | 测试服务器是否以只读模式运行。如果处于只读模式，服务器将响应“ro”，如果不是只读模式，则响应“rw”。\n| hash | 返回与 zxid 关联的树摘要的最新历史记录。\n| gtmk | 以十进制格式的 64 位有符号长值形式获取当前跟踪掩码。有关stmk可能值的说明，请参见。\n| stmk | 设置当前跟踪掩码。跟踪掩码是 64 位，其中每一位启用或禁用服务器上特定类别的跟踪日志记录。Log4J 必须首先配置为启用TRACE级别才能查看跟踪日志消息。跟踪掩码的位对应于以下跟踪记录类别。\n\n\n> 更多四字命令可以参阅官方文档：https://zookeeper.apache.org/doc/current/zookeeperAdmin.html\n\n使用前需要使用 `sudo yum install nc` 安装 nc 命令，使用示例如下：\n\n```shell\n[root@hadoop001 bin]# echo stat | nc localhost 2181\nZookeeper version: 3.4.13-2d71af4dbe22557fda74f9a9b4309b15a7487f03, \nbuilt on 06/29/2018 04:05 GMT\nClients:\n /0:0:0:0:0:0:0:1:50584[1](queued=0,recved=371,sent=371)\n /0:0:0:0:0:0:0:1:50656[0](queued=0,recved=1,sent=0)\nLatency min/avg/max: 0/0/19\nReceived: 372\nSent: 371\nConnections: 2\nOutstanding: 0\nZxid: 0x150\nMode: standalone\nNode count: 167\n```\n\n\n```shell\n[hadoop@node02 ~]$ echo stat | nc localhost 2181\nstat is not executed because it is not in the whitelist.\n```\n- 若如上提示 指令不在白名单中，则需要修改`$ZK_HOME/conf/zoo.cfg`\n\n\n```conf\nvim $ZK_HOME/conf/zoo.cfg\n\n# 将需要的命令添加到白名单中\n4lw.commands.whitelist=stat, ruok, conf, isro\n\n# 将所有命令添加到白名单中\n4lw.commands.whitelist=*\n```\n\n- node01修改后将文件同步至其他节点\n\n```shell\ncd $ZK_HOME/conf\nscp zoo.cfg node02:$PWD\nscp zoo.cfg node03:$PWD\n```\n\n- 重启zookeeper"
  },
  {
    "path": "大数据框架学习/Zookeeper简介及核心概念.md",
    "content": "# Zookeeper简介及核心概念\n\n<nav>\n<a href=\"#一Zookeeper简介\">一、Zookeeper简介</a><br/>\n<a href=\"#二Zookeeper设计目标\">二、Zookeeper设计目标</a><br/>\n<a href=\"#三核心概念\">三、核心概念</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-集群角色\">3.1 集群角色</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-会话\">3.2 会话</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-数据节点\">3.3 数据节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-节点信息\">3.4 节点信息</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-Watcher\">3.5 Watcher</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#36-ACL\">3.6 ACL</a><br/>\n<a href=\"#四ZAB协议\">四、ZAB协议</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-ZAB协议与数据一致性\">4.1 ZAB协议与数据一致性</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42--ZAB协议的内容\">4.2  ZAB协议的内容</a><br/>\n<a href=\"#五Zookeeper的典型应用场景\">五、Zookeeper的典型应用场景</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#51数据的发布订阅\">5.1数据的发布/订阅</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#52-命名服务\">5.2 命名服务</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#53-Master选举\">5.3 Master选举</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#54-分布式锁\">5.4 分布式锁</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#55-集群管理\">5.5 集群管理</a><br/>\n</nav>\n\n\n## 一、Zookeeper简介\n\nZookeeper 是一个开源的分布式协调服务，目前由 Apache 进行维护。Zookeeper 可以用于实现分布式系统中常见的发布/订阅、负载均衡、命令服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。它具有以下特性：\n\n+ **顺序一致性**：从一个客户端发起的事务请求，最终都会严格按照其发起顺序被应用到 Zookeeper 中；\n+ **原子性**：所有事务请求的处理结果在整个集群中所有机器上都是一致的；不存在部分机器应用了该事务，而另一部分没有应用的情况；\n+ **单一视图**：所有客户端看到的服务端数据模型都是一致的；\n+ **可靠性**：一旦服务端成功应用了一个事务，则其引起的改变会一直保留，直到被另外一个事务所更改；\n+ **实时性**：一旦一个事务被成功应用后，Zookeeper 可以保证客户端立即可以读取到这个事务变更后的最新状态的数据。\n\n\n\n## 二、Zookeeper设计目标\n\nZookeeper 致力于为那些高吞吐的大型分布式系统提供一个高性能、高可用、且具有严格顺序访问控制能力的分布式协调服务。它具有以下四个目标：\n\n### 2.1 目标一：简单的数据模型\n\nZookeeper 通过树形结构来存储数据，它由一系列被称为 ZNode 的数据节点组成，类似于常见的文件系统。不过和常见的文件系统不同，Zookeeper 将数据全量存储在内存中，以此来实现高吞吐，减少访问延迟。\n\n<div align=\"center\"> <img  src=\"../pictures/zookeeper-zknamespace.jpg\"/> </div>\n\n### 2.2 目标二：构建集群\n\n可以由一组 Zookeeper 服务构成 Zookeeper 集群，集群中每台机器都会单独在内存中维护自身的状态，并且每台机器之间都保持着通讯，只要集群中有半数机器能够正常工作，那么整个集群就可以正常提供服务。\n\n<div align=\"center\"> <img  src=\"../pictures/zookeeper-zkservice.jpg\"/> </div>\n\n### 2.3 目标三：顺序访问\n\n对于来自客户端的每个更新请求，Zookeeper 都会分配一个全局唯一的递增 ID，这个 ID 反映了所有事务请求的先后顺序。\n\n### 2.4 目标四：高性能高可用\n\nZooKeeper 将数据存全量储在内存中以保持高性能，并通过服务集群来实现高可用，由于 Zookeeper 的所有更新和删除都是基于事务的，所以其在读多写少的应用场景中有着很高的性能表现。\n\n\n\n## 三、核心概念\n\n### 3.1 集群角色\n\nZookeeper 集群中的机器分为以下三种角色：\n\n+ **Leader** ：为客户端提供读写服务，并维护集群状态，它是由集群选举所产生的；\n+ **Follower** ：为客户端提供读写服务，并定期向 Leader 汇报自己的节点状态。同时也参与写操作“过半写成功”的策略和 Leader 的选举；\n+ **Observer** ：为客户端提供读写服务，并定期向 Leader 汇报自己的节点状态，但不参与写操作“过半写成功”的策略和 Leader 的选举，因此 Observer 可以在不影响写性能的情况下提升集群的读性能。\n\n### 3.2 会话\n\nZookeeper 客户端通过 TCP 长连接连接到服务集群，会话 (Session) 从第一次连接开始就已经建立，之后通过心跳检测机制来保持有效的会话状态。通过这个连接，客户端可以发送请求并接收响应，同时也可以接收到 Watch 事件的通知。\n\n关于会话中另外一个核心的概念是 sessionTimeOut(会话超时时间)，当由于网络故障或者客户端主动断开等原因，导致连接断开，此时只要在会话超时时间之内重新建立连接，则之前创建的会话依然有效。\n\n### 3.3 数据节点\n\nZookeeper 数据模型是由一系列基本数据单元 `Znode`(数据节点) 组成的节点树，其中根节点为 `/`。每个节点上都会保存自己的数据和节点信息。Zookeeper 中节点可以分为两大类：\n\n+ **持久节点** ：节点一旦创建，除非被主动删除，否则一直存在；\n+ **临时节点** ：一旦创建该节点的客户端会话失效，则所有该客户端创建的临时节点都会被删除。\n\n临时节点和持久节点都可以添加一个特殊的属性：`SEQUENTIAL`，代表该节点是否具有递增属性。如果指定该属性，那么在这个节点创建时，Zookeeper 会自动在其节点名称后面追加一个由父节点维护的递增数字。\n\n### 3.4 节点信息\n\n每个 ZNode 节点在存储数据的同时，都会维护一个叫做 `Stat` 的数据结构，里面存储了关于该节点的全部状态信息。如下：\n\n| **状态属性**   | **说明**                                                     |\n| -------------- | ------------------------------------------------------------ |\n| czxid          | 数据节点创建时的事务 ID                                       |\n| ctime          | 数据节点创建时的时间                                         |\n| mzxid          | 数据节点最后一次更新时的事务 ID                               |\n| mtime          | 数据节点最后一次更新时的时间                                 |\n| pzxid          | 数据节点的子节点最后一次被修改时的事务 ID                     |\n| cversion       | 子节点的更改次数                                             |\n| version        | 节点数据的更改次数                                           |\n| aversion       | 节点的 ACL 的更改次数                                          |\n| ephemeralOwner | 如果节点是临时节点，则表示创建该节点的会话的 SessionID；如果节点是持久节点，则该属性值为 0 |\n| dataLength     | 数据内容的长度                                               |\n| numChildren    | 数据节点当前的子节点个数                                     |\n\n### 3.5 Watcher\n\nZookeeper 中一个常用的功能是 Watcher(事件监听器)，它允许用户在指定节点上针对感兴趣的事件注册监听，当事件发生时，监听器会被触发，并将事件信息推送到客户端。该机制是 Zookeeper 实现分布式协调服务的重要特性。\n\n### 3.6 ACL\n\nZookeeper 采用 ACL(Access Control Lists) 策略来进行权限控制，类似于 UNIX 文件系统的权限控制。它定义了如下五种权限：\n\n- **CREATE**：允许创建子节点；\n- **READ**：允许从节点获取数据并列出其子节点；\n- **WRITE**：允许为节点设置数据；\n- **DELETE**：允许删除子节点；\n- **ADMIN**：允许为节点设置权限。  \n\n\n\n## 四、ZAB协议\n\n### 4.1 ZAB协议与数据一致性\n\nZAB 协议是 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。通过该协议，Zookeepe 基于主从模式的系统架构来保持集群中各个副本之间数据的一致性。具体如下：\n\nZookeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求，并采用原子广播协议将数据状态的变更以事务 Proposal 的形式广播到所有的副本进程上去。如下图：\n\n<div align=\"center\"> <img  src=\"../pictures/zookeeper-zkcomponents.jpg\"/> </div>\n\n具体流程如下：\n\n所有的事务请求必须由唯一的 Leader 服务来处理，Leader 服务将事务请求转换为事务 Proposal，并将该 Proposal 分发给集群中所有的 Follower 服务。如果有半数的 Follower 服务进行了正确的反馈，那么 Leader 就会再次向所有的 Follower 发出 Commit 消息，要求将前一个 Proposal 进行提交。\n\n### 4.2  ZAB协议的内容\n\nZAB 协议包括两种基本的模式，分别是崩溃恢复和消息广播：\n\n#### 1. 崩溃恢复\n\n当整个服务框架在启动过程中，或者当 Leader 服务器出现异常时，ZAB 协议就会进入恢复模式，通过过半选举机制产生新的 Leader，之后其他机器将从新的 Leader 上同步状态，当有过半机器完成状态同步后，就退出恢复模式，进入消息广播模式。\n\n#### 2. 消息广播\n\nZAB 协议的消息广播过程使用的是原子广播协议。在整个消息的广播过程中，Leader 服务器会每个事物请求生成对应的 Proposal，并为其分配一个全局唯一的递增的事务 ID(ZXID)，之后再对其进行广播。具体过程如下：\n\nLeader 服务会为每一个 Follower 服务器分配一个单独的队列，然后将事务 Proposal 依次放入队列中，并根据 FIFO(先进先出) 的策略进行消息发送。Follower 服务在接收到 Proposal 后，会将其以事务日志的形式写入本地磁盘中，并在写入成功后反馈给 Leader 一个 Ack 响应。当 Leader 接收到超过半数 Follower 的 Ack 响应后，就会广播一个 Commit 消息给所有的 Follower 以通知其进行事务提交，之后 Leader 自身也会完成对事务的提交。而每一个 Follower 则在接收到 Commit 消息后，完成事务的提交。\n\n<div align=\"center\"> <img  src=\"../pictures/zookeeper-brocast.jpg\"/> </div>\n\n\n\n## 五、Zookeeper的典型应用场景\n\n### 5.1数据的发布/订阅\n\n数据的发布/订阅系统，通常也用作配置中心。在分布式系统中，你可能有成千上万个服务节点，如果想要对所有服务的某项配置进行更改，由于数据节点过多，你不可逐台进行修改，而应该在设计时采用统一的配置中心。之后发布者只需要将新的配置发送到配置中心，所有服务节点即可自动下载并进行更新，从而实现配置的集中管理和动态更新。\n\nZookeeper 通过 Watcher 机制可以实现数据的发布和订阅。分布式系统的所有的服务节点可以对某个 ZNode 注册监听，之后只需要将新的配置写入该 ZNode，所有服务节点都会收到该事件。\n\n### 5.2 命名服务\n\n在分布式系统中，通常需要一个全局唯一的名字，如生成全局唯一的订单号等，Zookeeper 可以通过顺序节点的特性来生成全局唯一 ID，从而可以对分布式系统提供命名服务。\n\n### 5.3 Master选举\n\n分布式系统一个重要的模式就是主从模式 (Master/Salves)，Zookeeper 可以用于该模式下的 Matser 选举。可以让所有服务节点去竞争性地创建同一个 ZNode，由于 Zookeeper 不能有路径相同的 ZNode，必然只有一个服务节点能够创建成功，这样该服务节点就可以成为 Master 节点。\n\n### 5.4 分布式锁\n\n可以通过 Zookeeper 的临时节点和 Watcher 机制来实现分布式锁，这里以排它锁为例进行说明：\n\n分布式系统的所有服务节点可以竞争性地去创建同一个临时 ZNode，由于 Zookeeper 不能有路径相同的 ZNode，必然只有一个服务节点能够创建成功，此时可以认为该节点获得了锁。其他没有获得锁的服务节点通过在该 ZNode 上注册监听，从而当锁释放时再去竞争获得锁。锁的释放情况有以下两种：\n\n+ 当正常执行完业务逻辑后，客户端主动将临时 ZNode 删除，此时锁被释放；\n+ 当获得锁的客户端发生宕机时，临时 ZNode 会被自动删除，此时认为锁已经释放。\n\n当锁被释放后，其他服务节点则再次去竞争性地进行创建，但每次都只有一个服务节点能够获取到锁，这就是排他锁。\n\n### 5.5 集群管理\n\nZookeeper 还能解决大多数分布式系统中的问题：\n\n+ 如可以通过创建临时节点来建立心跳检测机制。如果分布式系统的某个服务节点宕机了，则其持有的会话会超时，此时该临时节点会被删除，相应的监听事件就会被触发。\n+ 分布式系统的每个服务节点还可以将自己的节点状态写入临时节点，从而完成状态报告或节点工作进度汇报。\n+ 通过数据的订阅和发布功能，Zookeeper 还能对分布式系统进行模块的解耦和任务的调度。\n+ 通过监听机制，还能对分布式系统的服务节点进行动态上下线，从而实现服务的动态扩容。\n\n<br/>\n\n## 参考资料\n\n1. 倪超 . 从 Paxos 到 Zookeeper——分布式一致性原理与实践 . 电子工业出版社 . 2015-02-01\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "大数据框架学习/installation/Azkaban_3.x_编译及部署.md",
    "content": "# Azkaban 3.x 编译及部署\n\n<nav>\n<a href=\"#一Azkaban-源码编译\">一、Azkaban 源码编译</a><br/>\n<a href=\"#二Azkaban-部署模式\">二、Azkaban 部署模式</a><br/>\n<a href=\"#三-Solo-Server-模式部署\">三、Solo Server 模式部署</a><br/>\n</nav>\n\n\n\n## 一、Azkaban 源码编译\n\n### 1.1 下载并解压\n\nAzkaban 在 3.0 版本之后就不提供对应的安装包，需要自己下载源码进行编译。\n\n下载所需版本的源码，Azkaban 的源码托管在 GitHub 上，地址为 https://github.com/azkaban/azkaban 。可以使用 `git clone` 的方式获取源码，也可以使用 `wget` 直接下载对应 release 版本的 `tar.gz` 文件，这里我采用第二种方式：\n\n```shell\n# 下载\nwget https://github.com/azkaban/azkaban/archive/3.70.0.tar.gz\n# 解压\ntar -zxvf azkaban-3.70.0.tar.gz\n```\n\n### 1.2 准备编译环境\n\n#### 1. JDK\n\nAzkaban 编译依赖 JDK 1.8+ ，JDK 安装方式见本仓库：\n\n> [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n\n#### 2. Gradle\n\nAzkaban 3.70.0 编译需要依赖 `gradle-4.6-all.zip`。Gradle 是一个项目自动化构建开源工具，类似于 Maven，但由于采用 Groovy 语言进行项目配置，所以比 Maven 更为灵活，目前广泛用于 Android 开发、Spring 项目的构建。\n\n需要注意的是不同版本的 Azkaban 依赖 Gradle 版本不同，可以在解压后的 `/gradle/wrapper/gradle-wrapper.properties` 文件查看\n\n<div align=\"center\"> <img  src=\"../../pictures/azkaban-gradle-wrapper.png\"/> </div>\n\n在编译时程序会自动去图中所示的地址进行下载，但是下载速度很慢。为避免影响编译过程，建议先手动下载至 `/gradle/wrapper/` 目录下：\n\n```shell\n# wget https://services.gradle.org/distributions/gradle-4.6-all.zip\n```\n\n然后修改配置文件 `gradle-wrapper.properties` 中的 `distributionUrl` 属性，指明使用本地的 gradle。\n\n<div align=\"center\"> <img  src=\"../../pictures/azkaban-gradle-wrapper-2.png\"/> </div>\n\n#### 3. Git\n\nAzkaban 的编译过程需要用 Git 下载部分 JAR 包，所以需要预先安装 Git：\n\n```shell\n# yum install git\n```\n\n### 1.3 项目编译\n\n在根目录下执行编译命令，编译成功后会有 `BUILD SUCCESSFUL` 的提示：\n\n```shell\n# ./gradlew build installDist -x test\n```\n\n编译过程中需要注意以下问题：\n\n+ 因为编译的过程需要下载大量的 Jar 包，下载速度根据网络情况而定，通常都不会很快，如果网络不好，耗费半个小时，一个小时都是很正常的；\n+ 编译过程中如果出现网络问题而导致 JAR 无法下载，编译可能会被强行终止，这时候重复执行编译命令即可，gradle 会把已经下载的 JAR 缓存到本地，所以不用担心会重复下载 JAR 包。\n\n\n\n## 二、Azkaban 部署模式\n\n>After version 3.0, we provide two modes: the stand alone “solo-server” mode and distributed multiple-executor mode. The following describes thedifferences between the two modes.\n\n按照官方文档的说明，Azkaban 3.x 之后版本提供 2 种运行模式：\n\n+ **solo server model(单服务模式)** ：元数据默认存放在内置的 H2 数据库（可以修改为 MySQL），该模式中 `webServer`(管理服务器) 和 `executorServer`(执行服务器) 运行在同一个进程中，进程名是 `AzkabanSingleServer`。该模式适用于小规模工作流的调度。\n- **multiple-executor(分布式多服务模式)** ：存放元数据的数据库为 MySQL，MySQL 应采用主从模式进行备份和容错。这种模式下 `webServer` 和 `executorServer` 在不同进程中运行，彼此之间互不影响，适合用于生产环境。\n\n下面主要介绍 `Solo Server` 模式。\n\n\n\n## 三 、Solo Server 模式部署\n\n### 2.1  解压\n\nSolo Server 模式安装包在编译后的 `/azkaban-solo-server/build/distributions` 目录下，找到后进行解压即可：\n\n```shell\n# 解压\ntar -zxvf  azkaban-solo-server-3.70.0.tar.gz\n```\n\n### 2.2 修改时区\n\n这一步不是必须的。但是因为 Azkaban 默认采用的时区是 `America/Los_Angeles`，如果你的调度任务中有定时任务的话，就需要进行相应的更改，这里我更改为常用的 `Asia/Shanghai`\n\n<div align=\"center\"> <img  src=\"../../pictures/azkaban-setting.png\"/> </div>\n\n### 2.3 启动\n\n执行启动命令，需要注意的是一定要在根目录下执行，不能进入 `bin` 目录下执行，不然会抛出 `Cannot find 'database.properties'` 异常。\n\n```shell\n# bin/start-solo.sh\n```\n\n### 2.4 验证\n\n验证方式一：使用 `jps` 命令查看是否有 `AzkabanSingleServer` 进程：\n\n<div align=\"center\"> <img  src=\"../../pictures/akaban-jps.png\"/> </div>\n<br/>\n\n验证方式二：访问 8081 端口，查看 Web UI 界面，默认的登录名密码都是 `azkaban`，如果需要修改或新增用户，可以在 `conf/azkaban-users.xml ` 文件中进行配置：\n\n<div align=\"center\"> <img width=\"700px\" src=\"../../pictures/azkaban-web-ui.png\"/> </div>\n\n\n"
  },
  {
    "path": "大数据框架学习/installation/Flink_Standalone_Cluster.md",
    "content": "# Flink Standalone Cluster\n<nav>\n<a href=\"#一部署模式\">一、部署模式</a><br/>\n<a href=\"#二单机模式\">二、单机模式</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-安装部署\">2.1 安装部署</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-作业提交\">2.2 作业提交</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-停止作业\">2.3 停止作业</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-停止-Flink\">2.4 停止 Flink </a><br/>\n<a href=\"#三Standalone-Cluster\">三、Standalone Cluster</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-前置条件\">3.1 前置条件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-搭建步骤\">3.2 搭建步骤</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-可选配置\">3.3 可选配置</a><br/>\n<a href=\"#四Standalone-Cluster-HA\">四、Standalone Cluster HA</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-前置条件\">4.1 前置条件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-搭建步骤\">4.2 搭建步骤</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-常见异常\">4.3 常见异常</a><br/>\n</nav>\n\n\n## 一、部署模式\n\nFlink 支持使用多种部署模式来满足不同规模应用的需求，常见的有单机模式，Standalone Cluster 模式，同时 Flink 也支持部署在其他第三方平台上，如 YARN，Mesos，Docker，Kubernetes 等。以下主要介绍其单机模式和 Standalone Cluster 模式的部署。\n\n## 二、单机模式\n\n单机模式是一种开箱即用的模式，可以在单台服务器上运行，适用于日常的开发和调试。具体操作步骤如下：\n\n### 2.1 安装部署\n\n**1. 前置条件**\n\nFlink 的运行依赖 JAVA 环境，故需要预先安装好 JDK，具体步骤可以参考：[Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n\n**2. 下载 & 解压 & 运行**\n\nFlink 所有版本的安装包可以直接从其[官网](https://flink.apache.org/downloads.html)进行下载，这里我下载的 Flink 的版本为 `1.9.1` ，要求的 JDK 版本为 `1.8.x +`。 下载后解压到指定目录：\n\n```shell\ntar -zxvf flink-1.9.1-bin-scala_2.12.tgz  -C /usr/app\n```\n\n不需要进行任何配置，直接使用以下命令就可以启动单机版本的 Flink：\n\n```shell\nbin/start-cluster.sh\n```\n\n**3. WEB UI 界面**\n\nFlink 提供了 WEB 界面用于直观的管理 Flink 集群，访问端口为 `8081`：\n\n<div align=\"center\"> <img src=\"../../pictures/flink-dashboard.png\"/> </div>\n\n\n\nFlink 的 WEB UI 界面支持大多数常用功能，如提交作业，取消作业，查看各个节点运行情况，查看作业执行情况等，大家可以在部署完成后，进入该页面进行详细的浏览。\n\n### 2.2 作业提交\n\n启动后可以运行安装包中自带的词频统计案例，具体步骤如下：\n\n**1. 开启端口**\n\n```shell\nnc -lk 9999\n```\n\n**2. 提交作业**\n\n```shell\nbin/flink run examples/streaming/SocketWindowWordCount.jar --port 9999\n```\n\n该 JAR 包的源码可以在 Flink 官方的 GitHub 仓库中找到，地址为 ：[SocketWindowWordCount](https://github.com/apache/flink/blob/master/flink-examples/flink-examples-streaming/src/main/java/org/apache/flink/streaming/examples/socket/SocketWindowWordCount.java) ，可选传参有 hostname， port，对应的词频数据需要使用空格进行分割。\n\n**3. 输入测试数据**\n\n```shell\na a b b c c c a e\n```\n\n**4. 查看控制台输出**\n\n可以通过 WEB UI 的控制台查看作业统运行情况：\n\n<div align=\"center\"> <img src=\"../../pictures/flink-socket-wordcount.png\"/> </div>\n\n\n\n也可以通过 WEB 控制台查看到统计结果：\n\n<div align=\"center\"> <img src=\"../../pictures/flink-socket-wordcount-stdout.png\"/> </div>\n\n\n\n### 2.3 停止作业\n\n可以直接在 WEB 界面上点击对应作业的 `Cancel Job`  按钮进行取消，也可以使用命令行进行取消。使用命令行进行取消时，需要先获取到作业的 JobId，可以使用 `flink list` 命令查看，输出如下：\n\n```shell\n[root@hadoop001 flink-1.9.1]# ./bin/flink list\nWaiting for response...\n------------------ Running/Restarting Jobs -------------------\n05.11.2019 08:19:53 : ba2b1cc41a5e241c32d574c93de8a2bc : Socket Window WordCount (RUNNING)\n--------------------------------------------------------------\nNo scheduled jobs.\n```\n\n获取到 JobId 后，就可以使用 `flink cancel` 命令取消作业：\n\n```shell\nbin/flink cancel ba2b1cc41a5e241c32d574c93de8a2bc\n```\n\n### 2.4 停止 Flink \n\n命令如下：\n\n```shell\nbin/stop-cluster.sh\n```\n\n\n\n## 三、Standalone Cluster\n\nStandalone Cluster 模式是 Flink 自带的一种集群模式，具体配置步骤如下：\n\n### 3.1 前置条件\n\n使用该模式前，需要确保所有服务器间都已经配置好 SSH 免密登录服务。这里我以三台服务器为例，主机名分别为 hadoop001，hadoop002，hadoop003 , 其中 hadoop001 为 master 节点，其余两台为 slave 节点，搭建步骤如下：\n\n### 3.2 搭建步骤\n\n修改 `conf/flink-conf.yaml` 中 jobmanager 节点的通讯地址为 hadoop001:\n\n```yaml\njobmanager.rpc.address: hadoop001\n```\n\n修改 `conf/slaves` 配置文件，将 hadoop002 和 hadoop003 配置为 slave 节点：\n\n```shell\nhadoop002\nhadoop003\n```\n\n将配置好的 Flink 安装包分发到其他两台服务器上：\n\n```shell\n scp -r /usr/app/flink-1.9.1 hadoop002:/usr/app\n scp -r /usr/app/flink-1.9.1 hadoop003:/usr/app\n```\n\n在 hadoop001 上使用和单机模式相同的命令来启动集群：\n\n```shell\nbin/start-cluster.sh\n```\n\n此时控制台输出如下：\n\n<div align=\"center\"> <img src=\"../../pictures/flink-start-cluster-shell.png\"/> </div>\n\n\n\n启动完成后可以使用 `Jps` 命令或者通过 WEB 界面来查看是否启动成功。\n\n### 3.3 可选配置\n\n除了上面介绍的 *jobmanager.rpc.address* 是必选配置外，Flink h还支持使用其他可选参数来优化集群性能，主要如下：\n\n- **jobmanager.heap.size**：JobManager 的 JVM 堆内存大小，默认为 1024m 。\n- **taskmanager.heap.size**：Taskmanager 的 JVM 堆内存大小，默认为 1024m 。\n- **taskmanager.numberOfTaskSlots**：Taskmanager 上 slots 的数量，通常设置为 CPU 核心的数量，或其一半。\n- **parallelism.default**：任务默认的并行度。\n- **io.tmp.dirs**：存储临时文件的路径，如果没有配置，则默认采用服务器的临时目录，如 LInux 的 `/tmp` 目录。\n\n更多配置可以参考 Flink 的官方手册：[Configuration](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/config.html)\n\n## 四、Standalone Cluster HA\n\n上面我们配置的 Standalone 集群实际上只有一个 JobManager，此时是存在单点故障的，所以官方提供了 Standalone Cluster HA 模式来实现集群高可用。\n\n### 4.1 前置条件\n\n在 Standalone Cluster HA 模式下，集群可以由多个 JobManager，但只有一个处于 active 状态，其余的则处于备用状态，Flink 使用 ZooKeeper 来选举出 Active JobManager，并依赖其来提供一致性协调服务，所以需要预先安装 ZooKeeper 。\n\n另外在高可用模式下，还需要使用分布式文件系统来持久化存储 JobManager 的元数据，最常用的就是 HDFS，所以 Hadoop 也需要预先安装。关于 Hadoop 集群和 ZooKeeper 集群的搭建可以参考：\n\n+ [Hadoop 集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop集群环境搭建.md)\n+ [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md) \n\n### 4.2 搭建步骤\n\n修改 `conf/flink-conf.yaml` 文件，增加如下配置：\n\n```yaml\n# 配置使用zookeeper来开启高可用模式\nhigh-availability: zookeeper\n# 配置zookeeper的地址，采用zookeeper集群时，可以使用逗号来分隔多个节点地址\nhigh-availability.zookeeper.quorum: hadoop003:2181\n# 在zookeeper上存储flink集群元信息的路径\nhigh-availability.zookeeper.path.root: /flink\n# 集群id\nhigh-availability.cluster-id: /standalone_cluster_one\n# 持久化存储JobManager元数据的地址，zookeeper上存储的只是指向该元数据的指针信息\nhigh-availability.storageDir: hdfs://hadoop001:8020/flink/recovery\n```\n\n修改 `conf/masters` 文件，将 hadoop001 和 hadoop002 都配置为 master 节点：\n\n```shell\nhadoop001:8081\nhadoop002:8081\n```\n\n确保 Hadoop 和 ZooKeeper 已经启动后，使用以下命令来启动集群：\n\n```shell\nbin/start-cluster.sh\n```\n\n此时输出如下：\n\n<div align=\"center\"> <img src=\"../../pictures/flink-standalone-cluster-ha.png\"/> </div>\n\n\n\n可以看到集群已经以 HA 的模式启动，此时还需要在各个节点上使用 `jps` 命令来查看进程是否启动成功，正常情况如下：\n\n<div align=\"center\"> <img src=\"../../pictures/flink-standalone-cluster-jps.png\"/> </div>\n\n\n\n只有 hadoop001 和 hadoop002 的 JobManager 进程，hadoop002 和 hadoop003 上的 TaskManager 进程都已经完全启动，才表示 Standalone Cluster HA 模式搭建成功。\n\n### 4.3 常见异常\n\n如果进程没有启动，可以通过查看 `log` 目录下的日志来定位错误，常见的一个错误如下：\n\n```shell\n2019-11-05 09:18:35,877 INFO  org.apache.flink.runtime.entrypoint.ClusterEntrypoint      \n- Shutting StandaloneSessionClusterEntrypoint down with application status FAILED. Diagnostics\njava.io.IOException: Could not create FileSystem for highly available storage (high-availability.storageDir)\n.......\nCaused by: org.apache.flink.core.fs.UnsupportedFileSystemSchemeException: Could not find a file \nsystem implementation for scheme 'hdfs'. The scheme is not directly supported by Flink and no \nHadoop file system to support this scheme could be loaded.\n.....\nCaused by: org.apache.flink.core.fs.UnsupportedFileSystemSchemeException: Hadoop is not in \nthe classpath/dependencies.\n......\n```\n\n可以看到是因为在 classpath 目录下找不到 Hadoop 的相关依赖，此时需要检查是否在环境变量中配置了 Hadoop 的安装路径，如果路径已经配置但仍然存在上面的问题，可以从 [Flink 官网](https://flink.apache.org/downloads.html)下载对应版本的 Hadoop 组件包：\n\n<div align=\"center\"> <img src=\"../../pictures/flink-optional-components.png\"/> </div>\n\n\n\n下载完成后，将该 JAR 包上传至**所有** Flink 安装目录的 `lib` 目录即可。\n\n\n\n## 参考资料\n\n+ [Standalone Cluster](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/deployment/cluster_setup.html#standalone-cluster)\n+ [JobManager High Availability (HA)](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/jobmanager_high_availability.html)\n\n"
  },
  {
    "path": "大数据框架学习/installation/HBase单机环境搭建.md",
    "content": "# HBase基本环境搭建\n\n<nav>\n<a href=\"#一安装前置条件说明\">一、安装前置条件说明</a><br/>\n<a href=\"#二Standalone-模式\">二、Standalone 模式</a><br/>\n<a href=\"#三伪集群模式安装Pseudo-Distributed\">三、伪集群模式安装（Pseudo-Distributed）</a><br/>\n</nav>\n\n## 一、安装前置条件说明\n\n### 1.1 JDK版本说明\n\nHBase 需要依赖 JDK 环境，同时 HBase 2.0+ 以上版本不再支持 JDK 1.7 ，需要安装 JDK 1.8+ 。JDK 安装方式见本仓库：\n\n> [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n\n### 1.2 Standalone模式和伪集群模式的区别\n\n+ 在 `Standalone` 模式下，所有守护进程都运行在一个 `jvm` 进程/实例中；\n+ 在伪分布模式下，HBase 仍然在单个主机上运行，但是每个守护进程 (HMaster，HRegionServer 和 ZooKeeper) 则分别作为一个单独的进程运行。\n\n**说明：两种模式任选其一进行部署即可，对于开发测试来说区别不大。**\n\n\n\n## 二、Standalone 模式\n\n### 2.1 下载并解压\n\n从[官方网站](https://hbase.apache.org/downloads.html) 下载所需要版本的二进制安装包，并进行解压：\n\n```shell\n# tar -zxvf hbase-2.1.4-bin.tar.gz\n```\n\n### 2.2 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport HBASE_HOME=/usr/app/hbase-2.1.4\nexport PATH=$HBASE_HOME/bin:$PATH\n```\n\n使得配置的环境变量生效：\n\n```shell\n# source /etc/profile\n```\n\n### 2.3 进行HBase相关配置\n\n修改安装目录下的 `conf/hbase-env.sh`,指定 JDK 的安装路径：\n\n```shell\n# The java implementation to use.  Java 1.8+ required.\nexport JAVA_HOME=/usr/java/jdk1.8.0_201\n```\n\n修改安装目录下的 `conf/hbase-site.xml`，增加如下配置：\n\n```xml\n<configuration>\n <property>\n    <name>hbase.rootdir</name>\n    <value>file:///home/hbase/rootdir</value>\n  </property>\n  <property>\n    <name>hbase.zookeeper.property.dataDir</name>\n    <value>/home/zookeeper/dataDir</value>\n  </property>\n  <property>\n    <name>hbase.unsafe.stream.capability.enforce</name>\n    <value>false</value>\n  </property>\n</configuration>\n```\n\n`hbase.rootdir`: 配置 hbase 数据的存储路径；\n\n`hbase.zookeeper.property.dataDir`: 配置 zookeeper 数据的存储路径；\n\n`hbase.unsafe.stream.capability.enforce`: 使用本地文件系统存储，不使用 HDFS 的情况下需要禁用此配置，设置为 false。\n\n### 2.4 启动HBase\n\n由于已经将 HBase 的 bin 目录配置到环境变量，直接使用以下命令启动：\n\n```shell\n# start-hbase.sh\n```\n\n### 2.5 验证启动是否成功\n\n验证方式一 ：使用 `jps` 命令查看 HMaster 进程是否启动。\n\n```\n[root@hadoop001 hbase-2.1.4]# jps\n16336 Jps\n15500 HMaster\n```\n\n验证方式二 ：访问 HBaseWeb UI 页面，默认端口为 `16010` 。\n\n<div align=\"center\"> <img src=\"../../pictures/hbase-web-ui.png\"/> </div>\n\n\n## 三、伪集群模式安装（Pseudo-Distributed）\n\n### 3.1 Hadoop单机伪集群安装\n\n这里我们采用 HDFS 作为 HBase 的存储方案，需要预先安装 Hadoop。Hadoop 的安装方式单独整理至：\n\n> [Hadoop 单机伪集群搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop单机版本环境搭建.md)\n\n### 3.2 Hbase版本选择\n\nHBase 的版本必须要与 Hadoop 的版本兼容，不然会出现各种 Jar 包冲突。这里我 Hadoop 安装的版本为 `hadoop-2.6.0-cdh5.15.2`，为保持版本一致，选择的 HBase 版本为 `hbase-1.2.0-cdh5.15.2` 。所有软件版本如下：\n\n+ Hadoop 版本： hadoop-2.6.0-cdh5.15.2\n+ HBase 版本： hbase-1.2.0-cdh5.15.2\n+ JDK 版本：JDK 1.8\n\n\n\n### 3.3 软件下载解压\n\n下载后进行解压，下载地址：http://archive.cloudera.com/cdh5/cdh/5/    \n\n```shell\n# tar -zxvf hbase-1.2.0-cdh5.15.2.tar.gz\n```\n\n### 3.4 配置环境变量\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport HBASE_HOME=/usr/app/hbase-1.2.0-cdh5.15.2\nexport PATH=$HBASE_HOME/bin:$PATH\n```\n\n使得配置的环境变量生效：\n\n```shell\n# source /etc/profile\n```\n\n\n\n\n### 3.5 进行HBase相关配置\n\n1.修改安装目录下的 `conf/hbase-env.sh`,指定 JDK 的安装路径：\n\n```shell\n# The java implementation to use.  Java 1.7+ required.\nexport JAVA_HOME=/usr/java/jdk1.8.0_201\n```\n\n2.修改安装目录下的 `conf/hbase-site.xml`，增加如下配置 (hadoop001 为主机名)：\n\n```xml\n<configuration>\n <!--指定 HBase 以分布式模式运行-->   \n <property>\n    <name>hbase.cluster.distributed</name>\n    <value>true</value>\n </property>\n <!--指定 HBase 数据存储路径为 HDFS 上的 hbase 目录,hbase 目录不需要预先创建，程序会自动创建-->   \n <property>\n    <name>hbase.rootdir</name>\n    <value>hdfs://hadoop001:8020/hbase</value>\n  </property>\n    <!--指定 zookeeper 数据的存储位置-->   \n  <property>\n    <name>hbase.zookeeper.property.dataDir</name>\n    <value>/home/zookeeper/dataDir</value>\n  </property>\n</configuration>\n```\n\n3.修改安装目录下的 `conf/regionservers`，指定 region  servers 的地址，修改后其内容如下：\n\n```shell\nhadoop001\n```\n\n\n\n### 3.6 启动\n\n```shell\n# bin/start-hbase.sh\n```\n\n\n\n### 3.7 验证启动是否成功\n\n验证方式一 ：使用 `jps` 命令查看进程。其中 `HMaster`，`HRegionServer` 是 HBase 的进程，`HQuorumPeer` 是 HBase 内置的 Zookeeper 的进程，其余的为 HDFS 和 YARN 的进程。\n\n```shell\n[root@hadoop001 conf]# jps\n28688 NodeManager\n25824 GradleDaemon\n10177 Jps\n22083 HRegionServer\n20534 DataNode\n20807 SecondaryNameNode\n18744 Main\n20411 NameNode\n21851 HQuorumPeer\n28573 ResourceManager\n21933 HMaster\n```\n\n验证方式二 ：访问 HBase Web UI 界面，需要注意的是 1.2 版本的 HBase 的访问端口为 `60010`\n\n<div align=\"center\"> <img src=\"../../pictures/hbase-60010.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/installation/HBase集群环境搭建.md",
    "content": "# HBase集群环境配置\n\n<nav>\n<a href=\"#一集群规划\">一、集群规划</a><br/>\n<a href=\"#二前置条件\">二、前置条件</a><br/>\n<a href=\"#三集群搭建\">三、集群搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-下载并解压\">3.1 下载并解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-配置环境变量\">3.2 配置环境变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-集群配置\">3.3 集群配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-HDFS客户端配置\">3.4 HDFS客户端配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35-安装包分发\">3.5 安装包分发</a><br/>\n<a href=\"#四启动集群\">四、启动集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-启动ZooKeeper集群\">4.1 启动ZooKeeper集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-启动Hadoop集群\">4.2 启动Hadoop集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-启动HBase集群\">4.3 启动HBase集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#45-查看服务\">4.5 查看服务</a><br/>\n</nav>\n\n\n\n## 一、集群规划\n\n这里搭建一个 3 节点的 HBase 集群，其中三台主机上均为 `Regin Server`。同时为了保证高可用，除了在 hadoop001 上部署主 `Master` 服务外，还在 hadoop002 上部署备用的 `Master` 服务。Master 服务由 Zookeeper 集群进行协调管理，如果主 `Master` 不可用，则备用 `Master` 会成为新的主 `Master`。\n\n<div align=\"center\"> <img  src=\"../../pictures/hbase集群规划.png\"/> </div>\n\n## 二、前置条件\n\nHBase 的运行需要依赖 Hadoop 和 JDK(`HBase 2.0+` 对应 `JDK 1.8+`) 。同时为了保证高可用，这里我们不采用 HBase 内置的 Zookeeper 服务，而采用外置的 Zookeeper 集群。相关搭建步骤可以参阅：\n\n- [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n- [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md)\n- [Hadoop 集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop集群环境搭建.md)\n\n\n\n## 三、集群搭建\n\n### 3.1 下载并解压\n\n下载并解压，这里我下载的是 CDH 版本 HBase，下载地址为：http://archive.cloudera.com/cdh5/cdh/5/\n\n```shell\n# tar -zxvf hbase-1.2.0-cdh5.15.2.tar.gz\n```\n\n### 3.2 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport HBASE_HOME=usr/app/hbase-1.2.0-cdh5.15.2\nexport PATH=$HBASE_HOME/bin:$PATH\n```\n\n使得配置的环境变量立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 3.3 集群配置\n\n进入 `${HBASE_HOME}/conf` 目录下，修改配置：\n\n#### 1. hbase-env.sh \n\n```shell\n# 配置JDK安装位置\nexport JAVA_HOME=/usr/java/jdk1.8.0_201\n# 不使用内置的zookeeper服务\nexport HBASE_MANAGES_ZK=false\n```\n\n#### 2. hbase-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!-- 指定 hbase 以分布式集群的方式运行 -->\n        <name>hbase.cluster.distributed</name>\n        <value>true</value>\n    </property>\n    <property>\n        <!-- 指定 hbase 在 HDFS 上的存储位置 -->\n        <name>hbase.rootdir</name>\n        <value>hdfs://hadoop001:8020/hbase</value>\n    </property>\n    <property>\n        <!-- 指定 zookeeper 的地址-->\n        <name>hbase.zookeeper.quorum</name>\n        <value>hadoop001:2181,hadoop002:2181,hadoop003:2181</value>\n    </property>\n</configuration>\n```\n\n#### 3. regionservers\n\n```\nhadoop001\nhadoop002\nhadoop003\n```\n\n#### 4. backup-masters\n\n```\nhadoop002\n```\n\n` backup-masters` 这个文件是不存在的，需要新建，主要用来指明备用的 master 节点，可以是多个，这里我们以 1 个为例。\n\n### 3.4 HDFS客户端配置\n\n这里有一个可选的配置：如果您在 Hadoop 集群上进行了 HDFS 客户端配置的更改，比如将副本系数 `dfs.replication` 设置成 5，则必须使用以下方法之一来使 HBase 知道，否则 HBase 将依旧使用默认的副本系数 3 来创建文件：\n\n> 1. Add a pointer to your `HADOOP_CONF_DIR` to the `HBASE_CLASSPATH` environment variable in *hbase-env.sh*.\n> 2. Add a copy of *hdfs-site.xml* (or *hadoop-site.xml*) or, better, symlinks, under *${HBASE_HOME}/conf*, or\n> 3. if only a small set of HDFS client configurations, add them to *hbase-site.xml*.\n\n以上是官方文档的说明，这里解释一下：\n\n**第一种** ：将 Hadoop 配置文件的位置信息添加到 `hbase-env.sh` 的 `HBASE_CLASSPATH` 属性，示例如下：\n\n```shell\nexport HBASE_CLASSPATH=usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop\n```\n\n**第二种** ：将 Hadoop 的 ` hdfs-site.xml` 或 `hadoop-site.xml` 拷贝到  `${HBASE_HOME}/conf ` 目录下，或者通过符号链接的方式。如果采用这种方式的话，建议将两者都拷贝或建立符号链接，示例如下：\n\n```shell\n# 拷贝\ncp core-site.xml hdfs-site.xml /usr/app/hbase-1.2.0-cdh5.15.2/conf/\n# 使用符号链接\nln -s   /usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop/core-site.xml\nln -s   /usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop/hdfs-site.xml\n```\n\n> 注：`hadoop-site.xml` 这个配置文件现在叫做 `core-site.xml`\n\n**第三种** ：如果你只有少量更改，那么直接配置到 `hbase-site.xml` 中即可。\n\n\n\n### 3.5 安装包分发\n\n将 HBase 的安装包分发到其他服务器，分发后建议在这两台服务器上也配置一下 HBase 的环境变量。\n\n```shell\nscp -r /usr/app/hbase-1.2.0-cdh5.15.2/  hadoop002:usr/app/\nscp -r /usr/app/hbase-1.2.0-cdh5.15.2/  hadoop003:usr/app/\n```\n\n\n\n## 四、启动集群\n\n### 4.1 启动ZooKeeper集群\n\n分别到三台服务器上启动 ZooKeeper 服务：\n\n```shell\n zkServer.sh start\n```\n\n### 4.2 启动Hadoop集群\n\n```shell\n# 启动dfs服务\nstart-dfs.sh\n# 启动yarn服务\nstart-yarn.sh\n```\n\n### 4.3 启动HBase集群\n\n进入 hadoop001 的 `${HBASE_HOME}/bin`，使用以下命令启动 HBase 集群。执行此命令后，会在 hadoop001 上启动 `Master` 服务，在 hadoop002 上启动备用 `Master` 服务，在 `regionservers` 文件中配置的所有节点启动 `region server` 服务。\n\n```shell\nstart-hbase.sh\n```\n\n\n\n### 4.5 查看服务\n\n访问 HBase 的 Web-UI 界面，这里我安装的 HBase 版本为 1.2，访问端口为 `60010`，如果你安装的是 2.0 以上的版本，则访问端口号为 `16010`。可以看到 `Master` 在 hadoop001 上，三个 `Regin Servers` 分别在 hadoop001，hadoop002，和 hadoop003 上，并且还有一个 `Backup Matser` 服务在 hadoop002 上。\n\n<div align=\"center\"> <img  src=\"../../pictures/hbase-集群搭建1.png\"/> </div>\n<br/>\n\nhadoop002 上的 HBase 出于备用状态：\n\n<br/>\n\n<div align=\"center\"> <img  src=\"../../pictures/hbase-集群搭建2.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/installation/Hadoop单机环境搭建.md",
    "content": "# Hadoop单机版环境搭建\n\n<nav>\n<a href=\"#一前置条件\">一、前置条件</a><br/>\n<a href=\"#二配置-SSH-免密登录\">二、配置 SSH 免密登录</a><br/>\n<a href=\"#三HadoopHDFS环境搭建\">三、Hadoop(HDFS)环境搭建</a><br/>\n<a href=\"#四HadoopYARN环境搭建\">四、Hadoop(YARN)环境搭建</a><br/>\n</nav>\n\n\n\n\n## 一、前置条件\n\nHadoop 的运行依赖 JDK，需要预先安装，安装步骤见：\n\n+ [Linux 下 JDK 的安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n\n\n\n## 二、配置免密登录\n\nHadoop 组件之间需要基于 SSH 进行通讯。\n\n#### 2.1 配置映射\n\n配置 ip 地址和主机名映射：\n\n```shell\nvim /etc/hosts\n# 文件末尾增加\n192.168.43.202  hadoop001\n```\n\n### 2.2  生成公私钥\n\n执行下面命令行生成公匙和私匙：\n\n```\nssh-keygen -t rsa\n```\n\n### 3.3 授权\n\n进入 `~/.ssh` 目录下，查看生成的公匙和私匙，并将公匙写入到授权文件：\n\n```shell\n[root@@hadoop001 sbin]#  cd ~/.ssh\n[root@@hadoop001 .ssh]# ll\n-rw-------. 1 root root 1675 3 月  15 09:48 id_rsa\n-rw-r--r--. 1 root root  388 3 月  15 09:48 id_rsa.pub\n```\n\n```shell\n# 写入公匙到授权文件\n[root@hadoop001 .ssh]# cat id_rsa.pub >> authorized_keys\n[root@hadoop001 .ssh]# chmod 600 authorized_keys\n```\n\n\n\n## 三、Hadoop(HDFS)环境搭建\n\n\n\n### 3.1 下载并解压\n\n下载 Hadoop 安装包，这里我下载的是 CDH 版本的，下载地址为：http://archive.cloudera.com/cdh5/cdh/5/\n\n```shell\n# 解压\ntar -zvxf hadoop-2.6.0-cdh5.15.2.tar.gz \n```\n\n\n\n### 3.2 配置环境变量\n\n```shell\n# vi /etc/profile\n```\n\n配置环境变量：\n\n```\nexport HADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2\nexport  PATH=${HADOOP_HOME}/bin:$PATH\n```\n\n执行 `source` 命令，使得配置的环境变量立即生效：\n\n```shell\n# source /etc/profile\n```\n\n\n\n### 3.3 修改Hadoop配置\n\n进入 `${HADOOP_HOME}/etc/hadoop/ ` 目录下，修改以下配置：\n\n#### 1. hadoop-env.sh\n\n```shell\n# JDK安装路径\nexport  JAVA_HOME=/usr/java/jdk1.8.0_201/\n```\n\n#### 2. core-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!--指定 namenode 的 hdfs 协议文件系统的通信地址-->\n        <name>fs.defaultFS</name>\n        <value>hdfs://hadoop001:8020</value>\n    </property>\n    <property>\n        <!--指定 hadoop 存储临时文件的目录-->\n        <name>hadoop.tmp.dir</name>\n        <value>/home/hadoop/tmp</value>\n    </property>\n</configuration>\n```\n\n#### 3. hdfs-site.xml\n\n指定副本系数和临时文件存储位置：\n\n```xml\n<configuration>\n    <property>\n        <!--由于我们这里搭建是单机版本，所以指定 dfs 的副本系数为 1-->\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>\n```\n\n#### 4. slaves\n\n配置所有从属节点的主机名或 IP 地址，由于是单机版本，所以指定本机即可：\n\n```shell\nhadoop001\n```\n\n\n\n### 3.4 关闭防火墙\n\n不关闭防火墙可能导致无法访问 Hadoop 的 Web UI 界面：\n\n```shell\n# 查看防火墙状态\nsudo firewall-cmd --state\n# 关闭防火墙:\nsudo systemctl stop firewalld.service\n```\n\n\n\n### 3.5 初始化\n\n第一次启动 Hadoop 时需要进行初始化，进入 `${HADOOP_HOME}/bin/` 目录下，执行以下命令：\n\n```shell\n[root@hadoop001 bin]# ./hdfs namenode -format\n```\n\n\n\n### 3.6 启动HDFS\n\n进入 `${HADOOP_HOME}/sbin/` 目录下，启动 HDFS：\n\n```shell\n[root@hadoop001 sbin]# ./start-dfs.sh\n```\n\n\n\n### 3.7 验证是否启动成功\n\n方式一：执行 `jps` 查看 `NameNode` 和 `DataNode` 服务是否已经启动：\n\n```shell\n[root@hadoop001 hadoop-2.6.0-cdh5.15.2]# jps\n9137 DataNode\n9026 NameNode\n9390 SecondaryNameNode\n```\n\n\n\n方式二：查看 Web UI 界面，端口为 `50070`：\n\n<div align=\"center\"> <img width=\"700px\" src=\"../../pictures/hadoop安装验证.png\"/> </div>\n\n\n## 四、Hadoop(YARN)环境搭建\n\n### 4.1 修改配置\n\n进入 `${HADOOP_HOME}/etc/hadoop/ ` 目录下，修改以下配置：\n\n#### 1. mapred-site.xml\n\n```shell\n# 如果没有mapred-site.xml，则拷贝一份样例文件后再修改\ncp mapred-site.xml.template mapred-site.xml\n```\n\n```xml\n<configuration>\n    <property>\n        <name>mapreduce.framework.name</name>\n        <value>yarn</value>\n    </property>\n</configuration>\n```\n\n#### 2. yarn-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!--配置 NodeManager 上运行的附属服务。需要配置成 mapreduce_shuffle 后才可以在 Yarn 上运行 MapReduce 程序。-->\n        <name>yarn.nodemanager.aux-services</name>\n        <value>mapreduce_shuffle</value>\n    </property>\n</configuration>\n```\n\n\n\n### 4.2 启动服务\n\n进入 `${HADOOP_HOME}/sbin/` 目录下，启动 YARN：\n\n```shell\n./start-yarn.sh\n```\n\n\n\n#### 4.3 验证是否启动成功\n\n方式一：执行 `jps` 命令查看 `NodeManager` 和 `ResourceManager` 服务是否已经启动：\n\n```shell\n[root@hadoop001 hadoop-2.6.0-cdh5.15.2]# jps\n9137 DataNode\n9026 NameNode\n12294 NodeManager\n12185 ResourceManager\n9390 SecondaryNameNode\n```\n\n方式二：查看 Web UI 界面，端口号为 `8088`：\n\n<div align=\"center\"> <img width=\"700px\" src=\"../../pictures/hadoop-yarn安装验证.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/installation/Hadoop集群环境搭建.md",
    "content": "# Hadoop集群环境搭建\n\n<nav>\n<a href=\"#一集群规划\">一、集群规划</a><br/>\n<a href=\"#二前置条件\">二、前置条件</a><br/>\n<a href=\"#三配置免密登录\">三、配置免密登录</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-生成密匙\">3.1 生成密匙</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-免密登录\">3.2 免密登录</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-验证免密登录\">3.3 验证免密登录</a><br/>\n<a href=\"#四集群搭建\">四、集群搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-下载并解压\">3.1 下载并解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-配置环境变量\">3.2 配置环境变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-修改配置\">3.3 修改配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-分发程序\">3.4 分发程序</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#35--初始化\">3.5  初始化</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#36-启动集群\">3.6 启动集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#37-查看集群\">3.7 查看集群</a><br/>\n<a href=\"#五提交服务到集群\">五、提交服务到集群</a><br/>\n</nav>\n\n\n## 一、集群规划\n\n这里搭建一个 3 节点的 Hadoop 集群，其中三台主机均部署 `DataNode` 和 `NodeManager` 服务，但只有 hadoop001 上部署 `NameNode` 和 `ResourceManager` 服务。\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop集群规划.png\"/> </div>\n\n## 二、前置条件\n\nHadoop 的运行依赖 JDK，需要预先安装。其安装步骤单独整理至：\n\n+ [Linux 下 JDK 的安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n\n\n\n## 三、配置免密登录\n\n### 3.1 生成密匙\n\n在每台主机上使用 `ssh-keygen` 命令生成公钥私钥对：\n\n```shell\nssh-keygen\n```\n\n### 3.2 免密登录\n\n将 `hadoop001` 的公钥写到本机和远程机器的 ` ~/ .ssh/authorized_key` 文件中：\n\n```shell\nssh-copy-id -i ~/.ssh/id_rsa.pub hadoop001\nssh-copy-id -i ~/.ssh/id_rsa.pub hadoop002\nssh-copy-id -i ~/.ssh/id_rsa.pub hadoop003\n```\n\n### 3.3 验证免密登录\n\n```she\nssh hadoop002\nssh hadoop003\n```\n\n\n\n## 四、集群搭建\n\n### 3.1 下载并解压\n\n下载 Hadoop。这里我下载的是 CDH 版本 Hadoop，下载地址为：http://archive.cloudera.com/cdh5/cdh/5/\n\n```shell\n# tar -zvxf hadoop-2.6.0-cdh5.15.2.tar.gz \n```\n\n### 3.2 配置环境变量\n\n编辑 `profile` 文件：\n\n```shell\n# vim /etc/profile\n```\n\n增加如下配置：\n\n```\nexport HADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2\nexport  PATH=${HADOOP_HOME}/bin:$PATH\n```\n\n执行 `source` 命令，使得配置立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 3.3 修改配置\n\n进入 `${HADOOP_HOME}/etc/hadoop` 目录下，修改配置文件。各个配置文件内容如下：\n\n#### 1. hadoop-env.sh\n\n```shell\n# 指定JDK的安装位置\nexport JAVA_HOME=/usr/java/jdk1.8.0_201/\n```\n\n#### 2.  core-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!--指定 namenode 的 hdfs 协议文件系统的通信地址-->\n        <name>fs.defaultFS</name>\n        <value>hdfs://hadoop001:8020</value>\n    </property>\n    <property>\n        <!--指定 hadoop 集群存储临时文件的目录-->\n        <name>hadoop.tmp.dir</name>\n        <value>/home/hadoop/tmp</value>\n    </property>\n</configuration>\n```\n\n#### 3. hdfs-site.xml\n\n```xml\n<property>\n      <!--namenode 节点数据（即元数据）的存放位置，可以指定多个目录实现容错，多个目录用逗号分隔-->\n    <name>dfs.namenode.name.dir</name>\n    <value>/home/hadoop/namenode/data</value>\n</property>\n<property>\n      <!--datanode 节点数据（即数据块）的存放位置-->\n    <name>dfs.datanode.data.dir</name>\n    <value>/home/hadoop/datanode/data</value>\n</property>\n```\n\n#### 4. yarn-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!--配置 NodeManager 上运行的附属服务。需要配置成 mapreduce_shuffle 后才可以在 Yarn 上运行 MapReduce 程序。-->\n        <name>yarn.nodemanager.aux-services</name>\n        <value>mapreduce_shuffle</value>\n    </property>\n    <property>\n        <!--resourcemanager 的主机名-->\n        <name>yarn.resourcemanager.hostname</name>\n        <value>hadoop001</value>\n    </property>\n</configuration>\n\n```\n\n#### 5.  mapred-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!--指定 mapreduce 作业运行在 yarn 上-->\n        <name>mapreduce.framework.name</name>\n        <value>yarn</value>\n    </property>\n</configuration>\n```\n\n#### 5. slaves\n\n配置所有从属节点的主机名或 IP 地址，每行一个。所有从属节点上的 `DataNode` 服务和 `NodeManager` 服务都会被启动。\n\n```properties\nhadoop001\nhadoop002\nhadoop003\n```\n\n### 3.4 分发程序\n\n将 Hadoop 安装包分发到其他两台服务器，分发后建议在这两台服务器上也配置一下 Hadoop 的环境变量。\n\n```shell\n# 将安装包分发到hadoop002\nscp -r /usr/app/hadoop-2.6.0-cdh5.15.2/  hadoop002:/usr/app/\n# 将安装包分发到hadoop003\nscp -r /usr/app/hadoop-2.6.0-cdh5.15.2/  hadoop003:/usr/app/\n```\n\n### 3.5  初始化\n\n在 `Hadoop001` 上执行 namenode 初始化命令：\n\n```\nhdfs namenode -format\n```\n\n### 3.6 启动集群\n\n进入到 `Hadoop001` 的 `${HADOOP_HOME}/sbin` 目录下，启动 Hadoop。此时 `hadoop002` 和 `hadoop003` 上的相关服务也会被启动：\n\n```shell\n# 启动dfs服务\nstart-dfs.sh\n# 启动yarn服务\nstart-yarn.sh\n```\n\n### 3.7 查看集群\n\n在每台服务器上使用 `jps` 命令查看服务进程，或直接进入 Web-UI 界面进行查看，端口为 `50070`。可以看到此时有三个可用的 `Datanode`：\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop-集群环境搭建.png\"/> </div>\n<BR/>\n\n点击 `Live Nodes` 进入，可以看到每个 `DataNode` 的详细情况：\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop-集群搭建2.png\"/> </div>\n<BR/>\n\n接着可以查看 Yarn 的情况，端口号为 `8088` ：\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop-集群搭建3.png\"/> </div>\n\n\n## 五、提交服务到集群\n\n提交作业到集群的方式和单机环境完全一致，这里以提交 Hadoop 内置的计算 Pi 的示例程序为例，在任何一个节点上执行都可以，命令如下：\n\n```shell\nhadoop jar /usr/app/hadoop-2.6.0-cdh5.15.2/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar  pi  3  3\n```\n\n"
  },
  {
    "path": "大数据框架学习/installation/Linux下Flume的安装.md",
    "content": "# Linux下Flume的安装\n\n\n## 一、前置条件\n\nFlume 需要依赖 JDK 1.8+，JDK 安装方式见本仓库：\n\n> [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n\n\n\n## 二 、安装步骤\n\n### 2.1 下载并解压\n\n下载所需版本的 Flume，这里我下载的是 `CDH` 版本的 Flume。下载地址为：http://archive.cloudera.com/cdh5/cdh/5/\n\n```shell\n# 下载后进行解压\ntar -zxvf  flume-ng-1.6.0-cdh5.15.2.tar.gz\n```\n\n### 2.2 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport FLUME_HOME=/usr/app/apache-flume-1.6.0-cdh5.15.2-bin\nexport PATH=$FLUME_HOME/bin:$PATH\n```\n\n使得配置的环境变量立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 2.3 修改配置\n\n进入安装目录下的 `conf/` 目录，拷贝 Flume 的环境配置模板 `flume-env.sh.template`：\n\n```shell\n# cp flume-env.sh.template flume-env.sh\n```\n\n修改 `flume-env.sh`,指定 JDK 的安装路径：\n\n```shell\n# Environment variables can be set here.\nexport JAVA_HOME=/usr/java/jdk1.8.0_201\n```\n\n### 2.4 验证\n\n由于已经将 Flume 的 bin 目录配置到环境变量，直接使用以下命令验证是否配置成功：\n\n```shell\n# flume-ng version\n```\n\n出现对应的版本信息则代表配置成功。\n\n![flume-version](https://github.com/heibaiying/BigData-Notes/blob/master/pictures/flume-version.png)\n\n"
  },
  {
    "path": "大数据框架学习/installation/Linux下JDK安装.md",
    "content": "# Linux下JDK的安装\n\n>**系统环境**：centos 7.6\n>\n>**JDK 版本**：jdk 1.8.0_20\n\n\n\n### 1. 下载并解压\n\n在[官网](https://www.oracle.com/technetwork/java/javase/downloads/index.html) 下载所需版本的 JDK，这里我下载的版本为[JDK 1.8](https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) ,下载后进行解压：\n\n```shell\n[root@ java]# tar -zxvf jdk-8u201-linux-x64.tar.gz\n```\n\n\n\n### 2. 设置环境变量\n\n```shell\n[root@ java]# vi /etc/profile\n```\n\n添加如下配置：\n\n```shell\nexport JAVA_HOME=/usr/java/jdk1.8.0_201  \nexport JRE_HOME=${JAVA_HOME}/jre  \nexport CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib  \nexport PATH=${JAVA_HOME}/bin:$PATH\n```\n\n执行 `source` 命令，使得配置立即生效：\n\n```shell\n[root@ java]# source /etc/profile\n```\n\n\n\n### 3. 检查是否安装成功\n\n```shell\n[root@ java]# java -version\n```\n\n显示出对应的版本信息则代表安装成功。\n\n```shell\njava version \"1.8.0_201\"\nJava(TM) SE Runtime Environment (build 1.8.0_201-b09)\nJava HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)\n\n```\n"
  },
  {
    "path": "大数据框架学习/installation/Linux下Python安装.md",
    "content": "## Linux下Python安装\n\n>**系统环境**：centos 7.6\n>\n>**Python 版本**：Python-3.6.8\n\n### 1. 环境依赖\n\nPython3.x 的安装需要依赖这四个组件：gcc， zlib，zlib-devel，openssl-devel；所以需要预先安装，命令如下：\n\n```shell\nyum install gcc -y\nyum install zlib -y\nyum install zlib-devel -y\nyum install openssl-devel -y\n```\n\n### 2. 下载编译\n\nPython 源码包下载地址： https://www.python.org/downloads/\n\n```shell\n# wget https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tgz\n```\n\n### 3. 解压编译\n\n```shell\n# tar -zxvf Python-3.6.8.tgz\n```\n\n进入根目录进行编译，可以指定编译安装的路径，这里我们指定为 `/usr/app/python3.6` ：\n\n```shell\n# cd Python-3.6.8\n# ./configure --prefix=/usr/app/python3.6\n# make && make install\n```\n\n### 4. 环境变量配置\n\n```shell\nvim  /etc/profile\n```\n\n```shell\nexport PYTHON_HOME=/usr/app/python3.6\nexport  PATH=${PYTHON_HOME}/bin:$PATH\n```\n\n使得配置的环境变量立即生效：\n\n```shell\nsource /etc/profile\n```\n\n### 5. 验证安装是否成功\n\n输入 `python3` 命令，如果能进入 python 交互环境，则代表安装成功：\n\n```shell\n[root@hadoop001 app]# python3\nPython 3.6.8 (default, Mar 29 2019, 10:17:41)\n[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux\nType \"help\", \"copyright\", \"credits\" or \"license\" for more information.\n>>> 1+1\n2\n>>> exit()\n[root@hadoop001 app]#\n```\n\n"
  },
  {
    "path": "大数据框架学习/installation/Linux环境下Hive的安装部署.md",
    "content": "# Linux环境下Hive的安装\n\n<nav>\n<a href=\"#一安装Hive\">一、安装Hive</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-下载并解压\">1.1 下载并解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-配置环境变量\">1.2 配置环境变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-修改配置\">1.3 修改配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-拷贝数据库驱动\">1.4 拷贝数据库驱动</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-初始化元数据库\">1.5 初始化元数据库</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-启动\">1.6 启动</a><br/>\n<a href=\"#二HiveServer2beeline\">二、HiveServer2/beeline</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-修改Hadoop配置\">2.1 修改Hadoop配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-启动hiveserver2\">2.2 启动hiveserver2</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-使用beeline\">2.3 使用beeline</a><br/>\n</nav>\n\n## 一、安装Hive\n\n### 1.1 下载并解压\n\n下载所需版本的 Hive，这里我下载版本为 `cdh5.15.2`。下载地址：http://archive.cloudera.com/cdh5/cdh/5/\n\n```shell\n# 下载后进行解压\n tar -zxvf hive-1.1.0-cdh5.15.2.tar.gz\n```\n\n### 1.2 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport HIVE_HOME=/usr/app/hive-1.1.0-cdh5.15.2\nexport PATH=$HIVE_HOME/bin:$PATH\n```\n\n使得配置的环境变量立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 1.3 修改配置\n\n**1. hive-env.sh**\n\n进入安装目录下的 `conf/` 目录，拷贝 Hive 的环境配置模板 `flume-env.sh.template`\n\n```shell\ncp hive-env.sh.template hive-env.sh\n```\n\n修改 `hive-env.sh`，指定 Hadoop 的安装路径：\n\n```shell\nHADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2\n```\n\n**2. hive-site.xml**\n\n新建 hive-site.xml 文件，内容如下，主要是配置存放元数据的 MySQL 的地址、驱动、用户名和密码等信息：\n\n```xml\n<?xml version=\"1.0\"?>\n<?xml-stylesheet type=\"text/xsl\" href=\"configuration.xsl\"?>\n\n<configuration>\n  <property>\n    <name>javax.jdo.option.ConnectionURL</name>\n    <value>jdbc:mysql://hadoop001:3306/hadoop_hive?createDatabaseIfNotExist=true</value>\n  </property>\n  \n  <property>\n    <name>javax.jdo.option.ConnectionDriverName</name>\n    <value>com.mysql.jdbc.Driver</value>\n  </property>\n  \n  <property>\n    <name>javax.jdo.option.ConnectionUserName</name>\n    <value>root</value>\n  </property>\n  \n  <property>\n    <name>javax.jdo.option.ConnectionPassword</name>\n    <value>root</value>\n  </property>\n\n</configuration>\n```\n\n\n\n### 1.4 拷贝数据库驱动\n\n将 MySQL 驱动包拷贝到 Hive 安装目录的 `lib` 目录下, MySQL 驱动的下载地址为：https://dev.mysql.com/downloads/connector/j/  , 在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下我也上传了一份，有需要的可以自行下载。\n\n<div align=\"center\"> <img  src=\"../../pictures/hive-mysql.png\"/> </div>\n\n\n\n### 1.5 初始化元数据库\n\n+ 当使用的 hive 是 1.x 版本时，可以不进行初始化操作，Hive 会在第一次启动的时候会自动进行初始化，但不会生成所有的元数据信息表，只会初始化必要的一部分，在之后的使用中用到其余表时会自动创建；\n\n+ 当使用的 hive 是 2.x 版本时，必须手动初始化元数据库。初始化命令：\n\n  ```shell\n  # schematool 命令在安装目录的 bin 目录下，由于上面已经配置过环境变量，在任意位置执行即可\n  schematool -dbType mysql -initSchema\n  ```\n\n这里我使用的是 CDH 的 `hive-1.1.0-cdh5.15.2.tar.gz`，对应 `Hive 1.1.0` 版本，可以跳过这一步。\n\n### 1.6 启动\n\n由于已经将 Hive 的 bin 目录配置到环境变量，直接使用以下命令启动，成功进入交互式命令行后执行 `show databases` 命令，无异常则代表搭建成功。\n\n```shell\n# hive\n```\n\n<div align=\"center\"> <img  src=\"../../pictures/hive-install-2.png\"/> </div>\n\n在 Mysql 中也能看到 Hive 创建的库和存放元数据信息的表\n\n<div align=\"center\"> <img  src=\"../../pictures/hive-mysql-tables.png\"/> </div>\n\n\n\n## 二、HiveServer2/beeline\n\nHive 内置了 HiveServer 和 HiveServer2 服务，两者都允许客户端使用多种编程语言进行连接，但是 HiveServer 不能处理多个客户端的并发请求，因此产生了 HiveServer2。HiveServer2（HS2）允许远程客户端可以使用各种编程语言向 Hive 提交请求并检索结果，支持多客户端并发访问和身份验证。HS2 是由多个服务组成的单个进程，其包括基于 Thrift 的 Hive 服务（TCP 或 HTTP）和用于 Web UI 的 Jetty Web 服务。\n\n HiveServer2 拥有自己的 CLI 工具——Beeline。Beeline 是一个基于 SQLLine 的 JDBC 客户端。由于目前 HiveServer2 是 Hive 开发维护的重点，所以官方更加推荐使用 Beeline 而不是 Hive CLI。以下主要讲解 Beeline 的配置方式。\n\n\n\n### 2.1 修改Hadoop配置\n\n修改 hadoop 集群的 core-site.xml 配置文件，增加如下配置，指定 hadoop 的 root 用户可以代理本机上所有的用户。\n\n```xml\n<property>\n <name>hadoop.proxyuser.root.hosts</name>\n <value>*</value>\n</property>\n<property>\n <name>hadoop.proxyuser.root.groups</name>\n <value>*</value>\n</property>\n```\n\n之所以要配置这一步，是因为 hadoop 2.0 以后引入了安全伪装机制，使得 hadoop 不允许上层系统（如 hive）直接将实际用户传递到 hadoop 层，而应该将实际用户传递给一个超级代理，由该代理在 hadoop 上执行操作，以避免任意客户端随意操作 hadoop。如果不配置这一步，在之后的连接中可能会抛出 `AuthorizationException` 异常。\n\n>关于 Hadoop 的用户代理机制，可以参考：[hadoop 的用户代理机制](https://blog.csdn.net/u012948976/article/details/49904675#官方文档解读) 或 [Superusers Acting On Behalf Of Other Users](http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/Superusers.html)\n\n\n\n### 2.2 启动hiveserver2\n\n由于上面已经配置过环境变量，这里直接启动即可：\n\n```shell\n# nohup hiveserver2 &\n```\n\n\n\n### 2.3 使用beeline\n\n可以使用以下命令进入 beeline 交互式命令行，出现 `Connected` 则代表连接成功。\n\n```shell\n# beeline -u jdbc:hive2://hadoop001:10000 -n root\n```\n\n<div align=\"center\"> <img src=\"../../pictures/hive-beeline-cli.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/installation/Spark开发环境搭建.md",
    "content": "# Spark开发环境搭建\n\n<nav>\n<a href=\"#一安装Spark\">一、安装Spark</a><br/>\n<a href=\"#二词频统计案例\">二、词频统计案例</a><br/>\n<a href=\"#三Scala开发环境配置\">三、Scala开发环境配置</a><br/>\n</nav>\n\n## 一、安装Spark\n\n### 1.1 下载并解压\n\n官方下载地址：http://spark.apache.org/downloads.html ，选择 Spark 版本和对应的 Hadoop 版本后再下载：\n\n<div align=\"center\"> <img width=\"600px\" src=\"../../pictures/spark-download.png\"/> </div>\n\n解压安装包：\n\n```shell\n# tar -zxvf  spark-2.2.3-bin-hadoop2.6.tgz\n```\n\n\n\n### 1.2 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport SPARK_HOME=/usr/app/spark-2.2.3-bin-hadoop2.6\nexport  PATH=${SPARK_HOME}/bin:$PATH\n```\n\n使得配置的环境变量立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 1.3 Local模式\n\nLocal 模式是最简单的一种运行方式，它采用单节点多线程方式运行，不用部署，开箱即用，适合日常测试开发。\n\n```shell\n# 启动spark-shell\nspark-shell --master local[2]\n```\n\n- **local**：只启动一个工作线程；\n- **local[k]**：启动 k 个工作线程；\n- **local[*]**：启动跟 cpu 数目相同的工作线程数。\n\n<div align=\"center\"> <img src=\"../../pictures/spark-shell-local.png\"/> </div>\n\n<br/>\n\n进入 spark-shell 后，程序已经自动创建好了上下文 `SparkContext`，等效于执行了下面的 Scala 代码：\n\n```scala\nval conf = new SparkConf().setAppName(\"Spark shell\").setMaster(\"local[2]\")\nval sc = new SparkContext(conf)\n```\n\n\n## 二、词频统计案例\n\n安装完成后可以先做一个简单的词频统计例子，感受 spark 的魅力。准备一个词频统计的文件样本 `wc.txt`，内容如下：\n\n```txt\nhadoop,spark,hadoop\nspark,flink,flink,spark\nhadoop,hadoop\n```\n\n在 scala 交互式命令行中执行如下 Scala 语句：\n\n```scala\nval file = spark.sparkContext.textFile(\"file:///usr/app/wc.txt\")\nval wordCounts = file.flatMap(line => line.split(\",\")).map((word => (word, 1))).reduceByKey(_ + _)\nwordCounts.collect\n```\n\n执行过程如下，可以看到已经输出了词频统计的结果：\n\n<div align=\"center\"> <img  src=\"../../pictures/spark-shell.png\"/> </div>\n\n同时还可以通过 Web UI 查看作业的执行情况，访问端口为 `4040`：\n\n<div align=\"center\"> <img  src=\"../../pictures/spark-shell-web-ui.png\"/> </div>\n\n\n\n\n\n## 三、Scala开发环境配置\n\nSpark 是基于 Scala 语言进行开发的，分别提供了基于 Scala、Java、Python 语言的 API，如果你想使用 Scala 语言进行开发，则需要搭建 Scala 语言的开发环境。\n\n### 3.1 前置条件\n\nScala 的运行依赖于 JDK，所以需要你本机有安装对应版本的 JDK，最新的 Scala 2.12.x 需要 JDK 1.8+。\n\n### 3.2 安装Scala插件\n\nIDEA 默认不支持 Scala 语言的开发，需要通过插件进行扩展。打开 IDEA，依次点击 **File** => **settings**=> **plugins** 选项卡，搜索 Scala 插件 (如下图)。找到插件后进行安装，并重启 IDEA 使得安装生效。\n\n<div align=\"center\"> <img width=\"700px\" src=\"../../pictures/idea-scala-plugin.png\"/> </div>\n\n\n\n### 3.3 创建Scala项目\n\n在 IDEA 中依次点击 **File** => **New** => **Project** 选项卡，然后选择创建 `Scala—IDEA` 工程：\n\n<div align=\"center\"> <img  width=\"700px\"   src=\"../../pictures/idea-newproject-scala.png\"/> </div>\n\n\n\n### 3.4 下载Scala SDK\n\n#### 1. 方式一\n\n此时看到 `Scala SDK` 为空，依次点击 `Create` => `Download` ，选择所需的版本后，点击 `OK` 按钮进行下载，下载完成点击 `Finish` 进入工程。\n\n<div align=\"center\"> <img  width=\"700px\"  src=\"../../pictures/idea-scala-select.png\"/> </div>\n\n\n\n#### 2. 方式二\n\n方式一是 Scala 官方安装指南里使用的方式，但下载速度通常比较慢，且这种安装下并没有直接提供 Scala 命令行工具。所以个人推荐到官网下载安装包进行安装，下载地址：https://www.scala-lang.org/download/\n\n这里我的系统是 Windows，下载 msi 版本的安装包后，一直点击下一步进行安装，安装完成后会自动配置好环境变量。\n\n<div align=\"center\"> <img  width=\"700px\"   src=\"../../pictures/scala-other-resources.png\"/> </div>\n\n\n\n由于安装时已经自动配置好环境变量，所以 IDEA 会自动选择对应版本的 SDK。\n\n<div align=\"center\"> <img  width=\"700px\"  src=\"../../pictures/idea-scala-2.1.8.png\"/> </div>\n\n\n\n### 3.5 创建Hello World\n\n在工程 `src` 目录上右击 **New** => **Scala class** 创建 `Hello.scala`。输入代码如下，完成后点击运行按钮，成功运行则代表搭建成功。\n\n<div align=\"center\"> <img  width=\"700px\"   src=\"../../pictures/scala-hello-world.png\"/> </div>\n\n\n\n\n\n### 3.6 切换Scala版本\n\n在日常的开发中，由于对应软件（如 Spark）的版本切换，可能导致需要切换 Scala 的版本，则可以在 `Project Structures` 中的 `Global Libraries` 选项卡中进行切换。\n\n<div align=\"center\"> <img  width=\"700px\"  src=\"../../pictures/idea-scala-change.png\"/> </div>\n\n\n\n\n\n### 3.7 可能出现的问题\n\n在 IDEA 中有时候重新打开项目后，右击并不会出现新建 `scala` 文件的选项，或者在编写时没有 Scala 语法提示，此时可以先删除 `Global Libraries` 中配置好的 SDK，之后再重新添加：\n\n<div align=\"center\"> <img src=\"../../pictures/scala-sdk.png\"/> </div>\n\n\n\n**另外在 IDEA 中以本地模式运行 Spark 项目是不需要在本机搭建 Spark 和 Hadoop 环境的。**\n\n"
  },
  {
    "path": "大数据框架学习/installation/Spark集群环境搭建.md",
    "content": "# 基于ZooKeeper搭建Spark高可用集群\n\n<nav>\n<a href=\"#一集群规划\">一、集群规划</a><br/>\n<a href=\"#二前置条件\">二、前置条件</a><br/>\n<a href=\"#三Spark集群搭建\">三、Spark集群搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#31-下载解压\">3.1 下载解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#32-配置环境变量\">3.2 配置环境变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#33-集群配置\">3.3 集群配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#34-安装包分发\">3.4 安装包分发</a><br/>\n<a href=\"#四启动集群\">四、启动集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-启动ZooKeeper集群\">4.1 启动ZooKeeper集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-启动Hadoop集群\">4.2 启动Hadoop集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-启动Spark集群\">4.3 启动Spark集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#44-查看服务\">4.4 查看服务</a><br/>\n<a href=\"#五验证集群高可用\">五、验证集群高可用</a><br/>\n<a href=\"#六提交作业\">六、提交作业</a><br/>\n</nav>\n\n\n## 一、集群规划\n\n这里搭建一个 3 节点的 Spark 集群，其中三台主机上均部署 `Worker` 服务。同时为了保证高可用，除了在 hadoop001 上部署主 `Master` 服务外，还在 hadoop002 和 hadoop003 上分别部署备用的 `Master` 服务，Master 服务由 Zookeeper 集群进行协调管理，如果主 `Master` 不可用，则备用 `Master` 会成为新的主 `Master`。\n\n<div align=\"center\"> <img  src=\"../../pictures/spark集群规划.png\"/> </div>\n\n## 二、前置条件\n\n搭建 Spark 集群前，需要保证 JDK 环境、Zookeeper 集群和 Hadoop 集群已经搭建，相关步骤可以参阅：\n\n- [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n- [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md)\n- [Hadoop 集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop集群环境搭建.md)\n\n## 三、Spark集群搭建\n\n### 3.1 下载解压\n\n下载所需版本的 Spark，官网下载地址：http://spark.apache.org/downloads.html\n\n<div align=\"center\"> <img width=\"600px\" src=\"../../pictures/spark-download.png\"/> </div>\n\n\n\n下载后进行解压：\n\n```shell\n# tar -zxvf  spark-2.2.3-bin-hadoop2.6.tgz\n```\n\n\n\n### 3.2 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport SPARK_HOME=/usr/app/spark-2.2.3-bin-hadoop2.6\nexport  PATH=${SPARK_HOME}/bin:$PATH\n```\n\n使得配置的环境变量立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 3.3 集群配置\n\n进入 `${SPARK_HOME}/conf` 目录，拷贝配置样本进行修改：\n\n#### 1. spark-env.sh\n\n```she\n cp spark-env.sh.template spark-env.sh\n```\n\n```shell\n# 配置JDK安装位置\nJAVA_HOME=/usr/java/jdk1.8.0_201\n# 配置hadoop配置文件的位置\nHADOOP_CONF_DIR=/usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop\n# 配置zookeeper地址\nSPARK_DAEMON_JAVA_OPTS=\"-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=hadoop001:2181,hadoop002:2181,hadoop003:2181 -Dspark.deploy.zookeeper.dir=/spark\"\n```\n\n#### 2. slaves\n\n```\ncp slaves.template slaves\n```\n\n配置所有 Woker 节点的位置：\n\n```properties\nhadoop001\nhadoop002\nhadoop003\n```\n\n### 3.4 安装包分发\n\n将 Spark 的安装包分发到其他服务器，分发后建议在这两台服务器上也配置一下 Spark 的环境变量。\n\n```shell\nscp -r /usr/app/spark-2.4.0-bin-hadoop2.6/   hadoop002:usr/app/\nscp -r /usr/app/spark-2.4.0-bin-hadoop2.6/   hadoop003:usr/app/\n```\n\n\n\n## 四、启动集群\n\n### 4.1 启动ZooKeeper集群\n\n分别到三台服务器上启动 ZooKeeper 服务：\n\n```shell\n zkServer.sh start\n```\n\n### 4.2 启动Hadoop集群\n\n```shell\n# 启动dfs服务\nstart-dfs.sh\n# 启动yarn服务\nstart-yarn.sh\n```\n\n### 4.3 启动Spark集群\n\n进入 hadoop001 的 ` ${SPARK_HOME}/sbin` 目录下，执行下面命令启动集群。执行命令后，会在 hadoop001 上启动 `Maser` 服务，会在 `slaves` 配置文件中配置的所有节点上启动 `Worker` 服务。\n\n```shell\nstart-all.sh\n```\n\n分别在 hadoop002 和 hadoop003 上执行下面的命令，启动备用的 `Master` 服务：\n\n```shell\n# ${SPARK_HOME}/sbin 下执行\nstart-master.sh\n```\n\n### 4.4 查看服务\n\n查看 Spark 的 Web-UI 页面，端口为 `8080`。此时可以看到 hadoop001 上的 Master 节点处于 `ALIVE` 状态，并有 3 个可用的 `Worker` 节点。\n\n<div align=\"center\"> <img  src=\"../../pictures/spark-集群搭建1.png\"/> </div>\n\n而 hadoop002 和 hadoop003 上的 Master 节点均处于 `STANDBY` 状态，没有可用的 `Worker` 节点。\n\n<div align=\"center\"> <img  src=\"../../pictures/spark-集群搭建2.png\"/> </div>\n\n<div align=\"center\"> <img  src=\"../../pictures/spark-集群搭建3.png\"/> </div>\n\n\n\n## 五、验证集群高可用\n\n此时可以使用 `kill` 命令杀死 hadoop001 上的 `Master` 进程，此时备用 `Master` 会中会有一个再次成为 ` 主 Master`，我这里是 hadoop002，可以看到 hadoop2 上的 `Master` 经过 `RECOVERING` 后成为了新的主 `Master`，并且获得了全部可以用的 `Workers`。\n\n<div align=\"center\"> <img  src=\"../../pictures/spark-集群搭建4.png\"/> </div>\n\nHadoop002 上的 `Master` 成为主 `Master`，并获得了全部可以用的 `Workers`。\n\n<div align=\"center\"> <img  src=\"../../pictures/spark-集群搭建5.png\"/> </div>\n\n此时如果你再在 hadoop001 上使用 `start-master.sh` 启动 Master 服务，那么其会作为备用 `Master` 存在。\n\n## 六、提交作业\n\n和单机环境下的提交到 Yarn 上的命令完全一致，这里以 Spark 内置的计算 Pi 的样例程序为例，提交命令如下：\n\n```shell\nspark-submit \\\n--class org.apache.spark.examples.SparkPi \\\n--master yarn \\\n--deploy-mode client \\\n--executor-memory 1G \\\n--num-executors 10 \\\n/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \\\n100\n```\n\n"
  },
  {
    "path": "大数据框架学习/installation/Storm单机环境搭建.md",
    "content": "# Storm单机版本环境搭建\n\n### 1. 安装环境要求\n\n> you need to install Storm's dependencies on Nimbus and the worker machines. These are:\n>\n> 1. Java 7+ (Apache Storm 1.x is tested through travis ci against both java 7 and java 8 JDKs)\n> 2. Python 2.6.6 (Python 3.x should work too, but is not tested as part of our CI enviornment)\n\n按照[官方文档](http://storm.apache.org/releases/1.2.2/Setting-up-a-Storm-cluster.html) 的说明：storm 运行依赖于 Java 7+ 和 Python 2.6.6 +，所以需要预先安装这两个软件。由于这两个软件在多个框架中都有依赖，其安装步骤单独整理至  ：\n\n+ [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n\n+ [Linux 环境下 Python 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下Python安装.md)\n\n  \n\n### 2. 下载并解压\n\n下载并解压，官方下载地址：http://storm.apache.org/downloads.html \n\n```shell\n# tar -zxvf apache-storm-1.2.2.tar.gz\n```\n\n### 3. 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport STORM_HOME=/usr/app/apache-storm-1.2.2\nexport PATH=$STORM_HOME/bin:$PATH\n```\n\n使得配置的环境变量生效：\n\n```shell\n# source /etc/profile\n```\n\n\n\n### 4. 启动相关进程\n\n因为要启动多个进程，所以统一采用后台进程的方式启动。进入到 `${STORM_HOME}/bin` 目录下，依次执行下面的命令：\n\n```shell\n# 启动zookeeper\nnohup sh storm dev-zookeeper &\n# 启动主节点 nimbus\nnohup sh storm nimbus &\n# 启动从节点 supervisor \nnohup sh storm supervisor &\n# 启动UI界面 ui  \nnohup sh storm ui &\n# 启动日志查看服务 logviewer \nnohup sh storm logviewer &\n```\n\n\n\n### 5. 验证是否启动成功\n\n验证方式一：jps 查看进程：\n\n```shell\n[root@hadoop001 app]# jps\n1074 nimbus\n1283 Supervisor\n620 dev_zookeeper\n1485 core\n9630 logviewer\n```\n\n验证方式二： 访问 8080 端口，查看 Web-UI 界面：\n\n<div align=\"center\"> <img  src=\"../../pictures/storm-web-ui.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/installation/Storm集群环境搭建.md",
    "content": "# Storm集群环境搭建\n\n<nav>\n<a href=\"#一集群规划\">一、集群规划</a><br/>\n<a href=\"#二前置条件\">二、前置条件</a><br/>\n<a href=\"#三集群搭建\">三、集群搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-下载并解压\">1. 下载并解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-配置环境变量\">2. 配置环境变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3-集群配置\">3. 集群配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#4-安装包分发\">4. 安装包分发</a><br/>\n<a href=\"#四-启动集群\">四. 启动集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#41-启动ZooKeeper集群\">4.1 启动ZooKeeper集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#42-启动Storm集群\">4.2 启动Storm集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#43-查看集群\">4.3 查看集群</a><br/>\n<a href=\"#五高可用验证\">五、高可用验证</a><br/>\n</nav>\n\n\n\n\n\n\n\n## 一、集群规划\n\n这里搭建一个 3 节点的 Storm 集群：三台主机上均部署 `Supervisor` 和 `LogViewer` 服务。同时为了保证高可用，除了在 hadoop001 上部署主 `Nimbus` 服务外，还在 hadoop002 上部署备用的 `Nimbus` 服务。`Nimbus` 服务由 Zookeeper 集群进行协调管理，如果主 `Nimbus` 不可用，则备用 `Nimbus` 会成为新的主 `Nimbus`。\n\n<div align=\"center\"> <img  src=\"../../pictures/storm-集群规划.png\"/> </div>\n\n## 二、前置条件\n\nStorm 运行依赖于 Java 7+ 和 Python 2.6.6 +，所以需要预先安装这两个软件。同时为了保证高可用，这里我们不采用 Storm 内置的 Zookeeper，而采用外置的 Zookeeper 集群。由于这三个软件在多个框架中都有依赖，其安装步骤单独整理至 ：\n\n- [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)\n- [Linux 环境下 Python 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下Python安装.md)\n- [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md)\n\n\n\n## 三、集群搭建\n\n### 1. 下载并解压\n\n下载安装包，之后进行解压。官方下载地址：http://storm.apache.org/downloads.html \n\n```shell\n# 解压\ntar -zxvf apache-storm-1.2.2.tar.gz\n\n```\n\n### 2. 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport STORM_HOME=/usr/app/apache-storm-1.2.2\nexport PATH=$STORM_HOME/bin:$PATH\n```\n\n使得配置的环境变量生效：\n\n```shell\n# source /etc/profile\n```\n\n### 3. 集群配置\n\n修改 `${STORM_HOME}/conf/storm.yaml` 文件，配置如下：\n\n```yaml\n# Zookeeper集群的主机列表\nstorm.zookeeper.servers:\n     - \"hadoop001\"\n     - \"hadoop002\"\n     - \"hadoop003\"\n\n# Nimbus的节点列表\nnimbus.seeds: [\"hadoop001\",\"hadoop002\"]\n\n# Nimbus和Supervisor需要使用本地磁盘上来存储少量状态（如jar包，配置文件等）\nstorm.local.dir: \"/home/storm\"\n\n# workers进程的端口，每个worker进程会使用一个端口来接收消息\nsupervisor.slots.ports:\n     - 6700\n     - 6701\n     - 6702\n     - 6703\n```\n\n`supervisor.slots.ports` 参数用来配置 workers 进程接收消息的端口，默认每个 supervisor 节点上会启动 4 个 worker，当然你也可以按照自己的需要和服务器性能进行设置，假设只想启动 2 个 worker 的话，此处配置 2 个端口即可。\n\n### 4. 安装包分发\n\n将 Storm 的安装包分发到其他服务器，分发后建议在这两台服务器上也配置一下 Storm 的环境变量。\n\n```shell\nscp -r /usr/app/apache-storm-1.2.2/ root@hadoop002:/usr/app/\nscp -r /usr/app/apache-storm-1.2.2/ root@hadoop003:/usr/app/\n```\n\n\n\n## 四. 启动集群\n\n### 4.1 启动ZooKeeper集群\n\n分别到三台服务器上启动 ZooKeeper 服务：\n\n```shell\n zkServer.sh start\n```\n\n### 4.2 启动Storm集群\n\n因为要启动多个进程，所以统一采用后台进程的方式启动。进入到 `${STORM_HOME}/bin` 目录下，执行下面的命令：\n\n**hadoop001 & hadoop002 ：**\n\n```shell\n# 启动主节点 nimbus\nnohup sh storm nimbus &\n# 启动从节点 supervisor \nnohup sh storm supervisor &\n# 启动UI界面 ui  \nnohup sh storm ui &\n# 启动日志查看服务 logviewer \nnohup sh storm logviewer &\n```\n\n**hadoop003 ：**\n\nhadoop003 上只需要启动 `supervisor` 服务和 `logviewer` 服务：\n\n```shell\n# 启动从节点 supervisor \nnohup sh storm supervisor &\n# 启动日志查看服务 logviewer \nnohup sh storm logviewer &\n```\n\n\n\n### 4.3 查看集群\n\n使用 `jps` 查看进程，三台服务器的进程应该分别如下：\n\n<div align=\"center\"> <img  src=\"../../pictures/storm-集群-shell.png\"/> </div>\n\n\n<br/>\n\n访问 hadoop001 或 hadoop002 的 `8080` 端口，界面如下。可以看到有一主一备 2 个 `Nimbus` 和 3 个 `Supervisor`，并且每个 `Supervisor` 有四个 `slots`，即四个可用的 `worker` 进程，此时代表集群已经搭建成功。\n\n<div align=\"center\"> <img  src=\"../../pictures/storm-集群搭建1.png\"/> </div>\n\n\n## 五、高可用验证\n\n这里手动模拟主 `Nimbus` 异常的情况，在 hadoop001 上使用 `kill` 命令杀死 `Nimbus` 的线程，此时可以看到 hadoop001 上的 `Nimbus` 已经处于 `offline` 状态，而 hadoop002 上的 `Nimbus` 则成为新的 `Leader`。\n\n<div align=\"center\"> <img  src=\"../../pictures/storm集群搭建2.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/installation/Zookeeper单机环境和集群环境搭建.md",
    "content": "# Zookeeper单机环境和集群环境搭建\n\n<nav>\n<a href=\"#一单机环境搭建\">一、单机环境搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-下载\">1.1 下载</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-解压\">1.2 解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-配置环境变量\">1.3 配置环境变量</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-修改配置\">1.4 修改配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-启动\">1.5 启动</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#16-验证\">1.6 验证</a><br/>\n<a href=\"#二集群环境搭建\">二、集群环境搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-修改配置\">2.1 修改配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-标识节点\">2.2 标识节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-启动集群\">2.3 启动集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-集群验证\">2.4 集群验证</a><br/>\n</nav>\n\n\n## 一、单机环境搭建\n\n### 1.1 下载\n\n下载对应版本 Zookeeper，这里我下载的版本 `3.4.14`。官方下载地址：https://archive.apache.org/dist/zookeeper/\n\n```shell\n# wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz\n```\n\n### 1.2 解压\n\n```shell\n# tar -zxvf zookeeper-3.4.14.tar.gz\n```\n\n### 1.3 配置环境变量\n\n```shell\n# vim /etc/profile\n```\n\n添加环境变量：\n\n```shell\nexport ZOOKEEPER_HOME=/usr/app/zookeeper-3.4.14\nexport PATH=$ZOOKEEPER_HOME/bin:$PATH\n```\n\n使得配置的环境变量生效：\n\n```shell\n# source /etc/profile\n```\n\n### 1.4 修改配置\n\n进入安装目录的 `conf/` 目录下，拷贝配置样本并进行修改：\n\n```\n# cp zoo_sample.cfg  zoo.cfg\n```\n\n指定数据存储目录和日志文件目录（目录不用预先创建，程序会自动创建），修改后完整配置如下：\n\n```properties\n# The number of milliseconds of each tick\ntickTime=2000\n# The number of ticks that the initial\n# synchronization phase can take\ninitLimit=10\n# The number of ticks that can pass between\n# sending a request and getting an acknowledgement\nsyncLimit=5\n# the directory where the snapshot is stored.\n# do not use /tmp for storage, /tmp here is just\n# example sakes.\ndataDir=/usr/local/zookeeper/data\ndataLogDir=/usr/local/zookeeper/log\n# the port at which the clients will connect\nclientPort=2181\n# the maximum number of client connections.\n# increase this if you need to handle more clients\n#maxClientCnxns=60\n#\n# Be sure to read the maintenance section of the\n# administrator guide before turning on autopurge.\n#\n# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance\n#\n# The number of snapshots to retain in dataDir\n#autopurge.snapRetainCount=3\n# Purge task interval in hours\n# Set to \"0\" to disable auto purge feature\n#autopurge.purgeInterval=1\n```\n\n>配置参数说明：\n>\n>- **tickTime**：用于计算的基础时间单元。比如 session 超时：N*tickTime；\n>- **initLimit**：用于集群，允许从节点连接并同步到 master 节点的初始化连接时间，以 tickTime 的倍数来表示；\n>- **syncLimit**：用于集群， master 主节点与从节点之间发送消息，请求和应答时间长度（心跳机制）；\n>- **dataDir**：数据存储位置；\n>- **dataLogDir**：日志目录；\n>- **clientPort**：用于客户端连接的端口，默认 2181\n\n\n\n### 1.5 启动\n\n由于已经配置过环境变量，直接使用下面命令启动即可：\n\n```\nzkServer.sh start\n```\n\n### 1.6 验证\n\n使用 JPS 验证进程是否已经启动，出现 `QuorumPeerMain` 则代表启动成功。\n\n```shell\n[root@hadoop001 bin]# jps\n3814 QuorumPeerMain\n```\n\n\n\n## 二、集群环境搭建\n\n为保证集群高可用，Zookeeper 集群的节点数最好是奇数，最少有三个节点，所以这里演示搭建一个三个节点的集群。这里我使用三台主机进行搭建，主机名分别为 hadoop001，hadoop002，hadoop003。\n\n### 2.1 修改配置\n\n解压一份 zookeeper 安装包，修改其配置文件 `zoo.cfg`，内容如下。之后使用 scp 命令将安装包分发到三台服务器上：\n\n```shell\ntickTime=2000\ninitLimit=10\nsyncLimit=5\ndataDir=/usr/local/zookeeper-cluster/data/\ndataLogDir=/usr/local/zookeeper-cluster/log/\nclientPort=2181\n\n# server.1 这个1是服务器的标识，可以是任意有效数字，标识这是第几个服务器节点，这个标识要写到dataDir目录下面myid文件里\n# 指名集群间通讯端口和选举端口\nserver.1=hadoop001:2287:3387\nserver.2=hadoop002:2287:3387\nserver.3=hadoop003:2287:3387\n```\n\n### 2.2 标识节点\n\n分别在三台主机的 `dataDir` 目录下新建 `myid` 文件,并写入对应的节点标识。Zookeeper 集群通过 `myid` 文件识别集群节点，并通过上文配置的节点通信端口和选举端口来进行节点通信，选举出 Leader 节点。\n\n创建存储目录：\n\n```shell\n# 三台主机均执行该命令\nmkdir -vp  /usr/local/zookeeper-cluster/data/\n```\n\n创建并写入节点标识到 `myid` 文件：\n\n```shell\n# hadoop001主机\necho \"1\" > /usr/local/zookeeper-cluster/data/myid\n# hadoop002主机\necho \"2\" > /usr/local/zookeeper-cluster/data/myid\n# hadoop003主机\necho \"3\" > /usr/local/zookeeper-cluster/data/myid\n```\n\n### 2.3 启动集群\n\n分别在三台主机上，执行如下命令启动服务：\n\n```shell\n/usr/app/zookeeper-cluster/zookeeper/bin/zkServer.sh start\n```\n\n### 2.4 集群验证\n\n启动后使用 `zkServer.sh status` 查看集群各个节点状态。如图所示：三个节点进程均启动成功，并且 hadoop002 为 leader 节点，hadoop001 和 hadoop003 为 follower 节点。\n\n<div align=\"center\"> <img src=\"../../pictures/zookeeper-hadoop001.png\"/> </div>\n\n<div align=\"center\"> <img src=\"../../pictures/zookeeper-hadoop002.png\"/> </div>\n\n<div align=\"center\"> <img src=\"../../pictures/zookeeper-hadoop003.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/installation/基于Zookeeper搭建Hadoop高可用集群.md",
    "content": "# 基于ZooKeeper搭建Hadoop高可用集群\n\n<nav>\n<a href=\"#一高可用简介\">一、高可用简介</a><br/>\n<a href=\"#二集群规划\">二、集群规划</a><br/>\n<a href=\"#三前置条件\">三、前置条件</a><br/>\n<a href=\"#四集群配置\">四、集群配置</a><br/>\n<a href=\"#五启动集群\">五、启动集群</a><br/>\n<a href=\"#六查看集群\">六、查看集群</a><br/>\n<a href=\"#七集群的二次启动\">七、集群的二次启动</a><br/>\n</nav>\n\n\n\n## 一、高可用简介\n\nHadoop 高可用 (High Availability) 分为 HDFS 高可用和 YARN 高可用，两者的实现基本类似，但 HDFS NameNode 对数据存储及其一致性的要求比 YARN ResourceManger 高得多，所以它的实现也更加复杂，故下面先进行讲解：\n\n### 1.1 高可用整体架构\n\nHDFS 高可用架构如下：\n\n<div align=\"center\"> <img  src=\"../../pictures/HDFS-HA-Architecture-Edureka.png\"/> </div>\n\n> *图片引用自：https://www.edureka.co/blog/how-to-set-up-hadoop-cluster-with-hdfs-high-availability/*\n\nHDFS 高可用架构主要由以下组件所构成：\n\n+ **Active NameNode 和 Standby NameNode**：两台 NameNode 形成互备，一台处于 Active 状态，为主 NameNode，另外一台处于 Standby 状态，为备 NameNode，只有主 NameNode 才能对外提供读写服务。\n\n+ **主备切换控制器 ZKFailoverController**：ZKFailoverController 作为独立的进程运行，对 NameNode 的主备切换进行总体控制。ZKFailoverController 能及时检测到 NameNode 的健康状况，在主 NameNode 故障时借助 Zookeeper 实现自动的主备选举和切换，当然 NameNode 目前也支持不依赖于 Zookeeper 的手动主备切换。\n\n+ **Zookeeper 集群**：为主备切换控制器提供主备选举支持。\n\n+ **共享存储系统**：共享存储系统是实现 NameNode 的高可用最为关键的部分，共享存储系统保存了 NameNode 在运行过程中所产生的 HDFS 的元数据。主 NameNode 和 NameNode 通过共享存储系统实现元数据同步。在进行主备切换的时候，新的主 NameNode 在确认元数据完全同步之后才能继续对外提供服务。\n\n+ **DataNode 节点**：除了通过共享存储系统共享 HDFS 的元数据信息之外，主 NameNode 和备 NameNode 还需要共享 HDFS 的数据块和 DataNode 之间的映射关系。DataNode 会同时向主 NameNode 和备 NameNode 上报数据块的位置信息。\n\n### 1.2 基于 QJM 的共享存储系统的数据同步机制分析\n\n目前 Hadoop 支持使用 Quorum Journal Manager (QJM) 或 Network File System (NFS) 作为共享的存储系统，这里以 QJM 集群为例进行说明：Active NameNode 首先把 EditLog 提交到 JournalNode 集群，然后 Standby NameNode 再从 JournalNode 集群定时同步 EditLog，当 Active NameNode  宕机后， Standby  NameNode 在确认元数据完全同步之后就可以对外提供服务。\n\n需要说明的是向 JournalNode 集群写入 EditLog 是遵循 “过半写入则成功” 的策略，所以你至少要有 3 个 JournalNode 节点，当然你也可以继续增加节点数量，但是应该保证节点总数是奇数。同时如果有 2N+1 台 JournalNode，那么根据过半写的原则，最多可以容忍有 N 台 JournalNode 节点挂掉。\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop-QJM-同步机制.png\"/> </div>\n\n### 1.3 NameNode 主备切换\n\nNameNode 实现主备切换的流程下图所示：\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop-namenode主备切换.png\"/> </div>\n1. HealthMonitor 初始化完成之后会启动内部的线程来定时调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法，对 NameNode 的健康状态进行检测。\n2. HealthMonitor 如果检测到 NameNode 的健康状态发生变化，会回调 ZKFailoverController 注册的相应方法进行处理。\n3. 如果 ZKFailoverController 判断需要进行主备切换，会首先使用 ActiveStandbyElector 来进行自动的主备选举。\n4. ActiveStandbyElector 与 Zookeeper 进行交互完成自动的主备选举。\n5. ActiveStandbyElector 在主备选举完成后，会回调 ZKFailoverController 的相应方法来通知当前的 NameNode 成为主 NameNode 或备 NameNode。\n6. ZKFailoverController 调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法将 NameNode 转换为 Active 状态或 Standby 状态。\n\n   \n### 1.4 YARN高可用\n\nYARN ResourceManager 的高可用与 HDFS NameNode 的高可用类似，但是 ResourceManager 不像 NameNode ，没有那么多的元数据信息需要维护，所以它的状态信息可以直接写到 Zookeeper 上，并依赖 Zookeeper 来进行主备选举。\n\n\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop-rm-ha-overview.png\"/> </div>\n\n\n## 二、集群规划\n\n按照高可用的设计目标：需要保证至少有两个 NameNode (一主一备)  和 两个 ResourceManager (一主一备)  ，同时为满足“过半写入则成功”的原则，需要至少要有 3 个 JournalNode 节点。这里使用三台主机进行搭建，集群规划如下：\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop高可用集群规划.png\"/> </div>\n\n\n## 三、前置条件\n\n+ 所有服务器都安装有 JDK，安装步骤可以参见：[Linux 下 JDK 的安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md)；\n+ 搭建好 ZooKeeper 集群，搭建步骤可以参见：[Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md)\n+ 所有服务器之间都配置好 SSH 免密登录。\n\n\n\n## 四、集群配置\n\n### 4.1 下载并解压\n\n下载 Hadoop。这里我下载的是 CDH 版本 Hadoop，下载地址为：http://archive.cloudera.com/cdh5/cdh/5/\n\n```shell\n# tar -zvxf hadoop-2.6.0-cdh5.15.2.tar.gz \n```\n\n### 4.2 配置环境变量\n\n编辑 `profile` 文件：\n\n```shell\n# vim /etc/profile\n```\n\n增加如下配置：\n\n```\nexport HADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2\nexport  PATH=${HADOOP_HOME}/bin:$PATH\n```\n\n执行 `source` 命令，使得配置立即生效：\n\n```shell\n# source /etc/profile\n```\n\n### 4.3 修改配置\n\n进入 `${HADOOP_HOME}/etc/hadoop` 目录下，修改配置文件。各个配置文件内容如下：\n\n#### 1. hadoop-env.sh\n\n```shell\n# 指定JDK的安装位置\nexport JAVA_HOME=/usr/java/jdk1.8.0_201/\n```\n\n#### 2.  core-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!-- 指定 namenode 的 hdfs 协议文件系统的通信地址 -->\n        <name>fs.defaultFS</name>\n        <value>hdfs://hadoop001:8020</value>\n    </property>\n    <property>\n        <!-- 指定 hadoop 集群存储临时文件的目录 -->\n        <name>hadoop.tmp.dir</name>\n        <value>/home/hadoop/tmp</value>\n    </property>\n    <property>\n        <!-- ZooKeeper 集群的地址 -->\n        <name>ha.zookeeper.quorum</name>\n        <value>hadoop001:2181,hadoop002:2181,hadoop002:2181</value>\n    </property>\n    <property>\n        <!-- ZKFC 连接到 ZooKeeper 超时时长 -->\n        <name>ha.zookeeper.session-timeout.ms</name>\n        <value>10000</value>\n    </property>\n</configuration>\n```\n\n#### 3. hdfs-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!-- 指定 HDFS 副本的数量 -->\n        <name>dfs.replication</name>\n        <value>3</value>\n    </property>\n    <property>\n        <!-- namenode 节点数据（即元数据）的存放位置，可以指定多个目录实现容错，多个目录用逗号分隔 -->\n        <name>dfs.namenode.name.dir</name>\n        <value>/home/hadoop/namenode/data</value>\n    </property>\n    <property>\n        <!-- datanode 节点数据（即数据块）的存放位置 -->\n        <name>dfs.datanode.data.dir</name>\n        <value>/home/hadoop/datanode/data</value>\n    </property>\n    <property>\n        <!-- 集群服务的逻辑名称 -->\n        <name>dfs.nameservices</name>\n        <value>mycluster</value>\n    </property>\n    <property>\n        <!-- NameNode ID 列表-->\n        <name>dfs.ha.namenodes.mycluster</name>\n        <value>nn1,nn2</value>\n    </property>\n    <property>\n        <!-- nn1 的 RPC 通信地址 -->\n        <name>dfs.namenode.rpc-address.mycluster.nn1</name>\n        <value>hadoop001:8020</value>\n    </property>\n    <property>\n        <!-- nn2 的 RPC 通信地址 -->\n        <name>dfs.namenode.rpc-address.mycluster.nn2</name>\n        <value>hadoop002:8020</value>\n    </property>\n    <property>\n        <!-- nn1 的 http 通信地址 -->\n        <name>dfs.namenode.http-address.mycluster.nn1</name>\n        <value>hadoop001:50070</value>\n    </property>\n    <property>\n        <!-- nn2 的 http 通信地址 -->\n        <name>dfs.namenode.http-address.mycluster.nn2</name>\n        <value>hadoop002:50070</value>\n    </property>\n    <property>\n        <!-- NameNode 元数据在 JournalNode 上的共享存储目录 -->\n        <name>dfs.namenode.shared.edits.dir</name>\n        <value>qjournal://hadoop001:8485;hadoop002:8485;hadoop003:8485/mycluster</value>\n    </property>\n    <property>\n        <!-- Journal Edit Files 的存储目录 -->\n        <name>dfs.journalnode.edits.dir</name>\n        <value>/home/hadoop/journalnode/data</value>\n    </property>\n    <property>\n        <!-- 配置隔离机制，确保在任何给定时间只有一个 NameNode 处于活动状态 -->\n        <name>dfs.ha.fencing.methods</name>\n        <value>sshfence</value>\n    </property>\n    <property>\n        <!-- 使用 sshfence 机制时需要 ssh 免密登录 -->\n        <name>dfs.ha.fencing.ssh.private-key-files</name>\n        <value>/root/.ssh/id_rsa</value>\n    </property>\n    <property>\n        <!-- SSH 超时时间 -->\n        <name>dfs.ha.fencing.ssh.connect-timeout</name>\n        <value>30000</value>\n    </property>\n    <property>\n        <!-- 访问代理类，用于确定当前处于 Active 状态的 NameNode -->\n        <name>dfs.client.failover.proxy.provider.mycluster</name>\n        <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>\n    </property>\n    <property>\n        <!-- 开启故障自动转移 -->\n        <name>dfs.ha.automatic-failover.enabled</name>\n        <value>true</value>\n    </property>\n</configuration>\n```\n\n#### 4. yarn-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!--配置 NodeManager 上运行的附属服务。需要配置成 mapreduce_shuffle 后才可以在 Yarn 上运行 MapReduce 程序。-->\n        <name>yarn.nodemanager.aux-services</name>\n        <value>mapreduce_shuffle</value>\n    </property>\n    <property>\n        <!-- 是否启用日志聚合 (可选) -->\n        <name>yarn.log-aggregation-enable</name>\n        <value>true</value>\n    </property>\n    <property>\n        <!-- 聚合日志的保存时间 (可选) -->\n        <name>yarn.log-aggregation.retain-seconds</name>\n        <value>86400</value>\n    </property>\n    <property>\n        <!-- 启用 RM HA -->\n        <name>yarn.resourcemanager.ha.enabled</name>\n        <value>true</value>\n    </property>\n    <property>\n        <!-- RM 集群标识 -->\n        <name>yarn.resourcemanager.cluster-id</name>\n        <value>my-yarn-cluster</value>\n    </property>\n    <property>\n        <!-- RM 的逻辑 ID 列表 -->\n        <name>yarn.resourcemanager.ha.rm-ids</name>\n        <value>rm1,rm2</value>\n    </property>\n    <property>\n        <!-- RM1 的服务地址 -->\n        <name>yarn.resourcemanager.hostname.rm1</name>\n        <value>hadoop002</value>\n    </property>\n    <property>\n        <!-- RM2 的服务地址 -->\n        <name>yarn.resourcemanager.hostname.rm2</name>\n        <value>hadoop003</value>\n    </property>\n    <property>\n        <!-- RM1 Web 应用程序的地址 -->\n        <name>yarn.resourcemanager.webapp.address.rm1</name>\n        <value>hadoop002:8088</value>\n    </property>\n    <property>\n        <!-- RM2 Web 应用程序的地址 -->\n        <name>yarn.resourcemanager.webapp.address.rm2</name>\n        <value>hadoop003:8088</value>\n    </property>\n    <property>\n        <!-- ZooKeeper 集群的地址 -->\n        <name>yarn.resourcemanager.zk-address</name>\n        <value>hadoop001:2181,hadoop002:2181,hadoop003:2181</value>\n    </property>\n    <property>\n        <!-- 启用自动恢复 -->\n        <name>yarn.resourcemanager.recovery.enabled</name>\n        <value>true</value>\n    </property>\n    <property>\n        <!-- 用于进行持久化存储的类 -->\n        <name>yarn.resourcemanager.store.class</name>\n        <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>\n    </property>\n</configuration>\n```\n\n#### 5.  mapred-site.xml\n\n```xml\n<configuration>\n    <property>\n        <!--指定 mapreduce 作业运行在 yarn 上-->\n        <name>mapreduce.framework.name</name>\n        <value>yarn</value>\n    </property>\n</configuration>\n```\n\n#### 5. slaves\n\n配置所有从属节点的主机名或 IP 地址，每行一个。所有从属节点上的 `DataNode` 服务和 `NodeManager` 服务都会被启动。\n\n```properties\nhadoop001\nhadoop002\nhadoop003\n```\n\n### 4.4 分发程序\n\n将 Hadoop 安装包分发到其他两台服务器，分发后建议在这两台服务器上也配置一下 Hadoop 的环境变量。\n\n```shell\n# 将安装包分发到hadoop002\nscp -r /usr/app/hadoop-2.6.0-cdh5.15.2/  hadoop002:/usr/app/\n# 将安装包分发到hadoop003\nscp -r /usr/app/hadoop-2.6.0-cdh5.15.2/  hadoop003:/usr/app/\n```\n\n\n\n## 五、启动集群\n\n### 5.1 启动ZooKeeper\n\n分别到三台服务器上启动 ZooKeeper 服务：\n\n```ssh\n zkServer.sh start\n```\n\n### 5.2 启动Journalnode\n\n分别到三台服务器的 `${HADOOP_HOME}/sbin` 目录下，启动 `journalnode` 进程：\n\n```shell\nhadoop-daemon.sh start journalnode\n```\n\n### 5.3 初始化NameNode\n\n在 `hadop001` 上执行 `NameNode` 初始化命令：\n\n```\nhdfs namenode -format\n```\n\n执行初始化命令后，需要将 `NameNode` 元数据目录的内容，复制到其他未格式化的 `NameNode` 上。元数据存储目录就是我们在 `hdfs-site.xml` 中使用 `dfs.namenode.name.dir` 属性指定的目录。这里我们需要将其复制到 `hadoop002` 上：\n\n```shell\n scp -r /home/hadoop/namenode/data hadoop002:/home/hadoop/namenode/\n```\n\n### 5.4 初始化HA状态\n\n在任意一台 `NameNode` 上使用以下命令来初始化 ZooKeeper 中的 HA 状态：\n\n```shell\nhdfs zkfc -formatZK\n```\n\n### 5.5 启动HDFS\n\n进入到 `hadoop001` 的 `${HADOOP_HOME}/sbin` 目录下，启动 HDFS。此时 `hadoop001` 和 `hadoop002` 上的 `NameNode` 服务，和三台服务器上的 `DataNode` 服务都会被启动：\n\n```shell\nstart-dfs.sh\n```\n\n### 5.6 启动YARN\n\n进入到 `hadoop002` 的 `${HADOOP_HOME}/sbin` 目录下，启动 YARN。此时 `hadoop002` 上的 `ResourceManager` 服务，和三台服务器上的 `NodeManager` 服务都会被启动：\n\n```SHEll\nstart-yarn.sh\n```\n\n需要注意的是，这个时候 `hadoop003` 上的 `ResourceManager` 服务通常是没有启动的，需要手动启动：\n\n```shell\nyarn-daemon.sh start resourcemanager\n```\n\n## 六、查看集群\n\n### 6.1 查看进程\n\n成功启动后，每台服务器上的进程应该如下：\n\n```shell\n[root@hadoop001 sbin]# jps\n4512 DFSZKFailoverController\n3714 JournalNode\n4114 NameNode\n3668 QuorumPeerMain\n5012 DataNode\n4639 NodeManager\n\n\n[root@hadoop002 sbin]# jps\n4499 ResourceManager\n4595 NodeManager\n3465 QuorumPeerMain\n3705 NameNode\n3915 DFSZKFailoverController\n5211 DataNode\n3533 JournalNode\n\n\n[root@hadoop003 sbin]# jps\n3491 JournalNode\n3942 NodeManager\n4102 ResourceManager\n4201 DataNode\n3435 QuorumPeerMain\n```\n\n\n\n### 6.2 查看Web UI\n\nHDFS 和 YARN 的端口号分别为 `50070` 和 `8080`，界面应该如下：\n\n此时 hadoop001 上的 `NameNode` 处于可用状态：\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop高可用集群1.png\"/> </div>\n而 hadoop002 上的 `NameNode` 则处于备用状态：\n\n<br/>\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop高可用集群3.png\"/> </div>\n<br/>\n\nhadoop002 上的 `ResourceManager` 处于可用状态：\n\n<br/>\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop高可用集群4.png\"/> </div>\n<br/>\n\nhadoop003 上的 `ResourceManager` 则处于备用状态：\n\n<br/>\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop高可用集群5.png\"/> </div>\n<br/>\n\n同时界面上也有 `Journal Manager` 的相关信息：\n\n<br/>\n\n<div align=\"center\"> <img  src=\"../../pictures/hadoop高可用集群2.png\"/> </div>\n## 七、集群的二次启动\n\n上面的集群初次启动涉及到一些必要初始化操作，所以过程略显繁琐。但是集群一旦搭建好后，想要再次启用它是比较方便的，步骤如下（首选需要确保 ZooKeeper 集群已经启动）：\n\n在 ` hadoop001` 启动 HDFS，此时会启动所有与 HDFS 高可用相关的服务，包括 NameNode，DataNode 和 JournalNode：\n\n```shell\nstart-dfs.sh\n```\n\n在 `hadoop002` 启动 YARN：\n\n```SHEll\nstart-yarn.sh\n```\n\n这个时候 `hadoop003` 上的 `ResourceManager` 服务通常还是没有启动的，需要手动启动：\n\n```shell\nyarn-daemon.sh start resourcemanager\n```\n\n\n\n\n\n## 参考资料\n\n以上搭建步骤主要参考自官方文档：\n\n+ [HDFS High Availability Using the Quorum Journal Manager](https://hadoop.apache.org/docs/r3.1.2/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html)\n+ [ResourceManager High Availability](https://hadoop.apache.org/docs/r3.1.2/hadoop-yarn/hadoop-yarn-site/ResourceManagerHA.html)\n\n关于 Hadoop 高可用原理的详细分析，推荐阅读：\n\n[Hadoop NameNode 高可用 (High Availability) 实现解析](https://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop-name-node/index.html)\n\n"
  },
  {
    "path": "大数据框架学习/installation/基于Zookeeper搭建Kafka高可用集群.md",
    "content": "# 基于Zookeeper搭建Kafka高可用集群\n\n<nav>\n<a href=\"#一Zookeeper集群搭建\">一、Zookeeper集群搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-下载--解压\">1.1 下载 & 解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-修改配置\">1.2 修改配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-标识节点\">1.3 标识节点</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-启动集群\">1.4 启动集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-集群验证\">1.5 集群验证</a><br/>\n<a href=\"#二Kafka集群搭建\">二、Kafka集群搭建</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-下载解压\">2.1 下载解压</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-拷贝配置文件\">2.2 拷贝配置文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-修改配置\">2.3 修改配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-启动集群\">2.4 启动集群</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#25-创建测试主题\">2.5 创建测试主题</a><br/>\n</nav>\n\n## 一、Zookeeper集群搭建\n\n为保证集群高可用，Zookeeper 集群的节点数最好是奇数，最少有三个节点，所以这里搭建一个三个节点的集群。\n\n### 1.1 下载 & 解压\n\n下载对应版本 Zookeeper，这里我下载的版本 `3.4.14`。官方下载地址：https://archive.apache.org/dist/zookeeper/\n\n```shell\n# 下载\nwget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz\n# 解压\ntar -zxvf zookeeper-3.4.14.tar.gz\n```\n\n### 1.2 修改配置\n\n拷贝三份 zookeeper 安装包。分别进入安装目录的 `conf` 目录，拷贝配置样本 `zoo_sample.cfg ` 为 `zoo.cfg` 并进行修改，修改后三份配置文件内容分别如下：\n\nzookeeper01 配置：\n\n```shell\ntickTime=2000\ninitLimit=10\nsyncLimit=5\ndataDir=/usr/local/zookeeper-cluster/data/01\ndataLogDir=/usr/local/zookeeper-cluster/log/01\nclientPort=2181\n\n# server.1 这个1是服务器的标识，可以是任意有效数字，标识这是第几个服务器节点，这个标识要写到dataDir目录下面myid文件里\n# 指名集群间通讯端口和选举端口\nserver.1=127.0.0.1:2287:3387\nserver.2=127.0.0.1:2288:3388\nserver.3=127.0.0.1:2289:3389\n```\n\n> 如果是多台服务器，则集群中每个节点通讯端口和选举端口可相同，IP 地址修改为每个节点所在主机 IP 即可。\n\nzookeeper02 配置，与 zookeeper01 相比，只有 `dataLogDir` 和 `dataLogDir` 不同：\n\n```shell\ntickTime=2000\ninitLimit=10\nsyncLimit=5\ndataDir=/usr/local/zookeeper-cluster/data/02\ndataLogDir=/usr/local/zookeeper-cluster/log/02\nclientPort=2182\n\nserver.1=127.0.0.1:2287:3387\nserver.2=127.0.0.1:2288:3388\nserver.3=127.0.0.1:2289:3389\n```\n\nzookeeper03 配置，与 zookeeper01，02 相比，也只有 `dataLogDir` 和 `dataLogDir` 不同：\n\n```shell\ntickTime=2000\ninitLimit=10\nsyncLimit=5\ndataDir=/usr/local/zookeeper-cluster/data/03\ndataLogDir=/usr/local/zookeeper-cluster/log/03\nclientPort=2183\n\nserver.1=127.0.0.1:2287:3387\nserver.2=127.0.0.1:2288:3388\nserver.3=127.0.0.1:2289:3389\n```\n\n> 配置参数说明：\n>\n> - **tickTime**：用于计算的基础时间单元。比如 session 超时：N*tickTime；\n> - **initLimit**：用于集群，允许从节点连接并同步到 master 节点的初始化连接时间，以 tickTime 的倍数来表示；\n> - **syncLimit**：用于集群， master 主节点与从节点之间发送消息，请求和应答时间长度（心跳机制）；\n> - **dataDir**：数据存储位置；\n> - **dataLogDir**：日志目录；\n> - **clientPort**：用于客户端连接的端口，默认 2181\n\n\n\n### 1.3 标识节点\n\n分别在三个节点的数据存储目录下新建 `myid` 文件,并写入对应的节点标识。Zookeeper 集群通过 `myid` 文件识别集群节点，并通过上文配置的节点通信端口和选举端口来进行节点通信，选举出 leader 节点。\n\n创建存储目录：\n\n```shell\n# dataDir\nmkdir -vp  /usr/local/zookeeper-cluster/data/01\n# dataDir\nmkdir -vp  /usr/local/zookeeper-cluster/data/02\n# dataDir\nmkdir -vp  /usr/local/zookeeper-cluster/data/03\n```\n\n创建并写入节点标识到 `myid` 文件：\n\n```shell\n#server1\necho \"1\" > /usr/local/zookeeper-cluster/data/01/myid\n#server2\necho \"2\" > /usr/local/zookeeper-cluster/data/02/myid\n#server3\necho \"3\" > /usr/local/zookeeper-cluster/data/03/myid\n```\n\n### 1.4 启动集群\n\n分别启动三个节点：\n\n```shell\n# 启动节点1\n/usr/app/zookeeper-cluster/zookeeper01/bin/zkServer.sh start\n# 启动节点2\n/usr/app/zookeeper-cluster/zookeeper02/bin/zkServer.sh start\n# 启动节点3\n/usr/app/zookeeper-cluster/zookeeper03/bin/zkServer.sh start\n```\n\n### 1.5 集群验证\n\n使用 jps 查看进程，并且使用 `zkServer.sh status` 查看集群各个节点状态。如图三个节点进程均启动成功，并且两个节点为 follower 节点，一个节点为 leader 节点。\n\n<div align=\"center\"> <img  src=\"../../pictures/zookeeper-cluster.png\"/> </div>\n\n\n\n## 二、Kafka集群搭建\n\n### 2.1 下载解压\n\nKafka 安装包官方下载地址：http://kafka.apache.org/downloads ，本用例下载的版本为 `2.2.0`，下载命令：\n\n```shell\n# 下载\nwget https://www-eu.apache.org/dist/kafka/2.2.0/kafka_2.12-2.2.0.tgz\n# 解压\ntar -xzf kafka_2.12-2.2.0.tgz\n```\n\n>这里 j 解释一下 kafka 安装包的命名规则：以 `kafka_2.12-2.2.0.tgz` 为例，前面的 2.12 代表 Scala 的版本号（Kafka 采用 Scala 语言进行开发），后面的 2.2.0 则代表 Kafka 的版本号。\n\n### 2.2 拷贝配置文件\n\n进入解压目录的 ` config` 目录下 ，拷贝三份配置文件：\n\n```shell\n# cp server.properties server-1.properties\n# cp server.properties server-2.properties\n# cp server.properties server-3.properties\n```\n\n### 2.3 修改配置\n\n分别修改三份配置文件中的部分配置，如下：\n\nserver-1.properties：\n\n```properties\n# The id of the broker. 集群中每个节点的唯一标识\nbroker.id=0\n# 监听地址\nlisteners=PLAINTEXT://hadoop001:9092\n# 数据的存储位置\nlog.dirs=/usr/local/kafka-logs/00\n# Zookeeper连接地址\nzookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183\n```\n\nserver-2.properties：\n\n```properties\nbroker.id=1\nlisteners=PLAINTEXT://hadoop001:9093\nlog.dirs=/usr/local/kafka-logs/01\nzookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183\n```\n\nserver-3.properties：\n\n```properties\nbroker.id=2\nlisteners=PLAINTEXT://hadoop001:9094\nlog.dirs=/usr/local/kafka-logs/02\nzookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183\n```\n\n这里需要说明的是 `log.dirs` 指的是数据日志的存储位置，确切的说，就是分区数据的存储位置，而不是程序运行日志的位置。程序运行日志的位置是通过同一目录下的 `log4j.properties` 进行配置的。\n\n### 2.4 启动集群\n\n分别指定不同配置文件，启动三个 Kafka 节点。启动后可以使用 jps 查看进程，此时应该有三个 zookeeper 进程和三个 kafka 进程。\n\n```shell\nbin/kafka-server-start.sh config/server-1.properties\nbin/kafka-server-start.sh config/server-2.properties\nbin/kafka-server-start.sh config/server-3.properties\n```\n\n### 2.5 创建测试主题\n\n创建测试主题：\n\n```shell\nbin/kafka-topics.sh --create --bootstrap-server hadoop001:9092 \\\n\t\t\t\t\t--replication-factor 3 \\\n\t\t\t\t\t--partitions 1 --topic my-replicated-topic\n```\n\n创建后可以使用以下命令查看创建的主题信息：\n\n```shell\nbin/kafka-topics.sh --describe --bootstrap-server hadoop001:9092 --topic my-replicated-topic\n```\n\n<div align=\"center\"> <img src=\"../../pictures/kafka-cluster-shell.png\"/> </div>\n\n\n\n可以看到分区 0 的有 0,1,2 三个副本，且三个副本都是可用副本，都在 ISR(in-sync Replica 同步副本) 列表中，其中 1 为首领副本，此时代表集群已经搭建成功。\n\n\n\n"
  },
  {
    "path": "大数据框架学习/installation/虚拟机静态IP及多IP配置.md",
    "content": "# 虚拟机静态IP及多IP配置\n\n<nav>\n<a href=\"#一虚拟机静态IP配置\">一、虚拟机静态IP配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-编辑网络配置文件\">1. 编辑网络配置文件</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-重启网络服务\">2. 重启网络服务</a><br/>\n<a href=\"#二虚拟机多个静态IP配置\">二、虚拟机多个静态IP配置</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-配置多网卡\">1. 配置多网卡</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-查看网卡名称\">2. 查看网卡名称</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3-配置第二块网卡\">3. 配置第二块网卡</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#4-重启网络服务器\">4. 重启网络服务器</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#5-使用说明\">5. 使用说明</a><br/>\n</nav>\n\n\n\n## 一、虚拟机静态IP配置\n\n### 1. 编辑网络配置文件\n\n```shell\n# vim /etc/sysconfig/network-scripts/ifcfg-enp0s3\n```\n\n添加如下网络配置：\n\n+ IPADDR 需要和宿主机同一个网段；\n+ GATEWAY 保持和宿主机一致；\n\n```properties\nBOOTPROTO=static\nIPADDR=192.168.0.107\nNETMASK=255.255.255.0\nGATEWAY=192.168.0.1\nDNS1=192.168.0.1\nONBOOT=yes\n```\n\n我的主机配置：\n\n<div align=\"center\"> <img  src=\"../../pictures/ipconfig.png\"/> </div>\n\n修改后完整配置如下：\n\n```properties\nTYPE=Ethernet\nPROXY_METHOD=none\nBROWSER_ONLY=no\nBOOTPROTO=static\nIPADDR=192.168.0.107\nNETMASK=255.255.255.0\nGATEWAY=192.168.0.1\nBROADCAST=192.168.0.255\nDNS1=192.168.0.1\nDEFROUTE=yes\nIPV4_FAILURE_FATAL=no\nIPV6INIT=yes\nIPV6_AUTOCONF=yes\nIPV6_DEFROUTE=yes\nIPV6_FAILURE_FATAL=no\nIPV6_ADDR_GEN_MODE=stable-privacy\nNAME=enp0s3\nUUID=03d45df1-8514-4774-9b47-fddd6b9d9fca\nDEVICE=enp0s3\nONBOOT=yes\n```\n\n### 2. 重启网络服务\n\n```shell\n#  systemctl restart network\n```\n\n\n\n## 二、虚拟机多个静态IP配置\n\n如果一台虚拟机需要经常在不同网络环境使用，可以配置多个静态 IP。\n\n### 1. 配置多网卡\n\n这里我是用的虚拟机是 virtualBox，开启多网卡配置方式如下：\n\n<div align=\"center\"> <img  src=\"../../pictures/virtualbox-multi-network.png\"/> </div>\n\n### 2. 查看网卡名称\n\n使用 `ifconfig`，查看第二块网卡名称，这里我的名称为 `enp0s8`：\n\n<div align=\"center\"> <img  src=\"../../pictures/mutli-net-ip.png\"/> </div>\n\n### 3. 配置第二块网卡\n\n开启多网卡后并不会自动生成配置文件，需要拷贝 `ifcfg-enp0s3` 进行修改：\n\n```shell\n# cp ifcfg-enp0s3 ifcfg-enp0s8\n```\n\n静态 IP 配置方法如上，这里不再赘述。除了静态 IP 参数外，以下三个参数还需要修改，UUID 必须与 `ifcfg-enp0s3` 中的不一样：\n\n```properties\nNAME=enp0s8\nUUID=03d45df1-8514-4774-9b47-fddd6b9d9fcb\nDEVICE=enp0s8\n```\n\n### 4. 重启网络服务器\n\n```shell\n#  systemctl restart network\n```\n\n### 5. 使用说明\n\n使用时只需要根据所处的网络环境，勾选对应的网卡即可，不使用的网卡尽量不要勾选启动。\n\n<div align=\"center\"> <img  src=\"../../pictures/virtualbox启用网络.png\"/> </div>\n"
  },
  {
    "path": "大数据框架学习/大数据学习路线.md",
    "content": "# 大数据学习路线\n\n<nav>\n<a href=\"#一大数据处理流程\">一、大数据处理流程</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#11-数据收集\">1.1 数据收集</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#12-数据存储\">1.2 数据存储</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#13-数据分析\">1.3 数据分析</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#14-数据应用\">1.4 数据应用</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#15-其他框架\">1.5 其他框架</a><br/>\n<a href=\"#二学习路线\">二、学习路线</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#21-语言基础\">2.1 语言基础</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#22-Linux-基础\">2.2 Linux 基础</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#23-构建工具\">2.3 构建工具</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#24-框架学习\">2.4 框架学习</a><br/>\n<a href=\"#三开发工具\">三、开发工具</a><br/>\n<a href=\"#四结语\">四、结语</a><br/>\n</nav>\n\n## 一、大数据处理流程\n\n\n<div align=\"center\"> <img  src=\"../pictures/大数据处理简化流程.png\"/> </div>\n上图是一个简化的大数据处理流程图，大数据处理的主要流程包括数据收集、数据存储、数据处理、数据应用等主要环节。下面我们逐一对各个环节所需要的技术栈进行讲解：\n\n### 1.1 数据收集\n\n大数据处理的第一步是数据的收集。现在的中大型项目通常采用微服务架构进行分布式部署，所以数据的采集需要在多台服务器上进行，且采集过程不能影响正常业务的开展。基于这种需求，就衍生了多种日志收集工具，如 Flume 、Logstash、Kibana 等，它们都能通过简单的配置完成复杂的数据收集和数据聚合。\n\n### 1.2 数据存储\n\n收集到数据后，下一个问题就是：数据该如何进行存储？通常大家最为熟知是 MySQL、Oracle 等传统的关系型数据库，它们的优点是能够快速存储结构化的数据，并支持随机访问。但大数据的数据结构通常是半结构化（如日志数据）、甚至是非结构化的（如视频、音频数据），为了解决海量半结构化和非结构化数据的存储，衍生了 Hadoop HDFS 、KFS、GFS 等分布式文件系统，它们都能够支持结构化、半结构和非结构化数据的存储，并可以通过增加机器进行横向扩展。\n\n分布式文件系统完美地解决了海量数据存储的问题，但是一个优秀的数据存储系统需要同时考虑数据存储和访问两方面的问题，比如你希望能够对数据进行随机访问，这是传统的关系型数据库所擅长的，但却不是分布式文件系统所擅长的，那么有没有一种存储方案能够同时兼具分布式文件系统和关系型数据库的优点，基于这种需求，就产生了 HBase、MongoDB。\n\n### 1.3 数据分析\n\n大数据处理最重要的环节就是数据分析，数据分析通常分为两种：批处理和流处理。\n\n+ **批处理**：对一段时间内海量的离线数据进行统一的处理，对应的处理框架有 Hadoop MapReduce、Spark、Flink 等；\n+ **流处理**：对运动中的数据进行处理，即在接收数据的同时就对其进行处理，对应的处理框架有 Storm、Spark Streaming、Flink Streaming 等。\n\n批处理和流处理各有其适用的场景，时间不敏感或者硬件资源有限，可以采用批处理；时间敏感和及时性要求高就可以采用流处理。随着服务器硬件的价格越来越低和大家对及时性的要求越来越高，流处理越来越普遍，如股票价格预测和电商运营数据分析等。\n\n上面的框架都是需要通过编程来进行数据分析，那么如果你不是一个后台工程师，是不是就不能进行数据的分析了？当然不是，大数据是一个非常完善的生态圈，有需求就有解决方案。为了能够让熟悉 SQL 的人员也能够进行数据的分析，查询分析框架应运而生，常用的有 Hive 、Spark SQL 、Flink SQL、 Pig、Phoenix 等。这些框架都能够使用标准的 SQL 或者 类 SQL 语法灵活地进行数据的查询分析。这些 SQL 经过解析优化后转换为对应的作业程序来运行，如 Hive 本质上就是将 SQL 转换为 MapReduce 作业，Spark SQL 将 SQL 转换为一系列的 RDDs 和转换关系（transformations），Phoenix 将 SQL 查询转换为一个或多个 HBase Scan。\n\n### 1.4 数据应用\n\n数据分析完成后，接下来就是数据应用的范畴，这取决于你实际的业务需求。比如你可以将数据进行可视化展现，或者将数据用于优化你的推荐算法，这种运用现在很普遍，比如短视频个性化推荐、电商商品推荐、头条新闻推荐等。当然你也可以将数据用于训练你的机器学习模型，这些都属于其他领域的范畴，都有着对应的框架和技术栈进行处理，这里就不一一赘述。\n\n### 1.5 其他框架\n\n上面是一个标准的大数据处理流程所用到的技术框架。但是实际的大数据处理流程比上面复杂很多，针对大数据处理中的各种复杂问题分别衍生了各类框架：\n\n+ 单机的处理能力都是存在瓶颈的，所以大数据框架都是采用集群模式进行部署，为了更方便的进行集群的部署、监控和管理，衍生了 Ambari、Cloudera Manager 等集群管理工具；\n+ 想要保证集群高可用，需要用到 ZooKeeper ，ZooKeeper 是最常用的分布式协调服务，它能够解决大多数集群问题，包括首领选举、失败恢复、元数据存储及其一致性保证。同时针对集群资源管理的需求，又衍生了 Hadoop YARN ;\n+ 复杂大数据处理的另外一个显著的问题是，如何调度多个复杂的并且彼此之间存在依赖关系的作业？基于这种需求，产生了 Azkaban 和 Oozie 等工作流调度框架；\n+ 大数据流处理中使用的比较多的另外一个框架是 Kafka，它可以用于消峰，避免在秒杀等场景下并发数据对流处理程序造成冲击；\n+ 另一个常用的框架是 Sqoop ，主要是解决了数据迁移的问题，它能够通过简单的命令将关系型数据库中的数据导入到 HDFS 、Hive 或 HBase 中，或者从 HDFS 、Hive 导出到关系型数据库上。\n\n## 二、学习路线\n\n介绍完大数据框架，接着就可以介绍其对应的学习路线了，主要分为以下几个方面：\n\n### 2.1 语言基础\n\n#### 1.  Java\n\n大数据框架大多采用 Java 语言进行开发，并且几乎全部的框架都会提供 Java API 。Java 是目前比较主流的后台开发语言，所以网上免费的学习资源也比较多。如果你习惯通过书本进行学习，这里推荐以下入门书籍：\n\n+ [《Java 编程的逻辑》](https://book.douban.com/subject/30133440/)：这里一本国人编写的系统入门 Java 的书籍，深入浅出，内容全面；\n+ 《Java 核心技术》：目前最新的是第 10 版，有[卷一](https://book.douban.com/subject/26880667/) 和[卷二](https://book.douban.com/subject/27165931/) 两册，卷二可以选择性阅读，因为其中很多章节的内容在实际开发中很少用到。\n\n目前大多数框架要求 Java 版本至少是 1.8，这是由于 Java 1.8 提供了函数式编程，使得可以用更精简的代码来实现之前同样的功能，比如你调用 Spark API，使用 1.8 可能比 1.7 少数倍的代码，所以这里额外推荐阅读 [《Java 8 实战》](https://book.douban.com/subject/26772632/) 这本书籍。\n\n#### 2. Scala\n\nScala 是一门综合了面向对象和函数式编程概念的静态类型的编程语言，它运行在 Java 虚拟机上，可以与所有的 Java 类库无缝协作，著名的 Kafka 就是采用 Scala 语言进行开发的。\n\n为什么需要学习 Scala 语言 ？ 这是因为当前最火的计算框架 Flink 和 Spark 都提供了 Scala 语言的接口，使用它进行开发，比使用 Java 8 所需要的代码更少，且 Spark 就是使用 Scala 语言进行编写的，学习 Scala 可以帮助你更深入的理解 Spark。同样的，对于习惯书本学习的小伙伴，这里推荐两本入门书籍：\n\n- [《快学 Scala(第 2 版)》](https://book.douban.com/subject/27093751/) \n- [《Scala 编程 (第 3 版)》](https://book.douban.com/subject/27591387/) \n\n> 这里说明一下，如果你的时间有限，不一定要学完 Scala 才去学习大数据框架。Scala 确实足够的精简和灵活，但其在语言复杂度上略大于 Java，例如隐式转换和隐式参数等概念在初次涉及时会比较难以理解，所以你可以在了解 Spark 后再去学习 Scala，因为类似隐式转换等概念在 Spark 源码中有大量的运用。\n\n### 2.2 Linux 基础\n\n通常大数据框架都部署在 Linux 服务器上，所以需要具备一定的 Linux 知识。Linux 书籍当中比较著名的是 《鸟哥私房菜》系列，这个系列很全面也很经典。但如果你希望能够快速地入门，这里推荐[《Linux 就该这么学》](https://www.linuxprobe.com/)，其网站上有免费的电子书版本。\n\n### 2.3  构建工具\n\n这里需要掌握的自动化构建工具主要是 Maven。Maven 在大数据场景中使用比较普遍，主要在以下三个方面：\n\n+ 管理项目 JAR 包，帮助你快速构建大数据应用程序；\n+ 不论你的项目是使用 Java 语言还是 Scala 语言进行开发，提交到集群环境运行时，都需要使用 Maven 进行编译打包；\n+ 大部分大数据框架使用 Maven 进行源码管理，当你需要从其源码编译出安装包时，就需要使用到 Maven。\n\n### 2.4 框架学习\n\n#### 1. 框架分类\n\n上面我们介绍了很多大数据框架，这里进行一下分类总结：\n\n**日志收集框架**：Flume 、Logstash、Kibana\n\n**分布式文件存储系统**：Hadoop HDFS\n\n**数据库系统**：Mongodb、HBase\n\n**分布式计算框架**：\n\n+ 批处理框架：Hadoop MapReduce\n+ 流处理框架：Storm\n+ 混合处理框架：Spark、Flink\n\n**查询分析框架**：Hive 、Spark SQL 、Flink SQL、 Pig、Phoenix \n\n**集群资源管理器**：Hadoop YARN\n\n**分布式协调服务**：Zookeeper\n\n**数据迁移工具**：Sqoop\n\n**任务调度框架**：Azkaban、Oozie\n\n**集群部署和监控**：Ambari、Cloudera Manager\n\n上面列出的都是比较主流的大数据框架，社区都很活跃，学习资源也比较丰富。建议从 Hadoop 开始入门学习，因为它是整个大数据生态圈的基石，其它框架都直接或者间接依赖于 Hadoop 。接着就可以学习计算框架，Spark 和 Flink 都是比较主流的混合处理框架，Spark 出现得较早，所以其应用也比较广泛。 Flink 是当下最火热的新一代的混合处理框架，其凭借众多优异的特性得到了众多公司的青睐。两者可以按照你个人喜好或者实际工作需要进行学习。\n\n<div align=\"center\"> <img  src=\"../pictures/HADOOP-ECOSYSTEM-Edureka.png\"/> </div>\n\n> *图片引用自* ：*https://www.edureka.co/blog/hadoop-ecosystem*\n\n至于其它框架，在学习上并没有特定的先后顺序，如果你的学习时间有限，建议初次学习时候，同一类型的框架掌握一种即可，比如日志收集框架就有很多种，初次学习时候只需要掌握一种，能够完成日志收集的任务即可，之后工作上有需要可以再进行针对性地学习。\n\n#### 2.  学习资料\n\n大数据最权威和最全面的学习资料就是官方文档。热门的大数据框架社区都比较活跃、版本更新迭代也比较快，所以其出版物都明显滞后于其实际版本，基于这个原因采用书本学习不是一个最好的方案。比较庆幸的是，大数据框架的官方文档都写的比较好，内容完善，重点突出，同时都采用了大量配图进行辅助讲解。当然也有一些优秀的书籍历经时间的检验，至今依然很经典，这里列出部分个人阅读过的经典书籍：\n\n- [《hadoop 权威指南 (第四版)》](https://book.douban.com/subject/27115351/) 2017 年\n- [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) 2017 年\n- [《从 Paxos 到 Zookeeper  分布式一致性原理与实践》](https://book.douban.com/subject/26292004/)  2015 年\n- [《Spark 技术内幕  深入解析 Spark 内核架构设计与实现原理》](https://book.douban.com/subject/26649141/) 2015 年\n- [《Spark.The.Definitive.Guide》](https://book.douban.com/subject/27035127/) 2018 年\n- [《HBase 权威指南》](https://book.douban.com/subject/10748460/) 2012 年\n- [《Hive 编程指南》](https://book.douban.com/subject/25791255/) 2013 年\n\n#### 3. 视频学习资料\n\n上面我推荐的都是书籍学习资料，很少推荐视频学习资料，这里说明一下原因：因为书籍历经时间的考验，能够再版的或者豆瓣等平台评价高的证明都是被大众所认可的，从概率的角度上来说，其必然更加优秀，不容易浪费大家的学习时间和精力，所以我个人更倾向于官方文档或者书本的学习方式，而不是视频。因为视频学习资料，缺少一个公共的评价平台和完善的评价机制，所以其质量良莠不齐。但是视频任然有其不可替代的好处，学习起来更直观、印象也更深刻，所以对于习惯视频学习的小伙伴，这里我各推荐一个免费的和付费的视频学习资源，大家按需选择：\n\n+ 免费学习资源：尚硅谷大数据学习路线 ——  [下载链接](http://www.atguigu.com/bigdata_video.shtml#bigdata) \\ [在线观看链接](https://space.bilibili.com/302417610/)\n+ 付费学习资源：[慕课网 Michael PK 的系列课程](https://www.imooc.com/t/2781843)\n\n## 三、开发工具\n\n这里推荐一些大数据常用的开发工具：\n\n**Java IDE**：IDEA  和 Eclipse 都可以。从个人使用习惯而言，更倾向于 IDEA ;\n\n**VirtualBox**：在学习过程中，你可能经常要在虚拟机上搭建服务和集群。VirtualBox 是一款开源、免费的虚拟机管理软件，虽然是轻量级软件，但功能很丰富，基本能够满足日常的使用需求；\n\n**MobaXterm**：大数据的框架通常都部署在服务器上，这里推荐使用 MobaXterm 进行连接。同样是免费开源的，支持多种连接协议，支持拖拽上传文件，支持使用插件扩展；\n\n**Translate Man**：一款浏览器上免费的翻译插件 (谷歌和火狐均支持)。它采用谷歌的翻译接口，准确性非常高，支持划词翻译，可以辅助进行官方文档的阅读。\n\n## 四、结语\n\n以上就是个人关于大数据的学习心得和路线推荐。本片文章对大数据技术栈做了比较狭义的限定，随着学习的深入，大家也可以把 Python 语言、推荐系统、机器学习等逐步加入到自己的大数据技术栈中。\n\n"
  },
  {
    "path": "大数据框架学习/大数据常用软件安装指南.md",
    "content": "## 大数据常用软件安装指南\n\n为方便大家查阅，本仓库所有软件的安装方式单独整理如下：\n\n### 一、基础软件安装\n\n1. [Linux 环境下 JDK 安装](installation/Linux下JDK安装.md)\n2. [Linux 环境下 Python 安装](installation/Linux下Python安装.md)\n3. [虚拟机静态 IP 及多 IP 配置](installation/虚拟机静态IP及多IP配置.md)\n\n### 二、Hadoop\n\n1. [Hadoop 单机环境搭建](installation/Hadoop单机环境搭建.md)\n2. [Hadoop 集群环境搭建](installation/Hadoop集群环境搭建.md)\n3. [基于 Zookeeper 搭建 Hadoop 高可用集群](installation/基于Zookeeper搭建Hadoop高可用集群.md)\n\n### 三、Spark\n\n1. [Spark 开发环境搭建](installation/Spark开发环境搭建.md)\n2. [基于 Zookeeper 搭建 Spark 高可用集群](installation/Spark集群环境搭建.md)\n\n\n### 四、Flink \n\n1. [Flink Standalone 集群部署](installation/Flink_Standalone_Cluster.md)\n\n### 五、Storm\n\n1. [Storm 单机环境搭建](installation/Storm单机环境搭建.md)\n2. [Storm 集群环境搭建](installation/Storm集群环境搭建.md)\n\n### 六、HBase\n\n1. [HBase 单机环境搭建](installation/HBase单机环境搭建.md)\n2. [HBase 集群环境搭建](installation/HBase集群环境搭建.md)\n\n### 七、Flume\n\n1. [Linux 环境下 Flume 的安装部署](installation/Linux下Flume的安装.md)\n\n### 八、Azkaban\n\n1. [Azkaban3.x 编译及部署](installation/Azkaban_3.x_编译及部署.md)\n\n### 九、Hive\n\n1. [Linux 环境下 Hive 的安装部署](installation/Linux环境下Hive的安装部署.md)\n\n### 十、Zookeeper\n\n1. [Zookeeper 单机环境和集群环境搭建](installation/Zookeeper单机环境和集群环境搭建.md) \n\n### 十一、Kafka\n\n1. [基于 Zookeeper 搭建 Kafka 高可用集群](installation/基于Zookeeper搭建Kafka高可用集群.md)\n\n\n### 版本说明\n\n由于 Apache Hadoop 原有安装包之间兼容性比较差，所以如无特殊需求，本仓库一律选择 **CDH** (Cloudera's Distribution, including Apache Hadoop) 版本的安装包。它基于稳定版本的 Apache Hadoop 构建，并做了兼容性测试，是目前生产环境中使用最为广泛的版本。\n\n最新的 CDH 5 的下载地址为：http://archive.cloudera.com/cdh5/cdh/5/  。这个页面很大且加载速度比较慢，需要耐心等待页面加载完成。上半部分是文档链接，后半部分才是安装包。同一个 CDH 版本的不同框架间都做了集成测试，可以保证没有任何 JAR 包冲突。安装包包名通常如下所示，这里 CDH 版本都是 `5.15.2`  ，前面是各个软件自己的版本 ，未避免出现不必要的 JAR 包冲突，**请务必保持 CDH 的版本一致**。\n\n```hsell\nhadoop-2.6.0-cdh5.15.2.tar.gz \nhbase-1.2.0-cdh5.15.2\nhive-1.1.0-cdh5.15.2.tar.gz\n```\n"
  },
  {
    "path": "大数据框架学习/大数据应用常用打包方式.md",
    "content": "# 大数据应用常用打包方式\n\n<nav>\n<a href=\"#一简介\">一、简介</a><br/>\n<a href=\"#二mvn-package\">二、mvn package</a><br/>\n<a href=\"#三maven-assembly-plugin插件\">三、maven-assembly-plugin插件</a><br/>\n<a href=\"#四maven-shade-plugin插件\">四、maven-shade-plugin插件</a><br/>\n<a href=\"#五其他打包需求\">五、其他打包需求</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#1-使用非Maven仓库中的Jar\">1. 使用非Maven仓库中的Jar</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#2-排除集群中已经存在的Jar\">2. 排除集群中已经存在的Jar</a><br/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#3-打包Scala文件\">3. 打包Scala文件</a><br/>\n</nav>\n\n\n\n\n\n## 一、简介\n\n在提交大数据作业到集群上运行时，通常需要先将项目打成 JAR 包。这里以 Maven 为例，常用打包方式如下：\n\n- 不加任何插件，直接使用 mvn package 打包；\n- 使用 maven-assembly-plugin 插件；\n- 使用 maven-shade-plugin 插件；\n- 使用 maven-jar-plugin 和 maven-dependency-plugin 插件；\n\n以下分别进行详细的说明。\n\n## 二、mvn package\n\n不在 POM 中配置任何插件，直接使用 `mvn package` 进行项目打包，这对于没有使用外部依赖包的项目是可行的。但如果项目中使用了第三方 JAR 包，就会出现问题，因为 `mvn package` 打的 JAR 包中是不含有依赖包，会导致作业运行时出现找不到第三方依赖的异常。这种方式局限性比较大，因为实际的项目往往很复杂，通常都会依赖第三方 JAR。\n\n大数据框架的开发者也考虑到这个问题，所以基本所有的框架都支持在提交作业时使用 `--jars` 指定第三方依赖包，但是这种方式的问题同样很明显，就是你必须保持生产环境与开发环境中的所有 JAR 包版本一致，这是有维护成本的。\n\n基于上面这些原因，最简单的是采用 `All In One` 的打包方式，把所有依赖都打包到一个 JAR 文件中，此时对环境的依赖性最小。要实现这个目的，可以使用 Maven 提供的 `maven-assembly-plugin` 或 `maven-shade-plugin` 插件。\n\n## 三、maven-assembly-plugin插件\n\n`Assembly` 插件支持将项目的所有依赖、文件都打包到同一个输出文件中。目前支持输出以下文件类型：\n\n- zip\n- tar\n- tar.gz (or tgz)\n- tar.bz2 (or tbz2)\n- tar.snappy\n- tar.xz (or txz)\n- jar\n- dir\n- war\n\n### 3.1 基本使用\n在 POM.xml 中引入插件，指定打包格式的配置文件 `assembly.xml`(名称可自定义)，并指定作业的主入口类：\n\n```xml\n<build>\n    <plugins>\n        <plugin>\n            <artifactId>maven-assembly-plugin</artifactId>\n            <configuration>\n                <descriptors>\n                    <descriptor>src/main/resources/assembly.xml</descriptor>\n                </descriptors>\n                <archive>\n                    <manifest>\n                        <mainClass>com.heibaiying.wordcount.ClusterWordCountApp</mainClass>\n                    </manifest>\n                </archive>\n            </configuration>\n        </plugin>\n    </plugins>\n</build>\n```\n\nassembly.xml 文件内容如下：\n\n```xml\n<assembly xmlns=\"http://maven.apache.org/ASSEMBLY/2.0.0\"\n          xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://maven.apache.org/ASSEMBLY/2.0.0 \n                              http://maven.apache.org/xsd/assembly-2.0.0.xsd\">\n    \n    <id>jar-with-dependencies</id>\n\n    <!--指明打包方式-->\n    <formats>\n        <format>jar</format>\n    </formats>\n\n    <includeBaseDirectory>false</includeBaseDirectory>\n    <dependencySets>\n        <dependencySet>\n            <outputDirectory>/</outputDirectory>\n            <useProjectArtifact>true</useProjectArtifact>\n            <unpack>true</unpack>\n            <scope>runtime</scope>\n            <!--这里以排除 storm 环境中已经提供的 storm-core 为例，演示排除 Jar 包-->\n            <excludes>\n                <exclude>org.apache.storm:storm-core</exclude>\n            </excludes>\n        </dependencySet>\n    </dependencySets>\n</assembly>\n```\n\n### 3.2  打包命令\n\n采用 maven-assembly-plugin 进行打包时命令如下：\n\n```shell\n# mvn assembly:assembly \n```\n\n打包后会同时生成两个 JAR 包，其中后缀为 `jar-with-dependencies` 是含有第三方依赖的 JAR 包，后缀是由 `assembly.xml` 中 `<id>` 标签指定的，可以自定义修改。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-jar.png\"/> </div>\n\n\n\n\n\n## 四、maven-shade-plugin插件\n\n`maven-shade-plugin` 比 `maven-assembly-plugin` 功能更为强大，比如你的工程依赖很多的 JAR 包，而被依赖的 JAR 又会依赖其他的 JAR 包，这样,当工程中依赖到不同的版本的 JAR 时，并且 JAR 中具有相同名称的资源文件时，shade 插件会尝试将所有资源文件打包在一起时，而不是和 assembly 一样执行覆盖操作。\n\n**通常使用 `maven-shade-plugin` 就能够完成大多数的打包需求，其配置简单且适用性最广，因此建议优先使用此方式。**\n\n### 4.1  基本配置\n\n采用 `maven-shade-plugin` 进行打包时候，配置示例如下：\n\n```xml\n<plugin>\n    <groupId>org.apache.maven.plugins</groupId>\n    <artifactId>maven-shade-plugin</artifactId>\n    <configuration>\n        <createDependencyReducedPom>true</createDependencyReducedPom>\n        <filters>\n            <filter>\n                <artifact>*:*</artifact>\n                <excludes>\n                    <exclude>META-INF/*.SF</exclude>\n                    <exclude>META-INF/*.sf</exclude>\n                    <exclude>META-INF/*.DSA</exclude>\n                    <exclude>META-INF/*.dsa</exclude>\n                    <exclude>META-INF/*.RSA</exclude>\n                    <exclude>META-INF/*.rsa</exclude>\n                    <exclude>META-INF/*.EC</exclude>\n                    <exclude>META-INF/*.ec</exclude>\n                    <exclude>META-INF/MSFTSIG.SF</exclude>\n                    <exclude>META-INF/MSFTSIG.RSA</exclude>\n                </excludes>\n            </filter>\n        </filters>\n        <artifactSet>\n            <excludes>\n                <exclude>org.apache.storm:storm-core</exclude>\n            </excludes>\n        </artifactSet>\n    </configuration>\n    <executions>\n        <execution>\n            <phase>package</phase>\n            <goals>\n                <goal>shade</goal>\n            </goals>\n            <configuration>\n                <transformers>\n                    <transformer\n                       implementation=\"org.apache.maven.plugins.shade.resource.ServicesResourceTransformer\"/>\n                    <transformer\n                       implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                    </transformer>\n                </transformers>\n            </configuration>\n        </execution>\n    </executions>\n</plugin>\n```\n\n以上配置来源于 Storm Github，在上面的配置中，排除了部分文件，这是因为有些 JAR 包生成时，会使用 jarsigner 生成文件签名 (完成性校验)，分为两个文件存放在 META-INF 目录下：\n\n- a signature file, with a .SF extension；\n- a signature block file, with a .DSA, .RSA, or .EC extension。\n\n如果某些包的存在重复引用，这可能会导致在打包时候出现 `Invalid signature file digest for Manifest main attributes` 异常，所以在配置中排除这些文件。\n\n### 4.2 打包命令\n\n使用 maven-shade-plugin 进行打包的时候，打包命令和普通打包一样：\n\n```shell\n# mvn package\n```\n\n打包后会生成两个 JAR 包，提交到服务器集群时使用非 original 开头的 JAR。\n\n<div align=\"center\"> <img  src=\"../pictures/storm-jar2.png\"/> </div>\n\n\n\n## 五、其他打包需求\n\n### 1. 使用非Maven仓库中的Jar\n\n通常上面两种打包能够满足大多数的使用场景。但是如果你想把某些没有被 Maven 管理 JAR 包打入到最终的 JAR 中，比如你在 `resources/lib` 下引入的其他非 Maven 仓库中的 JAR，此时可以使用 `maven-jar-plugin` 和 `maven-dependency-plugin` 插件将其打入最终的 JAR 中。\n\n```xml\n<build>\n    <plugins>\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-jar-plugin</artifactId>\n            <configuration>\n                <archive>\n                    <manifest>\n                        <addClasspath>true</addClasspath>\n                          <!--指定 resources/lib 目录-->\n                        <classpathPrefix>lib/</classpathPrefix>\n                          <!--应用的主入口类-->\n                        <mainClass>com.heibaiying.BigDataApp</mainClass>\n                    </manifest>\n                </archive>\n            </configuration>\n        </plugin>\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-dependency-plugin</artifactId>\n            <executions>\n                <execution>\n                    <id>copy</id>\n                    <phase>compile</phase>\n                    <goals>\n                         <!--将 resources/lib 目录所有 Jar 包打进最终的依赖中-->\n                        <goal>copy-dependencies</goal>\n                    </goals>\n                    <configuration>\n                         <!--将 resources/lib 目录所有 Jar 包一并拷贝到输出目录的 lib 目录下-->\n                        <outputDirectory>\n                            ${project.build.directory}/lib\n                        </outputDirectory>\n                    </configuration>\n                </execution>\n            </executions>\n        </plugin>\n    </plugins>\n</build>\n```\n\n### 2. 排除集群中已经存在的Jar\n\n通常为了避免冲突，官方文档都会建议你排除集群中已经提供的 JAR 包，如下：\n\nSpark 官方文档 Submitting Applications 章节: \n\n> When creating assembly jars, list Spark and Hadoop as `provided` dependencies; these need not be bundled since they are provided by the cluster manager at runtime.\n\nStrom 官方文档 Running Topologies on a Production Cluster 章节：\n\n>Then run mvn assembly:assembly to get an appropriately packaged jar. Make sure you exclude the Storm jars since the cluster already has Storm on the classpath.\n\n按照以上说明，排除 JAR 包的方式主要有两种：\n\n+ 对需要排除的依赖添加 `<scope>provided</scope>` 标签，此时该 JAR 包会被排除，但是不建议使用这种方式，因为此时你在本地运行也无法使用该 JAR 包；\n+ 建议直接在 `maven-assembly-plugin` 或 `maven-shade-plugin` 的配置文件中使用 `<exclude>` 进行排除。\n\n### 3. 打包Scala文件\n\n如果你使用到 Scala 语言进行编程，此时需要特别注意 ：默认情况下 Maven 是不会把 `scala` 文件打入最终的 JAR 中，需要额外添加 `maven-scala-plugin` 插件，常用配置如下：\n\n```xml\n<plugin>\n    <groupId>org.scala-tools</groupId>\n    <artifactId>maven-scala-plugin</artifactId>\n    <version>2.15.1</version>\n    <executions>\n        <execution>\n            <id>scala-compile</id>\n            <goals>\n                <goal>compile</goal>\n            </goals>\n            <configuration>\n                <includes>\n                    <include>**/*.scala</include>\n                </includes>\n            </configuration>\n        </execution>\n        <execution>\n            <id>scala-test-compile</id>\n            <goals>\n                <goal>testCompile</goal>\n            </goals>\n        </execution>\n    </executions>\n</plugin>\n```\n\n\n\n## 参考资料\n关于 Maven 各个插件的详细配置可以查看其官方文档：\n+ maven-assembly-plugin : http://maven.apache.org/plugins/maven-assembly-plugin/\n+ maven-shade-plugin : http://maven.apache.org/plugins/maven-shade-plugin/\n+ maven-jar-plugin : http://maven.apache.org/plugins/maven-jar-plugin/\n+ maven-dependency-plugin : http://maven.apache.org/components/plugins/maven-dependency-plugin/\n\n关于 maven-shade-plugin 的更多配置也可以参考该博客：  [maven-shade-plugin 入门指南](https://www.jianshu.com/p/7a0e20b30401)\n"
  },
  {
    "path": "大数据框架学习/大数据技术栈思维导图.md",
    "content": "<div align=\"center\"> <img  src=\"../pictures/大数据技术栈思维导图.png\"/> </div>\n\n"
  },
  {
    "path": "大数据框架学习/资料分享与工具推荐.md",
    "content": "这里分享一些自己学习过程中觉得不错的资料和开发工具。\n\n\n\n## :book: 经典书籍\n\n- [《hadoop 权威指南 (第四版)》](https://book.douban.com/subject/27115351/) 2017 年\n- [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) 2017 年\n- [《从 Paxos 到 Zookeeper  分布式一致性原理与实践》](https://book.douban.com/subject/26292004/)  2015 年\n- [《Spark 技术内幕  深入解析 Spark 内核架构设计与实现原理》](https://book.douban.com/subject/26649141/) 2015 年\n- [《Spark.The.Definitive.Guide》](https://book.douban.com/subject/27035127/) 2018 年\n- [《HBase 权威指南》](https://book.douban.com/subject/10748460/) 2012 年\n- [《Hive 编程指南》](https://book.douban.com/subject/25791255/) 2013 年\n- [《快学 Scala(第 2 版)》](https://book.douban.com/subject/27093751/) 2017 年\n- [《Scala 编程》](https://book.douban.com/subject/27591387/) 2018 年\n\n\n\n## :computer: 官方文档\n\n上面的书籍我都列出了出版日期，可以看到大部分书籍的出版时间都比较久远了，虽然这些书籍比较经典，但是很多书籍在软件版本上已经滞后了很多。所以推荐优先选择各个框架的**官方文档**作为学习资料。大数据框架的官方文档都很全面，并且对知识点的讲解都做到了简明扼要。这里以 [Spark RDD 官方文档](https://spark.apache.org/docs/latest/rdd-programming-guide.html) 为例，你会发现不仅清晰的知识点导航，而且所有示例都给出了 Java，Scala，Python 三种语言的版本，除了官方文档，其他书籍很少能够做到这一点。\n\n\n\n## :orange_book: 优秀博客\n\n- 有态度的 HBase/Spark/BigData：http://hbasefly.com/\n- 深入 Apache Spark 的设计和实现原理 ： https://github.com/JerryLead/SparkInternals\n- Jark's Blog - Flink 系列文章：http://wuchong.me/categories/Flink/ \n\n\n\n## :triangular_ruler:开发工具\n\n#### 1.  VirtualBox\n\n一款开源、免费的虚拟机管理软件，虽然是轻量级软件，但功能很丰富，基本能够满足全部的使用需求。\n\n官方网站：https://www.virtualbox.org/\n\n#### 2. MobaXterm\n\n大数据的框架通常都部署在服务器上，这里推荐使用 MobaXterm 进行连接。同样是免费开源的，支持多种连接协议，支持拖拽上传文件，支持使用插件扩展。\n\n官方网站：https://mobaxterm.mobatek.net/\n\n#### 3. Translate Man\n\nTranslate Man 是一款浏览器上的翻译插件 (谷歌和火狐均支持)。它采用谷歌的翻译接口，准确性非常高，支持划词翻译，可以辅助进行官方文档的阅读。\n\n#### 4. ProcessOn\n\nProcessOn 式一个在线绘图平台，使用起来非常便捷，可以用于笔记或者博客配图的绘制。\n\n官方网站：https://www.processon.com/\n\n"
  },
  {
    "path": "实战系列文章/Flink实战.md",
    "content": "# Flink实战进阶文章合集\n\n1. [菜鸟供应链实时技术架构演进](https://mp.weixin.qq.com/s/fnx2GnbCWNcaptVPsSp7dw)\n2. [趣头条实战-基于Flink+ClickHouse构建实时数据平台](https://mp.weixin.qq.com/s/s6YFOINMw9TKg-QVOkZT9A)\n3. [ApacheFlink新场景-OLAP引擎](https://mp.weixin.qq.com/s/NLwYpjzNgkR8O5zRg7RCoQ)\n4. [说说Flink DataStream的八种物理分区逻辑](https://mp.weixin.qq.com/s/d_jzHb-b7LEGNz1CN34zMg)\n5. [State Processor API：如何读取，写入和修改 Flink 应用程序的状态](https://mp.weixin.qq.com/s/eHPQx3kGKnhXeZpLhUNvng)\n6. [Flink滑动窗口原理与细粒度滑动窗口的性能问题](https://mp.weixin.qq.com/s/Q4k0xgPCOUQ-A2DQ-XaJgw)\n7. [基于Flink快速开发实时TopN](https://mp.weixin.qq.com/s/Ppz5740WTB7lTTLHNL72Tg)\n8. [使用 Apache Flink 开发实时 ETL](https://mp.weixin.qq.com/s/kLjEceslHQxDDi3mRrjR-g)\n9. [Flink Source/Sink探究与实践：RocketMQ数据写入HBase](https://mp.weixin.qq.com/s/kZQRQBVjYiXKfMhKM7SSqQ)\n10. [Spark/Flink广播实现作业配置动态更新](https://mp.weixin.qq.com/s/GDylIWCDnjpX9_X6T9NmMA)\n11. [Flink全链路延迟的测量方式](https://mp.weixin.qq.com/s/A6CIPsGf-aCWXkB7O-toVw)\n12. [Flink原理-Flink中的数据抽象及数据交换过程](https://mp.weixin.qq.com/s/5BlCzguYiEP1h48jwkos2w)\n13. [Flink SQL Window源码全解析](https://mp.weixin.qq.com/s/UkpkS_JiRGR0ibZKYechbg)\n14. [Flink DataStream维度表Join的简单方案](https://mp.weixin.qq.com/s/e-lyViKV4NPmOVwA5Jn6Qw)\n15. [Apache Flink的内存管理](https://mp.weixin.qq.com/s/cBMrF814jGtEFdve0Lrr6g)\n16. [Flink1.9整合Kafka实战](https://mp.weixin.qq.com/s/e0BQoY5Y79NHhcQ9MqltFQ)\n17. [Apache Flink在小米的发展和应用](https://mp.weixin.qq.com/s/KbhmJCW80UmeFwRxM3jerg)\n18. [基于Kafka+Flink+Redis的电商大屏实时计算案例](https://mp.weixin.qq.com/s/BPzOBz7oTfn2_yW8tevEEw)\n19. [Flink实战-壳找房基于Flink的实时平台建设](https://mp.weixin.qq.com/s/TsU_5N0Csfw-afN9AdAihw)\n20. [用Flink取代Spark Streaming！知乎实时数仓架构演进](https://mp.weixin.qq.com/s/0M8XLTgpj6jWNcokNhyxAw)\n21. [Flink实时数仓-美团点评实战](https://mp.weixin.qq.com/s/Oom-TaEsT6GKGs95dJil5Q)\n22. [来将可留姓名？Flink最强学习资源合集!](https://mp.weixin.qq.com/s/13w43iYT3-riIj757HPGxw)\n23. [数据不撒谎，Flink-Kafka性能压测全记录!](https://mp.weixin.qq.com/s/0VXqbzLBj5rZjjf4jAc3UQ)\n24. [菜鸟在物流场景中基于Flink的流计算实践](https://mp.weixin.qq.com/s/2_8uOdDJwzYxUP-NLh6VhA)\n25. [基于Flink构建实时数据仓库](https://mp.weixin.qq.com/s/Rhgt33y102WzR9-Zq15iVQ)\n26. [Flink/Spark 如何实现动态更新作业配置](https://mp.weixin.qq.com/s/sjRV_F9tXEfqKL_00rJc7w)\n27. [Apache Flink结合Apache Kafka实现端到端的一致性语义](https://mp.weixin.qq.com/s/Rza4aozOuTI928542Q8eQA)\n28. [Flink在滴滴出行的应用与实践](https://mp.weixin.qq.com/s/vRwWOf38Ay_f2wfgcCvEQQ)\n29. [批流统一计算引擎的动力源泉—Flink Shuffle机制的重构与优化](https://mp.weixin.qq.com/s/pcr7xn2D5Nrvmsb5zPXJmQ)\n30. [腾讯基于Flink的实时流计算平台演进之路](https://mp.weixin.qq.com/s/CYLrUxYojxpYFlUXhqEesg)\n31. [Flink进阶-Flink CEP(复杂事件处理)](https://mp.weixin.qq.com/s/27sXcAZQJ33gOTfmGCAqPA)\n32. [Flink基于EventTime和WaterMark处理乱序事件和晚到的数据](https://mp.weixin.qq.com/s/Pbzd7aK42bcbvRf40w4EXw)\n33. [Flink 最锋利的武器：Flink SQL 入门和实战](https://mp.weixin.qq.com/s/d-zJ2ZGElARsfxTWb7VW-Q)\n34. [Flink Back Pressure](https://mp.weixin.qq.com/s/xkmLN2OJl_bwyCcgXx6ZeQ)\n35. [最火的实时计算框架Flink和下一代分布式消息队列Pulsar的批流融合](https://mp.weixin.qq.com/s/_5Vxsz7hQytwZYQDZmRdyQ)\n36. [Flink Exactly-Once 投递实现浅析](https://mp.weixin.qq.com/s/tJwS9lGmcJ5uwLJfrsKK-g)\n37. [Flink 网络传输优化技术](https://mp.weixin.qq.com/s/kQsWaqQWjF0agSmrzmpttw)\n38. [Apache Flink 在快手的应用与实践](https://mp.weixin.qq.com/s/c9GhUuDONCtcS_4NaOMGlQ)\n39. [基于Flink构建实时数据仓库](https://mp.weixin.qq.com/s/Rhgt33y102WzR9-Zq15iVQ)\n40. [菜鸟在物流场景中基于Flink的流计算实践](https://mp.weixin.qq.com/s/2_8uOdDJwzYxUP-NLh6VhA)\n41. [数据不撒谎，Flink-Kafka性能压测全记录！](https://mp.weixin.qq.com/s/0VXqbzLBj5rZjjf4jAc3UQ)\n42. [腾讯基于 Flink 的实时流计算平台演进之路](https://mp.weixin.qq.com/s/9UEouSK_yInR1GqoUYhmNQ)\n43. [Flink CEP在哈啰出行的应用](https://mp.weixin.qq.com/s/6_Wco8e1BX0NgrsXMI0haQ)\n44. [基于Flink和Kafka构建批流一体的数据集成平台](https://mp.weixin.qq.com/s/1oB5xyy2MPlfMBUXX3JI4w)\n45. [补齐短板 | 看阿里巴巴如何玩转Flink+Hive](https://mp.weixin.qq.com/s/D4gGgqdfNU8BD7rRScRzGQ)\n46. [Flink实时数仓 | 美团点评实战](https://mp.weixin.qq.com/s/Oom-TaEsT6GKGs95dJil5Q)\n47. [用Flink取代Spark Streaming！知乎实时数仓架构演进](https://mp.weixin.qq.com/s/0M8XLTgpj6jWNcokNhyxAw)\n48. [Flink CheckPoint奇技淫巧 | 原理和在生产中的应用](https://mp.weixin.qq.com/s/FVU2FpMUwkALoreyKSICFw)\n49. [使用 Apache Flink 开发实时ETL](https://mp.weixin.qq.com/s/CwOTkmWaoPuHssgbF1-XKw)\n50. [使用 Kubernetes 部署 Flink 应用](https://mp.weixin.qq.com/s/fnvmcmIq1y_Vkf3X1vgzRQ)\n51. [Flink 实战 | 贝壳找房基于Flink的实时平台建设](https://mp.weixin.qq.com/s/TsU_5N0Csfw-afN9AdAihw)\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "实战系列文章/Kafka实战.md",
    "content": "\n# Kafka实战系列\n\n1. [Apache Kafka简单入门](https://mp.weixin.qq.com/s/6iPfswLE7H_fdN2q6-8zUg)\n2. [Kafka在字节跳动的实践和灾备方案](https://mp.weixin.qq.com/s/ebVvMYCGaEnCxCu7fq9w_g)\n3. [Kafka 最佳实践](https://mp.weixin.qq.com/s/kCKieI66CQhb2ii4nrspAg)\n4. [Kafka Exactly-Once 之事务性实现](https://mp.weixin.qq.com/s/U84JUd-9mGsha4R3uRKFYg)\n5. [Kafka连接器深度解读之错误处理和死信队列](https://mp.weixin.qq.com/s/m4e8NKHoatXi985yKiGKOw)\n6. [基于Kafka+ELK搭建海量日志平台](https://mp.weixin.qq.com/s/RM5wLt8qtJjHX6lwWCIhnA)\n7. [聊聊page cache与Kafka之间的事儿](https://mp.weixin.qq.com/s/-fxhNgBhF6dozUMrBfgwUg)"
  },
  {
    "path": "实战系列文章/OLAP.md",
    "content": "# OLAP 实战和面试系列\n\n1. [Hive使用必知必会系列](https://mp.weixin.qq.com/s/ZICn0wn-M0KC6xDbwX3sKA)\n2. [一个小知识点-Hive行转列实现Pivot](https://mp.weixin.qq.com/s/8SaKs4zcRl17PkjfGDiYEw)\n3. [HBase在滴滴出行的应用场景和最佳实践](https://mp.weixin.qq.com/s/CH2xZJKCNTZyVS3oqY-Wlg)\n4. [Phoenix=HBase+SQL,让HBase插上了翅膀](https://mp.weixin.qq.com/s/-bzrUv1MCEv--j_qK-tSgQ)\n5. [一个知识点将你拒之门外之Hbase的二级索引](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=100001086&idx=1&sn=f553d0e56f6f3c5e9cd37c3788df7be0&chksm=7d3d43ab4a4acabd1e3223af9f8259379add8a9c0981400ac3afca9bc9452eb79032e1af01fd)\n6. [Phoenix重磅 | Phoenix核心功能原理及应用场景介绍](https://mp.weixin.qq.com/s/YLf6W7jC5Hq941ZXZPz2dQ)\n7. [HBase分享 | Flink+HBase场景化解决方案](https://mp.weixin.qq.com/s/2ReDJYbM_ULER2-n5nvzmQ)\n8. [Kudu设计要点面面观](https://mp.weixin.qq.com/s/rGk5tzDRvQJHUvkObLeRBw)\n9. [Apache Kylin VS Apache Doris全方位对比](https://mp.weixin.qq.com/s/3MI6v61a6ElRnVS38-5xkA)\n10. [Apache Kylin 从零开始构建Cube(含优化策略)](https://mp.weixin.qq.com/s/wat5qwvBd3_fv-2-Nv5Jww)\n11. [Druid实时OLAP数据分析存储系统极简入门](https://mp.weixin.qq.com/s/M9bVQ1wET8_O0D4IsyHtBg)\n12. [OLAP红与黑 | 也许你应该考虑一下Druid](https://mp.weixin.qq.com/s/Zb2G5TUHqzeTREoaOmf9wg)\n13. [Hive和Hbase的各自适用场景](https://mp.weixin.qq.com/s/L4kq29lxKtNmXBAnCT92uw)\n14. [你需要的不是实时数仓 | 你需要的是一款强大的OLAP数据库(上)](https://mp.weixin.qq.com/s/9MZ9ztr8fYJTl1HchqtQqA)\n15. [你需要的不是实时数仓 | 你需要的是一款强大的OLAP数据库(下)](https://mp.weixin.qq.com/s/OjlXYxUyClEQibGdCWchuQ)\n16. [Hadoop YARN：调度性能优化实践](https://mp.weixin.qq.com/s/4MBwN8Ak1uPn_0PgZCoMeg)\n17. [HBase实践 | 数据人看Feed流-架构实践](https://mp.weixin.qq.com/s/R762sjEvqcGOSRMzLWR5Qg)\n18. [Apache Kylin 在美团数十亿数据 OLAP 场景下的实践](https://mp.weixin.qq.com/s/Q-zHLGdd2NAtnc8l-RExKg)\n"
  },
  {
    "path": "实战系列文章/Spark实战.md",
    "content": "# Spark实战进阶文章合集\n\n1. [如果你在准备面试，好好看看这130道题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486986&idx=1&sn=422d1a3c11c72ff97b32cc01142839f4&chksm=fd3d489fca4ac1895242ab94b932b12c65dc57b5f3a16acc7084dc8a189e9026290245a64c4f&token=1999457569&lang=zh_CN#rd)\n2. [ORC文件存储格式的深入探究](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486981&idx=1&sn=9c8fc4c127d7e6108ac4e171e750d490&chksm=fd3d4890ca4ac186614f0dda8ffb2d35693a925b03861a01769c898652b53d0d436bca05ea12&token=1999457569&lang=zh_CN#rd)\n3. [基于SparkStreaming+Kafka+HBase实时点击流案例](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486938&idx=1&sn=83347b444fc721442d2b4e1a58eca0e8&chksm=fd3d4b4fca4ac2597abd4a39a21dea83220858ae5efd1ae287b471bb9970f165b4854426f4c2&token=1999457569&lang=zh_CN#rd)\n4. [HyperLogLog函数在Spark中的高级应用](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486931&idx=1&sn=9c3f3d6a677ed2aa6cc508046ca6da78&chksm=fd3d4b46ca4ac25044d6033458d3b43e7d1f50f447bded4eed8b246991cf620c91919c51e35b&token=1999457569&lang=zh_CN#rd)\n5. [我们常说的海量小文件的根源是什么？](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486931&idx=2&sn=1c5987f3bad7a805895484ebfd683e11&chksm=fd3d4b46ca4ac250d31502a76f0cfd02ebea29d1161b9e9faec23ea6aa8947d0dd7d4268427f&token=1999457569&lang=zh_CN#rd)\n6. [Structured Streaming | Apache Spark中处理实时数据的声明式API](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486904&idx=1&sn=5f4b673a87497a9c1dc9d7ce253994f3&chksm=fd3d4b2dca4ac23b9850c54d62ebe8be5920cbd4a491a46a0325e1fbbd91f6b1aef9adf2f638&token=1999457569&lang=zh_CN#rd)\n7. [Spark面对OOM问题的解决方法及优化总结](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486765&idx=1&sn=516a32e8c1e9842606a7670862ec7e97&chksm=fd3d4bb8ca4ac2ae24e315083cdb195fdf897e1c3cce69d2c94183d5c20c1af1bfbe0e5480fd&token=1999457569&lang=zh_CN#rd)\n8. [Spark 动态资源分配(Dynamic Resource Allocation) 解析](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486761&idx=1&sn=959aaa5266307a64181631ba1ae46e86&chksm=fd3d4bbcca4ac2aad8a3958da40c22e565997180c635bb6c32a9b4aa7e82a74a56423320c65a&token=1999457569&lang=zh_CN#rd)\n9. [Apache Spark在海致大数据平台中的优化实践](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486668&idx=1&sn=981028fdcf937ba45b3914e2d48b94db&chksm=fd3d4a59ca4ac34f089ff1dce9542a0f481a73439acec9223fa2d776ff3f2eaec498cc543d06&token=1999457569&lang=zh_CN#rd)\n10. [Spark/Flink广播实现作业配置动态更新](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486644&idx=1&sn=d2637a1e918c2b1be4c9fe3d74f75a92&chksm=fd3d4a21ca4ac3377cc8836939cc041cf934bb57f73b6b618fd1de608495d86e278c1c7e4cdc&token=1999457569&lang=zh_CN#rd)\n11. [Spark SQL读数据库时不支持某些数据类型的问题](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486629&idx=2&sn=54d05858d659756a46b9be36b7b63ee5&chksm=fd3d4a30ca4ac326f69ba9b7cf6a82418afd31193e96aa28346779264a7d8a1df4c7a3a31fba&token=1999457569&lang=zh_CN#rd)\n12. [这个面试问题很难么 | 如何处理大数据中的数据倾斜](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486586&idx=1&sn=e35def6429adaada0910b91eed8d7b1f&chksm=fd3d4aefca4ac3f9918d57b947d4bc8f71afd9d7666ffe28e3660e5efab332c714bef0879a90&token=1999457569&lang=zh_CN#rd)\n13. [Spark难点 | Join的实现原理](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486582&idx=1&sn=7b6291dedb2e6892342e1ed705bdfb2e&chksm=fd3d4ae3ca4ac3f591f297635c0ff8e63e3deb6d6aec72c8fb18e19f8f6fcdb5cd2abcd37d09&token=1999457569&lang=zh_CN#rd)\n14. [面试注意点 | Spark&Flink的区别拾遗](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486577&idx=1&sn=49fd0138ad9837192b6eb78cbdc478e6&chksm=fd3d4ae4ca4ac3f2aec91f40435d1eb55a9e497fe7d8ab9ad52396838f467879ac56cb83c9c2&token=1999457569&lang=zh_CN#rd)\n15. [Spark Checkpoint的运行原理和源码实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486514&idx=1&sn=a3c3084b8a797a0889d7583fb10d70c9&chksm=fd3d4aa7ca4ac3b12416ba8269041032a515b7a4e71bda1709e359a15a04df4f638738c6e01a&token=1999457569&lang=zh_CN#rd)\n16. [阿里云Spark Shuffle的优化](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486505&idx=1&sn=91316d3aa5a99945ccd992c0a98e500d&chksm=fd3d4abcca4ac3aa26f051504244239ff1ab48eb71cf01c14f689eb4767c121b1477d93b60ea&token=1999457569&lang=zh_CN#rd)\n17. [使用Kafka+Spark+Cassandra构建实时处理引擎](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486485&idx=1&sn=999b84e0ac87f2aa1be4870921279a21&chksm=fd3d4a80ca4ac39695d9307582ff58938180d0c5b12f6db84ddae933edb345b0cbe46f6284ed&token=1999457569&lang=zh_CN#rd)\n18. [基于HBase和Spark构建企业级数据处理平台](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486477&idx=1&sn=21394cca8fe279bbc2032d48f65672f6&chksm=fd3d4a98ca4ac38e3bf9700cfe65131fffadbbf9493ccc658b3e7be97ddb6665ad41a70f67f9&token=1999457569&lang=zh_CN#rd)\n19. [SparkSQL在字节跳动的应用实践和优化实战](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486419&idx=1&sn=3bc8af144370a602817ca87415fe525d&chksm=fd3d4d46ca4ac450ec6f1ac84b2f3162071eb85221464bdb1976178ca2363cd2acb3d63d401c&token=1999457569&lang=zh_CN#rd)\n20. [SparkRDD转DataSet/DataFrame的一个深坑](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486369&idx=1&sn=9a760114ed2c7509e191ad29370eecce&chksm=fd3d4d34ca4ac4222d310c37fb67bcaafd5425e1a16c43434c4d1fea403756e266de21b83181&token=1999457569&lang=zh_CN#rd)\n21. [Spark和Flink的状态管理State的区别和应用](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486332&idx=1&sn=ecbe21981c5c36f6c420755e8d63fb8f&chksm=fd3d4de9ca4ac4ff1b11093432f444c9e4f66c45a6e01155d69cface70456fb418a98163757d&token=1999457569&lang=zh_CN#rd)\n22. [Kafka+Spark Streaming管理offset的几种方法](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486328&idx=1&sn=b5d53e0007032114fb277e440e5ce4bf&chksm=fd3d4dedca4ac4fbb5026338af7dbbfe1d845fdc11e7e2279035dddc8ff0b6dd24c32be129ce&token=1999457569&lang=zh_CN#rd)\n23. [从 PageRank Example谈Spark应用程序调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247486322&idx=2&sn=00ddcd16109249e45a70233d5ef959ba&chksm=fd3d4de7ca4ac4f15f85d9a2873c5d1070af3bb929479bd66f7a0dc89ac0777b6960cbce5970&token=1999457569&lang=zh_CN#rd)\n24. [Spark调优|SparkSQL参数调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485574&idx=1&sn=723c02562ee1f44e88c389d6ac8a2c87&chksm=fd3d4e13ca4ac705ad892ada2c68792c906946f271bb1e6afd15a93749f8f87d0ea967ba2d37&token=1999457569&lang=zh_CN#rd)\n25. [Flink/Spark 如何实现动态更新作业配置](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485507&idx=1&sn=2cab7a3714ce4e16351b8edcde95e777&chksm=fd3d4ed6ca4ac7c058d820f36b2d03c3ccddc293e1035b6897e99544e55931f6cd6a2f343647&token=1999457569&lang=zh_CN#rd)\n26. [Stream SQL的执行原理与Flink的实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485361&idx=1&sn=8348203b6f17662a64fa5d412de97296&chksm=fd3d4124ca4ac8329bac00fae705f37c08236d583e9900f268796d463be9323d82ba4b8b1554&token=1999457569&lang=zh_CN#rd)\n27. [Spark将Dataframe数据写入Hive分区表的方案](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485321&idx=1&sn=13e73673fb29bd134ab79e03a369288c&chksm=fd3d411cca4ac80ac68204d18aee55b95cadde7bf18cc2f8a549fc0b6ff4ec4db570190566c3&token=1999457569&lang=zh_CN#rd)\n28. [Spark中几种ShuffleWriter的区别你都知道吗？](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485316&idx=1&sn=8a7a02023f15324885de7a5c93d4dd94&chksm=fd3d4111ca4ac807f34e1fa03023494d46f3770c035290ddefe1333419453a9258f504584aee&token=1999457569&lang=zh_CN#rd)\n29. [SparkSQL的3种Join实现](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485217&idx=1&sn=3ce9fa8ad179c008754873129e51fbe7&chksm=fd3d41b4ca4ac8a25957fc9541437e6546fc2926df2908bad5adbd30a6cecf9f797fef74f894&token=1999457569&lang=zh_CN#rd)\n30. [周期性清除Spark Streaming流状态的方法](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485138&idx=1&sn=8f71070470c8963e7c973b5f10bf3c03&chksm=fd3d4047ca4ac951981f7f0fa08f9f6a1821270441b5008da28115b67020b01ef2936b5f1e88&token=1999457569&lang=zh_CN#rd)\n31. [Structured Streaming之状态存储解析](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485119&idx=1&sn=fd172b1f9c9ef99eac2ed976a7d4459f&chksm=fd3d402aca4ac93ced929dfb9b3d785fa5e00d82939b4ea7ed31e68096b87f31a0d11c5f7414&token=1999457569&lang=zh_CN#rd)\n32. [Spark SQL重点知识总结](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485105&idx=1&sn=0bea228e6845d04739937b75bd2f8d9a&chksm=fd3d4024ca4ac9322cae55569b7bdc6d8546dc9c4583f25801d3820ab3b30094c50816c7c1c8&token=1999457569&lang=zh_CN#rd)\n33. [SparkSQL极简入门](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485058&idx=1&sn=4d3b5c25ca1fdf1f0fb0cd99959d2371&chksm=fd3d4017ca4ac90140442dfc6032346d5841d6705bd92441ad3d0a0366db346752a6b154b976&token=1999457569&lang=zh_CN#rd)\n34. [Spark Shuffle在网易的优化](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247485051&idx=1&sn=a1a70cad450634ceae44d4e14a4fc3ef&chksm=fd3d40eeca4ac9f8901a93271683811da05c62e7a6f1d2825378c32542ef2edf9947273a601a&token=1999457569&lang=zh_CN#rd)\n35. [广告点击数实时统计：Spark StructuredStreaming + Redis Streams](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484999&idx=1&sn=f9cf6eae39bc1d54faaa144357731d2f&chksm=fd3d40d2ca4ac9c4e886bd0208521c45dbab98e9cfd34826e6808d414bcd66f203f0359382e1&token=1999457569&lang=zh_CN#rd)\n36. [Spark内存调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484972&idx=1&sn=ff9a2925c31e07b558504be17937872b&chksm=fd3d40b9ca4ac9afe2cce8ff4a6c50724a146b87d2d05d87d6dcdfb34a177f3be4d5ba79b492&token=1999457569&lang=zh_CN#rd)\n37. [Structured Streaming 实现思路与实现概述](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484959&idx=1&sn=2173f71a32e16b510f047fa716549bc2&chksm=fd3d408aca4ac99cef4ec3079d9c7376c14f457e80f814e1494f5324612b553fb5487783a486&token=1999457569&lang=zh_CN#rd)\n38. [Spark之数据倾斜调优](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484956&idx=1&sn=9182a40fcf1fced04acee81aa9261bfe&chksm=fd3d4089ca4ac99f23952f0d627db4600a81808d98b1a635ae40e06c939e4a229a47d666de47&token=1999457569&lang=zh_CN#rd)\n39. [你不得不知道的知识-零拷贝](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484766&idx=1&sn=8d0aeaa1166a9338df9f28bb47959f4a&chksm=fd3d43cbca4acadd0dfc9e753ca4fe1cc2ca49060d886b359bdf3537809015b04f82b4be27ac&token=1999457569&lang=zh_CN#rd)\n40. [Spark Streaming消费Kafka数据的两种方案](https://mp.weixin.qq.com/s?__biz=MzU3MzgwNTU2Mg==&mid=2247484751&idx=1&sn=11315f599b39eac96c17a78da2fa1258&chksm=fd3d43daca4acaccc624947f5fa84f650e7f61d8638feda67db07cacf51bc985faae74cc94c3&token=1999457569&lang=zh_CN#rd)\n41. [Spark Shuffle在网易的优化](https://mp.weixin.qq.com/s/1WNda357-UtClsVbwWvDfA)\n42. [SparkSQL极简入门](https://mp.weixin.qq.com/s/r6s3a4hqAjxsrRy49aNS_w)\n43. [Spark SQL重点知识总结](https://mp.weixin.qq.com/s/MFWziDtpIjRBXkdgoNQ3Fw)\n44. [Apache Spark3.0什么样？一文读懂Apache Spark最新技术发展与展望](https://mp.weixin.qq.com/s/rOzg23blA6efKLAcMZ18nw)\n45. [Flink/Spark 如何实现动态更新作业配置](https://mp.weixin.qq.com/s/sjRV_F9tXEfqKL_00rJc7w)\n46. [[Spark调优 | Spark SQL参数调优](https://mp.weixin.qq.com/s/jkncoXo5fn_GQtp-eImFiA)]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "实战系列文章/数据仓库.md",
    "content": "# 数据仓库实战合集\n\n1. [基于Hadoop的数据仓库Hive基础知识](https://mp.weixin.qq.com/s/XRhQkry9oHWkgKVxfy-Uog)\n2. [阿里云10 PB+／天的日志系统设计和实现](https://mp.weixin.qq.com/s/Dno8qSqkwKFh9-WSD6Hu8g)\n3. [DB、DW、DM、ODS、OLAP、OLTP和BI的概念理解](https://mp.weixin.qq.com/s/FofS9F-6wDh-t40LxYsYFA)\n4. [从0建设离线数据仓库](https://mp.weixin.qq.com/s/ArnrxdRgslrwvSlrFnzMyw)\n5. [OLTP与OLAP的区别精简总结](https://mp.weixin.qq.com/s/7bZ3EYYpcTGFmQU15yI0Qg)\n6. [Apache Phoenix系列 | 真 · 从入门到精通](https://mp.weixin.qq.com/s/kOuQuHZf-vjGx0A6Q5sPrw)\n7. [大数据平台演进之路 | 淘宝 & 滴滴 & 美团](https://mp.weixin.qq.com/s/veyB1YigCO5M5x51rTZ8rA)\n8. [漫谈数仓五重奏](https://mp.weixin.qq.com/s/JoMt8xfNRZrAzVNdo0T2aw)\n9. [HBase实践 | HBase TB级数据规模不停机迁移最佳实践](https://mp.weixin.qq.com/s/WmBK_2PPAyBShP3AdJNNWA)\n"
  },
  {
    "path": "实战系列文章/面试系列.md",
    "content": "# 面试系列\n\n## 语言和计算机基础\n\n1. [你不得不知道的知识-零拷贝](https://mp.weixin.qq.com/s/zQ0KdPFl34AllB01MHi03A)\n2. [阿里云Redis开发规范](https://mp.weixin.qq.com/s/6a6ydm3CxUQfZtzA4lCIcA)\n3. [面试系列：十个海量数据处理方法大总结](https://mp.weixin.qq.com/s/1IYi-uOWTxhkZcT830jMTg)\n4. [一致性协议浅析：从逻辑时钟到Raft](https://mp.weixin.qq.com/s/U9RtZkyqqGRQP3Y4xO6Icw)\n5. [你确定不来了解一下Redis中字符串的原理吗](https://mp.weixin.qq.com/s/pV1clfZkTXZlcRNlfBU5uA)\n6. [关于Redis的几件小事 | 使用目的与问题及线程模型](https://mp.weixin.qq.com/s/wmuaZfi6K0s3gWix5zft-g)\n7. [关于Redis的几件小事 | Redis的数据类型/过期策略/内存淘汰](https://mp.weixin.qq.com/s/069J8hxfzhZXbV3Ca8seaA)\n8. [关于Redis的几件小事 | 高并发和高可用](https://mp.weixin.qq.com/s/ElFO-OEKsWO08upz8mp_yQ)\n9. [一个细节 | Java中asList的缺陷](https://mp.weixin.qq.com/s/4Q0uQSzuA3KRkacT26iu8w)\n\n\n\n## 离线数据框架面试\n\n1. [面试必备技能-HiveSQL优化](https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzU3MzgwNTU2Mg==&scene=124#wechat_redirect)\n2. [一篇文章入门Hbase](https://mp.weixin.qq.com/s/YhhD9jA7kkJuKM8JLLn-PQ)\n3. [敲黑板：HBase的RowKey设计](https://mp.weixin.qq.com/s/LzPaZ0znIEs34NMbYTHmjA)\n4. [Hive/HiveSQL常用优化方法全面总结](https://mp.weixin.qq.com/s/DfvN7S_00oYw1hqAQDr48g)\n5. []\n\n## 实时计算面试系列\n\n1. [剑谱总纲 | 大数据方向学习面试知识图谱](https://mp.weixin.qq.com/s/mi7ZhIpbgqGi9yu0_nuVTA)\n2. [助力秋招-独孤九剑破剑式 | 10家企业面试真题](https://mp.weixin.qq.com/s/jk6y-uMQeZixBhMItEU_LQ)\n3. [助力秋招-独孤九剑荡剑式 | Java语言&基础面试题]\n\n\n### Flink\n\n1. [你有必要了解一下Flink底层RPC使用的框架和原理](https://mp.weixin.qq.com/s/db7lRwuhLvsrfcfsZ8dpLw)\n2. [昨天面试别人说他熟悉Flink，结果我问了他Flink是如何实现exactly-once语义的？](https://mp.weixin.qq.com/s/G1as9FtfFPCgfOwydglrEQ)\n3. [Flink UDF自动注册实践](https://mp.weixin.qq.com/s/bdIuRKZg2DDfK0P4rPD9TQ)\n4. [Stream SQL的执行原理与Flink的实现](https://mp.weixin.qq.com/s/CAZUzaGnujI6GvoVmOmgkw)\n5. [分布式快照算法: Chandy-Lamport 算法](https://mp.weixin.qq.com/s/lgi_b7s7USsy7pARzp4kMQ)\n6. [科学使用HBase Connection](https://mp.weixin.qq.com/s/ualjrwTX3Df5EgTnkc3Q2Q)\n7. [全网第一份 | Flink学习面试灵魂40问，看看你能答上来几个？](https://mp.weixin.qq.com/s/-J-UZ6vs8BD9sYjdeMOmTQ)\n8. [全网第一 | Flink学习面试灵魂40问答案，文末有福利!](https://mp.weixin.qq.com/s/k26RLt-aWjWv1Ts7XIdscw)\n9. []()\n\n\n### Spark\n\n1. [Spark之数据倾斜调优](https://mp.weixin.qq.com/s/mLi6dQpvv45Ptthvwq67EA)\n2. [Structured Streaming 实现思路与实现概述](https://mp.weixin.qq.com/s/aTq19nQ9NlyZYAch0AyH2A)\n3. [Spark内存调优](https://mp.weixin.qq.com/s/-wMHIZDh0cIDq5RfvlFRwg)\n4. [广告点击数实时统计：Spark StructuredStreaming + Redis Streams](https://mp.weixin.qq.com/s/0a70Bhyc_6PeJMm1wIKuOQ)\n5. [Structured Streaming 之状态存储解析](https://mp.weixin.qq.com/s/YPbry9dpI6iEOJh3wnDQAg)\n6. [周期性清除Spark Streaming流状态的方法](https://mp.weixin.qq.com/s/8EHn7R5OEt2KJCTj2FVznA)\n7. [SparkSQL的3种Join实现](https://mp.weixin.qq.com/s/4EQj_FDXK2znyiHx-H9MtQ)\n8. [Spark将Dataframe数据写入Hive分区表的方案](https://mp.weixin.qq.com/s/dCSUCqvc78Th_UgD6LRGrg)\n9. []\n\n\n\n\n### Kafka\n\n1. [万字长文干货 | Kafka 事务性之幂等性实现](https://mp.weixin.qq.com/s/SQ1Ya-eX4Kt1CVDcbMPhVA)\n2. [一道真实的阿里面试题 | 如何保证消息队列的高可用](https://mp.weixin.qq.com/s/hYfTl8eR2Vkue8-EpgZY7g)\n3. [关于MQ面试的几件小事 | 消息队列的用途、优缺点、技术选型](https://mp.weixin.qq.com/s/yID2OPYk40CzIAxmZEQpvw)\n4. [关于MQ面试的几件小事 | 如何保证消息不丢失](https://mp.weixin.qq.com/s/EaJbOLabVd2YGWznDjGoNQ)\n5. [关于MQ面试的几件小事 | 如何保证消息按顺序执行](https://mp.weixin.qq.com/s/KNrsKLakgOPde2Tmw3viaA)\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(ArrayBlockingQueue).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n\n####**多线程**\n\n###**集合框架**\n\n###**NIO**\n\n###**Java并发容器**\n\n### ArrayBlockingQueue介绍\nArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。\n线程安全是指，ArrayBlockingQueue内部通过“互斥锁”保护竞争资源，实现了多线程对竞争资源的互斥访问。而有界，则是指ArrayBlockingQueue对应的数组是有界限的。 阻塞队列，是指多线程访问竞争资源时，当竞争资源已被某线程获取时，其它要获取该资源的线程需要阻塞等待；而且，ArrayBlockingQueue是按 FIFO（先进先出）原则对元素进行排序，元素都是从尾部插入到队列，从头部开始返回。\n\n\n注意:ArrayBlockingQueue不同于ConcurrentLinkedQueue，ArrayBlockingQueue是数组实现的，并且是有界限的;而ConcurrentLinkedQueue是链表实现的，是无界限的.\n\n### ArrayBlockingQueue原理和数据结构\nArrayBlockingQueue的数据结构，如下图所示：![7adae419f9a6f74c644642c6bda2673b](大数据成神之路-Java高级特性增强(ArrayBlockingQueue).resources/0BA99FFA-FAC7-470F-AB00-4523EDCCFF80.jpg)\n说明：    \n1. ArrayBlockingQueue继承于AbstractQueue，并且它实现了BlockingQueue接口。    \n2. ArrayBlockingQueue内部是通过Object[]数组保存数据的，也就是说ArrayBlockingQueue本质上是通过数组实现的。ArrayBlockingQueue的大小，即数组的容量是创建ArrayBlockingQueue时指定的。    \n3. ArrayBlockingQueue与ReentrantLock是组合关系，ArrayBlockingQueue中包含一个ReentrantLock对象(lock)。ReentrantLock是可重入的互斥锁，ArrayBlockingQueue就是根据该互斥锁实现“多线程对竞争资源的互斥访问”。而且，ReentrantLock分为公平锁和非公平锁，关于具体使用公平锁还是非公平锁，在创建ArrayBlockingQueue时可以指定；而且，ArrayBlockingQueue默认会使用非公平锁。    \n4. ArrayBlockingQueue与Condition是组合关系，ArrayBlockingQueue中包含两个Condition对象(notEmpty和notFull)。而且，Condition又依赖于ArrayBlockingQueue而存在，通过Condition可以实现对ArrayBlockingQueue的更精确的访问 -- (01)若某线程(线程A)要取数据时，数组正好为空，则该线程会执行notEmpty.await()进行等待；当其它某个线程(线程B)向数组中插入了数据之后，会调用notEmpty.signal()唤醒“notEmpty上的等待线程”。此时，线程A会被唤醒从而得以继续运行。(02)若某线程(线程H)要插入数据时，数组已满，则该线程会它执行notFull.await()进行等待；当其它某个线程(线程I)取出数据之后，会调用notFull.signal()唤醒“notFull上的等待线程”。此时，线程H就会被唤醒从而得以继续运行。\n\n### ArrayBlockingQueue函数列表\n```\n// 创建一个带有给定的（固定）容量和默认访问策略的 ArrayBlockingQueue。\nArrayBlockingQueue(int capacity)\n// 创建一个具有给定的（固定）容量和指定访问策略的 ArrayBlockingQueue。\nArrayBlockingQueue(int capacity, boolean fair)\n// 创建一个具有给定的（固定）容量和指定访问策略的 ArrayBlockingQueue，它最初包含给定 collection 的元素，并以 collection 迭代器的遍历顺序添加元素。\nArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c)\n\n// 将指定的元素插入到此队列的尾部（如果立即可行且不会超过该队列的容量），在成功时返回 true，如果此队列已满，则抛出 IllegalStateException。\nboolean add(E e)\n// 自动移除此队列中的所有元素。\nvoid clear()\n// 如果此队列包含指定的元素，则返回 true。\nboolean contains(Object o)\n// 移除此队列中所有可用的元素，并将它们添加到给定 collection 中。\nint drainTo(Collection<? super E> c)\n// 最多从此队列中移除给定数量的可用元素，并将这些元素添加到给定 collection 中。\nint drainTo(Collection<? super E> c, int maxElements)\n// 返回在此队列中的元素上按适当顺序进行迭代的迭代器。\nIterator<E> iterator()\n// 将指定的元素插入到此队列的尾部（如果立即可行且不会超过该队列的容量），在成功时返回 true，如果此队列已满，则返回 false。\nboolean offer(E e)\n// 将指定的元素插入此队列的尾部，如果该队列已满，则在到达指定的等待时间之前等待可用的空间。\nboolean offer(E e, long timeout, TimeUnit unit)\n// 获取但不移除此队列的头；如果此队列为空，则返回 null。\nE peek()\n// 获取并移除此队列的头，如果此队列为空，则返回 null。\nE poll()\n// 获取并移除此队列的头部，在指定的等待时间前等待可用的元素（如果有必要）。\nE poll(long timeout, TimeUnit unit)\n// 将指定的元素插入此队列的尾部，如果该队列已满，则等待可用的空间。\nvoid put(E e)\n// 返回在无阻塞的理想情况下（不存在内存或资源约束）此队列能接受的其他元素数量。\nint remainingCapacity()\n// 从此队列中移除指定元素的单个实例（如果存在）。\nboolean remove(Object o)\n// 返回此队列中元素的数量。\nint size()\n// 获取并移除此队列的头部，在元素变得可用之前一直等待（如果有必要）。\nE take()\n// 返回一个按适当顺序包含此队列中所有元素的数组。\nObject[] toArray()\n// 返回一个按适当顺序包含此队列中所有元素的数组；返回数组的运行时类型是指定数组的运行时类型。\n<T> T[] toArray(T[] a)\n// 返回此 collection 的字符串表示形式。\nString toString()\n```\n### ArrayBlockingQueue源码分析\n下面从ArrayBlockingQueue的创建，添加，取出，遍历这几个方面对ArrayBlockingQueue进行分析。\n**1. 创建**\n下面以ArrayBlockingQueue(int capacity, boolean fair)来进行说明。\n```\npublic ArrayBlockingQueue(int capacity, boolean fair) {\n    if (capacity <= 0)\n        throw new IllegalArgumentException();\n    this.items = new Object[capacity];\n    lock = new ReentrantLock(fair);\n    notEmpty = lock.newCondition();\n    notFull =  lock.newCondition();\n}\n```\n说明：\n(01) items是保存“阻塞队列”数据的数组。它的定义如下：\n```\nfinal Object[] items;\n```\n(02) fair是“可重入的独占锁(ReentrantLock)”的类型。fair为true，表示是公平锁；fair为false，表示是非公平锁。\nnotEmpty和notFull是锁的两个Condition条件。它们的定义如下：\n```\nfinal ReentrantLock lock;\nprivate final Condition notEmpty;\nprivate final Condition notFull;\n```\nLock的作用是提供独占锁机制，来保护竞争资源；而Condition是为了更加精细的对锁进行控制，它依赖于Lock，通过某个条件对多线程进行控制。\nnotEmpty表示“锁的非空条件”。当某线程想从队列中取数据时，而此时又没有数据，则该线程通过notEmpty.await()进行等待；当其它线程向队列中插入了元素之后，就调用notEmpty.signal()唤醒“之前通过notEmpty.await()进入等待状态的线程”。\n同理，notFull表示“锁的满条件”。当某线程想向队列中插入元素，而此时队列已满时，该线程等待；当其它线程从队列中取出元素之后，就唤醒该等待的线程。\n\n**2. 添加**\n\n下面以offer(E e)为例，对ArrayBlockingQueue的添加方法进行说明。\n```\npublic boolean offer(E e) {\n    // 创建插入的元素是否为null，是的话抛出NullPointerException异常\n    checkNotNull(e);\n    // 获取“该阻塞队列的独占锁”\n    final ReentrantLock lock = this.lock;\n    lock.lock();\n    try {\n        // 如果队列已满，则返回false。\n        if (count == items.length)\n            return false;\n        else {\n        // 如果队列未满，则插入e，并返回true。\n            insert(e);\n            return true;\n        }\n    } finally {\n        // 释放锁\n        lock.unlock();\n    }\n}\n```\n说明：offer(E e)的作用是将e插入阻塞队列的尾部。如果队列已满，则返回false，表示插入失败；否则，插入元素，并返回true。(01) count表示”队列中的元素个数“。除此之外，队列中还有另外两个遍历takeIndex和putIndex。takeIndex表示下一个被取出元素的索引，putIndex表示下一个被添加元素的索引。它们的定义如下：\n```\n// 下一个被添加元素的索引\nint takeIndex;\n// 下一个被取出元素的索引\nint putIndex;\n// 队列中的元素个数\nint count;\n```\n(02) insert()的源码如下：\n```\nprivate void insert(E x) {\n    // 将x添加到”队列“中\n    items[putIndex] = x;\n    // 设置”下一个被取出元素的索引“\n    putIndex = inc(putIndex);\n    // 将”队列中的元素个数”+1\n    ++count;\n    // 唤醒notEmpty上的等待线程\n    notEmpty.signal();\n}\n```\ninsert()在插入元素之后，会唤醒notEmpty上面的等待线程。inc()的源码如下：\n```\nfinal int inc(int i) {\n    return (++i == items.length) ? 0 : i;\n}\n```\n若i+1的值等于“队列的长度”，即添加元素之后，队列满；则设置“下一个被添加元素的索引”为0。\n**3. 取出**\n\n下面以take()为例，对ArrayBlockingQueue的取出方法进行说明。\n```\npublic E take() throws InterruptedException {\n    // 获取“队列的独占锁”\n    final ReentrantLock lock = this.lock;\n    // 获取“锁”，若当前线程是中断状态，则抛出InterruptedException异常\n    lock.lockInterruptibly();\n    try {\n        // 若“队列为空”，则一直等待。\n        while (count == 0)\n            notEmpty.await();\n        // 取出元素\n        return extract();\n    } finally {\n        // 释放“锁”\n        lock.unlock();\n    }\n}\n```\n说明：take()的作用是取出并返回队列的头。若队列为空，则一直等待。\nextract()的源码如下：\n```\nprivate E extract() {\n    final Object[] items = this.items;\n    // 强制将元素转换为“泛型E”\n    E x = this.<E>cast(items[takeIndex]);\n    // 将第takeIndex元素设为null，即删除。同时，帮助GC回收。\n    items[takeIndex] = null;\n    // 设置“下一个被取出元素的索引”\n    takeIndex = inc(takeIndex);\n    // 将“队列中元素数量”-1\n    --count;\n    // 唤醒notFull上的等待线程。\n    notFull.signal();\n    return x;\n}\n```\n说明：extract()在删除元素之后，会唤醒notFull上的等待线程。\n\n**4. 遍历**\n下面对ArrayBlockingQueue的遍历方法进行说明。\n```\npublic Iterator<E> iterator() {\n    return new Itr();\n}\n```\nItr是实现了Iterator接口的类，它的源码如下：\n```\nprivate class Itr implements Iterator<E> {\n    // 队列中剩余元素的个数\n    private int remaining; // Number of elements yet to be returned\n    // 下一次调用next()返回的元素的索引\n    private int nextIndex; // Index of element to be returned by next\n    // 下一次调用next()返回的元素\n    private E nextItem;    // Element to be returned by next call to next\n    // 上一次调用next()返回的元素\n    private E lastItem;    // Element returned by last call to next\n    // 上一次调用next()返回的元素的索引\n    private int lastRet;   // Index of last element returned, or -1 if none\n\n    Itr() {\n        // 获取“阻塞队列”的锁\n        final ReentrantLock lock = ArrayBlockingQueue.this.lock;\n        lock.lock();\n        try {\n            lastRet = -1;\n            if ((remaining = count) > 0)\n                nextItem = itemAt(nextIndex = takeIndex);\n        } finally {\n            // 释放“锁”\n            lock.unlock();\n        }\n    }\n\n    public boolean hasNext() {\n        return remaining > 0;\n    }\n\n    public E next() {\n        // 获取“阻塞队列”的锁\n        final ReentrantLock lock = ArrayBlockingQueue.this.lock;\n        lock.lock();\n        try {\n            // 若“剩余元素<=0”，则抛出异常。\n            if (remaining <= 0)\n                throw new NoSuchElementException();\n            lastRet = nextIndex;\n            // 获取第nextIndex位置的元素\n            E x = itemAt(nextIndex);  // check for fresher value\n            if (x == null) {\n                x = nextItem;         // we are forced to report old value\n                lastItem = null;      // but ensure remove fails\n            }\n            else\n                lastItem = x;\n            while (--remaining > 0 && // skip over nulls\n                   (nextItem = itemAt(nextIndex = inc(nextIndex))) == null)\n                ;\n            return x;\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    public void remove() {\n        final ReentrantLock lock = ArrayBlockingQueue.this.lock;\n        lock.lock();\n        try {\n            int i = lastRet;\n            if (i == -1)\n                throw new IllegalStateException();\n            lastRet = -1;\n            E x = lastItem;\n            lastItem = null;\n            // only remove if item still at index\n            if (x != null && x == items[i]) {\n                boolean removingHead = (i == takeIndex);\n                removeAt(i);\n                if (!removingHead)\n                    nextIndex = dec(nextIndex);\n            }\n        } finally {\n            lock.unlock();\n        }\n    }\n}\n```\n### ArrayBlockingQueue示例\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n/*\n *   ArrayBlockingQueue是“线程安全”的队列，而LinkedList是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历queue”的示例\n *   (01) 当queue是ArrayBlockingQueue对象时，程序能正常运行。\n *   (02) 当queue是LinkedList对象时，程序会产生ConcurrentModificationException异常。\n */\npublic class ArrayBlockingQueueDemo1{\n    // TODO: queue是LinkedList对象时，程序会出错。\n    //private static Queue<String> queue = new LinkedList<String>();\n    private static Queue<String> queue = new ArrayBlockingQueue<String>(20);\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对queue进行操作！\n        new MyThread(\"ta\").start();\n        new MyThread(\"tb\").start();\n    }\n\n    private static void printAll() {\n        String value;\n        Iterator iter = queue.iterator();\n        while(iter.hasNext()) {\n            value = (String)iter.next();\n            System.out.print(value+\", \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 6) {\n                // “线程名” + \"-\" + \"序号\"\n                String val = Thread.currentThread().getName()+i;\n                queue.add(val);\n                // 通过“Iterator”遍历queue。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\nta1, ta1, \ntb1, ta1, \ntb1, ta1, ta2, \ntb1, ta1, ta2, tb1, tb2, \nta2, ta1, tb2, tb1, ta3, \nta2, ta1, tb2, tb1, ta3, ta2, tb3, \ntb2, ta1, ta3, tb1, tb3, ta2, ta4, \ntb2, ta1, ta3, tb1, tb3, ta2, ta4, tb2, tb4, \nta3, ta1, tb3, tb1, ta4, ta2, tb4, tb2, ta5, \nta3, ta1, tb3, tb1, ta4, ta2, tb4, tb2, ta5, ta3, tb5, \ntb3, ta1, ta4, tb1, tb4, ta2, ta5, tb2, tb5, ta3, ta6, \ntb3, ta4, tb4, ta5, tb5, ta6, tb6,\n```\n结果说明：如果将源码中的queue改成LinkedList对象时，程序会产生ConcurrentModificationException异常。\n"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(ConcurrentHashMap).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n\n####**多线程** \n###**集合框架** \n###**NIO** \n###**Java并发容器** \n```\n文章出自:https://www.jianshu.com/p/c0642afe03e0\n```\n#### 前言\nHashMap是我们平时开发过程中用的比较多的集合，但它是非线程安全的，在涉及到多线程并发的情况，进行get操作有可能会引起死循环，导致CPU利用率接近100%。\n```\n\nfinal HashMap<String, String> map = new HashMap<String, String>(2);\nfor (int i = 0; i < 10000; i++) {\n    new Thread(new Runnable() {\n        @Override\n        public void run() {\n            map.put(UUID.randomUUID().toString(), \"\");\n        }\n    }).start();\n}\n```\n解决方案有Hashtable和Collections.synchronizedMap(hashMap)，不过这两个方案基本上是对读写进行加锁操作，一个线程在读写元素，其余线程必须等待，性能可想而知。\n#### JDK1.7分析\nConcurrentHashMap采用 分段锁的机制，实现并发的更新操作，底层采用数组+链表的存储结构。\n其包含两个核心静态内部类 Segment和HashEntry。\n\nSegment继承ReentrantLock用来充当锁的角色，每个 Segment 对象守护每个散列映射表的若干个桶。\nHashEntry 用来封装映射表的键/值对；\n每个桶是由若干个 HashEntry 对象链接起来的链表。\n一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组，下面我们通过一个图来演示一下 ConcurrentHashMap 的结构：\n![68c2784b9b62e5bfb40a1db079754bde](大数据成神之路-Java高级特性增强(ConcurrentHashMap).resources/4C9E2E7A-9471-46E4-BC40-3F7803EA8C2B.png)\n#### JDK1.8分析\n1.8的实现已经抛弃了Segment分段锁机制，利用CAS+Synchronized来保证并发更新的安全，底层采用数组+链表+红黑树的存储结构。\n![d6f12bc8a40be41b048307d46102a014](大数据成神之路-Java高级特性增强(ConcurrentHashMap).resources/33571F0A-5AF5-4191-AC11-06AC6A8D8E03.png)\n**重要概念**\n在开始之前，有些重要的概念需要介绍一下:\n\n* table:默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。\n* nextTable:默认为null，扩容时新生成的数组，其大小为原数组的两倍。\n* sizeCtl:默认为0，用来控制table的初始化和扩容操作，具体应用在后续会体现出来。\n* -1代表table正在初始化\n* -N表示有N-1个线程正在进行扩容操作\n\n* 其余情况：\n    1、如果table未初始化，表示table需要初始化的大小。\n    2、如果table初始化完成，表示table的容量，默认是table大小的0.75倍，居然用这个公式算0.75（n - (n >>> 2)）。\n* Node：保存key，value及key的hash值的数据结构。\n\n```\nclass Node<K,V> implements Map.Entry<K,V> {\n    final int hash;\n    final K key;\n    volatile V val;\n    volatile Node<K,V> next;\n    ... 省略部分代码\n}\n```\n其中value和next都用volatile修饰，保证并发的可见性。\n\n* ForwardingNode：一个特殊的Node节点，hash值为-1，其中存储nextTable的引用。\n```\nfinal class ForwardingNode<K,V> extends Node<K,V> {\n    final Node<K,V>[] nextTable;\n    ForwardingNode(Node<K,V>[] tab) {\n        super(MOVED, null, null, null);\n        this.nextTable = tab;\n    }\n}\n```\n只有table发生扩容的时候，ForwardingNode才会发挥作用，作为一个占位符放在table中表示当前节点为null或则已经被移动。\n**实例初始化**\n实例化ConcurrentHashMap时带参数时，会根据参数调整table的大小，假设参数为100，最终会调整成256，确保table的大小总是2的幂次方，算法如下：\n```\nConcurrentHashMap<String, String> hashMap = new ConcurrentHashMap<>(100);\nprivate static final int tableSizeFor(int c) {\n    int n = c - 1;\n    n |= n >>> 1;\n    n |= n >>> 2;\n    n |= n >>> 4;\n    n |= n >>> 8;\n    n |= n >>> 16;\n    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;\n}\n```\n注意，ConcurrentHashMap在构造函数中只会初始化sizeCtl值，并不会直接初始化table，而是延缓到第一次put操作。\n**table初始化**\n前面已经提到过，table初始化操作会延缓到第一次put行为。但是put是可以并发执行的，Doug Lea是如何实现table只初始化一次的？让我们来看看源码的实现。\n```\nprivate final Node<K,V>[] initTable() {\n    Node<K,V>[] tab; int sc;\n    while ((tab = table) == null || tab.length == 0) {\n//如果一个线程发现sizeCtl<0，意味着另外的线程执行CAS操作成功，当前线程只需要让出cpu时间片\n        if ((sc = sizeCtl) < 0) \n            Thread.yield(); // lost initialization race; just spin\n        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {\n            try {\n                if ((tab = table) == null || tab.length == 0) {\n                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;\n                    @SuppressWarnings(\"unchecked\")\n                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];\n                    table = tab = nt;\n                    sc = n - (n >>> 2);\n                }\n            } finally {\n                sizeCtl = sc;\n            }\n            break;\n        }\n    }\n    return tab;\n}\n```\nsizeCtl默认为0，如果ConcurrentHashMap实例化时有传参数，sizeCtl会是一个2的幂次方的值。所以执行第一次put操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl为-1，有且只有一个线程能够修改成功，其它线程通过Thread.yield()让出CPU时间片等待table初始化完成。\n**put操作**\n假设table已经初始化完成，put操作采用CAS+synchronized实现并发插入或更新操作，具体实现如下。\n```\nfinal V putVal(K key, V value, boolean onlyIfAbsent) {\n    if (key == null || value == null) throw new NullPointerException();\n    int hash = spread(key.hashCode());\n    int binCount = 0;\n    for (Node<K,V>[] tab = table;;) {\n        Node<K,V> f; int n, i, fh;\n        if (tab == null || (n = tab.length) == 0)\n            tab = initTable();\n        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {\n            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))\n                break;                   // no lock when adding to empty bin\n        }\n        else if ((fh = f.hash) == MOVED)\n            tab = helpTransfer(tab, f);\n        ...省略部分代码\n    }\n    addCount(1L, binCount);\n    return null;\n}\n```\n**hash算法**\n```\nstatic final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;}\n```\ntable中定位索引位置，n是table的大小\n```\nint index = (n - 1) & hash\n```\n**获取table中对应索引的元素f:**\nDoug Lea采用Unsafe.getObjectVolatile来获取，也许有人质疑，直接table[index]不可以么，为什么要这么复杂？\n在java内存模型中，我们已经知道每个线程都有一个工作内存，里面存储着table的副本，虽然table是volatile修饰的，但不能保证线程每次都拿到table中的最新元素，Unsafe.getObjectVolatile可以直接获取指定内存的数据，保证了每次拿到数据都是最新的。\n如果f为null，说明table中这个位置第一次插入元素，利用Unsafe.compareAndSwapObject方法插入Node节点。\n如果CAS成功，说明Node节点已经插入，随后addCount(1L, binCount)方法会检查当前容量是否需要进行扩容。\n如果CAS失败，说明有其它线程提前插入了节点，自旋重新尝试在这个位置插入节点。\n如果f的hash值为-1，说明当前f是ForwardingNode节点，意味有其它线程正在扩容，则一起进行扩容操作。\n其余情况把新的Node节点按链表或红黑树的方式插入到合适的位置，这个过程采用同步内置锁实现并发，代码如下:\n```\nsynchronized (f) {\n    if (tabAt(tab, i) == f) {\n        if (fh >= 0) {\n            binCount = 1;\n            for (Node<K,V> e = f;; ++binCount) {\n                K ek;\n                if (e.hash == hash &&\n                    ((ek = e.key) == key ||\n                     (ek != null && key.equals(ek)))) {\n                    oldVal = e.val;\n                    if (!onlyIfAbsent)\n                        e.val = value;\n                    break;\n                }\n                Node<K,V> pred = e;\n                if ((e = e.next) == null) {\n                    pred.next = new Node<K,V>(hash, key,\n                                              value, null);\n                    break;\n                }\n            }\n        }\n        else if (f instanceof TreeBin) {\n            Node<K,V> p;\n            binCount = 2;\n            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,\n                                           value)) != null) {\n                oldVal = p.val;\n                if (!onlyIfAbsent)\n                    p.val = value;\n            }\n        }\n    }\n}\n```\n在节点f上进行同步，节点插入之前，再次利用tabAt(tab, i) == f判断，防止被其它线程修改。\n\n* 如果f.hash >= 0，说明f是链表结构的头结点，遍历链表，如果找到对应的node节点，则修改value，否则在链表尾部加入节点。\n* 如果f是TreeBin类型节点，说明f是红黑树根节点，则在树结构上遍历元素，更新或增加节点。\n* 如果链表中节点数binCount >= TREEIFY_THRESHOLD(默认是8)，则把链表转化为红黑树结构。\n**table扩容**\n当table容量不足的时候，即table的元素数量达到容量阈值sizeCtl，需要对table进行扩容。\n整个扩容分为两部分：\n* 构建一个nextTable，大小为table的两倍。\n* 把table的数据复制到nextTable中。\n\n这两个过程在单线程下实现很简单，但是ConcurrentHashMap是支持并发插入的，扩容操作自然也会有并发的出现，这种情况下，第二步可以支持节点的并发复制，这样性能自然提升不少，但实现的复杂度也上升了一个台阶。\n先看第一步，构建nextTable，毫无疑问，这个过程只能只有单个线程进行nextTable的初始化，具体实现如下：\n```\nprivate final void addCount(long x, int check) {\n    ... 省略部分代码\n    if (check >= 0) {\n        Node<K,V>[] tab, nt; int n, sc;\n        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&\n               (n = tab.length) < MAXIMUM_CAPACITY) {\n            int rs = resizeStamp(n);\n            if (sc < 0) {\n                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||\n                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||\n                    transferIndex <= 0)\n                    break;\n                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))\n                    transfer(tab, nt);\n            }\n            else if (U.compareAndSwapInt(this, SIZECTL, sc,\n                                         (rs << RESIZE_STAMP_SHIFT) + 2))\n                transfer(tab, null);\n            s = sumCount();\n        }\n    }\n}\n```\n通过Unsafe.compareAndSwapInt修改sizeCtl值，保证只有一个线程能够初始化nextTable，扩容后的数组长度为原来的两倍，但是容量是原来的1.5。\n节点从table移动到nextTable，大体思想是遍历、复制的过程。\n* 首先根据运算得到需要遍历的次数i，然后利用tabAt方法获得i位置的元素f，初始化一个forwardNode实例fwd。\n* 如果f == null，则在table中的i位置放入fwd，这个过程是采用Unsafe.compareAndSwapObjectf方法实现的，很巧妙的实现了节点的并发移动。\n* 如果f是链表的头节点，就构造一个反序链表，把他们分别放在nextTable的i和i+n的位置上，移动完成，采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。\n* 如果f是TreeBin节点，也做一个反序处理，并判断是否需要untreeify，把处理的结果分别放在nextTable的i和i+n的位置上，移动完成，同样采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。\n遍历过所有的节点以后就完成了复制工作，把table指向nextTable，并更新sizeCtl为新数组大小的0.75倍 ，扩容完成。\n**红黑树构造**\n注意：如果链表结构中元素超过TREEIFY_THRESHOLD阈值，默认为8个，则把链表转化为红黑树，提高遍历查询效率。\n```\nif (binCount != 0) {\n    if (binCount >= TREEIFY_THRESHOLD)\n        treeifyBin(tab, i);\n    if (oldVal != null)\n        return oldVal;\n    break;\n}\n```\n接下来我们看看如何构造树结构，代码如下：\n```\nprivate final void treeifyBin(Node<K,V>[] tab, int index) {\n    Node<K,V> b; int n, sc;\n    if (tab != null) {\n        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)\n            tryPresize(n << 1);\n        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {\n            synchronized (b) {\n                if (tabAt(tab, index) == b) {\n                    TreeNode<K,V> hd = null, tl = null;\n                    for (Node<K,V> e = b; e != null; e = e.next) {\n                        TreeNode<K,V> p =\n                            new TreeNode<K,V>(e.hash, e.key, e.val,\n                                              null, null);\n                        if ((p.prev = tl) == null)\n                            hd = p;\n                        else\n                            tl.next = p;\n                        tl = p;\n                    }\n                    setTabAt(tab, index, new TreeBin<K,V>(hd));\n                }\n            }\n        }\n    }\n}\n```\n可以看出，生成树节点的代码块是同步的，进入同步代码块之后，再次验证table中index位置元素是否被修改过。\n1、根据table中index位置Node链表，重新生成一个hd为头结点的TreeNode链表。\n2、根据hd头结点，生成TreeBin树结构，并把树结构的root节点写到table的index位置的内存中，具体实现如下：\n```\nTreeBin(TreeNode<K,V> b) {\n    super(TREEBIN, null, null, null);\n    this.first = b;\n    TreeNode<K,V> r = null;\n    for (TreeNode<K,V> x = b, next; x != null; x = next) {\n        next = (TreeNode<K,V>)x.next;\n        x.left = x.right = null;\n        if (r == null) {\n            x.parent = null;\n            x.red = false;\n            r = x;\n        }\n        else {\n            K k = x.key;\n            int h = x.hash;\n            Class<?> kc = null;\n            for (TreeNode<K,V> p = r;;) {\n                int dir, ph;\n                K pk = p.key;\n                if ((ph = p.hash) > h)\n                    dir = -1;\n                else if (ph < h)\n                    dir = 1;\n                else if ((kc == null &&\n                          (kc = comparableClassFor(k)) == null) ||\n                         (dir = compareComparables(kc, k, pk)) == 0)\n                    dir = tieBreakOrder(k, pk);\n                    TreeNode<K,V> xp = p;\n                if ((p = (dir <= 0) ? p.left : p.right) == null) {\n                    x.parent = xp;\n                    if (dir <= 0)\n                        xp.left = x;\n                    else\n                        xp.right = x;\n                    r = balanceInsertion(r, x);\n                    break;\n                }\n            }\n        }\n    }\n    this.root = r;\n    assert checkInvariants(root);\n}\n```\n主要根据Node节点的hash值大小构建二叉树。这个红黑树的构造过程实在有点复杂，感兴趣的同学可以看看源码。\n**get操作**\nget操作和put操作相比，显得简单了许多\n```\npublic V get(Object key) {\n    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;\n    int h = spread(key.hashCode());\n    if ((tab = table) != null && (n = tab.length) > 0 &&\n        (e = tabAt(tab, (n - 1) & h)) != null) {\n        if ((eh = e.hash) == h) {\n            if ((ek = e.key) == key || (ek != null && key.equals(ek)))\n                return e.val;\n        }\n        else if (eh < 0)\n            return (p = e.find(h, key)) != null ? p.val : null;\n        while ((e = e.next) != null) {\n            if (e.hash == h &&\n                ((ek = e.key) == key || (ek != null && key.equals(ek))))\n                return e.val;\n        }\n    }\n    return null;\n}\n```\n1.判断table是否为空，如果为空，直接返回null。\n2.计算key的hash值，并获取指定table中指定位置的Node节点，通过遍历链表或则树结构找到对应的节点，返回value值。\n**总结**\nConcurrentHashMap是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于HashTable和同步包装器包装的 HashMap,使用一个全局的锁来同步不同线程间的并发访问,同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器,这虽然保证多线程间的安全并发访问，但同时也导致对容器的访问变成串行化的了。\n"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(ConcurrentLinkedQueue).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n### ConcurrentLinkedQueue介绍\nConcurrentLinkedQueue是线程安全的队列，它适用于“高并发”的场景。\n它是一个基于链接节点的无界线程安全队列，按照 FIFO（先进先出）原则对元素进行排序。队列元素中不可以放置null元素（内部实现的特殊节点除外）。\n\n### ConcurrentLinkedQueue原理和数据结构\nConcurrentLinkedQueue的数据结构，如下图所示：![2c9f34f0d8819f5a0c03ecbe99b7ca82](大数据成神之路-Java高级特性增强(ConcurrentLinkedQueue).resources/2C447958-48AF-4B02-A30E-52AA0038497C.jpg)\n说明：\n\n1. ConcurrentLinkedQueue继承于AbstractQueue。\n2. ConcurrentLinkedQueue内部是通过链表来实现的。它同时包含链表的头节点head和尾节点tail。ConcurrentLinkedQueue按照FIFO（先进先出）原则对元素进行排序。元素都是从尾部插入到链表，从头部开始返回。\n3. ConcurrentLinkedQueue的链表Node中的next的类型是volatile，而且链表数据item的类型也是volatile。关于volatile，我们知道它的语义包含：\"即对一个volatile变量的读，总是能看到（任意线程）对这个volatile变量最后的写入\"。ConcurrentLinkedQueue就是通过volatile来实现多线程对竞争资源的互斥访问的.\n\n### ConcurrentLinkedQueue函数列表\n\n```\n// 创建一个最初为空的 ConcurrentLinkedQueue。\nConcurrentLinkedQueue()\n// 创建一个最初包含给定 collection 元素的 ConcurrentLinkedQueue，按照此 collection 迭代器的遍历顺序来添加元素。\nConcurrentLinkedQueue(Collection<? extends E> c)\n\n// 将指定元素插入此队列的尾部。\nboolean add(E e)\n// 如果此队列包含指定元素，则返回 true。\nboolean contains(Object o)\n// 如果此队列不包含任何元素，则返回 true。\nboolean isEmpty()\n// 返回在此队列元素上以恰当顺序进行迭代的迭代器。\nIterator<E> iterator()\n// 将指定元素插入此队列的尾部。\nboolean offer(E e)\n// 获取但不移除此队列的头；如果此队列为空，则返回 null。\nE peek()\n// 获取并移除此队列的头，如果此队列为空，则返回 null。\nE poll()\n// 从队列中移除指定元素的单个实例（如果存在）。\nboolean remove(Object o)\n// 返回此队列中的元素数量。\nint size()\n// 返回以恰当顺序包含此队列所有元素的数组。\nObject[] toArray()\n// 返回以恰当顺序包含此队列所有元素的数组；返回数组的运行时类型是指定数组的运行时类型。\n<T> T[] toArray(T[] a)\n```\n### ConcurrentLinkedQueue源码分析\n\n下面从ConcurrentLinkedQueue的创建，添加，删除这几个方面对它进行分析。\n**1 创建**\n下面以ConcurrentLinkedQueue()来进行说明。\n```\npublic ConcurrentLinkedQueue() {\n    head = tail = new Node<E>(null);\n}\n```\n说明：在构造函数中，新建了一个“内容为null的节点”，并设置表头head和表尾tail的值为新节点。\n\nhead和tail的定义如下：\n```\nprivate transient volatile Node<E> head;\nprivate transient volatile Node<E> tail;\n```\nhead和tail都是volatile类型，他们具有volatile赋予的含义：“即对一个volatile变量的读，总是能看到（任意线程）对这个volatile变量最后的写入”。\nNode的声明如下：\n```\nprivate static class Node<E> {\n    volatile E item;\n    volatile Node<E> next;\n\n    Node(E item) {\n        UNSAFE.putObject(this, itemOffset, item);\n    }\n\n    boolean casItem(E cmp, E val) {\n        return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);\n    }\n\n    void lazySetNext(Node<E> val) {\n        UNSAFE.putOrderedObject(this, nextOffset, val);\n    }\n\n    boolean casNext(Node<E> cmp, Node<E> val) {\n        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);\n    }\n\n    // Unsafe mechanics\n    private static final sun.misc.Unsafe UNSAFE;\n    private static final long itemOffset;\n    private static final long nextOffset;\n\n    static {\n        try {\n            UNSAFE = sun.misc.Unsafe.getUnsafe();\n            Class k = Node.class;\n            itemOffset = UNSAFE.objectFieldOffset\n                (k.getDeclaredField(\"item\"));\n            nextOffset = UNSAFE.objectFieldOffset\n                (k.getDeclaredField(\"next\"));\n        } catch (Exception e) {\n            throw new Error(e);\n        }\n    }\n}\n```\n说明：\nNode是个单向链表节点，next用于指向下一个Node，item用于存储数据。Node中操作节点数据的API，都是通过Unsafe机制的CAS函数实现的；例如casNext()是通过CAS函数“比较并设置节点的下一个节点”。\n\n\n**2. 添加**\n\n下面以add(E e)为例对ConcurrentLinkedQueue中的添加进行说明。\n\n```\npublic boolean add(E e) {\n    return offer(e);\n}\n```\n说明：add()实际上是调用的offer()来完成添加操作的。\n\noffer()的源码如下：\n```\npublic boolean offer(E e) {\n    // 检查e是不是null，是的话抛出NullPointerException异常。\n    checkNotNull(e);\n    // 创建新的节点\n    final Node<E> newNode = new Node<E>(e);\n\n    // 将“新的节点”添加到链表的末尾。\n    for (Node<E> t = tail, p = t;;) {\n        Node<E> q = p.next;\n        // 情况1：q为空\n        if (q == null) {\n            // CAS操作：如果“p的下一个节点为null”(即p为尾节点)，则设置p的下一个节点为newNode。\n            // 如果该CAS操作成功的话，则比较“p和t”(若p不等于t，则设置newNode为新的尾节点)，然后返回true。\n            // 如果该CAS操作失败，这意味着“其它线程对尾节点进行了修改”，则重新循环。\n            if (p.casNext(null, newNode)) {\n                if (p != t) // hop two nodes at a time\n                    casTail(t, newNode);  // Failure is OK.\n                return true;\n            }\n        }\n        // 情况2：p和q相等\n        else if (p == q)\n            p = (t != (t = tail)) ? t : head;\n        // 情况3：其它\n        else\n            p = (p != t && t != (t = tail)) ? t : q;\n    }\n}\n```\n说明：offer(E e)的作用就是将元素e添加到链表的末尾。offer()比较的地方是理解for循环，下面区分3种情况对for进行分析。\n\n情况1 -- q为空。这意味着q是尾节点的下一个节点。此时，通过p.casNext(null, newNode)将“p的下一个节点设为newNode”，若设置成功的话，则比较“p和t”(若p不等于t，则设置newNode为新的尾节点)，然后返回true。否则的话(意味着“其它线程对尾节点进行了修改”)，什么也不做，继续进行for循环。\np.casNext(null, newNode)，是调用CAS对p进行操作。若“p的下一个节点等于null”，则设置“p的下一个节点等于newNode”；设置成功的话，返回true，失败的话返回false。\n\n情况2 -- p和q相等。这种情况什么时候会发生呢？通过“情况3”，我们知道，经过“情况3”的处理后，p的值可能等于q。\n此时，若尾节点没有发生变化的话，那么，应该是头节点发生了变化，则设置p为头节点，然后重新遍历链表；否则(尾节点变化的话)，则设置p为尾节点。\n\n情况3 -- 其它。\n我们将p = (p != t && t != (t = tail)) ? t : q;转换成如下代码。\n```\nif (p==t) {\n    p = q;\n} else {\n    Node<E> tmp=t;\n    t = tail;\n    if (tmp==t) {\n        p=q;\n    } else {\n        p=t;\n    }\n}\n```\n如果p和t相等，则设置p为q。否则的话，判断“尾节点是否发生变化”，没有变化的话，则设置p为q；否则，设置p为尾节点。\n\ncheckNotNull()的源码如下：\n\n```\nprivate static void checkNotNull(Object v) {\n    if (v == null)\n        throw new NullPointerException();\n}\n```\n**3. 删除**\n\n下面以poll()为例对ConcurrentLinkedQueue中的删除进行说明。\n\n```\npublic E poll() {\n    // 设置“标记”\n    restartFromHead:\n    for (;;) {\n        for (Node<E> h = head, p = h, q;;) {\n            E item = p.item;\n\n            // 情况1\n            // 表头的数据不为null，并且“设置表头的数据为null”这个操作成功的话;\n            // 则比较“p和h”(若p!=h，即表头发生了变化，则更新表头，即设置表头为p)，然后返回原表头的item值。\n            if (item != null && p.casItem(item, null)) {\n                if (p != h) // hop two nodes at a time\n                    updateHead(h, ((q = p.next) != null) ? q : p);\n                return item;\n            }\n            // 情况2\n            // 表头的下一个节点为null，即链表只有一个“内容为null的表头节点”。则更新表头为p，并返回null。\n            else if ((q = p.next) == null) {\n                updateHead(h, p);\n                return null;\n            }\n            // 情况3\n            // 这可能到由于“情况4”的发生导致p=q，在该情况下跳转到restartFromHead标记重新操作。\n            else if (p == q)\n                continue restartFromHead;\n            // 情况4\n            // 设置p为q\n            else\n                p = q;\n        }\n    }\n}\n```\n说明：poll()的作用就是删除链表的表头节点，并返回被删节点对应的值。poll()的实现原理和offer()比较类似，下面根将or循环划分为4种情况进行分析。\n\n情况1：“表头节点的数据”不为null，并且“设置表头节点的数据为null”这个操作成功。\np.casItem(item, null) -- 调用CAS函数，比较“节点p的数据值”与item是否相等，是的话，设置节点p的数据值为null。\n在情况1发生时，先比较“p和h”，若p!=h，即表头发生了变化，则调用updateHead()更新表头；然后返回删除节点的item值。\nupdateHead()的源码如下：\n```\nfinal void updateHead(Node<E> h, Node<E> p) {\n    if (h != p && casHead(h, p))\n        h.lazySetNext(h);\n}\n```\n说明：updateHead()的最终目的是更新表头为p，并设置h的下一个节点为h本身。\ncasHead(h,p)是通过CAS函数设置表头，若表头等于h的话，则设置表头为p。\nlazySetNext()的源码如下：\n```\nvoid lazySetNext(Node<E> val) {\n    UNSAFE.putOrderedObject(this, nextOffset, val);\n}\n```\nputOrderedObject()函数，我们在前面一章“TODO”中介绍过。h.lazySetNext(h)的作用是通过CAS函数设置h的下一个节点为h自身，该设置可能会延迟执行。\n\n情况2：如果表头的下一个节点为null，即链表只有一个“内容为null的表头节点”。\n则调用updateHead(h, p)，将表头更新p；然后返回null。\n\n情况3：p=q\n在“情况4”的发生后，会导致p=q；此时，“情况3”就会发生。当“情况3”发生后，它会跳转到restartFromHead标记重新操作。\n\n情况4：其它情况。\n设置p=q。\n\n### ConcurrentLinkedQueue示例\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/*\n *   ConcurrentLinkedQueue是“线程安全”的队列，而LinkedList是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历queue”的示例\n *   (01) 当queue是ConcurrentLinkedQueue对象时，程序能正常运行。\n *   (02) 当queue是LinkedList对象时，程序会产生ConcurrentModificationException异常。\n *\n * @author skywang\n */\npublic class ConcurrentLinkedQueueDemo1 {\n\n    // TODO: queue是LinkedList对象时，程序会出错。\n    //private static Queue<String> queue = new LinkedList<String>();\n    private static Queue<String> queue = new ConcurrentLinkedQueue<String>();\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对queue进行操作！\n        new MyThread(\"ta\").start();\n        new MyThread(\"tb\").start();\n    }\n\n    private static void printAll() {\n        String value;\n        Iterator iter = queue.iterator();\n        while(iter.hasNext()) {\n            value = (String)iter.next();\n            System.out.print(value+\", \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 6) {\n                // “线程名” + \"-\" + \"序号\"\n                String val = Thread.currentThread().getName()+i;\n                queue.add(val);\n                // 通过“Iterator”遍历queue。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\nta1, ta1, tb1, tb1,\n\nta1, ta1, tb1, tb1, ta2, ta2, tb2, \ntb2, \nta1, ta1, tb1, tb1, ta2, ta2, tb2, tb2, ta3, tb3, \nta3, ta1, tb3, tb1, ta4, \nta2, ta1, tb2, tb1, ta3, ta2, tb3, tb2, ta4, ta3, tb4, \ntb3, ta1, ta4, tb1, tb4, ta2, ta5, \ntb2, ta1, ta3, tb1, tb3, ta2, ta4, tb2, tb4, ta3, ta5, tb3, tb5, \nta4, ta1, tb4, tb1, ta5, ta2, tb5, tb2, ta6, \nta3, ta1, tb3, tb1, ta4, ta2, tb4, tb2, ta5, ta3, tb5, tb3, ta6, ta4, tb6, \ntb4, ta5, tb5, ta6, tb6,\n```\n结果说明：如果将源码中的queue改成LinkedList对象时，程序会产生ConcurrentModificationException异常。"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(ConcurrentSkipListMap).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n### 概要\n本章对Java.util.concurrent包中的ConcurrentSkipListMap类进行详细的介绍。内容包括：\n\n* ConcurrentSkipListMap介绍\n* ConcurrentSkipListMap原理和数据结构\n* ConcurrentSkipListMap函数列表\n* ConcurrentSkipListMap源码分析\n* ConcurrentSkipListMap示例\n\n\n### ConcurrentSkipListMap介绍\nConcurrentSkipListMap是线程安全的有序的哈希表，适用于高并发的场景。\nConcurrentSkipListMap和TreeMap，它们虽然都是有序的哈希表。但是，第一，它们的线程安全机制不同，TreeMap是非线程安全的，而ConcurrentSkipListMap是线程安全的。\n第二，ConcurrentSkipListMap是通过跳表实现的，而TreeMap是通过红黑树实现的。\n关于跳表(Skip List)，它是平衡树的一种替代的数据结构，但是和红黑树不相同的是，跳表对于树的平衡的实现是基于一种随机化的算法的，这样也就是说跳表的插入和删除的工作是比较简单的。\n### ConcurrentSkipListMap原理和数据结构\nConcurrentSkipListMap的数据结构，如下图所示：![7d7d48450f836bf600443210d6da0af4](大数据成神之路-Java高级特性增强(ConcurrentSkipListMap).resources/AA985055-21D7-47B9-B826-0BBF8CC3F359.jpg)\n**说明：**\n先以数据“7,14,21,32,37,71,85”序列为例，来对跳表进行简单说明。\n跳表分为许多层(level)，每一层都可以看作是数据的索引，这些索引的意义就是加快跳表查找数据速度。每一层的数据都是有序的，上一层数据是下一层数据的子集，并且第一层(level 1)包含了全部的数据；层次越高，跳跃性越大，包含的数据越少。\n跳表包含一个表头，它查找数据时，是从上往下，从左往右进行查找。现在“需要找出值为32的节点”为例，来对比说明跳表和普遍的链表。\n**情况1：链表中查找“32”节点**\n路径如下图1-02所示：\n![4f6f535b328b98b16daac97ff5cac0da](大数据成神之路-Java高级特性增强(ConcurrentSkipListMap).resources/1E5B73C5-5DDD-4A44-8328-0A34F43BE2B1.jpg)\n需要4步(红色部分表示路径)。 \n**情况2：跳表中查找“32”节点**\n路径如下图1-03所示:\n![292bc626f03ecc1ada81083787e8d395](大数据成神之路-Java高级特性增强(ConcurrentSkipListMap).resources/0F2F9532-CF90-471C-BEE7-FB3AD0126E1C.jpg)\n忽略索引垂直线路上路径的情况下，只需要2步(红色部分表示路径)。下面说说Java中ConcurrentSkipListMap的数据结构。\n(01) ConcurrentSkipListMap继承于AbstractMap类，也就意味着它是一个哈希表。\n(02) Index是ConcurrentSkipListMap的内部类，它与“跳表中的索引相对应”。HeadIndex继承于Index，ConcurrentSkipListMap中含有一个HeadIndex的对象head，head是“跳表的表头”。\n(03) Index是跳表中的索引，它包含“右索引的指针(right)”，“下索引的指针(down)”和“哈希表节点node”。node是Node的对象，Node也是ConcurrentSkipListMap中的内部类。\n\n### ConcurrentSkipListMap函数列表\n```\n// 构造一个新的空映射，该映射按照键的自然顺序进行排序。\nConcurrentSkipListMap()\n// 构造一个新的空映射，该映射按照指定的比较器进行排序。\nConcurrentSkipListMap(Comparator<? super K> comparator)\n// 构造一个新映射，该映射所包含的映射关系与给定映射包含的映射关系相同，并按照键的自然顺序进行排序。\nConcurrentSkipListMap(Map<? extends K,? extends V> m)\n// 构造一个新映射，该映射所包含的映射关系与指定的有序映射包含的映射关系相同，使用的顺序也相同。\nConcurrentSkipListMap(SortedMap<K,? extends V> m)\n\n// 返回与大于等于给定键的最小键关联的键-值映射关系；如果不存在这样的条目，则返回 null。\nMap.Entry<K,V> ceilingEntry(K key)\n// 返回大于等于给定键的最小键；如果不存在这样的键，则返回 null。\nK ceilingKey(K key)\n// 从此映射中移除所有映射关系。\nvoid clear()\n// 返回此 ConcurrentSkipListMap 实例的浅表副本。\nConcurrentSkipListMap<K,V> clone()\n// 返回对此映射中的键进行排序的比较器；如果此映射使用键的自然顺序，则返回 null。\nComparator<? super K> comparator()\n// 如果此映射包含指定键的映射关系，则返回 true。\nboolean containsKey(Object key)\n// 如果此映射为指定值映射一个或多个键，则返回 true。\nboolean containsValue(Object value)\n// 返回此映射中所包含键的逆序 NavigableSet 视图。\nNavigableSet<K> descendingKeySet()\n// 返回此映射中所包含映射关系的逆序视图。\nConcurrentNavigableMap<K,V> descendingMap()\n// 返回此映射中所包含的映射关系的 Set 视图。\nSet<Map.Entry<K,V>> entrySet()\n// 比较指定对象与此映射的相等性。\nboolean equals(Object o)\n// 返回与此映射中的最小键关联的键-值映射关系；如果该映射为空，则返回 null。\nMap.Entry<K,V> firstEntry()\n// 返回此映射中当前第一个（最低）键。\nK firstKey()\n// 返回与小于等于给定键的最大键关联的键-值映射关系；如果不存在这样的键，则返回 null。\nMap.Entry<K,V> floorEntry(K key)\n// 返回小于等于给定键的最大键；如果不存在这样的键，则返回 null。\nK floorKey(K key)\n// 返回指定键所映射到的值；如果此映射不包含该键的映射关系，则返回 null。\nV get(Object key)\n// 返回此映射的部分视图，其键值严格小于 toKey。\nConcurrentNavigableMap<K,V> headMap(K toKey)\n// 返回此映射的部分视图，其键小于（或等于，如果 inclusive 为 true）toKey。\nConcurrentNavigableMap<K,V> headMap(K toKey, boolean inclusive)\n// 返回与严格大于给定键的最小键关联的键-值映射关系；如果不存在这样的键，则返回 null。\nMap.Entry<K,V> higherEntry(K key)\n// 返回严格大于给定键的最小键；如果不存在这样的键，则返回 null。\nK higherKey(K key)\n// 如果此映射未包含键-值映射关系，则返回 true。\nboolean isEmpty()\n// 返回此映射中所包含键的 NavigableSet 视图。\nNavigableSet<K> keySet()\n// 返回与此映射中的最大键关联的键-值映射关系；如果该映射为空，则返回 null。\nMap.Entry<K,V> lastEntry()\n// 返回映射中当前最后一个（最高）键。\nK lastKey()\n// 返回与严格小于给定键的最大键关联的键-值映射关系；如果不存在这样的键，则返回 null。\nMap.Entry<K,V> lowerEntry(K key)\n// 返回严格小于给定键的最大键；如果不存在这样的键，则返回 null。\nK lowerKey(K key)\n// 返回此映射中所包含键的 NavigableSet 视图。\nNavigableSet<K> navigableKeySet()\n// 移除并返回与此映射中的最小键关联的键-值映射关系；如果该映射为空，则返回 null。\nMap.Entry<K,V> pollFirstEntry()\n// 移除并返回与此映射中的最大键关联的键-值映射关系；如果该映射为空，则返回 null。\nMap.Entry<K,V> pollLastEntry()\n// 将指定值与此映射中的指定键关联。\nV put(K key, V value)\n// 如果指定键已经不再与某个值相关联，则将它与给定值关联。\nV putIfAbsent(K key, V value)\n// 从此映射中移除指定键的映射关系（如果存在）。\nV remove(Object key)\n// 只有目前将键的条目映射到给定值时，才移除该键的条目。\nboolean remove(Object key, Object value)\n// 只有目前将键的条目映射到某一值时，才替换该键的条目。\nV replace(K key, V value)\n// 只有目前将键的条目映射到给定值时，才替换该键的条目。\nboolean replace(K key, V oldValue, V newValue)\n// 返回此映射中的键-值映射关系数。\nint size()\n// 返回此映射的部分视图，其键的范围从 fromKey 到 toKey。\nConcurrentNavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)\n// 返回此映射的部分视图，其键值的范围从 fromKey（包括）到 toKey（不包括）。\nConcurrentNavigableMap<K,V> subMap(K fromKey, K toKey)\n// 返回此映射的部分视图，其键大于等于 fromKey。\nConcurrentNavigableMap<K,V> tailMap(K fromKey)\n// 返回此映射的部分视图，其键大于（或等于，如果 inclusive 为 true）fromKey。\nConcurrentNavigableMap<K,V> tailMap(K fromKey, boolean inclusive)\n// 返回此映射中所包含值的 Collection 视图。\nCollection<V> values()\n```\n### ConcurrentSkipListMap源码分析\n下面从ConcurrentSkipListMap的添加，删除，获取这3个方面对它进行分析。\n**1. 添加**\n下面以put(K key, V value)为例，对ConcurrentSkipListMap的添加方法进行说明。\n```\npublic V put(K key, V value) {\n    if (value == null)\n        throw new NullPointerException();\n    return doPut(key, value, false);\n}\n```\n实际上，put()是通过doPut()将key-value键值对添加到ConcurrentSkipListMap中的。\ndoPut()的源码如下：\n```\nprivate V doPut(K kkey, V value, boolean onlyIfAbsent) {\n    Comparable<? super K> key = comparable(kkey);\n    for (;;) {\n        // 找到key的前继节点\n        Node<K,V> b = findPredecessor(key);\n        // 设置n为“key的前继节点的后继节点”，即n应该是“插入节点”的“后继节点”\n        Node<K,V> n = b.next;\n        for (;;) {\n            if (n != null) {\n                Node<K,V> f = n.next;\n                // 如果两次获得的b.next不是相同的Node，就跳转到”外层for循环“，重新获得b和n后再遍历。\n                if (n != b.next)\n                    break;\n                // v是“n的值”\n                Object v = n.value;\n                // 当n的值为null(意味着其它线程删除了n)；此时删除b的下一个节点，然后跳转到”外层for循环“，重新获得b和n后再遍历。\n                if (v == null) {               // n is deleted\n                    n.helpDelete(b, f);\n                    break;\n                }\n                // 如果其它线程删除了b；则跳转到”外层for循环“，重新获得b和n后再遍历。\n                if (v == n || b.value == null) // b is deleted\n                    break;\n                // 比较key和n.key\n                int c = key.compareTo(n.key);\n                if (c > 0) {\n                    b = n;\n                    n = f;\n                    continue;\n                }\n                if (c == 0) {\n                    if (onlyIfAbsent || n.casValue(v, value))\n                        return (V)v;\n                    else\n                        break; // restart if lost race to replace value\n                }\n                // else c < 0; fall through\n            }\n\n            // 新建节点(对应是“要插入的键值对”)\n            Node<K,V> z = new Node<K,V>(kkey, value, n);\n            // 设置“b的后继节点”为z\n            if (!b.casNext(n, z))\n                break;         // 多线程情况下，break才可能发生(其它线程对b进行了操作)\n            // 随机获取一个level\n            // 然后在“第1层”到“第level层”的链表中都插入新建节点\n            int level = randomLevel();\n            if (level > 0)\n                insertIndex(z, level);\n            return null;\n        }\n    }\n}\n```\n说明：doPut() 的作用就是将键值对添加到“跳表”中。\n要想搞清doPut()，首先要弄清楚它的主干部分 —— 我们先单纯的只考虑“单线程的情况下，将key-value添加到跳表中”，即忽略“多线程相关的内容”。它的流程如下：\n第1步：找到“插入位置”。\n即，找到“key的前继节点(b)”和“key的后继节点(n)”；key是要插入节点的键。\n第2步：新建并插入节点。\n即，新建节点z(key对应的节点)，并将新节点z插入到“跳表”中(设置“b的后继节点为z”，“z的后继节点为n”)。\n第3步：更新跳表。\n即，随机获取一个level，然后在“跳表”的第1层～第level层之间，每一层都插入节点z；在第level层之上就不再插入节点了。若level数值大于“跳表的层次”，则新建一层。\n主干部分“对应的精简后的doPut()的代码”如下(仅供参考)：\n```\nprivate V doPut(K kkey, V value, boolean onlyIfAbsent) {\n    Comparable<? super K> key = comparable(kkey);\n    for (;;) {\n        // 找到key的前继节点\n        Node<K,V> b = findPredecessor(key);\n        // 设置n为key的后继节点\n        Node<K,V> n = b.next;\n        for (;;) {\n            \n            // 新建节点(对应是“要被插入的键值对”)\n            Node<K,V> z = new Node<K,V>(kkey, value, n);\n            // 设置“b的后继节点”为z\n            b.casNext(n, z);\n\n            // 随机获取一个level\n            // 然后在“第1层”到“第level层”的链表中都插入新建节点\n            int level = randomLevel();\n            if (level > 0)\n                insertIndex(z, level);\n            return null;\n        }\n    }\n}\n```\n理清主干之后，剩余的工作就相对简单了。主要是上面几步的对应算法的具体实现，以及多线程相关情况的处理！\n**2. 删除**\n下面以remove(Object key)为例，对ConcurrentSkipListMap的删除方法进行说明。\n```\npublic V remove(Object key) {\n    return doRemove(key, null);\n}\n```\n实际上，remove()是通过doRemove()将ConcurrentSkipListMap中的key对应的键值对删除的。\ndoRemove()的源码如下：\n```\nfinal V doRemove(Object okey, Object value) {\n    Comparable<? super K> key = comparable(okey);\n    for (;;) {\n        // 找到“key的前继节点”\n        Node<K,V> b = findPredecessor(key);\n        // 设置n为“b的后继节点”(即若key存在于“跳表中”，n就是key对应的节点)\n        Node<K,V> n = b.next;\n        for (;;) {\n            if (n == null)\n                return null;\n            // f是“当前节点n的后继节点”\n            Node<K,V> f = n.next;\n            // 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表)，则返回到“外层for循环”重新遍历。\n            if (n != b.next)                    // inconsistent read\n                break;\n            // 如果“当前节点n的值”变为null(其它线程操作了该跳表)，则返回到“外层for循环”重新遍历。\n            Object v = n.value;\n            if (v == null) {                    // n is deleted\n                n.helpDelete(b, f);\n                break;\n            }\n            // 如果“前继节点b”被删除(其它线程操作了该跳表)，则返回到“外层for循环”重新遍历。\n            if (v == n || b.value == null)      // b is deleted\n                break;\n            int c = key.compareTo(n.key);\n            if (c < 0)\n                return null;\n            if (c > 0) {\n                b = n;\n                n = f;\n                continue;\n            }\n\n            // 以下是c=0的情况\n            if (value != null && !value.equals(v))\n                return null;\n            // 设置“当前节点n”的值为null\n            if (!n.casValue(v, null))\n                break;\n            // 设置“b的后继节点”为f\n            if (!n.appendMarker(f) || !b.casNext(n, f))\n                findNode(key);                  // Retry via findNode\n            else {\n                // 清除“跳表”中每一层的key节点\n                findPredecessor(key);           // Clean index\n                // 如果“表头的右索引为空”，则将“跳表的层次”-1。\n                if (head.right == null)\n                    tryReduceLevel();\n            }\n            return (V)v;\n        }\n    }\n}\n```\n说明：doRemove()的作用是删除跳表中的节点。\n和doPut()一样，我们重点看doRemove()的主干部分，了解主干部分之后，其余部分就非常容易理解了。下面是“单线程的情况下，删除跳表中键值对的步骤”：\n第1步：找到“被删除节点的位置”。\n即，找到“key的前继节点(b)”，“key所对应的节点(n)”，“n的后继节点f”；key是要删除节点的键。\n第2步：删除节点。\n即，将“key所对应的节点n”从跳表中移除 -- 将“b的后继节点”设为“f”！\n第3步：更新跳表。\n即，遍历跳表，删除每一层的“key节点”(如果存在的话)。如果删除“key节点”之后，跳表的层次需要-1；则执行相应的操作！\n主干部分“对应的精简后的doRemove()的代码”如下(仅供参考)：\n```\nfinal V doRemove(Object okey, Object value) {\n    Comparable<? super K> key = comparable(okey);\n    for (;;) {\n        // 找到“key的前继节点”\n        Node<K,V> b = findPredecessor(key);\n        // 设置n为“b的后继节点”(即若key存在于“跳表中”，n就是key对应的节点)\n        Node<K,V> n = b.next;\n        for (;;) {\n            // f是“当前节点n的后继节点”\n            Node<K,V> f = n.next;\n\n            // 设置“当前节点n”的值为null\n            n.casValue(v, null);\n\n            // 设置“b的后继节点”为f\n            b.casNext(n, f);\n            // 清除“跳表”中每一层的key节点\n            findPredecessor(key);\n            // 如果“表头的右索引为空”，则将“跳表的层次”-1。\n            if (head.right == null)\n                tryReduceLevel();\n            return (V)v;\n        }\n    }\n}\n```\n**3. 获取**\n下面以get(Object key)为例，对ConcurrentSkipListMap的获取方法进行说明\n```\npublic V get(Object key) {\n    return doGet(key);\n}\n```\ndoGet的源码如下：\n```\nprivate V doGet(Object okey) {\n    Comparable<? super K> key = comparable(okey);\n    for (;;) {\n        // 找到“key对应的节点”\n        Node<K,V> n = findNode(key);\n        if (n == null)\n            return null;\n        Object v = n.value;\n        if (v != null)\n            return (V)v;\n    }\n}\n```\n说明：doGet()是通过findNode()找到并返回节点的。\n```\nprivate Node<K,V> findNode(Comparable<? super K> key) {\n    for (;;) {\n        // 找到key的前继节点\n        Node<K,V> b = findPredecessor(key);\n        // 设置n为“b的后继节点”(即若key存在于“跳表中”，n就是key对应的节点)\n        Node<K,V> n = b.next;\n        for (;;) {\n            // 如果“n为null”，则跳转中不存在key对应的节点，直接返回null。\n            if (n == null)\n                return null;\n            Node<K,V> f = n.next;\n            // 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表)，则返回到“外层for循环”重新遍历。\n            if (n != b.next)                // inconsistent read\n                break;\n            Object v = n.value;\n            // 如果“当前节点n的值”变为null(其它线程操作了该跳表)，则返回到“外层for循环”重新遍历。\n            if (v == null) {                // n is deleted\n                n.helpDelete(b, f);\n                break;\n            }\n            if (v == n || b.value == null)  // b is deleted\n                break;\n            // 若n是当前节点，则返回n。\n            int c = key.compareTo(n.key);\n            if (c == 0)\n                return n;\n            // 若“节点n的key”小于“key”，则说明跳表中不存在key对应的节点，返回null\n            if (c < 0)\n                return null;\n            // 若“节点n的key”大于“key”，则更新b和n，继续查找。\n            b = n;\n            n = f;\n        }\n    }\n}\n```\n说明：findNode(key)的作用是在返回跳表中key对应的节点；存在则返回节点，不存在则返回null。\n先弄清函数的主干部分，即抛开“多线程相关内容”，单纯的考虑单线程情况下，从跳表获取节点的算法。\n第1步：找到“被删除节点的位置”。\n根据findPredecessor()定位key所在的层次以及找到key的前继节点(b)，然后找到b的后继节点n。\n第2步：根据“key的前继节点(b)”和“key的前继节点的后继节点(n)”来定位“key对应的节点”。\n具体是通过比较“n的键值”和“key”的大小。如果相等，则n就是所要查找的键。\n\n### ConcurrentSkipListMap示例\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/*\n *   ConcurrentSkipListMap是“线程安全”的哈希表，而TreeMap是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历map”的示例\n *   (01) 当map是ConcurrentSkipListMap对象时，程序能正常运行。\n *   (02) 当map是TreeMap对象时，程序会产生ConcurrentModificationException异常。\n\npublic class ConcurrentSkipListMapDemo1 {\n\n    // TODO: map是TreeMap对象时，程序会出错。\n    //private static Map<String, String> map = new TreeMap<String, String>();\n    private static Map<String, String> map = new ConcurrentSkipListMap<String, String>();\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对map进行操作！\n        new MyThread(\"a\").start();\n        new MyThread(\"b\").start();\n    }\n\n    private static void printAll() {\n        String key, value;\n        Iterator iter = map.entrySet().iterator();\n        while(iter.hasNext()) {\n            Map.Entry entry = (Map.Entry)iter.next();\n            key = (String)entry.getKey();\n            value = (String)entry.getValue();\n            System.out.print(\"(\"+key+\", \"+value+\"), \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 6) {\n                // “线程名” + \"序号\"\n                String val = Thread.currentThread().getName()+i;\n                map.put(val, \"0\");\n                // 通过“Iterator”遍历map。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\n(a1, 0), (a1, 0), (b1, 0), (b1, 0),\n\n(a1, 0), (b1, 0), (b2, 0), \n(a1, 0), (a1, 0), (a2, 0), (a2, 0), (b1, 0), (b1, 0), (b2, 0), (b2, 0), (b3, 0), \n(b3, 0), (a1, 0), \n(a2, 0), (a3, 0), (a1, 0), (b1, 0), (a2, 0), (b2, 0), (a3, 0), (b3, 0), (b1, 0), (b4, 0), \n(b2, 0), (a1, 0), (b3, 0), (a2, 0), (b4, 0), \n(a3, 0), (a1, 0), (a4, 0), (a2, 0), (b1, 0), (a3, 0), (b2, 0), (a4, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), \n(b3, 0), (a1, 0), (b4, 0), (a2, 0), (b5, 0), \n(a3, 0), (a1, 0), (a4, 0), (a2, 0), (a5, 0), (a3, 0), (b1, 0), (a4, 0), (b2, 0), (a5, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (b6, 0), \n(b4, 0), (a1, 0), (b5, 0), (a2, 0), (b6, 0), \n(a3, 0), (a4, 0), (a5, 0), (a6, 0), (b1, 0), (b2, 0), (b3, 0), (b4, 0), (b5, 0), (b6, 0),\n```\n结果说明：\n示例程序中，启动两个线程(线程a和线程b)分别对ConcurrentSkipListMap进行操作。以线程a而言，它会先获取“线程名”+“序号”，然后将该字符串作为key，将“0”作为value，插入到ConcurrentSkipListMap中；接着，遍历并输出ConcurrentSkipListMap中的全部元素。 线程b的操作和线程a一样，只不过线程b的名字和线程a的名字不同。\n当map是ConcurrentSkipListMap对象时，程序能正常运行。如果将map改为TreeMap时，程序会产生ConcurrentModificationException异常。"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(ConcurrentSkipListSet).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n#### ConcurrentSkipListSet介绍\nConcurrentSkipListSet是线程安全的有序的集合，适用于高并发的场景。\nConcurrentSkipListSet和TreeSet，它们虽然都是有序的集合。但是，\n第一，它们的线程安全机制不同，TreeSet是非线程安全的，而ConcurrentSkipListSet是线程安全的。\n第二，ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的，而TreeSet是通过TreeMap实现的。\n\n#### ConcurrentSkipListSet原理和数据结构\nConcurrentSkipListSet的数据结构，如下图所示：![ceaa5eae278d00721ece58625b3b45a0](大数据成神之路-Java高级特性增强(ConcurrentSkipListSet).resources/1378EC09-5D7F-4544-AA4C-F43796D9A609.jpg)\n说明：\n\n(01) ConcurrentSkipListSet继承于AbstractSet。因此，它本质上是一个集合。\n(02) ConcurrentSkipListSet实现了NavigableSet接口。因此，ConcurrentSkipListSet是一个有序的集合。\n(03) ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的。它包含一个ConcurrentNavigableMap对象m，而m对象实际上是ConcurrentNavigableMap的实现类ConcurrentSkipListMap的实例。ConcurrentSkipListMap中的元素是key-value键值对；而ConcurrentSkipListSet是集合，它只用到了ConcurrentSkipListMap中的key！\n#### ConcurrentSkipListSet函数列表\n```\n// 构造一个新的空 set，该 set 按照元素的自然顺序对其进行排序。\nConcurrentSkipListSet()\n// 构造一个包含指定 collection 中元素的新 set，这个新 set 按照元素的自然顺序对其进行排序。\nConcurrentSkipListSet(Collection<? extends E> c)\n// 构造一个新的空 set，该 set 按照指定的比较器对其元素进行排序。\nConcurrentSkipListSet(Comparator<? super E> comparator)\n// 构造一个新 set，该 set 所包含的元素与指定的有序 set 包含的元素相同，使用的顺序也相同。\nConcurrentSkipListSet(SortedSet<E> s)\n\n// 如果此 set 中不包含指定元素，则添加指定元素。\nboolean add(E e)\n// 返回此 set 中大于等于给定元素的最小元素；如果不存在这样的元素，则返回 null。\nE ceiling(E e)\n// 从此 set 中移除所有元素。\nvoid clear()\n// 返回此 ConcurrentSkipListSet 实例的浅表副本。\nConcurrentSkipListSet<E> clone()\n// 返回对此 set 中的元素进行排序的比较器；如果此 set 使用其元素的自然顺序，则返回 null。\nComparator<? super E> comparator()\n// 如果此 set 包含指定的元素，则返回 true。\nboolean contains(Object o)\n// 返回在此 set 的元素上以降序进行迭代的迭代器。\nIterator<E> descendingIterator()\n// 返回此 set 中所包含元素的逆序视图。\nNavigableSet<E> descendingSet()\n// 比较指定对象与此 set 的相等性。\nboolean equals(Object o)\n// 返回此 set 中当前第一个（最低）元素。\nE first()\n// 返回此 set 中小于等于给定元素的最大元素；如果不存在这样的元素，则返回 null。\nE floor(E e)\n// 返回此 set 的部分视图，其元素严格小于 toElement。\nNavigableSet<E> headSet(E toElement)\n// 返回此 set 的部分视图，其元素小于（或等于，如果 inclusive 为 true）toElement。\nNavigableSet<E> headSet(E toElement, boolean inclusive)\n// 返回此 set 中严格大于给定元素的最小元素；如果不存在这样的元素，则返回 null。\nE higher(E e)\n// 如果此 set 不包含任何元素，则返回 true。\nboolean isEmpty()\n// 返回在此 set 的元素上以升序进行迭代的迭代器。\nIterator<E> iterator()\n// 返回此 set 中当前最后一个（最高）元素。\nE last()\n// 返回此 set 中严格小于给定元素的最大元素；如果不存在这样的元素，则返回 null。\nE lower(E e)\n// 获取并移除第一个（最低）元素；如果此 set 为空，则返回 null。\nE pollFirst()\n// 获取并移除最后一个（最高）元素；如果此 set 为空，则返回 null。\nE pollLast()\n// 如果此 set 中存在指定的元素，则将其移除。\nboolean remove(Object o)\n// 从此 set 中移除包含在指定 collection 中的所有元素。\nboolean removeAll(Collection<?> c)\n// 返回此 set 中的元素数目。\nint size()\n// 返回此 set 的部分视图，其元素范围从 fromElement 到 toElement。\nNavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive)\n// 返回此 set 的部分视图，其元素从 fromElement（包括）到 toElement（不包括）。\nNavigableSet<E> subSet(E fromElement, E toElement)\n// 返回此 set 的部分视图，其元素大于等于 fromElement。\nNavigableSet<E> tailSet(E fromElement)\n// 返回此 set 的部分视图，其元素大于（或等于，如果 inclusive 为 true）fromElement。\nNavigableSet<E> tailSet(E fromElement, boolean inclusive)\n```\n#### ConcurrentSkipListSet示例\nConcurrentSkipListSet是通过ConcurrentSkipListMap实现的，它的接口基本上都是通过调用ConcurrentSkipListMap接口来实现的\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/*\n *   ConcurrentSkipListSet是“线程安全”的集合，而TreeSet是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历集合set”的示例\n *   (01) 当set是ConcurrentSkipListSet对象时，程序能正常运行。\n *   (02) 当set是TreeSet对象时，程序会产生ConcurrentModificationException异常。\n *\n * @author skywang\n */\npublic class ConcurrentSkipListSetDemo1 {\n\n    // TODO: set是TreeSet对象时，程序会出错。\n    //private static Set<String> set = new TreeSet<String>();\n    private static Set<String> set = new ConcurrentSkipListSet<String>();\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对set进行操作！\n        new MyThread(\"a\").start();\n        new MyThread(\"b\").start();\n    }\n\n    private static void printAll() {\n        String value = null;\n        Iterator iter = set.iterator();\n        while(iter.hasNext()) {\n            value = (String)iter.next();\n            System.out.print(value+\", \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 10) {\n                // “线程名” + \"序号\"\n                String val = Thread.currentThread().getName() + (i%6);\n                set.add(val);\n                // 通过“Iterator”遍历set。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\na1, b1, \na1, a1, a2, b1, \nb1, a1, a2, a3, b1,\n\na1, a2, a3, a1, a4, b1, b2, \na2, a1, a2, a3, a4, a5, b1, b2, \na3, a0, a4, a5, a1, b1, a2, b2, \na3, a0, a4, a1, a5, a2, b1, a3, b2, a4, b3, \na5, a0, b1, a1, b2, a2, b3, \na3, a0, a4, a1, a5, a2, b1, a3, b2, a4, b3, a5, b4, \nb1, a0, b2, a1, b3, a2, b4, \na3, a0, a4, a1, a5, a2, b1, a3, b2, a4, b3, a5, b4, b1, b5, \nb2, a0, a1, a2, a3, a4, a5, b3, b1, b4, b2, b5, \nb3, a0, b4, a1, b5, \na2, a0, a3, a1, a4, a2, a5, a3, b0, a4, b1, a5, b2, b0, b3, b1, b4, b2, b5, b3, \nb4, a0, b5, \na1, a2, a3, a4, a5, b0, b1, b2, b3, b4, b5, \na0, a1, a2, a3, a4, a5, b0, b1, b2, b3, b4, b5, \na0, a1, a2, a3, a4, a5, b0, b1, b2, b3, b4, b5, \na0, a1, a2, a3, a4, a5, b0, b1, b2, b3, b4, b5,\n```\n结果说明：\n示例程序中，启动两个线程(线程a和线程b)分别对ConcurrentSkipListSet进行操作。以线程a而言，它会先获取“线程名”+“序号”，然后将该字符串添加到ConcurrentSkipListSet集合中；接着，遍历并输出集合中的全部元素。 线程b的操作和线程a一样，只不过线程b的名字和线程a的名字不同。\n当set是ConcurrentSkipListSet对象时，程序能正常运行。如果将set改为TreeSet时，程序会产生ConcurrentModificationException异常。\n"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(CopyOnWriteArrayList).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n* * *\n## 概要\n本章是\"并发容器\"的CopyOnWriteArrayList篇。接下来，会先对CopyOnWriteArrayList进行基本介绍，然后再说明它的原理，接着通过代码去分析，最后通过示例更进一步的了解CopyOnWriteArrayList。内容包括：\n\n* CopyOnWriteArrayList介绍\n* CopyOnWriteArrayList原理和数据结构\n* CopyOnWriteArrayList函数列表\n* CopyOnWriteArrayList源码分析(JDK1.7.0_40版本)\n* CopyOnWriteArrayList示例\n\n\n## CopyOnWriteArrayList介绍\n它相当于线程安全的ArrayList。和ArrayList一样，它是个可变数组；但是和ArrayList不同的时，它具有以下特性：\n1. 它最适合于具有以下特征的应用程序：List 大小通常保持很小，只读操作远多于可变操作，需要在遍历期间防止线程间的冲突。\n2. 它是线程安全的。\n3. 因为通常需要复制整个基础数组，所以可变操作（add()、set() 和 remove() 等等）的开销很大。\n4. 迭代器支持hasNext(), next()等不可变操作，但不支持可变 remove()等操作。\n5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。\n建议：在学习CopyOnWriteArraySet之前,先对ArrayList进行了解！ \n\n#### CopyOnWriteArrayList原理和数据结构\nCopyOnWriteArrayList的数据结构，如下图所示：![8c9a554d0e197a027a35fe2f0d483b81](大数据成神之路-Java高级特性增强(CopyOnWriteArrayList).resources/D89A47F9-4C4F-4E57-BAE1-A314892BFFD8.jpg)\n说明：\n1.CopyOnWriteArrayList实现了List接口,因此它是一个队列。\n2.CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock，实现了对CopyOnWriteArrayList的互斥访问。\n3. CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。\n下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。\n1. **CopyOnWriteArrayList的“动态数组”机制** -- 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时，都会新建一个数组，并将更新后的数据拷贝到新建的数组中，最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因！CopyOnWriteArrayList就是通过这种方式实现的动态数组；不过正由于它在“添加/修改/删除”数据时，都会新建数组，所以涉及到修改数据的操作，CopyOnWriteArrayList效率很低；但是单单只是进行遍历查找的话，效率比较高。\n2. **CopyOnWriteArrayList的“线程安全”机制** -- 是通过volatile和互斥锁来实现的。(01) CopyOnWriteArrayList是通过\"volatile数组\"来保存数据的。一个线程读取volatile数组时，总能看到其它线程对该volatile变量最后的写入;就这样，通过volatile提供了\"读取到的数据总是最新的\"这个机制的保证。(02) CopyOnWriteArrayList通过互斥锁来保护数据。在\"添加/修改/删除\"数据时，会先\"获取互斥锁\",再修改完毕之后，先将数据更新到“volatile数组”中，然后再\"释放互斥锁\",这样,就达到了保护数据的目的。 \n\n#### CopyOnWriteArrayList函数列表\n```\n// 创建一个空列表。\nCopyOnWriteArrayList()\n// 创建一个按 collection 的迭代器返回元素的顺序包含指定 collection 元素的列表。\nCopyOnWriteArrayList(Collection&lt;? extends E&gt; c)\n// CopyOnWriteArrayList(E[] toCopyIn)\n创建一个保存给定数组的副本的列表。\n// 将指定元素添加到此列表的尾部。\nboolean add(E e)\n// 在此列表的指定位置上插入指定元素。\nvoid add(int index, E element)\n// 按照指定 collection 的迭代器返回元素的顺序，将指定 collection 中的所有元素添加此列表的尾部。\nboolean addAll(Collection&lt;? extends E&gt; c)\n// 从指定位置开始，将指定 collection 的所有元素插入此列表。\nboolean addAll(int index, Collection&lt;? extends E&gt; c)\n// 按照指定 collection 的迭代器返回元素的顺序，将指定 collection 中尚未包含在此列表中的所有元素添加列表的尾部。\nint addAllAbsent(Collection&lt;? extends E&gt; c)\n// 添加元素（如果不存在）。\nboolean addIfAbsent(E e)\n// 从此列表移除所有元素。\nvoid clear()\n// 返回此列表的浅表副本。\nObject clone()\n// 如果此列表包含指定的元素，则返回 true。\nboolean contains(Object o)\n// 如果此列表包含指定 collection 的所有元素，则返回 true。\nboolean containsAll(Collection&lt;?&gt; c)\n// 比较指定对象与此列表的相等性。\nboolean equals(Object o)\n// 返回列表中指定位置的元素。\nE get(int index)\n// 返回此列表的哈希码值。\nint hashCode()\n// 返回第一次出现的指定元素在此列表中的索引，从 index 开始向前搜索，如果没有找到该元素，则返回 -1。\nint indexOf(E e, int index)\n// 返回此列表中第一次出现的指定元素的索引；如果此列表不包含该元素，则返回 -1。\nint indexOf(Object o)\n// 如果此列表不包含任何元素，则返回 true。\nboolean isEmpty()\n// 返回以恰当顺序在此列表元素上进行迭代的迭代器。\nIterator&lt;E&gt; iterator()\n// 返回最后一次出现的指定元素在此列表中的索引，从 index 开始向后搜索，如果没有找到该元素，则返回 -1。\nint lastIndexOf(E e, int index)\n// 返回此列表中最后出现的指定元素的索引；如果列表不包含此元素，则返回 -1。\nint lastIndexOf(Object o)\n// 返回此列表元素的列表迭代器（按适当顺序）。\nListIterator&lt;E&gt; listIterator()\n// 返回列表中元素的列表迭代器（按适当顺序），从列表的指定位置开始。\nListIterator&lt;E&gt; listIterator(int index)\n// 移除此列表指定位置上的元素。\nE remove(int index)\n// 从此列表移除第一次出现的指定元素（如果存在）。\nboolean remove(Object o)\n// 从此列表移除所有包含在指定 collection 中的元素。\nboolean removeAll(Collection&lt;?&gt; c)\n// 只保留此列表中包含在指定 collection 中的元素。\nboolean retainAll(Collection&lt;?&gt; c)\n// 用指定的元素替代此列表指定位置上的元素。\nE set(int index, E element)\n// 返回此列表中的元素数。\nint size()\n// 返回此列表中 fromIndex（包括）和 toIndex（不包括）之间部分的视图。\nList&lt;E&gt; subList(int fromIndex, int toIndex)\n// 返回一个按恰当顺序（从第一个元素到最后一个元素）包含此列表中所有元素的数组。\nObject[] toArray()\n// 返回以恰当顺序（从第一个元素到最后一个元素）包含列表所有元素的数组；返回数组的运行时类型是指定数组的运行时类型。\n&lt;T&gt; T[] toArray(T[] a)\n// 返回此列表的字符串表示形式。\nString toString()\n```\n#### CopyOnWriteArrayList源码分析\n下面我们从\"创建,添加,删除,获取,遍历\"这5个方面去分析CopyOnWriteArrayList的原理。\n**1. 创建**\nCopyOnWriteArrayList共3个构造函数。它们的源码如下：\n```\npublic CopyOnWriteArrayList() {\n    setArray(new Object[0]);\n}\n\npublic CopyOnWriteArrayList(Collection&lt;? extends E&gt; c) {\n    Object[] elements = c.toArray();\n    if (elements.getClass() != Object[].class)\n        elements = Arrays.copyOf(elements, elements.length, Object[].class);\n    setArray(elements);\n}\n\npublic CopyOnWriteArrayList(E[] toCopyIn) {\n    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));\n}\n```\n说明：这3个构造函数都调用了setArray()，setArray()的源码如下：\n```\nprivate volatile transient Object[] array;\nfinal Object[] getArray() {\n    return array;\n}\nfinal void setArray(Object[] a) {\n    array = a;\n}\n```\n说明：setArray()的作用是给array赋值；其中，array是volatile transient Object[]类型，即array是“volatile数组”。关于volatile关键字，我们知道“volatile能让变量变得可见”，即对一个volatile变量的读，总是能看到（任意线程）对这个volatile变量最后的写入。正在由于这种特性，每次更新了“volatile数组”之后，其它线程都能看到对它所做的更新。关于transient关键字，它是在序列化中才起作用，transient变量不会被自动序列化。\n**2. 添加**\n以add(E e)为例，来对\"CopyOnWriteArrayList\"的添加操作进行说明。下面是add(E e)的代码:\n```\npublic boolean add(E e) {\n    final ReentrantLock lock = this.lock;\n    // 获取“锁”\n    lock.lock();\n    try {\n        // 获取原始”volatile数组“中的数据和数据长度。\n        Object[] elements = getArray();\n        int len = elements.length;\n        // 新建一个数组newElements，并将原始数据拷贝到newElements中；\n        // newElements数组的长度=“原始数组的长度”+1\n        Object[] newElements = Arrays.copyOf(elements, len + 1);\n        // 将“新增加的元素”保存到newElements中。\n        newElements[len] = e;\n        // 将newElements赋值给”volatile数组“。\n        setArray(newElements);\n        return true;\n    } finally {\n        // 释放“锁”\n        lock.unlock();\n    }\n}\n```\n**说明:** add(E e)的作用就是将数据e添加到”volatile数组“中。它的实现方式是,新建一个数组,接着将原始的”volatile数组“的数据拷贝到新数组中，然后将新增数据也添加到新数组中:最后,将新数组赋值给”volatile数组“。在add(E e)中有两点需要关注。\n第一,在”添加操作“开始前，获取独占锁(lock)，若此时有需要线程要获取锁，则必须等待；在操作完毕后，释放独占锁(lock)，此时其它线程才能获取锁。通过独占锁，来防止多线程同时修改数据！lock的定义如下：\n```\ntransient final ReentrantLock lock = \nnew ReentrantLock();  \n```\n第二,操作完毕时，会通过setArray()来更新”volatile数组“。而且，前面我们提过”即对一个volatile变量的读，总是能看到（任意线程）对这个volatile变量最后的写入“；这样，每次添加元素之后，其它线程都能看到新添加的元素。 \n**3. 获取**\n以get(int index)为例，来对“CopyOnWriteArrayList的删除操作”进行说明。下面是get(int index)的代码：![51e409b11aa51c150090697429a953ed](大数据成神之路-Java高级特性增强(CopyOnWriteArrayList).resources/465A838B-6367-4535-96B8-C9693E5D0B61.gif)\npublic E get(int index) {\n    return get(getArray(), index);\n}\n\nprivate E get(Object[] a, int index) {\n    return (E) a[index];\n}\n![51e409b11aa51c150090697429a953ed](大数据成神之路-Java高级特性增强(CopyOnWriteArrayList).resources/465A838B-6367-4535-96B8-C9693E5D0B61.gif)\n说明：get(int index)的实现很简单，就是返回\"volatile数组\"中的第index个元素。 \n**4. 删除**\n以remove(int index)为例，来对“CopyOnWriteArrayList的删除操作”进行说明。下面是remove(int index)的代码：\n```\npublic E remove(int index) {\n    final ReentrantLock lock = this.lock;\n    // 获取“锁”\n    lock.lock();\n    try {\n        // 获取原始”volatile数组“中的数据和数据长度。\n        Object[] elements = getArray();\n        int len = elements.length;\n        // 获取elements数组中的第index个数据。\n        E oldValue = get(elements, index);\n        int numMoved = len - index - 1;\n        // 如果被删除的是最后一个元素，则直接通过Arrays.copyOf()进行处理，而不需要新建数组。\n        // 否则，新建数组，然后将”volatile数组中被删除元素之外的其它元素“拷贝到新数组中；最后，将新数组赋值给”volatile数组“。\n        if (numMoved == 0)\n            setArray(Arrays.copyOf(elements, len - 1));\n        else {\n            Object[] newElements = new Object[len - 1];\n            System.arraycopy(elements, 0, newElements, 0, index);\n            System.arraycopy(elements, index + 1, newElements, index,\n                             numMoved);\n            setArray(newElements);\n        }\n        return oldValue;\n    } finally {\n        // 释放“锁”\n        lock.unlock();\n    }\n}\n```\n**说明：**remove(int index)的作用就是将”volatile数组“中第index个元素删除。它的实现方式是，如果被删除的是最后一个元素，则直接通过Arrays.copyOf()进行处理，而不需要新建数组。否则，新建数组，然后将\"volatile数组中被删除元素之外的其它元素\"拷贝到新数组中;最后,将新数组赋值给\"volatile数组\"。和add(E e)一样，remove(int index)也是”在操作之前，获取独占锁；操作完成之后，释放独占是“;并且\"在操作完成时，会通过将数据更新到volatile数组中\"。 \n**5. 遍历**\n以iterator()为例，来对CopyOnWriteArrayList的遍历操作进行说明。\n下面是iterator()的代码：\n```\npublic Iterator<E> iterator() {\n    return new COWIterator<E>(getArray(), 0);\n}\n```\n说明：iterator()会返回COWIterator对象。COWIterator实现额ListIterator接口，它的源码如下：\n```\nprivate static class COWIterator&lt;E&gt; implements ListIterator&lt;E&gt; {\n    private final Object[] snapshot;\n    private int cursor;\n\n    private COWIterator(Object[] elements, int initialCursor) {\n        cursor = initialCursor;\n        snapshot = elements;\n    }\n\n    public boolean hasNext() {\n        return cursor &lt; snapshot.length;\n    }\n\n    public boolean hasPrevious() {\n        return cursor &gt; 0;\n    }\n\n    // 获取下一个元素\n    @SuppressWarnings(\"unchecked\")\n    public E next() {\n        if (! hasNext())\n            throw new NoSuchElementException();\n        return (E) snapshot[cursor++];\n    }\n\n    // 获取上一个元素\n    @SuppressWarnings(\"unchecked\")\n    public E previous() {\n        if (! hasPrevious())\n            throw new NoSuchElementException();\n        return (E) snapshot[--cursor];\n    }\n\n    public int nextIndex() {\n        return cursor;\n    }\n\n    public int previousIndex() {\n        return cursor-1;\n    }\n\n    public void remove() {\n        throw new UnsupportedOperationException();\n    }\n\n    public void set(E e) {\n        throw new UnsupportedOperationException();\n    }\n\n    public void add(E e) {\n        throw new UnsupportedOperationException();\n    }\n}\n```\n说明：COWIterator不支持修改元素的操作。例如，对于remove(),set(),add()等操作,COWIterator都会抛出异常！另外，需要提到的一点是，CopyOnWriteArrayList返回迭代器不会抛出ConcurrentModificationException异常，即它不是fail-fast机制的！\n\n#### CopyOnWriteArrayList示例\n下面，我们通过一个例子去对比ArrayList和CopyOnWriteArrayList。\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/*\n *   CopyOnWriteArrayList是“线程安全”的动态数组，而ArrayList是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历list”的示例\n *   (01) 当list是CopyOnWriteArrayList对象时，程序能正常运行。\n *   (02) 当list是ArrayList对象时，程序会产生ConcurrentModificationException异常。\n *\n */\npublic class CopyOnWriteArrayListTest1 {\n\n    // TODO: list是ArrayList对象时，程序会出错。\n    //private static List<String> list = new ArrayList<String>();\n    private static List<String> list = new CopyOnWriteArrayList<String>();\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对list进行操作！\n        new MyThread(\"ta\").start();\n        new MyThread(\"tb\").start();\n    }\n\n    private static void printAll() {\n        String value = null;\n        Iterator iter = list.iterator();\n        while(iter.hasNext()) {\n            value = (String)iter.next();\n            System.out.print(value+\", \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 6) {\n                // “线程名” + \"-\" + \"序号\"\n                String val = Thread.currentThread().getName()+\"-\"+i;\n                list.add(val);\n                // 通过“Iterator”遍历List。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\nta-1, tb-1, ta-1, \ntb-1, \nta-1, ta-1, tb-1, tb-1, tb-2, \ntb-2, ta-1, ta-2, \ntb-1, ta-1, tb-2, tb-1, ta-2, tb-2, tb-3, \nta-2, ta-1, tb-3, tb-1, ta-3, \ntb-2, ta-1, ta-2, tb-1, tb-3, tb-2, ta-3, ta-2, tb-4, \ntb-3, ta-1, ta-3, tb-1, tb-4, tb-2, ta-4, \nta-2, ta-1, tb-3, tb-1, ta-3, tb-2, tb-4, ta-2, ta-4, tb-3, tb-5, \nta-3, ta-1, tb-4, tb-1, ta-4, tb-2, tb-5, ta-2, ta-5, \ntb-3, ta-1, ta-3, tb-1, tb-4, tb-2, ta-4, ta-2, tb-5, tb-3, ta-5, ta-3, tb-6, \ntb-4, ta-4, tb-5, ta-5, tb-6, ta-6,\n```\n结果说明：如果将源码中的list改成ArrayList对象时，程序会产生ConcurrentModificationException异常。"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(CopyOnWriteArraySet).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n#### 概要\n内容包括：\n* CopyOnWriteArraySet介绍\n* CopyOnWriteArraySet原理和数据结构\n* CopyOnWriteArraySet函数列表\n* CopyOnWriteArraySet源码\n* CopyOnWriteArraySet示例\n\n\n#### CopyOnWriteArraySet介绍\n它是线程安全的无序的集合，可以将它理解成线程安全的HashSet。有意思的是，CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet；但是，HashSet是通过“散列表(HashMap)”实现的，而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的，并不是散列表。\n和CopyOnWriteArrayList类似，CopyOnWriteArraySet具有以下特性：\n1. 它最适合于具有以下特征的应用程序：Set 大小通常保持很小，只读操作远多于可变操作，需要在遍历期间防止线程间的冲突。\n2. 它是线程安全的。\n3. 因为通常需要复制整个基础数组，所以可变操作（add()、set() 和 remove() 等等）的开销很大。\n4. 迭代器支持hasNext(), next()等不可变操作，但不支持可变 remove()等 操作。\n5. 使用迭代器进行遍历的速度很快，并且不会与其他线程发生冲突。在构造迭代器时，迭代器依赖于不变的数组快照。\n\n建议：在学习CopyOnWriteArraySet之前,先对HashSet进行了解。\n\n#### CopyOnWriteArraySet原理和数据结构\n\nCopyOnWriteArraySet的数据结构，如下图所示：![126a88369f7024046d46da2fb20bba03](大数据成神之路-Java高级特性增强(CopyOnWriteArraySet).resources/F023A78F-7010-495C-B34F-FBCDE1801AF8.jpg)\n说明：  \n1. CopyOnWriteArraySet继承于AbstractSet，这就意味着它是一个集合。  \n2. CopyOnWriteArraySet包含CopyOnWriteArrayList对象，它是通过CopyOnWriteArrayList实现的。而CopyOnWriteArrayList本质是个动态数组队列，所以CopyOnWriteArraySet相当于通过通过动态数组实现的“集合”！ CopyOnWriteArrayList中允许有重复的元素；但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时，只有当元素不存在时才执行添加操作！   \n至于CopyOnWriteArraySet的\"线程安全\"机制,和CopyOnWriteArrayList一样,是通过volatile和互斥锁来实现的。这个在前一章节介绍CopyOnWriteArrayList时数据结构时,已经进行了说明，这里就不再重复叙述了。\n\n#### CopyOnWriteArraySet函数列表\n```\n// 创建一个空 set。\nCopyOnWriteArraySet()\n// 创建一个包含指定 collection 所有元素的 set。\nCopyOnWriteArraySet(Collection<? extends E> c)\n\n// 如果指定元素并不存在于此 set 中，则添加它。\nboolean add(E e)\n// 如果此 set 中没有指定 collection 中的所有元素，则将它们都添加到此 set 中。\nboolean addAll(Collection<? extends E> c)\n// 移除此 set 中的所有元素。\nvoid clear()\n// 如果此 set 包含指定元素，则返回 true。\nboolean contains(Object o)\n// 如果此 set 包含指定 collection 的所有元素，则返回 true。\nboolean containsAll(Collection<?> c)\n// 比较指定对象与此 set 的相等性。\nboolean equals(Object o)\n// 如果此 set 不包含任何元素，则返回 true。\nboolean isEmpty()\n// 返回按照元素添加顺序在此 set 中包含的元素上进行迭代的迭代器。\nIterator<E> iterator()\n// 如果指定元素存在于此 set 中，则将其移除。\nboolean remove(Object o)\n// 移除此 set 中包含在指定 collection 中的所有元素。\nboolean removeAll(Collection<?> c)\n// 仅保留此 set 中那些包含在指定 collection 中的元素。\nboolean retainAll(Collection<?> c)\n// 返回此 set 中的元素数目。\nint size()\n// 返回一个包含此 set 所有元素的数组。\nObject[] toArray()\n// 返回一个包含此 set 所有元素的数组；返回数组的运行时类型是指定数组的类型。\n<T> T[] toArray(T[] a)\n```\n#### CopyOnWriteArraySet示例\nCopyOnWriteArraySet是通过CopyOnWriteArrayList实现的，它的API基本上都是通过调用CopyOnWriteArrayList的API来实现的。相信对CopyOnWriteArrayList了解的话，对CopyOnWriteArraySet的了解是水到渠成的事。\n下面，我们通过一个例子去对比HashSet和CopyOnWriteArraySet。\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/*\n *   CopyOnWriteArraySet是“线程安全”的集合，而HashSet是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历集合set”的示例\n *   (01) 当set是CopyOnWriteArraySet对象时，程序能正常运行。\n *   (02) 当set是HashSet对象时，程序会产生ConcurrentModificationException异常。\n *\n */\npublic class CopyOnWriteArraySetTest1 {\n\n    // TODO: set是HashSet对象时，程序会出错。\n    //private static Set<String> set = new HashSet<String>();\n    private static Set<String> set = new CopyOnWriteArraySet<String>();\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对set进行操作！\n        new MyThread(\"ta\").start();\n        new MyThread(\"tb\").start();\n    }\n\n    private static void printAll() {\n        String value = null;\n        Iterator iter = set.iterator();\n        while(iter.hasNext()) {\n            value = (String)iter.next();\n            System.out.print(value+\", \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 10) {\n                // “线程名” + \"-\" + \"序号\"\n                String val = Thread.currentThread().getName() + \"-\" + (i%6);\n                set.add(val);\n                // 通过“Iterator”遍历set。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\nta-1, tb-1, ta-1, \ntb-1, ta-1, \ntb-1, ta-1, ta-2, \ntb-1, ta-1, ta-2, tb-1, tb-2, \nta-2, ta-1, tb-2, tb-1, ta-3, \nta-2, ta-1, tb-2, tb-1, ta-3, ta-2, tb-3, \ntb-2, ta-1, ta-3, tb-1, tb-3, ta-2, ta-4, \ntb-2, ta-1, ta-3, tb-1, tb-3, ta-2, ta-4, tb-2, tb-4, \nta-3, ta-1, tb-3, tb-1, ta-4, ta-2, tb-4, tb-2, ta-5, \nta-3, ta-1, tb-3, tb-1, ta-4, ta-2, tb-4, tb-2, ta-5, ta-3, tb-5, \ntb-3, ta-1, ta-4, tb-1, tb-4, ta-2, ta-5, tb-2, tb-5, ta-3, ta-0, \ntb-3, ta-1, ta-4, tb-1, tb-4, ta-2, ta-5, tb-2, tb-5, ta-3, ta-0, tb-3, tb-0, \nta-4, ta-1, tb-4, tb-1, ta-5, ta-2, tb-5, tb-2, ta-0, ta-3, tb-0, \ntb-3, ta-1, ta-4, tb-1, tb-4, ta-2, ta-5, tb-5, ta-0, tb-0, \nta-1, tb-2, tb-1, ta-3, ta-2, tb-3, tb-2, ta-4, ta-3, tb-4, tb-3, ta-5, ta-4, tb-5, tb-4, ta-0, ta-5, tb-0, \ntb-5, ta-1, ta-0, tb-1, tb-0, \nta-2, ta-1, tb-2, tb-1, ta-3, ta-2, tb-3, tb-2, ta-4, ta-3, tb-4, tb-3, ta-5, tb-5, ta-0, tb-0, \nta-4, ta-1, tb-4, tb-1, ta-5, ta-2, tb-5, tb-2, ta-0, ta-3, tb-0, \ntb-3, ta-1, ta-4, tb-1, tb-4, ta-2, ta-5, tb-2, tb-5, ta-3, ta-0, tb-3, tb-0, \nta-4, tb-4, ta-5, tb-5, ta-0, tb-0,\n```\n结果说明：\n由于set是集合对象,因此它不会包含重复的元素。\n如果将源码中的set改成HashSet对象时,程序会产生ConcurrentModificationException异常。"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(LinkedBlockingDeque).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n### LinkedBlockingDeque介绍\nLinkedBlockingDeque是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式，即可以从队列的头和尾同时操作(插入/删除)；并且，该阻塞队列是支持线程安全。\n\n此外，LinkedBlockingDeque还是可选容量的(防止过度膨胀)，即可以指定队列的容量。如果不指定，默认容量大小等于Integer.MAX_VALUE。\n\n### LinkedBlockingDeque原理和数据结构\nLinkedBlockingDeque的数据结构，如下图所示：\n\n\n![6da00030f98f048da20fa09de9784f74](大数据成神之路-Java高级特性增强(LinkedBlockingDeque).resources/385B998A-3394-4B44-AE6B-B66F7775E0A4.jpg)\n说明：1. LinkedBlockingDeque继承于AbstractQueue，它本质上是一个支持FIFO和FILO的双向的队列。\n2. LinkedBlockingDeque实现了BlockingDeque接口，它支持多线程并发。当多线程竞争同一个资源时，某线程获取到该资源之后，其它线程需要阻塞等待。\n3. LinkedBlockingDeque是通过双向链表实现的。\n3.1 first是双向链表的表头。\n3.2 last是双向链表的表尾。\n3.3 count是LinkedBlockingDeque的实际大小，即双向链表中当前节点个数。\n3.4 capacity是LinkedBlockingDeque的容量，它是在创建LinkedBlockingDeque时指定的。\n3.5 lock是控制对LinkedBlockingDeque的互斥锁，当多个线程竞争同时访问LinkedBlockingDeque时，某线程获取到了互斥锁lock，其它线程则需要阻塞等待，直到该线程释放lock，其它线程才有机会获取lock从而获取cpu执行权。\n3.6 notEmpty和notFull分别是“非空条件”和“未满条件”。通过它们能够更加细腻进行并发控制。\n### LinkedBlockingDeque函数列表\n```\n// 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingDeque。\nLinkedBlockingDeque()\n// 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingDeque，最初包含给定 collection 的元素，以该 collection 迭代器的遍历顺序添加。\nLinkedBlockingDeque(Collection<? extends E> c)\n// 创建一个具有给定（固定）容量的 LinkedBlockingDeque。\nLinkedBlockingDeque(int capacity)\n\n// 在不违反容量限制的情况下，将指定的元素插入此双端队列的末尾。\nboolean add(E e)\n// 如果立即可行且不违反容量限制，则将指定的元素插入此双端队列的开头；如果当前没有空间可用，则抛出 IllegalStateException。\nvoid addFirst(E e)\n// 如果立即可行且不违反容量限制，则将指定的元素插入此双端队列的末尾；如果当前没有空间可用，则抛出 IllegalStateException。\nvoid addLast(E e)\n// 以原子方式 (atomically) 从此双端队列移除所有元素。\nvoid clear()\n// 如果此双端队列包含指定的元素，则返回 true。\nboolean contains(Object o)\n// 返回在此双端队列的元素上以逆向连续顺序进行迭代的迭代器。\nIterator<E> descendingIterator()\n// 移除此队列中所有可用的元素，并将它们添加到给定 collection 中。\nint drainTo(Collection<? super E> c)\n// 最多从此队列中移除给定数量的可用元素，并将这些元素添加到给定 collection 中。\nint drainTo(Collection<? super E> c, int maxElements)\n// 获取但不移除此双端队列表示的队列的头部。\nE element()\n// 获取，但不移除此双端队列的第一个元素。\nE getFirst()\n// 获取，但不移除此双端队列的最后一个元素。\nE getLast()\n// 返回在此双端队列元素上以恰当顺序进行迭代的迭代器。\nIterator<E> iterator()\n// 如果立即可行且不违反容量限制，则将指定的元素插入此双端队列表示的队列中（即此双端队列的尾部），并在成功时返回 true；如果当前没有空间可用，则返回 false。\nboolean offer(E e)\n// 将指定的元素插入此双端队列表示的队列中（即此双端队列的尾部），必要时将在指定的等待时间内一直等待可用空间。\nboolean offer(E e, long timeout, TimeUnit unit)\n// 如果立即可行且不违反容量限制，则将指定的元素插入此双端队列的开头，并在成功时返回 true；如果当前没有空间可用，则返回 false。\nboolean offerFirst(E e)\n// 将指定的元素插入此双端队列的开头，必要时将在指定的等待时间内等待可用空间。\nboolean offerFirst(E e, long timeout, TimeUnit unit)\n// 如果立即可行且不违反容量限制，则将指定的元素插入此双端队列的末尾，并在成功时返回 true；如果当前没有空间可用，则返回 false。\nboolean offerLast(E e)\n// 将指定的元素插入此双端队列的末尾，必要时将在指定的等待时间内等待可用空间。\nboolean offerLast(E e, long timeout, TimeUnit unit)\n// 获取但不移除此双端队列表示的队列的头部（即此双端队列的第一个元素）；如果此双端队列为空，则返回 null。\nE peek()\n// 获取，但不移除此双端队列的第一个元素；如果此双端队列为空，则返回 null。\nE peekFirst()\n// 获取，但不移除此双端队列的最后一个元素；如果此双端队列为空，则返回 null。\nE peekLast()\n// 获取并移除此双端队列表示的队列的头部（即此双端队列的第一个元素）；如果此双端队列为空，则返回 null。\nE poll()\n// 获取并移除此双端队列表示的队列的头部（即此双端队列的第一个元素），如有必要将在指定的等待时间内等待可用元素。\nE poll(long timeout, TimeUnit unit)\n// 获取并移除此双端队列的第一个元素；如果此双端队列为空，则返回 null。\nE pollFirst()\n// 获取并移除此双端队列的第一个元素，必要时将在指定的等待时间等待可用元素。\nE pollFirst(long timeout, TimeUnit unit)\n// 获取并移除此双端队列的最后一个元素；如果此双端队列为空，则返回 null。\nE pollLast()\n// 获取并移除此双端队列的最后一个元素，必要时将在指定的等待时间内等待可用元素。\nE pollLast(long timeout, TimeUnit unit)\n// 从此双端队列所表示的堆栈中弹出一个元素。\nE pop()\n// 将元素推入此双端队列表示的栈。\nvoid push(E e)\n// 将指定的元素插入此双端队列表示的队列中（即此双端队列的尾部），必要时将一直等待可用空间。\nvoid put(E e)\n// 将指定的元素插入此双端队列的开头，必要时将一直等待可用空间。\nvoid putFirst(E e)\n// 将指定的元素插入此双端队列的末尾，必要时将一直等待可用空间。\nvoid putLast(E e)\n// 返回理想情况下（没有内存和资源约束）此双端队列可不受阻塞地接受的额外元素数。\nint remainingCapacity()\n// 获取并移除此双端队列表示的队列的头部。\nE remove()\n// 从此双端队列移除第一次出现的指定元素。\nboolean remove(Object o)\n// 获取并移除此双端队列第一个元素。\nE removeFirst()\n// 从此双端队列移除第一次出现的指定元素。\nboolean removeFirstOccurrence(Object o)\n// 获取并移除此双端队列的最后一个元素。\nE removeLast()\n// 从此双端队列移除最后一次出现的指定元素。\nboolean removeLastOccurrence(Object o)\n// 返回此双端队列中的元素数。\nint size()\n// 获取并移除此双端队列表示的队列的头部（即此双端队列的第一个元素），必要时将一直等待可用元素。\nE take()\n// 获取并移除此双端队列的第一个元素，必要时将一直等待可用元素。\nE takeFirst()\n// 获取并移除此双端队列的最后一个元素，必要时将一直等待可用元素。\nE takeLast()\n// 返回以恰当顺序（从第一个元素到最后一个元素）包含此双端队列所有元素的数组。\nObject[] toArray()\n// 返回以恰当顺序包含此双端队列所有元素的数组；返回数组的运行时类型是指定数组的运行时类型。\n<T> T[] toArray(T[] a)\n// 返回此 collection 的字符串表示形式。\nString toString()\n```\n### LinkedBlockingDeque源码分析\n下面从ArrayBlockingQueue的创建，添加，取出，遍历这几个方面对LinkedBlockingDeque进行分析\n\n**1. 创建**\n\n下面以LinkedBlockingDeque(int capacity)来进行说明。\n```\npublic LinkedBlockingDeque(int capacity) {\n    if (capacity <= 0) throw new IllegalArgumentException();\n    this.capacity = capacity;\n}\n```\n说明：capacity是“链式阻塞队列”的容量。\n\nLinkedBlockingDeque中相关的数据结果定义如下：\n```\n// “双向队列”的表头\ntransient Node<E> first;\n// “双向队列”的表尾\ntransient Node<E> last;\n// 节点数量\nprivate transient int count;\n// 容量\nprivate final int capacity;\n// 互斥锁 , 互斥锁对应的“非空条件notEmpty”, 互斥锁对应的“未满条件notFull”\nfinal ReentrantLock lock = new ReentrantLock();\nprivate final Condition notEmpty = lock.newCondition();\nprivate final Condition notFull = lock.newCondition();\n```\n说明：lock是互斥锁，用于控制多线程对LinkedBlockingDeque中元素的互斥访问；而notEmpty和notFull是与lock绑定的条件，它们用于实现对多线程更精确的控制。\n\n双向链表的节点Node的定义如下：\n```\nstatic final class Node<E> {\n    E item;       // 数据\n    Node<E> prev; // 前一节点\n    Node<E> next; // 后一节点\n\n    Node(E x) { item = x; }\n}\n```\n2. 添加\n\n下面以offer(E e)为例，对LinkedBlockingDeque的添加方法进行说明。\n```\npublic boolean offer(E e) {\n    return offerLast(e);\n}\n```\noffer()实际上是调用offerLast()将元素添加到队列的末尾。\nofferLast()的源码如下：\n```\npublic boolean offerLast(E e) {\n    if (e == null) throw new NullPointerException();\n    // 新建节点\n    Node<E> node = new Node<E>(e);\n    final ReentrantLock lock = this.lock;\n    // 获取锁\n    lock.lock();\n    try {\n        // 将“新节点”添加到双向链表的末尾\n        return linkLast(node);\n    } finally {\n        // 释放锁\n        lock.unlock();\n    }\n}\n```\n说明：offerLast()的作用，是新建节点并将该节点插入到双向链表的末尾。它在插入节点前，会获取锁；操作完毕，再释放锁。\n\nlinkLast()的源码如下：\n```\nprivate boolean linkLast(Node<E> node) {\n    // 如果“双向链表的节点数量” > “容量”，则返回false，表示插入失败。\n    if (count >= capacity)\n        return false;\n    // 将“node添加到链表末尾”，并设置node为新的尾节点\n    Node<E> l = last;\n    node.prev = l;\n    last = node;\n    if (first == null)\n        first = node;\n    else\n        l.next = node;\n    // 将“节点数量”+1\n    ++count;\n    // 插入节点之后，唤醒notEmpty上的等待线程。\n    notEmpty.signal();\n    return true;\n}\n```\n说明：linkLast()的作用，是将节点插入到双向队列的末尾；插入节点之后，唤醒notEmpty上的等待线程。\n\n**3. 删除**\n\n下面以take()为例，对LinkedBlockingDeque的取出方法进行说明。\n```\npublic E take() throws InterruptedException {\n    return takeFirst();\n}\n```\ntake()实际上是调用takeFirst()队列的第一个元素。\ntakeFirst()的源码如下：\n```\npublic E takeFirst() throws InterruptedException {\n    final ReentrantLock lock = this.lock;\n    // 获取锁\n    lock.lock();\n    try {\n        E x;\n        // 若“队列为空”，则一直等待。否则，通过unlinkFirst()删除第一个节点。\n        while ( (x = unlinkFirst()) == null)\n            notEmpty.await();\n        return x;\n    } finally {\n        // 释放锁\n        lock.unlock();\n    }\n}\n```\n说明：takeFirst()的作用，是删除双向链表的第一个节点，并返回节点对应的值。它在插入节点前，会获取锁；操作完毕，再释放锁。\n\nunlinkFirst()的源码如下：\n```\nprivate E unlinkFirst() {\n    // assert lock.isHeldByCurrentThread();\n    Node<E> f = first;\n    if (f == null)\n        return null;\n    // 删除并更新“第一个节点”\n    Node<E> n = f.next;\n    E item = f.item;\n    f.item = null;\n    f.next = f; // help GC\n    first = n;\n    if (n == null)\n        last = null;\n    else\n        n.prev = null;\n    // 将“节点数量”-1\n    --count;\n    // 删除节点之后，唤醒notFull上的等待线程。\n    notFull.signal();\n    return item;\n}\n```\n说明：unlinkFirst()的作用，是将双向队列的第一个节点删除；删除节点之后，唤醒notFull上的等待线程。\n\n4. 遍历\n下面对LinkedBlockingDeque的遍历方法进行说明。\n\n```\npublic Iterator<E> iterator() {\n    return new Itr();\n}\n```\niterator()实际上是返回一个Iter对象。\nItr类的定义如下：\n```\nprivate class Itr extends AbstractItr {\n    // “双向队列”的表头\n    Node<E> firstNode() { return first; }\n    // 获取“节点n的下一个节点”\n    Node<E> nextNode(Node<E> n) { return n.next; }\n}\n```\nItr继承于AbstractItr，而AbstractItr的定义如下：\n```\nprivate abstract class AbstractItr implements Iterator<E> {\n    // next是下一次调用next()会返回的节点。\n    Node<E> next;\n    // nextItem是next()返回节点对应的数据。\n    E nextItem;\n    // 上一次next()返回的节点。\n    private Node<E> lastRet;\n    // 返回第一个节点\n    abstract Node<E> firstNode();\n    // 返回下一个节点\n    abstract Node<E> nextNode(Node<E> n);\n\n    AbstractItr() {\n        final ReentrantLock lock = LinkedBlockingDeque.this.lock;\n        // 获取“LinkedBlockingDeque的互斥锁”\n        lock.lock();\n        try {\n            // 获取“双向队列”的表头\n            next = firstNode();\n            // 获取表头对应的数据\n            nextItem = (next == null) ? null : next.item;\n        } finally {\n            // 释放“LinkedBlockingDeque的互斥锁”\n            lock.unlock();\n        }\n    }\n\n    // 获取n的后继节点\n    private Node<E> succ(Node<E> n) {\n        // Chains of deleted nodes ending in null or self-links\n        // are possible if multiple interior nodes are removed.\n        for (;;) {\n            Node<E> s = nextNode(n);\n            if (s == null)\n                return null;\n            else if (s.item != null)\n                return s;\n            else if (s == n)\n                return firstNode();\n            else\n                n = s;\n        }\n    }\n\n    // 更新next和nextItem。\n    void advance() {\n        final ReentrantLock lock = LinkedBlockingDeque.this.lock;\n        lock.lock();\n        try {\n            // assert next != null;\n            next = succ(next);\n            nextItem = (next == null) ? null : next.item;\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    // 返回“下一个节点是否为null”\n    public boolean hasNext() {\n        return next != null;\n    }\n\n    // 返回下一个节点\n    public E next() {\n        if (next == null)\n            throw new NoSuchElementException();\n        lastRet = next;\n        E x = nextItem;\n        advance();\n        return x;\n    }\n\n    // 删除下一个节点\n    public void remove() {\n        Node<E> n = lastRet;\n        if (n == null)\n            throw new IllegalStateException();\n        lastRet = null;\n        final ReentrantLock lock = LinkedBlockingDeque.this.lock;\n        lock.lock();\n        try {\n            if (n.item != null)\n                unlink(n);\n        } finally {\n            lock.unlock();\n        }\n    }\n}\n```\n### LinkedBlockingDeque示例\n\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/*\n *   LinkedBlockingDeque是“线程安全”的队列，而LinkedList是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历queue”的示例\n *   (01) 当queue是LinkedBlockingDeque对象时，程序能正常运行。\n *   (02) 当queue是LinkedList对象时，程序会产生ConcurrentModificationException异常。\n *\n */\npublic class LinkedBlockingDequeDemo1 {\n\n    // TODO: queue是LinkedList对象时，程序会出错。\n    //private static Queue<String> queue = new LinkedList<String>();\n    private static Queue<String> queue = new LinkedBlockingDeque<String>();\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对queue进行操作！\n        new MyThread(\"ta\").start();\n        new MyThread(\"tb\").start();\n    }\n\n    private static void printAll() {\n        String value;\n        Iterator iter = queue.iterator();\n        while(iter.hasNext()) {\n            value = (String)iter.next();\n            System.out.print(value+\", \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 6) {\n                // “线程名” + \"-\" + \"序号\"\n                String val = Thread.currentThread().getName()+i;\n                queue.add(val);\n                // 通过“Iterator”遍历queue。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\nta1, ta1, tb1, tb1,\n\nta1, ta1, tb1, tb1, tb2, tb2, ta2, \nta2, \nta1, ta1, tb1, tb1, tb2, tb2, ta2, ta2, tb3, tb3, ta3, \nta3, ta1, \ntb1, ta1, tb2, tb1, ta2, tb2, tb3, ta2, ta3, tb3, tb4, ta3, ta4, \ntb4, ta1, ta4, tb1, tb5, \ntb2, ta1, ta2, tb1, tb3, tb2, ta3, ta2, tb4, tb3, ta4, ta3, tb5, tb4, ta5, \nta4, ta1, tb5, tb1, ta5, tb2, tb6, \nta2, ta1, tb3, tb1, ta3, tb2, tb4, ta2, ta4, tb3, tb5, ta3, ta5, tb4, tb6, ta4, ta6, \ntb5, ta5, tb6, ta6,\n```\n结果说明：示例程序中，启动两个线程(线程ta和线程tb)分别对LinkedBlockingDeque进行操作。以线程ta而言，它会先获取“线程名”+“序号”，然后将该字符串添加到LinkedBlockingDeque中；接着，遍历并输出LinkedBlockingDeque中的全部元素。 线程tb的操作和线程ta一样，只不过线程tb的名字和线程ta的名字不同。\n当queue是LinkedBlockingDeque对象时，程序能正常运行。如果将queue改为LinkedList时，程序会产生ConcurrentModificationException异常。"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(LinkedBlockingQueue).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n### LinkedBlockingQueue介绍\nLinkedBlockingQueue是一个单向链表实现的阻塞队列。该队列按 FIFO（先进先出）排序元素，新元素插入到队列的尾部，并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列，但是在大多数并发应用程序中，其可预知的性能要低。\n\n此外，LinkedBlockingQueue还是可选容量的(防止过度膨胀)，即可以指定队列的容量。如果不指定，默认容量大小等于Integer.MAX_VALUE。\n\n### LinkedBlockingQueue原理和数据结构\nLinkedBlockingQueue的数据结构，如下图所示：![f8ea78da236a6ad11f3a04f1472e9017](大数据成神之路-Java高级特性增强(LinkedBlockingQueue).resources/C0887B04-3B23-4874-B6D5-702B2FE76242.jpg)\n说明：\n\n1. LinkedBlockingQueue继承于AbstractQueue，它本质上是一个FIFO(先进先出)的队列。\n2. LinkedBlockingQueue实现了BlockingQueue接口，它支持多线程并发。当多线程竞争同一个资源时，某线程获取到该资源之后，其它线程需要阻塞等待。\n3. LinkedBlockingQueue是通过单链表实现的:\n(01) head是链表的表头。取出数据时，都是从表头head处插入。\n(02) last是链表的表尾。新增数据时，都是从表尾last处插入。(03) count是链表的实际大小，即当前链表中包含的节点个数。(04) capacity是列表的容量，它是在创建链表时指定的。\n(05) putLock是插入锁，takeLock是取出锁；notEmpty是“非空条件”，notFull是“未满条件”。通过它们对链表进行并发控制。       \nLinkedBlockingQueue在实现“多线程对竞争资源的互斥访问”时，对于“插入”和“取出(删除)”操作分别使用了不同的锁。对于插入操作，通过“插入锁putLock”进行同步；对于取出操作，通过“取出锁takeLock”进行同步。       \n此外，插入锁putLock和“非满条件notFull”相关联，取出锁takeLock和“非空条件notEmpty”相关联。通过notFull和notEmpty更细腻的控制锁。\n\n```\n若某线程(线程A)要取出数据时，队列正好为空，则该线程会执行notEmpty.await()进行等待；当其它某个线程(线程B)向队列中插入了数据之后，会调用notEmpty.signal()唤醒“notEmpty上的等待线程”。此时，线程A会被唤醒从而得以继续运行。 此外，线程A在执行取操作前，会获取takeLock，在取操作执行完毕再释放takeLock。\n若某线程(线程H)要插入数据时，队列已满，则该线程会它执行notFull.await()进行等待；当其它某个线程(线程I)取出数据之后，会调用notFull.signal()唤醒“notFull上的等待线程”。此时，线程H就会被唤醒从而得以继续运行。 此外，线程H在执行插入操作前，会获取putLock，在插入操作执行完毕才释放putLock。\n```\n### LinkedBlockingQueue函数列表\n\n```\n// 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue。\nLinkedBlockingQueue()\n// 创建一个容量是 Integer.MAX_VALUE 的 LinkedBlockingQueue，最初包含给定 collection 的元素，元素按该 collection 迭代器的遍历顺序添加。\nLinkedBlockingQueue(Collection<? extends E> c)\n// 创建一个具有给定（固定）容量的 LinkedBlockingQueue。\nLinkedBlockingQueue(int capacity)\n\n// 从队列彻底移除所有元素。\nvoid clear()\n// 移除此队列中所有可用的元素，并将它们添加到给定 collection 中。\nint drainTo(Collection<? super E> c)\n// 最多从此队列中移除给定数量的可用元素，并将这些元素添加到给定 collection 中。\nint drainTo(Collection<? super E> c, int maxElements)\n// 返回在队列中的元素上按适当顺序进行迭代的迭代器。\nIterator<E> iterator()\n// 将指定元素插入到此队列的尾部（如果立即可行且不会超出此队列的容量），在成功时返回 true，如果此队列已满，则返回 false。\nboolean offer(E e)\n// 将指定元素插入到此队列的尾部，如有必要，则等待指定的时间以使空间变得可用。\nboolean offer(E e, long timeout, TimeUnit unit)\n// 获取但不移除此队列的头；如果此队列为空，则返回 null。\nE peek()\n// 获取并移除此队列的头，如果此队列为空，则返回 null。\nE poll()\n// 获取并移除此队列的头部，在指定的等待时间前等待可用的元素（如果有必要）。\nE poll(long timeout, TimeUnit unit)\n// 将指定元素插入到此队列的尾部，如有必要，则等待空间变得可用。\nvoid put(E e)\n// 返回理想情况下（没有内存和资源约束）此队列可接受并且不会被阻塞的附加元素数量。\nint remainingCapacity()\n// 从此队列移除指定元素的单个实例（如果存在）。\nboolean remove(Object o)\n// 返回队列中的元素个数。\nint size()\n// 获取并移除此队列的头部，在元素变得可用之前一直等待（如果有必要）。\nE take()\n// 返回按适当顺序包含此队列中所有元素的数组。\nObject[] toArray()\n// 返回按适当顺序包含此队列中所有元素的数组；返回数组的运行时类型是指定数组的运行时类型。\n<T> T[] toArray(T[] a)\n// 返回此 collection 的字符串表示形式。\nString toString()\n```\n### LinkedBlockingQueue源码分析\n\n下面从LinkedBlockingQueue的创建，添加，删除，遍历这几个方面对它进行分析。\n**1. 创建**\n下面以LinkedBlockingQueue(int capacity)来进行说明。\n```\npublic LinkedBlockingQueue(int capacity) {\n    if (capacity <= 0) throw new IllegalArgumentException();\n    this.capacity = capacity;\n    last = head = new Node<E>(null);\n}\n```\n说明：\n(01) capacity是“链式阻塞队列”的容量。\n(02) head和last是“链式阻塞队列”的首节点和尾节点。它们在LinkedBlockingQueue中的声明如下：\n```\n// 容量\nprivate final int capacity;\n// 当前数量\nprivate final AtomicInteger count = new AtomicInteger(0);\nprivate transient Node<E> head; // 链表的表头\nprivate transient Node<E> last; // 链表的表尾\n// 用于控制“删除元素”的互斥锁takeLock 和 锁对应的“非空条件”notEmpty\nprivate final ReentrantLock takeLock = new ReentrantLock();\nprivate final Condition notEmpty = takeLock.newCondition();\n// 用于控制“添加元素”的互斥锁putLock 和 锁对应的“非满条件”notFull\nprivate final ReentrantLock putLock = new ReentrantLock();\nprivate final Condition notFull = putLock.newCondition();\n```\n链表的节点定义如下：\n```\nstatic class Node<E> {\n    E item;         // 数据\n    Node<E> next;   // 下一个节点的指针\n\n    Node(E x) { item = x; }\n}\n```\n**2. 添加**\n\n下面以offer(E e)为例，对LinkedBlockingQueue的添加方法进行说明。\n```\npublic boolean offer(E e) {\n    if (e == null) throw new NullPointerException();\n    // 如果“队列已满”，则返回false，表示插入失败。\n    final AtomicInteger count = this.count;\n    if (count.get() == capacity)\n        return false;\n    int c = -1;\n    // 新建“节点e”\n    Node<E> node = new Node(e);\n    final ReentrantLock putLock = this.putLock;\n    // 获取“插入锁putLock”\n    putLock.lock();\n    try {\n        // 再次对“队列是不是满”的进行判断。\n        // 若“队列未满”，则插入节点。\n        if (count.get() < capacity) {\n            // 插入节点\n            enqueue(node);\n            // 将“当前节点数量”+1，并返回“原始的数量”\n            c = count.getAndIncrement();\n            // 如果在插入元素之后，队列仍然未满，则唤醒notFull上的等待线程。\n            if (c + 1 < capacity)\n                notFull.signal();\n        }\n    } finally {\n        // 释放“插入锁putLock”\n        putLock.unlock();\n    }\n    // 如果在插入节点前，队列为空；则插入节点后，唤醒notEmpty上的等待线程\n    if (c == 0)\n        signalNotEmpty();\n    return c >= 0;\n}\n```\n说明：offer()的作用很简单，就是将元素E添加到队列的末尾。\nenqueue()的源码如下：\n```\nprivate void enqueue(Node<E> node) {\n    // assert putLock.isHeldByCurrentThread();\n    // assert last.next == null;\n    last = last.next = node;\n}\n```\nenqueue()的作用是将node添加到队列末尾，并设置node为新的尾节点！\nsignalNotEmpty()的源码如下：\n```\nprivate void signalNotEmpty() {\n    final ReentrantLock takeLock = this.takeLock;\n    takeLock.lock();\n    try {\n        notEmpty.signal();\n    } finally {\n        takeLock.unlock();\n    }\n}\n```\nsignalNotEmpty()的作用是唤醒notEmpty上的等待线程。\n**3. 取出**\n\n下面以take()为例，对LinkedBlockingQueue的取出方法进行说明。\n```\npublic E take() throws InterruptedException {\n    E x;\n    int c = -1;\n    final AtomicInteger count = this.count;\n    final ReentrantLock takeLock = this.takeLock;\n    // 获取“取出锁”，若当前线程是中断状态，则抛出InterruptedException异常\n    takeLock.lockInterruptibly();\n    try {\n        // 若“队列为空”，则一直等待。\n        while (count.get() == 0) {\n            notEmpty.await();\n        }\n        // 取出元素\n        x = dequeue();\n        // 取出元素之后，将“节点数量”-1；并返回“原始的节点数量”。\n        c = count.getAndDecrement();\n        if (c > 1)\n            notEmpty.signal();\n    } finally {\n        // 释放“取出锁”\n        takeLock.unlock();\n    }\n    // 如果在“取出元素之前”，队列是满的；则在取出元素之后，唤醒notFull上的等待线程。\n    if (c == capacity)\n        signalNotFull();\n    return x;\n}\n```\n说明：take()的作用是取出并返回队列的头。若队列为空，则一直等待。\ndequeue()的源码如下：\n```\nprivate E dequeue() {\n    // assert takeLock.isHeldByCurrentThread();\n    // assert head.item == null;\n    Node<E> h = head;\n    Node<E> first = h.next;\n    h.next = h; // help GC\n    head = first;\n    E x = first.item;\n    first.item = null;\n    return x;\n}\n```\ndequeue()的作用就是删除队列的头节点，并将表头指向“原头节点的下一个节点”。\nsignalNotFull()的源码如下：\n```\nprivate void signalNotFull() {\n    final ReentrantLock putLock = this.putLock;\n    putLock.lock();\n    try {\n        notFull.signal();\n    } finally {\n        putLock.unlock();\n    }\n}\n```\nsignalNotFull()的作用就是唤醒notFull上的等待线程。\n\n**4. 遍历**\n下面对LinkedBlockingQueue的遍历方法进行说明。\n```\npublic Iterator<E> iterator() {\n  return new Itr();\n}\n```\niterator()实际上是返回一个Iter对象。\nItr类的定义如下：\n```\nprivate class Itr implements Iterator<E> {\n    // 当前节点\n    private Node<E> current;\n    // 上一次返回的节点\n    private Node<E> lastRet;\n    // 当前节点对应的值\n    private E currentElement;\n\n    Itr() {\n        // 同时获取“插入锁putLock” 和 “取出锁takeLock”\n        fullyLock();\n        try {\n            // 设置“当前元素”为“队列表头的下一节点”，即为队列的第一个有效节点\n            current = head.next;\n            if (current != null)\n                currentElement = current.item;\n        } finally {\n            // 释放“插入锁putLock” 和 “取出锁takeLock”\n            fullyUnlock();\n        }\n    }\n\n    // 返回“下一个节点是否为null”\n    public boolean hasNext() {\n        return current != null;\n    }\n\n    private Node<E> nextNode(Node<E> p) {\n        for (;;) {\n            Node<E> s = p.next;\n            if (s == p)\n                return head.next;\n            if (s == null || s.item != null)\n                return s;\n            p = s;\n        }\n    }\n\n    // 返回下一个节点\n    public E next() {\n        fullyLock();\n        try {\n            if (current == null)\n                throw new NoSuchElementException();\n            E x = currentElement;\n            lastRet = current;\n            current = nextNode(current);\n            currentElement = (current == null) ? null : current.item;\n            return x;\n        } finally {\n            fullyUnlock();\n        }\n    }\n\n    // 删除下一个节点\n    public void remove() {\n        if (lastRet == null)\n            throw new IllegalStateException();\n        fullyLock();\n        try {\n            Node<E> node = lastRet;\n            lastRet = null;\n            for (Node<E> trail = head, p = trail.next;\n                 p != null;\n                 trail = p, p = p.next) {\n                if (p == node) {\n                    unlink(p, trail);\n                    break;\n                }\n            }\n        } finally {\n            fullyUnlock();\n        }\n    }\n}\n```\n### LinkedBlockingQueue示例\n```\nimport java.util.*;\nimport java.util.concurrent.*;\n\n/*\n *   LinkedBlockingQueue是“线程安全”的队列，而LinkedList是非线程安全的。\n *\n *   下面是“多个线程同时操作并且遍历queue”的示例\n *   (01) 当queue是LinkedBlockingQueue对象时，程序能正常运行。\n *   (02) 当queue是LinkedList对象时，程序会产生ConcurrentModificationException异常。\n *\n */\npublic class LinkedBlockingQueueDemo1 {\n\n    // TODO: queue是LinkedList对象时，程序会出错。\n    //private static Queue<String> queue = new LinkedList<String>();\n    private static Queue<String> queue = new LinkedBlockingQueue<String>();\n    public static void main(String[] args) {\n    \n        // 同时启动两个线程对queue进行操作！\n        new MyThread(\"ta\").start();\n        new MyThread(\"tb\").start();\n    }\n\n    private static void printAll() {\n        String value;\n        Iterator iter = queue.iterator();\n        while(iter.hasNext()) {\n            value = (String)iter.next();\n            System.out.print(value+\", \");\n        }\n        System.out.println();\n    }\n\n    private static class MyThread extends Thread {\n        MyThread(String name) {\n            super(name);\n        }\n        @Override\n        public void run() {\n                int i = 0;\n            while (i++ < 6) {\n                // “线程名” + \"-\" + \"序号\"\n                String val = Thread.currentThread().getName()+i;\n                queue.add(val);\n                // 通过“Iterator”遍历queue。\n                printAll();\n            }\n        }\n    }\n}\n```\n其中一次运行结果：\n```\ntb1, ta1, \ntb1, ta1, ta2, \ntb1, ta1, ta2, ta3, \ntb1, ta1, ta2, ta3, ta4, \ntb1, ta1, tb1, ta2, ta1, ta3, ta2, ta4, ta3, ta5, \nta4, tb1, ta5, ta1, ta6, \nta2, tb1, ta3, ta1, ta4, ta2, ta5, ta3, ta6, ta4, tb2, \nta5, ta6, tb2, \ntb1, ta1, ta2, ta3, ta4, ta5, ta6, tb2, tb3, \ntb1, ta1, ta2, ta3, ta4, ta5, ta6, tb2, tb3, tb4, \ntb1, ta1, ta2, ta3, ta4, ta5, ta6, tb2, tb3, tb4, tb5, \ntb1, ta1, ta2, ta3, ta4, ta5, ta6, tb2, tb3, tb4, tb5, tb6,\n```\n结果说明：\n示例程序中，启动两个线程(线程ta和线程tb)分别对LinkedBlockingQueue进行操作。以线程ta而言，它会先获取“线程名”+“序号”，然后将该字符串添加到LinkedBlockingQueue中；接着，遍历并输出LinkedBlockingQueue中的全部元素。 线程tb的操作和线程ta一样，只不过线程tb的名字和线程ta的名字不同。\n当queue是LinkedBlockingQueue对象时，程序能正常运行。如果将queue改为LinkedList时，程序会产生ConcurrentModificationException异常。"
  },
  {
    "path": "并发容器/大数据成神之路-Java高级特性增强(并发容器大纲).md",
    "content": "### **Java高级特性增强-并发容器**\n本部分网络上有大量的资源可以参考，在这里做了部分整理并做了大量勘误，感谢前辈的付出，每节文章末尾有引用列表~\n####**多线程**\n###**集合框架**\n###**NIO**\n###**Java并发容器**\n\n* * *\n## Java并发容器大纲\n我将JUC包中的集合类划分为3部分来进行说明。在简单的了解JUC包中集合类的框架之后，后面的章节再逐步对各个类进行介绍。\n\n#### List和Set\n\nJUC(java.util.concurrent)集合包中的List和Set实现类包括: \n**CopyOnWriteArrayList\nCopyOnWriteArraySet\nConcurrentSkipListSet**\nConcurrentSkipListSet稍后在说明Map时再说明,\nCopyOnWriteArrayList和CopyOnWriteArraySet的框架如下图所示：\n![1a78b3149b2e4a75eecdc8659514d771](大数据成神之路-Java高级特性增强(并发容器大纲).resources/9AB2EEAE-5F35-4C72-9953-E24DB769027D.jpg)\n\nCopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口。CopyOnWriteArrayList是支持高并发的。\nCopyOnWriteArraySet相当于线程安全的HashSet,它继承于AbstractSet类。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象，它是通过CopyOnWriteArrayList实现的。\n\n#### Map\nJUC集合包中Map的实现类包括: ConcurrentHashMap和ConcurrentSkipListMap。它们的框架如下图所示：\n![4187acf99165786a1cf403125c521dd4](大数据成神之路-Java高级特性增强(并发容器大纲).resources/95BA2E89-44BB-461D-A8FA-9C62C4E65FE3.jpg)\n\n* ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap)；它继承于AbstractMap类，并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的，它支持并发。\n\n* ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap); 它继承于AbstractMap类，并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的，它支持并发。\n\n* ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet)；它继承于AbstractSet，并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的，它也支持并发。\n\n#### Queue\n\nJUC集合包中Queue的实现类包括: ArrayBlockingQueue, LinkedBlockingQueue, LinkedBlockingDeque, ConcurrentLinkedQueue和ConcurrentLinkedDeque。它们的框架如下图所示：\n![9e1832eddfe3346bc85588364e25fcf2](大数据成神之路-Java高级特性增强(并发容器大纲).resources/E30FE17C-4587-43C9-9DF3-7C0891408761.jpg)\n\n* ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。\n\n* LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列，该队列按 FIFO（先进先出）排序元素。\n\n* LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列，该阻塞队列同时支持FIFO和FILO两种操作方式。\n\n* ConcurrentLinkedQueue是单向链表实现的无界队列，该队列按 FIFO（先进先出）排序元素。\n\n* ConcurrentLinkedDeque是双向链表实现的无界队列，该队列同时支持FIFO和FILO两种操作方式。\n\n接下来\nHere We Go ~"
  },
  {
    "path": "面试系列/Flume面试题整理/Flume.md",
    "content": "## Flume面试题整理（一）  \n\n### 1、Flume使用场景（☆☆☆☆☆）  \n&emsp; 线上数据一般主要是落地（存储到磁盘）或者通过socket传输给另外一个系统，这种情况下，你很难推动线上应用或服务去修改接口，实现直接向kafka里写数据，这时候你可能就需要flume这样的系统帮你去做传输。  \n\n### 2、Flume丢包问题（☆☆☆☆☆）  \n&emsp; 单机upd的flume source的配置，100+M/s数据量，10w qps flume就开始大量丢包，因此很多公司在搭建系统时，抛弃了Flume，自己研发传输系统，但是往往会参考Flume的Source-Channel-Sink模式。  \n&emsp; 一些公司在Flume工作过程中，会对业务日志进行监控，例如Flume agent中有多少条日志，Flume到Kafka后有多少条日志等等，如果数据丢失保持在1%左右是没有问题的，当数据丢失达到5%左右时就必须采取相应措施。  \n\n### 3、Flume与Kafka的选取  \n&emsp; 采集层主要可以使用Flume、Kafka两种技术。  \n&emsp; Flume：Flume 是管道流方式，提供了很多的默认实现，让用户通过参数部署，及扩展API。  \n&emsp; Kafka：Kafka是一个可持久化的分布式的消息队列。  \n&emsp; Kafka 是一个非常通用的系统。你可以有许多生产者和很多的消费者共享多个主题Topics。相比之下，Flume是一个专用工具被设计为旨在往HDFS，HBase发送数据。它对HDFS有特殊的优化，并且集成了Hadoop的安全特性。所以，Cloudera 建议如果数据被多个系统消费的话，使用kafka；如果数据被设计给Hadoop使用，使用Flume。  \n&emsp; 正如你们所知Flume内置很多的source和sink组件。然而，Kafka明显有一个更小的生产消费者生态系统，并且Kafka的社区支持不好。希望将来这种情况会得到改善，但是目前：使用Kafka意味着你准备好了编写你自己的生产者和消费者代码。如果已经存在的Flume Sources和Sinks满足你的需求，并且你更喜欢不需要任何开发的系统，请使用Flume。  \n&emsp; Flume可以使用拦截器实时处理数据。这些对数据屏蔽或者过量是很有用的。Kafka需要外部的流处理系统才能做到。  \n&emsp; Kafka和Flume都是可靠的系统，通过适当的配置能保证零数据丢失。然而，Flume不支持副本事件。于是，如果Flume代理的一个节点奔溃了，即使使用了可靠的文件管道方式，你也将丢失这些事件直到你恢复这些磁盘。如果你需要一个高可靠性的管道，那么使用Kafka是个更好的选择。  \n&emsp; Flume和Kafka可以很好地结合起来使用。如果你的设计需要从Kafka到Hadoop的流数据，使用Flume代理并配置Kafka的Source读取数据也是可行的：你没有必要实现自己的消费者。你可以直接利用Flume与HDFS及HBase的结合的所有好处。你可以使用Cloudera Manager对消费者的监控，并且你甚至可以添加拦截器进行一些流处理。  \n\n### 4、数据怎么采集到Kafka，实现方式  \n&emsp; 使用官方提供的flumeKafka插件，插件的实现方式是自定义了flume的sink，将数据从channle中取出，通过kafka的producer写入到kafka中，可以自定义分区等。  \n\n### 5、flume管道内存，flume宕机了数据丢失怎么解决  \n&emsp; 1）Flume的channel分为很多种，可以将数据写入到文件。  \n&emsp; 2）防止非首个agent宕机的方法数可以做集群或者主备。  \n\n### 6、flume配置方式，flume集群（详细讲解下）  \n&emsp; Flume的配置围绕着source、channel、sink叙述，flume的集群是做在agent上的，而非机器上。  \n\n### 7、flume不采集Nginx日志，通过Logger4j采集日志，优缺点是什么？  \n&emsp; 优点：Nginx的日志格式是固定的，但是缺少sessionid，通过logger4j采集的日志是带有sessionid的，而session可以通过redis共享，保证了集群日志中的同一session落到不同的tomcat时，sessionId还是一样的，而且logger4j的方式比较稳定，不会宕机。  \n&emsp; 缺点：不够灵活，logger4j的方式和项目结合过于紧密，而flume的方式比较灵活，拔插式比较好，不会影响项目性能。  \n\n### 8、flume和kafka采集日志区别，采集日志时中间停了，怎么记录之前的日志？  \n&emsp; Flume采集日志是通过流的方式直接将日志收集到存储层，而kafka是将缓存在kafka集群，待后期可以采集到存储层。  \n&emsp; Flume采集中间停了，可以采用文件的方式记录之前的日志，而kafka是采用offset的方式记录之前的日志。  \n\n### 9、flume有哪些组件，flume的source、channel、sink具体是做什么的（☆☆☆☆☆）  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Flume%E9%9D%A2%E8%AF%95%E9%A2%98Pics/flume%E7%9A%84source%E3%80%81channel%E3%80%81sink.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n1）source：用于采集数据，Source是产生数据流的地方，同时Source会将产生的数据流传输到Channel，这个有点类似于Java IO部分的Channel。  \n2）channel：用于桥接Sources和Sinks，类似于一个队列。  \n3）sink：从Channel收集数据，将数据写到目标源(可以是下一个Source，也可以是HDFS或者HBase)。  \n**注意：要熟悉source、channel、sink的类型**  \n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/HBase面试题整理/HBase.md",
    "content": "## HBase面试题整理（一）  \n\n### 1、 HBase的特点是什么？   \n&emsp; 1）大：一个表可以有数十亿行，上百万列；  \n&emsp; 2）无模式：每行都有一个可排序的主键和任意多的列，列可以根据需要动态的增加，同一张表中不同的行可以有截然不同的列；  \n&emsp; 3）面向列：面向列（族）的存储和权限控制，列（族）独立检索；  \n&emsp; 4）稀疏：空（null）列并不占用存储空间，表可以设计的非常稀疏；  \n&emsp; 5）数据多版本：每个单元中的数据可以有多个版本，默认情况下版本号自动分配，是单元格插入时的时间戳；  \n&emsp; 6）数据类型单一：Hbase中的数据都是字符串，没有类型。  \n\n### 2、HBase和Hive的区别？  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/HBase%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HBase%E5%92%8CHive%E5%8C%BA%E5%88%AB.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n &emsp; Hive和Hbase是两种基于Hadoop的不同技术--Hive是一种类SQL的引擎，并且运行MapReduce任务，Hbase是一种在Hadoop之上的NoSQL 的Key/vale数据库。\n 当然，这两种工具是可以同时使用的。就像用Google来搜索，用FaceBook进行社交一样，Hive可以用来进行统计查询，HBase可以用来进行实时查询，\n 数据也可以从Hive写到Hbase，设置再从Hbase写回Hive。  \n\n### 3、HBase适用于怎样的情景？  \n&emsp; ① 半结构化或非结构化数据 \n&emsp; 对于数据结构字段不够确定或杂乱无章很难按一个概念去进行抽取的数据适合用HBase。以上面的例子为例，当业务发展需要存储author的email，phone，\naddress信息时RDBMS需要停机维护，而HBase支持动态增加。  \n&emsp; ② 记录非常稀疏  \n&emsp; RDBMS的行有多少列是固定的，为null的列浪费了存储空间。而如上文提到的，HBase为null的Column不会被存储，这样既节省了空间又提高了读性能。  \n&emsp; ③ 多版本数据  \n&emsp; 如上文提到的根据Row key和Column key定位到的Value可以有任意数量的版本值，因此对于需要存储变动历史记录的数据，用HBase就非常方便了。\n比如上例中的author的Address是会变动的，业务上一般只需要最新的值，但有时可能需要查询到历史值。  \n&emsp; ④ 超大数据量  \n&emsp; 当数据量越来越大，RDBMS数据库撑不住了，就出现了读写分离策略，通过一个Master专门负责写操作，多个Slave负责读操作，服务器成本倍增。\n随着压力增加，Master撑不住了，这时就要分库了，把关联不大的数据分开部署，一些join查询不能用了，需要借助中间层。随着数据量的进一步增加，\n一个表的记录越来越大，查询就变得很慢，于是又得搞分表，比如按ID取模分成多个表以减少单个表的记录数。经历过这些事的人都知道过程是多么的折腾。\n采用HBase就简单了，只需要加机器即可，HBase会自动水平切分扩展，跟Hadoop的无缝集成保障了其数据可靠性（HDFS）和海量数据分析的高性能（MapReduce）。  \n\n### 4、描述HBase的rowKey的设计原则？（☆☆☆☆☆）  \n（1）Rowkey长度原则  \n&emsp; Rowkey 是一个二进制码流，Rowkey 的长度被很多开发者建议说设计在10~100 个字节，不过建议是越短越好，不要超过16 个字节。  \n&emsp; 原因如下：  \n&emsp; ① 数据的持久化文件HFile 中是按照KeyValue 存储的，如果Rowkey 过长比如100 个字节，1000 万列数据光Rowkey 就要占用100*1000 万=10 亿个字节，\n将近1G 数据，这会极大影响HFile 的存储效率；  \n&emsp; ② MemStore 将缓存部分数据到内存，如果Rowkey 字段过长内存的有效利用率会降低，系统将无法缓存更多的数据，这会降低检索效率。\n因此Rowkey 的字节长度越短越好。  \n&emsp; ③ 目前操作系统是都是64 位系统，内存8 字节对齐。控制在16 个字节，8 字节的整数倍利用操作系统的最佳特性。  \n（2）Rowkey散列原则  \n&emsp; 如果Rowkey是按时间戳的方式递增，不要将时间放在二进制码的前面，建议将Rowkey的高位作为散列字段，由程序循环生成，低位放时间字段，\n这样将提高数据均衡分布在每个Regionserver 实现负载均衡的几率。如果没有散列字段，首字段直接是时间信息将产生所有新数据都在一个 RegionServer 上堆积的\n热点现象，这样在做数据检索的时候负载将会集中在个别RegionServer，降低查询效率。  \n（3）Rowkey唯一原则  \n&emsp; 必须在设计上保证其唯一性。  \n\n### 5、描述HBase中scan和get的功能以及实现的异同？（☆☆☆☆☆）  \nHBase的查询实现只提供两种方式：  \n&emsp; 1）按指定RowKey 获取唯一一条记录，get方法（org.apache.hadoop.hbase.client.Get） Get 的方法处理分两种 : 设置了ClosestRowBefore 和\n没有设置ClosestRowBefore的rowlock。主要是用来保证行的事务性，即每个get 是以一个row 来标记的。一个row中可以有很多family 和column。  \n&emsp; 2）按指定的条件获取一批记录，scan方法(org.apache.Hadoop.hbase.client.Scan）实现条件查询功能使用的就是scan 方式。  \n&emsp; &emsp; （1）scan 可以通过setCaching 与setBatch 方法提高速度(以空间换时间)；  \n&emsp; &emsp; （2）scan 可以通过setStartRow 与setEndRow 来限定范围([start，end)start 是闭区间，end 是开区间)。范围越小，性能越高。  \n&emsp; &emsp; （3）scan 可以通过setFilter 方法添加过滤器，这也是分页、多条件查询的基础。  \n\n### 6、请描述HBase中scan对象的setCache和setBatch方法的使用？（☆☆☆☆☆）  \n&emsp; setCache用于设置缓存，即设置一次RPC请求可以获取多行数据。对于缓存操作，如果行的数据量非常大，多行数据有可能超过客户端进程的内存容量，\n由此引入批量处理这一解决方案。  \n&emsp; setBatch 用于设置批量处理，批量可以让用户选择每一次ResultScanner实例的next操作要取回多少列，例如，在扫描中设置setBatch(5)，\n则一次next()返回的Result实例会包括5列。如果一行包括的列数超过了批量中设置的值，则可以将这一行分片，每次next操作返回一片，当一行的列数不能被批量中\n设置的值整除时，最后一次返回的Result实例会包含比较少的列，如，一行17列，batch设置为5，则一共返回4个Result实例，这4个实例中包括的列数分别\n为5、5、5、2。  \n&emsp; 组合使用扫描器缓存和批量大小，可以让用户方便地控制扫描一个范围内的行键所需要的RPC调用次数。Cache设置了服务器一次返回的行数，\n而Batch设置了服务器一次返回的列数。  \n&emsp; 假如我们建立了一张有两个列族的表，添加了10行数据，每个行的每个列族下有10列，这意味着整个表一共有200列（或单元格，因为每个列只有一个版本），\n其中每行有20列。  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/HBase%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HBase%E4%B8%ADscan%E5%AF%B9%E8%B1%A1%E7%9A%84setCache%E5%92%8CsetBatch%E6%96%B9%E6%B3%95.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; ① Batch参数决定了一行数据分为几个Result，它只针对一行数据，Batch再大，也只能将一行的数据放入一个Result中。所以当一行数据有10列，\n而Batch为100时，也只能将一行的所有列都放入一个Result，不会混合其他行；  \n&emsp; ② 缓存值决定一次RPC返回几个Result，根据Batch划分的Result个数除以缓存个数可以得到RPC消息个数（之前定义缓存值决定一次返回的行数，\n这是不准确的，准确来说是决定一次RPC返回的Result个数，由于在引入Batch之前，一行封装为一个Result，因此定义缓存值决定一次返回的行数，但引入Batch后，\n更准确的说法是缓存值决定了一次RPC返回的Result个数）；  \n&emsp; &emsp; RPC请求次数 = （行数 * 每行列数） / Min（每行的列数，批量大小） / 扫描器缓存  \n&emsp; 下图展示了缓存和批量两个参数如何联动，下图中有一个包含9行数据的表，每行都包含一些列。使用了一个缓存为6、批量大小为3的扫描器，\n需要三次RPC请求来传送数据：  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/HBase%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HBase%20Table.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n### 7、请详细描述HBase中一个cell的结构？  \n&emsp; HBase中通过row和columns确定的为一个存贮单元称为cell。  \n&emsp; Cell：由{row key, column(=<family> + <label>), version}唯一确定的单元。cell 中的数据是没有类型的，全部是字节码形式存贮。  \n\n### 8、简述HBase中compact用途是什么，什么时候触发，分为哪两种，有什么区别，有哪些相关配置参数？（☆☆☆☆☆）  \n&emsp; 在hbase中每当有memstore数据flush到磁盘之后，就形成一个storefile，当storeFile的数量达到一定程度后，就需要将 storefile 文件来\n进行 compaction 操作。  \n&emsp; Compact 的作用：  \n&emsp; ① 合并文件  \n&emsp; ② 清除过期，多余版本的数据  \n&emsp; ③ 提高读写数据的效率  \n&emsp; HBase 中实现了两种 compaction 的方式：minor and major. 这两种 compaction 方式的区别是：  \n&emsp; 1）Minor 操作只用来做部分文件的合并操作以及包括 minVersion=0 并且设置 ttl 的过期版本清理，不做任何删除数据、多版本数据的清理工作。   \n&emsp; 2）Major 操作是对 Region 下的HStore下的所有StoreFile执行合并操作，最终的结果是整理合并出一个文件。  \n\n### 9、每天百亿数据存入HBase，如何保证数据的存储正确和在规定的时间里全部录入完毕，不残留数据？（☆☆☆☆☆）  \n需求分析：  \n&emsp; 1）百亿数据：证明数据量非常大；  \n&emsp; 2）存入HBase：证明是跟HBase的写入数据有关；  \n&emsp; 3）保证数据的正确：要设计正确的数据结构保证正确性；  \n&emsp; 4）在规定时间内完成：对存入速度是有要求的。  \n解决思路：  \n&emsp; 1）数据量百亿条，什么概念呢？假设一整天60x60x24 = 86400秒都在写入数据，那么每秒的写入条数高达100万条，HBase当然是支持不了每秒百万条数据的，\n所以这百亿条数据可能不是通过实时地写入，而是批量地导入。批量导入推荐使用BulkLoad方式（推荐阅读：Spark之读写HBase），性能是普通写入方式几倍以上；  \n&emsp; 2）存入HBase：普通写入是用JavaAPI put来实现，批量导入推荐使用BulkLoad；  \n&emsp; 3）保证数据的正确：这里需要考虑RowKey的设计、预建分区和列族设计等问题；  \n&emsp; 4）在规定时间内完成也就是存入速度不能过慢，并且当然是越快越好，使用BulkLoad。  \n\n### 10、请列举几个HBase优化方法？（☆☆☆☆☆）  \n1）减少调整  \n&emsp; 减少调整这个如何理解呢？HBase中有几个内容会动态调整，如region（分区）、HFile，所以通过一些方法来减少这些会带来I/O开销的调整。  \n&emsp; ① Region  \n&emsp; &emsp; 如果没有预建分区的话，那么随着region中条数的增加，region会进行分裂，这将增加I/O开销，所以解决方法就是根据你的RowKey设计来进行预建分区，\n减少region的动态分裂。  \n&emsp; ② HFile  \n&emsp; &emsp; HFile是数据底层存储文件，在每个memstore进行刷新时会生成一个HFile，当HFile增加到一定程度时，会将属于一个region的HFile进行合并，\n这个步骤会带来开销但不可避免，但是合并后HFile大小如果大于设定的值，那么HFile会重新分裂。为了减少这样的无谓的I/O开销，建议估计项目数据量大小，\n给HFile设定一个合适的值。  \n2）减少启停  \n&emsp; 数据库事务机制就是为了更好地实现批量写入，较少数据库的开启关闭带来的开销，那么HBase中也存在频繁开启关闭带来的问题。  \n&emsp; ① 关闭Compaction，在闲时进行手动Compaction。  \n&emsp; &emsp; 因为HBase中存在Minor Compaction和Major Compaction，也就是对HFile进行合并，所谓合并就是I/O读写，大量的HFile进行肯定会带来I/O开销，\n甚至是I/O风暴，所以为了避免这种不受控制的意外发生，建议关闭自动Compaction，在闲时进行compaction。  \n&emsp; ② 批量数据写入时采用BulkLoad。  \n&emsp; 如果通过HBase-Shell或者JavaAPI的put来实现大量数据的写入，那么性能差是肯定并且还可能带来一些意想不到的问题，所以当需要写入大量离线数据时\n建议使用BulkLoad。  \n3）减少数据量  \n&emsp; 虽然我们是在进行大数据开发，但是如果可以通过某些方式在保证数据准确性同时减少数据量，何乐而不为呢？  \n&emsp; ① 开启过滤，提高查询速度  \n&emsp; &emsp; 开启BloomFilter，BloomFilter是列族级别的过滤，在生成一个StoreFile同时会生成一个MetaBlock，用于查询时过滤数据  \n&emsp; ② 使用压缩  \n&emsp; &emsp; 一般推荐使用Snappy和LZO压缩  \n4）合理设计  \n&emsp; 在一张HBase表格中RowKey和ColumnFamily的设计是非常重要，好的设计能够提高性能和保证数据的准确性  \n&emsp; ① RowKey设计：应该具备以下几个属性  \n&emsp; &emsp; 散列性：散列性能够保证相同相似的rowkey聚合，相异的rowkey分散，有利于查询。  \n&emsp; &emsp; 简短性：rowkey作为key的一部分存储在HFile中，如果为了可读性将rowKey设计得过长，那么将会增加存储压力。     \n&emsp; &emsp; 唯一性：rowKey必须具备明显的区别性。  \n&emsp; &emsp; 业务性：举例来说：  \n&emsp; &emsp; 假如我的查询条件比较多，而且不是针对列的条件，那么rowKey的设计就应该支持多条件查询。   \n&emsp; &emsp; 如果我的查询要求是最近插入的数据优先，那么rowKey则可以采用叫上Long.Max-时间戳的方式，这样rowKey就是递减排列。  \n&emsp; ② 列族的设计：列族的设计需要看应用场景     \n&emsp; &emsp; 优势：HBase中数据时按列进行存储的，那么查询某一列族的某一列时就不需要全盘扫描，只需要扫描某一列族，减少了读I/O；\n其实多列族设计对减少的作用不是很明显，适用于读多写少的场景  \n&emsp; &emsp; 劣势：降低了写的I/O性能。原因如下：数据写到store以后是先缓存在memstore中，同一个region中存在多个列族则存在多个store，\n每个store都一个memstore，当其实memstore进行flush时，属于同一个region的store中的memstore都会进行flush，增加I/O开销。  \n\n### 11、Region如何预建分区？  \n&emsp; 预分区的目的主要是在创建表的时候指定分区数，提前规划表有多个分区，以及每个分区的区间范围，这样在存储的时候rowkey按照分区的区间存储，\n可以避免region热点问题。  \n&emsp; 通常有两种方案：  \n&emsp; 方案1：shell 方法  \n&emsp; &emsp; create 'tb_splits', {NAME => 'cf',VERSIONS=> 3},{SPLITS => ['10','20','30']}  \n&emsp; 方案2：JAVA程序控制  \n&emsp; &emsp; ① 取样，先随机生成一定数量的rowkey,将取样数据按升序排序放到一个集合里；  \n&emsp; &emsp; ② 根据预分区的region个数，对整个集合平均分割，即是相关的splitKeys；  \n&emsp; &emsp; ③ HBaseAdmin.createTable(HTableDescriptor tableDescriptor,byte[][]splitkeys)可以指定预分区的splitKey，\n即是指定region间的rowkey临界值。  \n\n### 12、HRegionServer宕机如何处理？（☆☆☆☆☆）  \n1）ZooKeeper会监控HRegionServer的上下线情况，当ZK发现某个HRegionServer宕机之后会通知HMaster进行失效备援；  \n2）该HRegionServer会停止对外提供服务，就是它所负责的region暂时停止对外提供服务；  \n3）HMaster会将该HRegionServer所负责的region转移到其他HRegionServer上，并且会对HRegionServer上存在memstore中还未持久化到磁盘中的数据进行恢复；   \n4）这个恢复的工作是由WAL重播来完成，这个过程如下：  \n&emsp; ① wal实际上就是一个文件，存在/hbase/WAL/对应RegionServer路径下。  \n&emsp; ② 宕机发生时，读取该RegionServer所对应的路径下的wal文件，然后根据不同的region切分成不同的临时文件recover.edits。     \n&emsp; ③ 当region被分配到新的RegionServer中，RegionServer读取region时会进行是否存在recover.edits，如果有则进行恢复。  \n\n### 13、HBase读写流程？（☆☆☆☆☆）  \n&emsp; **读**：  \n&emsp; ① HRegionServer保存着meta表以及表数据，要访问表数据，首先Client先去访问zookeeper，从zookeeper里面获取meta表所在的位置信息，\n即找到这个meta表在哪个HRegionServer上保存着。  \n&emsp; ② 接着Client通过刚才获取到的HRegionServer的IP来访问Meta表所在的HRegionServer，从而读取到Meta，进而获取到Meta表中存放的元数据。  \n&emsp; ③ Client通过元数据中存储的信息，访问对应的HRegionServer，然后扫描所在HRegionServer的Memstore和Storefile来查询数据。  \n&emsp; ④ 最后HRegionServer把查询到的数据响应给Client。  \n&emsp; **写**：  \n&emsp; ① Client先访问zookeeper，找到Meta表，并获取Meta表元数据。  \n&emsp; ② 确定当前将要写入的数据所对应的HRegion和HRegionServer服务器。  \n&emsp; ③ Client向该HRegionServer服务器发起写入数据请求，然后HRegionServer收到请求并响应。  \n&emsp; ④ Client先把数据写入到HLog，以防止数据丢失。  \n&emsp; ⑤ 然后将数据写入到Memstore。  \n&emsp; ⑥ 如果HLog和Memstore均写入成功，则这条数据写入成功。  \n&emsp; ⑦ 如果Memstore达到阈值，会把Memstore中的数据flush到Storefile中。  \n&emsp; ⑧ 当Storefile越来越多，会触发Compact合并操作，把过多的Storefile合并成一个大的Storefile。  \n&emsp; ⑨ 当Storefile越来越大，Region也会越来越大，达到阈值后，会触发Split操作，将Region一分为二。  \n\n### 14、HBase内部机制是什么？  \n&emsp; Hbase是一个能适应联机业务的数据库系统  \n&emsp; 物理存储：hbase的持久化数据是将数据存储在HDFS上。  \n&emsp; 存储管理：一个表是划分为很多region的，这些region分布式地存放在很多regionserver上Region内部还可以划分为store，\nstore内部有memstore和storefile。  \n&emsp; 版本管理：hbase中的数据更新本质上是不断追加新的版本，通过compact操作来做版本间的文件合并Region的split。  \n&emsp; 集群管理：ZooKeeper + HMaster + HRegionServer。  \n\n### 15、Hbase中的memstore是用来做什么的？  \n&emsp; hbase为了保证随机读取的性能，所以hfile里面的rowkey是有序的。当客户端的请求在到达regionserver之后，为了保证写入rowkey的有序性，\n所以不能将数据立刻写入到hfile中，而是将每个变更操作保存在内存中，也就是memstore中。memstore能够很方便的支持操作的随机插入，\n并保证所有的操作在内存中是有序的。当memstore达到一定的量之后，会将memstore里面的数据flush到hfile中，这样能充分利用hadoop写入大文件的性能优势，\n提高写入性能。  \n&emsp; 由于memstore是存放在内存中，如果regionserver因为某种原因死了，会导致内存中数据丢失。所有为了保证数据不丢失，\nhbase将更新操作在写入memstore之前会写入到一个write ahead log(WAL)中。WAL文件是追加、顺序写入的，WAL每个regionserver只有一个，\n同一个regionserver上所有region写入同一个的WAL文件。这样当某个regionserver失败时，可以通过WAL文件，将所有的操作顺序重新加载到memstore中。  \n\n### 16、HBase在进行模型设计时重点在什么地方？一张表中定义多少个Column Family最合适？为什么？（☆☆☆☆☆）  \n&emsp; Column Family的个数具体看表的数据，一般来说划分标准是根据数据访问频度，如一张表里有些列访问相对频繁，而另一些列访问很少，\n这时可以把这张表划分成两个列族，分开存储，提高访问效率。  \n\n### 17、如何提高HBase客户端的读写性能？请举例说明（☆☆☆☆☆）  \n&emsp; ① 开启bloomfilter过滤器，开启bloomfilter比没开启要快3、4倍  \n&emsp; ② Hbase对于内存有特别的需求，在硬件允许的情况下配足够多的内存给它  \n&emsp; ③ 通过修改hbase-env.sh中的 export HBASE_HEAPSIZE=3000  #这里默认为1000m  \n&emsp; ④ 增大RPC数量  \n&emsp; &emsp; 通过修改hbase-site.xml中的hbase.regionserver.handler.count属性，可以适当的放大RPC数量，默认值为10有点小。  \n\n### 18、HBase集群安装注意事项？  \n&emsp; ① HBase需要HDFS的支持，因此安装HBase前确保Hadoop集群安装完成；  \n&emsp; ② HBase需要ZooKeeper集群的支持，因此安装HBase前确保ZooKeeper集群安装完成；  \n&emsp; ③ 注意HBase与Hadoop的版本兼容性；  \n&emsp; ④ 注意hbase-env.sh配置文件和hbase-site.xml配置文件的正确配置；  \n&emsp; ⑤ 注意regionservers配置文件的修改；  \n&emsp; ⑥ 注意集群中的各个节点的时间必须同步，否则启动HBase集群将会报错。  \n\n### 19、直接将时间戳作为行健，在写入单个region 时候会发生热点问题，为什么呢？（☆☆☆☆☆）  \n&emsp; region中的rowkey是有序存储，若时间比较集中。就会存储到一个region中，这样一个region的数据变多，其它的region数据很少，加载数据就会很慢，\n直到region分裂，此问题才会得到缓解。  \n\n### 20、请描述如何解决HBase中region太小和region太大带来的冲突？（☆☆☆☆☆）  \n&emsp; Region过大会发生多次compaction，将数据读一遍并重写一遍到hdfs 上，占用io，region过小会造成多次split，region 会下线，影响访问服务，\n最佳的解决方法是调整hbase.hregion. max.filesize 为256m。  \n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Hadoop面试题总结/Hadoop面试题总结（一）.md",
    "content": "## Hadoop面试题（一）\n\n### 1、集群的最主要瓶颈  \n&emsp; 磁盘IO  \n\n### 2、Hadoop运行模式  \n&emsp; 单机版、伪分布式模式、完全分布式模式  \n\n### 3、Hadoop生态圈的组件并做简要描述  \n&emsp; 1）Zookeeper：是一个开源的分布式应用程序协调服务,基于zookeeper可以实现同步服务，配置维护，命名服务。  \n&emsp; 2）Flume：一个高可用的，高可靠的，分布式的海量日志采集、聚合和传输的系统。   \n&emsp; 3）Hbase：是一个分布式的、面向列的开源数据库, 利用Hadoop HDFS作为其存储系统。   \n&emsp; 4）Hive：基于Hadoop的一个数据仓库工具，可以将结构化的数据档映射为一张数据库表，并提供简单的sql 查询功能，可以将sql语句转换为MapReduce任务进行运行。   \n&emsp; 5）Sqoop：将一个关系型数据库中的数据导进到Hadoop的 HDFS中，也可以将HDFS的数据导进到关系型数据库中。  \n\n### 4、解释“hadoop”和“hadoop 生态系统”两个概念  \n&emsp; Hadoop是指Hadoop框架本身；hadoop生态系统，不仅包含hadoop，还包括保证hadoop框架正常高效运行其他框架，比如zookeeper、Flume、Hbase、Hive、Sqoop等辅助框架。  \n\n### 5、请列出正常工作的Hadoop集群中Hadoop都分别需要启动哪些进程，它们的作用分别是什么?  \n&emsp; 1）NameNode：它是hadoop中的主服务器，管理文件系统名称空间和对集群中存储的文件的访问，保存有metadate。  \n&emsp; 2）SecondaryNameNode：它不是namenode的冗余守护进程，而是提供周期检查点和清理任务。帮助NN合并editslog，减少NN启动时间。  \n&emsp; 3）DataNode：它负责管理连接到节点的存储（一个集群中可以有多个节点）。每个存储数据的节点运行一个datanode守护进程。  \n&emsp; 4）ResourceManager（JobTracker）：JobTracker负责调度DataNode上的工作。每个DataNode有一个TaskTracker，它们执行实际工作。  \n&emsp; 5）NodeManager：（TaskTracker）执行任务。  \n&emsp; 6）DFSZKFailoverController：高可用时它负责监控NN的状态，并及时的把状态信息写入ZK。它通过一个独立线程周期性的调用NN上的一个特定接口来获取NN的健康状态。FC也有选择谁作为Active NN的权利，因为最多只有两个节点，目前选择策略还比较简单（先到先得，轮换）。  \n&emsp; 7）JournalNode：高可用情况下存放namenode的editlog文件。  \n"
  },
  {
    "path": "面试系列/Hadoop面试题总结/Hadoop面试题总结（三）——MapReduce.md",
    "content": "## Hadoop面试题总结（三）——MapReduce  \n\n### 1、谈谈Hadoop序列化和反序列化及自定义bean对象实现序列化?  \n1）序列化和反序列化  \n&emsp; （1）序列化就是把内存中的对象，转换成字节序列（或其他数据传输协议）以便于存储（持久化）和网络传输。   \n&emsp; （2）反序列化就是将收到字节序列（或其他数据传输协议）或者是硬盘的持久化数据，转换成内存中的对象。  \n&emsp; （3）Java的序列化是一个重量级序列化框架（Serializable），一个对象被序列化后，会附带很多额外的信息（各种校验信息，header，继承体系等），不便于在网络中高效传输。所以，hadoop自己开发了一套序列化机制（Writable），精简、高效。  \n2）自定义bean对象要想序列化传输步骤及注意事项：  \n&emsp; （1）必须实现Writable接口  \n&emsp; （2）反序列化时，需要反射调用空参构造函数，所以必须有空参构造  \n&emsp; （3）重写序列化方法  \n&emsp; （4）重写反序列化方法  \n&emsp; （5）注意反序列化的顺序和序列化的顺序完全一致  \n&emsp; （6）要想把结果显示在文件中，需要重写toString()，且用\"\\t\"分开，方便后续用  \n&emsp; （7）如果需要将自定义的bean放在key中传输，则还需要实现comparable接口，因为mapreduce框中的shuffle过程一定会对key进行排序  \n\n### 2、FileInputFormat切片机制（☆☆☆☆☆）  \njob提交流程源码详解  \n&emsp; waitForCompletion()  \n&emsp; submit();  \n&emsp; // 1、建立连接  \n&emsp; &emsp; connect();   \n&emsp; &emsp; &emsp; // 1）创建提交job的代理  \n&emsp; &emsp; &emsp; new Cluster(getConfiguration());  \n&emsp; &emsp; &emsp; &emsp; // （1）判断是本地yarn还是远程  \n&emsp; &emsp; &emsp; &emsp; initialize(jobTrackAddr, conf);  \n&emsp; // 2、提交job  \n&emsp; submitter.submitJobInternal(Job.this, cluster)  \n&emsp; &emsp; // 1）创建给集群提交数据的Stag路径  \n&emsp; &emsp; Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);  \n&emsp; &emsp; // 2）获取jobid ，并创建job路径  \n&emsp; &emsp; JobID jobId = submitClient.getNewJobID();  \n&emsp; &emsp; // 3）拷贝jar包到集群  \n&emsp; &emsp; copyAndConfigureFiles(job, submitJobDir);  \n&emsp; &emsp; rUploader.uploadFiles(job, jobSubmitDir);  \n&emsp; &emsp; // 4）计算切片，生成切片规划文件  \n&emsp; &emsp; writeSplits(job, submitJobDir);  \n&emsp; &emsp; maps = writeNewSplits(job, jobSubmitDir);  \n&emsp; &emsp; input.getSplits(job);  \n&emsp; &emsp; // 5）向Stag路径写xml配置文件  \n&emsp; &emsp; writeConf(conf, submitJobFile);  \n&emsp; &emsp; conf.writeXml(out);  \n&emsp; &emsp; // 6）提交job,返回提交状态  \n&emsp; &emsp; status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());  \n\n### 3、在一个运行的Hadoop 任务中，什么是InputSplit？（☆☆☆☆☆）\nFileInputFormat源码解析(input.getSplits(job))  \n（1）找到你数据存储的目录。  \n（2）开始遍历处理（规划切片）目录下的每一个文件。  \n（3）遍历第一个文件ss.txt。  \n&emsp; a）获取文件大小fs.sizeOf(ss.txt);。  \n&emsp; b）计算切片大小computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M。  \n&emsp; c）**默认情况下，切片大小=blocksize**。  \n&emsp; d）开始切，形成第1个切片：ss.txt—0:128M 第2个切片ss.txt—128:256M 第3个切片ss.txt—256M:300M（每次切片时，都要判断切完剩下的部分是否大于块的1.1倍，**不大于1.1倍就划分一块切片**）。   \n&emsp; e）将切片信息写到一个切片规划文件中。  \n&emsp; f）整个切片的核心过程在getSplit()方法中完成。  \n&emsp; g）数据切片只是在逻辑上对输入数据进行分片，并不会再磁盘上将其切分成分片进行存储。InputSplit只记录了分片的元数据信息，比如起始位置、长度以及所在的节点列表等。  \n&emsp; h）注意：block是HDFS上物理上存储的存储的数据，切片是对数据逻辑上的划分。  \n（4）**提交切片规划文件到yarn上，yarn上的MrAppMaster就可以根据切片规划文件计算开启maptask个数**。  \n\n### 4、如何判定一个job的map和reduce的数量?\n1）map数量  \n&emsp; splitSize=max{minSize,min{maxSize,blockSize}}  \n&emsp; map数量由处理的数据分成的block数量决定default_num = total_size / split_size;  \n2）reduce数量  \n&emsp; reduce的数量job.setNumReduceTasks(x);x 为reduce的数量。不设置的话默认为 1。  \n\n### 5、 Maptask的个数由什么决定？\n&emsp; 一个job的map阶段MapTask并行度（个数），由客户端提交job时的切片个数决定。  \n\n### 6、MapTask和ReduceTask工作机制（☆☆☆☆☆）（也可回答MapReduce工作原理）\n**MapTask工作机制**\n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/MapTask%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n（1）Read阶段：Map Task通过用户编写的RecordReader，从输入InputSplit中解析出一个个key/value。  \n（2）Map阶段：该节点主要是将解析出的key/value交给用户编写map()函数处理，并产生一系列新的key/value。  \n（3）Collect收集阶段：在用户编写map()函数中，当数据处理完成后，一般会调用OutputCollector.collect()输出结果。在该函数内部，它会将生成的key/value分区（调用Partitioner），并写入一个环形内存缓冲区中。  \n（4）Spill阶段：即“溢写”，当环形缓冲区满后，MapReduce会将数据写到本地磁盘上，生成一个临时文件。需要注意的是，将数据写入本地磁盘之前，先要对数据进行一次本地排序，并在必要时对数据进行合并、压缩等操作。  \n（5）Combine阶段：当所有数据处理完成后，MapTask对所有临时文件进行一次合并，以确保最终只会生成一个数据文件。  \n\n**ReduceTask工作机制**\n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/ReduceTask%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n（1）Copy阶段：ReduceTask从各个MapTask上远程拷贝一片数据，并针对某一片数据，如果其大小超过一定阈值，则写到磁盘上，否则直接放到内存中。  \n（2）Merge阶段：在远程拷贝数据的同时，ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并，以防止内存使用过多或磁盘上文件过多。  \n（3）Sort阶段：按照MapReduce语义，用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起，Hadoop采用了基于排序的策略。 由于各个MapTask已经实现对自己的处理结果进行了局部排序，因此，ReduceTask只需对所有数据进行一次归并排序即可。  \n（4）Reduce阶段：reduce()函数将计算结果写到HDFS上。  \n\n### 7、描述mapReduce有几种排序及排序发生的阶段（☆☆☆☆☆）\n1）排序的分类：  \n&emsp; （1）部分排序：  \n&emsp; &emsp; MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部排序。  \n&emsp; （2）全排序：  \n&emsp; &emsp; 如何用Hadoop产生一个全局排序的文件？最简单的方法是使用一个分区。但该方法在处理大型文件时效率极低，因为一台机器必须处理所有输出文件，从而完全丧失了MapReduce所提供的并行架构。  \n&emsp; &emsp; 替代方案：首先创建一系列排好序的文件；其次，串联这些文件；最后，生成一个全局排序的文件。主要思路是使用一个分区来描述输出的全局排序。例如：可以为待分析文件创建3个分区，在第一分区中，记录的单词首字母a-g，第二分区记录单词首字母h-n, 第三分区记录单词首字母o-z。  \n&emsp; （3）辅助排序：（GroupingComparator分组）  \n&emsp; &emsp; Mapreduce框架在记录到达reducer之前按键对记录排序，但键所对应的值并没有被排序。甚至在不同的执行轮次中，这些值的排序也不固定，因为它们来自不同的map任务且这些map任务在不同轮次中完成时间各不相同。一般来说，大多数MapReduce程序会避免让reduce函数依赖于值的排序。但是，有时也需要通过特定的方法对键进行排序和分组等以实现对值的排序。  \n&emsp; （4）二次排序：  \n&emsp; &emsp; 在自定义排序过程中，如果compareTo中的判断条件为两个即为二次排序。  \n2）自定义排序WritableComparable  \n&emsp; bean对象实现WritableComparable接口重写compareTo方法，就可以实现排序  \n&emsp; &emsp; @Override  \n&emsp; &emsp; public int compareTo(FlowBean o) {  \n&emsp; &emsp; &emsp; // 倒序排列，从大到小  \n&emsp; &emsp; &emsp; return this.sumFlow > o.getSumFlow() ? -1 : 1;  \n&emsp; &emsp; }  \n3）排序发生的阶段：  \n&emsp; （1）一个是在map side发生在spill后partition前。  \n&emsp; （2）一个是在reduce side发生在copy后 reduce前。  \n\n### 8、描述mapReduce中shuffle阶段的工作流程，如何优化shuffle阶段（☆☆☆☆☆）\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/mapReduce%E4%B8%ADshuffle%E9%98%B6%E6%AE%B5%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png\"/>  \n\n分区，排序，溢写，拷贝到对应reduce机器上，增加combiner，压缩溢写的文件。  \n\n### 9、描述mapReduce中combiner的作用是什么，一般使用情景，哪些情况不需要，及和reduce的区别？\n1）Combiner的意义就是对每一个maptask的输出进行局部汇总，以减小网络传输量。  \n2）Combiner能够应用的前提是不能影响最终的业务逻辑，而且，Combiner的输出kv应该跟reducer的输入kv类型要对应起来。  \n3）Combiner和reducer的区别在于运行的位置。  \n&emsp; Combiner是在每一个maptask所在的节点运行；  \n&emsp; Reducer是接收全局所有Mapper的输出结果。  \n\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/mapReduce%E4%B8%ADcombiner%E4%BD%9C%E7%94%A8.png\"/>\n\n### 10、如果没有定义partitioner，那数据在被送达reducer前是如何被分区的？\n&emsp; 如果没有自定义的 partitioning，则默认的 partition 算法，即根据每一条数据的 key 的 hashcode 值摸运算（%）reduce 的数量，得到的数字就是“分区号“。  \n\n### 11、MapReduce 出现单点负载多大，怎么负载平衡？ （☆☆☆☆☆）\n&emsp; 通过Partitioner实现  \n\n### 12、MapReduce 怎么实现 TopN？ （☆☆☆☆☆）\n&emsp; 可以自定义groupingcomparator，对结果进行最大值排序，然后再reduce输出时，控制只输出前n个数。就达到了topn输出的目的。  \n\n### 13、Hadoop的缓存机制（Distributedcache）（☆☆☆☆☆）\n&emsp; 分布式缓存一个最重要的应用就是在进行join操作的时候，如果一个表很大，另一个表很小，我们就可以将这个小表进行广播处理，即每个计算节点上都存一份，然后进行map端的连接操作，经过我的实验验证，这种情况下处理效率大大高于一般的reduce端join，广播处理就运用到了分布式缓存的技术。  \n&emsp; DistributedCache将拷贝缓存的文件到Slave节点在任何Job在节点上执行之前，文件在每个Job中只会被拷贝一次，缓存的归档文件会被在Slave节点中解压缩。将本地文件复制到HDFS中去，接着Client会通过addCacheFile() 和addCacheArchive()方法告诉DistributedCache在HDFS中的位置。当文件存放到文地时，JobClient同样获得DistributedCache来创建符号链接，其形式为文件的URI加fragment标识。当用户需要获得缓存中所有有效文件的列表时，JobConf 的方法 getLocalCacheFiles() 和getLocalArchives()都返回一个指向本地文件路径对象数组。  \n\n### 14、如何使用mapReduce实现两个表的join?（☆☆☆☆☆）\n&emsp; 1）reduce side join : 在map阶段，map函数同时读取两个文件File1和File2，为了区分两种来源的key/value数据对，对每条数据打一个标签（tag）,比如：tag=0 表示来自文件File1，tag=2 表示来自文件File2。  \n&emsp; 2）map side join : Map side join 是针对以下场景进行的优化：两个待连接表中，有一个表非常大，而另一个表非常小，以至于小表可以直接存放到内存中。这样，我们可以将小表复制多份，让每个map task 内存中存在一份（比如存放到hash table 中），然后只扫描大表：对于大表中的每一条记录key/value，在hash table 中查找是否有相同的key 的记录，如果有，则连接后输出即可。  \n\n### 15、什么样的计算不能用mr来提速？\n&emsp; 1）数据量很小。  \n&emsp; 2）繁杂的小文件。  \n&emsp; 3）索引是更好的存取机制的时候。  \n&emsp; 4）事务处理。  \n&emsp; 5）只有一台机器的时候。  \n\n### 16、ETL是哪三个单词的缩写\n&emsp; Extraction-Transformation-Loading的缩写，中文名称为数据提取、转换和加载。  \n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Hadoop面试题总结/Hadoop面试题总结（二）——HDFS.md",
    "content": "## Hadoop面试题总结（二）——HDFS  \n\n### 1、 HDFS 中的 block 默认保存几份？  \n&emsp; 默认保存3份  \n\n### 2、HDFS 默认 BlockSize 是多大？  \n&emsp; 默认64MB  \n\n### 3、负责HDFS数据存储的是哪一部分？  \n&emsp; DataNode负责数据存储  \n\n### 4、SecondaryNameNode的目的是什么？  \n&emsp; 他的目的使帮助NameNode合并编辑日志，减少NameNode 启动时间  \n\n### 5、文件大小设置，增大有什么影响？  \n&emsp; HDFS中的文件在物理上是分块存储（block），块的大小可以通过配置参数( dfs.blocksize)来规定，默认大小在hadoop2.x版本中是128M，老版本中是64M。  \n&emsp; **思考：为什么块的大小不能设置的太小，也不能设置的太大？**  \n&emsp; &emsp; HDFS的块比磁盘的块大，其目的是为了最小化寻址开销。如果块设置得足够大，从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间。\n因而，**传输一个由多个块组成的文件的时间取决于磁盘传输速率**。  \n&emsp; 如果寻址时间约为10ms，而传输速率为100MB/s，为了使寻址时间仅占传输时间的1%，我们要将块大小设置约为100MB。默认的块大小128MB。  \n&emsp; 块的大小：10ms×100×100M/s = 100M，如图  \n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E5%9D%97.png\"/>  \n&emsp; 增加文件块大小，需要增加磁盘的传输速率。  \n\n### 6、hadoop的块大小，从哪个版本开始是128M  \n&emsp; Hadoop1.x都是64M，hadoop2.x开始都是128M。  \n\n### 7、HDFS的存储机制（☆☆☆☆☆）  \n&emsp; HDFS存储机制，包括HDFS的**写入数据过程**和**读取数据过程**两部分  \n&emsp; **HDFS写数据过程**  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E5%86%99%E6%95%B0%E6%8D%AE%E6%B5%81%E7%A8%8B.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 1）客户端通过Distributed FileSystem模块向NameNode请求上传文件，NameNode检查目标文件是否已存在，父目录是否存在。  \n&emsp; 2）NameNode返回是否可以上传。  \n&emsp; 3）客户端请求第一个 block上传到哪几个datanode服务器上。  \n&emsp; 4）NameNode返回3个datanode节点，分别为dn1、dn2、dn3。  \n&emsp; 5）客户端通过FSDataOutputStream模块请求dn1上传数据，dn1收到请求会继续调用dn2，然后dn2调用dn3，将这个通信管道建立完成。  \n&emsp; 6）dn1、dn2、dn3逐级应答客户端。  \n&emsp; 7）客户端开始往dn1上传第一个block（先从磁盘读取数据放到一个本地内存缓存），以packet为单位，dn1收到一个packet就会传给dn2，dn2传给dn3；\ndn1每传一个packet会放入一个应答队列等待应答。  \n&emsp; 8）当一个block传输完成之后，客户端再次请求NameNode上传第二个block的服务器。（重复执行3-7步）。  \n\n&emsp; **HDFS读数据过程**  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E8%AF%BB%E6%95%B0%E6%8D%AE%E6%B5%81%E7%A8%8B.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 1）客户端通过Distributed FileSystem向NameNode请求下载文件，NameNode通过查询元数据，找到文件块所在的DataNode地址。  \n&emsp; 2）挑选一台DataNode（就近原则，然后随机）服务器，请求读取数据。  \n&emsp; 3）DataNode开始传输数据给客户端（从磁盘里面读取数据输入流，以packet为单位来做校验）。  \n&emsp; 4）客户端以packet为单位接收，先在本地缓存，然后写入目标文件。  \n\n### 8、secondary namenode工作机制（☆☆☆☆☆）  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/secondary%20namenode%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n**1）第一阶段：NameNode启动**  \n&emsp; （1）第一次启动NameNode格式化后，创建fsimage和edits文件。如果不是第一次启动，直接加载编辑日志和镜像文件到内存。   \n&emsp; （2）客户端对元数据进行增删改的请求。   \n&emsp; （3）NameNode记录操作日志，更新滚动日志。   \n&emsp; （4）NameNode在内存中对数据进行增删改查。  \n**2）第二阶段：Secondary NameNode工作**  \n&emsp; （1）Secondary NameNode询问NameNode是否需要checkpoint。直接带回NameNode是否检查结果。  \n&emsp; （2）Secondary NameNode请求执行checkpoint。  \n&emsp; （3）NameNode滚动正在写的edits日志。  \n&emsp; （4）将滚动前的编辑日志和镜像文件拷贝到Secondary NameNode。  \n&emsp; （5）Secondary NameNode加载编辑日志和镜像文件到内存，并合并。  \n&emsp; （6）生成新的镜像文件fsimage.chkpoint。  \n&emsp; （7）拷贝fsimage.chkpoint到NameNode。  \n&emsp; （8）NameNode将fsimage.chkpoint重新命名成fsimage。\n\n### 9、NameNode与SecondaryNameNode 的区别与联系？（☆☆☆☆☆）  \n**机制流程看第7题**  \n1）区别  \n&emsp; （1）NameNode负责管理整个文件系统的元数据，以及每一个路径（文件）所对应的数据块信息。  \n&emsp; （2）SecondaryNameNode主要用于定期合并命名空间镜像和命名空间镜像的编辑日志。  \n2）联系：  \n&emsp; （1）SecondaryNameNode中保存了一份和namenode一致的镜像文件（fsimage）和编辑日志（edits）。  \n&emsp; （2）在主namenode发生故障时（假设没有及时备份数据），可以从SecondaryNameNode恢复数据。  \n\n### 10、HDFS组成架构（☆☆☆☆☆）  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E7%BB%84%E6%88%90%E6%9E%B6%E6%9E%84.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n架构主要由四个部分组成，分别为**HDFS Client、NameNode、DataNode和Secondary NameNode**。下面我们分别介绍这四个组成部分。  \n1）Client：就是客户端。       \n&emsp; （1）文件切分。文件上传HDFS的时候，Client将文件切分成一个一个的Block，然后进行存储；         \n&emsp; （2）与NameNode交互，获取文件的位置信息；  \n&emsp; （3）与DataNode交互，读取或者写入数据；      \n&emsp; （4）Client提供一些命令来管理HDFS，比如启动或者关闭HDFS；  \n&emsp; （5）Client可以通过一些命令来访问HDFS；  \n2）NameNode：就是Master，它是一个主管、管理者。  \n&emsp; （1）管理HDFS的名称空间；  \n&emsp; （2）管理数据块（Block）映射信息；  \n&emsp; （3）配置副本策略；  \n&emsp; （4）处理客户端读写请求。  \n3）DataNode：就是Slave。NameNode下达命令，DataNode执行实际的操作。  \n&emsp; （1）存储实际的数据块；  \n&emsp; （2）执行数据块的读/写操作。  \n4）Secondary NameNode：并非NameNode的热备。当NameNode挂掉的时候，它并不能马上替换NameNode并提供服务。  \n&emsp; （1）辅助NameNode，分担其工作量；  \n&emsp; （2）定期合并Fsimage和Edits，并推送给NameNode；  \n&emsp; （3）在紧急情况下，可辅助恢复NameNode。  \n\n### 11、HAnamenode 是如何工作的? （☆☆☆☆☆）  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HAnamenode%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\nZKFailoverController主要职责  \n&emsp; 1）健康监测：周期性的向它监控的NN发送健康探测命令，从而来确定某个NameNode是否处于健康状态，如果机器宕机，心跳失败，那么zkfc就会标记它处于一个不健康的状态。  \n&emsp; 2）会话管理：如果NN是健康的，zkfc就会在zookeeper中保持一个打开的会话，如果NameNode同时还是Active状态的，那么zkfc还会在Zookeeper中占有一个类型为短暂类型的znode，当这个NN挂掉时，这个znode将会被删除，然后备用的NN，将会得到这把锁，升级为主NN，同时标记状态为Active。  \n&emsp; 3）当宕机的NN新启动时，它会再次注册zookeper，发现已经有znode锁了，便会自动变为Standby状态，如此往复循环，保证高可靠，需要注意，目前仅仅支持最多配置2个NN。  \n&emsp; 4）master选举：如上所述，通过在zookeeper中维持一个短暂类型的znode，来实现抢占式的锁机制，从而判断那个NameNode为Active状态  \n\n"
  },
  {
    "path": "面试系列/Hadoop面试题总结/Hadoop面试题总结（五）——优化问题.md",
    "content": "## Hadoop面试题总结（五）——优化问题\n\n### 1、MapReduce跑得慢的原因？（**☆☆☆☆☆**）\nMapreduce 程序效率的瓶颈在于两点：  \n1）计算机性能  \n&emsp; CPU、内存、磁盘健康、网络  \n2）I/O 操作优化  \n&emsp; （1）数据倾斜  \n&emsp; （2）map和reduce数设置不合理  \n&emsp; （3）reduce等待过久  \n&emsp; （4）小文件过多   \n&emsp; （5）大量的不可分块的超大文件   \n&emsp; （6）spill次数过多  \n&emsp; （7）merge次数过多等  \n\n### 2、MapReduce优化方法（☆☆☆☆☆）  \n1）数据输入  \n&emsp; （1）合并小文件：在执行mr任务前将小文件进行合并，大量的小文件会产生大量的map任务，增大map任务装载次数，而任务的装载比较耗时，从而导致mr运行较慢。   \n&emsp; （2）采用ConbinFileInputFormat来作为输入，解决输入端大量小文件场景。  \n2）map阶段  \n&emsp; （1）减少spill次数：通过调整io.sort.mb及sort.spill.percent参数值，增大触发spill的内存上限，减少spill次数，从而减少磁盘 IO。   \n&emsp; （2）减少merge次数：通过调整io.sort.factor参数，增大merge的文件数目，减少merge的次数，从而缩短mr处理时间。    \n&emsp; （3）在 map 之后先进行combine处理，减少I/O。  \n3）reduce阶段  \n&emsp; （1）合理设置map和reduce数：两个都不能设置太少，也不能设置太多。太少，会导致task等待，延长处理时间；太多，会导致 map、reduce任务间竞争资源，造成处理超时等错误。   \n&emsp; （2）设置map、reduce共存：调整slowstart.completedmaps参数，使map运行到一定程度后，reduce也开始运行，减少reduce的等待时间。  \n&emsp; （3）规避使用reduce，因为Reduce在用于连接数据集的时候将会产生大量的网络消耗。  \n&emsp; （4）合理设置reduce端的buffer，默认情况下，数据达到一个阈值的时候，buffer中的数据就会写入磁盘，然后reduce会从磁盘中获得所有的数据。也就是说，buffer和reduce是没有直接关联的，中间多个一个写磁盘->读磁盘的过程，既然有这个弊端，那么就可以通过参数来配置，使得buffer中的一部分数据可以直接输送到reduce，从而减少IO开销：mapred.job.reduce.input.buffer.percent，默认为0.0。当值大于0的时候，会保留指定比例的内存读buffer中的数据直接拿给reduce使用。这样一来，设置buffer需要内存，读取数据需要内存，reduce计算也要内存，所以要根据作业的运行情况进行调整。  \n4）IO传输  \n&emsp; （1）采用数据压缩的方式，减少网络IO的时间。安装Snappy和LZOP压缩编码器。  \n&emsp; （2）使用SequenceFile二进制文件  \n5）数据倾斜问题  \n&emsp; （1）数据倾斜现象 \n&emsp; &emsp; 数据频率倾斜——某一个区域的数据量要远远大于其他区域。  \n&emsp; &emsp; 数据大小倾斜——部分记录的大小远远大于平均值。  \n&emsp; （2）如何收集倾斜数据  \n&emsp; &emsp; 在reduce方法中加入记录map输出键的详细情况的功能。\n```java\npublic static final String MAX_VALUES = \"skew.maxvalues\";\nprivate int maxValueThreshold;\n\n@Override\npublic void configure(JobConf job) {\n     maxValueThreshold = job.getInt(MAX_VALUES, 100);\n}\n\n@Override\npublic void reduce(Text key, Iterator<Text> values,\n                     OutputCollector<Text, Text> output,\n                     Reporter reporter) throws IOException {\n     int i = 0;\n     while (values.hasNext()) {\n         values.next();\n         i++;\n     }\n     if (++i > maxValueThreshold) {\n         log.info(\"Received \" + i + \" values for key \" + key);\n     }\n}\n```\n&emsp; （3）减少数据倾斜的方法  \n&emsp; &emsp; 方法1：抽样和范围分区  \n&emsp; &emsp; &emsp; 可以通过对原始数据进行抽样得到的结果集来预设分区边界值。  \n&emsp; &emsp; 方法2：自定义分区   \n&emsp; &emsp; &emsp; 另一个抽样和范围分区的替代方案是基于输出键的背景知识进行自定义分区。例如，如果map输出键的单词来源于一本书。其中大部分必然是省略词（stopword）。那么就可以将自定义分区将这部分省略词发送给固定的一部分reduce实例。而将其他的都发送给剩余的reduce实例。  \n&emsp; &emsp; 方法3：Combine  \n&emsp; &emsp; &emsp; 使用Combine可以大量地减小数据频率倾斜和数据大小倾斜。在可能的情况下，combine的目的就是聚合并精简数据。  \n\n### 3、HDFS小文件优化方法（☆☆☆☆☆）  \n1）HDFS小文件弊端：  \n&emsp; HDFS上每个文件都要在namenode上建立一个索引，这个索引的大小约为150byte，这样当小文件比较多的时候，就会产生很多的索引文件，一方面会大量占用namenode的内存空间，另一方面就是索引文件过大是的索引速度变慢。   \n2）解决的方式：   \n&emsp; （1）Hadoop本身提供了一些文件压缩的方案。 \n&emsp; （2）从系统层面改变现有HDFS存在的问题，其实主要还是小文件的合并，然后建立比较快速的索引。  \n3）Hadoop自带小文件解决方案  \n&emsp; （1）Hadoop Archive：  \n&emsp; &emsp; 是一个高效地将小文件放入HDFS块中的文件存档工具，它能够将多个小文件打包成一个HAR文件，这样在减少namenode内存使用的同时。   \n&emsp; （2）Sequence file：  \n&emsp; &emsp; sequence file由一系列的二进制key/value组成，如果为key小文件名，value为文件内容，则可以将大批小文件合并成一个大文件。   \n&emsp; （3）CombineFileInputFormat：  \n&emsp; &emsp; CombineFileInputFormat是一种新的inputformat，用于将多个文件合并成一个单独的split，另外，它会考虑数据的存储位置。  \n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Hadoop面试题总结/Hadoop面试题总结（四）——YARN.md",
    "content": "## Hadoop面试题（四）——YARN  \n\n### 1、简述hadoop1与hadoop2 的架构异同  \n&emsp; 1）加入了yarn解决了资源调度的问题。  \n&emsp; 2）加入了对zookeeper的支持实现比较可靠的高可用。  \n    \n### 2、为什么会产生 yarn,它解决了什么问题，有什么优势？  \n&emsp; 1）Yarn最主要的功能就是解决运行的用户程序与yarn框架完全解耦。  \n&emsp; 2）Yarn上可以运行各种类型的分布式运算程序（mapreduce只是其中的一种），比如mapreduce、storm程序，spark程序……  \n\n### 3、HDFS的数据压缩算法?（☆☆☆☆☆）  \n&emsp; Hadoop中常用的压缩算法有**bzip2、gzip、lzo、snappy**，其中lzo、snappy需要操作系统安装native库才可以支持。  \n&emsp; 数据可以压缩的位置如下所示。  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/YARN-Pics/MapReduce%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; **企业开发用的比较多的是snappy**。  \n\n### 4、Hadoop的调度器总结（☆☆☆☆☆）  \n（1）默认的调度器FIFO  \n&emsp; Hadoop中默认的调度器，它先按照作业的优先级高低，再按照到达时间的先后选择被执行的作业。  \n（2）计算能力调度器Capacity Scheduler  \n&emsp; 支持多个队列，每个队列可配置一定的资源量，每个队列采用FIFO调度策略，为了防止同一个用户的作业独占队列中的资源，该调度器会对同一用户提交的作业所占资源量进行限定。调度时，首先按以下策略选择一个合适队列：计算每个队列中正在运行的任务数与其应该分得的计算资源之间的比值，选择一个该比值最小的队列；然后按以下策略选择该队列中一个作业：按照作业优先级和提交时间顺序选择，同时考虑用户资源量限制和内存限制。   \n（3）公平调度器Fair Scheduler  \n&emsp; 同计算能力调度器类似，支持多队列多用户，每个队列中的资源量可以配置，同一队列中的作业公平共享队列中所有资源。实际上，Hadoop的调度器远不止以上三种，最近，出现了很多针对新型应用的Hadoop调度器。  \n\n### 5、MapReduce 2.0 容错性（☆☆☆☆☆）  \n1）MRAppMaster容错性  \n&emsp; 一旦运行失败，由YARN的ResourceManager负责重新启动，最多重启次数可由用户设置，默认是2次。一旦超过最高重启次数，则作业运行失败。   \n2）Map Task/Reduce  \n&emsp; Task Task周期性向MRAppMaster汇报心跳；一旦Task挂掉，则MRAppMaster将为之重新申请资源，并运行之。最多重新运行次数可由用户设置，默认4次。  \n\n### 6、mapreduce推测执行算法及原理（☆☆☆☆☆）  \n1）作业完成时间取决于最慢的任务完成时间  \n&emsp; 一个作业由若干个Map 任务和Reduce 任务构成。因硬件老化、软件Bug 等，某些任务可能运行非常慢。  \n&emsp; 典型案例：系统中有99%的Map任务都完成了，只有少数几个Map老是进度很慢，完不成，怎么办？  \n2）推测执行机制  \n&emsp; 发现拖后腿的任务，比如某个任务运行速度远慢于任务平均速度。为拖后腿任务启动一个备份任务，同时运行。谁先运行完，则采用谁的结果。  \n3）不能启用推测执行机制情况  \n&emsp; （1）任务间存在严重的负载倾斜；  \n&emsp; （2）特殊任务，比如任务向数据库中写数据。  \n4）算法原理  \n&emsp; 假设某一时刻，任务T的执行进度为progress，则可通过一定的算法推测出该任务的最终完成时刻estimateEndTime。另一方面，如果此刻为该任务启动一个备份任务，则可推断出它可能的完成时刻estimateEndTime`,于是可得出以下几个公式：  \n&emsp; &emsp; estimateEndTime=estimatedRunTime+taskStartTime  \n&emsp; &emsp; estimatedRunTime=(currentTimestamp-taskStartTime)/progress  \n&emsp; &emsp; estimateEndTime`= currentTimestamp+averageRunTime  \n&emsp; 其中，currentTimestamp为当前时刻；taskStartTime为该任务的启动时刻；averageRunTime为已经成功运行完成的任务的平均运行时间。这样，MRv2总是选择（estimateEndTime- estimateEndTime·）差值最大的任务，并为之启动备份任务。为了防止大量任务同时启动备份任务造成的资源浪费，MRv2为每个作业设置了同时启动的备份任务数目上限。  \n&emsp; 推测执行机制实际上采用了经典的算法优化方法：以空间换时间，它同时启动多个相同任务处理相同的数据，并让这些任务竞争以缩短数据处理时间。显然，这种方法需要占用更多的计算资源。在集群资源紧缺的情况下，应合理使用该机制，争取在多用少量资源的情况下，减少作业的计算时间。  \n\n\n\n\n"
  },
  {
    "path": "面试系列/Hive面试题总结/Hive（一）.md",
    "content": "## Hive面试题整理（一）\n\n### 1、Hive表关联查询，如何解决数据倾斜的问题？（☆☆☆☆☆）\n&emsp; 1）倾斜原因：map输出数据按key Hash的分配到reduce中，由于key分布不均匀、业务数据本身的特、建表时考虑不周、等原因造成的reduce 上的数据量差异过大。  \n&emsp; （1）key分布不均匀;  \n&emsp; （2）业务数据本身的特性;  \n&emsp; （3）建表时考虑不周;  \n&emsp; （4）某些SQL语句本身就有数据倾斜;  \n&emsp; 如何避免：对于key为空产生的数据倾斜，可以对其赋予一个随机值。  \n&emsp; 2）解决方案  \n&emsp; （1）参数调节：  \n&emsp; &emsp; hive.map.aggr = true  \n&emsp; &emsp; hive.groupby.skewindata=true  \n&emsp; 有数据倾斜的时候进行负载均衡，当选项设定位true,生成的查询计划会有两个MR Job。第一个MR Job中，Map的输出结果集合会随机分布到Reduce中，每个Reduce做部分聚合操作，并输出结果，这样处理的结果是相同的Group By Key有可能被分发到不同的Reduce中，从而达到负载均衡的目的；第二个MR Job再根据预处理的数据结果按照Group By Key 分布到 Reduce 中（这个过程可以保证相同的 Group By Key 被分布到同一个Reduce中），最后完成最终的聚合操作。  \n&emsp; （2）SQL 语句调节：  \n&emsp; ① 选用join key分布最均匀的表作为驱动表。做好列裁剪和filter操作，以达到两表做join 的时候，数据量相对变小的效果。  \n&emsp; ② 大小表Join：  \n&emsp; &emsp; 使用map join让小的维度表（1000 条以下的记录条数）先进内存。在map端完成reduce。  \n&emsp; ③ 大表Join大表：  \n&emsp; &emsp; 把空值的key变成一个字符串加上随机数，把倾斜的数据分到不同的reduce上，由于null 值关联不上，处理后并不影响最终结果。  \n&emsp; ④ count distinct大量相同特殊值:  \n&emsp; &emsp; count distinct 时，将值为空的情况单独处理，如果是计算count distinct，可以不用处理，直接过滤，在最后结果中加1。如果还有其他计算，需要进行group by，可以先将值为空的记录单独处理，再和其他计算结果进行union。  \n\n### 2、Hive的HSQL转换为MapReduce的过程？（☆☆☆☆☆）\n&emsp; HiveSQL ->AST(抽象语法树) -> QB(查询块) ->OperatorTree（操作树）->优化后的操作树->mapreduce任务树->优化后的mapreduce任务树  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hive%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HSQL%E8%BD%ACMR%EF%BC%881%EF%BC%89.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hive%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HSQL%E8%BD%ACMR%EF%BC%882%EF%BC%89.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 过程描述如下：  \n&emsp; &emsp; SQL Parser：Antlr定义SQL的语法规则，完成SQL词法，语法解析，将SQL转化为抽象语法树AST Tree；  \n&emsp; &emsp; Semantic Analyzer：遍历AST Tree，抽象出查询的基本组成单元QueryBlock；  \n&emsp; &emsp; Logical plan：遍历QueryBlock，翻译为执行操作树OperatorTree；  \n&emsp; &emsp; Logical plan optimizer: 逻辑层优化器进行OperatorTree变换，合并不必要的ReduceSinkOperator，减少shuffle数据量；  \n&emsp; &emsp; Physical plan：遍历OperatorTree，翻译为MapReduce任务；  \n&emsp; &emsp; Logical plan optimizer：物理层优化器进行MapReduce任务的变换，生成最终的执行计划。  \n\n### 3、Hive底层与数据库交互原理？（☆☆☆☆☆）\n&emsp; 由于Hive的元数据可能要面临不断地更新、修改和读取操作，所以它显然不适合使用Hadoop文件系统进行存储。目前Hive将元数据存储在RDBMS中，比如存储在MySQL、Derby中。元数据信息包括：存在的表、表的列、权限和更多的其他信息。  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hive%E9%9D%A2%E8%AF%95%E9%A2%98Pics/Hive%E5%BA%95%E5%B1%82%E4%B8%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%A4%E4%BA%92%E5%8E%9F%E7%90%86.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n### 4、Hive的两张表关联，使用MapReduce怎么实现？（☆☆☆☆☆）\n&emsp; 如果其中有一张表为小表，直接使用map端join的方式（map端加载小表）进行聚合。  \n&emsp; 如果两张都是大表，那么采用联合key，联合key的第一个组成部分是join on中的公共字段，第二部分是一个flag，0代表表A，1代表表B，由此让Reduce区分客户信息和订单信息；在Mapper中同时处理两张表的信息，将join on公共字段相同的数据划分到同一个分区中，进而传递到一个Reduce中，然后在Reduce中实现聚合。  \n\n### 5、请谈一下Hive的特点，Hive和RDBMS有什么异同？\n&emsp; hive是基于Hadoop的一个数据仓库工具，可以将结构化的数据文件映射为一张数据库表，并提供完整的sql查询功能，可以将sql语句转换为MapReduce任务进行运行。其优点是学习成本低，可以通过类SQL语句快速实现简单的MapReduce统计，不必开发专门的MapReduce应用，十分适合数据仓库的统计分析，但是Hive不支持实时查询。  \n&emsp; Hive与关系型数据库的区别：  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Hive%E9%9D%A2%E8%AF%95%E9%A2%98Pics/Hive%E5%92%8CRDBMS%E5%BC%82%E5%90%8C.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n### 6、请说明hive中 Sort By，Order By，Cluster By，Distrbute By各代表什么意思？\n&emsp; order by：会对输入做全局排序，因此只有一个reducer（多个reducer无法保证全局有序）。只有一个reducer，会导致当输入规模较大时，需要较长的计算时间。  \n&emsp; sort by：不是全局排序，其在数据进入reducer前完成排序。  \n&emsp; distribute by：按照指定的字段对数据进行划分输出到不同的reduce中。  \n&emsp; cluster by：除了具有 distribute by 的功能外还兼具 sort by 的功能。  \n\n### 7、写出hive中split、coalesce及collect_list函数的用法（可举例）？\n&emsp; split将字符串转化为数组，即：split('a,b,c,d' , ',') ==> [\"a\",\"b\",\"c\",\"d\"]。  \n&emsp; coalesce(T v1, T v2, …) 返回参数中的第一个非空值；如果所有值都为 NULL，那么返回NULL。  \n&emsp; collect_list列出该字段所有的值，不去重 => select collect_list(id) from table。  \n\n### 8、Hive有哪些方式保存元数据，各有哪些特点？\n&emsp; Hive支持三种不同的元存储服务器，分别为：内嵌式元存储服务器、本地元存储服务器、远程元存储服务器，每种存储方式使用不同的配置参数。   \n&emsp; 内嵌式元存储主要用于单元测试，在该模式下每次只有一个进程可以连接到元存储，Derby是内嵌式元存储的默认数据库。  \n&emsp; 在本地模式下，每个Hive客户端都会打开到数据存储的连接并在该连接上请求SQL查询。  \n&emsp; 在远程模式下，所有的Hive客户端都将打开一个到元数据服务器的连接，该服务器依次查询元数据，元数据服务器和客户端之间使用Thrift协议通信。  \n\n### 9、Hive内部表和外部表的区别？\n&emsp; 创建表时：创建内部表时，会将数据移动到数据仓库指向的路径；若创建外部表，仅记录数据所在的路径，不对数据的位置做任何改变。  \n&emsp; 删除表时：在删除表的时候，内部表的元数据和数据会被一起删除， 而外部表只删除元数据，不删除数据。这样外部表相对来说更加安全些，数据组织也更加灵活，方便共享源数据。  \n\n### 10、Hive 中的压缩格式TextFile、SequenceFile、RCfile 、ORCfile各有什么区别？\n&emsp; **1、TextFile**  \n&emsp; 默认格式，**存储方式为行存储，数据不做压缩，磁盘开销大，数据解析开销大**。可结合Gzip、Bzip2使用(系统自动检查，执行查询时自动解压)，但使用这种方式，压缩后的文件不支持split，Hive不会对数据进行切分，从而无法对数据进行并行操作。并且在反序列化过程中，必须逐个字符判断是不是分隔符和行结束符，因此反序列化开销会比SequenceFile高几十倍。  \n&emsp; **2、SequenceFile**  \n&emsp; SequenceFile是Hadoop API提供的一种二进制文件支持，**存储方式为行存储，其具有使用方便、可分割、可压缩的特点**。  \n&emsp; SequenceFile支持三种压缩选择：NONE，RECORD，BLOCK。Record压缩率低，**一般建议使用BLOCK压缩**。  \n&emsp; 优势是文件和hadoop api中的MapFile是相互兼容的  \n&emsp; **3、RCFile**  \n&emsp; 存储方式：**数据按行分块，每块按列存储**。结合了行存储和列存储的优点：  \n&emsp; &emsp; 首先，RCFile 保证同一行的数据位于同一节点，因此元组重构的开销很低；  \n&emsp; &emsp; 其次，像列存储一样，RCFile 能够利用列维度的数据压缩，并且能跳过不必要的列读取；  \n&emsp; **4、ORCFile**  \n&emsp; 存储方式：数据按行分块 每块按照列存储。  \n&emsp; 压缩快、快速列存取。  \n&emsp; 效率比rcfile高，是rcfile的改良版本。  \n&emsp; 总结：**相比TEXTFILE和SEQUENCEFILE，RCFILE由于列式存储方式，数据加载时性能消耗较大，但是具有较好的压缩比和查询响应**。  \n&emsp; **数据仓库的特点是一次写入、多次读取，因此，整体来看，RCFILE相比其余两种格式具有较明显的优势**。  \n\n### 11、所有的Hive任务都会有MapReduce的执行吗？\n&emsp; 不是，从Hive0.10.0版本开始，对于简单的不需要聚合的类似SELECT <col> from <table> LIMIT n语句，不需要起MapReduce job，直接通过Fetch task获取数据。  \n\n### 12、Hive的函数：UDF、UDAF、UDTF的区别？\n&emsp; UDF：单行进入，单行输出  \n&emsp; UDAF：多行进入，单行输出  \n&emsp; UDTF：单行输入，多行输出  \n\n### 13、说说对Hive桶表的理解？\n&emsp; 桶表是对数据进行哈希取值，然后放到不同文件中存储。  \n&emsp; 数据加载到桶表时，会对字段取hash值，然后与桶的数量取模。把数据放到对应的文件中。物理上，每个桶就是表(或分区）目录里的一个文件，一个作业产生的桶(输出文件)和reduce任务个数相同。  \n&emsp; 桶表专门用于抽样查询，是很专业性的，不是日常用来存储数据的表，需要抽样查询时，才创建和使用桶表。  \n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Hive面试题总结/Hive（二）.md",
    "content": "## Hive面试题整理（二）  \n\n### 1、Fetch抓取  \n&emsp; Fetch抓取是指，Hive中对某些情况的查询可以不必使用MapReduce计算。例如：SELECT * FROM employees;在这种情况下，Hive可以简单地读取employee对应的存储目录下的文件，然后输出查询结果到控制台。   \n&emsp; 在hive-default.xml.template文件中hive.fetch.task.conversion默认是more，老版本hive默认是minimal，该属性修改为more以后，在全局查找、字段查找、limit查找等都不走mapreduce。  \n\n### 2、本地模式  \n&emsp; 大多数的Hadoop Job是需要Hadoop提供的完整的可扩展性来处理大数据集的。不过，有时Hive的输入数据量是非常小的。在这种情况下，为查询触发执行任务时消耗可能会比实际job的执行时间要多的多。对于大多数这种情况，Hive可以通过本地模式在单台机器上处理所有的任务。对于小数据集，执行时间可以明显被缩短。  \n&emsp; 用户可以通过设置hive.exec.mode.local.auto的值为true，来让Hive在适当的时候自动启动这个优化。  \n\n### **表的优化**  \n### 3、小表、大表Join  \n&emsp; 将key相对分散，并且数据量小的表放在join的左边，这样可以有效减少内存溢出错误发生的几率；再进一步，可以使用Group让小的维度表（1000条以下的记录条数）先进内存。在map端完成reduce。  \n&emsp; 实际测试发现：新版的hive已经对小表JOIN大表和大表JOIN小表进行了优化。小表放在左边和右边已经没有明显区别。  \n\n### 4、大表Join大表  \n1）空KEY过滤  \n&emsp; 有时join超时是因为某些key对应的数据太多，而相同key对应的数据都会发送到相同的reducer上，从而导致内存不够。此时我们应该仔细分析这些异常的key，很多情况下，这些key对应的数据是异常数据，我们需要在SQL语句中进行过滤。例如key对应的字段为空。  \n2）空key转换  \n&emsp; 有时虽然某个key为空对应的数据很多，但是相应的数据不是异常数据，必须要包含在join的结果中，此时我们可以表a中key为空的字段赋一个随机的值，使得数据随机均匀地分不到不同的reducer上。  \n\n### 5、Group By  \n&emsp; 默认情况下，Map阶段同一Key数据分发给一个reduce，当一个key数据过大时就倾斜了。  \n&emsp; 并不是所有的聚合操作都需要在Reduce端完成，很多聚合操作都可以先在Map端进行部分聚合，最后在Reduce端得出最终结果。  \n1）开启Map端聚合参数设置  \n&emsp; &emsp; （1）是否在Map端进行聚合，默认为True  \n&emsp; &emsp; &emsp; hive.map.aggr = true  \n&emsp; &emsp; （2）在Map端进行聚合操作的条目数目     \n&emsp; &emsp; &emsp; hive.groupby.mapaggr.checkinterval = 100000  \n&emsp; &emsp; （3）有数据倾斜的时候进行负载均衡（默认是false）     \n&emsp; &emsp; &emsp; hive.groupby.skewindata = true  \n&emsp; **当选项设定为 true，生成的查询计划会有两个MR Job**。第一个MR Job中，Map的输出结果会随机分布到Reduce中，每个Reduce做部分聚合操作，并输出结果，这样处理的结果是**相同的Group By Key有可能被分发到不同的Reduce中**，从而达到负载均衡的目的；第二个MR Job再根据预处理的数据结果按照Group By Key分布到Reduce中（这个过程可以保证相同的Group By Key被分布到同一个Reduce中），最后完成最终的聚合操作。  \n\n### 6、Count(Distinct) 去重统计  \n&emsp; 数据量小的时候无所谓，数据量大的情况下，由于COUNT DISTINCT操作需要用一个Reduce Task来完成，这一个Reduce需要处理的数据量太大，就会导致整个Job很难完成，一般COUNT DISTINCT使用先GROUP BY再COUNT的方式替换  \n\n### 7、笛卡尔积  \n&emsp; 尽量避免笛卡尔积，join的时候不加on条件，或者无效的on条件，Hive只能使用1个reducer来完成笛卡尔积  \n\n### 8、行列过滤  \n&emsp; 列处理：在SELECT中，只拿需要的列，如果有，尽量使用分区过滤，少用SELECT *。  \n&emsp; 行处理：在分区剪裁中，当使用外关联时，如果将副表的过滤条件写在Where后面，那么就会先全表关联，之后再过滤。  \n\n### **数据倾斜**  \n### 9、 Map数  \n1）通常情况下，作业会通过input的目录产生一个或者多个map任务。  \n&emsp; 主要的决定因素有：input的文件总个数，input的文件大小，集群设置的文件块大小。  \n2）是不是map数越多越好？  \n&emsp; 答案是否定的。如果一个任务有很多小文件（远远小于块大小128m），则每个小文件也会被当做一个块，用一个map任务来完成，而一个map任务启动和初始化的时间远远大于逻辑处理的时间，就会造成很大的资源浪费。而且，同时可执行的map数是受限的。  \n3）是不是保证每个map处理接近128m的文件块，就高枕无忧了？  \n&emsp; 答案也是不一定。比如有一个127m的文件，正常会用一个map去完成，但这个文件只有一个或者两个小字段，却有几千万的记录，如果map处理的逻辑比较复杂，用一个map任务去做，肯定也比较耗时。  \n&emsp; 针对上面的问题2和3，我们需要采取两种方式来解决：即减少map数和增加map数；  \n\n### 10、小文件进行合并  \n&emsp; 在map执行前合并小文件，减少map数：CombineHiveInputFormat具有对小文件进行合并的功能（系统默认的格式）。HiveInputFormat没有对小文件合并功能。  \n&emsp; set hive.input.format= org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;  \n\n### 11、复杂文件增加Map数  \n&emsp; 当input的文件都很大，任务逻辑复杂，map执行非常慢的时候，可以考虑增加Map数，来使得每个map处理的数据量减少，从而提高任务的执行效率。  \n&emsp; 增加map的方法为：根据computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M公式，调整maxSize最大值。让maxSize最大值低于blocksize就可以增加map的个数。  \n\n### 12、Reduce数  \n1）调整reduce个数方法一   \n&emsp; （1）每个Reduce处理的数据量默认是256MB  \n&emsp; &emsp; hive.exec.reducers.bytes.per.reducer=256000000  \n&emsp; （2）每个任务最大的reduce数，默认为1009  \n&emsp; &emsp; hive.exec.reducers.max=1009  \n&emsp; （3）计算reducer数的公式  \n&emsp; &emsp; N=min(参数2，总输入数据量/参数1)  \n2）调整reduce个数方法二  \n&emsp; 在hadoop的mapred-default.xml文件中修改  \n&emsp; 设置每个job的Reduce个数  \n&emsp; set mapreduce.job.reduces = 15;  \n3）reduce个数并不是越多越好  \n&emsp; （1）过多的启动和初始化reduce也会消耗时间和资源；  \n&emsp; （2）另外，有多少个reduce，就会有多少个输出文件，如果生成了很多个小文件，那么如果这些小文件作为下一个任务的输入，则也会出现小文件过多的问题；   \n&emsp; 在设置reduce个数的时候也需要考虑这两个原则：处理大数据量利用合适的reduce数；使单个reduce任务处理数据量大小要合适。  \n\n**==========================**  \n\n### 13、并行执行  \n&emsp; Hive会将一个查询转化成一个或者多个阶段。这样的阶段可以是MapReduce阶段、抽样阶段、合并阶段、limit阶段。或者Hive执行过程中可能需要的其他阶段。默认情况下，Hive一次只会执行一个阶段。不过，某个特定的job可能包含众多的阶段，而这些阶段可能并非完全互相依赖的，也就是说有些阶段是可以并行执行的，这样可能使得整个job的执行时间缩短。不过，如果有更多的阶段可以并行执行，那么job可能就越快完成。  \n&emsp; 通过设置参数hive.exec.parallel值为true，就可以开启并发执行。不过，在共享集群中，需要注意下，如果job中并行阶段增多，那么集群利用率就会增加。  \n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Kafka面试题整理/Kafka（一）.md",
    "content": "## Kafka面试题总结（一）  \n\n### 1、Kafka 都有哪些特点？  \n&emsp; 高吞吐量、低延迟：kafka每秒可以处理几十万条消息，它的延迟最低只有几毫秒，每个topic可以分多个partition, consumer group 对partition进行consume操作。  \n&emsp; 可扩展性：kafka集群支持热扩展  \n&emsp; 持久性、可靠性：消息被持久化到本地磁盘，并且支持数据备份防止数据丢失  \n&emsp; 容错性：允许集群中节点失败（若副本数量为n,则允许n-1个节点失败）  \n&emsp; 高并发：支持数千个客户端同时读写  \n\n### 2、请简述下你在哪些场景下会选择 Kafka？  \n&emsp; 日志收集：一个公司可以用Kafka可以收集各种服务的log，通过kafka以统一接口服务的方式开放给各种consumer，例如hadoop、HBase、Solr等。  \n&emsp; 消息系统：解耦和生产者和消费者、缓存消息等。  \n&emsp; 用户活动跟踪：Kafka经常被用来记录web用户或者app用户的各种活动，如浏览网页、搜索、点击等活动，这些活动信息被各个服务器发布到kafka的topic中，然后订阅者通过订阅这些topic来做实时的监控分析，或者装载到hadoop、数据仓库中做离线分析和挖掘。  \n&emsp; 运营指标：Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据，生产各种操作的集中反馈，比如报警和报告。  \n&emsp; 流式处理：比如spark streaming和 Flink  \n\n### 3、Kafka 的设计架构？  \n简单架构如下：  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98Pics/Kafka%E7%AE%80%E5%8D%95%E6%9E%B6%E6%9E%84.jpg\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n详细架构如下：  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98Pics/Kafka%E8%AF%A6%E7%BB%86%E6%9E%B6%E6%9E%84.jpg\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\nKafka 架构分为以下几个部分：  \n&emsp; Producer：消息生产者，就是向 kafka broker 发消息的客户端。  \n&emsp; Consumer：消息消费者，向 kafka broker 取消息的客户端。  \n&emsp; Topic：可以理解为一个队列，一个 Topic 又分为一个或多个分区。  \n&emsp; Consumer Group：这是 kafka 用来实现一个 topic 消息的广播（发给所有的 consumer）和单播（发给任意一个 consumer）的手段。一个 topic 可以有多个 Consumer Group。  \n&emsp; Broker：一台 kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。  \n&emsp; Partition：为了实现扩展性，一个非常大的 topic 可以分布到多个 broker上，每个 partition 是一个有序的队列。partition 中的每条消息都会被分配一个有序的id（offset）。将消息发给 consumer，kafka 只保证按一个 partition 中的消息的顺序，不保证一个 topic 的整体（多个 partition 间）的顺序。  \n&emsp; Offset：kafka 的存储文件都是按照 offset.kafka 来命名，用 offset 做名字的好处是方便查找。例如你想找位于 2049 的位置，只要找到 2048.kafka 的文件即可。当然 the first offset 就是 00000000000.kafka。  \n\n### 4、Kafka 分区的目的？  \n&emsp; 分区对于 Kafka 集群的好处是：实现负载均衡。分区对于消费者来说，可以提高并发度，提高效率。  \n\n### 5、Kafka 是如何做到消息的有序性？  \n&emsp; kafka 中的每个 partition 中的消息在写入时都是有序的，而且单独一个 partition 只能由一个消费者去消费，可以在里面保证消息的顺序性。但是分区之间的消息是不保证有序的。  \n\n### 6、Kafka 的高可靠性是怎么实现的？  \n**可回答：  \n&emsp; Kafka 在什么情况下会出现消息丢失？**  \n1）**数据可靠性（可回答 怎么尽可能保证 Kafka 的可靠性？）**  \n&emsp; Kafka 作为一个商业级消息中间件，消息可靠性的重要性可想而知。本文从 Producter 往 Broker 发送消息、Topic 分区副本以及 Leader 选举几个角度介绍数据的可靠性。  \n&emsp; **Topic分区副本**  \n&emsp; 在 Kafka 0.8.0 之前，Kafka 是没有副本的概念的，那时候人们只会用 Kafka 存储一些不重要的数据，因为没有副本，数据很可能会丢失。但是随着业务的发展，支持副本的功能越来越强烈，所以为了保证数据的可靠性，Kafka 从 0.8.0 版本开始引入了分区副本（详情请参见 KAFKA-50）。也就是说每个分区可以人为的配置几个副本（比如创建主题的时候指定 replication-factor，也可以在 Broker 级别进行配置 default.replication.factor），一般会设置为3。  \n&emsp; Kafka 可以保证单个分区里的事件是有序的，分区可以在线（可用），也可以离线（不可用）。在众多的分区副本里面有一个副本是 Leader，其余的副本是 follower，所有的读写操作都是经过 Leader 进行的，同时 follower 会定期地去 leader 上的复制数据。当 Leader 挂了的时候，其中一个 follower 会重新成为新的 Leader。通过分区副本，引入了数据冗余，同时也提供了 Kafka 的数据可靠性。  \n&emsp; **Kafka 的分区多副本架构是 Kafka 可靠性保证的核心，把消息写入多个副本可以使 Kafka 在发生崩溃时仍能保证消息的持久性**。  \n&emsp; **Producer 往 Broker 发送消息**  \n&emsp; 如果我们要往 Kafka 对应的主题发送消息，我们需要通过 Producer 完成。前面我们讲过 Kafka 主题对应了多个分区，每个分区下面又对应了多个副本；为了让用户设置数据可靠性， Kafka 在 Producer 里面提供了消息确认机制。也就是说我们可以通过配置来决定消息发送到对应分区的几个副本才算消息发送成功。可以在定义 Producer 时通过 acks 参数指定（在 0.8.2.X 版本之前是通过 request.required.acks 参数设置的）。  \n&emsp; 这个参数支持以下三种值：  \n&emsp; acks = 0：意味着如果生产者能够通过网络把消息发送出去，那么就认为消息已成功写入Kafka。在这种情况下还是有可能发生错误，比如发送的对象无能被序列化或者网卡发生故障，但如果是分区离线或整个集群长时间不可用，那就不会收到任何错误。在 acks=0 模式下的运行速度是非常快的（这就是为什么很多基准测试都是基于这个模式），你可以得到惊人的吞吐量和带宽利用率，不过如果选择了这种模式， 一定会丢失一些消息。  \n&emsp; acks = 1：意味若 Leader 在收到消息并把它写入到分区数据文件（不一定同步到磁盘上）时会返回确认或错误响应。在这个模式下，如果发生正常的 Leader 选举，生产者会在选举时收到一个 LeaderNotAvailableException 异常，如果生产者能恰当地处理这个错误，它会重试发送悄息，最终消息会安全到达新的 Leader 那里。不过在这个模式下仍然有可能丢失数据，比如消息已经成功写入 Leader，但在消息被复制到 follower 副本之前 Leader发生崩溃。  \n&emsp; acks = all（这个和 request.required.acks = -1 含义一样）：意味着 Leader 在返回确认或错误响应之前，会等待所有同步副本都收到悄息。如果和 min.insync.replicas 参数结合起来，就可以决定在返回确认前至少有多少个副本能够收到悄息，生产者会一直重试直到消息被成功提交。不过这也是最慢的做法，因为生产者在继续发送其他消息之前需要等待所有副本都收到当前的消息。  \n&emsp; 根据实际的应用场景，我们设置不同的 acks，以此保证数据的可靠性。  \n&emsp; 另外，Producer 发送消息还可以选择同步（默认，通过 producer.type=sync 配置） 或者异步（producer.type=async）模式。如果设置成异步，虽然会极大的提高消息发送的性能，但是这样会增加丢失数据的风险。如果需要确保消息的可靠性，必须将 producer.type 设置为 sync。  \n&emsp; **Leader 选举**  \n&emsp; 在介绍 Leader 选举之前，让我们先来了解一下 ISR（in-sync replicas）列表。每个分区的 leader 会维护一个 ISR 列表，ISR 列表里面就是 follower 副本的 Borker 编号，只有跟得上 Leader 的 follower 副本才能加入到 ISR 里面，这个是通过 replica.lag.time.max.ms 参数配置的。只有 ISR 里的成员才有被选为 leader 的可能。  \n2）**数据一致性（可回答 Kafka数据一致性原理？）**  \n&emsp; 这里介绍的数据一致性主要是说不论是老的 Leader 还是新选举的 Leader，Consumer 都能读到一样的数据。那么 Kafka 是如何实现的呢？  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E6%95%B0%E6%8D%AE%E4%B8%80%E8%87%B4%E6%80%A7.jpg\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 假设分区的副本为3，其中副本0是 Leader，副本1和副本2是 follower，并且在 ISR 列表里面。虽然副本0已经写入了 Message4，但是 Consumer 只能读取到 Message2。因为所有的 ISR 都同步了 Message2，只有 High Water Mark 以上的消息才支持 Consumer 读取，而 High Water Mark 取决于 ISR 列表里面偏移量最小的分区，对应于上图的副本2，这个很类似于木桶原理。  \n&emsp; 这样做的原因是还没有被足够多副本复制的消息被认为是“不安全”的，如果 Leader 发生崩溃，另一个副本成为新 Leader，那么这些消息很可能丢失了。如果我们允许消费者读取这些消息，可能就会破坏一致性。试想，一个消费者从当前 Leader（副本0） 读取并处理了 Message4，这个时候 Leader 挂掉了，选举了副本1为新的 Leader，这时候另一个消费者再去从新的 Leader 读取消息，发现这个消息其实并不存在，这就导致了数据不一致性问题。  \n&emsp; 当然，引入了 High Water Mark 机制，会导致 Broker 间的消息复制因为某些原因变慢，那么消息到达消费者的时间也会随之变长（因为我们会先等待消息复制完毕）。延迟时间可以通过参数 replica.lag.time.max.ms 参数配置，它指定了副本在复制消息时可被允许的最大延迟时间。  \n\n### 7、ISR、OSR、AR 是什么？  \n&emsp; ISR：In-Sync Replicas 副本同步队列  \n&emsp; OSR：Out-of-Sync Replicas  \n&emsp; AR：Assigned Replicas 所有副本  \n&emsp; ISR是由leader维护，follower从leader同步数据有一些延迟（具体可以参见 图文了解 Kafka 的副本复制机制），超过相应的阈值会把 follower 剔除出 ISR, 存入OSR（Out-of-Sync Replicas ）列表，新加入的follower也会先存放在OSR中。AR=ISR+OSR。  \n\n### 8、LEO、HW、LSO、LW等分别代表什么？  \n&emsp; LEO：是 LogEndOffset 的简称，代表当前日志文件中下一条  \n&emsp; HW：水位或水印（watermark）一词，也可称为高水位(high watermark)，通常被用在流式处理领域（比如Apache Flink、Apache Spark等），以表征元素或事件在基于时间层面上的进度。在Kafka中，水位的概念反而与时间无关，而是与位置信息相关。严格来说，它表示的就是位置信息，即位移（offset）。取 partition 对应的 ISR中 最小的 LEO 作为 HW，consumer 最多只能消费到 HW 所在的位置上一条信息。  \n&emsp; LSO：是 LastStableOffset 的简称，对未完成的事务而言，LSO 的值等于事务中第一条消息的位置(firstUnstableOffset)，对已完成的事务而言，它的值同 HW 相同  \n&emsp; LW：Low Watermark 低水位, 代表 AR 集合中最小的 logStartOffset 值。  \n\n### 9、数据传输的事务有几种？  \n&emsp; 数据传输的事务定义通常有以下三种级别：  \n&emsp; 最多一次：消息不会被重复发送，最多被传输一次，但也有可能一次不传输  \n&emsp; 最少一次：消息不会被漏发送，最少被传输一次，但也有可能被重复传输  \n&emsp; 精确的一次（Exactly once）：不会漏传输也不会重复传输，每个消息都传输被接收  \n\n### 10、Kafka 消费者是否可以消费指定分区消息？  \n&emsp; Kafa consumer消费消息时，向broker发出fetch请求去消费特定分区的消息，consumer指定消息在日志中的偏移量（offset），就可以消费从这个位置开始的消息，customer拥有了offset的控制权，可以向后回滚去重新消费之前的消息，这是很有意义的。  \n\n### 11、Kafka消息是采用Pull模式，还是Push模式？  \n&emsp; Kafka最初考虑的问题是，customer应该从brokes拉取消息还是brokers将消息推送到consumer，也就是pull还push。在这方面，Kafka遵循了一种大部分消息系统共同的传统的设计：producer将消息推送到broker，consumer从broker拉取消息。  \n&emsp; 一些消息系统比如Scribe和Apache Flume采用了push模式，将消息推送到下游的consumer。这样做有好处也有坏处：由broker决定消息推送的速率，对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息，但不幸的是，push模式下，当broker推送的速率远大于consumer消费的速率时，consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。  \n&emsp; Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率，将可能导致一次只推送较少的消息而造成浪费。Pull模式下，consumer就可以根据自己的消费能力去决定这些策略。 Pull有个缺点是，如果broker没有可供消费的消息，将导致consumer不断在循环中轮询，直到新消息到t达。为了避免这点，Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发送）  \n\n### 12、Kafka 高效文件存储设计特点？  \n&emsp; 1）Kafka把topic中一个parition大文件分成多个小文件段，通过多个小文件段，就容易定期清除或删除已经消费完文件，减少磁盘占用。  \n&emsp; 2）通过索引信息可以快速定位message和确定response的最大大小。  \n&emsp; 3）通过index元数据全部映射到memory，可以避免segment file的IO磁盘操作。  \n&emsp; 4）通过索引文件稀疏存储，可以大幅降低index文件元数据占用空间大小。  \n\n### 13、Kafka创建Topic时如何将分区放置到不同的Broker中？  \n&emsp; 1）副本因子不能大于 Broker 的个数；  \n&emsp; 2）第一个分区（编号为0）的第一个副本放置位置是随机从 brokerList 选择的；  \n&emsp; 3）其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker，5个分区，假设第一个分区放在第四个 Broker 上，那么第二个分区将会放在第五个 Broker 上；第三个分区将会放在第一个 Broker 上；第四个分区将会放在第二个 Broker 上，依次类推；  \n&emsp; 4）剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的，而这个数也是随机产生的；  \n\n### 14、Kafka新建的分区会在哪个目录下创建？  \n&emsp; 我们知道，在启动 Kafka 集群之前，我们需要配置好 log.dirs 参数，其值是 Kafka 数据的存放目录，这个参数可以配置多个目录，目录之间使用逗号分隔，通常这些目录是分布在不同的磁盘上用于提高读写性能。当然我们也可以配置 log.dir 参数，含义一样。只需要设置其中一个即可。  \n&emsp; 如果 log.dirs 参数只配置了一个目录，那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。  \n&emsp; 但是如果 log.dirs 参数配置了多个目录，那么 Kafka 会在哪个文件夹中创建分区目录呢？答案是：Kafka 会在含有分区目录最少的文件夹中创建新的分区目录，分区目录名为 Topic名+分区ID。注意，是分区文件夹总数最少的目录，而不是磁盘使用量最少的目录！也就是说，如果你给 log.dirs 参数新增了一个新的磁盘，新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。  \n\n### 15、谈一谈 Kafka 的再均衡  \n&emsp; 在Kafka中，当有新消费者加入或者订阅的topic数发生变化时，会触发Rebalance(再均衡：在同一个消费者组当中，分区的所有权从一个消费者转移到另外一个消费者)机制，Rebalance顾名思义就是重新均衡消费者消费。Rebalance的过程如下：  \n&emsp; 第一步：所有成员都向coordinator发送请求，请求入组。一旦所有成员都发送了请求，coordinator会从中选择一个consumer担任leader的角色，并把组成员信息以及订阅信息发给leader。  \n&emsp; 第二步：leader开始分配消费方案，指明具体哪个consumer负责消费哪些topic的哪些partition。一旦完成分配，leader会将这个方案发给coordinator。coordinator接收到分配方案之后会把方案发给各个consumer，这样组内的所有成员就都知道自己应该消费哪些分区了。  \n&emsp; 所以对于Rebalance来说，Coordinator起着至关重要的作用。  \n\n### 16、Kafka分区分配策略  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98Pics/Kafka%E5%88%86%E5%8C%BA%E5%88%86%E9%85%8D%E7%AD%96%E7%95%A5.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 在 Kafka 内部存在两种默认的分区分配策略：Range 和 RoundRobin。当以下事件发生时，Kafka 将会进行一次分区分配：  \n&emsp; 1）同一个 Consumer Group 内新增消费者  \n&emsp; 2）消费者离开当前所属的Consumer Group，包括shuts down 或 crashes  \n&emsp; 3）订阅的主题新增分区   \n&emsp; 将分区的所有权从一个消费者移到另一个消费者称为重新平衡（rebalance），如何rebalance就涉及到下面提到的分区分配策略。下面我们将详细介绍 Kafka 内置的两种分区分配策略。本文假设我们有个名为 T1 的主题，其包含了10个分区，然后我们有两个消费者（C1，C2）来消费这10个分区里面的数据，而且 C1 的 num.streams = 1，C2 的 num.streams = 2。  \n&emsp; **Range strategy**  \n&emsp; Range策略是对每个主题而言的，首先对同一个主题里面的分区按照序号进行排序，并对消费者按照字母顺序进行排序。在我们的例子里面，排完序的分区将会是0, 1, 2, 3, 4, 5, 6, 7, 8, 9；消费者线程排完序将会是C1-0, C2-0, C2-1。然后将partitions的个数除于消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽，那么前面几个消费者线程将会多消费一个分区。  \n&emsp; 在我们的例子里面，我们有10个分区，3个消费者线程，10 / 3 = 3，而且除不尽，那么消费者线程 C1-0 将会多消费一个分区，所以最后分区分配的结果看起来是这样的：  \n&emsp; C1-0 将消费 0, 1, 2, 3 分区  \n&emsp; C2-0 将消费 4, 5, 6 分区  \n&emsp; C2-1 将消费 7, 8, 9 分区  \n&emsp; 假如我们有11个分区，那么最后分区分配的结果看起来是这样的：  \n&emsp; C1-0 将消费 0, 1, 2, 3 分区  \n&emsp; C2-0 将消费 4, 5, 6, 7 分区  \n&emsp; C2-1 将消费 8, 9, 10 分区  \n&emsp; 假如我们有2个主题(T1和T2)，分别有10个分区，那么最后分区分配的结果看起来是这样的：  \n&emsp; C1-0 将消费 T1主题的 0, 1, 2, 3 分区以及 T2主题的 0, 1, 2, 3分区  \n&emsp; C2-0 将消费 T1主题的 4, 5, 6 分区以及 T2主题的 4, 5, 6分区  \n&emsp; C2-1 将消费 T1主题的 7, 8, 9 分区以及 T2主题的 7, 8, 9分区  \n&emsp; 可以看出，C1-0 消费者线程比其他消费者线程多消费了2个分区，这就是Range strategy的一个很明显的弊端。  \n&emsp; **RoundRobin strategy**  \n&emsp; 使用RoundRobin策略有两个前提条件必须满足：  \n&emsp; 同一个Consumer Group里面的所有消费者的num.streams必须相等；  \n&emsp; 每个消费者订阅的主题必须相同。  \n&emsp; 所以这里假设前面提到的2个消费者的num.streams = 2。RoundRobin策略的工作原理：将所有主题的分区组成 TopicAndPartition 列表，然后对 TopicAndPartition 列表按照 hashCode 进行排序，这里文字可能说不清，看下面的代码应该会明白：  \n```scala\nval allTopicPartitions = ctx.partitionsForTopic.flatMap { case(topic, partitions) =>\n  info(\"Consumer %s rebalancing the following partitions for topic %s: %s\"\n       .format(ctx.consumerId, topic, partitions))\n  partitions.map(partition => {\n   TopicAndPartition(topic, partition)\n  })\n}.toSeq.sortWith((topicPartition1, topicPartition2) => {\n  /*\n   * Randomize the order by taking the hashcode to reduce the likelihood of all partitions of a given topic ending\n   * up on one consumer (if it has a high enough stream count).\n   */\n  topicPartition1.toString.hashCode < topicPartition2.toString.hashCode\n})\n```  \n&emsp; 最后按照round-robin风格将分区分别分配给不同的消费者线程。  \n&emsp; 在我们的例子里面，假如按照 hashCode 排序完的topic-partitions组依次为T1-5, T1-3, T1-0, T1-8, T1-2, T1-1, T1-4, T1-7, T1-6, T1-9，我们的消费者线程排序为C1-0, C1-1, C2-0, C2-1，最后分区分配的结果为：  \n&emsp; C1-0 将消费 T1-5, T1-2, T1-6 分区；  \n&emsp; C1-1 将消费 T1-3, T1-1, T1-9 分区；  \n&emsp; C2-0 将消费 T1-0, T1-4 分区；  \n&emsp; C2-1 将消费 T1-8, T1-7 分区。  \n&emsp; 多个主题的分区分配和单个主题类似。  \n\n### 17、Kafka 是如何实现高吞吐率的？  \n&emsp; Kafka是分布式消息系统，需要处理海量的消息，Kafka的设计是把所有的消息都写入速度低容量大的硬盘，以此来换取更强的存储能力，但实际上，使用硬盘并没有带来过多的性能损失。kafka主要使用了以下几个方式实现了超高的吞吐率：  \n&emsp; 1）顺序读写  \n&emsp; 2）零拷贝  \n&emsp; 3）文件分段  \n&emsp; 4）批量发送  \n&emsp; 5）数据压缩  \n\n### 18、Kafka 缺点？  \n&emsp; 1）由于是批量发送，数据并非真正的实时；  \n&emsp; 2）对于mqtt协议不支持；  \n&emsp; 3）不支持物联网传感数据直接接入；  \n&emsp; 4）仅支持统一分区内消息有序，无法实现全局消息有序；  \n&emsp; 5）监控不完善，需要安装插件；  \n&emsp; 6）依赖zookeeper进行元数据管理。  \n\n### 19、Kafka 新旧消费者的区别？  \n&emsp; 旧的 Kafka 消费者 API 主要包括：SimpleConsumer（简单消费者） 和 ZookeeperConsumerConnectir（高级消费者）。SimpleConsumer 名字看起来是简单消费者，但是其实用起来很不简单，可以使用它从特定的分区和偏移量开始读取消息。高级消费者和现在新的消费者有点像，有消费者群组，有分区再均衡，不过它使用 ZK 来管理消费者群组，并不具备偏移量和再均衡的可操控性。  \n&emsp; 现在的消费者同时支持以上两种行为，所以为啥还用旧消费者 API 呢？  \n\n### 20、Kafka 分区数可以增加或减少吗？为什么？  \n&emsp; 我们可以使用 bin/kafka-topics.sh 命令对 Kafka 增加 Kafka 的分区数据，但是 Kafka 不支持减少分区数。 Kafka 分区数据不支持减少是由很多原因的，比如减少的分区其数据放到哪里去？是删除，还是保留？删除的话，那么这些没消费的消息不就丢了。如果保留这些消息如何放到其他分区里面？追加到其他分区后面的话那么就破坏了 Kafka 单个分区的有序性。如果要保证删除分区数据插入到其他分区保证有序性，那么实现起来逻辑就会非常复杂。  \n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Kafka面试题整理/Kafka（二）.md",
    "content": "## Kafka面试题整理（二）  \n\n### 1、请说明什么是Apache Kafka？  \n&emsp; Apache Kafka是由Apache开发的一种发布订阅消息系统，它是一个分布式的、分区的和重复的日志服务。  \n\n### 2、请说明什么是传统的消息传递方法？  \n&emsp; 传统的消息传递方法包括两种：  \n&emsp; &emsp; 队列：在队列中，一组用户可以从服务器中读取消息，每条消息都发送给其中一个人。  \n&emsp; &emsp; 发布-订阅：在这个模型中，消息被广播给所有的用户。  \n\n### 3、请说明Kafka相对于传统的消息传递方法有什么优势？  \n&emsp; 高性能：单一的Kafka代理可以处理成千上万的客户端，每秒处理数兆字节的读写操作，Kafka性能远超过传统的ActiveMQ、RabbitMQ等，而且Kafka支持Batch操作；   \n&emsp; 可扩展：Kafka集群可以透明的扩展，增加新的服务器进集群；  \n&emsp; 容错性： Kafka每个Partition数据会复制到几台服务器，当某个Broker失效时，Zookeeper将通知生产者和消费者从而使用其他的Broker。  \n\n### 4、在Kafka中broker的意义是什么？  \n&emsp; 在Kafka集群中，broker指Kafka服务器。  \n&emsp; 术语解析：  \n<p align=\"center\">\n<img src=\"https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/pics/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98Pics/Kafka%E4%B8%ADbroker%E7%9A%84%E6%84%8F%E4%B9%89.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n### 5、Kafka服务器能接收到的最大信息是多少？  \n&emsp; Kafka服务器可以接收到的消息的最大大小是1000000字节。  \n\n### 6、Kafka中的ZooKeeper是什么？Kafka是否可以脱离ZooKeeper独立运行？  \n&emsp; Zookeeper是一个开放源码的、高性能的协调服务，它用于Kafka的分布式应用。  \n&emsp; 不可以，不可能越过Zookeeper直接联系Kafka broker，一旦Zookeeper停止工作，它就不能服务客户端请求。  \n&emsp; Zookeeper主要用于在集群中不同节点之间进行通信，在Kafka中，它被用于提交偏移量，因此如果节点在任何情况下都失败了，它都可以从之前提交的偏移量中获取，除此之外，它还执行其他活动，如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。  \n\n### 7、解释Kafka的用户如何消费信息？  \n&emsp; 在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节Socket转移到磁盘，通过内核空间保存副本，并在内核用户之间调用内核。  \n\n### 8、解释如何提高远程用户的吞吐量？  \n&emsp; 如果用户位于与broker不同的数据中心，则可能需要调优Socket缓冲区大小，以对长网络延迟进行摊销。  \n\n### 9、解释一下，在数据制作过程中，你如何能从Kafka得到准确的信息？  \n&emsp; 在数据中，为了精确地获得Kafka的消息，你必须遵循两件事：**在数据消耗期间避免重复，在数据生产过程中避免重复**。  \n&emsp; 这里有两种方法，可以在数据生成时准确地获得一个语义:   \n&emsp; 每个分区使用一个单独的写入器，每当你发现一个网络错误，检查该分区中的最后一条消息，以查看您的最后一次写入是否成功。  \n&emsp; 在消息中包含一个主键(UUID或其他)，并在用户中进行反复制。  \n\n### 10、解释如何减少ISR中的扰动？broker什么时候离开ISR？（☆☆☆☆☆）  \n&emsp; ISR是一组与leaders完全同步的消息副本，也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本，直到出现真正的故障。如果一个副本从leader中脱离出来，将会从ISR中删除。  \n\n### 11、Kafka为什么需要复制？  \n&emsp; Kafka的信息复制确保了任何已发布的消息不会丢失，并且可以在机器错误、程序错误或更常见些的软件升级中使用。  \n\n### 12、如果副本在ISR中停留了很长时间表明什么？  \n&emsp; 如果一个副本在ISR中保留了很长一段时间，那么它就表明，跟踪器无法像在leader收集数据那样快速地获取数据。  \n\n### 13、请说明如果首选的副本不在ISR中会发生什么？  \n&emsp; 如果首选的副本不在ISR中，控制器将无法将leadership转移到首选的副本。  \n\n### 14、Kafka有可能在生产后发生消息偏移吗？  \n&emsp; 在大多数队列系统中，作为生产者的类无法做到这一点，它的作用是触发并忘记消息。broker将完成剩下的工作，比如使用id进行适当的元数据处理、偏移量等。  \n&emsp; 作为消息的用户，你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类，你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外，当你对Kafka消息进行迭代时，你会拥有包括偏移量和消息发送的MessageAndOffset对象。  \n\n### 15、请说明Kafka 的消息投递保证（delivery guarantee）机制以及如何实现？（☆☆☆☆☆）  \n&emsp; Kafka支持三种消息投递语义：  \n&emsp; ① At most once 消息可能会丢，但绝不会重复传递  \n&emsp; ② At least one 消息绝不会丢，但可能会重复传递  \n&emsp; ③ Exactly once 每条消息肯定会被传输一次且仅传输一次，很多时候这是用户想要的  \n&emsp; consumer在从broker读取消息后，可以选择commit，该操作会在Zookeeper中存下该consumer在该partition下读取的消息的offset，该consumer下一次再读该partition时会从下一条开始读取。如未commit，下一次读取的开始位置会跟上一次commit之后的开始位置相同。  \n&emsp; 可以将consumer设置为autocommit，即consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程，那Kafka是确保了Exactly once。但实际上实际使用中consumer并非读取完数据就结束了，而是要进行进一步处理，而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。  \n&emsp; 读完消息先commit再处理消息。这种模式下，如果consumer在commit后还没来得及处理消息就crash了，下次重新开始工作后就无法读到刚刚已提交而未处理的消息，这就对应于At most once。  \n&emsp; 读完消息先处理再commit消费状态(保存offset)。这种模式下，如果在处理完消息之后commit之前Consumer crash了，下次重新开始工作时还会处理刚刚未commit的消息，实际上该消息已经被处理过了，这就对应于At least once。  \n&emsp; 如果一定要做到Exactly once，就需要协调offset和实际操作的输出。经典的做法是引入两阶段提交，但由于许多输出系统不支持两阶段提交，更为通用的方式是将offset和操作输入存在同一个地方。比如，consumer拿到数据后可能把数据放到HDFS，如果把最新的offset和数据本身一起写到HDFS，那就可以保证数据的输出和offset的更新要么都完成，要么都不完成，间接实现Exactly once。（目前就high level API而言，offset是存于Zookeeper中的，无法存于HDFS，而low level API的offset是由自己去维护的，可以将之存于HDFS中）。  \n&emsp; 总之，Kafka默认保证At least once，并且允许通过设置producer异步提交来实现At most once，而Exactly once要求与目标存储系统协作，Kafka提供的offset可以较为容易地实现这种方式。  \n\n### 16、如何保证Kafka的消息有序（☆☆☆☆☆）  \n&emsp; Kafka对于消息的重复、丢失、错误以及顺序没有严格的要求。  \n&emsp; Kafka只能保证一个partition中的消息被某个consumer消费时是顺序的，事实上，从Topic角度来说，当有多个partition时，消息仍然不是全局有序的。  \n\n### 17、kafka数据丢失问题,及如何保证？  \n1）数据丢失：  \n&emsp; acks=1的时候(只保证写入leader成功)，如果刚好leader挂了。数据会丢失。  \n&emsp; acks=0的时候，使用异步模式的时候，该模式下kafka无法保证消息，有可能会丢。  \n2）brocker如何保证不丢失：  \n&emsp; acks=all : 所有副本都写入成功并确认。  \n&emsp; retries = 一个合理值。  \n&emsp; min.insync.replicas=2  消息至少要被写入到这么多副本才算成功。  \n&emsp; unclean.leader.election.enable=false 关闭unclean leader选举，即不允许非ISR中的副本被选举为leader，以避免数据丢失。  \n3）Consumer如何保证不丢失  \n&emsp; 如果在消息处理完成前就提交了offset，那么就有可能造成数据的丢失。  \n&emsp; enabel.auto.commit=false关闭自动提交offset  \n&emsp; 处理完数据之后手动提交。  \n\n### 18、kafka的balance是怎么做的？  \n&emsp; 生产者将数据发布到他们选择的主题。生产者可以选择在主题中分配哪个分区的消息。这可以通过循环的方式来完成，只是为了平衡负载，或者可以根据一些语义分区功能（比如消息中的一些键）来完成。更多关于分区在一秒钟内的使用。  \n\n### 19、kafka的消费者方式？  \n&emsp; consumer采用pull（拉）模式从broker中读取数据。  \n&emsp; push（推）模式很难适应消费速率不同的消费者，因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息，但是这样很容易造成consumer来不及处理消息，典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。  \n&emsp; 对于Kafka而言，pull模式更合适，它可简化broker的设计，consumer可自主控制消费消息的速率，同时consumer可以自己控制消费方式——即可批量消费也可逐条消费，同时还能选择不同的提交方式从而实现不同的传输语义。  \n&emsp; pull模式不足之处是，如果kafka没有数据，消费者可能会陷入循环中，一直等待数据到达。为了避免这种情况，我们在我们的拉请求中有参数，允许消费者请求在等待数据到达的“长轮询”中进行阻塞。  \n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark调优/Shuffle配置调优.md",
    "content": "## Spark面试题（八）——Spark的Shuffle配置调优\n#### 1、Shuffle优化配置 `-spark.shuffle.file.buffer`  \n&emsp; **默认值**：32k  \n&emsp; **参数说明**：该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前，会先写入buffer缓冲中，待缓冲写满之后，才会溢写到磁盘。         \n&emsp; **调优建议**：如果作业可用的内存资源较为充足的话，可以适当增加这个参数的大小（比如64k），从而减少shuffle write过程中溢写磁盘文件的次数，也就可以减少磁盘IO次数，进而提升性能。在实践中发现，合理调节该参数，性能会有1%~5%的提升。  \n\n#### 2、Shuffle优化配置 `-spark.reducer.maxSizeInFlight`\n&emsp; **默认值**：48m  \n&emsp; **参数说明**：该参数用于设置shuffle read task的buffer缓冲大小，而这个buffer缓冲决定了每次能够拉取多少数据。        \n&emsp; **调优建议**：如果作业可用的内存资源较为充足的话，可以适当增加这个参数的大小（比如96m），从而减少拉取数据的次数，也就可以减少网络传输的次数，进而提升性能。在实践中发现，合理调节该参数，性能会有1%~5%的提升。 \n\n#### 3、Shuffle优化配置 `-spark.shuffle.io.maxRetries`\n&emsp; **默认值**：3  \n&emsp; **参数说明**：shuffle read task从shuffle write task所在节点拉取属于自己的数据时，如果因为网络异常导致拉取失败，是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功，就可能会导致作业执行失败。        \n&emsp; **调优建议**：对于那些包含了特别耗时的shuffle操作的作业，建议增加重试最大次数（比如60次），以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现，对于针对超大数据量（数十亿~上百亿）的shuffle过程，调节该参数可以大幅度提升稳定性。  \n\n#### 4、Shuffle优化配置 `-spark.shuffle.io.retryWait`\n&emsp; **默认值**：5s  \n&emsp; **参数说明**： shuffle read task从shuffle write task所在节点拉取属于自己的数据时，如果因为网络异常导致拉取失败，是会自动进行重试的，该参数代表了每次重试拉取数据的等待间隔，默认是5s。  \n&emsp; **调优建议**：建议加大间隔时长（比如60s），以增加shuffle操作的稳定性。  \n\n#### 5、Shuffle优化配置 `-spark.shuffle.memoryFraction`\n&emsp; **默认值**：0.2  \n&emsp; **参数说明**：该参数代表了Executor内存中，分配给shuffle read task进行聚合操作的内存比例，默认是20%。      \n&emsp; **调优建议**：在资源参数调优中讲解过这个参数。如果内存充足，而且很少使用持久化操作，建议调高这个比例，给shuffle read的聚合操作更多内存，以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现，合理调节该参数可以将性能提升10%左右。  \n\n#### 6、Shuffle优化配置 `-spark.shuffle.manager`\n&emsp; **默认值**：sort  \n&emsp; **参数说明**：该参数用于设置ShuffleManager的类型。Spark 1.5以后，有三个可选项：hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项，但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似，但是使用了tungsten计划中的堆外内存管理机制，内存使用效率更高。       \n&emsp; **调优建议**：由于SortShuffleManager默认会对数据进行排序，因此如果你的业务逻辑中需要该排序机制的话，则使用默认的SortShuffleManager就可以；而如果你的业务逻辑不需要对数据进行排序，那么建议参考后面的几个参数调优，通过bypass机制或优化的HashShuffleManager来避免排序操作，同时提供较好的磁盘读写性能。这里要注意的是，tungsten-sort要慎用，因为之前发现了一些相应的bug。  \n\n#### 7、Shuffle优化配置 `-spark.shuffle.sort.bypassMergeThreshold`\n&emsp; **默认值**：200   \n&emsp; **参数说明**：当ShuffleManager为SortShuffleManager时，如果shuffle read task的数量小于这个阈值（默认是200），则shuffle write过程中不会进行排序操作，而是直接按照未经优化的HashShuffleManager的方式去写数据，但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件，并会创建单独的索引文件。      \n&emsp; **调优建议**：当你使用SortShuffleManager时，如果的确不需要排序操作，那么建议将这个参数调大一些，大于shuffle read task的数量。那么此时就会自动启用bypass机制，map-side就不会进行排序了，减少了排序的性能开销。但是这种方式下，依然会产生大量的磁盘文件，因此shuffle write性能有待提高。  \n\n#### 8、Shuffle优化配置 `-spark.shuffle.consolidateFiles`\n&emsp; **默认值**：false  \n&emsp; **参数说明**：如果使用HashShuffleManager，该参数有效。如果设置为true，那么就会开启consolidate机制，会大幅度合并shuffle write的输出文件，对于shuffle read task数量特别多的情况下，这种方法可以极大地减少磁盘IO开销，提升性能。       \n&emsp; **调优建议**：如果的确不需要SortShuffleManager的排序机制，那么除了使用bypass机制，还可以尝试将spark.shffle.manager参数手动指定为hash，使用HashShuffleManager，同时开启consolidate机制。在实践中尝试过，发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。  \n\n#### 总结：\n&emsp; 1、`spark.shuffle.file.buffer`：主要是设置的Shuffle过程中写文件的缓冲，默认32k，如果内存足够，可以适当调大，来减少写入磁盘的数量。  \n&emsp; 2、`spark.reducer.maxSizeInFight`：主要是设置Shuffle过程中读文件的缓冲区，一次能够读取多少数据，如果内存足够，可以适当扩大，减少整个网络传输次数。  \n&emsp; 3、`spark.shuffle.io.maxRetries`：主要是设置网络连接失败时，重试次数，适当调大能够增加稳定性。  \n&emsp; 4、`spark.shuffle.io.retryWait`：主要设置每次重试之间的间隔时间，可以适当调大，增加程序稳定性。  \n&emsp; 5、`spark.shuffle.memoryFraction`：Shuffle过程中的内存占用，如果程序中较多使用了Shuffle操作，那么可以适当调大该区域。  \n&emsp; 6、`spark.shuffle.manager`：Hash和Sort方式，Sort是默认，Hash在reduce数量 比较少的时候，效率会很高。  \n&emsp; 7、`spark.shuffle.sort. bypassMergeThreshold`：设置的是Sort方式中，启用Hash输出方式的临界值，如果你的程序数据不需要排序，而且reduce数量比较少，那推荐可以适当增大临界值。  \n&emsp; 8、`spark. shuffle.cosolidateFiles`：如果你使用Hash shuffle方式，推荐打开该配置，实现更少的文件输出。  \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark调优/数据倾斜.md",
    "content": "## Spark面试题（五）——数据倾斜调优\n### 1、数据倾斜  \n&emsp; 数据倾斜指的是，并行处理的数据集中，某一部分（如Spark或Kafka的一个Partition）的数据显著多于其它部分，从而使得该部分的处理速度成为整个数据集处理的瓶颈。  \n&emsp; **数据倾斜俩大直接致命后果**。  \n&emsp; &emsp; 1、数据倾斜直接会导致一种情况：Out Of Memory。         \n&emsp; &emsp; 2、运行速度慢。  \n&emsp; 主要是发生在Shuffle阶段。同样Key的数据条数太多了。导致了某个key(下图中的80亿条)所在的Task数据量太大了。远远超过其他Task所处理的数据量。  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C%E8%B0%83%E4%BC%98/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 一个经验结论是：**一般情况下，OOM的原因都是数据倾斜**  \n\n### 2、如何定位数据倾斜  \n&emsp; 数据倾斜一般会发生在shuffle过程中。很大程度上是你使用了可能会触发shuffle操作的算子：distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。  \n&emsp; 原因：查看任务->查看Stage->查看代码  \n&emsp; 某个task执行特别慢的情况  \n&emsp; 某个task莫名其妙内存溢出的情况  \n&emsp; 查看导致数据倾斜的key的数据分布情况  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C%E8%B0%83%E4%BC%98/%E5%AE%9A%E4%BD%8D%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 也可从以下几种情况考虑：  \n&emsp; 1、是不是有OOM情况出现，一般是少数内存溢出的问题  \n&emsp; 2、是不是应用运行时间差异很大，总体时间很长  \n&emsp; 3、需要了解你所处理的数据Key的分布情况，如果有些Key有大量的条数，那么就要小心数据倾斜的问题  \n&emsp; 4、一般需要通过Spark Web UI和其他一些监控方式出现的异常来综合判断  \n&emsp; 5、看看代码里面是否有一些导致Shuffle的算子出现  \n\n### 3、数据倾斜的几种典型情况  \n&emsp; 3.1 数据源中的数据分布不均匀，Spark需要频繁交互  \n&emsp; 3.2 数据集中的不同Key由于分区方式，导致数据倾斜  \n&emsp; 3.3 JOIN操作中，一个数据集中的数据分布不均匀，另一个数据集较小（主要）  \n&emsp; 3.4 聚合操作中，数据集中的数据分布不均匀（主要）  \n&emsp; 3.5 JOIN操作中，两个数据集都比较大，其中只有几个Key的数据分布不均匀  \n&emsp; 3.6 JOIN操作中，两个数据集都比较大，有很多Key的数据分布不均匀  \n&emsp; 3.7 数据集中少数几个key数据量很大，不重要，其他数据均匀  \n\n注意：  \n&emsp; 1、需要处理的数据倾斜问题就是Shuffle后数据的分布是否均匀问题  \n&emsp; 2、只要保证最后的结果是正确的，可以采用任何方式来处理数据倾斜，只要保证在处理过程中不发生数据倾斜就可以  \n\n### 4、数据倾斜的处理方法\n#### 4.1 数据源中的数据分布不均匀，Spark需要频繁交互  \n&emsp; `解决方案`：避免数据源的数据倾斜  \n&emsp; `实现原理`：通过在Hive中对倾斜的数据进行预处理，以及在进行kafka数据分发时尽量进行平均分配。这种方案从根源上解决了数据倾斜，彻底避免了在Spark中执行shuffle类算子，那么肯定就不会有数据倾斜的问题了。  \n&emsp; `方案优点`：实现起来简单便捷，效果还非常好，完全规避掉了数据倾斜，Spark作业的性能会大幅度提升。   \n&emsp; `方案缺点`：治标不治本，Hive或者Kafka中还是会发生数据倾斜。       \n&emsp; `适用情况`：在一些Java系统与Spark结合使用的项目中，会出现Java代码频繁调用Spark作业的场景，而且对Spark作业的执行性能要求很高，就比较适合使用这种方案。将数据倾斜提前到上游的Hive ETL，每天仅执行一次，只有那一次是比较慢的，而之后每次Java调用Spark作业时，执行速度都会很快，能够提供更好的用户体验。  \n&emsp; **总结**：前台的Java系统和Spark有很频繁的交互，这个时候如果Spark能够在最短的时间内处理数据，往往会给前端有非常好的体验。这个时候可以将数据倾斜的问题抛给数据源端，在数据源端进行数据倾斜的处理。但是这种方案没有真正的处理数据倾斜问题。  \n\n#### 4.2 数据集中的不同Key由于分区方式，导致数据倾斜\n&emsp; `解决方案1`：调整并行度  \n&emsp; `实现原理`：增加shuffle read task的数量，可以让原本分配给一个task的多个key分配给多个task，从而让每个task处理比原来更少的数据。  \n&emsp; `方案优点`：实现起来比较简单，可以有效缓解和减轻数据倾斜的影响。   \n&emsp; `方案缺点`：只是缓解了数据倾斜而已，没有彻底根除问题，根据实践经验来看，其效果有限。       \n&emsp; `实践经验`：该方案通常无法彻底解决数据倾斜，因为如果出现一些极端情况，比如某个key对应的数据量有100万，那么无论你的task数量增加到多少，都无法处理。  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C%E8%B0%83%E4%BC%98/4.2%20%E6%95%B0%E6%8D%AE%E9%9B%86%E4%B8%AD%E7%9A%84%E4%B8%8D%E5%90%8CKey%E7%94%B1%E4%BA%8E%E5%88%86%E5%8C%BA%E6%96%B9%E5%BC%8F%EF%BC%8C%E5%AF%BC%E8%87%B4%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; **总结**：调整并行度：适合于有大量key由于分区算法或者分区数的问题，将key进行了不均匀分区，可以通过调大或者调小分区数来试试是否有效  \n\n解决方案2：  \n缓解数据倾斜（自定义Partitioner）  \n&emsp; `适用场景`：大量不同的Key被分配到了相同的Task造成该Task数据量过大。   \n&emsp; `解决方案`： 使用自定义的Partitioner实现类代替默认的HashPartitioner，尽量将所有不同的Key均匀分配到不同的Task中。   \n&emsp; `优势`： 不影响原有的并行度设计。如果改变并行度，后续Stage的并行度也会默认改变，可能会影响后续Stage。   \n&emsp; `劣势`： 适用场景有限，只能将不同Key分散开，对于同一Key对应数据集非常大的场景不适用。效果与调整并行度类似，只能缓解数据倾斜而不能完全消除数据倾斜。而且需要根据数据特点自定义专用的Partitioner，不够灵活。   \n\n#### 4.3 JOIN操作中，一个数据集中的数据分布不均匀，另一个数据集较小（主要）\n解决方案：Reduce side Join转变为Map side Join  \n&emsp; `方案适用场景`：在对RDD使用join类操作，或者是在Spark SQL中使用join语句时，而且join操作中的一个RDD或表的数据量比较小（比如几百M），比较适用此方案。  \n&emsp; `方案实现原理`：普通的join是会走shuffle过程的，而一旦shuffle，就相当于会将相同key的数据拉取到一个shuffle read task中再进行join，此时就是reduce join。但是如果一个RDD是比较小的，则可以采用广播小RDD全量数据+map算子来实现与join同样的效果，也就是map join，此时就不会发生shuffle操作，也就不会发生数据倾斜。   \n&emsp; `方案优点`：对join操作导致的数据倾斜，效果非常好，因为根本就不会发生shuffle，也就根本不会发生数据倾斜。   \n&emsp; `方案缺点`：适用场景较少，因为这个方案只适用于一个大表和一个小表的情况。  \n\n#### 4.4 聚合操作中，数据集中的数据分布不均匀（主要）  \n&emsp; `解决方案`：两阶段聚合（局部聚合+全局聚合）  \n&emsp; `适用场景`：对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时，比较适用这种方案  \n&emsp; `实现原理`：将原本相同的key通过附加随机前缀的方式，变成多个不同的key，就可以让原本被一个task处理的数据分散到多个task上去做局部聚合，进而解决单个task处理数据量过多的问题。接着去除掉随机前缀，再次进行全局聚合，就可以得到最终的结果。具体原理见下图。   \n&emsp; `优点`：对于聚合类的shuffle操作导致的数据倾斜，效果是非常不错的。通常都可以解决掉数据倾斜，或者至少是大幅度缓解数据倾斜，将Spark作业的性能提升数倍以上。       \n&emsp; `缺点`：仅仅适用于聚合类的shuffle操作，适用范围相对较窄。如果是join类的shuffle操作，还得用其他的解决方案将相同key的数据分拆处理  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C%E8%B0%83%E4%BC%98/4.4%20%E8%81%9A%E5%90%88%E6%93%8D%E4%BD%9C%E4%B8%AD%EF%BC%8C%E6%95%B0%E6%8D%AE%E9%9B%86%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E5%88%86%E5%B8%83%E4%B8%8D%E5%9D%87%E5%8C%80.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n#### 4.5 JOIN操作中，两个数据集都比较大，其中只有几个Key的数据分布不均匀\n&emsp; `解决方案`：为倾斜key增加随机前/后缀  \n&emsp; `适用场景`：两张表都比较大，无法使用Map侧Join。其中一个RDD有少数几个Key的数据量过大，另外一个RDD的Key分布较为均匀。  \n&emsp; `解决方案`：将有数据倾斜的RDD中倾斜Key对应的数据集单独抽取出来加上随机前缀，另外一个RDD每条数据分别与随机前缀结合形成新的RDD（笛卡尔积，相当于将其数据增到到原来的N倍，N即为随机前缀的总个数），然后将二者Join后去掉前缀。然后将不包含倾斜Key的剩余数据进行Join。最后将两次Join的结果集通过union合并，即可得到全部Join结果。   \n&emsp; `优势`：相对于Map侧Join，更能适应大数据集的Join。如果资源充足，倾斜部分数据集与非倾斜部分数据集可并行进行，效率提升明显。且只针对倾斜部分的数据做数据扩展，增加的资源消耗有限。   \n&emsp; `劣势`：如果倾斜Key非常多，则另一侧数据膨胀非常大，此方案不适用。而且此时对倾斜Key与非倾斜Key分开处理，需要扫描数据集两遍，增加了开销。   \n**注意**：具有倾斜Key的RDD数据集中，key的数量比较少   \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C%E8%B0%83%E4%BC%98/4.5%20JOIN%E6%93%8D%E4%BD%9C%E4%B8%AD%EF%BC%8C%E4%B8%A4%E4%B8%AA%E6%95%B0%E6%8D%AE%E9%9B%86%E9%83%BD%E6%AF%94%E8%BE%83%E5%A4%A7%EF%BC%8C%E5%85%B6%E4%B8%AD%E5%8F%AA%E6%9C%89%E5%87%A0%E4%B8%AAKey%E7%9A%84%E6%95%B0%E6%8D%AE%E5%88%86%E5%B8%83%E4%B8%8D%E5%9D%87%E5%8C%80.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n#### 4.6 JOIN操作中，两个数据集都比较大，有很多Key的数据分布不均匀\n&emsp; `解决方案`：随机前缀和扩容RDD进行join  \n&emsp; `适用场景`：如果在进行join操作时，RDD中有大量的key导致数据倾斜，那么进行分拆key也没什么意义。  \n&emsp; `实现思路`：将该RDD的每条数据都打上一个n以内的随机前缀。同时对另外一个正常的RDD进行扩容，将每条数据都扩容成n条数据，扩容出来的每条数据都依次打上一个0~n的前缀。最后将两个处理后的RDD进行join即可。和上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理，由于处理过程需要扩容RDD，因此上一种方案扩容RDD后对内存的占用并不大；而这一种方案是针对有大量倾斜key的情况，没法将部分key拆分出来进行单独处理，因此只能对整个RDD进行数据扩容，对内存资源要求很高。  \n&emsp; `优点`：对join类型的数据倾斜基本都可以处理，而且效果也相对比较显著，性能提升效果非常不错。   \n&emsp; `缺点`：该方案更多的是缓解数据倾斜，而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容，对内存资源要求很高。   \n&emsp; `实践经验`：曾经开发一个数据需求的时候，发现一个join导致了数据倾斜。优化之前，作业的执行时间大约是60分钟左右；使用该方案优化之后，执行时间缩短到10分钟左右，性能提升了6倍。   \n**注意**：将倾斜Key添加1-N的随机前缀，并将被Join的数据集相应的扩大N倍（需要将1-N数字添加到每一条数据上作为前缀）\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E6%95%B0%E6%8D%AE%E5%80%BE%E6%96%9C%E8%B0%83%E4%BC%98/4.6%20JOIN%E6%93%8D%E4%BD%9C%E4%B8%AD%EF%BC%8C%E4%B8%A4%E4%B8%AA%E6%95%B0%E6%8D%AE%E9%9B%86%E9%83%BD%E6%AF%94%E8%BE%83%E5%A4%A7%EF%BC%8C%E6%9C%89%E5%BE%88%E5%A4%9AKey%E7%9A%84%E6%95%B0%E6%8D%AE%E5%88%86%E5%B8%83%E4%B8%8D%E5%9D%87%E5%8C%80.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n#### 4.7 数据集中少数几个key数据量很大，不重要，其他数据均匀\n&emsp; `解决方案`：过滤少数倾斜Key  \n&emsp; `适用场景`：如果发现导致倾斜的key就少数几个，而且对计算本身的影响并不大的话，那么很适合使用这种方案。比如99%的key就对应10条数据，但是只有一个key对应了100万数据，从而导致了数据倾斜。  \n&emsp; `优点`：实现简单，而且效果也很好，可以完全规避掉数据倾斜。   \n&emsp; `缺点`：适用场景不多，大多数情况下，导致倾斜的key还是很多的，并不是只有少数几个。        \n&emsp; `实践经验`：在项目中我们也采用过这种方案解决数据倾斜。有一次发现某一天Spark作业在运行的时候突然OOM了，追查之后发现，是Hive表中的某一个key在那天数据异常，导致数据量暴增。因此就采取每次执行前先进行采样，计算出样本中数据量最大的几个key之后，直接在程序中将那些key给过滤掉。   \n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark调优/程序开发调优.md",
    "content": "## Spark面试题（七）——Spark程序开发调优\n#### 1、程序开发调优 ：避免创建重复的RDD\n需要对名为“hello.txt”的HDFS文件进行一次map操作，再进行一次reduce操作。也就是说，需要对一份数据执行两次算子操作。  \n**错误的做法**：  \n&emsp; 对于同一份数据执行多次算子操作时，创建多个RDD。//这里执行了两次textFile方法，针对同一个HDFS文件，创建了两个RDD出来，然后分别对每个RDD都执行了一个算子操作。  \n这种情况下，Spark需要从HDFS上两次加载hello.txt文件的内容，并创建两个单独的RDD；//第二次加载HDFS文件以及创建RDD的性能开销，很明显是白白浪费掉的。  \n```scala\n    val rdd1 = sc.textFile(\"hdfs://master:9000/hello.txt\")\n    rdd1.map(...)\n    val rdd2 = sc.textFile(\"hdfs://master:9000/hello.txt\")\n    rdd2.reduce(...)\n```  \n**正确的用法**：  \n&emsp; 对于一份数据执行多次算子操作时，只使用一个RDD。  \n\n#### 2、程序开发调优 ：尽可能复用同一个RDD\n**错误的做法**：  \n&emsp; 有一个<long , String>格式的RDD，即rdd1。  \n&emsp; 接着由于业务需要，对rdd1执行了一个map操作，创建了一个rdd2，而rdd2中的数据仅仅是rdd1中的value值而已，也就是说，rdd2是rdd1的子集。  \n```scala\n    JavaPairRDD<long , String> rdd1 = ...\n    JavaRDD<string> rdd2 = rdd1.map(...)\n```  \n&emsp; 分别对rdd1和rdd2执行了不同的算子操作。  \n```scala\n    rdd1.reduceByKey(...)\n    rdd2.map(...)\n```  \n**正确的做法**：  \n&emsp; rdd2的数据完全就是rdd1的子集而已，却创建了两个rdd，并对两个rdd都执行了一次算子操作。  \n&emsp; 此时会因为对rdd1执行map算子来创建rdd2，而多执行一次算子操作，进而增加性能开销。  \n&emsp; 其实在这种情况下完全可以复用同一个RDD。  \n&emsp; 我们可以使用rdd1，既做reduceByKey操作，也做map操作。  \n```scala\n    JavaPairRDD<long , String> \n    rdd1 = ...rdd1.reduceByKey(...)\n    rdd1.map(tuple._2...)\n```  \n\n#### 3、程序开发调优 ：对多次使用的RDD进行持久化\n**正确的做法**：  \n&emsp; cache()方法表示：使用非序列化的方式将RDD中的数据全部尝试持久化到内存中。  \n&emsp; 此时再对rdd1执行两次算子操作时，只有在第一次执行map算子时，才会将这个rdd1从源头处计算一次。  \n&emsp; 第二次执行reduce算子时，就会直接从内存中提取数据进行计算，不会重复计算一个rdd。  \n```scala\n    val rdd1 = sc.textFile(\"hdfs://192.168.0.1:9000/hello.txt\").cache()\n    rdd1.map(...)\n    rdd1.reduce(...)\n```  \n&emsp; 序列化的方式可以减少持久化的数据对内存/磁盘的占用量，进而避免内存被持久化数据占用过多，从而发生频繁GC。  \n```scala\n    val rdd1 = sc.textFile(\"hdfs://192.168.0.1:9000/hello.txt\")  .persist(StorageLevel.MEMORY_AND_DISK_SER)\n    rdd1.map(...)\n    rdd1.reduce(...)\n```   \n**注意**：通常不建议使用DISK_ONLY和后缀为_2的级别：因为完全基于磁盘文件进行数据的读写，会导致性能急剧降低，导致网络较大开销  \n\n#### 4、程序开发调优 ：尽量避免使用shuffle类算子\n&emsp; 如果有可能的话，要尽量避免使用shuffle类算子，最消耗性能的地方就是shuffle过程。  \n&emsp; shuffle过程中，各个节点上的相同key都会先写入本地磁盘文件中，然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时，还有可能会因为一个节点上处理的key过多，导致内存不够存放，进而溢写到磁盘文件中。因此在shuffle过程中，可能会发生大量的磁盘文件读写的IO操作，以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。  \n&emsp; **尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子，尽量使用map类的非shuffle算子**。  \n&emsp; 传统的join操作会导致shuffle操作。  \n&emsp; 因为两个RDD中，相同的key都需要通过网络拉取到一个节点上，由一个task进行join操作。  \n```scala\n    val rdd3 = rdd1.join(rdd2)\n```\n&emsp; Broadcast+map的join操作，不会导致shuffle操作。  \n&emsp; 使用Broadcast将一个数据量较小的RDD作为广播变量。  \n```scala\n    val rdd2Data = rdd2.collect()\n    val rdd2DataBroadcast = sc.broadcast(rdd2Data)\n    val rdd3 = rdd1.map(rdd2DataBroadcast...)\n```  \n**注意**：以上操作，建议仅仅在rdd2的数据量比较少（比如几百M，或者一两G）的情况下使用。因为每个Executor的内存中，都会驻留一份rdd2的全量数据。  \n\n#### 5、程序开发调优 ：使用map-side预聚合的shuffle操作\n&emsp; 如果因为业务需要，一定要使用shuffle操作，无法用map类的算子来替代，那么尽量使用可以map-side预聚合的算子，类似于MapReduce中的本地combiner。map-side预聚合之后，每个节点本地就只会有一条相同的key，因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时，就会大大减少需要拉取的数据数量，从而也就减少了磁盘IO以及网络传输开销。  \n&emsp; 建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91%E8%B0%83%E4%BC%98/5%E3%80%81%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91%E8%B0%83%E4%BC%98%20%EF%BC%9A%E4%BD%BF%E7%94%A8map-side%E9%A2%84%E8%81%9A%E5%90%88%E7%9A%84shuffle%E6%93%8D%E4%BD%9C.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n#### 6、程序开发调优 ：使用高性能的算子\n&emsp; 使用reduceByKey/aggregateByKey替代groupByKey              :   map-side  \n&emsp; 使用mapPartitions替代普通map                                              :  函数执行频率  \n&emsp; 使用foreachPartitions替代foreach                                           :  函数执行频率  \n&emsp; 使用filter之后进行coalesce操作                                                :  filter后对分区进行压缩  \n&emsp; 使用repartitionAndSortWithinPartitions替代repartition与sort类操作  \n&emsp; repartitionAndSortWithinPartitions是Spark官网推荐的一个算子，官方建议，如果需要在repartition重分区之后，还要进行排序，建议直接使用repartitionAndSortWithinPartitions算子  \n\n#### 7、程序开发调优 ：广播大变量\n&emsp; 有时在开发过程中，会遇到需要在算子函数中使用外部变量的场景（尤其是大变量，比如100M以上的大集合），那么此时就应该使用Spark的广播（Broadcast）功能来提升性能。  \n&emsp; 默认情况下，Spark会将该变量复制多个副本，通过网络传输到task中，此时每个task都有一个变量副本。如果变量本身比较大的话（比如100M，甚至1G），那么大量的变量副本在网络中传输的性能开销，以及在各个节点的Executor中占用过多内存导致的频繁GC，都会极大地影响性能。  \n&emsp; 广播后的变量，会保证每个Executor的内存中，只驻留一份变量副本，而Executor中的task执行时共享该Executor中的那份变量副本。  \n\n#### 8、程序开发调优 ：使用Kryo优化序列化性能\n&emsp; 1）在算子函数中使用到外部变量时，该变量会被序列化后进行网络传输。  \n&emsp; 2）将自定义的类型作为RDD的泛型类型时（比如JavaRDD，Student是自定义类型），所有自定义类型对象，都会进行序列化。因此这种情况下，也要求自定义的类必须实现Serializable接口。   \n&emsp; 3）使用可序列化的持久化策略时（比如MEMORY_ONLY_SER），Spark会将RDD中的每个partition都序列化成一个大的字节数组。  \nSpark默认使用的是Java的序列化机制，你可以使用Kryo作为序列化类库，效率要比Java的序列化机制要高  \n```scala\n// 创建SparkConf对象。\nval conf = new SparkConf().setMaster(...).setAppName(...)\n// 设置序列化器为KryoSerializer。\nconf.set(\"spark.serializer\", \"org.apache.spark.serializer.KryoSerializer\")\n// 注册要序列化的自定义类型。\nconf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))\n```  \n\n#### 9、程序开发调优 ：分区Shuffle优化\n&emsp; 当遇到userData和events进行join时，userData比较大，而且join操作比较频繁，这个时候，可以先将userData调用了 partitionBy()分区，可以极大提高效率。  \n&emsp; cogroup()、 groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、 combineByKey() 以及 lookup()等都能够受益  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91%E8%B0%83%E4%BC%98/9%E3%80%81%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91%E8%B0%83%E4%BC%98%20%EF%BC%9A%E5%88%86%E5%8C%BAShuffle%E4%BC%98%E5%8C%96.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n**总结**：如果遇到一个RDD频繁和其他RDD进行Shuffle类操作，比如 cogroup()、 groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、 combineByKey() 以及 lookup()等，那么最好将该RDD通过partitionBy()操作进行预分区，这些操作在Shuffle过程中会减少Shuffle的数据量  \n\n#### 10、程序开发调优 ：优化数据结构\nJava中，有三种类型比较耗费内存：  \n&emsp; 1）对象，每个Java对象都有对象头、引用等额外的信息，因此比较占用内存空间。   \n&emsp; 2）字符串，每个字符串内部都有一个字符数组以及长度等额外信息。         \n&emsp; 3）集合类型，比如HashMap、LinkedList等，因为集合类型内部通常会使用一些内部类来封装集合元素，比如Map.Entry  \n&emsp; Spark官方建议，在Spark编码实现中，特别是对于算子函数中的代码，尽量不要使用上述三种数据结构，尽量使用字符串替代对象，使用原始类型（比如Int、Long）替代字符串，使用数组替代集合类型，这样尽可能地减少内存占用，从而降低GC频率，提升性能。  \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark调优/资源调优.md",
    "content": "## Spark面试题（六）——Spark资源调优\n### 1、资源运行情况  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E8%B5%84%E6%BA%90%E8%B0%83%E4%BC%98/%E8%B5%84%E6%BA%90%E8%B0%83%E4%BC%98.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n### 2、资源运行中的集中情况  \n&emsp; （1）实践中跑的Spark job，有的特别慢，查看CPU利用率很低，可以尝试减少每个executor占用CPU core的数量，增加并行的executor数量，同时配合增加分片，整体上增加了CPU的利用率，加快数据处理速度。  \n&emsp; （2）发现某job很容易发生内存溢出，我们就增大分片数量，从而减少了每片数据的规模，同时还减少并行的executor数量，这样相同的内存资源分配给数量更少的executor，相当于增加了每个task的内存分配，这样运行速度可能慢了些，但是总比OOM强。   \n&emsp; （3）数据量特别少，有大量的小文件生成，就减少文件分片，没必要创建那么多task，这种情况，如果只是最原始的input比较小，一般都能被注意到；但是，如果是在运算过程中，比如应用某个reduceBy或者某个filter以后，数据大量减少，这种低效情况就很少被留意到。  \n\n### 3、运行资源优化配置\n&emsp; 一个CPU core同一时间只能执行一个线程。而每个Executor进程上分配到的多个task，都是以每个task一条线程的方式，多线程并发运行的。  \n&emsp; 一个应用提交的时候设置多大的内存？设置多少Core？设置几个Executor？  \n```xml\n        ./bin/spark-submit \\  \n            --master yarn-cluster \\  \n            --num-executors 100 \\  \n            --executor-memory 6G \\  \n            --executor-cores 4 \\  \n            --driver-memory 1G \\  \n            --conf spark.default.parallelism=1000 \\  \n            --conf spark.storage.memoryFraction=0.5 \\  \n            --conf spark.shuffle.memoryFraction=0.3 \\\n```  \n\n#### 3.1 运行资源优化配置 `-num-executors`  \n&emsp; **参数说明**：该参数用于**设置Spark作业总共要用多少个Executor进程来执行**。Driver在向YARN集群管理器申请资源时，YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上，启动相应数量的Executor进程。这个参数非常之重要，如果不设置的话，默认只会给你启动少量的Executor进程，此时你的Spark作业的运行速度是非常慢的。       \n&emsp; **参数调优建议**：每个Spark作业的运行**一般设置50~100个左右**的Executor进程比较合适，设置太少或太多的Executor进程都不好。设置的太少，无法充分利用集群资源；设置的太多的话，大部分队列可能无法给予充分的资源。  \n\n#### 3.2 运行资源优化配置 `-executor-memory`  \n&emsp; **参数说明**：该参数用于**设置每个Executor进程的内存**。Executor内存的大小，很多时候直接决定了Spark作业的性能，而且跟常见的JVM OOM异常，也有直接的关联。      \n&emsp; **参数调优建议**：**每个Executor进程的内存设置4G~8G较为合适**。但是这只是一个参考值，具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少，num-executors * executor-memory，是不能超过队列的最大内存量的。此外，如果你是跟团队里其他人共享这个资源队列，那么申请的内存量最好**不要超过资源队列最大总内存的1/3~1/2**，避免你自己的Spark作业占用了队列所有的资源，导致别的同事的作业无法运行。  \n\n#### 3.3 运行资源优化配置 `-executor-cores`\n&emsp; **参数说明**：该参数用于**设置每个Executor进程的CPU core数量**。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程，因此每个Executor进程的CPU core数量越多，越能够快速地执行完分配给自己的所有task线程。  \n&emsp; **参数调优建议**：Executor的CPU core数量**设置为2~4个较为合适**。同样得根据不同部门的资源队列来定，可以看看自己的资源队列的最大CPU core限制是多少，再依据设置的Executor数量，来决定每个Executor进程可以分配到几个CPU core。同样建议，如果是跟他人共享这个队列，那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适，也是避免影响其他同事的作业运行。 \n\n#### 3.4 运行资源优化配置 `-driver-memory`\n&emsp; **参数说明**：该参数用于**设置Driver进程的内存**。       \n&emsp; **参数调优建议**：Driver的内存**通常来说不设置，或者设置1G左右**应该就够了。唯一需要注意的一点是，如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理（或者是用map side join操作），那么必须确保Driver的内存足够大，否则会出现OOM内存溢出的问题。  \n\n#### 3.5 运行资源优化配置 `-spark.default.parallelism`\n&emsp; **参数说明**：该参数用于**设置每个stage的默认task数量，也可以认为是分区数**。这个参数极为重要，如果不设置可能会直接影响你的Spark作业性能。      \n&emsp; **参数调优建议**：Spark作业的**默认task数量为500~1000个较为合适**。很多人常犯的一个错误就是不去设置这个参数，那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量，默认是一个HDFS block对应一个task。通常来说，Spark默认设置的数量是偏少的（比如就几十个task），如果task数量偏少的话，就会导致你前面设置好的Executor的参数都前功尽弃。试想一下，无论你的Executor进程有多少个，内存和CPU有多大，但是task只有1个或者10个，那么90%的Executor进程可能根本就没有task执行，也就是白白浪费了资源！因此Spark官网建议的设置原则是，**设置该参数为num-executors * executor-cores的2~3倍较为合适**，比如Executor的总CPU core数量为300个，那么设置1000个task是可以的，此时可以充分地利用Spark集群的资源。  \n\n#### 3.6 运行资源优化配置 `-spark.storage.memoryFraction`\n&emsp; **参数说明**：该参数用于**设置RDD持久化数据在Executor内存中能占的比例，默认是0.6**。也就是说，默认Executor 60%的内存，可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略，如果内存不够时，可能数据就不会持久化，或者数据会写入磁盘。      \n&emsp; **参数调优建议**：如果Spark作业中，有较多的RDD持久化操作，该参数的值可以适当提高一些，保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据，导致数据只能写入磁盘中，降低了性能。但是如果Spark作业中的shuffle类操作比较多，而持久化操作比较少，那么这个参数的值适当降低一些比较合适。此外，如果发现作业由于频繁的gc导致运行缓慢（通过spark web ui可以观察到作业的gc耗时），意味着task执行用户代码的内存不够用，那么同样建议调低这个参数的值。  \n\n#### 3.7 运行资源优化配置 `-spark.shuffle.memoryFraction`\n&emsp; **参数说明**：该参数用于**设置shuffle过程中一个task拉取到上个stage的task的输出后，进行聚合操作时能够使用的Executor内存的比例，默认是0.2**。也就是说，Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时，如果发现使用的内存超出了这个20%的限制，那么多余的数据就会溢写到磁盘文件中去，此时就会极大地降低性能。        \n&emsp; **参数调优建议**：如果Spark作业中的RDD持久化操作较少，shuffle操作较多时，建议降低持久化操作的内存占比，提高shuffle操作的内存占比比例，避免shuffle过程中数据过多时内存不够用，必须溢写到磁盘上，降低了性能。此外，如果发现作业由于频繁的gc导致运行缓慢，意味着task执行用户代码的内存不够用，那么同样建议调低这个参数的值。  \n \n**总结**：  \n1、`num-executors`：应用运行时executor的数量，推荐50-100左右比较合适  \n2、`executor-memory`：应用运行时executor的内存，推荐4-8G比较合适  \n3、`executor-cores`：应用运行时executor的CPU核数，推荐2-4个比较合适  \n4、`driver-memory`：应用运行时driver的内存量，主要考虑如果使用map side join或者一些类似于collect的操作，那么要相应调大内存量  \n5、`spark.default.parallelism`：每个stage默认的task数量，推荐参数为num-executors * executor-cores的2~3倍较为合适  \n6、`spark.storage.memoryFraction`：每一个executor中用于RDD缓存的内存比例，如果程序中有大量的数据缓存，可以考虑调大整个的比例，默认为60%  \n7、`spark.shuffle.memoryFraction`：每一个executor中用于Shuffle操作的内存比例，默认是20%，如果程序中有大量的Shuffle类算子，那么可以考虑其它的比例  \n\n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark（一）.md",
    "content": "## Spark面试题（一）  \n\n### 1、spark的有几种部署模式，每种模式特点？（☆☆☆☆☆）  \n1）本地模式  \n&emsp; Spark不一定非要跑在hadoop集群，可以在本地，起多个线程的方式来指定。将Spark应用以多线程的方式直接运行在本地，一般都是为了方便调试，本地模式分三类  \n&emsp; local：只启动一个executor  \n&emsp; local[k]:启动k个executor  \n&emsp; local[*]：启动跟cpu数目相同的 executor  \n2）standalone模式  \n&emsp; 分布式部署集群，自带完整的服务，资源管理和任务监控是Spark自己监控，这个模式也是其他模式的基础。  \n3）Spark on yarn模式  \n&emsp; 分布式部署集群，资源和任务监控交给yarn管理，但是目前仅支持粗粒度资源分配方式，包含cluster和client运行模式，cluster适合生产，driver运行在集群子节点，具有容错功能，client适合调试，dirver运行在客户端。  \n4）Spark On Mesos模式。  \n&emsp; 官方推荐这种模式（当然，原因之一是血缘关系）。正是由于Spark开发之初就考虑到支持Mesos，因此，目前而言，Spark运行在Mesos上会比运行在YARN上更加灵活，更加自然。用户可选择两种调度模式之一运行自己的应用程序：  \n&emsp; （1）粗粒度模式（Coarse-grained Mode）：每个应用程序的运行环境由一个Dirver和若干个Executor组成，其中，每个Executor占用若干资源，内部可运行多个Task（对应多少个“slot”）。应用程序的各个任务正式运行之前，需要将运行环境中的资源全部申请好，且运行过程中要一直占用这些资源，即使不用，最后程序运行结束后，回收这些资源。  \n&emsp; （2）细粒度模式（Fine-grained Mode）：鉴于粗粒度模式会造成大量资源浪费，Spark On Mesos还提供了另外一种调度模式：细粒度模式，这种模式类似于现在的云计算，思想是按需分配。  \n\n### 2、Spark为什么比mapreduce快？（☆☆☆☆☆）  \n&emsp; 1）基于内存计算，减少低效的磁盘交互；  \n&emsp; 2）高效的调度算法，基于DAG；  \n&emsp; 3）容错机制Linage，精华部分就是DAG和Lingae  \n\n### 3、简单说一下hadoop和spark的shuffle相同和差异？（☆☆☆☆☆）  \n&emsp; 1）从 high-level 的角度来看，两者并没有大的差别。 都是将 mapper（Spark 里是 ShuffleMapTask）的输出进行 partition，不同的 partition 送到不同的 reducer（Spark 里 reducer 可能是下一个 stage 里的 ShuffleMapTask，也可能是 ResultTask）。Reducer 以内存作缓冲区，边 shuffle 边 aggregate 数据，等到数据 aggregate 好以后进行 reduce() （Spark 里可能是后续的一系列操作）。  \n&emsp; 2）从 low-level 的角度来看，两者差别不小。 Hadoop MapReduce 是 sort-based，进入 combine() 和 reduce() 的 records 必须先 sort。这样的好处在于 combine/reduce() 可以处理大规模的数据，因为其输入数据可以通过外排得到（mapper 对每段数据先做排序，reducer 的 shuffle 对排好序的每段数据做归并）。目前的 Spark 默认选择的是 hash-based，通常使用 HashMap 来对 shuffle 来的数据进行 aggregate，不会对数据进行提前排序。如果用户需要经过排序的数据，那么需要自己调用类似 sortByKey() 的操作；如果你是Spark 1.1的用户，可以将spark.shuffle.manager设置为sort，则会对数据进行排序。在Spark 1.2中，sort将作为默认的Shuffle实现。  \n&emsp; 3）从实现角度来看，两者也有不少差别。 Hadoop MapReduce 将处理流程划分出明显的几个阶段：map(), spill, merge, shuffle, sort, reduce() 等。每个阶段各司其职，可以按照过程式的编程思想来逐一实现每个阶段的功能。在 Spark 中，没有这样功能明确的阶段，只有不同的 stage 和一系列的 transformation()，所以 spill, merge, aggregate 等操作需要蕴含在 transformation() 中。  \n&emsp; 如果我们将 map 端划分数据、持久化数据的过程称为 shuffle write，而将 reducer 读入数据、aggregate 数据的过程称为 shuffle read。那么在 Spark 中，问题就变为怎么在 job 的逻辑或者物理执行图中加入 shuffle write 和 shuffle read的处理逻辑？以及两个处理逻辑应该怎么高效实现？  \n&emsp; Shuffle write由于不要求数据有序，shuffle write 的任务很简单：将数据 partition 好，并持久化。之所以要持久化，一方面是要减少内存存储空间压力，另一方面也是为了 fault-tolerance。  \n\n### 4、spark工作机制？（☆☆☆☆☆）  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/Spark%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n① 构建Application的运行环境，Driver创建一个SparkContext\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/SparkContext.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n② SparkContext向资源管理器（Standalone、Mesos、Yarn）申请Executor资源，资源管理器启动StandaloneExecutorbackend（Executor）\n③ Executor向SparkContext申请Task\n④ SparkContext将应用程序分发给Executor\n⑤ SparkContext就建成DAG图，DAGScheduler将DAG图解析成Stage，每个Stage有多个task，形成taskset发送给task Scheduler，由task Scheduler将Task发送给Executor运行\n⑥ Task在Executor上运行，运行完释放所有资源\n\n### 5、spark的优化怎么做？ （☆☆☆☆☆）  \n&emsp; spark调优比较复杂，但是大体可以分为三个方面来进行  \n&emsp; 1）平台层面的调优：防止不必要的jar包分发，提高数据的本地性，选择高效的存储格式如parquet  \n&emsp; 2）应用程序层面的调优：过滤操作符的优化降低过多小任务，降低单条记录的资源开销，处理数据倾斜，复用RDD进行缓存，作业并行化执行等等  \n&emsp; 3）JVM层面的调优：设置合适的资源量，设置合理的JVM，启用高效的序列化方法如kyro，增大off head内存等等  \n\n### 6、数据本地性是在哪个环节确定的？（☆☆☆☆☆）  \n&emsp; 具体的task运行在那他机器上，dag划分stage的时候确定的  \n\n### 7、RDD的弹性表现在哪几点？（☆☆☆☆☆）  \n&emsp; 1）自动的进行内存和磁盘的存储切换； \n&emsp; 2）基于Lineage的高效容错；  \n&emsp; 3）task如果失败会自动进行特定次数的重试；  \n&emsp; 4）stage如果失败会自动进行特定次数的重试，而且只会计算失败的分片；   \n&emsp; 5）checkpoint和persist，数据计算之后持久化缓存；  \n&emsp; 6）数据调度弹性，DAG TASK调度和资源无关；  \n&emsp; 7）数据分片的高度弹性。  \n\n### 8、RDD有哪些缺陷？（☆☆☆☆☆）  \n&emsp; 1）不支持细粒度的写和更新操作（如网络爬虫），spark写数据是粗粒度的。所谓粗粒度，就是批量写入数据，为了提高效率。但是读数据是细粒度的也就是说可以一条条的读。  \n&emsp; 2）不支持增量迭代计算，Flink支持  \n\n### 9、Spark的shuffle过程？（☆☆☆☆☆）\n&emsp; 从下面三点去展开  \n&emsp; 1）shuffle过程的划分  \n&emsp; 2）shuffle的中间结果如何存储  \n&emsp; 3）shuffle的数据如何拉取过来  \n&emsp; 可以参考这篇博文：http://www.cnblogs.com/jxhd1/p/6528540.html  \n\n### 10、 Spark的数据本地性有哪几种？（☆☆☆☆☆）  \n&emsp; Spark中的数据本地性有三种：  \n&emsp; 1）PROCESS_LOCAL是指读取缓存在本地节点的数据  \n&emsp; 2）NODE_LOCAL是指读取本地节点硬盘数据  \n&emsp; 3）ANY是指读取非本地节点数据  \n&emsp; 通常读取数据PROCESS_LOCAL>NODE_LOCAL>ANY，尽量使数据以PROCESS_LOCAL或NODE_LOCAL方式读取。其中PROCESS_LOCAL还和cache有关，如果RDD经常用的话将该RDD cache到内存中，注意，由于cache是lazy的，所以必须通过一个action的触发，才能真正的将该RDD cache到内存中。  \n\n### 11、Spark为什么要持久化，一般什么场景下要进行persist操作？（☆☆☆）  \n&emsp; 为什么要进行持久化？  \n&emsp; spark所有复杂一点的算法都会有persist身影，spark默认数据放在内存，spark很多内容都是放在内存的，非常适合高速迭代，1000个步骤只有第一个输入数据，中间不产生临时数据，但分布式系统风险很高，所以容易出错，就要容错，rdd出错或者分片可以根据血统算出来，如果没有对父rdd进行persist 或者cache的化，就需要重头做。 \n&emsp; 以下场景会使用persist  \n&emsp; 1）某个步骤计算非常耗时，需要进行persist持久化  \n&emsp; 2）计算链条非常长，重新恢复要算很多步骤，很好使，persist  \n&emsp; 3）checkpoint所在的rdd要持久化persist。checkpoint前，要持久化，写个rdd.cache或者rdd.persist，将结果保存起来，再写checkpoint操作，这样执行起来会非常快，不需要重新计算rdd链条了。checkpoint之前一定会进行persist。   \n&emsp; 4）shuffle之后要persist，shuffle要进性网络传输，风险很大，数据丢失重来，恢复代价很大  \n&emsp; 5）shuffle之前进行persist，框架默认将数据持久化到磁盘，这个是框架自动做的。  \n\n### 12、介绍一下join操作优化经验？（☆☆☆☆☆）  \n&emsp; join其实常见的就分为两类： map-side join 和  reduce-side join。当大表和小表join时，用map-side join能显著提高效率。将多份数据进行关联是数据处理过程中非常普遍的用法，不过在分布式计算系统中，这个问题往往会变的非常麻烦，因为框架提供的 join 操作一般会将所有数据根据 key 发送到所有的 reduce 分区中去，也就是 shuffle 的过程。造成大量的网络以及磁盘IO消耗，运行效率极其低下，这个过程一般被称为 reduce-side-join。如果其中有张表较小的话，我们则可以自己实现在 map 端实现数据关联，跳过大量数据进行 shuffle 的过程，运行时间得到大量缩短，根据不同数据可能会有几倍到数十倍的性能提升。   \n&emsp; 备注：这个题目面试中非常非常大概率见到，务必搜索相关资料掌握，这里抛砖引玉。  \n\n### 13、描述Yarn执行一个任务的过程？（☆☆☆☆☆）  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/YARN%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 1）客户端client向ResouceManager提交Application，ResouceManager接受Application并根据集群资源状况选取一个node来启动Application的任务调度器driver（ApplicationMaster）。  \n&emsp; 2）ResouceManager找到那个node，命令其该node上的nodeManager来启动一个新的 JVM进程运行程序的driver（ApplicationMaster）部分，driver（ApplicationMaster）启动时会首先向ResourceManager注册，说明由自己来负责当前程序的运行。  \n&emsp; 3）driver（ApplicationMaster）开始下载相关jar包等各种资源，基于下载的jar等信息决定向ResourceManager申请具体的资源内容。  \n&emsp; 4）ResouceManager接受到driver（ApplicationMaster）提出的申请后，会最大化的满足 资源分配请求，并发送资源的元数据信息给driver（ApplicationMaster）。  \n&emsp; 5）driver（ApplicationMaster）收到发过来的资源元数据信息后会根据元数据信息发指令给具体机器上的NodeManager，让其启动具体的container。  \n&emsp; 6）NodeManager收到driver发来的指令，启动container，container启动后必须向driver（ApplicationMaster）注册。  \n&emsp; 7）driver（ApplicationMaster）收到container的注册，开始进行任务的调度和计算，直到 任务完成。  \n&emsp; 注意：如果ResourceManager第一次没有能够满足driver（ApplicationMaster）的资源请求 ，后续发现有空闲的资源，会主动向driver（ApplicationMaster）发送可用资源的元数据信息以提供更多的资源用于当前程序的运行。  \n\n### 14、Spark on Yarn 模式有哪些优点？（☆☆☆☆☆）  \n&emsp; 1）与其他计算框架共享集群资源（Spark框架与MapReduce框架同时运行，如果不用Yarn进行资源分配，MapReduce分到的内存资源会很少，效率低下）；资源按需分配，进而提高集群资源利用等。  \n&emsp; 2）相较于Spark自带的Standalone模式，Yarn的资源分配更加细致。   \n&emsp; 3）Application部署简化，例如Spark，Storm等多种框架的应用由客户端提交后，由Yarn负责资源的管理和调度，利用Container作为资源隔离的单位，以它为单位去使用内存,cpu等。   \n&emsp; 4）Yarn通过队列的方式，管理同时运行在Yarn集群中的多个服务，可根据不同类型的应用程序负载情况，调整对应的资源使用量，实现资源弹性管理。  \n\n### 15、谈谈你对container的理解？（☆☆☆☆☆）  \n&emsp; 1）Container作为资源分配和调度的基本单位，其中封装了的资源如内存，CPU，磁盘，网络带宽等。 目前yarn仅仅封装内存和CPU   \n&emsp; 2）Container由ApplicationMaster向ResourceManager申请的，由ResouceManager中的资源调度器异步分配给ApplicationMaster  \n&emsp; 3）Container的运行是由ApplicationMaster向资源所在的NodeManager发起的，Container运行时需提供内部执行的任务命令  \n\n## 16、Spark使用parquet文件存储格式能带来哪些好处？（☆☆☆☆☆）  \n&emsp; 1）如果说HDFS是大数据时代分布式文件系统首选标准，那么parquet则是整个大数据时代文件存储格式实时首选标准。  \n&emsp; 2）速度更快：从使用spark sql操作普通文件CSV和parquet文件速度对比上看，绝大多数情况会比使用csv等普通文件速度提升10倍左右，在一些普通文件系统无法在spark上成功运行的情况下，使用parquet很多时候可以成功运行。  \n&emsp; 3）parquet的压缩技术非常稳定出色，在spark sql中对压缩技术的处理可能无法正常的完成工作（例如会导致lost task，lost executor）但是此时如果使用parquet就可以正常的完成。  \n&emsp; 4）极大的减少磁盘I/o,通常情况下能够减少75%的存储空间，由此可以极大的减少spark sql处理数据的时候的数据输入内容，尤其是在spark1.6x中有个下推过滤器在一些情况下可以极大的减少磁盘的IO和内存的占用，（下推过滤器）。  \n&emsp; 5）spark 1.6x parquet方式极大的提升了扫描的吞吐量，极大提高了数据的查找速度spark1.6和spark1.5x相比而言，提升了大约1倍的速度，在spark1.6X中，操作parquet时候cpu也进行了极大的优化，有效的降低了cpu消耗。  \n&emsp; 6）采用parquet可以极大的优化spark的调度和执行。我们测试spark如果用parquet可以有效的减少stage的执行消耗，同时可以优化执行路径。  \n\n### 17、介绍parition和block有什么关联关系？（☆☆☆☆☆）  \n&emsp; 1）hdfs中的block是分布式存储的最小单元，等分，可设置冗余，这样设计有一部分磁盘空间的浪费，但是整齐的block大小，便于快速找到、读取对应的内容；  \n&emsp; 2）Spark中的partion是弹性分布式数据集RDD的最小单元，RDD是由分布在各个节点上的partion组成的。partion是指的spark在计算过程中，生成的数据在计算空间内最小单元，同一份数据（RDD）的partion大小不一，数量不定，是根据application里的算子和最初读入的数据分块数量决定；  \n&emsp; 3）block位于存储空间、partion位于计算空间，block的大小是固定的、partion大小是不固定的，是从2个不同的角度去看数据。  \n\n### 18、Spark应用程序的执行过程是什么？（☆☆☆☆☆）  \n&emsp; 1）构建Spark Application的运行环境（启动SparkContext），SparkContext向资源管理器（可以是Standalone、Mesos或YARN）注册并申请运行Executor资源；  \n&emsp; 2）资源管理器分配Executor资源并启动StandaloneExecutorBackend，Executor运行情况将随着心跳发送到资源管理器上；  \n&emsp; 3）SparkContext构建成DAG图，将DAG图分解成Stage，并把Taskset发送给Task Scheduler。Executor向SparkContext申请Task，Task Scheduler将Task发放给Executor运行同时SparkContext将应用程序代码发放给Executor；  \n&emsp; 4）Task在Executor上运行，运行完毕释放所有资源。  \n\n### 19、不需要排序的hash shuffle是否一定比需要排序的sort shuffle速度快？（☆☆☆☆☆）  \n&emsp; 不一定，当数据规模小，Hash shuffle快于Sorted Shuffle数据规模大的时候；当数据量大，sorted Shuffle会比Hash shuffle快很多，因为数量大的有很多小文件，不均匀，甚至出现数据倾斜，消耗内存大，1.x之前spark使用hash，适合处理中小规模，1.x之后，增加了Sorted shuffle，Spark更能胜任大规模处理了。  \n\n### 20、Sort-based shuffle的缺陷? （☆☆☆☆☆）  \n&emsp; 1）如果mapper中task的数量过大，依旧会产生很多小文件，此时在shuffle传递数据的过程中reducer段，reduce会需要同时大量的记录进行反序列化，导致大量的内存消耗和GC的巨大负担，造成系统缓慢甚至崩溃。   \n&emsp; 2）如果需要在分片内也进行排序，此时需要进行mapper段和reducer段的两次排序。  \n\n### 21、spark.storage.memoryFraction参数的含义,实际生产中如何调优？（☆☆☆☆☆）  \n&emsp; 1）用于设置RDD持久化数据在Executor内存中能占的比例，默认是0.6,，默认Executor 60%的内存，可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略，如果内存不够时，可能数据就不会持久化，或者数据会写入磁盘；  \n&emsp; 2）如果持久化操作比较多，可以提高spark.storage.memoryFraction参数，使得更多的持久化数据保存在内存中，提高数据的读取性能，如果shuffle的操作比较多，有很多的数据读写操作到JVM中，那么应该调小一点，节约出更多的内存给JVM，避免过多的JVM gc发生。在web ui中观察如果发现gc时间很长，可以设置spark.storage.memoryFraction更小一点。  \n\n### 22、介绍一下你对Unified Memory Management内存管理模型的理解？（☆☆☆☆☆）   \n&emsp; Spark中的内存使用分为两部分：执行（execution）与存储（storage）。执行内存主要用于shuffles、joins、sorts和aggregations，存储内存则用于缓存或者跨节点的内部数据传输。1.6之前，对于一个Executor，内存都由以下部分构成：  \n&emsp; 1）ExecutionMemory。这片内存区域是为了解决 shuffles,joins, sorts and aggregations 过程中为了避免频繁IO需要的buffer。 通过spark.shuffle.memoryFraction(默认 0.2) 配置。   \n&emsp; 2）StorageMemory。这片内存区域是为了解决 block cache(就是你显示调用rdd.cache, rdd.persist等方法), 还有就是broadcasts,以及task results的存储。可以通过参数 spark.storage.memoryFraction(默认0.6)设置。   \n&emsp; 3）OtherMemory。给系统预留的，因为程序本身运行也是需要内存的(默认为0.2)。 \n&emsp; 传统内存管理的不足：  \n&emsp; 1）Shuffle占用内存0.2*0.8，内存分配这么少，可能会将数据spill到磁盘，频繁的磁盘IO是很大的负担，Storage内存占用0.6，主要是为了迭代处理。传统的Spark内存分配对操作人的要求非常高。（Shuffle分配内存：ShuffleMemoryManager, TaskMemoryManager, ExecutorMemoryManager）一个Task获得全部的Execution的Memory，其他Task过来就没有内存了，只能等待；  \n&emsp; 2）默认情况下，Task在线程中可能会占满整个内存，分片数据\n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark（三）.md",
    "content": "## Spark面试题整理（三）  \n\n### 1、为什么要进行序列化序列化？  \n&emsp; 可以减少数据的体积，减少存储空间，高效存储和传输数据，不好的是使用的时候要反序列化，非常消耗CPU。  \n\n### 2、Yarn中的container是由谁负责销毁的，在Hadoop Mapreduce中container可以复用么？  \n&emsp; ApplicationMaster负责销毁，在Hadoop Mapreduce不可以复用，在spark on yarn程序container可以复用。  \n\n### 3、提交任务时，如何指定Spark Application的运行模式？  \n&emsp; 1）cluster模式：./spark-submit --class xx.xx.xx --master yarn --deploy-mode cluster xx.jar   \n&emsp; 2）client模式：./spark-submit --class xx.xx.xx --master yarn --deploy-mode client xx.jar  \n\n### 4、不启动Spark集群Master和work服务，可不可以运行Spark程序？  \n&emsp; 可以，只要资源管理器第三方管理就可以，如由yarn管理，spark集群不启动也可以使用spark；spark集群启动的是work和master，这个其实就是资源管理框架，\nyarn中的resourceManager相当于master，NodeManager相当于worker，做计算是Executor，和spark集群的work和manager可以没关系，归根接底还是JVM的运行，\n只要所在的JVM上安装了spark就可以。  \n\n### 5、spark on yarn Cluster 模式下，ApplicationMaster和driver是在同一个进程么？  \n&emsp; 是，driver 位于ApplicationMaster进程中。该进程负责申请资源，还负责监控程序、资源的动态情况。  \n\n### 6、运行在yarn中Application有几种类型的container？  \n&emsp; 1）运行ApplicationMaster的Container：这是由ResourceManager（向内部的资源调度器）申请和启动的，用户提交应用程序时，\n可指定唯一的ApplicationMaster所需的资源；  \n&emsp; 2）运行各类任务的Container：这是由ApplicationMaster向ResourceManager申请的，并由ApplicationMaster与NodeManager通信以启动之。  \n\n### 7、Executor启动时，资源通过哪几个参数指定？  \n&emsp; 1）num-executors是executor的数量  \n&emsp; 2）executor-memory 是每个executor使用的内存  \n&emsp; 3）executor-cores 是每个executor分配的CPU  \n\n### 8、为什么会产生yarn，解决了什么问题，有什么优势？  \n&emsp; 1）为什么产生yarn，针对MRV1的各种缺陷提出来的资源管理框架  \n&emsp; 2）解决了什么问题，有什么优势，参考这篇博文：http://www.aboutyun.com/forum.php?mod=viewthread&tid=6785  \n\n### 9、一个task的map数量由谁来决定？  \n&emsp; 一般情况下，在输入源是文件的时候，一个task的map数量由splitSize来决定的  \n&emsp; 那么splitSize是由以下几个来决定的   \n&emsp; &emsp; goalSize = totalSize / mapred.map.tasks  \n&emsp; &emsp; inSize = max {mapred.min.split.size, minSplitSize}  \n&emsp; &emsp; splitSize = max (minSize, min(goalSize, dfs.block.size))  \n&emsp; 一个task的reduce数量，由partition决定。  \n\n### 10、列出你所知道的调度器，说明其工作原理？  \n&emsp; 1）FiFo schedular 默认的调度器  先进先出  \n&emsp; 2）Capacity schedular  计算能力调度器  选择占用内存小  优先级高的  \n&emsp; 3）Fair schedular 调度器  公平调度器  所有job 占用相同资源  \n\n### 11、导致Executor产生FULL gc 的原因，可能导致什么问题？  \n&emsp; 可能导致Executor僵死问题，海量数据的shuffle和数据倾斜等都可能导致full gc。以shuffle为例，伴随着大量的Shuffle写操作，JVM的新生代不断GC，\nEden Space写满了就往Survivor Space写，同时超过一定大小的数据会直接写到老生代，当新生代写满了之后，也会把老的数据搞到老生代，如果老生代空间不足了，\n就触发FULL GC，还是空间不够，那就OOM错误了，此时线程被Blocked，导致整个Executor处理数据的进程被卡住。  \n\n### 12、Spark累加器有哪些特点？  \n&emsp; 1）累加器在全局唯一的，只增不减，记录全局集群的唯一状态；  \n&emsp; 2）在exe中修改它，在driver读取；  \n&emsp; 3）executor级别共享的，广播变量是task级别的共享两个application不可以共享累加器，但是同一个app不同的job可以共享。  \n\n### 13、spark hashParitioner的弊端是什么？  \n&emsp; HashPartitioner分区的原理很简单，对于给定的key，计算其hashCode，并除于分区的个数取余，如果余数小于0，则用余数+分区的个数，最后返回的值就是\n这个key所属的分区ID；弊端是数据不均匀，容易导致数据倾斜，极端情况下某几个分区会拥有rdd的所有数据。  \n\n### 14、RangePartitioner分区的原理？  \n&emsp; RangePartitioner分区则尽量保证每个分区中数据量的均匀，而且分区与分区之间是有序的，也就是说一个分区中的元素肯定都是比另一个分区内的元素小\n或者大；但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。其原理是水塘抽样。  \n\n### 15、rangePartioner分区器特点？  \n&emsp; rangePartioner尽量保证每个分区中数据量的均匀，而且分区与分区之间是有序的，一个分区中的元素肯定都是比另一个分区内的元素小或者大；\n但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。RangePartitioner作用：将一定范围内的数映射到某一个分区内，\n在实现中，分界的算法尤为重要。算法对应的函数是rangeBounds。  \n\n### 16、如何理解Standalone模式下，Spark资源分配是粗粒度的？  \n&emsp; spark默认情况下资源分配是粗粒度的，也就是说程序在提交时就分配好资源，后面执行的时候使用分配好的资源，除非资源出现了故障才会重新分配。\n比如Spark shell启动，已提交，一注册，哪怕没有任务，worker都会分配资源给executor。  \n\n### 17、union操作是产生宽依赖还是窄依赖？  \n&emsp; 产生窄依赖。  \n\n### 18、窄依赖父RDD的partition和子RDD的parition是不是都是一对一的关系？  \n&emsp; 不一定，除了一对一的窄依赖，还包含一对固定个数的窄依赖（就是对父RDD的依赖的Partition的数量不会随着RDD数量规模的改变而改变），\n比如join操作的每个partiion仅仅和已知的partition进行join，这个join操作是窄依赖，依赖固定数量的父rdd，因为是确定的partition关系。  \n\n### 19、Hadoop中，Mapreduce操作的mapper和reducer阶段相当于spark中的哪几个算子？  \n&emsp; 相当于spark中的map算子和reduceByKey算子，当然还是有点区别的,MR会自动进行排序的，spark要看你用的是什么partitioner。  \n\n### 20、什么是shuffle，以及为什么需要shuffle？  \n&emsp; shuffle中文翻译为洗牌，需要shuffle的原因是：某种具有共同特征的数据汇聚到一个计算节点上进行计算。  \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark（二）.md",
    "content": "## Spark面试题（二）\n\n### 1、Spark有哪两种算子？  \n&emsp; Transformation（转化）算子和Action（执行）算子。  \n\n### 2、Spark有哪些聚合类的算子,我们应该尽量避免什么类型的算子？  \n&emsp; 在我们的开发过程中，能避免则尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子，尽量使用map类的非shuffle算子。\n这样的话，没有shuffle操作或者仅有较少shuffle操作的Spark作业，可以大大减少性能开销。  \n\n### 3、如何从Kafka中获取数据？\n&emsp; 1）基于Receiver的方式  \n&emsp; 这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存\n中的，然后Spark Streaming启动的job会去处理那些数据。  \n&emsp; 2）基于Direct的方式  \n&emsp; 这种新的不基于Receiver的直接方式，是在Spark 1.3中引入的，从而能够确保更加健壮的机制。替代掉使用Receiver来接收数据后，这种方式会周期性地\n查询Kafka，来获得每个topic+partition的最新的offset，从而定义每个batch的offset的范围。当处理数据的job启动时，就会使用Kafka的简单consumer api来\n获取Kafka指定offset范围的数据。  \n\n### 4、RDD创建有哪几种方式？  \n&emsp; 1）使用程序中的集合创建rdd  \n&emsp; 2）使用本地文件系统创建rdd  \n&emsp; 3）使用hdfs创建rdd  \n&emsp; 4）基于数据库db创建rdd  \n&emsp; 5）基于Nosql创建rdd，如hbase  \n&emsp; 6）基于s3创建rdd  \n&emsp; 7）基于数据流，如socket创建rdd  \n\n### 5、Spark并行度怎么设置比较合适？  \n&emsp; spark并行度，每个core承载2~4个partition,如，32个core，那么64~128之间的并行度，也就是设置64~128个partion，并行读和数据规模无关，\n只和内存使用量和cpu使用时间有关。  \n\n### 6、Spark如何处理不能被序列化的对象？  \n&emsp; 将不能序列化的内容封装成object。  \n\n### 7、collect功能是什么，其底层是怎么实现的？  \n&emsp; driver通过collect把集群中各个节点的内容收集过来汇总成结果，collect返回结果是Array类型的，collect把各个节点上的数据抓过来，\n抓过来数据是Array型，collect对Array抓过来的结果进行合并，合并后Array中只有一个元素，是tuple类型（KV类型的）的。  \n\n### 8、为什么Spark Application在没有获得足够的资源，job就开始执行了，可能会导致什么什么问题发生？  \n&emsp; 会导致执行该job时候集群资源不足，导致执行job结束也没有分配足够的资源，分配了部分Executor，该job就开始执行task，应该是task的调度线程\n和Executor资源申请是异步的；如果想等待申请完所有的资源再执行job的：  \n&emsp; 需要将  \n&emsp; spark.scheduler.maxRegisteredResourcesWaitingTime设置的很大；  \n&emsp; spark.scheduler.minRegisteredResourcesRatio 设置为1，但是应该结合实际考虑  \n&emsp; 否则很容易出现长时间分配不到资源，job一直不能运行的情况。  \n\n### 9、map与flatMap的区别？  \n&emsp; map：对RDD每个元素转换，文件中的每一行数据返回一个数组对象。   \n&emsp; flatMap：对RDD每个元素转换，然后再扁平化。   \n&emsp; 将所有的对象合并为一个对象，文件中的所有行数据仅返回一个数组对象，会抛弃值为null的值。  \n\n### 10、Spark on Mesos中，什么是的粗粒度分配，什么是细粒度分配，各自的优点和缺点是什么？  \n&emsp; 1）粗粒度：启动时就分配好资源， 程序启动，后续具体使用就使用分配好的资源，不需要再分配资源；优点：作业特别多时，资源复用率高，适合粗粒度；\n缺点：容易资源浪费，假如一个job有1000个task，完成了999个，还有一个没完成，那么使用粗粒度，999个资源就会闲置在那里，资源浪费。  \n&emsp; 2）细粒度分配：用资源的时候分配，用完了就立即回收资源，启动会麻烦一点，启动一次分配一次，会比较麻烦。  \n\n### 11、driver的功能是什么？  \n&emsp; 1）一个Spark作业运行时包括一个Driver进程，也是作业的主进程，具有main函数，并且有SparkContext的实例，是程序的入口点；  \n&emsp; 2）功能：负责向集群申请资源，向master注册信息，负责了作业的调度，负责作业的解析、生成Stage并调度Task到Executor上。包括DAGScheduler，\nTaskScheduler。  \n\n### 12、Spark技术栈有哪些组件，每个组件都有什么功能，适合什么应用场景？  \n&emsp; 可以画一个这样的技术栈图先，然后分别解释下每个组件的功能和场景  \n&emsp; 1）Spark core：是其它组件的基础，spark的内核，主要包含：有向循环图、RDD、Lingage、Cache、broadcast等，并封装了底层通讯框架，\n是Spark的基础。  \n&emsp; 2）SparkStreaming是一个对实时数据流进行高通量、容错处理的流式处理系统，可以对多种数据源（如Kafka、Flume、Twitter、Zero和TCP 套接字）\n进行类似Map、Reduce和Join等复杂操作，将流式计算分解成一系列短小的批处理作业。  \n&emsp; 3）Spark sql：Shark是SparkSQL的前身，Spark SQL的一个重要特点是其能够统一处理关系表和RDD，使得开发人员可以轻松地使用SQL命令进行外部查询，\n同时进行更复杂的数据分析。  \n&emsp; 4）BlinkDB ：是一个用于在海量数据上运行交互式 SQL 查询的大规模并行查询引擎，它允许用户通过权衡数据精度来提升查询响应时间，其数据的精度\n被控制在允许的误差范围内。  \n&emsp; 5）MLBase是Spark生态圈的一部分专注于机器学习，让机器学习的门槛更低，让一些可能并不了解机器学习的用户也能方便地使用MLbase。\nMLBase分为四部分：MLlib、MLI、ML Optimizer和MLRuntime。  \n&emsp; 6）GraphX是Spark中用于图和图并行计算。  \n\n### 13、Spark中Worker的主要工作是什么？  \n&emsp; 主要功能：管理当前节点内存，CPU的使用状况，接收master分配过来的资源指令，通过ExecutorRunner启动程序分配任务，worker就类似于包工头，\n管理分配新进程，做计算的服务，相当于process服务。  \n&emsp; 需要注意的是：  \n&emsp; 1）worker会不会汇报当前信息给master，worker心跳给master主要只有workid，它不会发送资源信息以心跳的方式给mater，master分配的时候就知道work，\n只有出现故障的时候才会发送资源。   \n&emsp; 2）worker不会运行代码，具体运行的是Executor是可以运行具体appliaction写的业务逻辑代码，操作代码的节点，它不会运行程序的代码的。  \n\n### 14、Mapreduce和Spark的都是并行计算，那么他们有什么相同和区别？  \n&emsp; 两者都是用mr模型来进行并行计算:  \n&emsp; 1）hadoop的一个作业称为job，job里面分为map task和reduce task，每个task都是在自己的进程中运行的，当task结束时，进程也会结束。  \n&emsp; 2）spark用户提交的任务成为application，一个application对应一个SparkContext，app中存在多个job，每触发一次action操作就会产生一个job。  \n这些job可以并行或串行执行，每个job中有多个stage，stage是shuffle过程中DAGSchaduler通过RDD之间的依赖关系划分job而来的，每个stage里面有多个task，\n组成taskset有TaskSchaduler分发到各个executor中执行，executor的生命周期是和app一样的，即使没有job运行也是存在的，所以task可以快速启动读取内存\n进行计算。  \n&emsp; 3）hadoop的job只有map和reduce操作，表达能力比较欠缺而且在mr过程中会重复的读写hdfs，造成大量的io操作，多个job需要自己管理关系。  \n&emsp; 4）spark的迭代计算都是在内存中进行的，API中提供了大量的RDD操作如join，groupby等，而且通过DAG图可以实现良好的容错。  \n\n### 15、RDD机制？  \n&emsp; rdd分布式弹性数据集，简单的理解成一种数据结构，是spark框架上的通用货币。  所有算子都是基于rdd来执行的，不同的场景会有不同的rdd实现类，\n但是都可以进行互相转换。rdd执行过程中会形成dag图，然后形成lineage保证容错性等。 从物理的角度来看rdd存储的是block和node之间的映射。  \n\n### 16、什么是RDD宽依赖和窄依赖？  \n&emsp; RDD和它依赖的parent RDD(s)的关系有两种不同的类型，即窄依赖（narrow dependency）和宽依赖（wide dependency）  \n&emsp; 1）窄依赖指的是每一个parent RDD的Partition最多被子RDD的一个Partition使用  \n&emsp; 2）宽依赖指的是多个子RDD的Partition会依赖同一个parent RDD的Partition  \n\n### 17、cache和pesist的区别？  \n&emsp; cache和persist都是用于将一个RDD进行缓存的，这样在之后使用的过程中就不需要重新计算了，可以大大节省程序运行时间  \n&emsp; 1） cache只有一个默认的缓存级别MEMORY_ONLY ，cache调用了persist，而persist可以根据情况设置其它的缓存级别；  \n&emsp; 2）executor执行的时候，默认60%做cache，40%做task操作，persist是最根本的函数，最底层的函数。  \n\n### 18、 cache后面能不能接其他算子,它是不是action操作？  \n&emsp; cache可以接其他算子，但是接了算子之后，起不到缓存应有的效果，因为会重新触发cache。  \n&emsp; cache不是action操作。  \n\n### 19、reduceByKey是不是action？  \n&emsp; 不是，很多人都会以为是action，reduce rdd是action  \n\n### 20、 RDD通过Linage（记录数据更新）的方式为何很高效？  \n&emsp; 1）lazy记录了数据的来源，RDD是不可变的，且是lazy级别的，且RDD之间构成了链条，lazy是弹性的基石。由于RDD不可变，所以每次操作就产生新的rdd，\n不存在全局修改的问题，控制难度下降，所有有计算链条将复杂计算链条存储下来，计算的时候从后往前回溯 900步是上一个stage的结束，要么就checkpoint。  \n&emsp; 2）记录原数据，是每次修改都记录，代价很大如果修改一个集合，代价就很小，官方说rdd是粗粒度的操作，是为了效率，为了简化，每次都是操作数据集合，\n写或者修改操作，都是基于集合的rdd的写操作是粗粒度的，rdd的读操作既可以是粗粒度的也可以是细粒度，读可以读其中的一条条的记录。  \n&emsp; 3）简化复杂度，是高效率的一方面，写的粗粒度限制了使用场景如网络爬虫，现实世界中，大多数写是粗粒度的场景。  \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Spark面试题整理/Spark（四）.md",
    "content": "## Spark面试题（四）  \n\n### 1、Spark中的HashShufle的有哪些不足？  \n&emsp; 1）shuffle产生海量的小文件在磁盘上，此时会产生大量耗时的、低效的IO操作；  \n&emsp; 2）容易导致内存不够用，由于内存需要保存海量的文件操作句柄和临时缓存信息，如果数据处理规模比较大的话，容易出现OOM；  \n&emsp; 3）容易出现数据倾斜，导致OOM。  \n\n### 2、 conslidate是如何优化Hash shuffle时在map端产生的小文件？  \n&emsp; 1）conslidate为了解决Hash Shuffle同时打开过多文件导致Writer handler内存使用过大以及产生过多文件导致大量的随机读写带来的低效磁盘IO；  \n&emsp; 2）conslidate根据CPU的个数来决定每个task shuffle map端产生多少个文件，假设原来有10个task，100个reduce，每个CPU有10个CPU，那么\n使用hash shuffle会产生10*100=1000个文件，conslidate产生10*10=100个文件  \n&emsp; 注意：conslidate部分减少了文件和文件句柄，并行读很高的情况下（task很多时）还是会很多文件。  \n\n### 3、spark.default.parallelism这个参数有什么意义，实际生产中如何设置？  \n&emsp; 1）参数用于设置每个stage的默认task数量。这个参数极为重要，如果不设置可能会直接影响你的Spark作业性能；  \n&emsp; 2）很多人都不会设置这个参数，会使得集群非常低效，你的cpu，内存再多，如果task始终为1，那也是浪费，\nspark官网建议task个数为CPU的核数*executor的个数的2~3倍。  \n\n### 4、spark.shuffle.memoryFraction参数的含义，以及优化经验？  \n&emsp; 1）spark.shuffle.memoryFraction是shuffle调优中 重要参数，shuffle从上一个task拉去数据过来，要在Executor进行聚合操作，\n聚合操作时使用Executor内存的比例由该参数决定，默认是20%如果聚合时数据超过了该大小，那么就会spill到磁盘，极大降低性能；  \n&emsp; 2）如果Spark作业中的RDD持久化操作较少，shuffle操作较多时，建议降低持久化操作的内存占比，提高shuffle操作的内存占比比例，\n避免shuffle过程中数据过多时内存不够用，必须溢写到磁盘上，降低了性能。此外，如果发现作业由于频繁的gc导致运行缓慢，意味着task执行用户代码的内存不够用，\n那么同样建议调低这个参数的值。  \n\n### 5、Spark中standalone模式特点，有哪些优点和缺点？  \n1）特点：  \n&emsp; （1）standalone是master/slave架构，集群由Master与Worker节点组成，程序通过与Master节点交互申请资源，Worker节点启动Executor运行；  \n&emsp; （2）standalone调度模式使用FIFO调度方式；  \n&emsp; （3）无依赖任何其他资源管理系统，Master负责管理集群资源。   \n2）优点：  \n&emsp; （1）部署简单；  \n&emsp; （2）不依赖其他资源管理系统。  \n3）缺点：  \n&emsp; （1）默认每个应用程序会独占所有可用节点的资源，当然可以通过spark.cores.max来决定一个应用可以申请的CPU cores个数；  \n&emsp; （2）可能有单点故障，需要自己配置master HA。  \n\n### 6、FIFO调度模式的基本原理、优点和缺点？  \n&emsp; 基本原理：按照先后顺序决定资源的使用，资源优先满足最先来的job。第一个job优先获取所有可用的资源，接下来第二个job再获取剩余资源。  \n以此类推，如果第一个job没有占用所有的资源，那么第二个job还可以继续获取剩余资源，这样多个job可以并行运行，如果第一个job很大，占用所有资源，\n则第二job就需要等待，等到第一个job释放所有资源。  \n&emsp; 优点和缺点：  \n&emsp; &emsp; 1）适合长作业，不适合短作业；  \n&emsp; &emsp; 2）适合CPU繁忙型作业（计算时间长，相当于长作业），不利于IO繁忙型作业（计算时间短，相当于短作业）。  \n\n### 7、FAIR调度模式的优点和缺点？  \n&emsp; 所有的任务拥有大致相当的优先级来共享集群资源，spark多以轮训的方式为任务分配资源，不管长任务还是端任务都可以获得资源，并且获得不错的响应时间，\n对于短任务，不会像FIFO那样等待较长时间了，通过参数spark.scheduler.mode 为FAIR指定。  \n\n### 8、CAPCACITY调度模式的优点和缺点？  \n1）原理：  \n&emsp; 计算能力调度器支持多个队列，每个队列可配置一定的资源量，每个队列采用 FIFO 调度策略，为了防止同一个用户的作业独占队列中的资源，该调度器会对\n同一用户提交的作业所占资源量进行限定。调度时，首先按以下策略选择一个合适队列：计算每个队列中正在运行的任务数与其应该分得的计算资源之间的\n比值(即比较空闲的队列)，选择一个该比值最小的队列；然后按以下策略选择该队列中一个作业：按照作业优先级和提交时间顺序选择，\n同时考虑用户资源量限制和内存限制  \n2）优点：  \n&emsp; （1）计算能力保证。支持多个队列，某个作业可被提交到某一个队列中。每个队列会配置一定比例的计算资源，且所有提交到队列中的作业\n共享该队列中的资源；   \n&emsp; （2）灵活性。空闲资源会被分配给那些未达到资源使用上限的队列，当某个未达到资源的队列需要资源时，一旦出现空闲资源资源，便会分配给他们；  \n&emsp; （3）支持优先级。队列支持作业优先级调度（默认是FIFO）；  \n&emsp; （4）多重租赁。综合考虑多种约束防止单个作业、用户或者队列独占队列或者集群中的资源；  \n&emsp; （5）基于资源的调度。 支持资源密集型作业，允许作业使用的资源量高于默认值，进而可容纳不同资源需求的作业。不过，当前仅支持内存资源的调度。  \n\n### 9、常见的数压缩方式，你们生产集群采用了什么压缩方式，提升了多少效率？  \n1）数据压缩，大片连续区域进行数据存储并且存储区域中数据重复性高的状况下，可以使用适当的压缩算法。数组，对象序列化后都可以使用压缩，数更紧凑，\n减少空间开销。常见的压缩方式有snappy，LZO，gz等  \n2）Hadoop生产环境常用的是snappy压缩方式（使用压缩，实际上是CPU换IO吞吐量和磁盘空间，所以如果CPU利用率不高，不忙的情况下，\n可以大大提升集群处理效率）。snappy压缩比一般20%~30%之间，并且压缩和解压缩效率也非常高（参考数据如下）：  \n&emsp; （1）GZIP的压缩率最高，但是其实CPU密集型的，对CPU的消耗比其他算法要多，压缩和解压速度也慢；  \n&emsp; （2）LZO的压缩率居中，比GZIP要低一些，但是压缩和解压速度明显要比GZIP快很多，其中解压速度快的更多；  \n&emsp; （3）Zippy/Snappy的压缩率最低，而压缩和解压速度要稍微比LZO要快一些。  \n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Spark%E9%9D%A2%E8%AF%95%E9%A2%98Pics/%E5%8E%8B%E7%BC%A9%E6%96%B9%E5%BC%8F.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 提升了多少效率可以从2方面回答：1）数据存储节约多少存储，2）任务执行消耗时间节约了多少，可以举个实际例子展开描述。  \n\n### 10、使用scala代码实现WordCount？  \n&emsp; val conf = new SparkConf()   \n&emsp; val sc = new SparkContext(conf)   \n&emsp; val line = sc.textFile(\"xxxx.txt\") line.flatMap(_.split(\" \")).map((_,1)).reduceByKey(_+_). collect().foreach(println) sc.stop()  \n\n### 11、Spark RDD 和 MapReduce2的区别？  \n&emsp; 1）mr2只有2个阶段，数据需要大量访问磁盘，数据来源相对单一 ,spark RDD ,可以无数个阶段进行迭代计算，数据来源非常丰富，数据落地介质也\n非常丰富spark计算基于内存；  \n&emsp; 2）MapReduce2需要频繁操作磁盘IO，需要大家明确的是如果是SparkRDD的话，你要知道每一种数据来源对应的是什么，RDD从数据源加载数据，\n将数据放到不同的partition针对这些partition中的数据进行迭代式计算计算完成之后，落地到不同的介质当中。  \n\n### 12、spark和Mapreduce快？ 为什么快呢？ 快在哪里呢？  \n&emsp; Spark更加快的主要原因有几点：  \n&emsp; 1）基于内存计算，减少低效的磁盘交互；  \n&emsp; 2）高效的调度算法，基于DAG；  \n&emsp; 3）容错机制Lingage，主要是DAG和Lianage，即使spark不使用内存技术，也大大快于mapreduce。  \n\n### 13、Spark sql为什么比hive快呢？  \n&emsp; 计算引擎不一样，一个是spark计算模型，一个是mapreudce计算模型。  \n\n### 14、RDD的数据结构是怎么样的？  \n一个RDD对象，包含如下5个核心属性。  \n&emsp; 1）一个分区列表，每个分区里是RDD的部分数据（或称数据块）。  \n&emsp; 2）一个依赖列表，存储依赖的其他RDD。  \n&emsp; 3）一个名为compute的计算函数，用于计算RDD各分区的值。  \n&emsp; 4）分区器（可选），用于键/值类型的RDD，比如某个RDD是按散列来分区。  \n&emsp; 5）计算各分区时优先的位置列表（可选），比如从HDFS上的文件生成RDD时，RDD分区的位置优先选择数据所在的节点，这样可以避免数据移动带来的开销。  \n\n### 15、RDD算子里操作一个外部map，比如往里面put数据，然后算子外再遍历map，会有什么问题吗？  \n&emsp; 频繁创建额外对象，容易oom。  \n\n### 16、 说说你对Hadoop生态的认识。  \nhadoop生态主要分为三大类型，1）分布式文件系统，2）分布式计算引擎，3）周边工具   \n&emsp; 1）分布式系统：HDFS，hbase  \n&emsp; 2）分布式计算引擎：Spark，MapReduce  \n&emsp; 3）周边工具：如zookeeper，pig，hive，oozie，sqoop，ranger，kafka等  \n\n### 17、hbase region多大会分区，spark读取hbase数据是如何划分partition的？  \n&emsp; region超过了hbase.hregion.max.filesize这个参数配置的大小就会自动裂分，默认值是1G。  \n&emsp; 默认情况下，hbase有多少个region，Spark读取时就会有多少个partition  \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/Zookeeper面试题总结/Zookeeper.md",
    "content": "## Zookeeper面试题总结\n\n### 1、请简述Zookeeper的选举机制\n&emsp; 假设有五台服务器组成的zookeeper集群，它们的id从1-5，同时它们都是最新启动的，也就是没有历史数据，在存放数据量这一点上，都是一样的。\n假设这些服务器依序启动，来看看会发生什么。  \n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/ZK%E9%9D%A2%E8%AF%95%E9%A2%98Pics/ZK%E9%80%89%E4%B8%BE%E6%9C%BA%E5%88%B6.png\"/>  \n&emsp; （1）服务器1启动，此时只有它一台服务器启动了，它发出去的报没有任何响应，所以它的选举状态一直是LOOKING状态。  \n&emsp; （2）服务器2启动，它与最开始启动的服务器1进行通信，互相交换自己的选举结果，由于两者都没有历史数据，所以id值较大的服务器2胜出，\n但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是3)，所以服务器1、2还是继续保持LOOKING状态。  \n&emsp; （3）服务器3启动，根据前面的理论分析，服务器3成为服务器1、2、3中的Leader，而与上面不同的是，此时有三台服务器选举了它，\n所以它成为了这次选举的Leader。  \n&emsp; （4）服务器4启动，根据前面的分析，理论上服务器4应该是服务器1、2、3、4中最大的，但是由于前面已经有半数以上的服务器选举了服务器3，\n所以它成为Follower。  \n&emsp; （5）服务器5启动，同4一样成为Follower。  \n&emsp; **注意，如果按照5,4,3,2,1的顺序启动，那么5将成为Leader，因为在满足半数条件后，ZooKeeper集群启动，5的Id最大，被选举为Leader。**  \n\n### 2、客户端如何正确处理CONNECTIONLOSS(连接断开) 和 SESSIONEXPIRED(Session 过期)两类连接异常？  \n&emsp; 在ZooKeeper中，服务器和客户端之间维持的是一个长连接，在 SESSION_TIMEOUT 时间内，服务器会确定客户端是否正常连接(客户端会定时向服务器发送\nheart_beat),服务器重置下次SESSION_TIMEOUT时间。因此，在正常情况下，Session一直有效，并且zk集群所有机器上都保存这个Session信息。在出现问题的情况下，\n客户端与服务器之间连接断了（客户端所连接的那台zk机器挂了，或是其它原因的网络闪断），这个时候客户端会主动在地址列表（初始化的时候传入构造方法的那个\n参数connectString）中选择新的地址进行连接。  \n&emsp; 以上即为服务器与客户端之间维持长连接的过程，在这个过程中，用户可能会看到两类异常CONNECTIONLOSS(连接断开) 和SESSIONEXPIRED(Session 过期)。  \n&emsp; **发生CONNECTIONLOSS后，此时用户不需要关心我的会话是否可用，应用所要做的就是等待客户端帮我们自动连接上新的zk机器，一旦成功连接上新的zk机器后，\n确认之前的操作是否执行成功了**。  \n\n### 3、一个客户端修改了某个节点的数据，其他客户端能够马上获取到这个最新数据吗？  \n&emsp; ZooKeeper不能确保任何客户端能够获取（即Read Request）到一样的数据，除非客户端自己要求，方法是客户端在获取数据之前调用\norg.apache.zookeeper.AsyncCallbac k.VoidCallback, java.lang.Object) sync。  \n&emsp; 通常情况下（这里所说的通常情况满足：1. 对获取的数据是否是最新版本不敏感，2. 一个客户端修改了数据，其它客户端是否需要立即能够获取最新数据），\n可以不关心这点。  \n&emsp; 在其它情况下，最清晰的场景是这样：ZK客户端A对 /my_test 的内容从 v1->v2, 但是ZK客户端B对 /my_test 的内容获取，依然得到的是 v1. 请注意，\n这个是实际存在的现象，当然延时很短。**解决的方法是客户端B先调用 sync(), 再调用 getData()**。  \n\n### 4、ZooKeeper对节点的watch监听是永久的吗？为什么？  \n&emsp; 不是。  \n&emsp; 官方声明：一个Watch事件是一个一次性的触发器，当被设置了Watch的数据发生了改变的时候，则服务器将这个改变发送给设置了Watch的客户端，\n以便通知它们。  \n&emsp; 为什么不是永久的，举个例子，如果服务端变动频繁，而监听的客户端很多情况下，每次变动都要通知到所有的客户端，这太消耗性能了。  \n&emsp; 一般是客户端执行getData(“/节点A”,true)，如果节点A发生了变更或删除，客户端会得到它的watch事件，但是在之后节点A又发生了变更，\n而客户端又没有设置watch事件，就不再给客户端发送。  \n&emsp; 在实际应用中，很多情况下，我们的客户端不需要知道服务端的每一次变动，我只要最新的数据即可。  \n\n### 5、ZooKeeper中使用watch的注意事项有哪些？  \n&emsp; 使用watch需要注意的几点：  \n&emsp; 1）Watches通知是一次性的，必须重复注册。  \n&emsp; 2）发生CONNECTIONLOSS之后，只要在session_timeout之内再次连接上（即不发生SESSIONEXPIRED），那么这个连接注册的watches依然在。  \n&emsp; 3）节点数据的版本变化会触发NodeDataChanged，注意，这里特意说明了是版本变化。存在这样的情况，只要成功执行了setData()方法，\n无论内容是否和之前一致，都会触发NodeDataChanged。  \n&emsp; 4）对某个节点注册了watch，但是节点被删除了，那么注册在这个节点上的watches都会被移除。  \n&emsp; 5）同一个zk客户端对某一个节点注册相同的watch，只会收到一次通知。  \n&emsp; 6）Watcher对象只会保存在客户端，不会传递到服务端。  \n\n### 6、能否收到每次节点变化的通知？  \n&emsp; 如果节点数据的更新频率很高的话，不能。  \n&emsp; 原因在于：当一次数据修改，通知客户端，客户端再次注册watch，在这个过程中，可能数据已经发生了许多次数据修改，因此，\n千万不要做这样的测试：”数据被修改了n次，一定会收到n次通知”来测试server是否正常工作。  \n\n### 7、能否为临时节点创建子节点？  \n&emsp; ZooKeeper中不能为临时节点创建子节点，如果需要创建子节点，应该将要创建子节点的节点创建为永久性节点。  \n\n### 8、是否可以拒绝单个IP对ZooKeeper的访问？\n&emsp; 如何实现？ZK本身不提供这样的功能，它仅仅提供了对单个IP的连接数的限制。你可以通过修改iptables来实现对单个ip的限制。  \n\n### 9、创建的临时节点什么时候会被删除，是连接一断就删除吗？\n&emsp; 延时是多少？连接断了之后，ZK不会马上移除临时数据，只有当SESSIONEXPIRED之后，才会把这个会话建立的临时数据移除。因此，\n用户需要谨慎设置Session_TimeOut。  \n\n### 10、ZooKeeper是否支持动态进行机器扩容？如果目前不支持，那么要如何扩容呢？\n&emsp; ZooKeeper中的动态扩容其实就是水平扩容，Zookeeper对这方面的支持不太好，目前有两种方式：  \n&emsp; 全部重启：关闭所有Zookeeper服务，修改配置之后启动，不影响之前客户端的会话。  \n&emsp; 逐个重启：这是比较常用的方式。  \n\n### 11、ZooKeeper集群中服务器之间是怎样通信的？\n&emsp; Leader服务器会和每一个Follower/Observer服务器都建立TCP连接，同时为每个F/O都创建一个叫做LearnerHandler的实体。\nLearnerHandler主要负责Leader和F/O之间的网络通讯，包括数据同步，请求转发和Proposal提议的投票等。Leader服务器保存了所有F/O的LearnerHandler。  \n\n### 12、ZooKeeper是否会自动进行日志清理？\n&emsp; 如何进行日志清理？zk自己不会进行日志清理，需要运维人员进行日志清理。  \n\n### 13、谈谈你对ZooKeeper的理解？\n&emsp; Zookeeper 作为一个分布式的服务框架，主要用来解决分布式集群中应用系统的一致性问题。ZooKeeper提供的服务包括：分布式消息同步和协调机制、\n服务器节点动态上下线、统一配置管理、负载均衡、集群管理等。  \n&emsp; ZooKeeper提供基于类似于Linux文件系统的目录节点树方式的数据存储，即分层命名空间。Zookeeper 并不是用来专门存储数据的，\n它的作用主要是用来维护和监控你存储的数据的状态变化，通过监控这些数据状态的变化，从而可以达到基于数据的集群管理，ZooKeeper节点的数据上限是1MB。  \n&emsp; 我们可以认为Zookeeper=文件系统+通知机制，对于ZooKeeper的数据结构，每个子目录项如 NameService 都被称作为 znode，这个 znode 是被它所在的\n路径唯一标识，如 Server1 这个 znode 的标识为 /NameService/Server1；  \n&emsp; znode 可以有子节点目录，并且每个 znode 可以存储数据，注意 EPHEMERAL 类型的目录节点不能有子节点目录(因为它是临时节点)；    \n&emsp; znode 是有版本的，每个 znode 中存储的数据可以有多个版本，也就是一个访问路径中可以存储多份数据；  \n&emsp; znode 可以是临时节点，一旦创建这个 znode 的客户端与服务器失去联系，这个 znode 也将自动删除，Zookeeper 的客户端和服务器通信采用长连接方式，\n每个客户端和服务器通过心跳来保持连接，这个连接状态称为 session，如果 znode 是临时节点，这个 session 失效，znode 也就删除了；  \n&emsp; znode 的目录名可以自动编号，如 App1 已经存在，再创建的话，将会自动命名为 App2；  \n&emsp; znode 可以被监控，包括这个目录节点中存储的数据的修改，子节点目录的变化等，一旦变化可以通知设置监控的客户端，这个是 Zookeeper 的核心特性，\nZookeeper 的很多功能都是基于这个特性实现的，后面在典型的应用场景中会有实例介绍。  \n\n### 14、ZooKeeper节点类型？\n&emsp; 1）Znode有两种类型：  \n&emsp; &emsp; 短暂（ephemeral）：客户端和服务器端断开连接后，创建的节点自己删除。  \n&emsp; &emsp; 持久（persistent）：客户端和服务器端断开连接后，创建的节点不删除。  \n&emsp; 2）Znode有四种形式的目录节点（默认是persistent ）  \n&emsp; &emsp; （1）持久化目录节点（PERSISTENT） 客户端与zookeeper断开连接后，该节点依旧存在。  \n&emsp; &emsp; （2）持久化顺序编号目录节点（PERSISTENT_SEQUENTIAL） 客户端与zookeeper断开连接后，该节点依旧存在，\n只是Zookeeper给该节点名称进行顺序编号。  \n&emsp; &emsp; （3）临时目录节点（EPHEMERAL） 客户端与zookeeper断开连接后，该节点被删除。  \n&emsp; &emsp; （4）临时顺序编号目录节点（EPHEMERAL_SEQUENTIAL）客户端与zookeeper断开连接后，该节点被删除，只是Zookeeper给该节点名称进行顺序编号。  \n\n### 15、请说明ZooKeeper的通知机制？\n&emsp; ZooKeeper选择了基于通知（notification）的机制，即：客户端向ZooKeeper注册需要接受通知的znode，通过znode设置监控点（watch）来接受通知。\n监视点是一个单次触发的操作，意即监视点会触发一个通知。为了接收多个通知，客户端必须在每次通知后设置一个新的监视点。在下图阐述的情况下，\n当节点/task发生变化时，客户端会受到一个通知，并从ZooKeeper读取一个新值。  \n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/ZK%E9%9D%A2%E8%AF%95%E9%A2%98Pics/ZK%E9%80%9A%E7%9F%A5%E6%9C%BA%E5%88%B6.png\"/>  \n\n### 16、ZooKeeper的监听原理是什么？\n&emsp; 在应用程序中，mian()方法首先会创建zkClient，创建zkClient的同时就会产生两个进程，即Listener进程（监听进程）和\nconnect进程（网络连接/传输进程），当zkClient调用getChildren()等方法注册监视器时，connect进程向ZooKeeper注册监听器，\n注册后的监听器位于ZooKeeper的监听器列表中，监听器列表中记录了zkClient的IP，端口号以及要监控的路径，一旦目标文件发生变化，\nZooKeeper就会把这条消息发送给对应的zkClient的Listener()进程，Listener进程接收到后，就会执行process()方法，\n在process()方法中针对发生的事件进行处理。  \n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/ZK%E9%9D%A2%E8%AF%95%E9%A2%98Pics/ZK%E7%9B%91%E5%90%AC%E5%8E%9F%E7%90%86.png\"/>  \n\n### 17、请说明ZooKeeper使用到的各个端口的作用？\n&emsp; 2888：Follower与Leader交换信息的端口。  \n&emsp; 3888：万一集群中的Leader服务器挂了，需要一个端口来重新进行选举，选出一个新的Leader，而这个端口就是用来执行选举时服务器相互通信的端口。  \n\n### 18、ZooKeeper的部署方式有哪几种？集群中的角色有哪些？集群最少需要几台机器？\n&emsp; ZooKeeper的部署方式有单机模式和集群模式，集群中的角色有Leader和Follower，集群最少3（2N+1）台，根据选举算法，应保证奇数。  \n\n### 19、ZooKeeper集群如果有3台机器，挂掉一台是否还能工作？挂掉两台呢？\n&emsp; 对于ZooKeeper集群，过半存活即可使用。  \n\n### 20、ZooKeeper使用的ZAB协议与Paxo算法的异同？\n&emsp; Paxos算法是分布式选举算法，Zookeeper使用的 ZAB协议（Zookeeper原子广播），两者的异同如下：  \n&emsp; 1）相同之处：  \n&emsp; &emsp; 比如都有一个Leader，用来协调N个Follower的运行；Leader要等待超半数的Follower做出正确反馈之后才进行提案；\n二者都有一个值来代表Leader的周期。  \n&emsp; 2）不同之处：  \n&emsp; &emsp; ZAB用来构建高可用的分布式数据主备系统（Zookeeper），Paxos是用来构建分布式一致性状态机系统。  \n\n### 21、请谈谈对ZooKeeper对事务性的支持？\n&emsp; ZooKeeper对于事务性的支持主要依赖于四个函数，zoo_create_op_init， zoo_delete_op_init， zoo_set_op_init以及zoo_check_op_init。\n每一个函数都会在客户端初始化一个operation，客户端程序有义务保留这些operations。当准备好一个事务中的所有操作后，可以使用zoo_multi来提交所有的操作，\n由zookeeper服务来保证这一系列操作的原子性。也就是说只要其中有一个操作失败了，相当于此次提交的任何一个操作都没有对服务端的数据造成影响。\nZoo_multi的返回值是第一个失败操作的状态信号。  \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "面试系列/pics/Flume面试题Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/HBase面试题Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Hadoop面试题Pics/1.md",
    "content": "\n"
  },
  {
    "path": "面试系列/pics/Hadoop面试题Pics/HDFS文档-Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Hadoop面试题Pics/MR-Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Hadoop面试题Pics/YARN-Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Hive面试题Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Kafka面试题Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Spark面试题Pics/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Spark面试题Pics/数据倾斜调优/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/Spark面试题Pics/程序开发调优/1.md",
    "content": "\n"
  },
  {
    "path": "面试系列/pics/Spark面试题Pics/资源调优/1.md",
    "content": "1\n"
  },
  {
    "path": "面试系列/pics/ZK面试题Pics/1.md",
    "content": "1\n"
  }
]