-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 489 KB
/
content.json
1
{"meta":{"title":"APPARITION957","subtitle":null,"description":"沉迷学习无法自拔","author":"apparition957","url":"http://apparition957.github.io"},"pages":[{"title":"","date":"2020-03-26T15:20:00.845Z","updated":"2020-03-26T15:20:00.845Z","comments":true,"path":"google605185e3f7050a2d.html","permalink":"http://apparition957.github.io/google605185e3f7050a2d.html","excerpt":"","text":"google-site-verification: google605185e3f7050a2d.html"},{"title":"文章","date":"2017-02-25T16:23:19.000Z","updated":"2017-07-30T03:51:19.000Z","comments":true,"path":"articles/index.html","permalink":"http://apparition957.github.io/articles/index.html","excerpt":"","text":"[clean-my-archives author=”jianpeng957”]"},{"title":"about","date":"2017-11-10T13:10:22.000Z","updated":"2020-01-08T11:50:52.581Z","comments":true,"path":"about/index.html","permalink":"http://apparition957.github.io/about/index.html","excerpt":"","text":"关于我 应届入职于北京小米的全局搜索部门 热衷于后台开发工作,喜欢接触很多新的技术 喜欢摄影旅行,也喜欢看科幻、编程方面的书籍"},{"title":"categories","date":"2017-07-30T10:39:26.000Z","updated":"2017-07-30T10:46:05.000Z","comments":true,"path":"categories/index.html","permalink":"http://apparition957.github.io/categories/index.html","excerpt":"","text":""},{"title":"与我联系","date":"2017-02-25T15:52:58.000Z","updated":"2017-07-30T04:16:04.000Z","comments":true,"path":"与我联系/index.html","permalink":"http://apparition957.github.io/与我联系/index.html","excerpt":"","text":"请告知您针对我们的问题和/或反馈。我们会尽快与您联系。 谢谢!我们期待收到您的反馈。 [ninja_forms id=1]"},{"title":"文章","date":"2017-02-25T16:23:19.000Z","updated":"2017-07-30T04:16:04.000Z","comments":true,"path":"文章/index.html","permalink":"http://apparition957.github.io/文章/index.html","excerpt":"","text":"[clean-my-archives author=”jianpeng957”]"},{"title":"关于我","date":"2017-02-25T15:52:58.000Z","updated":"2017-07-30T04:16:04.000Z","comments":true,"path":"关于我/index.html","permalink":"http://apparition957.github.io/关于我/index.html","excerpt":"","text":"您好!我是 YOUR NAME。感谢光顾。 请在这里插入个人简历"}],"posts":[{"title":"《The Dataflow Model》论文翻译","slug":"《The-Dataflow-Model》论文翻译","date":"2020-01-06T16:08:05.000Z","updated":"2020-01-08T06:17:22.756Z","comments":true,"path":"2020/01/07/《The-Dataflow-Model》论文翻译/","link":"","permalink":"http://apparition957.github.io/2020/01/07/《The-Dataflow-Model》论文翻译/","excerpt":"","text":"The Dataflow Model 是 Google Research 于2015年发表的一篇流式处理领域的具有指导性意义的论文,它对数据集特征和相应的计算方式进行了归纳总结,并针对大规模/无边界/乱序数据集,提出一种可以平衡准确性/延迟/处理成本的数据模型。这篇论文的目的不在于解决目前流计算引擎无法解决的问题,而是提供一个灵活的通用数据模型,可以无缝地切合不同的应用场景。(来源于:时间与精神小屋的论文总结) 本论文是通过机翻+人翻结合一起的,里面包含大量的长句,如果纯人翻的话,完全啃下来有点难! ABSTRACTUnbounded, unordered, global-scale datasets are increasingly common in day-to-day business (e.g. Web logs, mobileusage statistics, and sensor networks). At the same time,consumers of these datasets have evolved sophisticated requirements, such as event-time ordering and windowing by features of the data themselves, in addition to an insatiable hunger for faster answers. Meanwhile, practicality dictates that one can never fully optimize along all dimensions of correctness, latency, and cost for these types of input. As a result, data processing practitioners are left with the quandary of how to reconcile the tensions between these seemingly competing propositions, often resulting in disparate implementations and systems. 无边界的、无序的、全球范围的数据集在日常业务中越来越普遍(例如,Web日志,移动设备使用情况统计信息和传感器网络)。 同时,这些数据集的消费者已经提出了更加复杂的需求,例如基于event-time(事件时间)的排序和数据特征本身的窗口聚合,以满足消费者对于快速消费数据的庞大需求。与此同时,从实用性的角度出发,对于以上提到的数据集,我们永远无法在准确(correctness),延迟(latency)和成本(cost)等所有维度上进行全面优化。 最后,数据处理人员需要在这些看似冲突的方面之间做出妥协与调和,而这些做法往往会产生不同的实现与框架。 We propose that a fundamental shift of approach is necessary to deal with these evolved requirements in modern data processing. We as a field must stop trying to groom unbounded datasets into finite pools of information that eventually become complete, and instead live and breathe under the assumption that we will never know if or when we have seen all of our data, only that new data will arrive, old data may be retracted, and the only way to make this problem tractable is via principled abstractions that allow the practitioner the choice of appropriate tradeoffs along the axes of interest: correctness, latency, and cost. 我们认为有关于数据处理的方法必须得到根本性的改变,以应对现代数据处理中这些不断发展的需求。作为流式处理的领域中,我们必须停止尝试将无边界的数据集归整成完整的、有限的信息池,因为在一般的情况下,我们永远不知道是否或者何时能看到所有的数据。使得该问题变得易于解决的唯一方法就是通过一些规则上的抽象,使得数据处理人员能够从准确(correctness),延迟(latency)和成本(cost)几个维度做出妥协。 In this paper, we present one such approach, the Dataflow Model, along with a detailed examination of the semantics it enables, an overview of the core principles that guided its design, and a validation of the model itself via the real-world experiences that led to its development. 在本文中,我们提出了一种这样的方法,The Dataflow Model,并对其支持的语义进行了详细的审视,概述其设计指导的核心原则,并通过实际的开发经验验证了模型本身的可行性。 1. INTRODUCTIONModern data processing is a complex and exciting field. From the scale enabled by MapReduce and its successors(e.g Hadoop, Pig, Hive, Spark), to the vast body of work on streaming within the SQL community (e.g.query systems, windowing, data streams,time domains, semantic models), to the more recent forays in low-latency processing such as Spark Streaming, MillWheel, and Storm, modern consumers of data wield remarkable amounts of power in shaping and taming massive-scale disorder into organized structures with far greater value. Yet, existing models and systems still fall short in a number of common use cases. 现代数据处理是一个复杂且令人兴奋的领域。从MapReduce及其继承者(e.g. Hadoop,Pig,Hive,Spark)实现的大规模运算,到SQL社区对流式处理做出的巨大工作(e.g. 查询系统(query system),窗口(windowing),数据流(data streams),时间域(time domains),语义模型(semantic model)),再到近期如Spark Streaming,MillWheel和Storm对于低延迟数据处理的初步尝试,现代数据的消费者挥舞着庞大的力量,尝试将大规模的、无序的海量数据规整为具有巨大价值的、易于管理的结构当中。然而,现有的模型和系统在许多常见的用例仍然存在不足的地方。 Consider an initial example: a streaming video provider wants to monetize their content by displaying video ads and billing advertisers for the amount of advertising watched. The platform supports online and offline views for content and ads. The video provider wants to know how much to bill each advertiser each day, as well as aggregate statistics about the videos and ads. In addition, they want to efficiently run offline experiments over large swaths of historical data. 考虑一个比较简单的例子:流视频提供者希望通过展示视频广告来使其视频内容能够盈利,并且通过广告的观看量对广告商收取一定的费用。该平台同时支持在线和离线观看视频和广告。视频提供者想要知道每天应向每个广告商收取多少费用,以及所有视频和广告的统计情况。此外,他们还希望能够有效率地对大量的历史数据进行离线实验。 Advertisers/content providers want to know how often and for how long their videos are being watched, with which content/ads, and by which demographic groups. They also want to know how much they are being charged/paid. They want all of this information as quickly as possible, so that they can adjust budgets and bids, change targeting, tweak campaigns, and plan future directions in as close to realtime as possible. Since money is involved, correctness is paramount. 而广告商/内容提供商想要知道他们的视频被观看的频率和时长,观看的内容/广告是什么,观看的人群是什么。他们也想知道他们需要为此要付出多少费用。他们希望尽可能快地获得所有这些信息,这样他们就可以调整预算和投标,改变目标,调整活动,并尽可能实时地规划未来的方向。因为涉及到钱,所以系统上设计时需要首要重点考虑其准确性。 Though data processing systems are complex by nature,the video provider wants a programming model that is simple and flexible. And finally, since the Internet has so greatly expanded the reach of any business that can be parceled along its backbone, they also require a system that can handle the diaspora of global scale data. 虽然数据处理系统本质上是复杂的,但是视频提供商却想要一个简单而灵活的编程模型。最后,由于互联网极大地扩展了任何可以沿着其主干分布的业务的范围,他们还需要一个能够处理全球范围内所有分散数据的系统。 The information that must be calculated for such a usecase is essentially the time and length of each video viewing,who viewed it, and with which ad or content it was paired(i.e. per-user, per-video viewing sessions). Conceptually this is straightforward, yet existing models and systems all fall short of meeting the stated requirements. 对于这样的一个用例,必须计算的信息本质上等同于每个视频观看的时长、谁观看了它,以及它与哪个广告或内容配对(e.g. 每个用户,每个视频观看会话)。从概念上讲,这很简单,但是现有的模型和系统都不能满足上述提到的需求。 Batch systems such as MapReduce (and its Hadoop vari-ants, including Pig and Hive), FlumeJava, and Spark suffer from the latency problems inherent with collecting all input data into a batch before processing it. For many streaming systems, it is unclear how they would remain fault-tolerantat scale (Aurora, TelegraphCQ, Niagara, Esper). Those that provide scalability and fault-tolerance fall short on expressiveness or correctness vectors. 诸如MapReduce(及其Hadoop变体,包括Pig和Hive),FlumeJava和Spark之类的批处理系统都碰到了在批处理之前需要将所有输入数据导入系统时所带来的延迟问题。对于许多流系统,我们无法清晰地知道他们是如何构建大规模的容错机制(Aurora,TelegraphCQ,Niagara,Esper),而那些提供可伸缩性和容错性的系统则在表达性或准确性方面上表现不足。 Many lack the ability to provide exactly-once semantics (Storm, Samza, Pulsar), impacting correctness. Others simply lack the temporal primitives necessary for windowing(Tigon), or provide windowing semantics that are limited to tuple- or processing-time-based windows (Spark Streaming, Sonora, Trident). 许多框架都缺乏提供exactly-once语义的能力(Storm,Samza,Pulsar),从而影响了数据的准确性。 而其他框架则缺少窗口所必需的时间原语(Tigon),或提供仅限于以元组(tuple-)或处理时间(processing-time)为基础的窗口语义(Spark Streaming,Sonora,Trident)。 Most that provide event-time-based windowing either rely on ordering (SQLStream),or have limited window triggering semantics in event-time mode (Stratosphere/Flink). CEDR and Trill are note worthy in that they not only provide useful triggering semantics via punctuations, but also provide an overall incremental model that is quite similar to the one we propose here; however, their windowing semantics are insufficient to express sessions, and their periodic punctuations are insufficient for some of the use cases in Section3.3. MillWheel and Spark Streaming are both sufficiently scalable, fault-tolerant, and low-latency to act as reasonable substrates, but lack high-level programming models that make calculating event-time sessions straightforward. 大多数框架提供的基于 event-time 的窗口机制要么依赖于排序(SQLStream),要么在event-time 模式下提供有限的窗口触发语义(Stratosphere / Flink)。值得一提的是,CEDR 和 Trill不仅可以通过标点符号(punctuations)提供有效的窗口触发语义,而且还提供了一个整体增量(overall incremental)的模型,该模型与我们此处提到的模型非常相似。然而,它们的窗口语义并不足以表达会话(sessions),并且它们的周期性标点符号不足以满足3.3节中的某些用例。MillWhell 和 Spark Streaming 都具有伸缩性,容错性和低延迟性,作为流框架合理的基础架构,但是其缺少能够让基于 event-time 的会话计算变得通俗易懂的高级编程模型。 The only scalable system we are aware of that supports a high-level notion of unaligned windows such as sessions is Pulsar, but that system fails to provide correctness, as noted above. Lambda Architecture systems can achieve many of the desired requirements, but fail on the simplicity axis on account of having to build and maintain two systems. Summingbird ameliorates this implementation complexity by abstracting the underlying batch and streaming systems behind a single interface, but in doing so imposes limitations on the types of computation that can be performed, and still requires double the operational complexity. 我们观察到唯一具有伸缩性,并且支持未对齐窗口(例如会话)这种高级概念的流数据系统是Pulsar,但是如上所述,该系统无法提供准确性。Lambda架构体系可以满足许多我们期望的要求,但是由于必须构建和维护两套系统,因此其在简单性这一维度上就注定失败。Summingbird通过在单一接口背后抽象底层的批系统和流系统,来改善其实现的复杂性,但是这样做会限制其可以执行的计算类型,并且仍会有两倍的操作复杂性。 None of these short comings are intractable, and systems in active development will likely overcome them in due time. But we believe a major shortcoming of all the models and systems mentioned above (with exception given to CEDR and Trill), is that they focus on input data (unbounded orotherwise) as something which will at some point become complete. We believe this approach is fundamentally flawed when the realities of today’s enormous, highly disordered datasets clash with the semantics and timeliness demanded by consumers. We also believe that any approach that is to have broad practical value across such a diverse and variedset of use cases as those that exist today (not to mention those lingering on the horizon) must provide simple, but powerful, tools for balancing the amount of correctness, latency, and cost appropriate for the specific use case at hand. 这些缺点都不是很难解决的,积极开发中的系统很可能会在适当的时候攻克它们。 但是我们认为,上述所有模型和系统(CEDR和Trill除外)的一个主要缺点是,它们只专注于那些最终在某些时刻达到完整的输入数据(无界或其他)。 我们认为,当现今庞大且高度混乱的数据集与消费者要求的语义和及时性发生冲突时,这种方法从根本上是有缺陷的。 我们还认为,任何在如今多样的用例中都具有广泛实用价值的方法(更不用说那些长期存在的用例)必须提供简单但强大的工具来平衡准确性,低延迟性和适合于特定用例的成本。 Lastly, we believe it is time to move beyond the prevailing mindset of an execution engine dictating system semantics; properly designed and built batch, micro-batch, and streaming systems can all provide equal levels of correctness, and all three see widespread use in unbounded data processing today. Abstracted away beneath a model of sufficient generality and flexibility, we believe the choice of execution engine can become one based solely on the practical underlying differences between them: those of latency and resource cost. 最后,我们认为是时候超越执行引擎决定系统语义的主流思维了。 经过正确设计和构建的批处理,微批处理和流传输系统都可以提供同等程度的准确性,并且这三者在当今的无边界数据处理中都可以得到了广泛使用。 在具有足够通用性和灵活性的模型下进行抽象,我们认为执行引擎的选择可以仅基于它们之间的实际潜在差异(即延迟和资源成本)进行选择。 Taken from that perspective, the conceptual contribution of this paper is a single unified model which: Allows for the calculation of event-time ordered results, windowed by features of the data themselves, over an unbounded, unordered data source, with correctness, latency, and cost tunable across a broad spectrum of combinations. Decomposes pipeline implementation across four related dimensions, providing clarity, composability, andflexibility: – What results are being computed. – Where in event time they are being computed. – When in processing time they are materialized. – How earlier results relate to later refinements. Separates the logical notion of data processing from the underlying physical implementation, allowing the choice of batch, micro-batch, or streaming engine to become one of simply correctness, latency, and cost. 从这个角度来看,本文提出了一个单一且统一的模型概念,即: 允许计算event-time排序的结果,并根据数据本身的特征在无边界,无序的数据源上进行窗口化,其准确性,延迟和成本可在多种组合中调整。 分解四个跨维度相关的管道实现,以提供清晰性,可组合性和灵活性: – What 正在计算什么结果。 – Where 在事件发生时,它们被计算在哪里。 – When 何时在prcoessing-time内实现。 – How 早期的结果如何与后来的改进相联系。 将数据处理的逻辑概念与底层物理实现分开,允许批处理,微批处理或流引擎的选择成为准确性,延迟和成本中的一种。 Concretely, this contribution is enabled by the following: A windowing model which supports unaligned event-time windows, and a simple API for their creation and use (Section 2.2). A triggering model that binds the output times of results to runtime characteristics of the pipeline, with a powerful and flexible declarative API for describing desired triggering semantics (Section 2.3). An incremental processing model that integrates retractions and updates into the windowing and triggering models described above (Section 2.3). Scalable implementations of the above atop the MillWheel streaming engine and the FlumeJava batch engine, with an external reimplementation for GoogleCloud Dataflow, including an open-source SDK that is runtime-agnostic (Section 3.1). A set of core principles that guided the design of this model (Section 3.2). Brief discussions of our real-world experiences with massive-scale, unbounded, out-of-order data processing at Google that motivated development of this model(Section 3.3). 具体来说,这一模型可由下面几个概念形成: 窗口模型(A windowing model)。支持未对齐的event-time窗口,以及提供易于创建和使用窗口 API 的模型(章节2.2)。 触发模型(A triggering model )。将输出的时间结果与具有运行特性的管道进行绑定,并提供功能强大且灵活的声明性 API,用于描述所需的触发语义(章节2.3)。 增量处理模型(incremental processing model)。将数据回撤功能和数据更新功能集成到上述窗口和触发模型中(章节2.3)。 可扩展的实现(Scalable implementations)。在MillWheel流引擎和FlumeJava批处理引擎之上的可扩展实现以及对GoogleCloud Dataflow的外部重新实现,包括与运行时无关的开源SDK(章节3.1)。 核心原则(core principles)。用于指导该模型设计的一组核心原则(章节3.2)。 真实经验( real-world experiences )。简要讨论了我们在Google上使用大规模,无边界,无序数据处理的真实经验,这些经验推动了该模型的发展(章节3.3)。 It is lastly worth noting that there is nothing magical about this model. Things which are computationally impractical in existing strongly-consistent batch, micro-batch, streaming, or Lambda Architecture systems remain so, with the inherent constraints of CPU, RAM, and disk left steadfastly in place. What it does provide is a common framework that allows for the relatively simple expression of parallel computation in a way that is independent of the underlying execution engine, while also providing the ability to dial in precisely the amount of latency and correctness for any specific problem domain given the realities of the data and resources at hand. In that sense, it is a model aimed at ease of use in building practical, massive-scale data processing pipelines. 最后值得注意的是,这个模型没有什么神奇之处。在现有的强一致批处理、微批处理、流处理或Lambda体系结构系统中,那些不现实的东西依旧存在,CPU、RAM和 Disk的固有约束仍然稳定存在。它所提供的是一个通用的框架,该框架允许以独立于底层执行引擎的方式对并行计算进行相对简单的表达,同时还提供了在现有数据和资源下,为任何特定问题精确计算延迟和准确性的能力。从某种意义上说,它是一种旨在易于使用的模型,可用于构建实用的大规模数据处理管道。 1.1 Unbounded/Bounded vs Streaming/BatchWhen describing infinite/finite data sets, we prefer the terms unbounded/bounded over streaming/batch, because the latter terms carry with them an implication of the use of a specific type of execution engine. In reality, unbounded datasets have been processed using repeated runs of batch systems since their conception, and well-designed streaming systems are perfectly capable of processing bounded data. From the perspective of the model, the distinction of streaming or batch is largely irrelevant, and we thus reserve those terms exclusively for describing runtime execution engines. 当描述无限/有限数据集时,我们首选“无界/有界”这一术语而不是“流/批处理”,因为后者会带来使用特定类型执行引擎的隐含含义。 实际上,自从无边界数据集的概念诞生以来,就已经使用批处理系统的重复运行对其进行了处理,而精心设计的流系统则完全能够处理有边界的数据。 从模型的角度来看,流或批处理的区别在很大程度上是无关紧要的,因此,我们保留了那些专门用于描述运行时执行引擎的术语。 1.2 WindowingWindowing slices up a dataset into finite chunks for processing as a group. When dealing with unbounded data, windowing is required for some operations (to delineate finite boundaries in most forms of grouping: aggregation,outer joins, time-bounded operations, etc.), and unnecessary for others (filtering, mapping, inner joins, etc.). For bounded data, windowing is essentially optional, though still a semantically useful concept in many situations (e.g. back-filling large scale updates to portions of a previously computed unbounded data source). 窗口化(Windowing)将数据集切成有限的数据块,以作为一组进行处理。 处理无边界数据时,某些操作(在大多数分组形式中描绘有限边界:聚合,外部联接,有时间限制的操作等)需要窗口化,而其他操作(过滤,映射,内部联接等)则不需要。 对于有界数据,窗口化在本质上是可选的,尽管在许多情况下仍然是语义上十分有用的概念(例如,回填大规模数据更新到先前计算的无界数据源的某些部分中)。 Windowing is effectively always time based, while many systems support tuple-based windowing, this is essentially time-based windowing over a logical time domain where elements in order have successively increasing logical timestamps. Windows may be either aligned, i.e. applied across all the data for the window of time in question, or unaligned, i.e. applied across only specific subsets of the data (e.g. per key) for the given window of time. Figure 1 highlights three of the major types ofwindows encountered when dealing with unbounded data. 实际上,窗口化总是基于时间的,虽然许多系统支持基于元组的窗口,但这本质上还是基于时间的窗口,并在逻辑时间域上,元素按顺序依次增加逻辑时间戳。窗口可以是对齐的,即在时间窗口中应用所有数据,也可以是未对齐的,即在给定时间窗口中只应用数据的特定子集(例如,每个键值)。图1突出显示了在处理无界数据时遇到的三种主要windows类型。 Fixed windows (sometimes called tumbling windows) are defined by a static window size, e.g. hourly windows or daily windows. They are generally aligned, i.e. every window applies across all of the data for the corresponding period of time. For the sake of spreading window completion load evenly across time, they are sometimes unaligned by phase shifting the windows for each key by some random value. 固定窗口(有时称为翻滚窗口)。固定窗口由静态窗口大小定义,例如每小时一次或每天一次。 它们通常是对齐的,即每个窗口都在相应的时间段内应用于所有数据。为了使窗口完成时间均匀地分布在整个时间上,有时通过将每个键的窗口位移某个随机值来使它们不对齐。 Sliding windows are defined by a window size and slide period, e.g. hourly windows starting every minute. The period may be less than the size, which means the windows may overlap. Sliding windows are also typically aligned; even though the diagram is drawn to give a sense of sliding motion, all five windows would be applied to all three keys inthe diagram, not just Window 3. Fixed windows are really a special case of sliding windows where size equals period. 滑动窗口。滑动窗口由窗口大小和滑动周期定义,例如每分钟启动一次统计每小时的窗口。周期可能会小于窗口大小,这意味着窗口之间可能会发生重叠。 滑动窗口通常也会对齐,即使绘制该图给人提供一种滑动的感觉,所有五个窗口也将应用于该图中的所有三个键,而不仅仅是窗口3。固定窗口实际上是窗口大小等于滑动周期大小的滑动窗口的一种特殊情况。 Sessions are windows that capture some period of activity over a subset of the data, in this case per key. Typically they are defined by a timeout gap. Any events that occur within a span of time less than the timeout are grouped together as a session. Sessions are unaligned windows. For example, Window 2 applies to Key 1 only, Window 3 to Key2 only, and Windows 1 and 4 to Key 3 only. 会话窗口。会话是捕获数据子集(在此情况下为每个键值)的一段时间活动的窗口。 通常,它们由超时时间间隔定义的。 在小于超时的时间间隔范围内发生的任何事件都被归为一个会话。 会话是未对齐的窗口。 例如,窗口2仅适用于键1,窗口3仅适用于键2,窗口1和4仅适用于键3。 1.3 Time DomainsWhen processing data which relate to events in time, there are two inherent domains of time to consider. Though captured in various places across the literature (particularly time management and semantic models, but also windowing, out-of-order processing, punctuations, heartbeats, watermarks, frames), the detailed examples in section 2.3 will be easier to follow with the concepts clearly in mind. The two domains of interest are: Event Time, which is the time at which the event itself actually occurred, i.e. a record of system clock time (for whatever system generated the event) at the time of occurrence. Processing Time, which is the time at which an event is observed at any given point during processing within the pipeline, i.e. the current time according to the system clock. Note that we make no assumptions about clock synchronization within a distributed system. 在处理与时间事件相关的数据时,需要考虑两个固有的时间域。虽然在文献的不同地方都已经提到过(特别是时间管理和语义模型,但也有窗口,无序处理,标点(punctuations),心跳,水印(watermarks),帧(frame)),详细的例子在章节2.3中展示,其将有助于帮助我们在脑海中更加清晰地掌握它。以下两个时间领域我们所关心的是: 事件时间(Event Time)。即事件本身实际发生的时间,即系统时钟时间(对于生成事件的任何系统)在事件发生时的记录。 处理时间 (Processing Time)。这是在流水线内处理期间在任何给定点观察到事件的时间,即根据系统时钟的当前时间。 注意,我们不对分布式系统中的时钟同步做任何假设。 Event time for a given event essentially never changes,but processing time changes constantly for each event as it flows through the pipeline and time marches ever forward. This is an important distinction when it comes to robustly analyzing events in the context of when they occurred. 给定事件的事件时间在本质上是不会改变,但是处理时间会随着事件在管道中的流动而不断变化,时间会不断前进。这是一个重要的区别,当它在事件发生的背景下进行清晰地分析时。 During processing, the realities of the systems in use (communication delays, scheduling algorithms, time spent processing, pipeline serialization, etc.) result in an inherent and dynamically changing amount of skew between the two domains. Global progress metrics, such as punctuations or watermarks, provide a good way to visualize this skew. For our purposes, we’ll consider something like MillWheel’swa-termark, which is a lower bound (often heuristically established) on event times that have been processed by the pipeline. As we’ve made very clear above, notions of completeness are generally incompatible with correctness, so we won’t rely on watermarks as such. They do, however, provide a useful notion of when the system thinks it likely that all data up to a given point in event time have been observed,and thus find application in not only visualizing skew, but in monitoring overall system health and progress, as well as making decisions around progress that do not require complete accuracy, such as basic garbage collection policies. 在处理过程中,市面上所有系统都会因为某些原因(通信延迟,调度算法,处理所花费的时间,流水线序列化等)导致两个时间域之间存在固有的,动态变化的偏移量。 诸如标点(punctuations)或水印(watermarks)之类的全局进度指标提供了一种可视化这种偏移量的好方法。为了我们的目的,我们将考虑使用MillWheel的水印,这是管道已处理的事件时间的下限(通常是启发式确定的)。 正如我们在上面非常清楚地指出的那样,完整性的概念通常与准确性是不兼容,因此我们不会像这样依赖水印。 但是,它们确实提供了一个有用的概念,即系统可在所有的数据中,观察那些给定的事件时间节点上的数据,因此不仅可以用于可视化其偏移量,而且可以用于监视整个系统的运行状况和进度, 以及围绕整体进度做出不要求准确性的决策,例如基本的垃圾回收策略。 In an ideal world, time domain skew would always bezero; we would always be processing all events immediately as they happen. Reality is not so favorable, however, and often what we end up with looks more like Figure 2. Starting around 12:00, the watermark starts to skew more away from real time as the pipeline lags, diving back close to real time around 12:02, then lagging behind again noticeably by the time 12:03 rolls around. This dynamic variance in skew is very common in distributed data processing systems, and will play a big role in defining what functionality is necessary for providing correct, repeatable results. 在理想的世界中,时间域的偏移量将始终为零,即我们将始终在事件发生时立即处理所有事件。但是,现实情况并非如此,通常,我们最终得到的结果看起来更像图2。从12:00开始,随着管线的滞后,水印开始偏离实时更多,然后回到接近实时12:02,然后到12:03时,又明显落后了。 时间偏移量的动态差异在分布式数据处理系统中非常常见,并且在定义提供准确,可重复的结果所需的功能方面将发挥重要作用。 2. DATAFLOW MODELIn this section, we will define the formal model for the system and explain why its semantics are general enough to subsume the standard batch, micro-batch, and streaming models, as well as the hybrid streaming and batch semantics of the Lambda Architecture. For code examples, we will usea simplified variant of the Dataflow Java SDK, which itself is an evolution of the FlumeJava API. 在本节中,我们将定义系统的正式模型,并解释为什么它的语义足够通用到可以包含标准批处理、微批处理和流模型,以及Lambda架构的混合流处理和批处理语义。对于代码示例,我们将使用Dataflow Java SDK的简化变体,它本身是FlumeJava API的演化。 2.1 Core PrimitivesTo begin with, let us consider primitives from the classic batch model. The Dataflow SDK has two core transforms that operate on the (key, value) pairs flowing through the system: ParDo for generic parallel processing. Each input element to be processed (which itself may be a finite collection) is provided to a user-defined function (called a DoFn in Dataflow), which can yield zero or more out-put elements per input. For example, consider an operation which expands all prefixes of the input key, duplicating the value across them: GroupByKey for key-grouping (key, value) pairs. 首先,让我们考虑经典批处理模型中的原语。Dataflow SDK有两个核心转换(transforms),它们对流经系统的(key、value)对进行操作: ParDo。ParDo用于通用并行处理。每个输入元素(它本身可能是一个有限的集合)均会被用户自定义的函数(在数据流中称为DoFn)所处理,该函数可以为每个输入生成零个或多个输出元素。例如,考虑这样一个操作,它展开输入key的所有前缀,在它们之间复制所有的value GroupByKey。GroupByKey用来基于 key键将数据进行聚合 The ParDo operation operates element-wise on each input element, and thus translates naturally to unbounded data.The GroupByKey operation, on the other hand, collects all data for a given key before sending them downstream for reduction. If the input source is unbounded, we have no way of knowing when it will end. The common solution to this problem is to window the data. ParDo操作是在每个输入元素上逐个操作元素,从而能够很自然地将其转换为无界数据。而在另一方面,GroupByKey操作收集给定key键的所有数据,然后将它们发送到下游进行缩减。如果输入源是无界的,我们无法知道它何时结束。这个问题的常见解决方案是将数据窗口化。 2.2 WindowingSystems which support grouping typically redefine their GroupByKey operation to essentially be GroupByKeyAndWindow. Our primary contribution here is support for un-aligned windows, for which there are two key insights. The first is that it is simpler to treat all windowing strategies as unaligned from the perspective of the model, and allow underlying implementations to apply optimizations relevant to the aligned cases where applicable. The second is that windowing can be broken apart into two related operations: Set<Window> AssignWindows(T datum), which assigns the element to zero or more windows. This is essentially the Bucket Operator from Li. Set<Window> MergeWindows(Set<Window> windows), which merges windows at grouping time. This allows data-driven windows to be constructed over time as data arrive and are grouped together. 支持分组的系统通常将GroupByKey操作重新定义为GroupByKeyAndWindow。我们在这里的主要贡献是支持未对齐的窗口,对此有两个关键的见解。首先,从模型的角度来看,将所有的窗口策略视为未对齐的比较简单,并允许底层实现对对齐的情况应用相关的优化。第二,窗口可以分解为两个相关的操作: Set<Window> AssignWindows(T datum),它将元素赋值给零个或多个窗口。 Set<Window> MergeWindows(Set<Window> windows),它允许按时间分组时合并窗口。这允许在数据到达并分组在一起时,随时间构建数据驱动窗口。 For any given windowing strategy, the two operations are intimately related; sliding window assignment requires slid-ing window merging, sessions window assignment requires sessions window merging, etc. 对于任何给定的窗口策略,这两个操作都是密切相关的,如滑动窗口分配需要滑动窗口合并,会话窗口分配需要会话窗口合并,等等。 Note that, to support event-time windowing natively, instead of passing (key, value) pairs through the system, we now pass (key, value, eventtime, window) 4-tuples. Elements are provided to the system with event-time timestamps (which may also be modified at any point in the pipeline), and are initially assigned to a default global window, covering all of event time, providing semantics that match the defaults in the standard batch model. 注意,为了在本地支持事件时间的窗口,我们现在传递(key, value, eventtime, window) 4元组,而不是传递(key, value)到系统中。元素以基于事件时间的时间戳(也可以在管道中的任何位置修改)提供给系统,并在最初时分配给一个默认的全局窗口,覆盖所有事件时间,提供与标准批处理模型中的默认值匹配的语义。 2.2.1 Window AssignmentFrom the model’s perspective, window assignment creates a new copy of the element in each of the windows to which it has been assigned. For example, consider windowing a dataset by sliding windows of two-minute width and one-minute period, as shown in Figure 3 (for brevity, timestamps are given in HH:MM format). 从模型的的角度来看,窗口赋值是在每个已赋值给它的窗口中创建元素的新副本。例如,考虑使用两分钟时间长度和以一分钟为时间周期的滑动窗口来窗口化一个数据集,如图3所示。 In this case, each of the two (key, value) pairs is duplicated to exist in both of the windows that overlapped the element’s timestamp. Since windows are associated directly with the elements to which they belong, this means window assignment can happen any where in the pipeline before grouping is applied. This is important, as the grouping operation may be buried somewhere downstream inside a composite transformation (e.g.Sum.integersPerKey()). 在本例中,这两个(key, value)对中的每一个都被复制到重叠元素时间戳的两个窗口中。由于窗口直接与它们所属的元素相关联,这意味着在应用分组之前,可以在管道中的任何位置进行窗口分配。这很重要,因为分组操作可能隐藏在复合转换(例如Sum.integersPerKey())下游中的某个地方。 2.2.2 Window MergingWindow merging occurs as part of the GroupByKeyAndWindow operation, and is best explained in the context of an example. We will use session windowing since it is our motivating use case. Figure 4 shows four example data, three for k1 and one for k2, as they are windowed by session, with a 30-minute session timeout. All are initially placed in a default global window by the system. The sessions implementation of AssignWindows puts each element into a single window that extends 30 minutes beyond its own timestamp; this window denotes the range of time into which later events can fall if they are to be considered part of the same session. We then begin the GroupByKeyAndWindow operation, which is really a five-part composite operation: DropTimestamps - Drops element timestamps, as only the window is relevant from here on out. GroupByKey - Groups (value, window) tuples by key. MergeWindows - Merges the set of currently buffered windows for a key. The actual merge logic is defined by the windowing strategy. In this case, the windows for v1 and v4 overlap, so the sessions windowing strategy merges them into a single new, larger session, as indicated in bold. GroupAlsoByWindow - For each key, groups values by window. After merging in the prior step,v1 and v4 are now in identical windows, and thus are grouped together at this step. ExpandToElements - Expands per-key, per-window groups of values into (key, value, eventtime, window)tuples, with new per-window timestamps. In this example, we set the timestamp to the end of the window, but any timestamp greater than or equal to the timestamp of the earliest event in the window is valid with respect to watermark correctness. 窗口合并是GroupByKeyAndWindow操作的一部分,这将会在后面的示例中进行解释。我们因其常见性,决定在本例中使用会话窗口。图4显示了四个示例数据,其中三个用于k1,一个用于k2,因为它们是按会话窗口显示的,并且有30分钟的会话超时。它们最初都被系统放置在一个默认的全局窗口中。AssignWindows的会话实现将每个元素放入一个单独的窗口中,这个窗口比它自身的时间戳延长了30分钟。此窗口表示如果迟到的事件被认为是同一会话的一部分的话,它们可能落入的时间范围。然后我们开始GroupByKeyAndWindow操作,这实际上是一个由五部分组成的复合操作: DropTimestamps -丢弃元素时间戳,因为从这里开始,只有窗口相关的部分。 GroupByKey -按key分组成(value、window)元组。 MergeWindows -合并key的当前缓冲窗口集。实际的合并逻辑是由窗口策略定义的。在这种情况下,v1和v4对应的窗口重叠,所以会话窗口将它们合并成一个新的、更大的会话。 GroupAlsoByWindow -对于每个key,通过窗口聚合所有的value。在前一步合并之后,v1和v4现在位于相同的窗口中,因此在这一步将它们组合在一起。 ExpandToElements -将每个key、每个窗口的value组扩展为(key、value、eventtime、window)元组,并使用新的窗口时间戳。在本例中,我们将时间戳设置在窗口的末端,但任何大于或等于窗口中最早事件的时间戳的事件时间戳在水印准确性方面都被认为是有效的。 2.2.3 APIAs a brief example of the use of windowing in practice,consider the following Cloud Dataflow SDK code to calculate keyed integer sums: 12PCollection<KV<String, Integer>> input = IO.read(...);PCollection<KV<String, Integer>> output = input.apply(Sum.integersPerKey()); 作为实际使用窗口的简要示例,请考虑以下Cloud Dataflow SDK代码以计算key 对应的整数和: To do the same thing, but windowed into sessions with a 30-minute timeout as in Figure 4, one would add a single Window.into call before initiating the summation: 1234PCollection<KV<String, Integer>> input = IO.read(...);PCollection<KV<String, Integer>> output = input .apply(Window.into(Sessions.withGapDuration(Duration.standardMinutes(30)))) .apply(Sum.integersPerKey()); 要执行相同的操作,但是要像图4那样以30分钟的超时时间窗口化到会话中,则需要在启动求和之前添加单个Window.into调用 2.3 Triggers & Incremental ProcessingThe ability to build un-aligned, event-time windows is an improvement, but now we have two more shortcomings to address: We need some way of providing support for tuple- and processing-time-based windows, otherwise we have regressed our windowing semantics relative to other systems in existence. We need some way of knowing when to emit the results for a window. Since the data are unordered with respect to event time, we require some other signal to tell us when the window is done. 拥有构建未对齐(un-aligned)的事件时间(event-time)窗口的能力是一种改进,但现在我们还有两个缺点需要解决: 我们需要某种方式来提供对基于元组和基于处理时间的窗口的支持,否则我们已经倒退了与现有的其他系统相关的窗口语义了。 我们需要一些方法知道什么时候发出窗口的结果。由于数据对于事件时间是无序的,我们需要一些其他信号来告诉我们什么时候窗口完成数据处理了。 The problem of tuple- and processing-time-based windows we will address in Section 2.4, once we have built up a solution to the window completeness problem. As to window completeness, an initial inclination for solving it might be to use some sort of global event-time progress metric, such as watermarks. However, watermarks themselves have two major shortcomings with respect to correctness: They are sometimes too fast, meaning there may be late data that arrives behind the watermark. For many distributed data sources, it is intractable to derive a completely perfect event time watermark, and thus impossible to rely on it solely if we want 100% correctness in our output data. They are sometimes too slow. Because they are a global progress metric, the watermark can be heldback for the entire pipeline by a single slow datum. And even for healthy pipelines with little variability in event-time skew, the baseline level of skew may still be multiple minutes or more, depending upon the input source. As a result, using watermarks as the sole signal for emitting window results is likely to yield higher latency of overall results than, for example, a comparable Lambda Architecture pipeline. 一旦我们建立了一个窗口完整性问题的解决方案,我们将在章节2.4中讨论基于元组和处理时间的窗口的问题。至于窗口完整性,解决它的最初倾向可能是使用某种全局的事件时间进度度量工具,例如水印(watermark)。但是,就准确性而言,水印(watermark)本身有两大缺点: 他们有时太快了,这意味着可能有迟来的数据可能会到达在水印后面。对于许多分布式数据源而言,它们很难获得十分完美的事件时间水印,因此如果我们想要输出数据100%正确,就不可能完全依赖于它。 他们有时太慢了。因为它们是一个全局进度度量,所以水印或许会被一个缓慢的数据来阻止整个管道。即使是在正常的管道中,即使在事件时间偏移量变化不大,偏移量的基线水平仍然可能是几分钟甚至更多,这取决于输入源。因此,使用水印作为唯一的信号来发送窗口结果可能会产生比类似的Lambda架构管道更高的延迟。 For these reasons, we postulate that watermarks alone are insufficient. A useful insight in addressing the completeness problem is that the Lambda Architecture effectively sidesteps the issue: it does not solve the completeness problem by somehow providing correct answers faster; it simply provides the best low-latency estimate of a result that the streaming pipeline can provide, with the promise of eventual consistency and correctness once the batch pipeline runs. If we want to do the same thing from within a single pipeline (regardless of execution engine), then we will need a way to provide multiple answers (or panes) for any given window.We call this feature triggers, since they allow the specification of when to trigger the output results for a given window. 由于这些原因,我们假定仅有水印(watermark)是不够的。解决窗口完整性问题的一个有用的方式(也是Lambda架构提出的一种有效回避该问题的方式):它并没有更快地通过某种方式提供正确的解决方法来处理完整性问题,而只是提供了流管道所能提供的结果的最佳低延迟估计值,并承诺一旦批处理管道运行起来,将在最终保持一致性和正确性。如果我们希望在单个管道中执行相同的操作(与执行引擎无关),那么我们将需要为任何给定窗口提供多个解决方法(或窗格)的方法。我们将此功能称为触发器(triggers),因为它们允许指定何时触发给定窗口的输出结果。 In a nutshell, triggers are a mechanism for stimulating the production of GroupByKeyAndWindow results in response to internal or external signals. They are complementary to the windowing model, in that they each affect system behaviour along a different axis of time: Windowing determines where in event time data are grouped together for processing. Triggering determines when in processing time the results of groupings are emitted as panes. 简而言之,触发器是一种机制,用于触发GroupByKeyAndWindow结果的生成,以响应内部或外部信号。它们是窗口模型的补充,因为它们都影响系统在不同时间轴上的行为: 窗口确定事件时间数据在哪里分组,并进行处理。 触发器决定在处理时间内分组的结果在什么时候以窗格的形式发出。 Our systems provide predefined trigger implementations for triggering at completion estimates (e.g. watermarks, including percentile watermarks, which provide useful semantics for dealing with stragglers in both batch and streaming execution engines when you care more about processing a minimum percentage of the input data quickly than processing every last piece of it), at points in processing time, and in response to data arriving (counts, bytes, data punctuations, pattern matching, etc.). We also support composing triggers into logical combinations (and, or, etc.), loops, sequences,and other such constructions. In addition, users may define their own triggers utilizing both the underlying primitives of the execution runtime (e.g. watermark timers, processing-time timers, data arrival, composition support) and any other relevant external signals (data injection requests, external progress metrics, RPC completion callbacks, etc.).We will look more closely at examples in Section 2.4. 我们的系统提供了用于在完成估算时触发的预定义触发器实现(例如,水印,包括百分位数水印,当您更关心快速处理最小百分比的输入数据而不是处理数据时,它们提供了有用的语义来处理批处理和流执行引擎中的散乱消息数据的最后一部分),当位于在处理时间点或者需要对数据到达(计数,字节,数据标点,模式匹配等)的响应时。 我们还支持将触发器组合成逻辑组合(and,or等),循环,序列和其他类似的构造。 另外,用户可以利用执行运行时的基本原语(例如水印计时器,处理时间计时器,数据到达,合成支持)和任何其他相关的外部信号(数据注入请求,外部进度指标,RPC回调等)来定义自己的触发器。。我们将在章节2.4中更详细地研究示例。 In addition to controlling when results are emitted, the triggers system provides a way to control how multiple panes for the same window relate to each other, via three different refinement modes: Discarding: Upon triggering, window contents are discarded, and later results bear no relation to previous results. This mode is useful in cases where the downstream consumer of the data (either internal or external to the pipeline) expects the values from various trigger fires to be independent (e.g. when injecting into a system that generates a sum of the values injected). It is also the most efficient in terms of amount of data buffered, though for associative and commutative operations which can be modeled as a Dataflow Combiner, the efficiency delta will often be minimal. For our video sessions use case, this is not sufficient, since it is impractical to require downstream consumers of our data to stitch together partial sessions. Accumulating: Upon triggering, window contents are left intact in persistent state, and later results become a refinement of previous results. This is useful when the downstream consumer expects to overwrite old values with new ones when receiving multiple results for the same window, and is effectively the mode used in Lambda Architecture systems, where the streaming pipeline produces low-latency results, which are then overwritten in the future by the results from the batch pipeline. For video sessions, this might be sufficient if we are simply calculating sessions and then immediately writing them to some output source that supports updates (e.g. a database or key/value store). Accumulating & Retracting: Upon triggering, inaddition to the Accumulating semantics, a copy of the emitted value is also stored in persistent state. When the window triggers again in the future, a retraction for the previous value will be emitted first, followed by the new value as a normal datum. Retractions are necessary in pipelines with multiple serial GroupByKeyAndWindow operations, since the multiple results generated by a single window over subsequent trigger fires may end up on separate keys when grouped downstream. In that case, the second grouping operation will generate incorrect results for those keys unless it is informed via a retraction that the effects of the original output should be reversed. Dataflow Combiner operations that are also reversible can support retractions efficiently via an uncombine method. For video sessions,this mode is the ideal. If we are performing aggregations downstream from session creation that depend on properties of the sessions themselves, for example detecting unpopular ads (such as those which are viewed for less than five seconds in a majority of sessions), initial results may be invalidated as inputs evolve overtime, e.g. as a significant number of offline mobile viewers come back online and upload session data. Retractions provide a way for us to adapt to these types of changes in complex pipelines with multiple serial grouping stages. 除了控制何时发出结果,触发器系统还提供了一种方法,可通过三种不同的优化模式来控制同一窗口的多个窗格之间的相互关系: 丢弃(Discarding):触发器触发时,窗口内容将会被丢弃,并且以后的结果将与以前的结果无关。 倘若数据的下游使用者(管道内部或外部)期望来自各种触发器触发的值是独立的情况下(例如,注入到生成注入值之和的系统中),此模式很有用。 就缓冲的数据量而言,它也是最有效的,尽管对于可以为数据流组合器建模的关联和交换操作,增量效率通常会很小。 对于我们的视频会话用例,这是不够的,因为要求数据的下游使用者将部分会话缝合在一起是不切实际的。 累加(Accumulating):触发器触发时,窗口内容将保持不变,以后的结果是以以前结果为基础,进行数据增量操作。这是十分有用的方法,当下游使用者希望在同一窗口中接收到多个结果时希望用新值覆盖旧值,并且系统能够有效地作用于Lambda架构系统。而在这其中,流管道产生低延迟的结果,这些结果随后将被来自批处理管道的结果覆盖。对于视频会话,如果我们只是简单地计算会话,然后立即将其写入支持更新的某个输出源中(例如数据库或key/value存储),这可能就足够了。 累积和回退(Accumulating & Retracting):触发器触发时,除了累积语义外,输出值的副本也以持久状态存储。 当窗口在未来再次触发时,将首先会对先前值的回退,然后是输出作为正常基准的新值。 在具有多个串行GroupByKeyAndWindow操作的管道中,回退操作是必要的,因为在下游分组时,单个窗口在后续触发器触发上生成的多个结果可能会在单独的键上结束。 在那种情况下,第二次分组操作将为那些键生成不正确的结果,除非通过回退通知其原始输出进行回退。 数据流Combiner操作也可以通过取消组合方法有效地支持回退。 对于视频会话,此模式是理想的。 如果我们在会话创建的下游执行依赖于会话本身属性的聚合,例如检测不受欢迎的广告(例如在大多数会话中观看时间少于五秒钟的广告),则随着输入的发展,初始结果可能会是无效的,例如因为大量的离线移动设备恢复了在线状态并上传了会话数据。 回退为我们提供了一种方法,使我们可以通过多个串行分组阶段来适应复杂管道中的这些类型的更改。 2.4 Examples 举例部分比较简单,就是结合上面提到的所有概念,进行综合举例,有空再挖坑回填。 3. IMPLEMENTATION & DESING 实现部分是作者自身在 Google 内部的实践与经验,对于流系统开发者而言能够了解到他们在实现时碰到的坑。因为是了解背后原理就不进行详细翻译了。 4. CONCLUSIONSThe future of data processing is unbounded data. Though bounded data will always have an important and useful place, it is semantically subsumed by its unbounded counterpart. Furthermore, the proliferation of unbounded datasets across modern business is staggering. At the same time, consumers of processed data grow savvier by the day, demanding powerful constructs like event-time ordering and unaligned windows. The models and systems that exist today serve as an excellent foundation on which to build the data processing tools of tomorrow, but we firmly believe that a shift in overall mindset is necessary to enable those tools to comprehensively address the needs of consumers of unbounded data. 无边界(无限)的数据是数据处理的未来。 尽管有边界(有限)的数据将始终具有重要和有用的位置,但从语义上讲,它由无边界的对应部分所包含。 此外,无限数据集在整个跨现代业务中的扩散令人震惊。 同时,处理数据的消费者一天比一天更加精明,因此需要强大的架构,例如事件时间顺序和未对齐的窗口等。 当今存在的模型和系统为构建未来的数据处理工具奠定了良好的基础,但是我们坚信,必须转变整体的观念,以使这些工具能够全面满足数据消费者的需求。 Based on our many years of experience with real-world,massive-scale, unbounded data processing within Google, we believe the model presented here is a good step in that direction. It supports the un-aligned, event-time-ordered windows modern data consumers require. It provides flexible triggering and integrated accumulation and retraction, refocusing the approach from one of finding completeness in data to one of adapting to the ever present changes manifest in real-world datasets. It abstracts away the distinction of batch vs.micro-batch vs. streaming, allowing pipeline builders a more fluid choice between them, while shielding them from the system-specific constructs that inevitably creep into models targeted at a single underlying system. Its overall flexibility allows pipeline builders to appropriately balance the dimensions of correctness, latency, and cost to fit their use case, which is critical given the diversity of needs in existence. And lastly, it clarifies pipeline implementations by separating the notions of what results are being computed, where in event time they are being computed, when in processing time they are materialized, and how earlier results relate to later refinements. We hope others will find this model useful as we all continue to push forward the state of the art in this fascinating, remarkably complex field. 根据我们多年在Google中真实,大规模,无边界数据处理的经验,我们相信此处介绍的模型是朝这个方向迈出的重要一步。 它支持消费者需要的未对齐,事件时间顺序的窗口现代数据。 它提供了灵活的触发方式以及集成的累积和回退功能,将寻找数据完整性的方法重新定位为适应现实数据集中不断变化的方法。 它抽象化了批处理、微型批处理和流式处理三者的区别,使管道构建器可以在它们之间进行更多的选择,同时使它们免受系统特定的构造的影响,这些构造不可避免地会渗入针对单个基础系统的模型。 它的整体灵活性使流水线构建者可以适当地平衡正确性,延迟和成本这三个维度,以适应其用例,考虑到现有需求的多样性,这一点至关重要。最后,它通过分离以下概念来澄清流水线实现:正在计算哪些结果,其中计算它们的事件时间,在处理时间何时实现它们,以及较早的结果与以后的改进有何关系。我们希望其他人会发现此模型有用,因为我们所有人都将继续在这个引人入胜,非常复杂的领域中发展最先进的技术。","categories":[],"tags":[{"name":"flink","slug":"flink","permalink":"http://apparition957.github.io/tags/flink/"}]},{"title":"八日漫游大西环线","slug":"八日漫游大西环线","date":"2018-12-06T11:27:01.000Z","updated":"2018-12-06T12:41:49.000Z","comments":true,"path":"2018/12/06/八日漫游大西环线/","link":"","permalink":"http://apparition957.github.io/2018/12/06/八日漫游大西环线/","excerpt":"","text":"缘起这次旅行可以认为是一场说走就走的旅行,缘起于朋友的一次不经意的漫谈,到最终构思出大致的计划不过两日,我俩就踏上了旅程,途中边走边规划,要去哪吃,要去哪玩。 这篇记录主要以风景照为主,美食的话没有拍,味道全留在肚子里了。 第一日:成都-兰州出发前一天,看到凌晨的机票十分便宜便立马下手,本以为捡到了大便宜,但是成都突如其来的大雾天气导致我们的航班延误了整整5个小时。在等待期间,别的航空公司的飞机由于机型缘故可以在较恶劣的条件下起飞,所以我们只能在同一个登机口眼巴巴地看着他们欢快的登机。 切勿贪小便宜乘坐廉价航空或者机型较小的飞机! 到达兰州的时候已经中午12点半了,我们拿着行李就跑去乘坐机场大巴赶去下榻酒店。中川机场到市中心的距离长达68公里之远,所以一般都不会考虑打车去市中心,而是选择两条路线:到隔壁的中川机场高铁站乘坐高铁或者乘坐机场大巴,两条路线的价格和花费时间都相差不多。 匆忙放完行李后,我们早已肚子饿的不得了,便到楼下的兰州拉面点上了两碗心心念念的牛肉面。 在兰州安排的第一个必游的景点是甘肃省博物馆,主要目的还是奔着镇馆之宝——马踏飞燕走的。但是不得不说,逛完博物馆后整个人都虚脱了,只能回酒店暂作休息。迷迷糊糊睡了会儿,便起身去看看兰州夜景。 第二日:兰州-西宁由于今日的我们没有安排过多的行程,便睡了个回笼觉,睡醒便已经10点钟了。早上我们只安排了一个景点——白塔山公园,虽说是公园,其实就有点像深圳的莲花山公园,还是有点山路的,况且我们还是拿着全副行李,但是想到能够俯瞰兰州全貌,便咬咬牙爬了上去。 从公园下来已经接近中午,吃了最后一顿美味的牛肉面后又马不停蹄的赶往兰州西站,乘坐高铁前往西宁。在西宁游玩时,给我印象最深的便是较近晚上时前去的东关清真大寺,印象深的并不是里面的建筑,而是里面的穆斯林老人,他们见到你时,会先递给你一张小纸片,上面记录着伊斯兰教中最重要的几句话,然后会十分热情地跟你述说了伊斯兰教的由来、信仰伊斯兰教与其他宗教的不同等等。从我的直觉上看,倘若我们不刻意打断他们(虽然很不礼貌),他们能讲上一整天。 第三日:西宁-塔尔寺-青海湖尽管昨日有过短暂的休息,但是还是忍不住今早早起的哈欠。我们与昨日联系好的小马哥(本次旅行的司机)约好九点半在酒店楼下集合,这一次同行的包括司机在内总共有七人(四男三女),出乎意料的是主要都来自广东。 后面的文字记录,我就不过多描述旅途中的辛酸了,主要还是以风景为重点进行记录。 路途风景 塔尔寺 青海湖 第四日:茶卡盐湖-翡翠湖-柴达木盆地清晨的茶卡镇 茶卡盐湖 翡翠湖 第五日:雅丹魔鬼城-敦煌雅丹魔鬼城 第六日:敦煌-莫高窟-鸣沙山月牙泉莫高窟由于景区规定了洞窟内不能摄影,所以就拍了一张外景图。 鸣沙山月牙泉 第七日:敦煌-七彩丹霞-张掖七彩丹霞 张掖在张掖这块地方,不得不提的就是羊肉了,真的可以说得上又便宜又好吃,60块一斤的羊肉肥而不腻,分量十足,加上大蒜以及泡菜,简单美味。 第八日:张掖-兰州-成都为什么第八日的行程看上去那么复杂?主要考虑到从张掖开车返回西宁的话,由于冬天的缘故,预计行程中的油菜花田是没有的,外加雪山封路会导致时间加长,所以我们就打算以高铁的行程直接返回兰州,再从兰州乘飞机返回成都,这样下来所花费的金钱只和西宁到达成都相差无几,但节省了不少时间。 结尾最后就附上我们这次两人这八天下来所预估的花费清单,淡季出行+学生半价(甚至免票)是一个很棒的结合!","categories":[],"tags":[]},{"title":"交友?","slug":"交友?","date":"2018-11-18T16:27:35.000Z","updated":"2018-11-18T16:30:54.000Z","comments":true,"path":"2018/11/19/交友?/","link":"","permalink":"http://apparition957.github.io/2018/11/19/交友?/","excerpt":"","text":"这篇文章纯属自己的有感而发写的。 这段时间也不知怎么回事,做起事来都充满了无力感 朋友的冷漠 敏感 怀疑自己","categories":[],"tags":[]},{"title":"出门走走-贵州岑巩县","slug":"出门走走-贵州岑巩县","date":"2018-11-12T08:16:05.000Z","updated":"2018-11-12T09:39:51.000Z","comments":true,"path":"2018/11/12/出门走走-贵州岑巩县/","link":"","permalink":"http://apparition957.github.io/2018/11/12/出门走走-贵州岑巩县/","excerpt":"","text":"上周参加了学校组织的扶贫活动,地点位于贵州岑巩县。并不是因为偷懒没写技术博客才去的呀:) 做了什么这次扶贫活动的主要是帮助各乡镇进行贫困户的信息录入工作,减少一些他们的工作量,以我前去的平庄镇而言,乡镇的人口基数相较于其他乡镇而言还算比较大的,外加上镇上的干部数量比较少,所以信息录入工作基本就是由一人负责,工作量大而繁琐。 额外说下的是,因为需要在2021年要达到“两个一百年”中第一个一百年的目标,不知从几年前开始,我所在的镇上所有干部就基本上过着加班的日子,隔三差五就有一个会议要开。值班的日子是以两周为间隔,也就是说至少需要值班两周才能够回家休息这样的一个状态。在平常的时候还需要经常下乡对每家每户进行调研统计,方便后期对各贫困户实施不同的扶贫措施。 真的感谢你们的辛苦工作! 一些感想跟同学一起进行贫困户信息录入的这段时间中,我接触了不少致贫原因各不相同的家庭,总结下自己的一些感触吧。 补助在个人收入中,可分为工资性收入、生产性经营收入和各项补助。若家庭中有患有重病或者残疾的人的话,前两项收入往往是较少的,更主要是通过补助的方式维持生活,而各项补助总和的金额却是较少的(1k-10k 浮动)。 虽然较偏远地区的生活水平较低,但我真的不清楚这些补助金额是否能够维持这些特殊人群的正常生活。 教育不知是否受限于九年义务教育的原因,有不少的人选择了初中毕业后就直接去外地工作,或独闯天下,或与父母一起,家庭总体收入较低且不具有稳定性(即数据相较于去年而言变化较大)。至于为什么选择直接工作也有各种各样的理由,有的人是因为家庭原因,有的人却是因为不想读了(原话)。与上述情况不同的,有些家庭的家长虽然身处外地打工,却依然支持自己的子女上高中上大学。在一些已有大学生的家庭中,我能够感受他们家庭自身的收入在一个中等偏上(相较于全村而言)的水平。 由于自己只能够通过纸张上的对比表,从家庭各成员学历、工作地、收入来分析他们的情况,所以我没法真真正正了解到他们每一个人的想法与感受。但是有一点我能感受到的是,接受了高等教育的人,能够为自己的家庭贡献出更多的力量。 美丽岑巩身处于城市过久,来乡村的一周时间中,觉得乡村真是一个很不错的地方,虽然在生活设施方面远不及城市,但无论是自然风景,还是饮食,乡村还是有其独特之处。(再次特别感谢每日饭堂的好饭菜!)","categories":[],"tags":[{"name":"旅行","slug":"旅行","permalink":"http://apparition957.github.io/tags/旅行/"}]},{"title":"在小米实习的180天","slug":"在小米实习的180天","date":"2018-07-20T14:46:20.000Z","updated":"2018-07-20T14:47:16.000Z","comments":true,"path":"2018/07/20/在小米实习的180天/","link":"","permalink":"http://apparition957.github.io/2018/07/20/在小米实习的180天/","excerpt":"","text":"感恩在小米的这段实习经历,感谢小米身边的每个人。","categories":[],"tags":[]},{"title":"TCP 协议中 Keep-Alive 特性","slug":"TCP 协议中 Keep-Alive 特性","date":"2018-05-27T10:47:59.000Z","updated":"2018-05-27T10:48:37.000Z","comments":true,"path":"2018/05/27/TCP 协议中 Keep-Alive 特性/","link":"","permalink":"http://apparition957.github.io/2018/05/27/TCP 协议中 Keep-Alive 特性/","excerpt":"","text":"在腾讯面试的时候问过我基于这个特性的问题,可惜我没答出来:(,以下为原题部分。 在 TCP 连接中,我们都知道客户端要与服务器端断开连接时需要经过”四次分手”。但如果客户端在未知因素的情况下宕机了,那服务器端会在什么时候认为客户端已掉线,从而服务器端”主动”断开连接呢? 前言抛弃上面的描述,我们知道在 TCP 协议中,如果客户端不主动断开与服务器端的连接时,服务器端便会一直持有对这个客户端的连接。如果不引入某些有效机制的话,这将会大大地消耗服务器端的资源。 keep-alive 机制确保了服务器端能够在客户端无消息发送的一段时间后,自主地断开与客户端的连接。 RFC 中 Keep-Alive 机制keep-alive 是 TCP 协议的可选特性(optional feature)。如果操作系统实现了这一特性,就必须保证应用程序能够为每个 TCP 连接打开或关闭该特性,且这一特性必须是默认关闭的。 keep-alive 的心跳包只能够在从最后一次接收到 ACK 包的时间起,经过一个固定的时间间隔后才能发送。这个时间间隔必须能够被配置,且默认值不能够低于2小时。 keep-alive 应当在服务器端启用,而客户端不做任何修改。倘若客户端开启了这一特性,当客户端异常崩溃或者出现连接故障的话,将会导致该连接无限期挂起和消耗不必要的资源。 在 TCP 规范中并不包含 keep-alive 机制的主要原因有三:(1)在短暂的网络故障期间,可能会导致一个良好正常的连接(perfectly good connections)断开。(2)消耗不必要的带宽资源(”if no one is using the connection, who cares if it is still good?”)。(3)在以数据包计费的互联网网络中(额外)花费金钱。 Linux 内核下 Keep-Alive 的重要参数在 Linux 内核中,keep-alive 机制涉及到三个重要的参数: tcp_keepalive_time。该参数是指最后一次数据包(不包含数据的 ACK 包)发送的时间到第一次发送的心跳包之间的时间间隔。默认值为7200s(2小时)。 tcp_keepalive_intvl。该参数是指连续两个心跳包之间的时间间隔。默认值为75s。 tcp_keepalive_probes。该参数是指在服务器端认为该连接失效(dead)并通知用户前,未确认的探测器(unacknowledged probes)发送的数量。默认值为9(次)。 Linux 的文档还特别声明了即使 keep-alive 这一机制在内核中被配置了,这一行为也不是 Linux 的默认行为。 面试题的一种合适的解释了解了这一特性背后的含义时,我们可以对面试官说到。在 Linux 环境下,如果该连接中 keep-alive 机制已开启时,服务器端会在 7200s + 75s * 9time 后断开与客户端的连接(即在底层清除失效的文件描述符)。 与 HTTP 中 Keep-Alive 的对比HTTP 协议中的 keep-alive 机制是为了通信双方的连接复用,避免消耗太多资源。而 TCP 协议中 keep-alive 机制是为了检验通信双方的是否活着(alive),保证通信能够正常进行。 参考资料: https://tools.ietf.org/html/rfc1122#page-101 http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html http://www.importnew.com/27624.html http://www.cnblogs.com/liuyong/archive/2011/07/01/2095487.html","categories":[],"tags":[]},{"title":"Scala - NonLocalReturnControl","slug":"Scala - NonLocalReturnControl","date":"2018-05-22T08:36:18.000Z","updated":"2018-05-22T08:38:50.000Z","comments":true,"path":"2018/05/22/Scala - NonLocalReturnControl/","link":"","permalink":"http://apparition957.github.io/2018/05/22/Scala - NonLocalReturnControl/","excerpt":"","text":"状态说明今天跑 Spark 作业的时候,刚进入 RUNNING 状态没多久就直接抛出了下面这种异常。 123456789101112131415User class threw exception: org.apache.spark.SparkException: Task not serializable at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298) at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288) at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108) at org.apache.spark.SparkContext.clean(SparkContext.scala:2100).....Caused by: java.io.NotSerializableException: java.lang.ObjectSerialization stack: - object not serializable (class: java.lang.Object, value: java.lang.Object@65c9e3ee) - field (class: com.xiaomi.search.websearch.hbase.SegTitlePick$$anonfun$1, name: nonLocalReturnKey1$1, type: class java.lang.Object) - object (class com.xiaomi.search.websearch.hbase.SegTitlePick$$anonfun$1, <function1>) at org.apache.spark.serializer.SerializationDebugger$.improveException(SerializationDebugger.scala:40) at org.apache.spark.serializer.JavaSerializationStream.writeObject(JavaSerializer.scala:46) at org.apache.spark.serializer.JavaSerializerInstance.serialize(JavaSerializer.scala:100) at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:295) 上网一查发现时某个匿名函数里面使用了 return 导致的。 报错理由是什么呢源代码就不贴出来了,我们以一个简单的例子来说明这个问题吧。 1234567891011object Test { def main(args: Array[String]): Unit = { val datas = List(1, 2, 3, 4) datas.foreach(t => { if (t % 2 == 0) return // 运行符合条件时便立刻返回 }) // 本例的目标想在遍历完 datas 后便输出该语句,但在实际情况下,return 语句会直接返回并退出当前函数(即 main 函数),所以以下语句并不会输出结果 println(\"finished!\") }} 让我们查看编译后这段遍历的代码有什么不一样的地方吧? 1234567891011121314151617181920// scalac -Xprint:explicitouter Test.scaladef main(args: Array[String]): Unit = { <synthetic> val nonLocalReturnKey1: Object = new Object(); try { val datas: List[Int] = scala.collection.immutable.List.apply[Int] (scala.Predef.wrapIntArray(Array[Int]{1, 2, 3, 4})); datas.foreach[Unit]({ final <artifact> def $anonfun$main(t: Int): Unit = if (t.%(2).==(0)) throw new scala.runtime.NonLocalReturnControl$mcV$sp(nonLocalReturnKey1, ()) else (); ((t: Int) => $anonfun$main(t)) }); scala.Predef.println(\"finished!\") } catch { case (ex @ (_: scala.runtime.NonLocalReturnControl[Unit @unchecked])) => if (ex.key().eq(nonLocalReturnKey1)) ex.value$mcV$sp() else throw ex } } 编译后我们可以看到原先匿名函数中的 return 语句被替换成抛出一个NonLocalReturnControl运行时异常,而try-catch环绕着整个 main 函数内部的代码块来尝试捕获这个异常。 而观察NonLocalReturnControl异常,我们发现这个异常是无法被序列化的,这就解释了之前的作业抛出异常的意思了。 为什么 return 语句要这么设计呢为什么 Scala 要这么做呢?这里有几篇不错的文章来说明,我就偷懒不去翻译了(建议从上往下看) 介绍什么是 non-local return - https://www.zhihu.com/question/22240354/answer/64673094 前半段介绍 return 语句该什么时候出现,后半段推测出这么做的两个原因 - https://stackoverflow.com/questions/17754976/scala-return-statements-in-anonymous-functions 讨论在 Scala 中 function 和 method 两者概念上的区别 - https://link.jianshu.com/?t=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F2529184%2Fdifference-between-method-and-function-in-scala 但其实翻阅了网上的资料,并没有真正地说明为什么这么设计。结合上面的几篇文章,我个人认为在 Scala 这一门函数式编程语言里,其更加讲究的是程序执行的结果,而并非执行过程。return 语句影响程序的顺序执行,从而可能会使代码变得复杂,也可能会发生若干次程序执行的结果不一致的情况,那么这将在很大程度上影响了我们对于代码的理解与认识。这也是 Scala 为什么不倡导我们使用 return。","categories":[],"tags":[]},{"title":"Scala - Iterator vs Stream vs View","slug":"Scala - Iterator vs Stream vs View","date":"2018-05-19T09:58:10.000Z","updated":"2018-05-19T09:58:33.000Z","comments":true,"path":"2018/05/19/Scala - Iterator vs Stream vs View/","link":"","permalink":"http://apparition957.github.io/2018/05/19/Scala - Iterator vs Stream vs View/","excerpt":"","text":"问题来源https://stackoverflow.com/questions/5159000/stream-vs-views-vs-iterators 优秀回答 该篇回答被收录到 Scala 文档中的 F&Q 部分。我尝试跟着这篇回答并对照源码部分去翻译,翻译不好多多谅解。 First, they are all non-strict. That has a particular mathematical meaning related to functions, but, basically, means they are computed on-demand instead of in advance. 首先,它们都是非严格(即惰性的)的。每个函数都有其特定的数学含义,但是基本上,其数学含义通常都意味着它们是按需计算而非提前计算。 Stream is a lazy list indeed. In fact, in Scala, a Stream is a List whose tail is a lazy val. Once computed, a value stays computed and is reused. Or, as you say, the values are cached. Stream确实是一个惰性列表。事实上,在 Scala 中,Stream是tail变量为惰性值的列表。一旦开始计算,Stream中的值便保持计算后的状态并被能够被重复使用。或者按照你的说法是,Stream中的值能够被缓存下来。 一篇比较不错的、科普Stream的文章:http://cuipengfei.me/blog/2014/10/23/scala-stream-application-scenario-and-how-its-implemented/ An Iterator can only be used once because it is a traversal pointer into a collection, and not a collection in itself. What makes it special in Scala is the fact that you can apply transformation such as map and filter and simply get a new Iterator which will only apply these transformations when you ask for the next element. Iterator只能够被使用一次,因为其是一个可遍历的指针存在于集合当中,而非集合本身存在于Iterator中。让其在 Scala 如此特殊的原因在于你能够使用 transformation 算子,如map或者filter,并且很容易地获得一个新的Iterator。需要注意的是,新的Iterator只有通过获取元素的时候才会应用那些 transformation 算子。 Scala used to provide iterators which could be reset, but that is very hard to support in a general manner, and they didn’t make version 2.8.0. Scala 曾尝试过给那些 iterator 一个可复位的功能,但这很难以一个通用的方式去支持。 Views are meant to be viewed much like a database view. It is a series of transformation which one applies to a collection to produce a “virtual” collection. As you said, all transformations are re-applied each time you need to fetch elements from it. Views 通常意味着元素需要被观察,类似于数据库中的 view。它是原集合通过一系列的 transformation 算子生成的一个”虚构”的集合。如你所说,每当你需要从原集合中获取数据时,都能够重复应用这些 transformation 算子。 Both Iterator and views have excellent memory characteristics. Stream is nice, but, in Scala, its main benefit is writing infinite sequences (particularly sequences recursively defined). One can avoid keeping all of the Stream in memory, though, by making sure you don’t keep a reference to its head (for example, by using def instead of val to define the Stream). Iterator和 views 两者都有不错内存(记忆?)特性。Stream也可以,但是在 Scala 中,其主要的好处在于能够保留无限长的序列(特别是那些序列是通过递归定义的[这一点需要通过 Stream 本身特性才能够理解])当中。不过,你可以避免将所有Stream保留在内存中,其方法是确保不保留那些对 Stream中head的引用。 针对最后提到的例子,https://stackoverflow.com/questions/13217222/should-i-use-val-or-def-when-defining-a-stream这篇回答有比较好的解释 Because of the penalties incurred by views, one should usually force it after applying the transformations, or keep it as a view if only few elements are expected to ever be fetched, compared to the total size of the view. 由于 views 所带来不良影响(个人认为是这么翻译的),我们通常需要在应用 transformations 后调用force进行计算,或者说如果相比于原 view 中大量元素,新 view 只有少量的元素需要去获取时,可以将其当做新的 view 对待。","categories":[],"tags":[]},{"title":"Scala - 有关变量赋值的问题","slug":"Scala - 有关变量赋值的问题","date":"2018-05-18T15:14:46.000Z","updated":"2018-05-18T15:15:12.000Z","comments":true,"path":"2018/05/18/Scala - 有关变量赋值的问题/","link":"","permalink":"http://apparition957.github.io/2018/05/18/Scala - 有关变量赋值的问题/","excerpt":"","text":"先看个小问题先贴下一段Scala代码,看下这段代码是否存在问题? 12345val persons = List[Person](Person(\"tom\"), Person(\"marry\"), null).iteratorvar person: Person = nullwhile ((person = persons.next()) != null) { println(\"obj name: \" + person.name)} 如果你的答案是这段代码运行不会出任何问题的话,那么你对于 Scala 的变量赋值还是了解太少。 为什么呢在我们一般的认知中,在 Java 和 C++ 中对变量赋值后,其会返回相对应该变量的值,而在 Scala 中,如果对变量赋值后,获取到的返回值却统一是 Unit。 Unit 是表示为无值,其作用与其他语言中的 void 作用相同,用作不返回任何结果的方法的结果类型。 回到刚才那段代码,根据以上说明,如果我们在赋值对person变量的话,那就会导致在每一次循环当中,其实我们一直都是拿 Unit 这个值去与 null 比较,那么就可以换做一个恒等式为Unit != null,这样做的结果就是这个循环不会中断。 在 IDEA 中,如果我们仔细查看代码,发现 IDE 已经提醒我们这个问题的存在了,这这也仅仅只是 Warning 而已。 若通过编译的方法查看源代码的话,会在编译的过程中,获得这样一句警告(并非错误!): 有个简单的例子可以检验自己是否明白懂了这个”bug”: 123var a: Int = 0var b: Int = 0a = b = 1 // 这行代码能够跑通,在其他语言呢? 解决方案在给出常见的解决方案前,先给出为什么 Scala 要这样设计的理由(Scala 之父亲自解释): https://stackoverflow.com/questions/1998724/what-is-the-motivation-for-scala-assignment-evaluating-to-unit-rather-than-the-v 常见的解决方案会有以下几种: 123456789101112131415// solution 1 - 封装成代码块返回最终值,直观但麻烦var person = nullwhile ({person = persons.next; person != null}) { println(\"obj name: \" + person.name)}// solution 2 (推荐)- 通过 Scala 的语法特性,使用它的奇淫技巧Iterator.continually(persons.next()) .takeWhile(_ != null) .foreach(t => {println(\"obj name: \" + t.name)})// solution 3 - 这个与 Solution2 的区别仅仅在于使用的类不同,但使用的类不同便意味着这两者之间存在着不同的遍历方式。两者的区别会在博客中更新。Stream.continually(persons.next()) .takeWhile(_ != null) .foreach(t => {println(\"obj name: \" + t.name)}) 参考资料: https://stackoverflow.com/questions/6881384/why-do-i-get-a-will-always-yield-true-warning-when-translating-the-following-f https://stackoverflow.com/questions/3062804/scala-unit-type https://stackoverflow.com/questions/2442318/how-would-i-express-a-chained-assignment-in-scala","categories":[],"tags":[]},{"title":"Scala - 类构造器","slug":"Scala - 类构造器","date":"2018-05-14T15:58:37.000Z","updated":"2018-05-14T15:59:09.000Z","comments":true,"path":"2018/05/14/Scala - 类构造器/","link":"","permalink":"http://apparition957.github.io/2018/05/14/Scala - 类构造器/","excerpt":"","text":"Scala 构造器可分为两种,主构造器和辅助构造器。 主构造器123456789// 无参主构造器class Demo { // 主构造器的构成部分}// 有参主构造器class Demo2(name: String, age: Int) { // 主构造器的构成部分} 从类的定义开始,花括号的部分为主构造器的构成部分。主构造器在执行时,会执行类中所有的语句。 1234567891011// exampleclass Demo() { val name = \"tom\" val age = 18 doSomething() // 初始化对象时,会打印 name: tome, age: 18 def doSomething() = { println(\"name: \" + name + \", age: \" + age) }} 辅助构造器123456789101112131415161718class Demo { var name = \"\" var age = 0 // 错误定义!! def this() { } def this(name: String) { this() this.name = name } def this(name: String, age: Int) { this(name) this.age = age }} 辅助构造器的名称为this,与 Java 的构造器名称不同(Java 构造器名称是以类名定义的),其代码大致结构为def this(...) {}。若一个类如果没有显式定义主构造器,则编译器会自动生成一个无参的主构造器。 必须注意的是,每个辅助构造器都必须以一个对先前已定义的其他辅助构造器或者主构造器的调用开始。","categories":[],"tags":[]},{"title":"寻找一种更快更高效的方法","slug":"寻找一种更快更高效的方法","date":"2018-05-07T14:35:49.000Z","updated":"2018-05-07T14:36:00.000Z","comments":true,"path":"2018/05/07/寻找一种更快更高效的方法/","link":"","permalink":"http://apparition957.github.io/2018/05/07/寻找一种更快更高效的方法/","excerpt":"","text":"这两天在对我们开发的模块进行最后的收尾,收尾的工作一般来说都是添加测试用例,测试模块调用时是否有 BUG 等。果不其然,老大还是叫我去做模块的测试。其实还是自己对于 C++了解太少,刚入门一个星期才勉强能够看懂之前的部分源码,而且原有工程十分庞大,还有自己封装好的又或自己开发的工具库。想去调用还得自己上网看看 example 熟悉下,没有 example 的那就苦逼自己慢慢摸索了 做测试没关系,毕竟怎么样都能够学到不一样的知识。 先说下这次测试的内容,就是将之前标注好的数据,利用我们的模块重新跑一遍,检验是否有错漏的地方。这上面说的简单,但其中含杂了大量的人工,这我可不干,所以才有了这一篇文章。 材料准备404页面错误检验模块(基于 URL 和 Content 两部分),编写爬虫将标注好的数据中 URL 所对应的页面存储于本地(csv文件) 人工方法如果按照人工方法走,就是针对于一个 URL 创建一个 HTML 文件,然后撰写一个测试用例,跑通了我们就往下走,没跑通那就回头重新梳理逻辑。这种方式如果针对于一两个文件还好说,那如果针对于上百个文件那怎么办?如果这还人工一个个弄,那算你厉害 自动化方法自动化方法是否能够运用在于在这过程当中是否存在一定的规律,相信读到这里的我们,可以明白自动化的方法就是在若干个循环当中,重复操作人工的方法,只是在这个过程当中,你需要用代码来证明你的想法,而非你的汗水 在材料准备中,我们已经有了包含测试数据的 csv 文件,可能读者会理所当然的认为这个自动化测试不就两行代码妥妥的就搞定吗?其实并不然,c++ 中并没有什么第三方库处理 csv 这样的文件(反正我是没找到),如果利用简单的split函数的话,那就会导致原有数据(HTML)的丢失。 这个时候,我们需要转向文件流,即将若干个 HTML 文件存储下来,并创建一个索引表,记录 URL 与其对应的文件名,如下所示: 123456~/htmls/0.html 1.html 2.html index.txt~/htmls/index.txthttps://www.baidu.com 0.htmlhttps://www.taobao.com 1.html 然后在实际编写代码过程中,先读取索引表,再利用索引表的信息,读取 HTML 文件然后运行模块,记录运行结果,当所有测试用例结束时,统计最终结果,并根据最终结果,调整内部的策略。","categories":[],"tags":[]},{"title":"有多少人工就有多少智能","slug":"有多少人工就有多少智能","date":"2018-04-19T15:04:25.000Z","updated":"2018-04-19T15:04:46.000Z","comments":true,"path":"2018/04/19/有多少人工就有多少智能/","link":"","permalink":"http://apparition957.github.io/2018/04/19/有多少人工就有多少智能/","excerpt":"","text":"这个标题其实是来自于小米小爱团队负责人王刚:语音交互背后,有多少人工就有多少智能这篇文章,虽然我现在的工作与人工智能没关系,但是与我现在的经历息息相关的。 最近在跟着老大去做页面分析的模块,现阶段有个问题在于怎么去解决网页软404问题。可行的解决方案当然有很多,HTTP 请求码、URL 的正则匹配、内容关键字匹配等。但是这么多的解决方案都需要的一个判断标准,判断跑出来的数据可不可靠,如果不可靠的话那么这个方案可能就行不通。 那么比较尴尬的部分来了,这个判断的过程是由人工来的,那这个活自然就落在我和其他同事身上啦。虽然知道这个是必然的过程,但是心还是不甘的,不甘于自己要去做人工筛选工作。 其实单单抛弃人工智能这个前提,“有多少人工就有多少智能”这句话适用于互联网的各个领域,只要能够投入了足够的人力,那么系统的未来也会有很大的改善,以上是我现阶段的看法。 经历了这次人工筛选的活后,我还从这句话体会到了一点,努力提升自己的技术,别让自己成为可取代的人工。加深自己的技术栈吧,再经历多点磨难,或许能够看见更多未来。","categories":[],"tags":[]},{"title":"Spark - take() 算子","slug":"Spark - take() 算子","date":"2018-04-14T05:41:00.000Z","updated":"2018-04-14T05:41:19.000Z","comments":true,"path":"2018/04/14/Spark - take() 算子/","link":"","permalink":"http://apparition957.github.io/2018/04/14/Spark - take() 算子/","excerpt":"","text":"以后遇到不懂的 Spark 算子的话,我都尽可能以笔记的方式去记录它 遇到的情况123rdd.map(...) // 重要前提:数据量在 TB 级别 .filter(...) // 根据某些条件筛选数据 .take(100000) // 取当前数据的前十万条 当时的程序大致就是这样,我的想法是根据filter()之后的数据直接利用take()拿前十万的数据,感觉方便又省事,但是实际的运行情况却是作业的运行时间很长,让人怀疑人生。而且take()一开始默认的分区是1,而后如果当前任务失败的话,会适当的扩增分区数来读取更多的数据。 源码分析废话不多,先贴源码 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152/** * Take the first num elements of the RDD. It works by first scanning one partition, and use the results from that partition to estimate the number of additional partitions needed to satisfy the limit. * * @note This method should only be used if the resulting array is expected to be small, as all the data is loaded into the driver's memory. * 此方法被使用时期望目标数组的大小比较小,即其数组中所有数据都能够存储在 driver 的内存当中。这里的函数解释当中提及到了处理的数据量应当较小,但是没说如果处理了比较大的数据时会怎么样,还得看看继续往下看 * * @note Due to complications in the internal implementation, this method will raise * an exception if called on an RDD of `Nothing` or `Null`. */def take(num: Int): Array[T] = withScope { // scaleUpFactor 字面意思是扩增因子,看到这里我们可以结合上图的例子,不难看出分区的扩增是按照一定的倍数增长的 val scaleUpFactor = Math.max(conf.getInt(\"spark.rdd.limit.scaleUpFactor\", 4), 2) if (num == 0) { new Array[T](0) } else { val buf = new ArrayBuffer[T] val totalParts = this.partitions.length var partsScanned = 0 // 这个循环是为什么 take 失败会进行重试的关键 while (buf.size < num && partsScanned < totalParts) { // The number of partitions to try in this iteration. It is ok for this number to be // greater than totalParts because we actually cap it at totalParts in runJob. // numPartsToTry - 此次循环迭代的分区个数,默认为1。 var numPartsToTry = 1L val left = num - buf.size if (partsScanned > 0) { // If we didn't find any rows after the previous iteration, quadruple and retry. // Otherwise, interpolate the number of partitions we need to try, but overestimate // it by 50%. We also cap the estimation in the end. // 重点!当在上一次迭代当中,我们没有找到任何满足条件的 row 时(至少是不满足指定数量时),有规律的重试(quadruple and retry,翻译水平有限) if (buf.isEmpty) { numPartsToTry = partsScanned * scaleUpFactor } else { // As left > 0, numPartsToTry is always >= 1 numPartsToTry = Math.ceil(1.5 * left * partsScanned / buf.size).toInt numPartsToTry = Math.min(numPartsToTry, partsScanned * scaleUpFactor) } } val p = partsScanned.until(math.min(partsScanned + numPartsToTry, totalParts).toInt) val res = sc.runJob(this, (it: Iterator[T]) => it.take(left).toArray, p) // 每一次循环迭代都会获取新的数据加到 buf 当中,所以并不是每一次重试都是从头对数据进行遍历,那这样会没完没了 res.foreach(buf ++= _.take(num - buf.size)) partsScanned += p.size } // 这里才是我们最终的结果 buf.toArray }} 总结take()算子使用的场景是当数据量规模较小的情况,亦或者说搭配filter()时,filter()能够较快的筛选出数据来。","categories":[],"tags":[]},{"title":"Spark - 由 foreach 引发的思考","slug":"Spark - 由 foreach 引发的思考","date":"2018-03-31T17:34:32.000Z","updated":"2018-03-31T17:34:55.000Z","comments":true,"path":"2018/04/01/Spark - 由 foreach 引发的思考/","link":"","permalink":"http://apparition957.github.io/2018/04/01/Spark - 由 foreach 引发的思考/","excerpt":"","text":"废话不说,先贴代码 12345val numbers = sc.parallelize(Array(1,2,3,4,5,6,7,8,9))val map = scala.collection.mutable.Map[Int, Int]()numbers.foreach(l => {map.put(l,l)})println(map.size) // 此时 map 的存储了几个键值对 首先我们先说个概念 —— 闭包 闭包是 Scala 中的特性,用通俗易懂的话讲就是函数内部的运算或者说函数返回值可由外部的变量所控制,用个例子解释就是: 1234567var factor = 10// multiplier 函数的返回值有有两个决定因素,输入参数变量 i 以及外部变量 factor。输入参数变量 i 是由我们调用该函数时决定的,相较于 factor 是可控的,而 factor 则是外部变量所定义,相较于 i 是不可控的val multiplier = (i: Int) => i * factor println(multiplier(1)) // 10factor = 20println(multiplier(1)) // 20 根据上述提及的闭包可知,刚才所写的代码中l => {map.put(1,1)}其所定义的函数就是一个闭包 既然标题中提到了 Spark,那就要说明闭包与 Spark 的关系了 在 Spark 中,用户自定义闭包函数并传递给相应的 RDD 所定义好的方法(如foreach、map)。Spark 在运行作业时会检查 DAG 中每个 RDD 所涉及的闭包,如是否可序列化、是否引用外部变量等。若存在引用外部变量的情况,则会将它们的副本复制到相应的工作节点上,保证程序运行的一致性 下面是 Spark 文档中解释的: Shared VariablesNormally, when a function passed to a Spark operation (such as map or reduce) is executed on a remote cluster node, it works on separate copies of all the variables used in the function. These variables are copied to each machine, and no updates to the variables on the remote machine are propagated back to the driver program. Supporting general, read-write shared variables across tasks would be inefficient. However, Spark does provide two limited types of shared variables for two common usage patterns: broadcast variables and accumulators. 共享变量通常情况下,当有函数传递给在远端集群节点上执行的 Spark 的算子(如map或reduce)时,Spark 会将所有在该函数内部所需要的用到的变量分别复制到相应的节点上。这些副本变量会被复制到每个节点上,且在算子执行结束后这些变量并不会回传给驱动程序(driver program)。 Normally, when a function passed to a Spark operation (such as map or reduce) is executed on a remote cluster node, it works on separate copies of all the variables used in the function. These variables are copied to each machine, and no updates to the variables on the remote machine are propagated back to the driver program. 总结,如果直接运行一开始所提及的程序时,那么所获得的答案是0,因为我们知道map变量会被拷贝多份至不同的工作节点上,而我们操作的也仅仅只是副本罢了 从编译器的角度来说,这段代码是一个闭包函数,而其调用了外部变量,代码上没问题。但是从运行结果中,这是错误操作方式,因为 Spark 会将其所调用的外部变量进行拷贝,并复制到相应的工作节点中,而不会对真正的变量产生任何影响 相应的解决方案有 12345val numbers = sc.parallelize(Array(1,2,3,4,5,6,7,8,9))val map = scala.collection.mutable.Map[Int, Int]()numbers.collect().foreach(l => {map.put(l,l)})println(map.size) 参考资料: http://spark.apache.org/docs/2.1.0/programming-guide.html#shared-variables","categories":[],"tags":[]},{"title":"列式存储 - HBase vs Parquet","slug":"列式存储 - HBase vs Parquet","date":"2018-03-24T07:05:48.000Z","updated":"2018-03-24T07:06:35.000Z","comments":true,"path":"2018/03/24/列式存储 - HBase vs Parquet/","link":"","permalink":"http://apparition957.github.io/2018/03/24/列式存储 - HBase vs Parquet/","excerpt":"","text":"虽然两者在使用场景上没有可比性,HBase 是非关系型数据库,而 Parquet 是数据存储格式,但是两者却存在相似的概念——列式存储。我在了解 Parquet 的时候,因为列式存储这个概念与 HBase 混淆时,所以特意坐下笔记,记录两者的区别 让我们直入正题,什么是列式存储?相比行式存储又有什么优势呢? 图源来自 http://zhuanlan.51cto.com/art/201703/535729.htm 首选先从 HBase 开始讲述。HBase是一个分布式的、面向列的非关系型数据库。它的架构设计如下: 简单说明一下: HMaster:HBase 主/从架构的主节点。通常在一个 HBase 集群中允许存在多个 HMaster 节点,其中一个节点被选举为 Active Master,而剩余节点为 Backup Master。其主要作用在于: 管理和分配 HRegionServer 中的 Region 管理 HRegionServer 的负载均衡 HRegionServer:HBase 主/从架构的从节点。主要负责响应 Client 端的 I/O 请求,并向底层文件存储系统 HDFS 中读写数据 HRegion:HBase 通过表中的 RowKey 将表进行水平切割后,会生成多个 HRegion。每个 HRegion 都会被安排到 HRegionServer 中 Store:每一个 HRegion 有一个或多个 Store 组成,Store 相对应表中的 Column Family(列族)。每个 Store 都由一个 MemStore 以及多个 StoreFile 组成 MemStore:MemStore 是一块内存区域,其将 Client 对 Store 的所有操作进行存储,并到达一定的阈值时会进行 flush 操作 StoreFile:MemStore 中的数据写入文件后就成为了 StoreFile,而 StoreFile 底层是以 HFile 为存储格式进行保存的 HFile:HBase 中 Key-Value 数据的存储格式,是 Hadoop 的二进制文件。其中 Key-Value 的格式为(Table, RowKey, Family, Qualifier, Timestamp)- Value HBase 的主要读写方式可以通过以下流程进行: 可以从上述的架构讲述看出,HBase 并非严格意义上的列式存储,而是基于“列族”存储的,所以其是列族的角度进行列式存储。 Parquet 是面向分析型业务的列式存储格式,其不与某一特定语言绑定,也不与任何一种数据处理框架绑定在一起,其性质类似于 JSON。 Parquet 相较于 HBase 对数据的处理方式,其将数据当做成一种嵌套数据的模型,并将其结构定义为 schema。每一个数据模型的 schema 包含多个字段,而每个字段又可以包含多个字段。每一字段都有三个属性:repetition、type 和 name,其中 repetition 可以是以下三种:required(出现1次)、optional(出现0次或1次)、repeated(出现0次或多次),而 type 可以是 group(嵌套类型)或者是 primitive(原生类型)。 举一个典型的例子: 在 Parquet 格式的存储当中,一个 schema 的树结构有几个叶子节点,在实际存储中就有多少个 column。例如上面 schema 的数据存储实际上有四个 column,如下所示: 从上面的图看来,与 HBase 好像没有什么区别,但这只是为了让用户更好的了解数据才这样表示,其内部实现的机制与 HBase 完全不同,而且 Parquet 是真正的基于列式存储。其能够进行列式存储归功于 Striping/Assembly 算法。 算法我就不详细说了,这篇文章讲的很详细,我就不献丑了。 参考资料: HBase 权威指南 HBase笔记:存储结构 深入分析Parquet列式存储格式","categories":[],"tags":[]},{"title":"Scala - identity() 函数","slug":"Scala - identity() 函数","date":"2018-03-19T12:51:59.000Z","updated":"2018-03-19T12:52:38.000Z","comments":true,"path":"2018/03/19/Scala - identity() 函数/","link":"","permalink":"http://apparition957.github.io/2018/03/19/Scala - identity() 函数/","excerpt":"","text":"最近在写 Spark 作业的时候,使用到了 groupBy和sortBy,在查找文档的时候,发现有的文档中的代码有着groupBy(identity)这样奇怪的写法。 在 Scala 文档中,identity 函数的作用就是将传入的参数“直接”当做返回值回传给调用者,这在正常使用中,可以说是毫无作用,但他在groupBy和sortBy等函数中的作用,在于避免程序员书写相同且容易出错的逻辑,原因如下: 12345678910111213141516171819// 前提条件:val array = Array(9,2,1,3,1,5,9,4,6,7,2)// 统计 array 中每个元素出现的次数// 正常逻辑:array.groupBy(n => n)// scala.collection.immutable.Map[Int,Array[Int]] = Map(5 -> Array(5), 1 -> Array(1, 1), 6 -> Array(6), 9 -> Array(9, 9), 2 -> Array(2, 2), 7 -> Array(7), 3 -> Array(3), 4 -> Array(4))// 使用 identityarray.groupBy(identity)// 将 array 进行排序(升序)// 正常逻辑:array.sortBy(n => n)// Array[Int] = Array(1, 1, 2, 2, 3, 4, 5, 6, 7, 9, 9)// 使用 identity 或者简化版本array.sortBy(identity)array.sorted","categories":[],"tags":[]},{"title":"分布式爬虫架构","slug":"分布式爬虫架构","date":"2018-03-06T14:53:55.000Z","updated":"2018-03-06T14:54:56.000Z","comments":true,"path":"2018/03/06/分布式爬虫架构/","link":"","permalink":"http://apparition957.github.io/2018/03/06/分布式爬虫架构/","excerpt":"","text":"最近突然对爬虫框架很感兴趣,但一直无奈于没有服务器能让我捣鼓捣鼓,所以脑子就一直想如何去设计这个框架。翻了很多篇资料,总结了挺多经验,然后就画了下面这张架构图。个人认为很不成熟,但毕竟也是一种想法。希望能力提升后能够想到更加全面完善的架构。","categories":[],"tags":[]},{"title":"大三上学期总结","slug":"大三上学期总结","date":"2018-01-22T14:30:09.000Z","updated":"2018-01-22T14:30:25.000Z","comments":true,"path":"2018/01/22/大三上学期总结/","link":"","permalink":"http://apparition957.github.io/2018/01/22/大三上学期总结/","excerpt":"","text":"今天是周一(2018/01/22),是我正式作为小米实习生的第一天,也是我第一次远离熟悉的地方来到北京闯荡。经历过这学期磨人的课程后,经历过让人背书背的头大的毛概后,经历过曾经一度让人绝望的面试后,经历过令人心寒的租房后,终于可以安下心来好好写我的学期总结。 下面我就这学期的比较重要的方向进行总结吧。 学习 这学期课程真的比以往的多,几乎每天都要上至少两节课,甚至还得上整天,真让人疲惫不堪,但是真正觉得心累的,还是宿舍的氛围,还是像大二那样过一天是一天,不到找工作/临近考试的时候不会去努力。这学期我就尝试着每晚都去图书馆,但是就算是十点半回到宿舍还是无法得到一片宁静,因为十点半的时候,有个宿友准点吃鸡,而且很吵,吵到连看美剧都没心情。当时也怪自己没肯直说吧,暂时不说了,不想开始就写一长篇的抱怨。 虽然这学期很累,但是过得也算充实,毕竟我认清自己的学习方向了,之前在大二中我接触的是Web开发,偏向于云计算/微服务方面,但是每次接触的工程都只是学习工具,学习如何使用,然后反复造轮子,跟着规整的MVC架构来搭建项目,我对这一过程心生厌恶,觉得自己不能这样。于是乎我寻找了另一个兴趣点——大数据进行学习。大数据既是现在的热点,也是我最感兴趣的地方,每次都能借好多书走,学习到很多新的内容,新的架构。 这也是为什么我在找岗位的时候,想要寻找大数据方面的职位,一是充实自己,提高技能;二是在实际开发工程中,切身体会到如何真正的运用大数据来进行对数据分析。 简历 当初写简历的时候觉得还很自信,秉持着简约的风格的简历外加上整齐规格的排版,一定能够在一月份前拿到一份心满意足的offer。经历过整整三个星期都没有一通面试电话时,我真的很绝望,发自心底的绝望,认为这三年学的东西是不是白学了。后来经过很多同学的指点过后,我才发现一份简约的简历,要遵循以下几个点: 只能是一页纸,不能够再多 只写有用的话(姓名,联系电话,工作/校园经历) 排版要规整,粗细得体 在这里真的要讲一句真心话,在正式修改了简历后,过没两天实习僧上的公司就真的给我面试电话通知了,而且后面陆陆续续也来了不少电话。 租房(注意粗体部分) 租房是个出来漂的首要大事啊,自从拿到offer后我就投入了租房这件事了,但是租房并不像想象中那么容易(除非你运气真的超级超级棒)。既要小心租房中遇到的中介/二房东/代理,还要小心合同中会不会收取额外的费用(中介费/物业管理费/燃气费/服务费),更要小心同住的人是否有良好的习惯。我几乎每晚回到宿舍都要花上二十分钟到半个小时,途径有豆瓣/自如(等各大互联网租房平台)/闲鱼/暖房(自动爬虫机制的网站,感觉还行),其中遇到了有让人觉得恶心的中介,也遇到了聊得上天的转租大哥,但是由于自己不在北京的缘故,无法确切的看到实际房子的状况,所以一直犹豫着要不要直接租房(实际原因是没看到让人一眼看中的房子,或者碍于价格太高了)。 出于以上原因,我决定了考完试后联系好之前找好的中介/转租房东一一探寻房子。当时我是提前购买了凌晨到北京的机票,打算在机场中睡一觉就赶过去,所以我就在前一天晚上急忙去联系人预约看房,等到凌晨6点时就赶了过去,从西二旗地铁口出发后,暴走3公里后到达下榻酒店(暴走的原因是因为要亲自熟悉周边的环境,才好对房子进行更加深刻的评估),放下行李就跟着中介出去跑了。 真的是比较幸运,在早上十点钟时,中介带我找到了一个不错的房子,房子空间很大,内部装饰还行,价格中等偏上(相当于拿出一半的实习工资还多)。自己当时就想下定决心去签合同,不过出于谨慎,还是与家里人详细沟通了一下。在得到家里人的赞成后,我当时就和中介签的合同了(还是很比较谨慎的,看了好多回合同才肯签字,生怕有什么坑自己没注意)。","categories":[],"tags":[]},{"title":"聊聊log4j","slug":"聊聊log4j","date":"2017-11-27T06:53:55.000Z","updated":"2017-11-27T07:03:40.000Z","comments":true,"path":"2017/11/27/聊聊log4j/","link":"","permalink":"http://apparition957.github.io/2017/11/27/聊聊log4j/","excerpt":"","text":"概要 最近在学习 Zookeeper 的时候,遇到了不少问题,想要在控制台中查看日志但是记录却死活不显示,于是找到了 /etc/zookeeper/log4j.properties 文件,但发现配置选项看不懂,想到之前在写 Web 应用的时候也是拿来就用,都没涉及到日志配置文件这一层面,所以打算整理一番。 log4j 是一个用 Java 编写的可靠,快速和灵活的日志框架(API),它在 Apache 软件许可下发布。log4j 是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX 系统日志等。 与 slf4j 的关系在实际开发当中,常常有人提醒我们,要使用 slf4j 来记录日志,为什么呢? 下面是 sl4fj 官网的介绍。 The Simple Logging Facade for Java (SLF4J) serves as a simple facade or abstraction for various logging frameworks (e.g. java.util.logging, logback, log4j) allowing the end user to plug in the desired logging framework at deployment time. slf4j(Simple Logging Facade For Java,Java 简易日志门面)是一套封装 Logging 框架的抽象层,而 log4j 是 slf4j 下一个具体实现的日志框架,其中还有许许多多的成熟的日志框架,如 logback 等,也是从属于 slf4j。 使用 slf4j 可以在应用层中屏蔽底层的日志框架,而不需理会过多的日志配置、管理等操作。 如何配置log4j 配置文件的基本格式如下所示: 123456789101112#配置根Loggerlog4j.rootLogger = [level], appenderName1, appenderName2, ...#配置日志信息输出目的地 Appenderlog4j.appender.appenderName = fully.qualified.name.of.appender.class log4j.appender.appenderName.option1 = value1 log4j.appender.appenderName.optionN = valueN #配置日志信息的格式(布局)log4j.appender.appenderName.layout = fully.qualified.name.of.layout.classlog4j.appender.appenderName.layout.option1 = value1 log4j.appender.appenderName.layout.optionN = valueN 其中: [level] - 日志输出级别,可分为以下级别(级别程度从上到下递增): 级别 描述 ALL 所有级别,包括定制级别。 TRACE 比 DEBUG 级别的粒度更细。 DEBUG 指明细致的事件信息,对调试应用最有用。 INFO 指明描述信息,从粗粒度上描述了应用运行过程。 WARN 指明潜在的有害状况。 ERROR 指明错误事件,但应用可能还能继续运行。 FATAL 指明非常严重的错误事件,可能会导致应用终止执行。 OFF 最高级别,用于关闭日志。 Appender - 日志输出目的地,常用的 Appender 有以下几种: Appender 作用 org.apache.log4j.ConsoleAppender 输出至控制台 org.apache.log4j.FileAppender 输出至文件 org.apache.log4j.DailyRollingFileAppender 每天产生一个日志文件 org.apache.log4j.RollingFileAppender 文件容量到达指定大小时产生一个新的文件 org.apache.log4j.WriterAppender 将日志信息以输出流格式发送到任意指定地方 Layout - 日志输出格式,常用的 Layout 有以下几种: Layout 作用 org.apache.log4j.HTMLLayout 以 HTML 表格形式布局 org.apache.log4j.PatternLauout(常用) 以格式化的方式定制布局 org.apache.log4j.SimpleLayout 包含日志信息的级别和信息字符串 org.apache.log4j.TTCCLayout 包含日志所在线程、产生时间、类名和日志内容等 打印参数(格式化输出格式,一般对应于 org.apache.log4j.PatternLauout) 参数 作用 %m 输出代码中指定的消息 %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL %r 输出自应用启动到输出该log信息耗费的毫秒数 %c 输出所属的类目,通常就是所在类的全名。%c{1} 可取当前类名称 %t 输出产生该日志事件的线程名 %n 输出一个回车换行符,Windows平台为“\\r\\n”,Unix平台为“\\n” %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式。标准格式为 %d{yyyy-MM-dd HH:mm:ss} %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。 option - 可选配置。一般来说每个 Appender 或者 Layout 都有默认配置,用户使用自定义日志配置,如指定输出地点等。常用的 option 有以下几种: 参数 作用 file 日志输出至指定文件 thresold 定制日志消息的输出在不同 level 时的行为, append 是否追加至日志文件中 参考资料: Log4J 教程 - 极客学院 log4j.properties配置详解","categories":[],"tags":[{"name":"log4j","slug":"log4j","permalink":"http://apparition957.github.io/tags/log4j/"}]},{"title":"SpringMVC源码分析 - DispatcherServlet请求处理过程","slug":"SpringMVC源码分析-DispatcherServlet请求处理过程","date":"2017-11-26T15:58:23.000Z","updated":"2017-11-26T15:59:13.000Z","comments":true,"path":"2017/11/26/SpringMVC源码分析-DispatcherServlet请求处理过程/","link":"","permalink":"http://apparition957.github.io/2017/11/26/SpringMVC源码分析-DispatcherServlet请求处理过程/","excerpt":"","text":"概要 这张图在网上搜到的,但是实际的来源处实在找不到了,如果后面找到一定补上链接。 上图的流程可用以下文字进行描述: DispatcherServelt 作为前端控制器,拦截所有的请求。 DispatcherServlet 接收到 http 请求之后, 根据访问的路由以及 HandlerMapping,获取一个 HandlerExecutionChain 对象。 DispatcherServlet 将 Handler 对象交由 HandlerAdapter,调用处理器 Controller 对应功能处理方法。 HandlerAdapter 返回 ModelAndView 对象,DispatcherServlet 将 view 交由 ViewResolver 进行解析,得到相应的视图,并用 Model 对 View 进行渲染。 返回响应结果。 源码分析源码部分我打算通过流程图的形式来分析,源代码部分还是根据流程图来一步步看会更好,否则会被陌生且复杂的源代码给搞混(欲哭无泪)。 DEBUG大法是真的好!","categories":[],"tags":[{"name":"SpringMVC","slug":"SpringMVC","permalink":"http://apparition957.github.io/tags/SpringMVC/"}]},{"title":"SpringMVC源码分析 - DispatcherServlet初始化过程","slug":"SpringMVC源码分析-DispatcherServlet初始化过程","date":"2017-11-26T15:58:09.000Z","updated":"2017-11-26T15:58:52.000Z","comments":true,"path":"2017/11/26/SpringMVC源码分析-DispatcherServlet初始化过程/","link":"","permalink":"http://apparition957.github.io/2017/11/26/SpringMVC源码分析-DispatcherServlet初始化过程/","excerpt":"","text":"继承体系 从 DispatcherServlet 继承体系来看(蓝色部分),DispatcherServlet 继承自 FrameworkServlet,而 FrameworkServlet 又继承自 HttpServletBean ,最终 HttpSevletBean 继承了 HttpServlet 。通过这一步步继承封装之后,才构成了如今的 DispatcherSevlet 架构基础。 下面将自上到下来说明 DispatcherServlet 的初始化过程。 HttpServletHttpServletBean 继承自 Servlet 架构中的 HttpServlet 类,并重写了init()方法。 Servlet 生命周期从创建到销毁的过程中,有三个重要的方法: init() - 负责初始化 Servlet 对象。在 Servlet 生命周期中只会调用一次。 service() - 负责响应客户的请求。每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service 方法中两个参数,分别是 ServletRequest 和 ServletResponse,用于传递 http 请求和回写。 destory() - 负责销毁 Servlet 对象。在 Servlet 生命周期中只会调用一次。 从 Servlet 的生命周期可知,在 init()方法中,我们可以进行初始化工作,HttpServletBean 正是也做了这样的工作。源码如下所示: 123456789101112131415161718192021222324252627282930313233@Overridepublic final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug(\"Initializing servlet '\" + getServletName() + \"'\"); } // Set bean properties from init parameters. // 加载 Servlet 的配置文件(一般指 web.xml) PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); if (!pvs.isEmpty()) { try { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); // 上面做了这么多的工作,到这里却是一个空方法,而它的子类都没有去重写这个方法,个人认为这是想让开发者自定义如何管理 Servlet 配置吧 bw.setPropertyValues(pvs, true); } catch (BeansException ex) { if (logger.isErrorEnabled()) { logger.error(\"Failed to set bean properties on servlet '\" + getServletName() + \"'\", ex); } throw ex; } } // Let subclasses do whatever initialization they like. // 交由子类(FrameworkServlet)来进行其特有的初始化工作 initServletBean(); if (logger.isDebugEnabled()) { logger.debug(\"Servlet '\" + getServletName() + \"' configured successfully\"); }} FrameworkServletFrameworkServlet 继承自 HttpServletBean,实现了initServletBean()方法。FrameworkServlet 在继承体系结构中,在 Servlet 与 SpringMVC 起到了承上启下的作用,它负责初始化 WebApplicationContext,还负责重写了 Servlet 生命周期中另外两个重要方法——service()和destory(),并改写了doGet()、doPost()等 http 方法,统一调用processHandler()方法来处理所有 http 请求。 ApplicationContext 是 Spring 的核心,相当于 Spring 环境中的上下文。而在WebApplicationContext 继承自 ApplicationContext,充当了在 Web 环境中使用 Spring 的上下文。在 Web 环境中,WebApplicationContext 实例需要 ServletContext,即它必须拥有 Web 容器才能够完成启动的工作。 下面重点讲initServletBean()方法,源码如下所示: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657@Overrideprotected final void initServletBean() throws ServletException { // do sth try { // 初始化 WebApplicationContext this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException ex) { this.logger.error(\"Context initialization failed\", ex); throw ex; } catch (RuntimeException ex) { this.logger.error(\"Context initialization failed\", ex); throw ex; }}// initServletBean()转而调用了initWebApplicationContext(),所以重点工作在这里protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { // A context instance was injected at construction time -> use it wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { if (cwac.getParent() == null) { cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // No context instance was injected at construction time -> see if one // has been registered in the servlet context. If one exists, it is assumed // that the parent context (if any) has already been set and that the // user has performed any initialization such as setting the context id wac = findWebApplicationContext(); } if (wac == null) { // No context instance is defined for this servlet -> create a local one wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // DispatcherSevlet 初始化工作的入口就在这里! onRefresh(wac); } // do sth return wac;} DispatcherServlet在进行下一步代码分析之前,先看下 DispatcherSevrlet 的静态代码块部分: 123456789101112131415private static final String DEFAULT_STRATEGIES_PATH = \"DispatcherServlet.properties\";static { // Load default strategy implementations from properties file. // This is currently strictly internal and not meant to be customized // by application developers. // 加载所有默认配置,用于后面的初始化工作 try { ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException ex) { throw new IllegalStateException(\"Could not load '\" + DEFAULT_STRATEGIES_PATH + \"': \" + ex.getMessage()); }} DispatcherServlet.properties配置文件中定义了DispatcherServlet各组件中的配置实现形式,如下所示: 123456789101112131415161718192021222324# Default implementation classes for DispatcherServlet's strategy interfaces.# Used as fallback when no matching beans are found in the DispatcherServlet context.# Not meant to be customized by application developers.org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolverorg.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolverorg.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMappingorg.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\\ org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapterorg.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\\ org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\\ org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolverorg.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslatororg.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolverorg.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager 回到正题,在onRefresh()方法,调用了initStrategies(),所以重点部分就在于initStrategies()。 12345678910111213141516171819202122232425@Overrideprotected void onRefresh(ApplicationContext context) { initStrategies(context);}protected void initStrategies(ApplicationContext context) { // 初始化多媒体解析器 initMultipartResolver(context); // 初始化位置解析器 initLocaleResolver(context); // 初始化主题解析器 initThemeResolver(context); // 初始化 HandlerMappings initHandlerMappings(context); // 初始化 HandlerAdapters initHandlerAdapters(context); // 初始化异常处理解析器 initHandlerExceptionResolvers(context); // 初始化请求到视图名转换器 initRequestToViewNameTranslator(context); // 初始化视图解析器 initViewResolvers(context); // 初始化 FlashMapManager initFlashMapManager(context);} 下面以initHandlerMappings()方法为例,分析如何加载 HandlerMapping。 1234567891011121314151617181920212223242526272829303132private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; if (this.detectAllHandlerMappings) { // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<>(matchingBeans.values()); // We keep HandlerMappings in sorted order. AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { try { HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } // Ensure we have at least one HandlerMapping, by registering // a default HandlerMapping if no other mappings are found. if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isDebugEnabled()) { logger.debug(\"No HandlerMappings found in servlet '\" + getServletName() + \"': using default\"); } }} 上面的源码注释解释的十分清楚了,值得注意一点的是,为了确保 DispatcherServlet 中至少有一个 HandlerMapping,它会从上面所述的默认配置项中加载所有默认组件,如 HandlerMapping 默认组件为 BeanNameUrlHandlerMapping、RequestMappingHandlerMapping。 参考资料: servlet清晰理解 WebApplicationContext初始化 Spring MVC之DispatcherServlet初始化","categories":[],"tags":[{"name":"SpringMVC","slug":"SpringMVC","permalink":"http://apparition957.github.io/tags/SpringMVC/"}]},{"title":"Executor 框架 - 线程池","slug":"Executor 框架 - 线程池","date":"2017-11-14T04:25:11.000Z","updated":"2017-11-14T04:25:28.000Z","comments":true,"path":"2017/11/14/Executor 框架 - 线程池/","link":"","permalink":"http://apparition957.github.io/2017/11/14/Executor 框架 - 线程池/","excerpt":"","text":"概念线程池(Thread Pool)是一种线程使用模式。当线程使用不当,创建过多时会带来调度的开销,进而影响缓存局部性和整体性能。而线程池维护着若干个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。 重要成员在 Java 中,线程池一般都是通过 ThreadPoolExecutor 来构建的,下面将介绍 ThreadPoolExecutor 的构造函数部分。 1234567public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 参数 作用 corePoolSize 线程池中所保存的核心线程数。线程池启动时默认是空的,只有任务来临时才会创建线程以处理请求。在 corePoolSize 范围内已创建的线程即使处于空闲状态,除非设置了 allowCoreThreadTimeOut,否则不会被销毁 maximumPoolSize 线程池允许创建的最大线程数。 keepAliveTime 当线程池中的线程超过了 corePoolSize 的范围时,终止过多的空闲线程的时间 unit keepAliveTime 的时间单位 workQueue 用于容纳所有待执行的任务的工作队列。该工作队列只能够容纳实通过 execute() 方法提交的实现 Runnable 接口的任务 threadFactory 用于 executor 如何创建一个线程(一般使用默认线程工厂DefaultThreadFactory) handler 当线程池与工作队列的容量已满时,任务提交被拒绝时所采取的拒绝策略。 工作流程以下流程为向线程池中正常提交任务时的基本流程: 线程池判断核心线程池(corePoolSize)里的线程是否都在执行任务,如果不是,则创建一个新的工作线程(或复用一个工作线程)来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。 线程池判断工作队列(workQueue)是否已经满了。如果工作队列没有满,则将新提交的任务存储到工作队列中,等待被调度执行。如果工作队列已满,则执行第三步。 线程池判断线程池的线程是否已经满了(maximumPoolSize)。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则根据线程池的拒绝策略来处理该任务。 以上两图均来自于这篇博客Java线程池(ThreadPoolExecutor)原理分析与使用 - 孙_悟__空 拒绝策略线程池中已经定义了四种任务提交的拒绝策略(以下策略我都贴出源码部分,怕翻译有问题): AbortPolicy :不执行任务,并直接抛出一个运行时异常。 1A handler for rejected tasks that throws a RejectedExecutionException DiscardPolicy :(silently)直接抛弃任务,其内部实现是一个空方法。 1A handler for rejected tasks that silently discards the rejected task. DiscardOldestPolicy : 从工作队列中抛弃最老的未处理的任务,并尝试重新执行该任务。 123A handler for rejected tasks that discards the oldest unhandled request and then retries {@code execute}, unless the executor is shut down, in which case the task is discarded. CallerRunsPolicy : 线程池直接创建一个 calling 线程来执行任务。 123A handler for rejected tasks that runs the rejected task directly in the calling thread of the {@code execute} method, unless the executor has been shut down, in which case the task is discarded. 参考资料 线程池 - 维基百科,自由的百科全书 Java:多线程,线程池,ThreadPoolExecutor详解 Java线程池(ThreadPoolExecutor)原理分析与使用","categories":[],"tags":[]},{"title":"Executor 框架 - 概念","slug":"Executor 框架 - 概念","date":"2017-11-14T02:19:26.000Z","updated":"2017-11-14T02:21:41.000Z","comments":true,"path":"2017/11/14/Executor 框架 - 概念/","link":"","permalink":"http://apparition957.github.io/2017/11/14/Executor 框架 - 概念/","excerpt":"","text":"概述Executor 框架是 Java 5 中引入的,位于 java.util.concurrent 包下,其内部使用了线程池机制,可用于启动、调度和管理多个线程。通过 Executor 来启动线程比使用 Thread 的 start 方法的好处不仅在于更易于管理,效率更好,还在于有助于避免 this 逃逸问题。 this 逃逸问题是指在构造函数返回之前其他线程就持有该对象的引用。调用尚未构造完全的对象的方法可能会引发令人疑惑的错误。this 逃逸经常发生在构造函数中启动线程或注册监听器时。 12345678910111213public class ThisEscape { public ThisEscape() { new Thread(new EscapeRunnable()).start(); // ... } private class EscapeRunnable implements Runnable { @Override public void run() { // 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸 } } } 组成部分Executor 框架包括有以下组件: 任务:包含被执行任务需要实现的接口:Runnable 接口和 Callable 接口。 任务的执行:包括任务任务机制的核心接口 Executor,以及继承自Executor 接口的 ExecutorService 接口与 CompletionService 接口。 异步计算的结果:包括接口 Future,以及实现 Future 接口的 FutureTask 类。 成员介绍ExecutorExecutor是一个Executor 框架的核心接口,它内部只定义了一个方法void execute(Runnable command),该方法接受一个 Runnable 实例,并将其执行。 ExecutorServiceExecutorService接口继承自 Executor 接口,它提供了更加丰富的管理多线程的方法,比如,ExecutorService 接口提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 ExecutorService 的生命周期包括三种状态:运行、关闭、终止。 运行:当实现 ExecutorService 接口的类的实例被创建后,便进入运行状态。 关闭:当调用了 ExecutorService 接口内部提供的 shutdown 方法时,便平滑地进入关闭状态。平滑过渡是指在关闭状态中,ExecutorService 会停止接收任何新的任务,并且会等待所有已经提交的任务执行完成(已经提交的任务分为两类:一类是已经在执行的,另一类是还没有开始执行的)。 终止:在所有已提交的任务执行完毕后,便进入了终止状态。 ExecutorsExecutors 提供了一系列工厂方法用于用于创建功能不同的线程池,所有返回的线程池都实现了ExecutorService 接口。以下为四种常见的线程池类型: 1234567891011// 创建固定数目线程的线程池public static ExecutorService newFixedThreadPool(int nThreads) // 创建一个可缓存的线程池,调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程不可用,则创建一个新线程并添加到线程池中。终止并从缓存中移除那些已有60秒未被使用的线程public static ExecutorService newCachedThreadPool() // 创建一个单线程化的线程池public static ExecutorService newSingleThreadExecutor();// 创建一个支持定时以及周期性的任务执行的线程池,多数情况可代替 Timer 类public static ExecutorService newScheduledThreadPool(int corePoolSize); Future/FutureTask/Callable/Runnable在 JDK5 之后,任务可分为两类:一类是实现了 Runnable 接口的类,另一类是实现了 Callable 接口的类。两者都能够被 ExecutorService 执行,但两者区别在于,Runnable 任务没有返回值,而 Callable 任务有返回值,且能够抛出检查异常(checked exception)。 Future 接口对具体提交的任务,封装并提供了获取结果,任务取消等操作。执行结果可通过调用 get() 方法来获取,该方法会阻塞直到任务返回结果。FutureTask 则是 Future 接口的具体实现类。 Future 封装的 Runnable 任务可以调用 get() 方法,但是其返回值为 null。 CompletionService若通过向线程池提交了若干个任务,并通过容器保存所有 FutureTask,当需要得到执行结果的时候,可以通过循环遍历 FutureTask 的方式,调用 get() 方法获取,但是如果此时 FutureTask 尚未完成,那么此时线程便会阻塞等待至任务运行结束。由于无法准确知道哪个任务将会优先执行完成,使用循环遍历的方式效率不会很高。 在 JDK5 中提供了 CompletionService,其内部通过 BlockingQueue 来管理若干线程。ExecutorCompletionService 为 CompletionService 接口的具体实现类。 take() :获取任务结果。获取并移除下一个已完成任务的 Future。如果任务不存在,则等待。 poll() : 与 take() 功能相同,不同之点在于任务不存在时,直接返回 null。 以上两种方法特性其实就是利用了 BlockingQueue 接口的特点。 参考资料 并发新特性—Executor 框架与线程池 变量可见性和volatile, this逃逸, 不可变对象, 以及安全公开–Java Concurrency In Practice C03读书笔记 Executor框架简介 - 加大装益达 java并发编程之CompletionService - miaoLoveCode","categories":[],"tags":[]},{"title":"《深入理解Java虚拟机》读书笔记 - 线程安全与锁优化","slug":"《深入理解Java虚拟机》读书笔记 - 线程安全与锁优化","date":"2017-11-11T08:32:18.000Z","updated":"2017-11-11T08:33:55.000Z","comments":true,"path":"2017/11/11/《深入理解Java虚拟机》读书笔记 - 线程安全与锁优化/","link":"","permalink":"http://apparition957.github.io/2017/11/11/《深入理解Java虚拟机》读书笔记 - 线程安全与锁优化/","excerpt":"","text":"此篇为《深入理解Java虚拟机》第十三章13.2部分的读书笔记 线程安全 对于线程安全较合适的定义为:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。 Java 语言中的线程安全按照线程安全的“安全程度”由强至弱来排序,我们可以将 Java 语言中各种操作共享的数据分为以下5类:不可变、线程绝对安全、相对线程安全、线程兼容和线程对立。 不可变 在 Java 中不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采用任何的线程安全保障措施。如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它就可以保证它的不可变性;如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响,如 String。 绝对线程安全 在 Java 中要求一个类如同开头的定义一般,不管运行环境如何,调用者都不需要任何额外的同步措施。这种做法虽然是安全可用的,但是这往往都会付出很大的、甚至是不切实际的代价。 相对线程安全 相对的线程安全就是我们通常意义上的所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要额外的保证措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段保证调用的正确性。例如 Vector、Hashtable、Collections.synchronizedCollection() 方法包装的集合等。 特别说明,Vector 内部函数都使用 synchronized 关键字修饰,看上去很安全,但如果调用者的操作不当,仍会出现不可避免的错误。即在查询一个元素的时候,某个线程就已经将这个元素删除了,那就会抛出 ArrayIndexOutOfBoundsException 异常。 线程兼容 线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下中可以安全使用。例如 Java API 中大部分的类都属于线程兼容,如与前面 Vector、Hashtable 内部所使用的就是 ArrayList 和 HashMap 等。 线程对立 线程对立是指无论调用端是否采用了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常是有害的,应当尽量避免。比如 Thread 类中的 suspend() 和 resume() 方法,suspend() 试图中断线程,resume() 试图恢复线程,如果并发进行的话,会存在很大的死锁风险,所以这两个方法已被抛弃(@Depreacted)使用。 线程安全的实现方法 P390 锁优化自 JDK1.5之后,HotSpot 虚拟机针对多线程并发花了十分的精力,去实现各种锁优化技术,如适应性自选(Adaptive Spinning)、锁清除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。 自旋锁与自适应自旋在线程互斥同步的时候,由于需要实现线程互斥,被阻塞线程需要由运行态转入阻塞态,而挂起线程和恢复线程的操作都需要从用户态转入到内核态中完成,这些操作给系统的并发性能带来了很大的压力。而往往线程并发时,线程共享数据的锁定状态只会持续很短的一段时间,为了这段时间选择去挂起和恢复线程是不值得的。 那么就引出了自旋锁的作用:如果在同一时刻中有两个以上的线程并行执行,我们可以让后面请求锁的线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快地就会释放锁。但持有锁的线程依旧不放弃锁,那么为了最大化降低 CPU 的消耗,将正在自旋等待的线程使用传统的方式进行挂起阻塞等待。上面所述中,为了让线程等待,我们只需要让线程执行一个忙循环(自旋)即可。 而在 JDK1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 锁消除锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。 锁粗化锁粗化是指虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,那将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。例如,在 for 循环中进行对字符串拼接的任务进行加锁,那么锁粗化就会将这一操作外提至 for 循环外。 轻量级锁轻量级锁是相对于传统的锁机制操作而言的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁同步代码所带来的性能消耗。轻量级锁本质上是一种乐观锁的实现。 偏向锁偏向锁是指在无竞争情况下,这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。如果轻量级锁是在无竞争情况下使用 CAS 操作去消除同步的互斥量,那么偏向锁就是在无竞争的情况下,把整个同步都消除掉,连 CAS 操作都不需要。","categories":[],"tags":[]},{"title":"从输入 URL 到页面加载完成的过程中都发生了什么事情?","slug":"从输入 URL 到页面加载完成的过程中都发生了什么事情","date":"2017-11-06T13:00:01.000Z","updated":"2017-11-06T13:00:30.000Z","comments":true,"path":"2017/11/06/从输入 URL 到页面加载完成的过程中都发生了什么事情/","link":"","permalink":"http://apparition957.github.io/2017/11/06/从输入 URL 到页面加载完成的过程中都发生了什么事情/","excerpt":"","text":"最近看到了一道面试题,叫做“从输入 URL 到页面加载完成的过程中都发生了什么事情?”。当看到这道题的时候,就瞬间联想了计网中的五层模型,并想了想大致的流程,但是天真的我看了下网上的回答,发现实在太 naive 了。 下面总结了几篇文章,看看能不能找到时间梳理一遍。 小白难度 https://www.zhihu.com/question/34873227 - 知乎上评分较高的文章。 中等难度 http://www.cnblogs.com/panxueji/archive/2013/05/12/3073924.html - 翻译自网上一篇不错的科普文章。 地狱难度 http://fex.baidu.com/blog/2014/05/what-happen/ - 对不起,这全篇我都没看懂,只想放上来纪念一下。","categories":[],"tags":[]},{"title":"HTTPS 机制原理分析","slug":"HTTPS 机制原理分析","date":"2017-11-06T12:29:51.000Z","updated":"2017-11-06T12:30:28.000Z","comments":true,"path":"2017/11/06/HTTPS 机制原理分析/","link":"","permalink":"http://apparition957.github.io/2017/11/06/HTTPS 机制原理分析/","excerpt":"","text":"概念超文本传输安全协议(Hypertext Transfer Protocol Secure,HTTPS) 是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。这个协议由网景公司在1994年首次提出,随后扩展至互联网上。 HTTPS 顺应时代被发展出来的很大原因在于 HTTP 协议本身的不安全性,即 HTTP 协议传输的内容是不加密的,直接由明文的方式传输,在复杂的网络通信容易被黑客截取,比如中间人攻击等手段。所以在发展 HTTP为前提下,网景公司加入了 SSL(Secure Socket Layer,安全套接字层) ,并在随后的发展过程中,扩展了 TSL(Transport Layer Security,传输层安全),如下所示: 相关术语在了解 HTTPS 如何在信息传输过程中保证数据的安全性前,需要了解下述的一些术语解释: 对称加密 对称加密是指对数据进行加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。对称加密优点在于算法公开,计算量小,加解密效率高,但是其明显的缺点在于若密钥在网络传输过程中被黑客截取,那么黑客就能够正确地解析数据,那么这样就无法保证数据的安全了。 非对称加密 非对称加密,与对称加密正好相反,该算法需要两个密钥:公开密钥(public key)和私有密钥(private key)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。其最大的优点在于安全性大大提高,原因在于数据接收方的私钥一般处于不公开的状态,黑客在获取数据的时候,无法通过私钥正确解析数据。那么其缺点,相对于对称加密,在于计算量大,加解密时效率较低。 哈希算法 哈希算法是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。哈希算法在 HTTPS 的应用当中起了数据校验和的作用。 数字证书 数字证书是一个经证书授权中心数字签名的包含公开密钥拥有者信息以及公开密钥的文件。它是一种权威性的电子文档,具有极高的安全性和可依赖性。 数字签名 数字签名就是在 HTTPS 验证过程中,用指定的哈希算法将信息进行哈希华后,将所得的值附加在信息后面,用于在数据传输后,方便信息接收端对数据进行校验,确保信息没有被恶意篡改。 过程 图源来自于HTTPS 原理解析 ,这张画的实在太棒了! 客户端首先从发送一个 HTTPS 请求,将自己所支持的加密算法,通知服务器端。 服务器端从客户端发来的加密算法列表中,选出一种加密算法和 HASH 算法,并将其自身的数字证书附加选出的算法一并发回给客户端。而证书中一般包含了网站的地址,公钥,证书失效日期以及证书的颁发机构等等。 客户端在收到服务器端的响应之后,会做一下几件事: 1)验证证书的合法性。一般通过证书的颁发机构是否是合法的、证书是否超过失效日期、证书中所包含的网站地址是否与你正在所访问的相同等方面进行验证。若证书合法,则通过验证,否则将提示用户该证书存在风险。 2)生成随机密码。在证书通过验证,或用户主动信任该证书后,客户端会随机生成一串序列号,并使用服务器端传来的公钥进行加密,并生成握手消息。 3)HASH 算法加密信息。利用服务器端所回传的 HASH 算法将客户端生成的握手信息进行加密,并将加密后的 HASH 值附加上握手消息中,用于数据校验。 服务器端接收到客户端的请求后,同样也会做下面几件事: 1)使用自己的私钥来解密客户端所传来的握手消息,得到客户端生成的随机序列号。在这一部分过程中就运用了非对称加密的技术。 2)使用随机序列号,对握手消息进行 HASH 算法加密,并将获得的 HASH 值与从客户单一并传来的 HASH 值进行对比,查看是否一致。 3)最后,使用该随机序列号,再用公钥加密一段握手消息,并附加上该握手消息的 HASH 值,发回给客户端。 客户端接收到服务器端的请求后,用生成的随机序列号对握手消息进行解密,并对比传来的 HASH 值是否一致。倘若 HASH 值一直,则握手过程正式结束,之后的所有通信将由客户端所生成的随机序列号并利用加密算法对消息进行加密处理。 参考资料 超文本传输安全协议 深度解析HTTPS原理 简单粗暴系列之HTTPS原理 HTTPS 原理解析","categories":[],"tags":[]},{"title":"《深入理解Java虚拟机》读书笔记 - 类加载机制","slug":"《深入理解Java虚拟机》读书笔记 - 类加载机制","date":"2017-11-05T13:19:02.000Z","updated":"2017-11-05T15:17:25.000Z","comments":true,"path":"2017/11/05/《深入理解Java虚拟机》读书笔记 - 类加载机制/","link":"","permalink":"http://apparition957.github.io/2017/11/05/《深入理解Java虚拟机》读书笔记 - 类加载机制/","excerpt":"","text":"此篇为《深入理解Java虚拟机》第七章7.2、7.3、7.4部分的读书笔记 概述虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。 类加载时机类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Loading)、准备(Loading)、解析(Loading)、初始化(Loading)、使用(Loading)、卸载(Loading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如下所示。 注意两点: 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类加载过程必须按照这个顺序进行,而解析阶段因为需要支持 Java语言的运行时绑定,可以在初始化阶段之后开始。 类加载时,这7个阶段虽然必须是要按顺序开始,但是并不要求7个阶段按顺序结束,它们通常以交叉混合式进行的。 类加载过程加载“加载”是“类加载”(Class Loading)过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事情: 通过一个类的全限定名来获取定义此类的二进制字节流。 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。 注意这里不一定非要从 Class 文件中获取,在这个阶段中,既可以从以下几个不同地方获取: 从ZIP 包中读取(JAR、EAR、WAR 格式的包也可以)。 从网络中获取。一般应用场景为RMI。 运行时计算生成。一般应用场景为动态代理。 由其他文件生成。一般应用场景为 JSP 应用。 从数据库中读取。 … 验证验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 验证阶段是非常重要的,这个阶段是否严谨,直接决定了 Java 虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量是在虚拟机的类加载子系统中又占了相当大的一部分。 在验证阶段中,大致会完成下面4个阶段的校验工作: 文件格式验证 元数据验证 字节码验证 符号引用验证 准备准备阶段是正式为类变量分配内存并设置类变量初始化值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段需要注意以下两点: 准备阶段进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。 这个阶段的初始化值,在通常情况下,是数据类型的所对应的零值,假设一个类变量的定义为: 1public static int value = 123; 那变量 value 在准备阶段过后的初始值为0而不是123,真正的赋值操作将延迟到初始化阶段进行。但若上述的类变量 value 的定义变为: 1public static finla value = 123; 那么,在编译时虚拟机将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为123。 解析解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用通常以 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等类型的常量出现。下面将解释符号引用和直接引用的关系: 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。各个虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在于内存中。 初始化类初始化阶段是类加载过程的最后一步,前面的类加载过程之中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其他动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。 初始化阶段是执行类加载器<clinit>()方法的过程。 <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。 为什么能够赋值,但不能访问呢?个人认为在运行期间的准备阶段时,类变量已经经过了零值的初始化了,所以赋值操作是正常进行的,但是在编译期间,编译器认为这种操作是错误的(非法向前引用)。 <clinit>()方式与类的构造函数(或者说是实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是 java.lang.Object。 由于父类的<clinit>()方法优先执行,那么父类中的静态语句块也优先于子类执行。 <clinit>()方法对于类和接口并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口和类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化阶段也一样不会执行接口的<clinit>()方法。 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁,同步,如果多线程同时去初始化一个类,那么只有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。 类加载器虚拟机设计团队把类加载阶段中的加载动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为”类加载器“。 类与类加载器对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在其 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立类名称空间。 双亲委派模型从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由 Java 语言实现的,独立于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader。 从 Java 开发人员的角度来看,绝大部分 Java 程序都会使用以下3种系统提供的类加载器: 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使存放在 lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接饮用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,那直接使用 null 代替即可。 扩展类加载器(Extension ClassLoader):这个类加载器有sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\\lib\\ext,目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用扩展类加载器。 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用应用程序类加载器。 如下图所示,以上的类加载器之间的层次关系,称之为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而都是通过组合关系来复用父类加载器的代码。 类加载的双亲委派模型不是虚拟机中强制性的约束模型,而是 Java 设计者推荐给开发者的一种类加载实现方式。 双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(即他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 采用双亲委派模型的好处在于,Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。无论是哪一个类加载器要加载某个类,最终都是委派给处于模型最顶端的的启动类加载器进行加载,这样保证了 Java 类在程序的各种类加载器环境中都是同一个类。 下面为简单解释一下实现双亲委派模型的关键代码loadClass()。 123456789101112131415161718192021222324252627282930313233343536protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查类是否已经被加载到内存中 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 这一步中运用了双亲委派模型。 if (parent != null) { // 当该类加载器中存在父类加载器,那么就调用其父类加载器来响应加载类的请求 c = parent.loadClass(name, false); } else { // 如果没有父类加载器,那么说明该类加载器为启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // ...其余代码 } } if (resolve) { resolveClass(c); } return c; }} 破坏双亲委派模型 具体操作实现在P231。","categories":[],"tags":[]},{"title":"理解悲观锁与乐观锁","slug":"理解悲观锁与乐观锁","date":"2017-11-05T06:58:26.000Z","updated":"2017-11-06T11:06:02.000Z","comments":true,"path":"2017/11/05/理解悲观锁与乐观锁/","link":"","permalink":"http://apparition957.github.io/2017/11/05/理解悲观锁与乐观锁/","excerpt":"","text":"悲观锁概念维基百科这样解释:在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,PCC)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。 悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。 从上述的解释可以知道,悲观锁是从一个悲观的角度看待用户操作,在用户想要操作数据时,悲观锁会“悲观”地认为其他用户也想要同时对同一数据进行操作,从而要求用户先获取锁,再进行操作,保证了用户操作的安全性。 优缺点优点: 通过“先取锁后访问”的保守策略,为数据处理的安全提供了保证。 缺点: 对数据加锁会让数据库系统产生额外的开销,还增加了死锁的机会。 在只读型事务处理中由于不会产生冲突,使用悲观锁,只会增加系统负载,降低并行性。 应用12select status from goods where id=1 for update; # for update 用于开启排它锁update goods set status=2; 乐观锁概念维基百科这样解释:在关系数据库管理系统中,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,OCC)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自的那部分数据。在提交数据更新之前,每个事务都会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务更新的话,正在提交的事务会进行回滚。 乐观并发控制多用于数据争用不大、冲突较少的环境下,这种环境中,偶尔回滚事务的成本会低于读取数据时锁定数据的成本,因为可以获得比其他并发控制方法更高。 从上述的解释可以知道,相较于悲观锁,乐观锁决定从一个乐观的角度看待用户操作。在用户想要操作数据时,乐观锁会“乐观”地认为其他用户不会同时进行对同一数据进行操作的,而当用户对数据进行再次提交时,乐观锁才会对数据是否被修改进行检测,如果被修改过只能放弃当前操作。 优缺点优点: 乐观并发控制相信事务之间的数据竞争的概率比较小,所以在事务处理过程中不会出现任何锁,或产生死锁现象。 缺点: 在系统并发量大的情况下,事务发生冲突的概率会大大增加,系统可用性会降低,用户体验也会随着操作不断失败而降低。 应用在乐观锁中,一般会采用以下两种机制来记录数据的唯一性: 数据版本(Version)。数据版本,即为数据增加一个版本标识,一般是通过为数据库增加一个数字类型的“version”字段来实现的。当读取数据时,将 version 字段的值一同读出,数据每更新一次,对此 version 的值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的 version 值进行比对,如果数据库当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据。 12select status, version from goods where id=1;update goods set status=2, version=version+1 where id=1 and version=version; 时间戳(timestamp)。使用方式与数据版本相同,只是字段类型为时间戳而已。 顺带一提,Java并发中的 CAS 机制也是乐观锁机制。 参考资料 悲观并发控制 乐观并发控制","categories":[],"tags":[]},{"title":"《深入理解Java虚拟机》读书笔记 - Java内存模型与线程","slug":"《深入理解Java虚拟机》读书笔记 - Java内存模型与线程","date":"2017-11-04T09:24:52.000Z","updated":"2017-11-11T07:43:34.000Z","comments":true,"path":"2017/11/04/《深入理解Java虚拟机》读书笔记 - Java内存模型与线程/","link":"","permalink":"http://apparition957.github.io/2017/11/04/《深入理解Java虚拟机》读书笔记 - Java内存模型与线程/","excerpt":"","text":"此篇为《深入理解Java虚拟机》第十二章12.3、12.4部分的读书笔记 Java 内存模型Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台上都能达到一致的内存访问效果。 定义 Java 内存模型并非一件容易的事情:这个模型必须定义得足够严谨,才能让 Java 的并发内存访问操作不会产生歧义;但是,也必须定义得足够宽松,使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器,高速缓冲和指令集中某些特有的指令)来获得更好的执行速度。 主内存与工作内存Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与 Java 编程所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效率,Java 内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。 Java 内存模型规定了所有的变量都存储在主内存中(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者关系图如下所示。 这部分中与前面笔记中所记录的 Java 内存区域并不是同一层次的内存划分,两者基本上没有什么关系。 内存间互相操作这一部分中, Java 内存模型定义了8种操作来完成主内存与工作内存之间的具体变量交互工作:lock(锁定)、unlock(解锁)、 read(读取)、load(载入)、 use(使用)、assign( 赋值)、 store(存储)、 write( 写入),虚拟机实现时必须保证以上所有操作都是原子的、不可再分的。 具体操作实现在P364。 对于 volatile 型变量的特殊规则关键字 volatile 是 Java 虚拟机所提供的轻量级同步机制。当一个变量定义为 volatile 之后,它将具备两种特性: 保证此变量对所有线程的可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递需要通过主内存来完成。例如:线程 A 修改一个普通变量的值,然后向主内存进行回写,另外一条线程 B 在线程 A 回写完成了之后再从主内存进行读取操作,新变量值才会对线程 B 可见。 由于 volatile 变量只能保证可见性,而无法保证操作的原子性。在不符合以下两条规则的运算场景下,我们仍然需要加锁机制来保证操作原子性: 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程来修改变量的值 变量不需要与其他的状态变量共同参与不变约束。 禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作顺序与代码执行顺序一致。 在于 volatile 的性能提升方面,可得出:volatile 变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏蔽指令保证处理器不发生乱序执行。不过即使如此,大多数场景下 volatile 的总开销仍然要比锁低,我们在 volatile 与锁之间的选择依据只取决于 volatile 是否能够满足当前场景所需的并发要求。 先行发生原则如果 Java 内存模型中所有的有序性都仅仅依靠 volatile 和 synchronized 来完成,那么有一些操作将会变得很烦琐,但是在实际编程当中,Java 语言内部中存在一种“先行发生”(happens-before)的原则来保证代码在正常情况下的并发处理。 先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先行于操作 B,其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,“影响”包括了修改了内存中共享变量的值、发送了消息、调用了方法等。 下面是 Java 内存模型中一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码,因为需要考虑分支、循环等结构。 管程锁定规则(Monitor Lock Rule): 一个 unlock 操作先行发生于后面(特指时间顺序)对同一个锁的 lock 操作。 volatile 变量规则(Volatile Variable Rule): 对一个 volatile 变量的写操作先行发生于后面(特指时间顺序)对这个变量的读操作。 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。 线程终止规则(Thread Termination Rule): 线程中的所有操作都先行发生于对此线程的终止检测。 线程中断规则(Thread Interruption Rule): 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 对象终结规则(Finalizer Rule): 一个对象的初始化完成(构造函数的结束)先行发生于它的 finalize() 方法的开始。 传递性( Transitivity): 如果操作 A 先行发生于操作 B,而操作 B 先行发生于操作 C,那就可以推导出操作 A 先行发生于操作 C 的结论。 Java 与线程线程创建&线程调度 P379,与操作系统创建线程相同,因为 Java 线程创建与线程调度的操作就是基于操作系统的。 状态转换 新建(New):创建后尚未未启动的线程。 运行(Runable):线程有可能正在运行,或也有可能正在等待CPU为它分配执行时间。 无限期等待(Waiting):不会被分配CPU执行时间,要等待被其他线程显式唤醒,以下方法会让线程处于无限期的等待状态: 没有设置 Timeout 参数的 Object.wait() 方法。 没有设置 Timeout 参数的 Thread.join() 方法。 LockSupport.park() 方法。 限期等待(Timed Waiting):不会被分配CPU执行时间,不需要等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒,以下方法会让线程处于限期的等待状态: Thread.sleep() 方法。 设置了 Timeout 参数的Object.wait() 方法。 设置了 Timeout 参数的Thread.join() 方法。 LockSupport.parkNanos() 方法。 LockSupport.parkUntil() 方法。 阻塞(Blocked):线程被阻塞了,在等待获取一个排它锁。例如线程A和B在执行同步方法C时,线程A先拿到排它锁,那么线程B的状态就是阻塞状态,等待线程B释放排它锁。 结束(Terminated):线程执行完毕。","categories":[],"tags":[]},{"title":"《深入理解Java虚拟机》读书笔记 - 垃圾回收机制","slug":"《深入理解Java虚拟机》读书笔记 - 垃圾回收机制","date":"2017-11-03T11:08:31.000Z","updated":"2017-11-05T11:51:05.000Z","comments":true,"path":"2017/11/03/《深入理解Java虚拟机》读书笔记 - 垃圾回收机制/","link":"","permalink":"http://apparition957.github.io/2017/11/03/《深入理解Java虚拟机》读书笔记 - 垃圾回收机制/","excerpt":"","text":"此篇为《深入理解Java虚拟机》第三章3.1、3.2、3.3部分的读书笔记 概述在垃圾收集(Garbage Collection,GC)中,我们需要考虑以下三个问题: 哪些内存需要回收? 什么时候回收? 如何回收? 经过半个多世纪的发展,目前内存的动态分配与内存回收技术已经相当成熟,一切看起来都进入“自动化”时代,那为什么我们还要去了解 GC 和内存分配呢?答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。 对象已死吗在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行垃圾回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。 引用计数算法引用计数算法是指:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 客观地说,引用计数算法(Reference Counting)的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,但是,至少在主流的 Java 虚拟机里面没有选用引用计数拳算法来管理内存,其中主要的原因是它很难解决对象之间互相循环引用的问题。 举个例子,若 Java 堆上同时存在 objA 和 objB 两个对象,两个对象中都有字段 instance,赋值令 objA.instance = objB 以及 objB.instance = objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数不为0,于是引用计数算法无法通知 GC 收集器来回收它们。 可达性分析算法可达性分析算法是指:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象 object5、object6、object7虽然互相有关联,但是它们到 GC Roots 是不可达的,所以它们将会被判定为是可回收对象。 在 Java 语言中,可作为 GC Roots 的对象包括下面几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象 再谈引用无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是都存活都与“引用”有关。对于对象“引用”的准确定义,希望通过这种方式来描述:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集之后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。 在 JDK1.2之后,Java 对引用的概念进行了补充,将引用以下四类,并且这四种引用强度自上到下依次减弱。 强引用(Strong Reference)。强引用就是指在程序之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 软引用(Soft Reference)。软引用是用来描述一些还有用但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出的异常。 弱引用(Weak Reference)。弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 虚引用(Phantom Reference)。虚引用也称为幽灵引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 生存还是死亡即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析之后,发现没有与 GC Roots 相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两次情况都视为“没有必要执行”。 如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这个所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalize() 方法中执行缓慢,或者发生了死循环,将很可能会导致 F-Queue 队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。 finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 会对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的就被回收了。 任何一个对象的 finalize() 方法都只会被系统自动调用一次,如果面临第二次回收,那么它的 finalize()方法将不会被调用。 对于 finalize() 方法,应该尽可能避免使用,因为这个 Java 在较早之前对于 C/C++程序员的妥协,最主要的原因是它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。所以了解它的概念,在实际使用上忽略它的存在即可。 垃圾收集算法标记-清除算法最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为“标记”和“清除“两个阶段:首先标记处所有需要回收的对象,在标记完成后,统一回收所有被标记的对象(标记过程参考上一小节)。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。 它的主要不足有两个: 一个是效率问题,标记和清除两个过程的效率并不高 一个是空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。复制算法 为了解决效率问题,一种称为”复制“(Copying)的收集算法出现了,他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,分配可能过高了。 按照 IBM 的研究表明,新生代中的对象98%都是”朝生夕死“的。所以内存分配的常见做法是:将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。 在 HotSpot 虚拟机中,默认 Eden 和 Survivor 的大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被”浪费“。在 GC 回收过程中,若未使用的那块 Survivor 空间不够时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)(具体规则取决于垃圾收集器)。 标记-整理算法复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 根据老年代的特点,顺应有了”标记-整理“算法,其过程与”标记-清除“算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 分代收集算法当前商业虚拟机的垃圾收集都采用”分代收集“(Generational Collection)算法,这种算法是根据对象的存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本 在老年代中,因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用”标记-清理“或”标记-整理“算法来进行回收 GC日志中的术语 Minor GC 指发生在新生代的垃圾收集动作,非常频繁,速度较快。 Major GC(通常与 Full GC 是等价de )指发生发生在整个 GC 堆中的垃圾收集动作,频次较少,一般由多次 Minor GC 后,内存空间仍不满足程序运行时调用。","categories":[],"tags":[]},{"title":"《深入理解Java虚拟机》读书笔记 - Java对象","slug":"《深入理解Java虚拟机》读书笔记 - Java对象","date":"2017-11-02T05:34:34.000Z","updated":"2017-11-02T05:44:09.000Z","comments":true,"path":"2017/11/02/《深入理解Java虚拟机》读书笔记 - Java对象/","link":"","permalink":"http://apparition957.github.io/2017/11/02/《深入理解Java虚拟机》读书笔记 - Java对象/","excerpt":"","text":"此篇为《深入理解Java虚拟机》第二章2.3部分的读书笔记 对象的创建Java 是一门面向对象的编程语言,在 Java 程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常仅仅是一个 new 关键字而已,而在虚拟机中,对象(仅限于普通 Java 对象,不包括数组和 Class 对象等)的创建又是怎样一个过程呢? 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。具体堆的内存如何划分以及怎么分配堆中的内存者这取决于虚拟机所采用的垃圾收集器是否带有压缩整理功能决定。 除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针分配内存的情况。解决这个问题有两种方案: 一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用 CAS + 失败重试的方式来保证更新操作的原子性; 另一种把内存分配动作按照线程划分在不同的空间中进行,即每个线程在 Java 堆中预先分配一小块内存,称之为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程需要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定。 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用了 TLAB,这一工作过程也可以提前至 TLAB 分配时进行。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息都存放在对象的对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。所以,一般来说,执行 new 指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 对象的内存布局在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为3个区域:对象头(Header)、实例数据(Instance Data)和对其补充(Padding)。 对象头。对象头可以分为以下两部分信息: 用于存储对象自身运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。另外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息来确定 Java 对象的大小,但是从数据的元数据中却无法确定数组的大小。 实例数据 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。 对齐补充 对齐补充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。 对象的访问定位建立对象是为了使用对象,我们的 Java 程序需要通过栈上的 renference 数据来操作堆上的具体对象。由于 reference 类型在 Java 虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问的方式也是取决于虚拟机实现而决定的。目前主流的访问方式有使用句柄和直接指针两种。 如果使用句柄访问的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。其最大好处在于 reference 中存储的是稳定的句柄地址,在对象移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 refernece 本身不需要修改。 如果使用直接指针访问(HotSpot 采取方式),那么 Java 堆对象的布局就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址。其最大好处在于速度相较于句柄访问更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。","categories":[],"tags":[]},{"title":"PriorityQueue 源码分析","slug":"PriorityQueue-源码分析","date":"2017-11-01T08:58:30.000Z","updated":"2017-11-01T09:00:10.000Z","comments":true,"path":"2017/11/01/PriorityQueue-源码分析/","link":"","permalink":"http://apparition957.github.io/2017/11/01/PriorityQueue-源码分析/","excerpt":"","text":"结构体系12public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable { PriorityQueue是通过最小堆(?)实现内部元素按一定顺序的队列,也称其为优先队列。从结构体系上看,PriorityQueue是继承自AbstractQueue的,即PriorityQueue实现了基本的队列的操作。但为何PriorityQueue能够实现元素按指定排序存在队列呢,那么我们应该看它的成员变量部分。 若忘了最大/小堆的概念,可以查看这篇文章堆排序(Heap Sort) 常量与重要的成员变量12345678910111213141516171819// 默认容器初始大小private static final int DEFAULT_INITIAL_CAPACITY = 11;// 容器最大大小private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;// PriorityQueue 真实操作容器(知道最大/小堆性质的,应该不难明白)transient Object[] queue;// 记录容器内部实际元素个数private int size = 0;/** * The comparator, or null if priority queue uses elements' * natural ordering. */// 上述注释表明comparator若为空时,即使用自然递增的顺序存储元素// 那么既然给出了这个comparator,就说明了该comparator可以由用户给定private final Comparator<? super E> comparator; 构造函数1234567891011121314151617181920public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null);}public PriorityQueue(int initialCapacity) { this(initialCapacity, null);}public PriorityQueue(Comparator<? super E> comparator) { this(DEFAULT_INITIAL_CAPACITY, comparator);}// 关键构造函数,这一步证实了可以由用户传递自定义的comparator来实现自定义顺序容器public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator;} 增加操作 —— add()1234567891011121314151617181920public boolean add(E e) { return offer(e);}public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; // 保证容器能够存储所有元素 if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else // 实际操作部分 siftUp(i, e); return true;} 关键部分如下所示: 12345678910111213141516171819202122232425262728293031323334353637383940// 这一步就是PriorityQueue的关键部分private void siftUp(int k, E x) { // 若comparator不为空,则使用用户给定的comparator,否则则使用元素本身提供的比较器 if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x);}@SuppressWarnings(\"unchecked\")// comparator为空时调用private void siftUpComparable(int k, E x) { // 获取元素本身,并转化为可Comparable类型 Comparable<? super E> key = (Comparable<? super E>) x; // 若k>0,即k未处于根元素位置 while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (key.compareTo((E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = key;}@SuppressWarnings(\"unchecked\")// comparator不为空时调用private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; // 使用用户给定的comparator if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x;} 删除操作 - poll()123456789101112public E poll() { if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) siftDown(0, x); return result;} 与add()同理。 12345678910111213141516171819202122232425262728293031323334353637383940414243private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x);}@SuppressWarnings(\"unchecked\")private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) c = queue[child = right]; if (key.compareTo((E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = key;}@SuppressWarnings(\"unchecked\")private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x;} 查找操作 - peek()1234// 时间复杂度O(1)public E peek() { return (size == 0) ? null : (E) queue[0];}","categories":[],"tags":[]},{"title":"《深入理解Java虚拟机》读书笔记 - Java内存分配","slug":"《深入理解Java虚拟机》读书笔记 - Java内存分配","date":"2017-11-01T05:57:11.000Z","updated":"2017-11-04T09:27:40.000Z","comments":true,"path":"2017/11/01/《深入理解Java虚拟机》读书笔记 - Java内存分配/","link":"","permalink":"http://apparition957.github.io/2017/11/01/《深入理解Java虚拟机》读书笔记 - Java内存分配/","excerpt":"","text":"此篇为《深入理解Java虚拟机》第二章2.2部分的读书笔记 概述对于 Java 程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个 new 操作去写配对的 delete/free 代码,不容易出现内存泄漏和内存溢出问题,由于虚拟机管理内存这一切看起来很美好,不过,也正是因为 Java 程序员将内存控制的权利交给了 Java 虚拟机,一旦出现内存泄漏和溢出问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将是一件异常艰难的工作。 运行时数据区域Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java 虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示。 程序计数器程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令(类似于操作系统一般),分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)(因为 Native 所修饰的方法是虚拟机根据当前系统,调用本地应用/库实现的,不归属于任何字节码指令)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。 Java 虚拟机栈与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。 其中64位长度的 long 和 doule 类型的数据会占用2个局部变量空间(slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 在 Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。 本地方法栈本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中,对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。 Java 堆对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。但随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。 Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC 堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等;从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。 根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。 方法区方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集。相对来言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如同永生代的名字一样“永远”存在了。 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”比较难令人满意(类似于操作系统中页面的换入换出算法),尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。 根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。 运行时常量池运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法等描述信息外,还有一项信息就是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。 Java 虚拟机对 Class 文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。 运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译器才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,类似于 String 类的 intern() 方法。 直接内存直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也会被频繁地使用。 在 JDK1.4中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区( Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 队中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受本机总内存大小以及处理器寻址空间的限制。","categories":[],"tags":[]},{"title":"Java抽象类与接口","slug":"Java抽象类与接口","date":"2017-10-31T12:41:15.000Z","updated":"2017-10-31T12:48:09.000Z","comments":true,"path":"2017/10/31/Java抽象类与接口/","link":"","permalink":"http://apparition957.github.io/2017/10/31/Java抽象类与接口/","excerpt":"","text":"概念抽象类是用来捕捉子类的通用特性的。它不能被实例化,只能被用作子类的超类。可以将抽象类当做是被用来创建继承层级里子类的模板。 接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的所有抽象方法,并需要确保这些方法全部实现,这就像是契约模式一般。接口只是一种形式,接口自身不能做任何事情。 在Java中,一个类只能够继承一个抽象类,但是一个类可以同时实现多个接口。 设计思想从概念上,可知抽象类与接口所设计的目的是不一样的 —— 接口是对动作的抽象,而抽象类是对根源的抽象。 举个例子而言,在这个世界上存在很多不同的车,跑车、轿车、货车等,那么对于抽象类而言,我们就需要从这些车中提取它们的公共部分,设计出一个更高级别的抽象类——四轮车。 但轿车与货车同为四轮车,但是普遍轿车都存在着自动驾驶功能,如果货车需要自动驾驶功能的话,那么对于接口而言,可以把自动驾驶功能所需的这些方法抽象成接口——自动驾驶。 如何选择从程序设计中,如何选择抽象类与接口?从我的观点而言,在程序设计中我们需要考虑的是高度抽象化以及程序可扩展性。 因为在Java中只能够继承一个父类,所以定义抽象类的代价比较高。即在程序设计中,需要从自下至上,综合分析所有子类中的共同点,高度抽象化成父类(这里可以联想Object,即使不是抽象类,但它却由始至终贯穿整个java体系)。 但相比于抽象类而言,接口所付出的代价相对的低很多,可扩展性也大大提高了,因为类可以实现多个接口,因此每个接口你只需要将特定的动作抽象到这个接口即可。 举个例子,如LinkedList的继承体系一般。 123public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable 抽象类的角度。LinkedList继承自AbstractSequentialList,这说明了LinkedList属于链表这种数据结构的继承体系中,它继承了所有链表的基本操作。 接口的角度。LinkedList实现了Deque接口,这表明了即使是链表,也能够实现双端队列的功能,使程序调用者可以将LinkedList同等视为队列使用。 参考资料 接口和抽象类有什么区别? Java抽象类与接口的区别","categories":[],"tags":[]},{"title":"Java关键字 - final","slug":"Java关键字-final","date":"2017-10-31T12:02:14.000Z","updated":"2017-10-31T12:02:20.000Z","comments":true,"path":"2017/10/31/Java关键字-final/","link":"","permalink":"http://apparition957.github.io/2017/10/31/Java关键字-final/","excerpt":"","text":"概念在Java中,我们可以通过final来表示某个变量、某个方法,甚至是某个类是“不变的”或“无法改变的”。 怎么使用finalfinal变量final修饰的变量称之为常量,其主要应用于以下地方: 编译器常量,永远不可改变。 运行其初始化时,我们希望它不会被改变。 对于编译器常量,它在类加载的过程就已经完成了初始化,所以当类加载完成后是不可改变的。而对于运行时变量,也称为空白final,即代表先声明,后赋值这一过程。 运行时变量可分为基本数据类型与引用数据类型,其中基本数据类型不可变的是内容,而引用数据类型的不可变的是引用,引用所指的对象内容是可变的。 1234567891011public class Test { private final int SIZE; private final String test = \"final can't change\"; public Test(int size) { // 运行期变量,可以根据传递的值来声明不同的常量 this.SIZE = size; // 编译器报错,因为test被final修饰,其在编译期间已确定的值,所以不可改变 this.test = \"final??\"; }} final方法被final修饰的方法都是不能被继承、更改的。 12345678910111213141516public class Father { public final void hello() { System.out.println(\"father can't change\"); } public static void main(String[] args) { new Son().hello(); }}class Son extends Father { // 编译器报错,因为Father类中的hello()被final修饰,所以子类是不能够重写的 public void hello() { System.out.println(\"hhello\"); }} final参数被final修饰的参数都是不可变的,即在函数作用域内,该参数的值都是不可变的。 12345public void hello(final String str) { // 编译出错 str = \"world\"; System.out.println(str);} final类被final修饰的类是不允许被继承的,所以可视该类为最终类。 12345678public final class Father { }// 编译出错,Father类不可被继承class Son extends Father { } final能提高性能吗?基于JVM对于声明为final的局部变量(local var)做了哪些性能优化?与在Java中使用final关键字会提高性能吗?两文,可以归纳总结出final关键字并不会从性能上有很大的提升,甚至可以说是没有。反而要求设计者在程序设计中,不要过分追求性能,需要注重的是代码的可读性与可维护性。 参考资料 java提高篇(十五)—–关键字final","categories":[],"tags":[]},{"title":"Java关键字 - static","slug":"Java关键字-static","date":"2017-10-31T07:11:17.000Z","updated":"2017-10-31T07:11:31.000Z","comments":true,"path":"2017/10/31/Java关键字-static/","link":"","permalink":"http://apparition957.github.io/2017/10/31/Java关键字-static/","excerpt":"","text":"概念在Java中,我们可以通过用static来表示某个字段或者方法为“全局“或者”静态“的意思,当然static也可以修饰代码块。 怎么使用staticstatic可以用于修饰成员变量和成员方法,我们将其称为静态变量和静态方法,可以直接通过类名进行访问。如下所示: ClassName.propertyName ClassName.methodName() 而static修饰的代码块也称之为静态代码块,当其所属的类被加载时,就会优先先执行这部分代码。 static变量static修饰的变量称之为静态变量,而没有static修饰的变量称之为实例变量。两者的区别在于: 静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且JVM也只会为它分配一次内存,同时类所有的实例都共享静态变量,可以直接通过类名来访问。 但是实例变量则不同,他是伴随着实例的创建而创建,也伴随着实例的消亡而消亡。而且实例变量只能够通过对象进行访问。 static函数static修饰的方法称之为静态方法,可以直接通过类名对其进行调用。正因为static修饰的函数在类加载的时候就已经存在了,它不依赖于任何实例,所以static方法必须被实现,也就是说它不能够同时与abstract搭配修饰函数。 static方法是类中的一种特殊方法,当我们需要这个类下的某个方法完成某个特定的目的而无须将其实例化时,才将这个方法修饰为static。如Math类下的所有方法都是静态static的。 static代码块static修饰的代码块称之为静态代码块,它会随着类加载的时候一块执行。静态代码块可以放置类中任意地方,且在类加载时,静态代码块按从下到上的顺序依次执行。 使用注意static修饰的方法不能够调用非static变量或者非static方法 static所修饰的方法是从属于类的,且被由该类实例化出的所有对象所共享。若在static方法中调用了非static变量或非static方法,那么在运行期间,程序则无法确定此时的所调用的非static变量的确切值(因为成员变量的实际值取决于其所属的对象),从而导致程序运行错误。 静态代码块与非静态代码块的初始化顺序优先级结论:静态代码块 == 静态变量初始化 > 实例代码块 == 实例变量初始化 > 实例构造器。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647package util_test;public class Main { { System.out.println(\"main instance block\"); } static { System.out.println(\"main static block\"); } public Main() { System.out.println(\"main instance constructor\"); } public static void main(String[] args) { new Son(); }}class Father { { System.out.println(\"father instance block\"); } static{ System.out.println(\"father static block\"); } public Father() { System.out.println(\"father instance constructor\"); }}class Son extends Father { { System.out.println(\"son instance block\"); } static { System.out.println(\"son static block\"); } public Son() { System.out.println(\"son instance constructor\"); }} 正确的执行顺序如下所示: 1234567main static blockfather static blockson static blockfather instance blockfather instance constructorson instance blockson instance constructor 分析上述的代码加载过程。 因为main方法在Main类中,所以首先加载Main类,因此会执行Main类中的static代码块。接着执行main方法中的new Son()语句,而此时Son类尚未被加载,因此此时需要加载Son类。在加载Son类的时候,发现Son类是继承自Father类,所以转而去优先加载Father类,所以Father类中的static代码块会被执行,然后再执行Son类中的static代码块。 在所有所需的类加载完毕后,就通过构造器来生成对象。而在生成对象的时候,必须先初始化父类的成员变量以及执行实例代码块。因此先执行Father类中的实例代码块与构造器,最后再执行Son类中实例代码块与构造器。 参考资料 Java中的static关键字解析 java提高篇(七)—–关键字static","categories":[],"tags":[]},{"title":"堆排序(Heap Sort)","slug":"堆排序(Heap Sort)","date":"2017-10-30T17:02:22.000Z","updated":"2017-10-31T12:55:27.000Z","comments":true,"path":"2017/10/31/堆排序(Heap Sort)/","link":"","permalink":"http://apparition957.github.io/2017/10/31/堆排序(Heap Sort)/","excerpt":"","text":"概述堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。 什么是堆堆的实现通过构造二叉堆,实为二叉树的一种分支。这种数据结构具有以下性质: 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。 堆总是一个完全二叉树,即除了最底层外,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。 将根节点最大的堆叫做最大堆,而将根节点最小的堆叫做最小堆。 堆排序中如何表示堆由于堆总是一个完全二叉树这一特性,这使得堆可以利用数组来表示,如下图所示。 对于给定的某个节点的下标i,可以很容易的计算出这个节点的父节点、左右孩子节点的下标: Parent(i) = floor((i - 1) / 2),i节点的父节点下标 Left(i) = 2i * 1,i节点的左子节点下标 Right(i) = 2 * (i - 1),i节点的右子节点下标 堆排序原理(以最大堆为例)堆排序就是把最大堆的堆顶取出,将剩余的堆继续调整为最大堆,再将堆顶的数值取出,不断重复这一过程,直至堆中只剩下一个节点为止。堆中定义以下几种操作: 最大堆调整(MaxHeapify):将堆的末端子节点作调整,使得子节点永远小于父节点 创建最大堆(BuildMaxHeap):将堆所有数据重新排序 推排序(HeapSort):移除在第一个数据的根节点,bin并做最大堆调整的递归运算 最大堆调整(MaxHeapify)最大堆调整的作用是保持最大堆的性质,是整个算法的核心部分。它内部的算法其实决定了堆到底是最大堆还是最小堆。 代码部分如下所示: 123456789101112131415161718192021222324252627282930// array代表需要排序的数组部分public void maxHeapAdjust(int index, int heapSize) { int largest = index; // 左右子节点在数组中的位置 int left = 2 * index + 1; int right = 2 * (index + 1); // 若左子节点大于父节点 if (left < heapSize && array[index] < array[left]) { largest = left; } // 若右子节点大于父节点 if (right < heapSize && array[largest] < array[right]) { largest = right; } // 倘若larget并非指向原节点index时,则证明父节点index小于某个子节点left/right if (largest != index) { swap(largest, index); // 因为largest并非index,所以节点largest的堆结构也发生了变化 maxHeapAdjust(largest, heapSize); }}private void swap(int i, int j) { int tmp = queue[i]; queue[i] = queue[j]; queue[j] = tmp;} 非递归实现版本 123456789101112131415161718192021222324252627public void maxHeapAdjustWhile(int index, int heapSize) { int largest, left, right; while (true) { largest = index; left = 2 * index + 1; right = 2 * (index + 1); // 若左子节点大于父节点 if (left < heapSize && queue[index] < queue[left]) { largest = left; } // 若右子节点大于父节点 if (right < heapSize && queue[largest] < queue[right]) { largest = right; } // 倘若larget并非指向原节点index时,则证明父节点index小于某个子节点left/right if (largest != index) { swap(largest, index); maxHeapAdjustWhile(largest, heapSize); } else { break; } }} 创建最大堆(BuildMaxHeap)创建最大堆的作用是将一个数组转换为一个最大堆。倘若堆中有n个元素,那么BuildMaxHeap就从Parent(n)开始(因为Parent(n)的节点刚刚好指向最后一个元素的父节点),从下往上地调用MaxHeapify。 代码结构如下所示: 1234567public void buildMaxHeap(int heapSize) { int parent = (heapSize - 1) / 2; // 可以参考图思考一下,为什么这样循环递减i for (int i = parent; i >= 0; i--) { maxHeapAdjust(i, heapSize); }} 推排序(HeapSort)堆排序是堆排序算法的接口算法部分,HeapSort先调用BuildMaxHeap将传递来的数组转换为最大堆,然后将最大堆堆顶元素与堆底最后一个元素对换,然后再重新调用MaxHeapify来保证最大堆的性质。 代码结构如下所示: 123456789101112public int[] maxHeapSort() { int heapSize = queue.length; buildMaxHeap(heapSize); for (int i = heapSize - 1; i > 0; i--) { // 交换堆顶的最大值放置数组末尾 swap(0, i); // 重新整理最大堆,范围缩小至除已排序的节点外 maxHeapAdjust(0, i); } return queue;} 时间与空间复杂度最优时间复杂度为O(nlogn),最坏时间复杂度O(nlogn)。 总空间复杂度为O(n),辅助空间复杂度O(1)。 参考资料 堆排序 堆 (数据结构)) 常见排序算法 - 堆排序 (Heap Sort)","categories":[],"tags":[]},{"title":"Java异常机制分析","slug":"Java异常机制分析","date":"2017-10-30T11:29:40.000Z","updated":"2017-10-30T11:30:37.000Z","comments":true,"path":"2017/10/30/Java异常机制分析/","link":"","permalink":"http://apparition957.github.io/2017/10/30/Java异常机制分析/","excerpt":"","text":"概念异常是指程序在运行期间所发生的错误,如使用了空指针、栈溢出、非法参数等。在程序编写期间,编译器会自动检查代码是否符合规范,并尽可能地帮助程序员将其纠正。但即使是看似正确的代码,也可能会在运行期间抛出一个意想不到的异常。 Java为此提供了异常处理机制,即在程序运行期间,倘若抛出了异常,则可以以适当的方式进行捕获处理,使得程序能够正常的运作下去。 体系结构 在Java中所有的异常类都是从java.lang.Throwable类集成的子类。 根类Throwable下(仅)有两个重要的子类——Error与Exception。 Error代表运行期间JVM(Java虚拟机)出现的异常,这种异常一般来说是无法处理的。 Exception代表运行期间程序本身的逻辑出现的异常,这种异常一般是程序本身可以处理的。 其中,Exception可分为两类:运行时异常和检查异常。 检查异常(CheckedException),是指程序在执行某段代码时,是可以提前知道这段代码是存在潜在异常的,而且要求程序必须以某种方式来处理。若不处理这种异常情况时,编译器是不会通过编译的。 运行时异常(RuntimeException),也称为非检查异常,是指程序在运行期间可能会抛出异常,但不要求程序必须处理该异常。在编译期间,编译器也不会要求用户去处理它。 TRY-CATCH会不会性能消耗当初的自己觉得如果在try-catch块中大量使用循环的话,想当然的认为会消耗大量的性能。但是通过阅读多篇文章后,得出以下结论: 异常如果没发生,也就不会去查异常表,也就是说你写不写try-catch,也就是有没有这个异常表的问题,如果没有发生异常,写try-catch对性能是木有消耗的,所以不会让程序跑得更慢。 try-catch 的范围大小其实就是异常表中两个值(开始地址和结束地址)的差异而已,也是不会影响性能的。 具体文章如下所示: Try-Catch真的会影响程序性能吗 Java上的try catch并不影响性能(转) 优化建议优化建议这一部分是结合了Java异常处理和设计和异常处理的 15 个处理原则两文的精华,小弟只能做个低调的搬运工。 只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程 即使在上述的说到异常机制不会怎么消耗性能,但这并不代表能够在程序中随处使用try-catch。要在程序中谨慎地使用异常,倘若异常使用过多仍然会很大程度上影响程序的性能。如果在程序中能够用if语句来进行逻辑判断,自然能更清楚地表明出当某个字段处于某个阶段时要进行的逻辑,也可以减少异常的使用,从而避免不必要的异常捕获和处理。 切忌使用空catch块 倘若程序在捕获了异常之后什么都不做,相当于你直接隐藏了这个异常,这可能会导致后面的程序逻辑出现不可控的执行结果,这是一种相当不负责任的行为。倘若有这种情况发生,不如改变程序本身的代码逻辑,使其变得更加健壮,并用日志的方式记录其异常的状态,方便日后的更新和维护。 检查异常与非检查异常的选择 当你决定要抛出一个自己新定义的异常,你就要决定以什么形式来处理这个异常。 当有些检查异常对开发人员来说是无法通过合理的手段处理的,例如SQLException,这样就会导致在代码中经常出现的一种情况:逻辑代码很少几行,但是要进行异常捕获和异常处理的代码却有很多行,这会导致逻辑代码阅读起来晦涩难懂,使得代码难以维护。 在检查异常与非检查异常的选择上面,如果存在该异常情况的出现很普遍,需要特别提醒调用者注意处理的话,就是用检查异常,否则就使用非检查异常。 注意catch块的顺序 切忌将捕获父类异常的catch块放置于捕获子类异常catch块前,否则将永远无法到达程序理想的异常处理逻辑状态中。 1234567891011121314try { FileInputStream inputStream = new FileInputStream(\"d:/a.txt\"); int ch = inputStream.read(); System.out.println(\"aaa\"); return \"step1\";} catch (Exception e) { System.out.println(\"io exception\"); return \"step2\";} catch (FileNotFoundException e) { // 永远到不了这一步,因为catch块是从上到下优先匹配到符合该异常类及其父类 // 由于Exception为FileNotFoundException的父类,所以catch块将在第一次匹配中结束 System.out.println(\"file not found\"); return \"step3\";} 避免多次在日志信息中记录同一异常 很多情况下异常都是层层向上抛出,如果在每次向上抛出异常的时候,都记录到日志中,则会导致冗余的异常重复记录在日志中,不仅大量浪费空间,而且很难查找到异常的根源。 妥当的做法是只在异常最开始发生的地方进行日志信息记录。 在finally中释放资源 如果在程序中存在文件读取、网络操作以及数据库操作等,需要在finally块中释放资源。这样不仅使得程序占用的资源更少,也会避免由于资源未及时释放而导致的异常情况。 不要在finally中使用return语句 倘若在正常try块中返回值,又或者是,在捕获异常后打算在catch块中返回值的话,切忌在finally块中再返回值,否则finally的返回值将直接取代catch块中的返回值。这不难想象,因为finally块在try-catch执行完后一定会执行的,所以finally中的操作将会正常执行。 12345678910111213try { FileInputStream inputStream = new FileInputStream(\"d:/a.txt\"); int ch = inputStream.read(); System.out.println(\"aaa\"); return \"step1\";} catch (Exception e) { System.out.println(\"io exception\"); return \"step2\";} finally { System.out.println(\"finally end\"); // 程序执行到这,会导致最终的返回值是\"step3\",而非\"step1\",也不会是\"step1\" return \"step3\";} 当方法判断出错该返回时应该抛出异常,而不是返回一些错误值 因为错误值在程序逻辑中可能会出现难以理解的情况,并且错误值在描述异常的情况并不直观。在文件找不到的时候,应当抛出类似 FileNotFoundException 异常,而不是返回 -1 或者 -2 之类的错误值。 参考资料 Java异常处理和设计 异常处理的 15 个处理原则","categories":[],"tags":[]},{"title":"ArrayList与LinkedList的循环效率对比","slug":"ArrayList与LinkedList的循环效率对比","date":"2017-10-28T17:06:14.000Z","updated":"2017-10-28T17:08:14.000Z","comments":true,"path":"2017/10/29/ArrayList与LinkedList的循环效率对比/","link":"","permalink":"http://apparition957.github.io/2017/10/29/ArrayList与LinkedList的循环效率对比/","excerpt":"","text":"问题来源for循环遍历存在这两种方式,如下所示: 12for (int i = 0; i < objects.length; i++);for (Object object: objects); ArrayList与LinkedList两种集合在两种方式的遍历存在着较大的性能差距,下面将以以下测试代码作为范例解释: 123456789101112131415161718192021222324252627282930public class ForTest { private static final int NUM_SIZE = 2000000; public static void testList(List<Integer> list) { long start = System.currentTimeMillis(); for (int i = 0; i < list.size(); i++) { list.get(i); } System.out.println(list.getClass().getSimpleName() + \"-普通for-花费时间: \" + (System.currentTimeMillis() - start) + \"ms\"); start = System.currentTimeMillis(); for (Integer num: list) { } System.out.println(list.getClass().getSimpleName() + \"-foreach-花费时间: \" + (System.currentTimeMillis() - start) + \"ms\"); } public static void main(String[] args) { List<Integer> arrayNums = new ArrayList<>(); List<Integer> linkNums = new LinkedList<>(); for (int i = 0; i < NUM_SIZE; i++) { arrayNums.add(i); linkNums.add(i); } testList(arrayNums); testList(linkNums); }} 测试运行结果可得: 1234ArrayList-普通for-花费时间: 5msArrayList-foreach-花费时间: 7msLinkedList-普通for-花费时间: 32459msLinkedList-foreach-花费时间: 9ms 从上述的结果中,可以看出ArrayList在普通for循环上更胜一筹,而在LinkedList在foreach上效率极高。倘若数据量不断上升,那么这个差距只会不断加大。 那么为什么会造成上述情况发生呢? 问题解释问题可以从两者的结构体系上分析。 123456public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable 在两者对比中,我们可以发现ArrayList中实现了RandomAccess接口,而LinkedList没有实现。仔细查看RandomAccess的注释解释,如果实现该接口的List,在普通for循环遍历的效率上会快于foreach循环。 123456789101112131415161718192021222324252627282930313233343536373839404142434445/** * Marker interface used by <tt>List</tt> implementations to indicate that * they support fast (generally constant time) random access. The primary * purpose of this interface is to allow generic algorithms to alter their * behavior to provide good performance when applied to either random or * sequential access lists. * List实现所使用的标记接口,用来表明实现了这个接口的list支持快速随机访问(通常在常数时 * 间内)。这个接口主要目的在于允许一般的算法更改它们的行为,以便在随机或者顺序访问list * 时有更好的性能。 * * <p>The best algorithms for manipulating random access lists (such as * <tt>ArrayList</tt>) can produce quadratic behavior when applied to * sequential access lists (such as <tt>LinkedList</tt>). Generic list * algorithms are encouraged to check whether the given list is an * <tt>instanceof</tt> this interface before applying an algorithm that would * provide poor performance if it were applied to a sequential access list, * and to alter their behavior if necessary to guarantee acceptable * performance. * 操作随机访问列表(如ArrayList)的最佳算法在顺序访问列表(如LinkedList)上应用,会产 * 生歧义行为。泛型列表的算法鼓励在将某个算法应用于顺序访问列表可能产生较差的性能之前, * 检查给定的列表是不是这个接口的实现,并在有必要的时候修改它们的行为,以保证提供可接受 * 的性能。 * * <p>It is recognized that the distinction between random and sequential * access is often fuzzy. For example, some <tt>List</tt> implementations * provide asymptotically linear access times if they get huge, but constant * access times in practice. Such a <tt>List</tt> implementation * should generally implement this interface. As a rule of thumb, a * <tt>List</tt> implementation should implement this interface if, * for typical instances of the class, this loop: * 在界定随机访问与顺序访问的界限一般都是模糊不清的。例如,某些列表在它们拥有大量的数据 * 时提供非线性访问时间,但实际上是常量级别的访问时间。这样的接口应该实现该接口。 * <pre> * for (int i=0, n=list.size(); i &lt; n; i++) * list.get(i); * </pre> * runs faster than this loop: * 比下面的循环运行速度更快。 * <pre> * for (Iterator i=list.iterator(); i.hasNext(); ) * i.next(); * </pre> */public interface RandomAccess {} 参考资料 Java集合干货系列-(二)LinkedList源码解析","categories":[],"tags":[]},{"title":"LinkedList 源码分析","slug":"LinkedList 源码分析","date":"2017-10-28T16:10:54.000Z","updated":"2017-10-29T07:31:33.000Z","comments":true,"path":"2017/10/29/LinkedList 源码分析/","link":"","permalink":"http://apparition957.github.io/2017/10/29/LinkedList 源码分析/","excerpt":"","text":"结构体系123public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable LinkedList继承自AbstractSequentialList,并同时实现了List、Deque、Cloneable与Serializable接口。从继承体系上与接口上,可以看出LinkedList不仅仅是双向链表而已,它可以同时被当做双向队列进行操作。 为什么是双向链表呢?后面一小节将会解释。 常量与重要成员123456// 记录链表上实际装载的节点个数transient int size = 0;// 指向链表头部的指针transient Node<E> first;// 指向链表尾部的指针transient Node<E> last; 在成员变量中,我们可以看到链表所承载的节点是Node,下面看看Node是怎么构成的。 1234567891011private static class Node<E> { E item; // 节点上承载的值 Node<E> next; // 指向前一个节点 Node<E> prev; // 指向后一个节点 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }} 从Node中,不难看出LinkedList就是一个双向链表的集合,如下所示。 构造函数1234567public LinkedList() {}public LinkedList(Collection<? extends E> c) { this(); addAll(c);} LinkedList的构造函数可以说是很简约了,毕竟内部不像ArrayList里面还有个数组作支撑,只需头/尾指针即可。 基本操作 - node()123456789101112131415161718// 在LinkedList中,在某些需要在指定节点中进行操作的时候,是怎么通过索引下标的形式找到指定节点的呢// LinkedList内部提供了node()方法来实现这一目标。顺便一提node()内部充分使用了头尾节点的好处,将遍历范围缩减到size/2内Node<E> node(int index) { // assert isElementIndex(index); // 若index > size/2 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; }} 添加操作 - add()12345678910111213public boolean add(E e) { linkLast(e); return true;}public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index));} 上面的add()方法均调用了link*()字样的方法,其实这是LinkedList内部用于对链表的中单个节点的操作。相对应的remove()方法也会调用unlink*()字样的方法。 1234567891011121314151617181920212223242526272829303132333435363738394041// 作为头节点链接到链表中private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; // 若f为null, 则表明链表为空 if (f == null) last = newNode; else f.prev = newNode; size++; modCount++;}// 作为尾节点链接到链表中void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; // 若l为null, 则表明链表为空 if (l == null) first = newNode; else l.next = newNode; size++; modCount++;}// 将新节点插在指定节点前void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++;} LinkedList也提供了将一组数据添加入链表中的方法。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546public boolean addAll(Collection<? extends E> c) { // 默认从尾节点后插入 return addAll(size, c);}public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index); Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Node<E> pred, succ; // 若指定位置就是尾节点时 if (index == size) { succ = null; pred = last; } else { succ = node(index); pred = succ.prev; } for (Object o : a) { @SuppressWarnings(\"unchecked\") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); // 确保头节点始终不为空 if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } // 确保尾节点不为空 if (succ == null) { last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true;} 删除操作 - remove()12345678910111213141516171819202122public E remove() { return removeFirst();}public E remove(int index) { checkElementIndex(index); return unlink(node(index));} public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f);}public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l);} remove()如同add()一样,使用着功能截然相反的unlink*()方法,但逻辑处理难度上较大与link*()。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364// 删除非空头节点private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // 方便GC进行垃圾回收 first = next; // 若链表中只有单个节点 if (next == null) last = null; else next.prev = null; size--; modCount++; return element;}// 删除非空尾节点private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // 方便GC进行垃圾回收 last = prev; // 若链表中只有单个节点 if (prev == null) first = null; else prev.next = null; size--; modCount++; return element;}// 删除指定节点E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; // 若prev指向的上一个节点为空,那么该节点为头节点 if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } // 若next指向的下一个节点为空,那么该节点为尾节点 if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element;} remove()操作中还提供了根据给定值删除指定节点的方法。 1234567891011121314151617181920public boolean remove(Object o) { // 若给定值为null if (o == null) { // 在这里就不能缩减范围提高效率了,只能老实的进行从头遍历到尾 for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false;} 查询操作 - get()123456789101112131415161718public E get(int index) { checkElementIndex(index); return node(index).item;}public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item;}public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item;} 修改操作 - set()1234567public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal;} 实现了Deque接口的相关操作别忘了,LinkedList实现了Deque接口,这说明操作LinkedList也可以像操作双向队列一样。如同*first()以及*last()这种操作也属于Deque要求实现的,下面将列出别的方法。 123456789public E peek() { final Node<E> f = first; return (f == null) ? null : f.item;}public E poll() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f);}","categories":[],"tags":[]},{"title":"Map.Entry 使用解析","slug":"Map-Entry 使用解析","date":"2017-10-28T03:04:07.000Z","updated":"2017-10-28T07:33:52.000Z","comments":true,"path":"2017/10/28/Map-Entry 使用解析/","link":"","permalink":"http://apparition957.github.io/2017/10/28/Map-Entry 使用解析/","excerpt":"","text":"基本概念Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它可用于 表示Map中的一个键值对。 在Map提供的EntrySet()返回的是Set<Map.Entry<K,V>>,是一个Set集合,刺激和类型是Map.Entry。相较于Map所提供的另一个方法keySet(),它所提供的以key值为数据的Set集合。 使用以及对比123456789101112// keySet()循环遍历for (Object key: map.keySet()) { Object key = key; Object value = map.get(key);}// entrySet()循环遍历for (Map.Entry<Object, Object> entry: map.entrySet()) { Object = entry.getKey(); Object = entry.getValue(); // entry.setValue()} 以上两种方式中,相较于keySet()遍历Map,entrySet()能够更加清晰的显示Map内部的数据结构,同时entrySet()提供了用于修改Map的值。 参考链接 Map.Entry使用详解","categories":[],"tags":[]},{"title":"ArrayList 源码分析","slug":"ArrayList 源码分析","date":"2017-10-27T11:57:40.000Z","updated":"2017-10-27T11:59:34.000Z","comments":true,"path":"2017/10/27/ArrayList 源码分析/","link":"","permalink":"http://apparition957.github.io/2017/10/27/ArrayList 源码分析/","excerpt":"","text":"常量与重要的成员变量1234567891011121314// 默认容量private static final int DEFAULT_CAPACITY = 10;// 空数组-对应第一个构造函数private static final Object[] EMPTY_ELEMENTDATA = {};// 默认空数组-对应第二个构造函数。这个与上面EMPTY_ELEMENTDATA要进行区别,根据源码解释说到,两个变量决定了当第一次插入数据时容器的扩容机制,在这里相当于起到了标志位的作用。实际操作看扩容与缩容部分private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// ArrayList内部的真实容器(数组)transient Object[] elementData;// ArrayList内部的记录实际装载数据个数private int size; 构造函数1234567891011121314151617181920212223242526272829// 根据用户自定义容量初始化容器public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(\"Illegal Capacity: \"+ initialCapacity); }}// 默认构造函数public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}// 由一组数组进行容器的初始化(前提该数组必须是实现了Collection接口,并继承或来源于<E>)public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // 倘若返回的Class类型并非Object[],需要Arrays.copy()将其类型转化为Object[] if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 若该组数组为空数组 this.elementData = EMPTY_ELEMENTDATA; }} 在EMPTY_ELEMENTDATA与DEFAULTCAPACITY_EMPTY_ELEMENTDATA两个标志位,个人认为: 如果以默认的构造函数模式初始化ArrayList,则以ArrayList内部的增长模式扩展,即初始化时容器大小就是DEFAULT_CAPACITY,即为10 如果 增加操作 —— add()12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152// 顺序插入数据public boolean add(E e) { // 无论是哪种插入操作,都需要提前进行扩容,防止抛出ArrayIndexOutOfBoundsException ensureCapacityInternal(size + 1); elementData[size++] = e; return true;}// 在指定位置上插入数据public void add(int index, E element) { // if (index<0 || index>size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // System.arraycopy(Object src, int srcPos, // Object dest, int destPos, // int length) // 即从原数组(src)中的指定位置(src),复制一定长度的数据(length),到目标数组(dest)的指定位置上(destPos) System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++;}// 顺序插入一组数据public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0;}// 在指定位置上,插入一组数据public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // 原数组中需要移动的个数 int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0;} 在ArrayList内部,数组的移动往往通过System.arraycopy()和Array.copy()进行操作。这种方式增加了提高了内聚性,也避免了大量的重复代码出现在不同地方中。 扩容与缩容操作 - ensureCapacity()与trimToSize()扩容1234567891011121314151617181920212223242526272829303132333435363738394041// 方法域为public,即该方法是方便用户根据自己的需求直接扩展容器容量public void ensureCapacity(int minCapacity) { // 此时出现了DEFAULTCAPACITY_EMPTY_ELEMENTDATA标志,该字段是表明不同的标志位所要求的最低扩展阈值不同 int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY; if (minCapacity > minExpand) { // 最终调用内部的ensureCapacityInternal() ensureExplicitCapacity(minCapacity); }}// 方法域为private,内部只存在简单的判断逻辑,用于判断该数组是不是DEFAULTCAPACITY_EMPTY_ELEMENTDATAprivate void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity);}// 最终确认容器是否有必要进行扩容操作(毕竟每扩容一次,代表这一次性能消耗,扩容操作越频繁,性能消耗越大)private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity);}// 实际执行扩容操作private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容器大小为旧容器的1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);} 缩容12345678910// 由于ArrayList本身是不会进行缩容的,于是在进行大量的数据插入删除后,会造成大面积的空间浪费// 此时用户可以自己通过trimToSize()来缩容public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); }} 删除操作 - remove()123456789101112131415161718192021222324252627282930313233343536373839404142434445// 在指定位置上删除数据public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // 数组直接向左移动ß System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // 进行清空操作,方便GC回收 return oldValue;}// 删除指定数据(碰到的第一个)public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false;}private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null;}// 还存在其他删除操作,但原理基本相同 查找操作 - get()1234567891011// 时间复杂度O(1)public E get(int index) { rangeCheck(index); return elementData(index);}@SuppressWarnings(\"unchecked\")E elementData(int index) { return (E) elementData[index];} 修改操作 - set()12345678// 时间复杂度O(1)public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue;} 三种遍历ArrayList的方式12345678910111213141516171819// 第一种,通过迭代器遍历Integer value = null;Iterator iter = list.iterator();while (iter.hasNext()) { value = (Integer)iter.next();}// 第二种,随机访问(RandomAccess),通过索引值去遍历 -> 效率最高Integer value = null;int size = list.size();for (int i = 0; i < size; i++) { value = (Integer) list.get(i); }// 第三种,for循环遍历 -> 效率最低Integer value = null;for (Integer integer: list) { value = integer;} 参考资料: Java集合干货系列-(一)ArrayList源码解析","categories":[],"tags":[]},{"title":"HashMap 源码分析","slug":"HashMap 源码分析","date":"2017-10-26T14:06:32.000Z","updated":"2017-11-09T13:36:42.000Z","comments":true,"path":"2017/10/26/HashMap 源码分析/","link":"","permalink":"http://apparition957.github.io/2017/10/26/HashMap 源码分析/","excerpt":"","text":"概念下图可从可视化的角度理解 HashMap(其实也是方便自己想起来)。 常量与重要的成员变量1234567891011121314151617// 默认容量(也是最小容量阈值)static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 最大容量阈值static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 从链表转变为红黑树的阈值static final int TREEIFY_THRESHOLD = 8;// 从红黑树转变为链表的阈值static final int UNTREEIFY_THRESHOLD = 6;// 从链表转变为红黑树的最小容量static final int MIN_TREEIFY_CAPACITY = 64;// HashMap 实际存储键值对的容器transient Node<K,V>[] table;// HashMap 实际阈值,其值由 capacity * loadFactor 决定int threshold; 构造函数123456789101112131415161718192021222324public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException(\"Illegal initial capacity: \" + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException(\"Illegal load factor: \" + loadFactor); this.loadFactor = loadFactor; // 重点! this.threshold = tableSizeFor(initialCapacity);}// 该方法用于返回大于给定容量的最小2的幂次方的数值static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;} 为什么 HashMap 的容量数值非要是2的幂次方呢?请看JDK 源码中 HashMap 的 hash 方法原理是什么? hash()1234static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} HashMap 中的实际 hash 值计算是通过 key.hashCode()所得出来的h ,与h无条件右移16位后,进行按位异或^得出来的。 但是怎么转化成实际上table数组的所索引值呢?剧透一下,table 的索引值是通过 capacity与hash进行按位与&计算出来的。 putVal()1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 当HashMap中的数组,即table为空,或者table的长度为0时,调用 resize 方式进行 HashMap 的初始化(HashMap真正的容器初始化阶段是在第一次插入时) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 根据n(capacity)-1与hash值进行按位运算,获得该key值对应的数组中的位置。若该索引(p)上的值为null,则直接创建新的节点 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 该索引上的值不为null,那么需要分以下三种情况分析 else { Node<K,V> e; K k; // 该p的hash值与传入的hash值相等,并且p的key值也与传入的key值相等,或者在hash值不相同的情况下,两者的key值是相同的 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 若p的key值不等于传入的key值 // p的类型属于TreeNode,即从属于红黑树,则转由红黑树进行实际节点添加的操作 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // p的类型属于Node,即从属于链表。这里就是HashMap中怎么处理哈希冲突的办法。 // 当传入元素的hash值与数组上的元素相同,但key不同时。 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 当上述的添加新节点的阶段结束后,若此时的e(即原始节点)不为空时,则进行值的替换。 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 用于记录修改的操作次数 // 若此时的容器容量大于阈值时,进行resize()扩容容器 if (++size > threshold) resize(); afterNodeInsertion(evict); return null;} get()1234567891011121314151617181920// 如果看懂了putVal(),那么get()就是同样的方式分析了final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;} resize()resize()实际上的目的在于将原数组中的值均匀地平摊到新数组中,这样无论是插入还是访问的效率也会有一定的提升。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697// 这一部分分析难度不亚于putVal()final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // 若老数组为0,那么老容量为0,否则为老数组长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 若老容量大于0 if (oldCap > 0) { // 若老容量是否大于最大容量阈值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 若扩容后的新容量小于最大容量阈值且老容量大于默认容量值,则新阈值为老阈值的两倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 若老容量等于0且老阈值大于0,那么新容量就等于老阈值 else if (oldThr > 0) newCap = oldThr; // 若老容量等于0且老阈值也为0,这种比较极端了 // 新容量为默认容量值,而新阈值也为默认阈值(0.75) else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 若新阈值为0,那么则由负载因子与新容量的乘积获得 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({\"rawtypes\",\"unchecked\"}) // 实际操作部分,初始化新容器! Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 其实HashMap的初始化阶段从这里就结束了,以下部分只适用于存有实际节点的容器 if (oldTab != null) { // 遍历老数组 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; // 若该索引上的节点部位不为空,则分以下三种情况分析 if ((e = oldTab[j]) != null) { oldTab[j] = null; // 单个节点 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 红黑树 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 链表 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab;} 实际可视化操作如下所示: 为什么 HashMap 不是线程安全的?根据《Java并发编程的艺术》中写道: HashMap 在并发执行 put 操作时会引起死循环,导致 CPU 利用率接近100%。因为多线程会导致 HashMap 的 Node 链表形成环形数据结构,一旦形成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。 实际原理可以疫苗:JAVA HASHMAP的死循环一文。","categories":[],"tags":[]},{"title":"《别让我思考》读书笔记","slug":"《别让我思考》读书笔记","date":"2017-10-12T10:25:10.000Z","updated":"2017-10-12T10:27:12.000Z","comments":true,"path":"2017/10/12/《别让我思考》读书笔记/","link":"","permalink":"http://apparition957.github.io/2017/10/12/《别让我思考》读书笔记/","excerpt":"","text":"别让我思考Krug 可用性第一定律设计者应该尽量做到,当我看一个页面时,他应该是不言而喻、一目了然、自我解释的。我应该能明白它——它是什么,怎样使用它——而不需要花费精力进行思考。 网页上每项内容都有可能迫使我们停下来,进行不必要的思考。 123作为一个用户,永远不该让我们花上几微秒去思考某个东西是否能点击。你可能会这么想,“其实,找出某个东西是否能点击并不需要花费多大工夫。如果你将鼠标移过去,它的光标由箭头变成一只小手,就表示可以点击。这会有很大的问题吗?”。问题是,当我们访问 Web 的时候,每个问号都会加重我们的认知负担,把我们的注意力从要完成的任务上拉开。这种干扰也许很轻微,但它们会累积起来,有时候这样的干扰不用太多,就足以让我们抓狂。况且,人们通常不喜欢苦苦思索背后的原理。 我们实际上是如何使用 Web 的扫描,满意即可,勉强应付如果想设计有效地网页,你必须开始接受关于网络使用情况的三个事实。 第一个事实:我们不是阅读,二是扫描人们会花极少的时间来阅读大部分的页面,其实,我们只是扫描一下(或者匆匆掠过)网页,寻找能够吸引我们注意力的文字或词语。 我们为什么扫描: 我们总是处于忙碌之中。Web 用户的行为更像鲨鱼,即它们不得不一直移动,否则就会死掉。我们没有时间阅读那些不必要的内容。 我们知道自己不必阅读所有内容。在绝大多数页面上,我们实际上只对其中一小部分内容感兴趣,剩下的内容我们并不关心。 我们善于扫描。 第二个事实:我们不做最佳选择,而是满意即可在设计页面时,我们通常假设用户只是扫过整个页面,考虑所有可能的选项,然后选择一个最好的。然而,事实上,大多数时间里我们不会选择最佳选项,而是选择第一个合理的选项,这就是满意策略。一旦我们发现一个链接,看起来似乎能够跳转到我们想去的地方,那就是一个我们将会点击它的大好机会。 我们为什么不寻找最佳选择: 我们总是处于忙碌之中。 如果猜错了,也不会产生什么严重的后果。与救火不同,在网站上做了一次错误选择的后果通常只是点击几次后退按钮。 对选择进行权衡并不会改善我们的机会。 猜测更有意思。猜测不会像仔细衡量那么累,而且如果猜对了,速度会更快。他还会带来一个机会因素——有可能无意中看到某个令人意外但不错的内容,这种可能性让人开心。 第三个事实:我们不是追根究底,而是勉强应对在很大程度上人们一直在使用这些东西,但并不理解它们的运作原理,甚至对它们的工作原理有完全错误的理解。无论面对哪种技术,很少有人会花时间读说明书。相反,我们贸然前进,勉强应对,编造出我们自己模棱两可的故事,来解释我们的所作所为,以及为什么这样能行得通。 为什么会这样: 这对我们来说并不重要。对于我们中的大多数人来说,是否明白事物背后的工作机制并不重要,只要我们能正常使用它们即可。这并不是智力低下的表现,而是我们并不关心。 如果发现某个事物能用,我们会一直用它。我们一旦发现某个事物能够用(不管有多难用),我们也不会去找一种更好的方法(至少不会主动去找)。 广告牌设计101法则为扫描设计,不为阅读设计如果用户们都是疾驰而过,那么,你需要注意以下5个重要方面,来保证他们尽可能地看到了并理解了你的网站: 在每个页面上建立清楚的视觉层次。 越重要的部分越突出。 逻辑上相关的部分在视觉上也相关。 逻辑上包含的部分在视觉上进行嵌套。 尽可能利用习惯用法。 (优)它们非常有用。通常,习惯用法因为有用才会成为习惯用法。适当使用习惯用法会使用户在网站之间的访问更容易,不需要花费额外的努力来得到背后的工作原理。 (劣)设计师通常不愿意利用他们。和使用习惯用法相比,设计师们都面临着很大的诱惑,想要重新发明轮子很大程度上是因为他们觉得他们的职业使命感,趋势他们去做一些崭新的,与众不同的设计。 把页面划分成明确定义的区域。把页面划分成明确意义的区域,可以让用户很快决定关注页面的哪些区域,或者放心地跳过哪些区域。 明显标识可以点击的地方。最大限度降低干扰。有效降低噪音的方式——在设计页面的收,先假定所有的内容都是视觉噪声,除非得到证明它们不是。 动物、植物、无机物为什么用户喜欢无须思考的选择“点击多少次都没关系,只要每次点击都是无须思考、明确无误的选择。”——Krug 可用性第二定律 如果我们需要一直在网络上进行选择,那么让这些选择变得无须思考是让一个网站容易使用的主要因素。 省略不必要的文字不要在 Web 上写作的艺术“去掉每个页面上一半的文字,然后把剩下的文字再去掉一半。”——Krug 可用性第三定律 省略多余的文字。有力的文字都很简练。句子里不应该有多余的文字,段落中不应该有多余的句子。同样,画上不应该有多余的线条,机器上不应该有多余的零件。 街头指示牌和面包屑设计导航如果在网站上找不到方向,人们不会使用你的网站。 导航有两个显而易见的用途:帮助我们找到想要的任何东西和告诉我们现身何处。此外,导航还有以下额外的好处: 他给了我们一些固定的感觉。 它告诉我们当前的位置。 它告诉我们如何使用网站。 它给了我们对网站建造者的信心。在网站上的每一刻,我们都会在头脑中保持一个标杆:这些人知道他们在做什么吗?这是我们决定是否离开,或者以后会不会来的主要考虑因素之一。 首先要承认,主页不由你控制设计主页主页要完成的任务: 站点的标识(Logo)和使命。主页要告诉我这是什么网站,它是做什么的。 站点层次。主页要给出网站提供的服务的概貌——既要包括内容(“我能在这里找到什么?”),也要包括功能(“我能做什么?”)——还有这些服务是如何组织的。 搜索。 导读。 内容更新。时常更新的内容让用户觉得这个网站并不是一成不变的。 友情链接。需要在主页上预留空间,用来放置广告,交叉推广,合作品牌的友情链接等。 快捷方式。 注册。 主页需要满足一些抽象的目标: 让我看到自己正在寻找的东西。 ··· ··· 还有我没有寻找的。 告诉我从哪里开始。 建立可信度和信任感。 农场主和牧牛人应该是朋友为什么 Web 设计团队讨论可用性是在浪费时间,如何避免这种情况1从个人角度来说,我们喜欢 Flash 动画,因为它们很好玩;我们也可能不喜欢它们,因为要花很长时间下载。我们喜欢每个页面左边的菜单,因为它们看起来很熟悉而且容易使用;我们可能不喜欢它们,因为它们很枯燥乏味。我们真的喜欢有____的网站,或者,我们发现____真是让人痛苦极了。 在网站日常开发当中,项目人员共同讨论关于某些的设计问题时,很难不讲以上例子所阐述的感觉牵涉进来。结果往往就是一堆人待在房间里面,每个人都有自持主见,不肯让步。而且,由于这些主张的力量——还有人的天性——自然有一种把这些喜欢或者不喜欢投射到整个 Wen 用户身上的倾向,认为绝大多数的 Web 用户喜欢我们所喜欢的。我们通常认为大部分 Web 用户和我们一样。 争辩人们喜欢什么既浪费时间又消耗团队的精力,而通过测试能将讨论对错转移到什么有效、什么无效上,更容易缓和争论,打破僵局。而且,测试会让我们看到用户的动机、理解、反应的不同,从而让我们不会再坚持认为用户的想法和我们的想法一样。 一天10美分的可用性测试让测试简单——这样你能进行充分的测试关于测试的几个重要事实: 如果想建立一个优秀的网站,一定要测试。测试更像是邀请外地的朋友,不可避免地,当你和他们一起四处游玩时,你会看到平时不会注意到的一些情况,因为你对它们太熟悉了。同时,你也意识到有很多你认为想当然的事情,对别人来说却并非如此。 测试一个用户比不做测试好一倍。测试总是有效果的,哪怕是对错误的用户做一次最糟糕的测试,也会让你看到一些改善网站的重要方面。 在项目中,早点测试一位用户,好过最后测试50位用户。一旦一个网站投入使用,要改变它就不会那么容易了。有些用户拒绝做出任何变化,因为即使很小的变更也会给他们带来深远的影响,让我们付出无法想象的代价(至少是项目初期所付出的数倍),所以任何在开始时就有助于防止你犯错误的方法都很划算。 人们对招募用户代表的重要性估计过高。 测试的关键不是要证明什么或者反驳什么,而是了解你的判断力。测试能做的就是给你提供有价值的参考,加上你的经验、专业判断和常识能够让你更容易地在 A 和 B 之间做出更明智——也更自信——的选择。 测试是一个迭代的过程。 没有什么比现场用户的反应更重要的。 跳楼大减价的简易可用性测试 传统可用性测试 跳楼大减价的建议可用性测试 每次测试的用户数量 通常需要八个或者更多个用户,因为建立测试的花费不菲 3-4个用户 招募方式 仔细选择,尽量靠近目标用户 随便找一些人,几乎任何会上网的人都可以 测试地点 一个可用性实验室,其中包括一个观察室和单向玻璃 任何办公室或会议室 主导测试 一位有经验的可用性专家 任何相对有耐心的人 提前计划 需要提前几个星期制定测试计划,预定可用性实验室,并预留招募时间 几乎可以在任何时间进行测试,稍微提前一些做计划即可 准备工作 起草、讨论并修订测试草案 决定你要展示什么 测试目标/时间 除非你预算充足,否则会把所有的鸡蛋放在一个篮子里,在网站快要完成的时候做一次测试 在开发过程中持续进行小规模的测试 成本 5000-15000美元(或者更多) 300美元(50-100美元是给每个用户的补贴),或者更少 后续工作 一周之后,产生一份20页的报告,然后开发团队朋友来决定怎样修改 开发团队(还有有兴趣的人员)利用当天的午餐时间进行总结 可用性是基本礼貌为什么你的网站应该让人尊敬降低好感的几种方式: 隐藏我想要的信息。 因为没有按照你们的方式形式而惩罚我。 向我询问不必要的信息。 敷衍我,欺骗我。 给我设置障碍。 你的网站看上去不专业。 提高好感的几种方式 知道人们在你的网站上想做什么,并让它们明白简易、清晰明了。 告诉我我想知道的。 尽量减少步骤。 花点心思。 知道我可能有哪些疑问,并且给予解答。 为我提供协助,例如打印友好页面。 容易从错误中恢复。 如有不确定,记得道歉。 可访问性、级联样式表和你正当你觉得已经完成了的时候,一只猫掉了下来,背上捆着涂了奶油的面包在页面设计中,可以从下面几个方面有效提高网站的可访问性: 为每张图片添加 alt 文本。 让你的表单配合屏幕阅读器。 在每页的最前面增加一个“跳转到主要内容”的链接。 让所有的内容都可以通过键盘访问。 如果没有充分的理由,不要使用 JavaScript。 使用客户端的影像地图。","categories":[],"tags":[]},{"title":"BIO/NIO/AIO 三者关系解析","slug":"BIO-NIO-AIO 三者关系解析","date":"2017-09-20T13:13:43.000Z","updated":"2017-11-10T13:21:53.000Z","comments":true,"path":"2017/09/20/BIO-NIO-AIO 三者关系解析/","link":"","permalink":"http://apparition957.github.io/2017/09/20/BIO-NIO-AIO 三者关系解析/","excerpt":"","text":"概述在 Java 的 I/O 体系架构中,存在三种截然不同的 I/O 模型,分别为 BIO(Block I/O,阻塞型 I/O)、NIO(New I/O,非阻塞型 I/O)以及 AIO(Asynchronous I/O,异步 I/O)。 下面分析将从基本的术语开始讲解,最后归整讲述不同 I/O 模型的区别。 同步与异步同步与异步关注的是消息通信机制。 同步是指发送方发出一个 I/O 请求时,在没有得到结果之前,该请求不返回结果。但是一旦请求返回时,就得到了相应的返回值。 异步是指发送方发出一个 I/O 请求之后,这个请求便立即返回,该请求没有返回结果。直至请求接收方(即被调用者)通过回调的方式来通知发送方,或者发送方主动询问接收方请求结果。 举个例子: 晚上我们需要去饭店预定位置,我们会优先打个电话给酒店来预定位置,当我们被告知饭店位置爆满时需要等待时。在同步的通信机制情况下,我们(发送方)只能默默地够保持通话的方式等待饭店(接收方)来通知我们空余位置的结果,不能够做别的事情。 而在异步的通信机制情况下,饭店(接收方)提供了特殊的服务,让我们(发送方)预留手机号码(回调方式),等有位置了可以主动通知你,我们就能够单方面切断通信,等待饭店通过我们预留的手机号码来通知我们,或者我们来主动询问饭店位置的空余情况。 阻塞与非阻塞阻塞与非阻塞关注的是程序在等待调用结果时的状态。 阻塞是指请求结果返回之前,当前线程会被挂起。请求线程只有在得到结果之后才会返回。此时的线程处于阻塞状态,相当于卡住不动了。 非阻塞是指请求结果返回之前,当前线程不会被阻塞,可以处理别的任务。 同举以上的例子: 当我们打电话给饭店,被告知饭店位置爆满时需要等待时。在阻塞线程的请求方式下,我们(发送方)只能够保持通讯(阻塞),直至饭店(接收方)通知我们空余位置的结果。 而在非阻塞线程的请求方式下,我们(发送方)可以单方面挂掉电话,继续去逛街(非阻塞),直至饭店(接收方)通知我们,亦或者我们主动打电话去询问。 同步/异步与阻塞/非阻塞的区别在以上的解释当中,同步/异步与阻塞/非阻塞两者之间的关系十分相似,但是它们却存在本质上的区别。 同步/异步注重的是消息的通信机制,重点在于消息本身。 阻塞/非阻塞注重的是程序在等待调用结果时的状态,重点在于程序本身。 BIOBIO(Block I/O)为同步阻塞型 I/O。在服务器端中实现模式为一个连接一个线程,即客户端有连接请求时,服务器端就会按需启动一个线程来处理。 如果这个连接不做任何事情时,就造成不必要的线程开销,此时可以通过线程池机制来对于空线程进行回收,但是对于线程的创建与销毁等操作,系统所消耗的资源依然很大。 NIONIO(New I/O)为同步非阻塞型 I/O。在服务器端中实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上(Selector),多路复用器轮询到连接有 I/O 请求(Channel)才启动一个线程(Handler)来处理。用户进程也需要时不时地询问 I/O 操作是否就绪。 在 NIO 的 I/O 模型上,可以仅通过单线程的方式来处理高并发问题。 AIOAIO(Asynchronous I/O)为异步非阻塞型 I/O。在此种模式下,用户进程只需要发起一个IO操作然后便立即返回,待 I/O 操作真正的完成以后,应用程序会得到I/O操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 I/O 读写操作,因为真正的 I/O 读取或者写入操作已经由内核完成了。 参考资料: 怎样理解阻塞非阻塞与同步异步的区别? 十分钟了解BIO、NIO、AIO JAVA 中BIO,NIO,AIO的理解 对Java BIO、NIO、AIO 学习","categories":[],"tags":[]},{"title":"Android 源码分析资料归纳","slug":"Android 源码分析资料归纳","date":"2017-09-07T13:32:24.000Z","updated":"2017-09-19T16:08:56.000Z","comments":true,"path":"2017/09/07/Android 源码分析资料归纳/","link":"","permalink":"http://apparition957.github.io/2017/09/07/Android 源码分析资料归纳/","excerpt":"","text":"这一篇是最近学习 Android 源码,在碰到不懂的知识点时,上网找到的不错的、通俗易懂的文章,在此进行归纳收藏,在比较深入了解的时候可以去整理笔记。 Context Context 是 Android 应用层架构中最重要类,它是维持 Android 程序中各个组件能够正常工作的核心功能类。下篇文章就阐明了这一论点,以及如何有效使用 Context。 Android Context完全解析,你所不知道的Context的各种细节 View 事件分发机制 该文章主要是通过图文结合的方式,并结合少量精炼的代码,详细地叙述了一个事件如何进行传递,又如何被处理。 Android事件分发机制详解:史上最全面、最易懂 Actvity 启动及工作流程 该文章讲述了 Activity 如何启动,以及科普了 Activity 启动时所涉及的重要组件,并根据主要源码讲述了过程,虽然我还没看懂内部机制(有些方法太变态!),但是还是从大局观上有了部分的了解。 【凯子哥带你学Framework】Activity启动过程全解析 Handler 消息机制 该文章从生产者-消费者这一设计模式中,阐明了 Handler 机制中的三大组件 Handler、Looper 以及 MessageQueue,以及组件间是如何相互配合的。 Android Handler机制全解析 BroadcastProvider 广播机制 该文章主要从三个角度来入手,广播接受者是如何进行注册,广播如何被发送,广播负载物是什么。 读源码-五分钟理解不了广播机制 Service 机制 该文章怎么说,通读下来,虽然能从文章中能够理解他这个意思,但是具体的实现却异常复杂,可能现在我这个水平,还理解不了为什么它们要这样设计,这样设计的好处。 从源码出发深入理解 Android Service ContentProvider 机制 理解ContentProvider原理 Android 动画机制 看了两位 csdn 大神所写的 Android 动画教学系列,了解了 Android 动画的大战里程从 View Animation 的卡帧动画到 Drawable Animation 的仅支持少数动画效果动画机制,最后到 Property Animation 的强大。 Android属性动画完全解析(上),初识属性动画的基本用法 Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法 Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法 Android 属性动画(Property Animation) 完全解析 (上) Android 属性动画(Property Animation) 完全解析 (下)","categories":[],"tags":[]},{"title":"Android 进行 HTTPS 网络通信","slug":"Android 进行 HTTPS 网络通信","date":"2017-09-01T06:52:32.000Z","updated":"2017-09-01T06:53:20.000Z","comments":true,"path":"2017/09/01/Android 进行 HTTPS 网络通信/","link":"","permalink":"http://apparition957.github.io/2017/09/01/Android 进行 HTTPS 网络通信/","excerpt":"","text":"遇到的问题前两天在 Andorid 上与使用自签名证书的服务器进行 https 网络通信遇到了问题,主要的问题出在于服务器端的证书不受客户端信任与认证,服务器端也不认识客户端,双方互不认识(在浏览器好歹也会提示用户添加安全证书)。 问题分析根据需求分析,Android 平台上需要进行双向验证才能够进行正常的 https 通信。而在之前的代码结构中,由于不熟悉 https 沟通方式,错误使用了服务端证书来进行身份识别与验证。 解决方式进行后续操作的调整,在 centOS 平台上使用 keytool 分别了生成了服务器端证书以及客户端证书,并将客户端证书放置 Android 上,用于连接服务器端时对服务器端的证书进行鉴别与认证。 具体操作如下所示: 12345678910111213141516171819202122231、生成服务器证书库keytool -validity 365 -genkey -v -alias server -keyalg RSA -keystore server.keystore -dname \"CN=commonName,OU=organizationalUnit,O=Organization,L=Locality,ST=state,c=country\" -storepass 123456 -keypass 123456 -keysize 20482、生成客户端证书库keytool -validity 365 -genkey -v -alias client -keyalg RSA -storetype PKCS12 -keystore client.p12 -dname \"CN=client,OU=organizationalUnit,O=Organization,L=Organization,ST=state,c=country\" -storepass 123456 -keypass 123456 -keysize 20483、从客户端证书库中导出客户端证书keytool -export -v -alias client -keystore client.p12 -storetype PKCS12 -storepass 123456 -rfc -file client.cer4、从服务器证书库中导出服务器证书keytool -export -v -alias server -keystore server.keystore -storepass 123456 -rfc -file server.cer5、生成客户端信任证书库(由服务端证书生成的证书库)keytool -import -v -alias server -file server.cer -keystore client.truststore -storepass 123456 -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider6、将客户端证书导入到服务器证书库(使得服务器信任客户端证书)keytool -import -v -alias client -file client.cer -keystore server.keystore -storepass 123456 keytool 常用参数说明: 参数 用途 -genkey 在用户主目录中创建一个默认文件“.keystore” -validity 指定创建的证书有效期为多少天 -alias 产生别名,每个 keystore 都关联一个独一无二的 alias -keystore 指定密钥库的名称 -keyalg 指定密钥的算法 -keysize 指定密钥的长度 -dname 指定证书发行者信息,其中: “CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名 称,ST=州或省份名称,C=单位的两字母国家代码” -storepass 指定密钥库的密码(.keystore密码) -keypass 指定别名条目的密码(私钥密码) -storetype 指定密钥库的存储类型 -export 将 alias 指定的证书导出到文件 -import 将已签名的证书导入密钥库中 -rfc 以Base64的编码格式打印证书 -file 指定导出到文件的文件名 -v 查看密钥库中的证书详细信息 在代码结构上,在 http 通信代码中添加 SSL 通信机制,如下所示: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public static final String KEY_STORE_TYPE_BKS = \"BKS\";//证书类型public static final String KEY_STORE_TYPE_P12 = \"PKCS12\";//证书类型public static final String KEY_STORE_CLIENT_PATH = \"swaypay.p12\";//客户端要给服务器端认证的证书public static final String KEY_STORE_TRUST_PATH = \"swaypay.truststore\";//客户端验证服务器端的证书库public static final String KEY_STORE_PASSWORD = \"superssl1\";// 客户端证书密码public static final String KEY_STORE_TRUST_PASSWORD = \"superssl1\";//客户端证书库密码public static final String KEY_STORE_TYPE_X509 = \"X509\";public static final String SSL_CONTEXT_PROTOCOL = \"TLS\";public void setSSLConnection() { // 1 - KeyStore - 用于存储各种类型的密钥,方便管理与使用 KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12); KeyStore trustStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS); // 2 - 将提前获取的受服务器端信任的客户端证书/用于验证服务器端的证书的证书库进行导入 InputStream ksIn = ContextHolder.getContext().getAssets().open(KEY_STORE_CLIENT_PATH); InputStream tsIn = ContextHolder.getContext().getAssets().open(KEY_STORE_TRUST_PATH); // 3 - 初始化 KeyManagerFactory keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KEY_STORE_TYPE_X509); keyManagerFactory.init(keyStore, KEY_STORE_PASSWORD.toCharArray()); // 4 - 初始化 TrustManagerFactory trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(trustStore); // 5 - 初始化 SSLContext 并添加通过 KeyManagerFactory 和 TrustManagerFactory 分别生成的 getKeyManagers 和 getTrustManagers // 这一步中,通过这种方式,才算 https 的双向验证机制的真正建立 sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);}... private HttpURLConnection createHttpURLConnection(String targetUrl) { ... // 6 if (httpURLConnection instanceof HttpsURLConnection) { setSSLConnection(); ((HttpsURLConnection) httpURLConnection).setSSLSocketFactory(sslContext.getSocketFactory()); // 暂不验证 host 有效性 ((HttpsURLConnection) httpURLConnection).setHostnameVerifier((String hostname, SSLSession session) -> { return true; }); } ...} 参考资料: [1] http://frank-zhu.github.io/android/2014/12/26/android-https-ssl/","categories":[],"tags":[]},{"title":"心跳机制","slug":"心跳机制","date":"2017-08-28T11:46:10.000Z","updated":"2017-08-28T11:47:44.000Z","comments":true,"path":"2017/08/28/心跳机制/","link":"","permalink":"http://apparition957.github.io/2017/08/28/心跳机制/","excerpt":"","text":"心跳机制及基本实现什么是心跳机制心跳机制是定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。这种机制在分布式系统中十分常见,也是确保分布式系统中的主机是否存在的有效机制之一。 一般的实现机制心跳机制一般会有两种,客户端实现和服务器端实现。 客户端实现客户端通过本机与服务器端连接后获得的 Socket 对象,在一定时间间隔中,发送附加有效信息的心跳包给服务器端。服务器端正确接收后,在服务器端上以适当的形式保存该客户端发送心跳包时间,以便用于检测客户端在规定的超时间隔内依然是否存在。 服务器端实现服务器端需要保留所有已连接的客户端 Socket 对象,并在一定时间间隔中,发送空载的心跳包给所有客户端,并在本机中维护一个发送时间计时器。客户端正确接收到心跳包后,回传给服务器端一个附加有效信息的心跳包,用于证明客户端依然存在。若服务器端在计时器超时之后,仍没有收到客户端发送来的心跳包,可视为客户端断开连接。 尝实现自己觉得好玩就尝试去实现,放在了 github,代码比较粗糙 Github: https://github.com/jianpeng957/heartbeat","categories":[],"tags":[]},{"title":"记得刷牙!","slug":"记得刷牙!","date":"2017-08-07T08:32:44.000Z","updated":"2017-08-07T08:33:05.000Z","comments":true,"path":"2017/08/07/记得刷牙!/","link":"","permalink":"http://apparition957.github.io/2017/08/07/记得刷牙!/","excerpt":"","text":"上个星期,小时候为了修复蛀牙而去补牙的填充物不知为何掉了出来,空剩我一颗大蛀牙在里面,虽然不会有很大疼痛,但是心情却异样烦闷。今天特意去医院看了口腔科,做了一系列诊断后,医生说我这颗牙已经烂得不行了,要么做根管治疗,要么就拔了做假牙,需要的费用也是挺贵的,前一个至少3000+,后一个至少1w+。 听了之后,再看看 X 光片中那颗糜烂的大蛀牙,我整个人都不好了,很懊恼为什么小时候老是不刷牙,尤其是吃了糖之后不刷牙。爱护牙齿,甚至爱护身体的每一部分都是自己生命的本钱,不好好珍惜,到后来都是要自己买单,甚至是给家里带来负担。 在爱护自己的道路上,一定要且行且珍惜。尤其是到了工作的部分,我相信肯定比大学时期的生活更加艰难。附上一张在医院拍的 X 光片。","categories":[],"tags":[]},{"title":"移位操作符","slug":"移位操作符","date":"2017-08-05T13:20:30.000Z","updated":"2017-10-28T07:35:27.000Z","comments":true,"path":"2017/08/05/移位操作符/","link":"","permalink":"http://apparition957.github.io/2017/08/05/移位操作符/","excerpt":"","text":"移位运算符就是在二进制的基础上对数学进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(右移)、>>>(无符号右移)。 Java 中的移位运算符也有三种: << - 左移运算符 >> - 右移运算符 >>> - 无符号右移运算符 代码展示12345678910111213141516// 左移运算符int a = 10; // 二进制 - 1010a << 1; // 二进制 - 10100 - 相当于乘于 2^1// 右移运算符int a = 10;a >> 1; // 二进制 - 101 - 相当于除于 2^1// 无符号右移运算符 - 分两种情况:非负数与负数// 非负数int a = 10;a >> 1; // 二进制 - 101 - 相当于除于 2^1// 负数int a = -10; // 二进制 - 11111111111111111111111111110110 - 负数在高位填补1a >> 1; // 二进制 - 01111111111111111111111111111011 - 十进制 - 2147483643 以上代码中,十进制转换为二进制中,为什么int a = -10的二进制数的总长度为32呢。这取决于数据类型,在 java 中,基本数据类型int是4个字节,即32位。如果将数据类型int改变为long,结果则完全不同。 12long a = -10;a >> 1; // 十进制 - 9223372036854775803 优势 移位运算是直接基于二进制对数值进行操作,主要目的是节约内存,运算时间比算术运算符更加快。 参考资料: https://baike.baidu.com/item/%E7%A7%BB%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6 http://www.cnblogs.com/hongten/p/hongten_java_yiweiyunsuangfu.html","categories":[],"tags":[]},{"title":"joda-time源码简略剖析.md","slug":"joda-time源码简略剖析","date":"2017-08-04T08:36:38.000Z","updated":"2017-08-04T08:48:37.000Z","comments":true,"path":"2017/08/04/joda-time源码简略剖析/","link":"","permalink":"http://apparition957.github.io/2017/08/04/joda-time源码简略剖析/","excerpt":"","text":"最近查看了 Java 中有关于日期/时间处理的 API,发现了 Joda-time 这一款被众人广泛使用的开源库,花了一个下午,研究了内部的架构解析,画了下草图(真草图),内部的有些计算解析涉及到位处理,有些难以消化。","categories":[],"tags":[]},{"title":"Linux命令 - mount与umount","slug":"Linux命令-mount与umount","date":"2017-07-31T13:35:00.000Z","updated":"2017-07-31T13:36:19.000Z","comments":true,"path":"2017/07/31/Linux命令-mount与umount/","link":"","permalink":"http://apparition957.github.io/2017/07/31/Linux命令-mount与umount/","excerpt":"","text":"简介 mount 与 umount 分别用于挂载与卸载文件系统 mount 基本语法123mount [-l|-h|-V]mount -amount [-fnrsvw] [-t fstype] [-o options] <device> <dir> 基本参数解释: 参数 作用 无参数 显示所有磁盘的挂载情况 -a 依照配置文件 /etc/fstab 的数据将所有未挂载的磁盘挂载上去 -l 显示时多一列 Label 名称 -t 可以加上文件系统种类来指定欲挂载的类型。常见 Linux 支持类型有:ext2, ext3, vfat, reiserfs 等 -n 在默认的情况下,系统会将实际挂载的情况实时写入 /etc/mtab 中,以方便其他程序的运行。但某些情况下允许用户不将挂在情况写入 -o 可以在挂载时附加一些参数(详情看链接) 常用基本操作 显示所有端口 123456789101112# mountsysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)devtmpfs on /dev type devtmpfs (rw,nosuid,size=49311808k,nr_inodes=12327952,mode=755)securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)...nfsd on /proc/fs/nfsd type nfsd (rw,relatime)/dev/sda2 on /boot type ext4 (rw,relatime,data=ordered)/dev/sda1 on /boot/efi type vfat (rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro)/dev/mapper/ze-home on /home type xfs (rw,relatime,attr2,inode64,noquota)tmpfs on /run/user/0 type tmpfs (rw,nosuid,nodev,relatime,size=9864680k,mode=700) 挂载硬盘 1# mount /dev/sdb /home 上面的命令是指将 /dev/sdb磁盘挂载至/home文件目录下。 其余操作在有空闲服务器时再添加 umount 基本语法12umount -aumount [dflnrv] [-t fstype] [-O options] <device|dir> 基本参数解释: 参数 作用 -a 将所有在 /proc/self/moutinfo 有描述记录的文件系统卸载下来,除了 proc, devfs, devpts, sysfs, rpc_pipefs 以及 nfsd 文件系统 -f 强制卸载! -n 不将磁盘卸载情况写入 /etc/mtab 常用基本操作 卸载磁盘 1# umount /dev/sdb 上面的命令是指将 /home文件目录下的/dev/sdb磁盘卸载下来 参考资料: [1] http://cn.linux.vbird.org/linux_basic/0230filesystem.php#mount [2] http://man7.org/linux/man-pages/man8/umount.8.html [3] http://man7.org/linux/man-pages/man8/mount.8.html","categories":[],"tags":[]},{"title":"《程序员修炼之道》读书笔记","slug":"《程序员修炼之道》读书笔记","date":"2017-07-30T04:52:57.000Z","updated":"2017-07-31T12:21:10.000Z","comments":true,"path":"2017/07/30/《程序员修炼之道》读书笔记/","link":"","permalink":"http://apparition957.github.io/2017/07/30/《程序员修炼之道》读书笔记/","excerpt":"","text":"注重实效的哲学这一章中,作者从六个相互独立的主题来阐述程序员应该从哪几个方面来注重实效,为注重实效的哲学奠定了基础。 责任 注重实效的程序员对他/她自己的职业生涯负责,并且不害怕承认无知和错误。 责任是你主动负担的东西。你承诺确保某件事情正确完成,但你不一定能直接控制事情的每一个方面。除了尽你所能以外,你必须分析风险是否超出了你的控制。对于不可能做到的事情或者风险太大的事情,你有权不去为之负责。你必须基于你自己的道德准则和判断来做出决定。 如果你确实同意要为某个结果负责,你就应切实负起责任。当你犯错误、或者判断错误时,诚实地承认它,并设法给出各种选择,而不是选择去责备别人或别的东西、或者拼凑借口。 在你走向任何人,告诉他们为何某事做不到、为何耽搁、为何出问题之前,先停下来,听一下你心里的声音。在你的头脑中把谈话预演一遍。其他人可能会说什么?你将怎样回答?在你去告诉他们坏消息之前,是否还有其他你可以再试一试的办法?有时,你其实知道他们会说什么,所以还是不要给他们添加麻烦吧。 软件的熵熵,指的是系统中的“无序”的总量。在热力学定律中,保证了宇宙中的熵倾向于最大化,而在相似的环境中,当软件正在无序、野蛮的增长时,程序员称之为“软件腐烂”。 破窗户理论:一扇破窗户,只要有那么一段时间不去修路,就会渐渐给建筑的居民带来一种废弃感——一种职权部门不关心这座建筑的感觉。于是又一扇窗户破了。人们开始乱扔垃圾。出现了乱涂图画。严重的结构损坏开始了。在相对较短的一段时间里,建筑就被损毁得超出了业主愿意修理的程度,而废弃感就成了现实。 从“破窗户理论”中,我们可以得知,当项目中出现了类似于低劣的设计或者糟糕的代码,倘若程序员不及时去修复时,放着不管,就容易使得负责该项目的人员产生一种随意感,这样做的后果往往会是项目失败的一个较大的原因。 所以,在项目开始时,不要留着”破窗户”不修。发现一个就修复一个。如果没有足够的时间进行适当的修理,就做一个 to-do-list,亦或者在问题的代码上放入具有警示性的注释,亦或者用虚设的数据(dummy data)加以替代。采用某种行动防止进一步的损坏,并说明情势仍处于你的控制之下。 若项目中的每个人员都注重于自身设计的简约,代码的工整,使得项目的结构合理得体,那么身为项目中的一员的你也不愿意去玷污这个工程,即使在项目的最后期限(deadline),也不愿成为第一个弄脏东西的人。 石头汤与青蛙这一章中的故事主要讲述了三个士兵利用人的好奇心,用石头汤换来一顿美味的大餐。 从士兵的角度思考整个故事士兵的角度解析这篇故事有两层寓意。士兵戏弄了村民,他们利用村民的好奇,从他们那里弄到了食物。但更重要的是,士兵充当了催化剂,把村民们团结起来,和他们一起做了他们自己本来做不到的事情。 在有些情况下,你也许确切地知道需要做什么,以及怎么样去做。整个系统就在你的眼前——你知道它是对的,但请求许可去处理整个事情,你会遇到拖延和漠然。每个人都会护卫他们自己的资源,不轻易去协助,甚至让步。 这正是拿出“石头”的时候。设计出你可以合理要求的东西,好好开发它。一旦完成雏形之时,可以与人分享,引起他们兴趣,人就自然而然会来帮助你。人们发现,参与正在发生的成功要更容易。让他们瞥见未来,你就能让他们聚集在你周围。 从村民的角度思考整个故事从故事中,我们知道村民们注意力过度集中,只想着石头,而忘却了自己身处的环境(贫困饥饿)。这种事情就好比温水煮青蛙一般,程序员如果没有观察到项目中的细小变化,而被一些微不足道的事情逐步侵蚀,最终的后果是灾难性的。 在项目开发中,不要像温水中青蛙一样。要留心大局(big picture)。要持续不断地观察周围发生的事情,而不只是你自己在做的事情。 足够好的软件 欲求更好,常把好事变糟。 —— 李尔王 在现实世界中,我们无法制作出完美的产品,特别是没有任何差错(bug)的软件。但是我们可以训练自己,编写出足够好的软件——对你的用户(功能)、对未来的维护者(文档)、对你自己内心的安宁(满足)来说足够好。 在编写软件的过程中,我们应当接纳用户的各种各样的意见,让他们参与权衡之中。无视用户的需求,一味地给程序增加新特性,闭门造车,或是一次又一次润饰代码,这不是有职业素养的做法。 在项目开发过程中,我们应该将项目的范围与质量作为项目需求的一部分规定下来,使之成为需求问题,一步步满足完善。但是同时,我们也不该过度修饰和过于求精而损毁完好的程序。继续前进,让你的代码凭着自己的质量(高可用)站立。它也许不完美,但你也不用担心,它不可能完美。 你的知识资产在程序员的职业生涯中,知识和经验是你重要的职业财富。遗憾的是,它们是有时效的资产(expiring asset)。随着新的技术、语言及环境的出现,你的知识可能会变得过时。不断变化的市场驱动也许会使你的经验变得陈旧或无关紧要。随着你的知识的价值降低,对你的公司或者你的客户来说,你的价值也在降低。 作为程序员,我们将所知道的关于计算技术和他们所工作的应用领域的全部事实,以及他们的所有经验视为他们的知识资产。管理知识资产与管理金融资产非常相似: 严肃的投资者定期投资。 就像金融投资一样,你必须定期地为你的知识资产投资。即使投资量很小,习惯自身也和总量一样重要。可以通过几种途径累积自己的资本: 每年至少学习一种新语言。不同语言以不同的方式解决相同的问题。通过不同的语言,有助于拓宽思维,并避免墨守成规。 每季度阅读一本技术书籍。起码至少一个月读一本书。 也要阅读非技术书籍。 上课。 试验不同的环境。 跟上潮流。了解时事,抓紧下一次流量窗口。 多元化是长期成功的关键。 你知道的不同的事情越多,你就越有价值。作为底线,你需要知道你目前所用的特定技术的各种特性。但不要就此止步。计算技术的面貌变化地很快——今天热门技术明天就可能变得近乎无用。 聪明的投资者在保守的投资和高风险、高回报的投资之间平衡他们的资产。 从高风险、可能有高回报,到低风险,低回报,技术存在于这样一条谱带中。把你所有的金钱都投入到可能突然崩盘的高风险股票并不是一个很好的主意;你也不应太过于保守,错过可能的机会。不要将你所有的技术鸡蛋放在一个篮子中。 投资者设法低买高卖,以获得最大回报。 在新型的技术流行之前学习它可能就和找到被低估的股票一样困难,但所得到的就会像那样的股票带来的收益一样。 应周期性地重新评估和平衡资产。 充分评估自身所拥有的资产(技术栈),并不断地丰富自己的技术栈,有助于自身的发展。 批判地思考你读到的和听到的。 所见非所得,你需要对不同途径获取的信息,进行批判性地思考,对接受的内容持有怀疑态度,直至被自己证实。 交流作为开发者,我们必须在许多层面上进行交流。我们把许多小时花在开会、倾听和交谈上。我们与最终用户一起工作,设法了解他们的需要。我们编写代码,与机器交流我们的意图;把我们的想法变成文档,留给以后的开发者。我们撰写提案和备忘录,用以申请资源并证明其正当性、报告我们的状态、以及提出各种新的办法。我们每天在团队中工作,宣扬我们的主意、修正现有的做法、并提出新的做法。我们的时间有很大一部分都花在交流上,所以我们需要把它做好。 知道你想说什么。 规划你想要说的东西。写出大纲。然后问你自己:”这是否讲清了我要说的所有内容?“提炼它,直到确实如此为止。 了解你的听众。 只有当你是在传达令人能够理解的信息时,你才是在进行交流。为此,你需要了解你的听众的需要、兴趣、能力。要在脑海里形成一幅明确的关于你的听众的画面。 WISDOM 译文 What do you want them to learn? 你想让他们学到什么? What is their interest in what you’ve got to say? 他们对你讲的什么感兴趣? How sophisticated are they? 他们有多少经验? How much detail do they want? 他们想要多少细节? Whom do you want to own the information? 你想要让谁获得这些信息? How canyou motivate them to to listen to you? 你如何使他们听你说话? 选择时机。 为了了解你的听众需要听什么,你需要弄清楚他们的”轻重缓急“是什么。要让你所说的适得其时,在内容上切实相关,满足听众需求。 选择风格。 调整你的交流风格,让其适应你的听众。有人要的是正式的会议简报。另一些人喜欢在进行正题之前高谈阔论一番。 让文档美观。 你的主意固然重要,但是结构良好,思路清晰的文档更容易地让听众接受。 让听众参与。 在项目开发过程中,在尽可能的情况下,让你的读者参与到文档的早起草稿的制作。获取他们的反馈,并汲取他们的智慧。你将建立良好的工作关系,并很可能在此过程中制作出比原先更好的文档。 做倾听者。 如果你想要大家听你说话,你必须使用一种方法:听他们说话。在正式会议中,即使你掌握着全部信息,倘若你只管你自己讲话,是没有任何听众愿意听你讲话的。 鼓励大家通过提问来交谈,或者让他们总结你告诉他们的东西。把会议变成对话,你将能更有效地阐述你的观点。 回复他人。 及时、随时回复他人,会让他们更容易原谅你偶然的疏忽,并有助于维持一段较为良好的关系。","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"Linux LVM磁盘管理","slug":"Linux-LVM磁盘管理","date":"2017-07-28T16:34:58.000Z","updated":"2017-07-30T04:20:44.000Z","comments":true,"path":"2017/07/29/Linux-LVM磁盘管理/","link":"","permalink":"http://apparition957.github.io/2017/07/29/Linux-LVM磁盘管理/","excerpt":"","text":"Linux LVM磁盘管理概述 LVM(Logical Volume Manager,逻辑卷管理器)是一种可用在 Linux 内核的逻辑分卷管理器,可用于管理磁盘驱动器或其他类似的大容量存储设备 在传统 Linux 环境下,磁盘分区是直接与文件目录(filesystem)直接相互挂载的。倘若用户需要对文件目录的容量进行伸缩的话,通常做法有两种:一是新增磁盘分区,二是对原有的磁盘分区进行划分。无论是上述哪一种做法,都会对原有的磁盘分区产生影响,亦或某些文件损坏,亦或磁盘损坏。 为了更加方便用户对磁盘分区进行操作,LVM 为计算机提供了更高层次的磁盘存储方式。原理如下所示:LVM 将一个或多个磁盘的分区在逻辑上集合,相当于一个整体的、容量大的磁盘,以便用来使用。当磁盘分区空间不足时,可以继续将其他的磁盘的分区加入其中。 与传统的磁盘管理相比,LVM 更富有弹性: 使用卷组(VG),使众多硬盘空间看起来像一个大硬盘 使用逻辑卷(LV),可以创建跨越众多硬盘空间的分区 可以创建小的逻辑卷(LV),在空间不足时再动态调整它的大小 在调整逻辑卷(LV)大小时可以不用考虑逻辑卷在硬盘上的位置,不用担心没有可用的连续空间 可以在线(online)对逻辑卷(LV)和卷组(VG)进行创建、删除、调整大小等操作。LVM上的文件系统也需要重新调整大小,某些文件系统也支持这样的在线操作 无需重新启动服务,就可以将服务中用到的逻辑卷(LV)在线(online)/动态(live)迁移至别的硬盘上 允许创建快照,可以保存文件系统的备份,同时使服务的下线时间(downtime)降低到最小 相关于 LVM 的几个重要名词: Physical Volume,PV, 物理卷 可以在上面建立卷组的媒介,可以是硬盘分区,也可以是硬盘本身或者回环文件(loopback file)。物理卷包括一个特殊的 header,其余部分被切割为一块块物理区域(physical extends) Volume Group,VG,卷组 将一组物理卷收集为一个管理单元。卷组可以视为一个由若干个物理卷组合而成的“磁盘”。卷组同时也能够包含若干个逻辑卷(logical volume) Logical Volume,LV,逻辑卷 一种特殊的虚拟分区,从属于卷组,可以由若干块物理区域构成。 Physical Extent,PE,物理区域 硬盘可供指派给逻辑卷的最小单位(通常为4MB) 基本操作Physical Volume,物理卷相关操作12345678910# 维护命令# pvscan # 在系统中的所有磁盘中搜索已存在的物理卷# pvdisplay [<物理卷>] # 显示 全部/指定 物理卷的属性信息# pvs # pvdisplay 简约版,仅能得到物理卷的概要信息# pvchange [-x {y|n}] [-u] # 用于指定物理卷的 PE 是否允许分配或重新生成物理卷的 UUID# pvmove <源物理卷> [<目的物理卷>] # 将同一 VG 下的 PV 内容进行迁移,若不指定目的物理卷则由 LVM 决定# 创建与删除命令# pvcreate <设备名> # 用于在磁盘或磁盘分区上创建物理卷初始化信息,以便对该物理卷进行操作# pvremove <物理卷> [-d][-f][-y] # 删除物理卷 Volume Group,卷组相关操作123456789101112131415161718192021222324# 维护命令# vgscan # 在系统中搜索所有已存在的 vg# vgck <卷组> # 用于检查卷组中卷组描述区域信息的一致性# vgdisplay [<卷组>] # 显示 全部/指定 卷组的属性信息# vgrename <旧卷组名> <新卷组名> # 卷组重命名# vgchange [-a {y|n}] [-x {y|n}] # 用于指定卷组是否允许分配或者卷组容量是否可伸缩# 创建与删除命令# vgcreate <卷组> # 用于创建 LVM 卷组# vgremove <卷组> # 用于删除 LVM 卷组# 扩充与缩小命令# vgextend <卷组> <物理卷> # 向卷组中添加物理卷来增加卷组的容量# vgreduce <卷组> <物理卷> # 向卷组中删除物理卷来减小卷组的容量# 合并与拆分命令# vgmerge <目的卷组> <源卷组> # 将源卷组合并至目的卷组,要求两个卷组的物理区域大小相等且源卷组是非活动的(inactive)# vgsplit <源卷组> <目的卷组> <源物理卷> # 将源卷组的源物理卷拆分到目的卷组# vgexport <卷组> # 用于输出卷组,将非活动的(inactive)的卷组导出,可用于其他系统中使用# vgimport <卷组> <物理卷> # 用于输入卷组# 备份与恢复命令# vgcfgbackup <卷组> # 备份卷组的元信息至 /etc/lvml/backup 目录中# vgcfgrestore <卷组> # 从备份文件中恢复指定卷组 Logical Volume,逻辑卷相关操作12345678910111213# 维护命令# lvscan # 在系统中搜索所有已存在的 lv# lvdisplay [<逻辑卷>] # 显示 全部/指定 逻辑卷的属性信息# lvrename {<卷组> <旧逻辑卷名> <新逻辑卷名> | <旧逻辑卷路径名> <新逻辑卷路径名>}# lvchange # 更改逻辑卷的属性# 创建与删除命令# lvcreate <逻辑卷> <卷组> # 用于创建卷组中的逻辑卷# lvremove <逻辑卷> <卷组> # 用于删除卷组中的逻辑卷# 扩充与缩小命令# lvextend -L +<增量> <逻辑卷> # 根据增量对逻辑卷容量进行扩充# lvreduce -L -<减量> <逻辑卷> # 根据减量对逻辑卷容量进行缩小 参考资料: [1] https://jingyan.baidu.com/article/fedf0737772d2835ac897790.html [2] https://wiki.archlinux.org/index.php/LVM_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)#.E5.88.9B.E5.BB.BA.E7.89.A9.E7.90.86.E5.8D.B7.EF.BC.88PV.EF.BC.89 [3] http://www.cnblogs.com/gaojun/archive/2012/08/22/2650229.html [4] http://www.cnblogs.com/xiaoluo501395377/archive/2013/05/22/3093405.html","categories":[{"name":"Linux","slug":"Linux","permalink":"http://apparition957.github.io/categories/Linux/"}],"tags":[]},{"title":"未选择的路","slug":"未选择的路","date":"2017-07-22T17:59:42.000Z","updated":"2017-07-30T04:40:42.000Z","comments":true,"path":"2017/07/23/未选择的路/","link":"","permalink":"http://apparition957.github.io/2017/07/23/未选择的路/","excerpt":"","text":"—— 罗伯特·弗罗斯特(美) 黄色的林子里有两条路, 很遗憾我无法同时选择两者 身在旅途的我久久站立 对着其中一条极目眺望 直到它蜿蜒拐进远处的树丛。 我选择了另外的一条,天经地义, 也许更为诱人 因为它充满荆棘,需要开括; 然而这样的路过 并未引起太大的改变。 那天清晨这两条小路一起静卧在 无人踩过的树叶丛中 哦,我把另一条路留给了明天! 明知路连着路, 我不知是否该回头。 我将轻轻叹息,叙述这一切 许多许多年以后: 树林里有两条路,我—— 选择了行人稀少的那一条 它改变了我的一生。","categories":[{"name":"不正常的日常","slug":"不正常的日常","permalink":"http://apparition957.github.io/categories/不正常的日常/"}],"tags":[]},{"title":"跟着老师喝喝茶聊聊人生","slug":"跟着老师喝喝茶聊聊人生","date":"2017-05-18T15:22:24.000Z","updated":"2017-07-31T13:29:21.000Z","comments":true,"path":"2017/05/18/跟着老师喝喝茶聊聊人生/","link":"","permalink":"http://apparition957.github.io/2017/05/18/跟着老师喝喝茶聊聊人生/","excerpt":"","text":"本来今晚老师说是小组开会的,后来或许是老师刚开完学校那边的会很累,就临时改成了出去喝东西。一路走去甜品店的时候,自己心里还是没底的,因为不知道一会儿聊的话题会是什么,或许严肃,或许轻松,。等到点了饮品,大家都齐齐坐下的时候,才知道今天老师的主题是聊聊现在小组的状态。 说实话,今天聊了挺开心的,谈到了关于技术的东西,比如着手实验室的项目,也谈到了关于学习的东西,比如准备明年的项目参加竞赛等。 今晚我问了很多的问题,我就说下今晚我觉得提出的比较有意义的问题,就是自己是否该去参加某些团队去开发一套通用的、开源的项目。跟着老师这边的说法是,其实大部分开源项目都是程序员下班后自己的业余兴趣去完成,但最主要的一点是,是需要一定的、较为深厚的技术基础的。这番话其实我也有所见解,但是听到老一辈的这么说,心里还是有个底,更加加深了自己的一番见解。 现阶段,就是需要去积累自己技术的广度与深度,主要是深度,其次是广度。在积累深度的同时,可以触类旁通,了解广度的东西。比如,我感兴趣的东西就是后端Web开发的一套,但是我还是得去了解前端的基本开发,基本框架,这样在以后出去工作,或者钻研技术的时候,才能更好地与前端人员进行交流,了解他们的进度,甚至是在一定程度上参与他们的工作。 在积累自己技术的同时,还得积累自己的实战经历,比如在实验室做项目。实验室这边是与某公司进行合作,进行公司内部的系统的重构。在这个阶段,我能够在浅面上能够了解公司的现有工程结构,以及可以适度掌握如何与公司方进行交涉等能力。 今晚,我更加印证了自己的想法,在一定的阶段之前,切记浮躁心态,沉下心来,扎牢基础,打磨自己。","categories":[{"name":"不正常的日常","slug":"不正常的日常","permalink":"http://apparition957.github.io/categories/不正常的日常/"}],"tags":[]},{"title":"《大型网站系统与Java中间件开发实践》笔记","slug":"《大型网站系统与Java中间件开发实践》笔记","date":"2017-05-17T05:19:43.000Z","updated":"2017-07-31T13:29:00.000Z","comments":true,"path":"2017/05/17/《大型网站系统与Java中间件开发实践》笔记/","link":"","permalink":"http://apparition957.github.io/2017/05/17/《大型网站系统与Java中间件开发实践》笔记/","excerpt":"","text":"《大型网站系统与Java中间件开发实践》 - 作者曾宪杰 :本书围绕大型网站和支撑大型网站架构的 Java 中间件的实践展开介绍。从分布式系统的知识切入,让读者对分布式系统有基本的了解;然后介绍大型网站随着数据量、访问量增长而发生的架构变迁;接着讲述构建 Java 中间件的相关知识;之后的几章都是根据笔者的经验来介绍支撑大型网站架构的 Java 中间件系统的设计和实践。希望读者通过本书可以了解大型网站架构变迁过程中的较为通用的问题和解法,并了解构建支撑大型网站的 Java 中间件的实践经验。 这本书通读下来,对于大型网站系统的有了基本的概念和看法。 第一次使用XMind整理读书笔记,感觉还挺不错的,希望以后坚持使用。","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"Cookie和Session","slug":"Cookie和Session","date":"2017-05-14T11:55:35.000Z","updated":"2017-07-31T13:28:11.000Z","comments":true,"path":"2017/05/14/Cookie和Session/","link":"","permalink":"http://apparition957.github.io/2017/05/14/Cookie和Session/","excerpt":"","text":"在日常生活中,作为用户的我们经常与互联网中的大大小小的网站进行交互,例如查看新闻、购买商品等等。在浏览商品的时候,我们经常会将喜欢的商品暂时加入购物车,以便后面的时候一起购买。但对于同一个商城的若干次页面请求当中,服务器在后台当中是如何判别该购物车是哪一个用户的呢? 对于一次网络的请求,一般都是基于HTTP,但是HTTP是一种无状态协议(即不保留管理用户数据),所以不能够单纯依靠HTTP协议。为了针对这一现象,运生出了Cookie(面向客户端)和Session(面向服务端)两种通用方案。 Cookie Cookie,中文名称为”小型文本文件“,指某些网站为了辨别用户而存储在用户本地终端上的数据(通常经过加密)。 用途在之前的介绍当中,我们谈及到了购物车。当用户选购一件商品的时候,服务器在向用户传递该商品页面的同时,还附加发送了一段Cookie(Response - Set-Cookie)。在用户点击加入购物车按钮时,客户端会将该商品的Cookie发送给服务器,这样,服务器就明白该用户的选购了哪些商品。最后,当用户点击查看购物车时,客户端会发送该用户的Cookie给服务器(Reuqest - Cookie),服务器就会回传该用户的购物车记录。 使用Cookie的另外一个经典场景是当登陆一个网站时,网站往往会请求用户输入用户名和密码,并且用户可以勾选“下次自动登录”。当用户勾选了,那么下次访问同一个网站的时候,用户会自动跳过登录界面,跳转至主页当中。这其中的过程就是,在前一次正常的登陆过程中,服务器回传了包含登录凭据(类似于token)的Cookie,并保存在用户的硬盘空间上,第二次访问时,客户端会自动将该Cookie发送至服务器中,从而无须再次输入凭证,即可成功登陆。 原理 常见结构 来源于Amazon 其中Cookie的内部格式常以键值对的形式进行保存,可以是系统规定的,也可以添加自定义的Cookie。以下为常用的Cookie属性。 属性 说明 NAME=VALUE 赋予 Cookie 的名称和其值(必须项) expires=DATE Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) path=PATH 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的目录) domain=域名 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) Secure 仅在 HTTPS 安全通信时才会发送 Cookie HttpOnly 加以限制,使 Cookie 不能被 Javascript 脚本访问 缺陷 Cookie会附加在客户端中的每一个HTTP请求,增加了传输流量。 Cookie中HTTP中是以明文的方式进行传递(即使进行了值加密也会以加密后的内容传送),倘若保存重要数据的Cookie被窃取(XSS-跨站式脚本),会产生严重的安全性问题。 Cookie能够发送的数量(最多20个,但通常浏览器支持会大于20个)和大小(最大为4KB)受到限制。 Session Session(会话)是一种持久网络协议,在用户(或用户代理)端和服务器端之间的创建关联,从而起到交换数据包的作用机制。 根据上述的介绍,很难将Session与Cookie进行区别。它们两者都用共同的作用——保存用户信息,但是最本质的区别在于Cookie是保留在客户端的,而Session是保留在服务器端。然而Session的实现却需要服务器端和客户端同时实现才能发挥作用。 以下以Tomcat为容器的Web应用为例。 客户端的Session 在用户的第一次发起正常的HTTP请求时,服务器端会在请求中(通常在Cookie当中)查看是否已经存在Session标识,若不存在,服务器端将会根据自动生成Session标识(常为SessionID),并在Response中设置Cookie。若存在,服务器将使用请求中的Session标识,来标识该用户。 服务器端的Session 在之后的请求当中,客户端都会附加含有SessionID的Cookie到服务器端。服务器端将鉴别该Cookie中的SessionID,赋予该发起请求的用户。 缺陷 生成Session的文件是保留在内存当中。倘若服务器每天接受千万次请求,那么服务器的资源肯定会被消耗殆尽,导致服务器不可用。通常的做法是用Redis保存用户的Session,查询时直接向Redis发出请求即可。 Session标识通常保存在Cookie当中,若浏览器禁用了Cookie,则导致服务器无法正常识别该用户。另一种可用的方式是URL重写,即直接将SessionID标识写在URL上(以QueryString的形式) 参考资料:[1] http://www.jianshu.com/p/e143ddf6fc84 [2] http://www.jianshu.com/p/2b7c10291aad [3] https://zh.wikipedia.org/wiki/Cookie [4]https://zh.wikipedia.org/zh-hans/%E4%BC%9A%E8%AF%9D_)(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"JSON笔记","slug":"JSON笔记","date":"2017-04-19T14:29:57.000Z","updated":"2017-07-31T13:23:17.000Z","comments":true,"path":"2017/04/19/JSON笔记/","link":"","permalink":"http://apparition957.github.io/2017/04/19/JSON笔记/","excerpt":"","text":"什么是 JSONJSON 的全称是 JavaScript Object Notation (JavaScript对象表示法)。它是一种独立于编程语言、轻量级的数据交换格式(类似于XML等)。 JSON 是基于 JavaScript 对象字面量的,是 JavaScript 的一个子集。详细方面可以参考 http://www.json.org/ 为什么使用JSONJSON 被认为是 XML 的很好替代者。因为 JSON 的可读性非常好,而且它没有像 XML 那样包含很多冗余的元素标签,这使得应用在使用JSON进行网络传输以及进行解析处理的速度更快,效率更高。 JSON 语法格式1234{ "name": "xiaoming", "age": 20,} JSON 以名称 - 值对(又称键 - 值)的方式存储数据。 JSON 中使用冒号(:)分隔名称和值。名称始终在左侧,值始终在右侧。 名称必须以双引号(”)括起,不能够使用单引号,亦或者不用。 从机器读取 JSON 的角度来解析 JSON 语法结构: { (左花括号)指”开始读取对象“ } (右花括号)值“结束读取对象” [ (左方括号)指“开始读取数组” ] (右方括号)指”结束读取数组“ : (冒号)指“在名称 - 值对中分隔名称和值” , (逗号)指“分隔对象中名称 - 值”或者“分隔数组中的值” JSON 数据类型 对象数据类型 JSON 本身就是对象,也就是一个被花括号包裹的名称 - 值对的列表。在 JSON 内部表示对象的方式,就如同与表示 JSON。下面 JSON 数据中包含一个 person 对象(person对象中还包含一个 hair 对象) 1234567891011{ "person": { "name": "xiaoming", "age": 20, "hair": { "color": "light blond", "length": "short" }, "eyes": "green" }} 字符串类型 JSON的值以字符串的形式表示,如同{ "animal": "cat"}中的"cat"。JSON 中的字符串可以由任何 UniCode 字符构成。字符串的两边必须被双引号包裹。 在 JSON 的值中,一对双引号代表字符串中起点与终点。如果我们在双引号内部再次使用双引号,JSON编译器就无法识别双引号后面的内容(如下 “Hello 后面的内容),就会出现报错。错误用法如下所示: 123{ "description": "Say "Hello" to everyone."} 一般来说,字符串中如果需要使用特殊符号(例如双引号),则必须使用转义符号来表示(\\)。正确用法如下所示: 123{ "description": "Say \\"Hello\\" to everyone."} 数字类型 数字是一种常见的用于传递数据的信息片段。JSON 中的数字可以是整数、浮点数(双精度)、负数或者是指数。 1234567{ "widgetInventory": 289, "sadSavingAccount": 22.59, "seattleLatitude": 47.606209, "seattleLongtitude": -122.332071, "earthMass": 5.97219e+24} 布尔值类型 1234{ "toastWithBreakfast": false, "breadWithLunch": true} null 类型 null 是一个表示“没有值”的特殊值。它表示名称对应的值为空。 1234{ "shoes": "slippers", "watch": null} 数组类型 数组类型表示,可以在形如列表的内部存储若干数组。 1234567{ "eggCarton": [ "egg", "egg", null ]} 特别需要注意的是,在 JSON 的数组内部,只存在值,而不存在数据类型等这样的说法。换句话说,就是在数组内部,可以存储任何合法的 JSON 数据类型(字符串、数字、对象、布尔值等)。 1234567{ "eggCarton": [ "egg", 5, null ]} 在 Web开发中需要注意的地方 在 HTTP 请求当中传递 JSON 时,需要设置媒体类型为 application/json。 参考资料: [1] https://book.douban.com/subject/26789960/","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"数据库连接查询","slug":"数据库连接查询","date":"2017-04-14T14:14:53.000Z","updated":"2017-07-31T13:22:49.000Z","comments":true,"path":"2017/04/14/数据库连接查询/","link":"","permalink":"http://apparition957.github.io/2017/04/14/数据库连接查询/","excerpt":"","text":"操作数据库当中,常常涉及多表查询。而在进行多表查询操作时,SQL提供了不同的表与表之间的连接方式,来获取用户所需的数据。 以下我们将主要围绕以下两个表进行数据库的多表查询操作: 123456789101112131415161718192021> -- 雇员表> select * from employee;+----+------+---------------+| id | name | department_id |+----+------+---------------+| 1 | 张三 | 1 || 2 | 李四 | 1 || 3 | 王五 | 2 || 4 | 赵六 | 2 || 5 | 郑七 | 3 |+----+------+---------------+> -- 部门表> select * from department;+----+--------+| id | name |+----+--------+| 1 | 技术部 || 2 | 技术部 || 3 | 工程部 |+----+--------+ 内连接(INNER JOIN) 内连接使用比较运算符根据每个表共有的列的值匹配两个表中的行。 123456789101112> -- 由于雇员表中郑七没有与部门表中任何一行匹配,即在返回的数据不会显示> SELECT e.id AS '员工编号', e. NAME AS '员工名称', d. NAME AS '所属部门'> FROM employee AS e> INNER JOIN department AS d ON e.department_id = d.id;+----------+----------+----------+| 员工编号 | 员工名称 | 所属部门 |+----------+----------+----------+| 1 | 张三 | 技术部 || 2 | 李四 | 技术部 || 3 | 王五 | 市场部 || 4 | 赵六 | 市场部 |+----------+----------+----------+ 左外连接(LEFT JOIN) 左向外连接的结果集包括LEFT OUTER子句中指定的左表的所有行,而不仅仅是连接列所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表列均为空值。 12345678910111213> -- 左外连接会根据左表中的所有内容与右表进行匹配,即使右表不匹配也会显示所有数据> SELECT e.id AS '员工编号', e. NAME AS '员工名称', d. NAME AS '所属部门'> FROM employee AS e> LEFT JOIN department AS d ON e.department_id = d.id;+----------+----------+----------+| 员工编号 | 员工名称 | 所属部门 |+----------+----------+----------+| 1 | 张三 | 技术部 || 2 | 李四 | 技术部 || 3 | 王五 | 市场部 || 4 | 赵六 | 市场部 || 5 | 郑七 | NULL |+----------+----------+----------+ 右外连接(RIGHT JOIN) 右向外连接是左向外连接的反向连接。将返回右表的所有行。如果右表的某行在左表中没有匹配行,则将为左表返回空值。 12345678910111213> -- 与左外连接完全相反> SELECT e.id AS '员工编号', e. NAME AS '员工名称', d. NAME AS '所属部门'> FROM employee AS e> RIGHT JOIN department AS d ON e.department_id = d.id;+----------+----------+----------+| 员工编号 | 员工名称 | 所属部门 |+----------+----------+----------+| 1 | 张三 | 技术部 || 2 | 李四 | 技术部 || 3 | 王五 | 市场部 || 4 | 赵六 | 市场部 || NULL | NULL | 工程部 |+----------+----------+----------+ 全外连接(FULL JOIN) 完整外部连接返回左表和右表中的所有行。当某行在另一个表中没有匹配行时,则另一个表的选择列表列包含空值。如果表之间有匹配行,则整个结果集行包含基表的数据值。 1234567891011121314> -- MYSQL不支持全外连接,这个例子为手写例子> SELECT e.id AS '员工编号', e. NAME AS '员工名称', d. NAME AS '所属部门'> FROM employee AS e> FULL JOIN department AS d ON e.department_id = d.id;+----------+----------+----------+| 员工编号 | 员工名称 | 所属部门 |+----------+----------+----------+| 1 | 张三 | 技术部 || 2 | 李四 | 技术部 || 3 | 王五 | 市场部 || 4 | 赵六 | 市场部 || 5 | 郑七 | NULL || NULL | NULL | 工程部 |+----------+----------+----------+ 参考资料: [1] http://www.cnblogs.com/devilmsg/archive/2009/03/24/1420543.html [2] http://www.cnblogs.com/youzhangjin/archive/2009/05/22/1486982.html","categories":[{"name":"MySQL","slug":"MySQL","permalink":"http://apparition957.github.io/categories/MySQL/"}],"tags":[]},{"title":"使用Thymeleaf遇到的问题及解决","slug":"使用Thymeleaf遇到的问题及解决","date":"2017-04-14T11:44:48.000Z","updated":"2017-07-31T13:22:28.000Z","comments":true,"path":"2017/04/14/使用Thymeleaf遇到的问题及解决/","link":"","permalink":"http://apparition957.github.io/2017/04/14/使用Thymeleaf遇到的问题及解决/","excerpt":"","text":"**使用th:each时,无法作用于循环最开始处获取循环体。 虽然像大多数的循环中都是无法在循环开始处就获得循环体,但仍有时候,根据某些框架的特性,需要在开始处进行属性设置。 以下示例中,想要在每次循环中在 tr 中根据 user 中的值 ${user.id} 设置 rel属性。 12345678<!-- 以下方式不会有用,也不会报错 --><tbody> <tr th:each="user: ${users}" rel="${user.id}" > <td th:text="${user.id}"></td> <td th:text="${user.username}"></td> <td th:text="${user.password}"></td> </tr></tbody> 如果想要必须设置的话,就需要在后期利用JS代码手动实现。 thymeleaf 提供的自定义属性设置在大多数时候不会有效。 在官方提供的例子中,用户在使用 thymeleaf 时可以对自定义属性来进行赋值。以下为官方示例: 1234567891011<!-- Thymeleaf offers a default attribute processor that allows us to set the value of any attribute, even if no specific th:* processor has been defined for it at the Standard Dialect.--><!-- So something like: --><span th:whatever="${user.name}">...</span><-- Will result in: --><span whatever="John Apricot">...</span> 但在实际使用过程中,这个特性不尽人意,从后台获取的值有时无法利用上面的方式作用到页面中。 有个可取的,稳定的解决方法如下所示: 12345<!-- 利用 th:attr 将需要自定义属性写在里面 --><span th:attr="whatever=${user.name}, another=${user.age}">...</span><!-- 实际效果 --><span whatever="John Apricot" another="23">...</span> 设置表单中的action属性时,注意花括号的引用 在设置URL地址时,thymeleaf 推荐使用@符号:@{...}。 以下示例指明了需要注意的地方: 12345<!-- Will produce '/gtvg/order/details?orderId=3' --><a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a><!-- Will produce '/gtvg/order/3/details' --><a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"Linux命令 - netstat","slug":"Linux命令-netstat","date":"2017-04-07T09:49:54.000Z","updated":"2017-07-31T13:34:30.000Z","comments":true,"path":"2017/04/07/Linux命令-netstat/","link":"","permalink":"http://apparition957.github.io/2017/04/07/Linux命令-netstat/","excerpt":"","text":"简介 netstat - 显示各种网络连接的相关信息,如网络连接(network connections)、路由表(routing tables)、接口状态(interface statistics)和多播成员(multicast memberships)。 基本语法1netstate [-a][-t][-u][-x][-n][-l][-p][-r][-e][-s][-c] 基本参数解释: 参数 作用 -a –all 显示所有连接和未连接的端口 -t –tcp 仅显示TCP套接字连接的端口 -u –udp 仅显示UDP套接字连接的端口 -x -unix 仅显示UNIX套接字连接的端口 -n –numeric 以完整名称的方式(即数字)显示所有端口 -l –listening 仅显示所有已连接的端口 -p –program 额外显示PID(进程ID)/Program name(进程名称) -r –route 显示路由表(routing table) -e –extend 显示网络连接的额外信息(如User) -s –statistics 显示网络连接的统计信息 -c –continuous 持续列出网络状态 常用基本操作 显示所有端口 123456789101112131415161718192021222324# netstat -aActive Internet connections (servers and established)Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 localhost:6379 *:* LISTENtcp 0 0 *:sunrpc *:* LISTENtcp 0 0 localhost:6379 localhost:36240 ESTABLISHEDtcp 1 0 172.16.8.69:38720 123.58.173.186:http CLOSE_WAITtcp 0 0 localhost:36000 localhost:6379 ESTABLISHEDtcp 0 0 localhost:36241 localhost:6379 ESTABLISHEDtcp 0 0 *:39395 *:* LISTENtcp 0 0 localhost:mxi *:* LISTENtcp 0 0 *:8009 *:* LISTENudp 0 0 *:sunrpc *:*udp 0 0 *:ipp *:*udp 0 0 192.168.122.1:ntp *:*udp 0 0 192.168.122.1:domain *:*Active UNIX domain sockets (servers and established)Proto RefCnt Flags Type State I-Node Pathunix 13 [ ] DGRAM 507392 /dev/logunix 2 [ ACC ] STREAM LISTENING 507758 /var/run/rpcbind.sockunix 2 [ ACC ] STREAM LISTENING 15533 /var/run/dbus/system_bus_socketunix 2 [ ACC ] STREAM LISTENING 18496 @/tmp/gdm-greeter-jHQBuBtcunix 2 [ ACC ] STREAM LISTENING 22717 @/tmp/dbus-Fzbdj98FAc... 仅显示TCP端口(同理于仅显示UDP端口) 123456789101112131415161718192021222324252627282930# netstat -t # 仅显示所有已建立连接的TCP端口Active Internet connections (w/o servers)Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 localhost:35206 localhost:6379 ESTABLISHEDtcp 0 0 localhost:35286 localhost:6379 ESTABLISHEDtcp 0 0 localhost:36227 localhost:6379 ESTABLISHEDtcp 0 0 localhost:6379 localhost:36306 ESTABLISHEDtcp 0 0 localhost:6379 localhost:35286 ESTABLISHEDtcp 0 0 localhost:6379 localhost:36226 ESTABLISHEDtcp 0 0 localhost:6379 localhost:36227 ESTABLISHEDtcp 0 0 localhost:6379 localhost:36000 ESTABLISHEDtcp 0 0 localhost:35201 localhost:6379 ESTABLISHEDtcp 0 0 localhost:6379 localhost:35201 ESTABLISHEDtcp 0 0 localhost:6379 localhost:35206 ESTABLISHED...# netstat -at # 显示所有TCP端口Active Internet connections (servers and established)Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 localhost:6379 *:* LISTENtcp 0 0 *:sunrpc *:* LISTENtcp 0 0 localhost:webcache *:* LISTENtcp 0 0 *:52912 *:* LISTENtcp 0 0 192.168.122.1:domain *:* LISTENtcp 0 0 *:ssh *:* LISTENtcp 0 0 localhost:ipp *:* LISTENtcp 0 0 localhost:smtp *:* LISTENtcp 0 0 *:rxapi *:* LISTENtcp 0 0 localhost:35206 localhost:6379 ESTABLISHEDtcp 0 0 localhost:35286 localhost:6379 ESTABLISHED 显示所有处于监听状态(LISTEN)的端口 123456789101112131415161718# netstat -lActive Internet connections (only servers)Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 localhost:6379 *:* LISTENtcp 0 0 localhost:smtp *:* LISTENtcp 0 0 *:39395 *:* LISTENtcp 0 0 localhost:mxi *:* LISTENtcp 0 0 *:8009 *:* LISTENudp 0 0 *:sunrpc *:*udp 0 0 *:ipp *:*udp 0 0 192.168.122.1:ntp *:*udp 0 0 172.16.8.69:ntp *:*Active UNIX domain sockets (only servers)Proto RefCnt Flags Type State I-Node Pathunix 2 [ ACC ] STREAM LISTENING 507758 /var/run/rpcbind.sockunix 2 [ ACC ] STREAM LISTENING 15533 /var/run/dbus/system_bus_socketunix 2 [ ACC ] STREAM LISTENING 18496 @/tmp/gdm-greeter-jHQBuBtc... 显示所有端口的统计信息 123456789101112131415161718192021222324252627# netstat -sIp: 6626695 total packets received 6625 with invalid addresses 0 forwarded 0 incoming packets discarded 5055725 incoming packets delivered 4377026 requests sent out 96 dropped because of missing route...Tcp: 3196 active connections openings 1666 passive connection openings 382 failed connection attempts 39 connection resets received 22 connections established 5043080 segments received 4352432 segments send out 6833 segments retransmited 73 bad segments received. 443 resets sentUdp: 10653 packets received 80 packets to unknown port received. 0 packet receive errors 14349 packets sent... 利用数字取代别名(主机、用户名),并加速输出所有网络连接信息 1234567891011121314# netstat -nActive Internet connections (w/o servers)Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 127.0.0.1:35206 127.0.0.1:6379 ESTABLISHEDtcp 0 0 127.0.0.1:35286 127.0.0.1:6379 ESTABLISHEDtcp 0 0 127.0.0.1:36227 127.0.0.1:6379 ESTABLISHEDtcp 0 0 127.0.0.1:6379 127.0.0.1:36306 ESTABLISHEDtcp 0 0 127.0.0.1:6379 127.0.0.1:35286 ESTABLISHEDtcp 0 0 127.0.0.1:6379 127.0.0.1:36226 ESTABLISHED...# netstat --numeric-ports # 仅替换端口别名# netstat --numeric-hosts # 仅替换主机别名# netstat --numeric-users # 仅替换用户别名 持续显示网络连接状况(以秒为单位,默认为1秒) 12345678# netstat -c 60 # 每隔60秒运行一次Active Internet connections (w/o servers)Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 localhost:35206 localhost:6379 ESTABLISHEDtcp 0 0 localhost:35286 localhost:6379 ESTABLISHEDtcp 0 0 localhost:36227 localhost:6379 ESTABLISHEDtcp 0 0 localhost:6379 localhost:36306 ESTABLISHED... 显示核心路由信息 123456# netstat -rKernel IP routing tableDestination Gateway Genmask Flags MSS Window irtt Iface192.168.0.1 * 255.255.255.0 U 0 0 0 eth0192.168.122.0 * 255.255.255.0 U 0 0 0 virbr0default 192.168.0.1 0.0.0.0 UG 0 0 0 eth0 与其他命令搭配使用 显示程序所占用的端口 12345# netstat -ap | grep sshtcp 0 0 *:ssh *:* LISTEN 18116/sshdtcp 0 0 173.15.1.50:ssh 113.56.213.179:newheights ESTABLISHED 6111/sshdtcp 0 0 *:ssh *:* LISTEN 18116/sshd... 参考资料: [1] http://www.cnblogs.com/ggjucheng/archive/2012/01/08/2316661.html","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"死锁的概念","slug":"死锁的概念","date":"2017-04-06T12:20:19.000Z","updated":"2017-07-31T13:21:38.000Z","comments":true,"path":"2017/04/06/死锁的概念/","link":"","permalink":"http://apparition957.github.io/2017/04/06/死锁的概念/","excerpt":"","text":"概述 由 Dijkstra 提出的哲学家进餐问题(The Dinning Philosophers Problem)是典型的进程同步问题。该问题描述的是有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五支筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,在饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子的时候才能够进餐。进餐完毕后,放下筷子继续思考。 在上述问题中,假设五个哲学家同时进入饥饿的状态,会同时拿起身边左边最靠近自己的筷子,但当有一个哲学家想要拿起右边最靠近自己的筷子时,由于该筷子已被拿起,并且获取该筷子的哲学家不放下,而造成无限地等待,这种特殊现象就被称之为死锁。 定义每个进程所等待的事件是该组中其他进程释放所占有的资源,由于所有这些进程处于阻塞态,都无法正常运行,因此它们谁也不能释放资源,致使没有任何一个进程可被唤醒。这样这组进程只能无限期地等待下去。由此可以给死锁做出如下的定义: 如果一组进程中的每一个进程都在等待仅由该组进程中的其他进程才能引发的时间,那么该组进程是死锁(Deadlock)。 产生死锁的必要条件虽然进程在运行过程中可能会发生死锁,但产生进程死锁是必须具备一定条件的。综上所述,产生死锁必须同时具备下面四个必要条件,只要其中任意一个条件不成立,死锁就不会发生。 互斥条件。进程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个进程占用。如果此时还有其它进程请求该资源,则请求进程只能等待,直至占有该资源的进程使用完后,进行释放。 请求和保持条件。进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。 不可抢占条件。进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时自己释放。 循环等待条件。在发生死锁时,必然存在一个进程-资源的循环链,即进程集合 {A, B, C, D, ..} 中 A 正在等待一个 B 占用的资源,B 正在等待 C 占用的资源,… ,Z 正在等待已被 A 占用的资源。 处理死锁的方法目前处理死锁的方法可归纳总结为四种: 预防死锁。这是一个较简答和直观的事先预防方法。该方法是通过设置某些限制条件,去破坏产生死锁四个条件的一个或几个来预防死锁。预防死锁是一种较易实现的方法,已被广泛使用。 避免死锁。同样是属于事先预防策略,但它并不是事先采取各种限制措施,去破坏产生死锁的四个必要条件,而是在资源的动态分配中,用某些方法防止系统进入不安全状态,从而避免发生死锁。 监测死锁。这种方法无须事先采取任何限制性措施,而是允许进程在运行过程中发生死锁。但该方法可以通过检测机制及时地检测出死锁的发生,根据检测结果,采取某些适当的措施。 解除死锁。当检测到系统中已发生死锁时,就采取相应措施,将进程从死锁状态中释放。常用的方法是撤销一些进程,强制回收它们的资源,将它们分配给已处于阻塞状态的进程,使其能够继续运行。 上述的四种方法,从(1)到(4)对死锁的防范程度逐渐减弱,但相对应的是对资源利用率的提高,以及进程因资源因素而阻塞的频率下降(即并发程度提高)。","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"设计模式笔记 - 中介者模式(Mediator Pattern)","slug":"设计模式笔记-中介者模式(Mediator-Pattern)","date":"2017-03-29T14:08:18.000Z","updated":"2017-07-31T13:21:00.000Z","comments":true,"path":"2017/03/29/设计模式笔记-中介者模式(Mediator-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/29/设计模式笔记-中介者模式(Mediator-Pattern)/","excerpt":"","text":"模式定义 中介者模式(Mediator Pattern)用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使耦合松散,而且可以独立地改变它们之间的交互。 中介者模式又称为调停者模式,它是一种对象行为型模式。 模式结构中介者模式包含如下角色: Mediator - 抽象中介者 ConcreteMediator - 具体中介者 Colleague - 抽象同事类 ConcreteColleague - 具体同事类 模式实例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125/* * 本例为建立一个虚拟聊天室。允许会员通过该聊天室进行信息交流,普通会员(Common Member)可以给其他会员发 * 送文本信息,钻石会员(Diamond Member)除了可以发送文本消息外,还可以发送图片。 */// Mediator - 抽象中介者public abstract class AbstractChatroom { private List<Member> members; public AbstractChatroom() { this.members = new ArrayList<Member>(); } public void register(Member member) { this.members.add(member); } public List<Member> getMembers() { return this.members; } public abstract void sendText(String text); public abstract void sendImage(Photo photo);} // ConcreteMediator - 具体中介者public class ChatGroup extends AbstractChatroom { public void register(Member member) { super.register(member); member.setChatroom(this); } public void sendText(String text) { List<Member> members = getMembers(); for (Member member: members) { member.receiveText(text); } } public void sendImage(Photo photo) { List<Member> members = getMembers(); for (Member member: members) { member.receiveImage(photo); } }}// Colleague - 抽象同事类public abstract class Member { private String name; private AbstractChatroom chatroom; public Member(String name) { this.name = name; } public void setChatroom(AbstractChatroom chatroom) { this.chatroom = chatroom; } public AbstractChatroom getChatroom() { return this.chatroom; } public void receiveImage(Photo photo) { System.out.println(name + " receive photo: " + photo.getName()); } public void receiveText(String text) { System.out.println(name + " receive text: " + text); } public void sendImage(Photo photo) { throw new IllegalStateException("you don't have an access"); } public void sendText(String text) { this.chatroom.sendText(text); }}// ConcreteColleague - 具体同事类// 普通会员public class CommonMember extends Member { public CommonMember(String name) { super(name); } public void sendText(String text) { getChatroom().sendText(text); }}// 钻石会员public class DiamondMember extends Member { public DiamondMember(String name) { super(name); } public void sendText(String text) { getChatroom().sendText(text); } public void sendImage(Photo photo) { getChatroom().sendImage(photo); }}// 测试类public class Text { public static void main(String[] args) { AbstractChatroom chatroom = new ChatGroup(); Member common = new CommonMember("Peter"); Member diamond = new DiamondMember("Mary"); chatroom.register(common); chatroom.register(diamond); common.sendText("Hello"); diamond.sendImage(new Photo("Cool")); System.out.println("END"); }} 模式分析 中介者模式可以使对象之间的关系数量急剧减少。 通过引入中介者对象,可以将系统的网状结构变成以中介者为中心的星型结构,中介者承担了中转作用和协调作用。中介者是中介者模式的核心,它对整个系统进行控制和协调,简化了对象之间的交互,还可以对对象间的交互进行进一步的控制。 中介者模式的优点: 简化对象之间的引用关系,降低对象间的耦合。 集中式管理,以中介者为中心,协调各对象之间的关系。 中介者模式的缺点: 在具体中介者类中包含了同事之间的交互细节,增加了具体中介者的复杂度,如果系统中具有太多的同时,会使得系统变得难以维护。 适用环境在以下情况下可以使用中介者模式: 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解。 一个对象由于引用了其他很多对象并且直接和这些对象通信,即耦合程度大,导致难以复用该对象。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/mediator.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 建造者模式(Builder Pattern)","slug":"设计模式笔记-建造者模式(Builder-Pattern)","date":"2017-03-29T06:37:41.000Z","updated":"2017-07-31T13:19:52.000Z","comments":true,"path":"2017/03/29/设计模式笔记-建造者模式(Builder-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/29/设计模式笔记-建造者模式(Builder-Pattern)/","excerpt":"","text":"模式定义 建造者模式(Builder Pattern)将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户无需知道内部的具体构建细节。 建造者模式属于对象创建型模式。 模式结构建造者模式包含如下角色: Builder - 抽象建造者 - 定义了产品的创建方法和返回方法 ConcreteBuilder - 具体建造者 Director - 指挥者 - 隔离了客户与生产过程,并负责控制产品的生成过程 Product - 产品角色 模式实例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100/* * 本例为McDonald's中超值套餐系列构造。虽然不同的超值套餐都自己不同的特点,但是总体的构造成分都是一致的,主食(汉堡)以及饮料(可乐)等。 */// Product - 产品角色public class ValueMeal { private Meal meal; private Drink drink; public void setMeal(Meal meal) { this.meal = meal; } public void setDrink(Drink drink) { this.drink = drink; } public Meal getMeal(Meal meal) { return this.meal; } public Drink getDrink(Drink drink) { return this.drink; }}// Builder - 抽象建造者public interface class valueMealBuilder { public void buildMeal(Meal meal); public void buildDrink(Drink drink); public ValueMeal getValueMeal(); }// ConcreteBuilder - 具体建造者// 麦辣鸡腿堡public class McCrispyPackage implements valueMealBuilder { private ValueMeal valueMeal = new ValueMeal(); public void buildMeal() { valueMeal.setMeal(new McCrispyChickenBurger()); } public void buildDrink() { valueMeal.setDrink(new CocaCola()); } public ValueMeal getValueMeal() { return this.valueMeal; } }// ConcreteBuilder - 具体建造者// 巨无霸public class BigMacPackage implements valueMealBuilder { private ValueMeal valueMeal = new ValueMeal(); public void buildMeal() { valueMeal.setMeal(new BigMac()); } public void buildDrink() { valueMeal.setDrink(new CocaCola()); } public ValueMeal getValueMeal() { return this.valueMeal; } }// Direct - 指挥者public class McDonaldWaiter { private ValueMealBuilder valueMealBuilder; public McDonaldWaiter(ValueMealBuilder valueMealBuilder) { this.valueMealBuilder = valueMealBuilder; } public void construct() { valueMealBuilder.buildMeal(); valueMealBuilder.buildDrink(); } public ValueMeal getValueMeal() { valueMealBuilder.getValueMeal(); } public void setValueMealBuilder(ValueMealBuilder valueMealBuilder) { this.valueMealBuilder = valueMealBuilder; }}// 测试类public class Test { public static void main(String[] args) { ValueMealBuilder mcCrispyPackage = new McCrispyPackage(); McDonaldWaiter waiter = new McDonaldWaiter(mcCrispyPackage); waiter.construct(); ValueMeal mcCrispy = waiter.getValueMeal(); }} 模式分析 建造者模式的重心在于分离构建算法和具体的构造实现,从而使得构建算法可以重用,具体的构造实现可以方便地扩展和切换,从而可以灵活地组合来构造出不同的产品对象。 增加新的具体建造者无需修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性较大,则不适合使用建造者模式,因此其适用范围受到一定的限制。 建造者模式的优点: 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此很方便地替换不同的具体建造者或增加新的具体建造者。 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法当中,使得构建者可以更加专注于逻辑处理部分。 建造者模式的缺点: 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,增加系统复杂度,难以维护。 模式应用 在游戏软件设计中,不同的地图包括天空、地面和背景等组成部分,不同的人物也包括身体,服装和装备等组成部分,可以利用建造者模式对其设计,通过不同的具体建造者创建不同类型的地图或人物。 与抽象工厂模式的对比: 与抽象工厂模式相比,建造者模式返回一个组装好的完整产品,可以类比于汽车组装工厂。而抽象工厂模式返回一系列的相关产品,这些产品位于不同的产品等级结构,构成了一个产品族,可以类比于汽车配件生产工厂。 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中,客户端可以不直接调用具体建造者的相关方法,而是通过指挥者类来指导如何生成对象,它侧重于如何一步步构建一个复杂对象,返回一个完整的对象。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/builder.html [2] http://www.cnblogs.com/java-my-life/archive/2012/04/07/2433939.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 状态模式(State Pattern)","slug":"设计模式笔记-状态模式(State-Pattern)","date":"2017-03-28T01:33:22.000Z","updated":"2017-07-31T01:46:38.000Z","comments":true,"path":"2017/03/28/设计模式笔记-状态模式(State-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/28/设计模式笔记-状态模式(State-Pattern)/","excerpt":"","text":"设计模式笔记 - 状态模式(State Pattern)模式定义 状态模式(State Pattern)允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式是一种对象行为型模式。 模式结构状态模式包含如下角色: Context - 环境类 State - 抽象状态类 ConcreteState - 具体状态类 模式实例1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071/* * 此例举了个游戏人物的状态变化时发生动作的例子,当人物活着时,可以进行攻 * 击。当人物死亡时,可以选择复活。而无论人处于什么状态,都能够进行购物。 */// Context - 环境类public class GamePlayer { private GamePlayerState gamePlayerState; public GamePlayer(GamePlayerState gamePlayerState) { this.gamePlayerState = gamePlayerState; } public void attack() { gamePlayerState.attack(); } public void reborn() { gamePlayerState.reborn(); } public void buy() { System.out.println(\"buying sth.\"); } public void changeState(GamePlayerState gamePlayerState) { this.gamePlayerState = gamePlayerState; }}// State - 抽象状态类public abstract class GamePlayerState { public abstract void attack(); public abstract void reborn();}// ConcreteState - 具体抽象类// 活着public class Alive extends GamePlayerState { public void attack() { System.out.println(\"Attacking!\"); } public void reborn() { System.out.println(\"You are alive!\"); }}// 死亡public class Death extends GamePlayerState { public void attack() { System.out.println(\"You are dead!\"); } public void reborn() { System.out.println(\"Alive for attack!\"); }}// 测试类public class Test { public static void main(String[] args) { GamePlayerState alive = new Alive(); GamePlayerState death = new Death(); GamePlayer gamePlayer = new GamePlayer(alive); gamePlayer.attack(); gamePlayer.changeState(death); gamePlayer.attack(); gamePlayer.reborn(); }} 模式分析 状态模式描述了对象状态的变化,以及对象如何在每一种状态下表现出不同的行为。 状态模式的关键是导入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同的具体状态类中实现了不同状态的行为,包括各种状态之间的转换。 在状态模式结构中需要理解环境类与抽象状态类的作用: 环境类 - 实际上就是拥有状态的对象,环境类有时候可以充当状态管理器(State Manager)的角色,可以在环境类中对状态进行切换操作。 抽象状态类 - 可以是抽象类,也可以是接口,不同状态类就是继承这个父类的不同子类,状态类的产生是由于环境类存在多个状态,同时还满足两个条件:一是这些状态经常需要切换,二是在不同状态下对象的行为不同。因此可以将不同对象下的行为单独提取出来封装在具体的状态类中,使得环境类对象在其内部状态改变时可以改变它的行为,让对象看起来似乎修改了它的类,而实际上是由于切换到不同的具体状态类实现的。 状态模式的优点: 封装了转换规则。 将所有与某个状态有关的行为放到同一个类中,并且方便地添加新的状态,只需改变对象状态,即可改变对象的行为。 允许状态转换逻辑与状态对象合为一体,舍弃巨大的逻辑条件块(if..else..) 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。 状态模式的缺点: 状态模式的使用必然会增加系统类和对象的个数。 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。 模式应用 状态模式在工作流或游戏等类型的软件中得以广泛使用,甚至可以用于这些系统的核心功能设计,如在政府OA办公系统中,一个批文的状态有多种:尚未办理,正在办理,正在批示,正在审核,已经完成等各种状态,而且批文状态不同时对批文的操作也有所差异。使用状态模式可以描述工作流对象(如批文)的状态转换以及不同状态下它所具有的行为。 模式扩展共享状态 在有些情况下多个环境对象需要共享同一个状态,如果希望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/state.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"SQL - GROUP BY 和 HAVING","slug":"SQL-GROUP-BY-和-HAVING","date":"2017-03-27T12:16:44.000Z","updated":"2017-07-31T13:18:09.000Z","comments":true,"path":"2017/03/27/SQL-GROUP-BY-和-HAVING/","link":"","permalink":"http://apparition957.github.io/2017/03/27/SQL-GROUP-BY-和-HAVING/","excerpt":"","text":"GROUP BYGROUP BY 简介GROUP BY 语句根据字面上的意思为“根据(by)一定的规则进行分组(group)”。它的主要作用是通过一定的规则将一个数据集划分成若干个小的区域,然后针对若干个小的区域进行数据处理。其常常结合聚合函数(COUNT/SUM)等一起使用。 GROUP BY 语法1234SELECT column_name, aggregate_function(column_name)FROM table_nameWHERE column_name operator valueGROUP BY column_name GROUP BY 使用 GROUP BY [EXPERSSIONS]: 数据集根据表达式中的若干字段将一个数据集划分成不同的分组。 12345678910111213-- 以下例子为,统计某人在全国范围中购买的地产总数SELECT owner.Name, COUNT(*) AS '购买总数' FROM registration LEFT JOIN owner ON owner.PersonID = registration.PersonIDGROUP BY registration.PersonID+------+----------+| Name | 购买总数 |+------+----------+| 小明 | 2 || 小红 | 1 || 小刚 | 2 |+------+----------+ 以上可以SQL语句可以解释为“按照房产登记表(regisration)中的身份证(PersonID)将数据集进行分组,然后按照分组来分别统计出各自的记录数量”。 GROUP BY [EXPERSSIONS] WITH ROLLUP 指定在结果集内不仅包含由 GROUP BY 提供的正常行,还包含汇总行。按层次结构顺序,从组内的最低级别到最高级别汇总组。组的层次结构取决于指定分组列时所使用的顺序。更改分组列的顺序会影响在结果集内生成的行数。”按层结结构顺序“这段话是指按照 GROUP BY 语句中字段的顺序进行排序,比如 GROUG BY column1,column2, column3,那么这个分组的级别从高到低的顺序是 column1 > column2 > column3。 1234567891011121314151617181920-- 以下例子为,统计全国各类型房产的销售面积情况select ESTATE.EstateCity, ESTATE.EstateType, SUM(ESTATE.PropertyArea) AS '销售面积' FROM ESTATE LEFT JOIN REGISTRATION ON REGISTRATION.EstateID=ESTATE.EstateID GROUP BY ESTATE.EstateCity, ESTATE.EstateType WITH ROLLUP+------------+------------+----------+| EstateCity | EstateType | 销售面积 |+------------+------------+----------+| 北京市 | 住宅 | 71.00 || 北京市 | NULL | 71.00 || 天津市 | 住宅 | 50.00 || 天津市 | NULL | 50.00 || 惠州市 | 别墅 | 170.00 || 惠州市 | NULL | 170.00 || 成都市 | 住宅 | 95.00 || 成都市 | 商铺 | 500.00 || 成都市 | NULL | 595.00 || NULL | NULL | 886.00 |+------------+------------+----------+ 查询结果的第一句、第二句为WITH ROLLUP分析的第一组,可以解释为在北京市中,所有类型的房产总销售面积的总结。而查询结果中的最后一句解释为在全国中,所有类型的房产总销售面积的总结。 GROUP BY 语句容易出现的错误通过以下语句利用 GROUP BY 进行查询的时候,会出现错误。 12345-- 利用城市进行分类返回查询到的所有数据select ESTATE.EstateCity, *, SUM(ESTATE.PropertyArea) AS '销售面积' FROM ESTATE LEFT JOIN REGISTRATION ON REGISTRATION.EstateID=ESTATE.EstateID GROUP BY ESTATE.EstateCity 这是利用 GROUP BY 进行单表查询,抑或者多表查询时需要注意的点。在需要查询的字段中,即在返回集字段中,这些字段要么就要包含在 GROUP BY 后面,作为分组的依据。要么就要被包含在聚合函数(COUNT/SUM等)中。 不同平台出现的错误不同,如SQL SERVER返回的错误为: 123> 消息 8120,级别 16,状态 1,第 1 行> 选择列表中的列 'ESTATE.EstateID' 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中。> 在MySQL中,返回的错误为: 123> [Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '*, SUM(ESTATE.PropertyArea) AS '销售面积' FROM ESTATE > LEFT JOIN REGISTRAT' at line 1> HAVINGHAVING 简介Having 和 GROUP BY 设置条件的方式与 WHERE 和 SELECT 的交互方式类似。WHERE 搜索条件在进行分组操作之前应用。而 HAVING 搜索条件在进行分组操作之后应用。 HAVING 语法与 WHERE 语法类似,但 HAVING 中可以包含聚合函数并可以引用选择列表中显示的任一项。 HAVING 语法12345SELECT column_name, aggregate_function(column_name)FROM table_nameWHERE column_name operator valueGROUP BY column_nameHAVING aggregate_function(column_name) operator value HAVING 使用HAVING 语句一般与GROUP BY搭配使用,没什么特别用法。 123456789101112131415-- 以下例子为,统计各地城市中各类住宅的总销售面积小于250平方米select ESTATE.EstateCity, ESTATE.EstateType, SUM(ESTATE.PropertyArea) AS '销售面积' FROM ESTATE LEFT JOIN REGISTRATION ON REGISTRATION.EstateID=ESTATE.EstateID GROUP BY ESTATE.EstateCity, ESTATE.EstateTypeHaving SUM(ESTATE.PropertyArea) < 250+------------+------------+----------+| EstateCity | EstateType | 销售面积 |+------------+------------+----------+| 北京市 | 住宅 | 71.00 || 天津市 | 住宅 | 50.00 || 惠州市 | 别墅 | 170.00 || 成都市 | 住宅 | 95.00 |+------------+------------+----------+ 参考资料: [1] http://www.cnblogs.com/glaivelee/archive/2010/11/19/1881381.html [2]http://blog.csdn.net/qq_26562641/article/details/53301063","categories":[{"name":"MySQL","slug":"MySQL","permalink":"http://apparition957.github.io/categories/MySQL/"}],"tags":[]},{"title":"正向代理与反向代理","slug":"正向代理与反向代理","date":"2017-03-27T07:14:40.000Z","updated":"2017-07-31T13:17:45.000Z","comments":true,"path":"2017/03/27/正向代理与反向代理/","link":"","permalink":"http://apparition957.github.io/2017/03/27/正向代理与反向代理/","excerpt":"","text":"概念正向代理(Forward Proxy)是一个位于客户端(Client)和目标服务器(Server)之间的代理服务器。为了从原始服务器取得内容,客户端向代理服务器发送一个请求并指定目标服务器,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端。 反向代理(Reverse Proxy)与正向代理恰恰相反,目标服务器是以反向代理服务器来接受客户端的连接请求,使客户端认为其是目标服务器。客户端向反向代理服务器发送请求,然后自身将请求转发给内部网络上的目标服务器,并将从目标服务器上得到的结果返回给客户端。 区别正向代理需要客户主动设置代理服务器ip或者域名进行访问,由设置的服务器ip或域名去访问内容并返回。而反向代理不需要客户做任何设置,直接访问代理服务器真实ip或域名,代理服务器在内网中根据访问内容进行跳转和内容返回,客户无需知道最终访问的是哪些机器。 正向代理是代理客户端,真正服务的对象是客户端,使真实客户端对服务器不可见。而反向代理是代理服务器端,真正服务的对象是服务器,使真实服务器对客户端不可见。 正向代理 反向代理 此图为综合例子,客户端可以通过正向代理绕过GFW访问某网站,其实客户端访问的是该网站的反向代理服务器。 从用途上区分: 正向代理:正向代理为在防火墙内的局域网提供访问Internet的途径。还可以使用缓冲减少网络使用率。 反向代理:反向代理将防火墙后面的服务器提供Internet用户访问。还可以完成诸如负载均衡,CDN等功能。 从安全上区分: 正向代理:正向代理允许客户端通过它访问任意网站并且隐蔽客户端自身,因此服务器端需要采取安全措施来确保仅为经过授权的客户端提供服务。 反向代理:反向代理隐藏局域网内的所有目标服务器,并对外界提供统一的接口,使得客户无需知道内部细节。从客户的角度说,他自身认为访问的就是原始服务器。 使用场景正向代理 无法访问服务器 客户端无法通过正常的渠道访问一些网站时,可以通过代理服务器接入其他网路节点去访问,然后将内容数据一并返回客户端。以上解释俗称“翻墙”。 加速访问服务器 假设用户A到服务器B,需要经过很多路由器节点,并且其路由链路为低速带宽,可想而知,获取服务器B内容的时延将无法估计。但代理服务器C与服务器B的路由链路为高速带宽,继而用户A可以通过代理服务器C提高访问服务器B的速度。 缓存内容(缓冲) 用户A第一次访问服务器B的内容时,代理服务器C可以将服务器B的内容暂时缓存起来,在用户A通过代理服务器C再次访问时,可以直接从代理服务器C中获取内容,降低服务器B的压力。 缓冲这一部分,其实不管正向代理和反向代理都会有应用,反向代理中有CDN等,范围比较广泛,界限也比较模糊。 反向代理 保护和隐藏原始资源服务器 负载均衡 加密和SSL加速 缓存静态内容 压缩 减速上传 安全 外网发布 下面做对两个功能进行简单的解释: 保护和隐藏原始资源服务器由于防火墙的作用,客户端不能够直接与目标服务器进行通信。只能够通过暴露在外面的经过认证的代理服务器来访问目标服务器。 负载均衡反向代理服务器通过负载均衡的技术,将客户端的每次访问请求,均匀分配到集群中的不同服务器。 参考资料: [1] https://zhuanlan.zhihu.com/p/25423394 [2] http://z00w00.blog.51cto.com/515114/1031287","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"开学近期有感而发的总结","slug":"开学近期有感而发的总结","date":"2017-03-26T07:15:07.000Z","updated":"2017-07-30T04:16:04.000Z","comments":true,"path":"2017/03/26/开学近期有感而发的总结/","link":"","permalink":"http://apparition957.github.io/2017/03/26/开学近期有感而发的总结/","excerpt":"","text":"最近心情不是很好,发一点小牢骚顺便尝试总结一下吧。 学习上,这学期搬进了新的实验室,也添加了许多新的人,不少研一、大三的学长,但更多的是同届的同学。前几周老师布置了任务去学习微服务架构的系列,从而我也接触了很多与以往不同的知识,单层架构的局限,大型网站架构的演化等。老实说,压力也是挺大的,对于这么多新知识,看的书都是走马观花,外加上要上课和写作业,事实上没有足够的时间去系统的消化这些知识,这一点个人认为比较遗憾 ,以后有时间一定要做笔记进行系统的补充。这周老师布置了五个任务,分两人一组共五组来完成,我和另外一个搭档挑选了最感兴趣的中间件任务,我比较喜欢中间件,在于它能够使不同模块之间的耦合降到最低,最大化地控制并发量,但最大的原因或许是感兴趣吧,觉得中间件要做起来是一件很有成就感的事情。 学业上,由于大二转了专业,所以在这一学期我需要补上之前专业没有的课程,而且这门课程只能在新校区上,并且还得翘了五六节的课跑去别的校区上七八节的课。但每次坐在去新校区的校巴上,总有一种很愉悦的感觉,毕竟这个方向才是我想要的,在这里我可以更好的发展,上自己感兴趣的课,这样一想,就觉得很值。还有一件很值得说的是,新校区的图书馆中空地带的天花板很漂亮,中午在那学习的心情都很不错。 生活上,宿舍的关系越来越好,新的宿友比以前来的时候想象的都很不同,每个都很好,就是爱玩游戏,跟我一样,虽然每次我都想过说好好学习,总是忍不了就去玩两把去学习。 就这样吧。","categories":[{"name":"不正常的日常","slug":"不正常的日常","permalink":"http://apparition957.github.io/categories/不正常的日常/"}],"tags":[]},{"title":"CAP、ACID、BASE理解","slug":"CAP、ACID、BASE理解","date":"2017-03-25T08:31:11.000Z","updated":"2017-07-31T13:13:25.000Z","comments":true,"path":"2017/03/25/CAP、ACID、BASE理解/","link":"","permalink":"http://apparition957.github.io/2017/03/25/CAP、ACID、BASE理解/","excerpt":"","text":"CAPCAP原则又称CAP定理,指的是一个分布式系统中,Consistency(一致性)、Availabiliy(可用性)、Partition Tolerance(分区容错性),三者不可兼得。 CAP理论将分布式系统的三个特性进行了系统归纳: 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否为同样的值。即同样数据在分布式系统中所有地方都是被复制成相同。对于分布式的存储系统中,一份数据往往会复制多份并存放在不同的主机上。一致性保证了对于客户对一份数据的操作的同时,其他数据也会相应进行同样的操作,而不会出现同一份数据在不同的主机上会有不同的副本的情况。 可用性(A):在集群中的一部分节点故障后,集群整体是否还能响应客户端的读写要求。即所有在分布式系统活跃的节点都能够处理操作且能响应查询。对于客户的操作,系统能够在部分节点故障(不可用)的情况下,仍然能够响应客户的操作并作出相关的数据修改。 分区容错性(P):是否允许数据的分区,即是否允许集群中的节点之间不进行通信。即除全部网络节点全部故障以外,所有子节点集合的故障都不允许导致整个系统不正确响应。即使部分的组件不可用,施加的操作也可以完成。在两个不同地方的存储节点由于某种原因,无法进行正常连接时,需要有一套完整的容错机制来保证数据的可操作。 一个数据存储系统不可能同时满足上述三个特性,只能同时满足其两个特性。如下所示: CA - 满足数据的一致性和高可用性,但没有可扩展性。 CP - 满足数据的一致性和分区性。但当节点达到一定数目时,性能(也即可用性)就会下降很快,并且节点之间的网络开销还在,需要实时同步各节点之间的数据。 AP - 满足数据的高可用性和分区性,但在数据一致性方面会用牺牲,各节点的之间数据无法立即做到同步更新,但能保存数据的最终一致性。 在软件架构设计过程中,往往倾向于构建高可用、数据高度一致且具备扩展性的大型软件工程项目,但基于以上分析可知,我们无法同时满足三个特点。尤其在于进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。 ACID传统关系型数据库系统的事务特点可以囊括为ACID: Atomic(原子性):整个事务当中的所有操作,要么全部执行,要么全部不执行,不可能停滞在中间某个阶段。当事务在执行过程中发生错误时,会进行回滚(rollback)到事务最初状态,数据不会受到影响。 Consistent(一致性):在事务开始之前和事务开始之后,数据库的完整性约束没有被破坏 Isolated(隔离性):两个事务的执行是互不干扰的。 Durable(持久性):在事务完成以后,该事务对数据库所有的更改是永久的,不可恢复的。 在传统数据库系统中,事务的ACID保证了数据库的高度一致性。比如在银行转账系统中,转账就是一个事务,从原账户扣除金额,以及向目标账户添加金额,两个操作总结为一个原子化的逻辑操作,不可拆分。 但在分布式操作系统中,ACID或许就是一个很好的选择,ACID的要求是数据的高度一致性。比如在大型的图书网站购买图书,每当用户进行购书并完成付款时,数据库系统就会锁住相关的数据库进行数据的修改(相关书本数量减少)。当数据修改完成后,其他用户都会看到该图书的库存减少了。以上操作,并不允许同一时间有两个或两个以上的用户进行图书的购买,不利于数据的高并发处理。基于以上例子,我们知道ACID放弃了系统的性能(P),进而保证数据的高度一致(C)。 BASEBASE是站在ACID的对立面,与其截然不同的设计思想。BASE有以下三个特点: Basically Availablilty(基本可用性):指在分布式系统中在出现不可遇见的故障的时候,允许损失部分可用性。正常情况下,在电子商务网站上购物时,消费者都能够以较流畅的过程来完成每一笔订单,但在面对数据高并发的情况(如双11),为了保护系统的稳定性以及提高性能,设计者往往采取服务降级的措施,来满足主要服务器的性能需求。 Sotf-State(软状态)与硬状态相比,其允许系统中的数据处于中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间更新存在一定的时间延迟。例如,在图书网站秒杀活动中,经常出现不同用户看到同一本畅销书的库存数量不同。 Eventually Consistency(最终一致性):指在系统中所有的数据副本,经过一段时间的同步后,最能能够到达一个一致的状态。最终一致性的本质在于所有数据在一定时间过后,一定能够到达一致状态,而不强制要求数据的实时一致性。 BASE其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。 参考资料: [1] http://www.cnblogs.com/duanxz/p/5229352.html [2] http://blog.csdn.net/sinat_27186785/article/details/52032510 [3] http://www.sigma.me/2011/06/17/database-ACID-and-BASE.html","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"UML中类图与类关系详解","slug":"UML中类图与类关系详解","date":"2017-03-19T11:59:22.000Z","updated":"2017-10-31T12:03:35.000Z","comments":true,"path":"2017/03/19/UML中类图与类关系详解/","link":"","permalink":"http://apparition957.github.io/2017/03/19/UML中类图与类关系详解/","excerpt":"","text":"前言 UML(Unified Modeling Language)又称统一建模语言或标准建模语言。它是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持,包括由需求分析到规格,到构造和配置。 前面以下图为例进行概念的分解: 类(Class) 泛化(generalization)关系 表示is-a关系,即继承关系,是对象之间耦合度最大的一种关系,子类继承父类所有属性和方法。 在类图中使用带三角箭头的实线表示,箭头由子类指向父类。 图中,程序员可以分为后端人员和前端人员,即它们之间存在泛化关系。 实现(Realization)关系 是一种类与接口的关系,表示类是接口所有特征和行为的实现。 在类图中使用带三角箭头的虚线表示,箭头从实现类指向接口(或抽象类) 图中,员工为抽象类,而销售人员和程序员分别为它的实现,即它们之间存在实现关系。 依赖(Dependency)关系 对象之间最弱的一种关联方式,是临时性的关联,即一个类的实现需要另一个类的协助。在代码结构上,一般指局部变量、函数参数等。 在类图中使用带箭头的虚线表示,箭头从使用类指向被依赖类。 图中,程序员在工作时不可能离开电脑,即它们之间存在依赖关系。 关联(Association)关系 对象之间的引用关系,如客户类与订单类之间的关系。这种关系通常使用类的属性表达。关联又分为一般关联、聚合关联与组合关联。 在类图中使用带箭头的实线表示,箭头从使用类指向被关联的类。箭头可以是单向或者双向的。 图中,客户可以拥有订单,而订单可以从属于客户,即它们之间存在(双向)关联关系。 多重性(Multiplicity):通常在关联、聚合、组合中使用。就是代表有多少个关联对象存在。使用数字..星号(数字)表示。如下图,一个割接通知可以关联0个到N个故障单。 聚合(Aggregation)关系 表示has-a关系,是一种不稳定的包含关系。较强于一般关联,有整体与局部关系,并且没有了整体,局部也可以独立存在。 在类图中使用空心的菱形表示,空心菱形从局部指向整体。 图中,企业可以与多个合作商合作,但企业不一定需要这些合作商,即它们之间存在聚合关系。 组合(Composition)关系 表示contains-a关系,是一种强烈的包含关系。组合类负责被组合类的生命周期。是一种更强的聚合关系。部分不能脱离整体存在。 在类图中使用实心的菱形表示,实心菱形从局部指向整体。 图中,企业与部门是一种强聚合关系,即企业不存在时,部门也会随着消失,即它们之间存在组合关系。 参考资料: [1] http://www.uml.org.cn/oobject/201104212.asp","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"大型网站架构演化笔记","slug":"大型网站架构演化笔记","date":"2017-03-18T13:46:07.000Z","updated":"2017-07-31T13:08:23.000Z","comments":true,"path":"2017/03/18/大型网站架构演化笔记/","link":"","permalink":"http://apparition957.github.io/2017/03/18/大型网站架构演化笔记/","excerpt":"","text":"前言在阅读《大型网站技术架构:核心原理与案例分析》里大型网站架构演化一文的时候,被其中的大型网站架构演化的剖析图深深所吸引,并根据书上的图,自己使用VISIO也尝试去绘制一遍,然后结合书上的描述以及自己的领悟写下这篇笔记。 《大型网站技术架构:核心原理与案例分析》通过梳理大型网站技术发展历程,剖析大型网站技术架构模式,深入讲述大型互联网架构设计的核心原理,并通过一组典型网站技术架构设计案例,为读者呈现一幅包括技术选型、架构设计、性能优化、Web 安全、系统发布、运维监控等在内的大型网站开发全景视图。 概要以往,我们都尝试建立过规模较小、流量较少以及较低并发的小型网站,例如学生信息交流平台、二手商城等。但是当规模逐渐庞大的时候,小型网站的架构就会凸显出性能不足,延迟较高等现象,软件架构师需要为此扩展和优化项目结构,从而不断升级成为大型网站。不同阶段的架构都能够适应于不同阶段的规模发展,应对不同的需求,但最终的目标是能够高效、迅捷地处理庞大的用户群、高并发的访问以及海量的数据。 初始阶段的网站架构 通常来说,建站初期流量会相对较少,一台服务器就足以应付所有请求。此时通常采用单块架构的形式,将应用层、数据层、服务层集合在同一台主机中。 单块架构 - 是在三层架构的基础之上,将整个项目放至一个服务器当中运行。通常以WAR包或者EAR包存在,当部署这类应用时,通常是将整个一个块作为一个整体,部署到同一个Web容器,如Tomcat或者Jetty中。当这类应用运行起来后,所有的功能也都运行在同一个进程中。 应用服务和数据服务分离 随着业务的不断发展,一台服务器逐渐不能满足需求:访客的增多导致访问的延迟不断增加、存储的数据增多导致服务器存储空间不够。此时,就需要将应用服务和数据服务分离。 分开的服务可以根据业务的需求购置不同性能的服务器。应用服务器需要更快的CPU来处理复杂的业务逻辑,文件服务器需要更大的磁盘空间来存储较大的文件,数据服务器需要更大的内存和更快的硬盘来进行数据缓存和数据索引。 使用缓存来改善网站性能 随着用户逐渐增多,数据库的压力太大导致访问延迟,从而影响了网站的性能,用户体验也受到了影响。网站访问的特点遵循“二八定律”:80%的业务访问集中在20%的数据上。可以根据以上特点总结,若大部分的业务访问集中在小部分的数据上,架构师可以提前将这一小部分数据进行缓存在内存当中,继而可以大幅度减少数据库的压力,提高网站的访问速度。 网站使用的缓存通常分为两种:本地缓存(缓存在应用服务器上)和远程缓存(缓存在分布式缓存服务器上)。本地缓存的访问速度较快,但是受限于应用服务器的内存,存储的数量有限。分布式远程缓存可以使用集群的方式,部署大内存的服务器作为专门的缓存服务器,存储的数量可以尽可能地大。 使用应用服务器集群改善网站的并发处理能力 在网站访问的高峰期处理高并发问题时,单一服务器明显显得性能不足。基于以上情况,可以使用集群的手段来增加若干个服务器,分担原有的服务器的压力,提高系统性能,从而实现系统的可伸缩性。并且结合负载均衡调节器,将不同的用户的请求尽可能均匀分发到集群中不同的服务器上,使得应用服务器的负载压力不再是整个网站的瓶颈。 集群 - 一组相互独立的、通过高速网络互联的计算机,它们构成了一个组,并以单一系统的模式加以管理。一个客户与集群相互作用时,集群像是一个独立的服务器。 负载均衡 - 建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。 数据库读写分离 网站在使用缓存后,可以使绝大部分的数据操作访问都可以不通过数据库就能够完成。但是仍有一部分读操作(缓存访问不命中、缓存过期)和全部写的操作需要访问数据库,在网站的用户达到一定的规模后,数据库因为负载压力过高成为网站的瓶颈。 大部分主流数据库都提供主从(Master/Slave)热备功能,通过配置两台数据库的主从关系,可以将一台主数据库服务器的数据同步更新到另外一台从数据库服务器上。网站利用数据库这一功能,实现数据库的读写分离,从而改善数据库负载压力。 应用服务器在写数据的时候,访问主数据库,主数据库通过主从复制机制将数据同步更新到从数据库中,这样当应用服务器读数据库的时候,可以通过从服务器获取数据。此外,为了便于应用程序读写分离后的数据库,通常提供一个专门的数据访问模块,使得数据库读写分离对应用透明,便于操作。 热备份 - 系统处于正常运转状态下的备份。 冷备份 - 也被称为离线备份,是指在关闭数据库并且数据库不能更新的状况下进行的数据库完整备份。 使用反向代理和CDN加速网站响应 随着网站业务不断发展,用户群规模不断扩大,由于用户群的地理位置不同,不同地区的用户访问同一网站时的速度也不尽相同,甚至差距很大。为了提高用户的访问速度,主要手段有两种:CDN(Content Delivery Network)和反向代理。 CDN和反向代理的基本原理都是缓存。区别在于CDN部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;而反向代理则部署在网站的中心机房。访问的顺序依次为CDN、反向代理、应用服务器。 使用CDN和反向代理的目的都是尽早返回数据给用户,一方面加快用户访问速度,另一方面也减轻后端服务器的负载压力。 CDN - 即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。 反向代理 - 以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。作用类似于网关入口。 使用分布式文件系统和分布式数据库系统 任何强大的单一服务器都无法满足大型网站持续增长的业务需求以及庞大的用户群。虽然数据库经过读写分离后,从一台高度集中的服务器拆分为两台各司其职的服务器,但仍然无法满足需求。这时需要使用分布式数据库,同理作用于文件系统,需要使用分布式文件系统。 分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常庞大的时候才使用。在大多数时候中,网站采用更多的手段为业务分库,将不同的业务数据部署在不同的物理服务器上。 分布式数据库 - 利用高速计算机网络将物理上分散的多个数据存储单元连接起来组成一个逻辑上统一的数据库。分布式数据库的基本思想是将原来集中式数据库中的数据分散存储到多个通过网络连接的数据存储节点上,以获取更大的存储容量和更高的并发访问量。 使用NoSQL和搜索引擎 NoSQL和搜索引擎都是源自互联网的技术手段,对可伸缩的分布式特性具有更好的支持,更有效利用空间来进行数据检索和存储。 应用服务器可以通过一个统一的数据访问模块访问各种数据,使得若干数据源对应用透明,减少应用程序管理诸多数据源的麻烦。 NoSQL - 业界指的是非关系型数据库。常用于超大规模数据的存储。需要存储的这些海量数据并不需要固定的模式,无需多余操作就可以横向扩展。 业务拆分 大型网站为了应对日益复杂的业务场景,通过使用分而治之的手段将整个网站业务分成不同的产品线,如大型购物交易网站将会将首页、商铺、订单、购物车等拆分成不同的产品线,分归不同的业务团队负责。 划分后的应用可以独立部署维护,应用之间可以通过网关路由建立关系,也可以通过消息队列进行数据分发。 微服务架构 - 一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。 分布式服务 随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。根据现有系统分析,很多应用系统中都需要执行相同的业务操作,比如用户管理、商品管理等,那么可以将这些共同的业务提取出来,单独部署维护。由这些可复用的业务连接数据库,提供业务共同服务,降低业务之间的耦合,提高业务的复用。 总结通过以上的大型网站架构繁衍,可以了解到如何通过技术来更有效的去解决不同阶段会遇到的难题。但是对通读这本书,技术固然可以解决当前问题,但是不能因为技术而技术,有些时候,利用业务的能力会比技术更有用处,比如提高用户的体验,设置更多具有交互式的体验等。综合各方因素,才能将大型网站完善的运营下去。","categories":[{"name":"技术笔记","slug":"技术笔记","permalink":"http://apparition957.github.io/categories/技术笔记/"}],"tags":[]},{"title":"设计模式笔记 - 模板模式(Template Pattern)","slug":"设计模式笔记-模板模式(Template-Pattern)","date":"2017-03-18T13:00:20.000Z","updated":"2017-07-31T13:04:56.000Z","comments":true,"path":"2017/03/18/设计模式笔记-模板模式(Template-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/18/设计模式笔记-模板模式(Template-Pattern)/","excerpt":"","text":"模式定义 模板模式(Template Pattern)定义一个操作中算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变算法的结构,重新定义算法中的某些特定步骤。 模式结构模板模式包含以下角色: AbstractClass:抽象类 ConcreteClass:具体类 模式实例12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970/* * 此例为实现模板模式的小例子。茶与咖啡同属于咖啡因的种类,两者的冲泡方式也十分相同:煮沸水、添 * 加咖啡豆(或茶包)、导入杯中、添加调料。利用模板模式,可以将两者的冲泡过程进行抽象泛化。 */// AbstractClass - 抽象类public abstract class CaffeineBeverage { // final - 标记子类无法重载该方法 final void prepareRecipe() { boilWater(); brew(); pourInCup(); hook(); addCondiments(); } // 指定子类必须实现以下两个抽象方法 abstract void brew(); abstract void addCondiments(); void boilWater() { System.out.println("Boiling Water!"); } void pourInCup() { System.out.println("Pouring into Cup!"); } // hook钩子 - 空方法 - 使算法更具拓展性 void hook() { ; }}// ConcreteClass - 具体类// 茶public class Tea extends CaffeineBeverage { public void brew() { System.out.println("Steeping the tea!"); } public void addCondiments() { System.out.println("Adding Lemon!"); }}// 咖啡public class Coffee extends CaffeineBeverage { public void brew() { System.out.println("Dripping Coffee through filter!"); } public void addCondiments() { System.out.println("Adding Sugar and Milk!"); } public void hook() { System.out.println("Please give me a spoon!"); }}public class Test { public static void main(String[] args) { CaffeineBeverage tea = new Tea(); CaffeineBeverage coffee = new Coffee(); tea.prepareRecipe(); coffee.prepareRecipe(); }} 模式分析 通常来说,模板模式中抽象类负责实现模板方法,定义算法的骨架,而具体类实现抽象类中的抽象方法,实现完整的算法。 模板模式的优点: 模板模式提供了抽象类专注于算法本身,并定义了算法的大致骨架,使代码的复用最大化。 子类可以继承模板类,并实现算法所指定的细节,有助于算法的扩展。 模板模式的缺点: 抽象类内部的细节若划分太过于细节,将减少子类实现该抽象类的弹性,不易于扩展。 模式扩展模板模式中不一定非要继承抽象类才能够实现算法的复用。java.util 包中 Array.sort() 静态方法为我们提供了排序的模板,但是我们并不能够继承数组类,所以它要求需要排序的类实现Comparable接口,才能够正常使用该排序。 Q&A 当我创建一个模板方法时,怎么知道什么时候才能使用抽象方法,什么时候用钩子当你的子类“必须”提供算法中某个方法或步骤的实现时,就使用抽象方法。如果算法的这个部分是可选的,就使用钩子。 使用钩子的真正目的钩子可以让子类实现算法中可选的部分,换种角度来说,在钩子对于子类的实现并不重要的时候,子类可以对此置之不理。钩子的另外一个用法,是让子类能够有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做出反应。 策略模式与模板模式的区别策略模式与模板模式的思想是封装算法。但策略模式的侧重点在于使用组合,实现算法的替换,而模板模式的侧重点使用继承,实现算法框架的某些细节。","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 外观模式(Facade Pattern)","slug":"设计模式笔记-外观模式(Facade-Pattern)","date":"2017-03-17T14:06:39.000Z","updated":"2017-07-31T13:03:54.000Z","comments":true,"path":"2017/03/17/设计模式笔记-外观模式(Facade-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/17/设计模式笔记-外观模式(Facade-Pattern)/","excerpt":"","text":"模式定义 外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 模式结构外观模式包含如下角色: Facade:外观角色 SubSystem:子系统角色 模式实例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748/* * 此例通过外观模式设计的门面,整合了三个子系统的接口并依次调用。 */// SubSystem - 子系统角色// 子系统Apublic class SystemA { public void operationA() { System.out.println("This is operationA!"); }}// 子系统Bpublic class SystemB { public void operationB() { System.out.println("This is operationB!"); }}// 子系统C public class SystemC { public void operationC() { System.out.println("This is operationC!"); }}// 外观角色public class Facade { SystemA systemA = new SystemA(); SystemB systemB = new SystemB(); SystemC systemC = new SystemC(); public void warpOperation() { systemA.operationA(); systemB.operationB(); systemC.operationC(); }}// Client - 客户端类public class Client { public static void main(String[] args) { Facade facade = new Facade(); facade.warpOperation(); }} 模式分析 根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信与互相依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的接口。 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,无需关注内部复杂的细节,提高客户的使用体验。 外观模式的优点: 对客户屏蔽了子系统的组件,减少客户处理的对象数目,也使得子系统使用起来更加容易。 同时也封装了子系统内部细节,降低了客户与子系统之间的耦合。 外观模式只是提供了一个访问子系统的统一入口,但是并不影响用户直接操作子系统(如果允许的情况下)。 外观模式的缺点: 不能很好地限制客户使用子系统类,如果对客户过多的访问子系统类,则会间接增加了客户与子系统类的耦合程度,减少了灵活性。 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端源代码,违背了“开闭原则”。 注意点: 不要通过继承外观类在子系统中加入新的行为。因为外观模式的本质在于为子系统提供一个集中化的、更加简化的接口,而不是向子系统中添加新的行为,而导致增加系统的复杂度。 外观模式最大的缺点在于违背了“开闭原则”,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/facade.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 适配器模式(Adapter Pattern)","slug":"设计模式笔记-适配器模式(Adapter-Pattern)","date":"2017-03-17T10:20:54.000Z","updated":"2017-07-31T13:03:18.000Z","comments":true,"path":"2017/03/17/设计模式笔记-适配器模式(Adapter-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/17/设计模式笔记-适配器模式(Adapter-Pattern)/","excerpt":"","text":"模式定义 适配器模式(Adapter Pattern)将一个接口转换成客户希望的另外一个接口,使得与客户现有接口不兼容的第三方接口可以与其一起正常工作 模式结构适配器模式包含如下角色: Target:目标抽象类(或接口) Adapter:适配器类 Adaptee:适配者类 Client:客户类 适配器模式有对象适配器(针对于组合)和类适配器(针对于多重继承)两种实现,以下重点讲述对象适配器实现: 模式实例1234567891011121314151617181920212223242526272829303132333435363738394041424344/* * 此例为英标插头提供能够正常插入国标插座的转换器。不过省去调节电压等细 * 节。 */// Target - 目标抽象类(或接口)public class ChinaSocket { public void workInAWay() { System.out.println("You are using ChinaScoket"); }}// Adaptee - 适配者类public class EnglandSocket { public void workInBWay() { System.out.println("You are using EnglandSocket"); }}// Adapter - 适配器类public class ChinaScoketAdapter { EnglandScoket englandScoket; public ChinaScoketAdapter(EnglandScoket englandScoket) { this.englandScoket = englandScoket; } public void workInAWay() { englandScoket.workInBWay(); }}// Client - 客户端public class Test { public static void main(String[] args) { ChinaSocket chinaSocket = new ChinaSocket(); EnglandSocket englandSocket = new EnglandSocket(); ChinaScoketAdapter adapter = new ChinaScoketAdapter(englandSocket); chinaSocket.workInAWay(); adapter.workInAWay(); }} 模式分析 适配器提供客户类需要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作。 适配器模式的优点: 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。 增加了类的透明性和复用性。将具体的实现封装在适配者类中,对于客户端来说是透明的,而且提高了适配者的复用性。 对于对象适配器来说,同一适配器可以兼容适配者类以及它的子类到目标接口中。 适配器模式的缺点: 若要更改适配器的接口的时候,则需要修改其所兼容适配器的所有接口,维护的难度较大。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/adapter.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 命令模式(Command Pattern)","slug":"设计模式笔记-命令模式(Command-Pattern)","date":"2017-03-16T15:24:35.000Z","updated":"2017-07-31T13:02:40.000Z","comments":true,"path":"2017/03/16/设计模式笔记-命令模式(Command-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/16/设计模式笔记-命令模式(Command-Pattern)/","excerpt":"","text":"模式定义 命令模式(Command Pattern)将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,又称事务(Transaction)模式。 模式结构命令模式包含如下角色: Command:抽象命令类 ConcreteCommand:具体命令类 Invoker:调用者 Receiver:接受者 Client:客户类 模式实例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596/* * 此例为电视机遥控器。其中电视机是请求的接受者,遥控器是请求的发送者,遥 * 控器上有一些按钮,不同的按钮对应电视机的不同的操作。抽象命令角色有一个 * 命令接口来扮演,有三个具体的命令类实现了该抽象命令接口:打开电视机、关 * 闭电视机、切换频道。 */// Command - 抽象命令类public interface Command { public void execute();}// ConcreteCommand - 具体命令类 - 篇幅问题只实现前两个// 打开电视机public class TVOpenCommand implements Command { private Televition tv; public TVOpenCommand(Televition tv) { this.tv = tv; } public void execute () { this.tv.open(); }}// 关闭电视机public class TVCloseCommand implements Command { private Televition tv; public TVCloseCommand(Televition tv) { this.tv = tv; } public void execute() { this.tv.close(); }}// 空命令 - 利用空命令取代null确保运行时的安全public class NoCommand implements Command { public void execute() { System.out.println("No command here"); }}// Invoker - 调用者public class Controller { private Command[] commands; private static final int length = 7; public Controller() { commands = new Command[length]; for (int i = 0; i < length; i++) { commands[i] = new NoCommand(); } } public setCommand(int index, Command command) { commands[index] = command; } public void pushButton(int index) { commands[index].execute(); }}// Receiver - 接受者public class Television { public Television() { ; } public void on() { System.out.println("Television is on now!"); } public void off() { System.out.println("Television is off now!"); }}// Client - 客户类public class Client { public static void main(String[] args) { Television tv = new Television(); TVOpenCommand tvOpen = new TVOpenCommand(tv); TVCloseCommand tvClose = new TVOCloseCommand(tv); Controller controller = new Controller(); controller.setCommand(0, tvOpen); controller.setCommand(1, tvClose); controller.pushButton(0); controller.pushButton(1); }} 模式分析命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。请求的一方发出请求,要求执行一个操作;接受的一方收到请求,并执行操作。 命令模式允许请求的一方和接受一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。 命令模式将请求本身封装称为一个对象,使得该请求可以像其他对象一样被存储或传递。 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接受者相关联。 命令模式的优点: 发送者与接受者分别负责命令的发送与处理,将彼此的责任分离,降低系统的耦合度,易于系统的扩展。 较容易设计出命令队列和宏命令(组合若干条命令) 较容易设计出撤销操作和恢复操作(利用栈存储已操作的命令) 命令模式的缺点: 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对于每一个命令都需要设计一个具体命令类(开与关等..),在一定程度上难以管理和维护所有的命令。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/command.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 – 单例模式(Singleton Patttern)","slug":"设计模式笔记-–-单例模式(Singleton-Patttern)","date":"2017-03-16T04:00:45.000Z","updated":"2017-07-31T13:02:02.000Z","comments":true,"path":"2017/03/16/设计模式笔记-–-单例模式(Singleton-Patttern)/","link":"","permalink":"http://apparition957.github.io/2017/03/16/设计模式笔记-–-单例模式(Singleton-Patttern)/","excerpt":"","text":"模式定义 单例模式(Singleton Pattern)确保某一个类只有一个实例,而且自行实例化并向整个系统提供该唯一实例。这个类称为单例类,它提供全局访问的方法。 单例模式有三个要点: 某个类中有一个实例 它必须自行创建这个实例 它必须自行向整个系统提供这个实例 模式结构单例模式包含如下角色: Singleton:单例 模式实例123456789101112131415161718// Singleton - 单例public class Singleton { private static Singleton singleton; // 设置Singleton的构造器为私有,即外部不可实例化该类 private Singleton() { ; } // 这是其中一种实现方式,但并不能保证在多线程情况下安全 public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } } 模式分析 单例模式的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式包含的角色有且只有一个。 单例模式拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。 单例模式的优点: 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样或何时访问它,并为设计及开发团队提供了共享的概念。 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑提高了系统的性能。 允许可变数目的实例。可以基于单例模式的思想进行扩展,使用与单例控制相似的方法来获取指定数目的实例。 单例模式的缺点: 单例类不能被继承(因为单例类的私有构造器对外不可见),也无法继承(因为继承,则必须确保父类也是单例类,才能确保一致性,但父类也是私有构造器,对子类不可见),对单例类的扩展有很大的难度。 单例类的职责过重,在一定程度上违背了类应遵循的“单一职责原则”。因为单例类即充当了工厂角色,提供工厂方法,同时也充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能耦合在一起。 传统实现方式引发的多线程问题:在多线程问题上,单例模式中传统的实现方式会在多个线程第一次同时访问创建实例的方法时,产生多个对象。 线程A调用getInstance(),判断singleton为空并创建Singleton对象,同时线程B也调用getInstance(),恰巧也判断singleton为空,继而创建了第二个Singleton对象。这样一来延伸至若干个线程也这么做,后果不堪设想。 以下有不同的解决方式,各有优缺点: 在单例类中静态变量中直接赋值,确保每次获取该类的实例中只有一个。但是这样就无法做到按需加载的目的,容易造成浪费资源。 1234567891011public class Singleton { private static Singleton singleton = new Singleton(); private Singleton() { ; } public static Singleton getInstance() { return singleton; }} 在单例类的传统的获取实例方法中,增加synchronized同步块。虽能做到同步加载,但每次获取该实例,都需要进行同步检查,性能会有所下降。 123456789101112131415public class Singleton { private static Singleton singleton; private Singleton() { ; } public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; }} 在单例类中的静态变量添加volatile关键字,并使用“双重检查加锁”机制。只有在对象不存在的时候使用同步。 12345678910111213141516171819202122232425262728293031323334public class Singleton { /* * 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是 * 共享的,因此不会将该变量上的操作与其他内存操作一起重排序。 * volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此 * 在读取volatile类型的变量时总会返回最新写入的值。 */ private volatile static Singleton singleton; private Singleton() { ; } public static Singleton getInstance() { // 只有第一次才彻底往下执行 if (singleton == null) { // 执行此同步块时,确保线程安全 synchronized (Singleton.class) { /* 当有多个线程同时访问时,会进行排队,若不进行二次判 * 断,当第一个线程访问结束已创建对象时,第二个线程紧接 * 着访问,仍会再次创建对象,不符合单例模式的思想。 */ if (singleton == null) { singleton == new Singleton(); } } } return singleton; }} 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/singleton.html [2] http://www.cnblogs.com/zhengbin/p/5654805.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 抽象工厂模式(Abstract Factory Pattern)","slug":"设计模式笔记-抽象工厂模式(Abstract-Factory-Pattern)","date":"2017-03-15T11:55:17.000Z","updated":"2017-07-31T13:01:05.000Z","comments":true,"path":"2017/03/15/设计模式笔记-抽象工厂模式(Abstract-Factory-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/15/设计模式笔记-抽象工厂模式(Abstract-Factory-Pattern)/","excerpt":"","text":"模式定义 抽象工厂模式(Abstract Factory Pattern)提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。 模式结构抽象工厂模式包含如下角色: AbstractFactory:抽象工厂 ConcreteFactory:具体工厂 AbstractProduct:抽象产品 ConcreteProduct:具体产品 模式实例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081/* * 结合工厂方法模式中的例子,各分部的披萨店快要开张了,但是不同地方的披萨 * 所需的原料都各不相同,同样的芝士披萨,纽约偏甜,芝加哥却偏酸。抽象工厂 * 模式很好地提供了如何针对于不同的地方供应不一样的原料。 */public class PizzaStore { PizzaFactory pizzaFactory; public PizzaStore(PizzaFactory pizzaFactory) { this.pizzaFactory = pizzaFactory; } public void orderPizza(String item) { Pizza pizza = pizzaFactory.createPizza(item); pizza.display(); }}// AbstractFactory - 抽象工厂public interface class PizzaIngredientFactory { // AbstractProduct - 抽象产品 public Cheese createCheese(); public Sauce createSauce();}// ConcreteFactory - 具体工厂// 纽约原料工厂public class NewYorkIngredientFactory implements PizzaIngredientFactory { // ConcreteProduct - 具体产品 @Override public Cheese createCheese() { System.out.println("reggiano cheese"); } @Override public Sauce createSauce() { System.out.println("marinara sauce"); }}// 芝加哥原料工厂public class ChicagoIngredientFactory implements PizzaIngredientFactory { @Override public Cheese createCheese() { System.out.println("sugar cheese"); } @Override public Sauce createSauce() { System.out.println("tomato sauce"); }}// 举一生产Pizza工厂为例,进行优化改进public class NewYorkPizzaFactory extends PizzaFactory { PizzaIngredientFactroy ingredientFactory; public NewYorkPizzaFactory(PizzaIngredientFactroy ingredientFactory) { this.ingredientFactory = ingredientFactory; } public Pizza createPizza(String item) { if (item.equals("cheese")) { // 此处将纽约原料工厂的引用转交给NewYorkCheesePizza对象处理,让其自主调用需要的材料 return new NewYorkCheesePizaa(ingredientFactory); } }}// 测试类public class Test { public static void main(String[] args) { PizzaFactory newYorkPizzaFactory = new NewYorkPizzaFactory(new NewYorkPizzaFactory()) PizzaStore newYorkStore = new PizzaStore(newYorkPizzaFactory); newYorkStore.orderPizza("cheese"); PizzaFactory chiCagoPizzaFactory = new ChiCagoPizzaFactory(new ChiCagoPizzaFactory()) PizzaStroe chicagoStore = new PizzaStore(chiCagoPizzaFactory); chicagoStore.orderPizza("cheese"); }} 项目源码后续更进.. 模式分析 在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。为了更清晰地理解工厂方法模式,需要先引入两个概念:产品等级结构 :产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。产品族 :在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。 当系统所提供的工厂所需生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构中属于不同类型的具体产品时需要使用抽象工厂模式。 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。 抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效率。 抽象工厂模式的优点: 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只是用同一个产品族中的对象。 方便地增加新的具体工厂和产品族,系统更易于扩展,也无须修改现有系统,符合“开闭原则”。 抽象工厂模式的缺点: 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对现有接口进行扩展,这样的做法将会涉及到对抽象工厂角色以及所有子类的修改,会带来较大的麻烦。 “开闭原则”的倾斜性(增加新的具体工厂和产品族容易,增加新的产品等级结构麻烦)。 工厂模式的退化当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/abstract_factory.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 工厂方法模式(Factory Method Pattern)","slug":"设计模式笔记-工厂方法模式(Factory-Method-Pattern)","date":"2017-03-15T03:17:41.000Z","updated":"2017-07-31T12:59:59.000Z","comments":true,"path":"2017/03/15/设计模式笔记-工厂方法模式(Factory-Method-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/15/设计模式笔记-工厂方法模式(Factory-Method-Pattern)/","excerpt":"","text":"模式定义 工厂方法模式(Factory Method Pattern)又称为多态工厂(Polymorphic Factory)模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。 模式结构工厂方法模式包含如下角色: Factory:抽象工厂 ConcreteFactory:具体工厂 Product:抽象产品 ConcreteProduct:具体产品 模式实例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657/* * 结合简单工厂模式中的例子,披萨店生意火爆,资金足以在外地开分店。但不同 * 地方所需求的披萨不同,比如纽约与芝加哥的风味完全不同。我们应该根据不同 * 地方的需求创建自己需要的生产工厂 */public class PizzaStore { PizzaFactory pizzaFactory; public PizzaStore(PizzaFactory pizzaFactory) { this.pizzaFactory = pizzaFactory; } public void orderPizza(String item) { Pizza pizza = pizzaFactory.createPizza(item); pizza.display(); }}// Factory - 抽象工厂public abstract class PizzaFactory { public abstract Pizza createPizza(String item);}// ConcreteFactory - 具体工厂// 纽约分店public class NewYorkPizzaFactory extends PizzaFactory { public Pizza createPizza(String item) { if (item.equals("cheese")) { return new NewYorkChessePizza(); } else { return null; } }}// 芝加哥分店public class ChiCagoPizzaFactory extends PizzaFactory { public Pizza createPizza(String item) { if (item.equals("cheese")) { return new ChicagoChessePizza(); } else { return null; } }}// Pizza实现等大同小异,不再一一复述// 测试类public class Test { public static void main(String[] args) { PizzaStore newYorkStore = new PizzaStore(new NewYorkPizzaFactory()); newYorkStore.orderPizza("new york"); PizzaStroe chicagoStore = new PizzaStore(new ChiCagoPizzaFactory()); chicagoStore.orderPizza("chicago"); }} 模式分析 工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需关心所需产品对应的工厂,无需关心创建细节,甚至无需知道具体产品类的类名。 工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。 工厂方法模式的优点: 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部,符合“开闭原则”。 在面对系统需要添加新的产品时,无须修改抽象工厂和抽象产品提供的接口,也无须修改现有的具体工厂和具体产品,而只需为新产品添加一个具体工厂和具体产品即可。 工厂方法模式的缺点: 在添加新产品时,需要编写新的具体产品类以及相对应的具体工厂类,系统中类的数目将成对增加,在一定程度上增加了系统复杂度。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/factory_method.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 简单工厂模式(Simple Factory Pattern)","slug":"设计模式笔记-简单工厂模式(Simple-Factory-Pattern)","date":"2017-03-14T13:21:08.000Z","updated":"2017-07-31T12:58:49.000Z","comments":true,"path":"2017/03/14/设计模式笔记-简单工厂模式(Simple-Factory-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/14/设计模式笔记-简单工厂模式(Simple-Factory-Pattern)/","excerpt":"","text":"模式定义 简单工厂模式(Simple Factory Pattern),又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门一个类来负责创建其他类的实例,被创建的实例通常都具有都具有共同的父类。 模式结构简单工厂模式包含如下角色: Factory:工厂角色 - 负责实现创建所有实例的内部逻辑 Product:抽象产品角色 - 为所有具体产品的类的父类,负责描述所有实例所共有的公共接口 ConcreteProduct:具体产品角色 模式实例1234567891011121314151617181920212223242526272829303132333435363738394041// 一家披萨店面急需拥有处理订单的系统public class PizzaStore { public void orderPizza(String item) { Pizza pizza = PizzaCreateFactory.createPizze(item); pizza.display(); }}// Product - 抽象产品角色public abstract class Pizza { public abstract String display(); }// ConcreteProduct - 具体产品角色// 产品1public class CheesePizza extends Pizza { public String display() { System.out.println("You order cheesePizza is ready!"); }}// 产品2public class HotDogPizza extends Pizza { public String display() { System.out.println("You order HotDogPizza is ready!"); }}// 工厂角色public class PizzaCreateFactory { public static Pizza createPizza(String item) { if (item.equals("cheesea")) { return new CheesePizza(); } else if (item.equals("hot dog")) { retrun new HotDogPizza(); } else { // 一般返回一个具有错误处理的对象,此例中没做任何异常处理 return null; } }} 模式分析 将对象的创建和对象本身业务处理分离可以降低系统的耦合度。 简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则(对扩展开放,对修改关闭)是相违背的。 简单工厂模式的要点在于:当你需要什么,只需要传入若干正确参数,就可以获取所需的对象,而无需知道其创建细节。 简单工厂模式的优点: 工厂类含有必要的判断逻辑,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品。简单工厂模式通过这种做法实现了对责任的分割,降低两者之间的耦合。 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应分参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量(两者间应有一套严格的明文规定进行对象获取) 简单工厂模式的缺点: 系统扩展困难,一旦添加新产品就不得不修改工厂类中的逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构(与后面的抽象工厂作对比)。 适用环境在以下情况下可以使用简单工厂模式: 工厂类负责创建的对象比较少。由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。 参考资料: [1] https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/simple_factory.html#simple-factory-pattern","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"数据库事务的基本概念","slug":"数据库事务的基本概念","date":"2017-03-14T03:01:17.000Z","updated":"2017-07-31T12:57:50.000Z","comments":true,"path":"2017/03/14/数据库事务的基本概念/","link":"","permalink":"http://apparition957.github.io/2017/03/14/数据库事务的基本概念/","excerpt":"","text":"事务(Transaction)是并发控制的基本单位。所谓的事务,可以看做一个操作序列,这一操作序列里面所包含的所有操作要么都执行,要么都不执行,它是一个不可分割的工作单位。 在银行转账的过程中,需要从一个账户的钱款转移到另外一个账户上,这样必定涉及一删一增的操作。在没有事务的情况下,假设在完成对甲方账户扣款,由于不可控力,数据在进行下一步操作时失败或丢失,这样乙方账户就正常无法收到这笔欠款,且甲方已扣除欠款而无法退还。但在存在事务的情况下,由于事务的特性,当在转账过程中出现异常,就会立即回滚,保证甲乙两方账户的安全。 事务具有以下4个基本特性: 原子性(Atomicity)事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。 一致性(Consistency)一个事务的执行前后,数据库都必须处于数据一致性状态。 隔离性(Isolation)事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。 持久性(Durability)事务结束后,对数据库中数据的影响是永久的。 事务的基本SQL语句123START TRANSACTION; -- 开启事务COMMIT; -- 提交事务ROLLBACK; -- 回滚事务 1234567891011121314151617181920212223242526272829303132333435-- 转账小例子> select * from account;+----+------+-------+| id | name | money |+----+------+-------+| 1 | 小明 | 5000 || 2 | 小红 | 8000 |+----+------+-------+>>> start transaction; -- 事务提交失败,进行回滚> update account set money=money-1000 where name='小明'; -- 从小明账户中减1000, 但出于某些原由需要进行回滚(极端情况直接关闭控制台)> rollback;>> select * from account;+----+------+-------+| id | name | money |+----+------+-------+| 1 | 小明 | 5000 || 2 | 小红 | 8000 |+----+------+-------+>>> start transaction; -- 事务提交成功> update account set money=money-1000 where name='小明';> update account set money=money+1000 where name='小红';> commit;>> select * from account;+----+------+-------+| id | name | money |+----+------+-------+| 1 | 小明 | 4000 || 2 | 小红 | 9000 |+----+------+-------+ 事务的并发执行在数据库系统中,如果各个事务都是按串行方式执行的,DBMS很容易实现事务的ACID特性。这是因为在DBMS中串行执行事务程序,不会导致数据不一致性和事务隔离性问题。但是,在实际应用中,需要在DBMS中多个事务并发执行,原因如下: 改善系统的资源利用率。一个事务一般都是由多个操作组合而成的,它们在不同执行阶段需要不同的资源。有时需要I/O资源,有时需要CPU资源或需要网络资源。如果事务能够并发执行的话,就能够充分利用系统资源,提高系统处理的吞吐能力。 减少事务运行的平均时间。每个事务的执行时长不同,倘若事务是串行处理的,那么当有些需要长时间处理的事务必定阻塞只需短时间处理的事务。所以事务的并发执行是刚需。 并发执行产生的后果事务并发执行倘若不加约束的话,不但效率不会增加,反而不如串行执行,主要表现在若干数据的不一致问题,如脏读、不可重复读、幻读、丢失更新等问题。 脏读(Dirty Read) 脏读是指当一个事务读取被另外一个事务所修改的共享数据后,若修改数据的事务因某种原因失败,数据未被提交到数据库中,而读取共享数据的事务则在同一时刻,获取一个垃圾数据,即脏数据。 脏数据是对未提交事务中所修改的数据的统称。如果别的事务读取了这个数据,则可能会导致应用的数据错误,也造成不同应用的数据不一致问题。 图中,事务A和事务B共享访问雇员表信息。其中事务B将雇员编号为1的年龄数据从18修改为20。在事务A结束前,事务A程序再次读取了该雇员信息,获得了新的年龄数据20。但事务B在结束前,由于某种原因,回滚了之前的操作数据,即雇员编号为1的年龄数据恢复到18,从而导致事务A获取了一个脏数据。 不可重复读(Unrepeatable Read) 不可重复读是指当一个事务对同一共享数据重复读取两次,但是发现原有的数据改变或丢失。这是由于多个事务并发执行时,其中一个事务对共享数据执行了修改或删除操作造成的。 图中,事务A和事务B共享访问雇员表信息。事务A第一次读取雇员年龄数据小于或等于20的数据为3条。其后事务B将雇员编号为2的数据删除。当事务A再次读取雇员年龄数据小于或等于20的数据时,则为2条。 幻读(Phantom Read) 幻读是指当一个事务对同一个共享数据重复读取两次,但是发现第二次读取比第一次读取时,新增了一些数据。这是由于多个事务并发执行时,其中一个事务同时对共享数据进行添加操作造成的。 图中,事务A和事务B共享访问雇员表信息。事务A第一次读取雇员年龄数据小于或等于20的数据为2条。其后事务B新添加了雇员编号为3的数据。当事务A再次读取雇员年龄数据小于或等于20的数据时,则为3条。 丢失更新(Lost Update) 丢失更新是指一个事务对一共享数据进行更新处理,但是以后再查询该共享数据值与原更新值不一致。这是由于多个事务并发执行,其中一个事务也对同一个共享数据进行了更新,并将前面事务的更新值改变了。 图中,事务A和事务B共享访问雇员表信息。事务A对雇员编号为1的年龄数据修改为19。其后事务B也对该雇员的年龄数据进行修改,数据改为20。当事务A再次读取该雇员的信息时,其年龄数据已与先前数据修改不同。 事务隔离级别为了避免事务并发执行可能出现的以上的并发问题而导致的数据不一致问题,可以在DBMS中设置事务隔离级别(Isolation Level)。 各个隔离级别解释: 读取为提交(READ UNCOMMITED)。指定语句可以读取已由事务修改但尚未修改的行。 读取已提交(READ COMMITED)。指定语句不能读取已由其他事务修改但尚未提交的数据。 可重复读(REPEATABLE READ)。指定语句不能读取已由其他事务修改但尚未提交的行,并且指定,其他任何事务都不能在当前事务完成之前修改由当前事务修改的数据。 可串行化(SERIALIZABLE)。指定事务中任何语句所读取的数据都将是在该事务开始时便存在的数据。 所有事务都是按顺序执行。 数据库锁机制// 有空再写笔记补充 参考资料: [1] http://blog.csdn.net/zdwzzu2006/article/details/5947062 [2] http://www.cnblogs.com/Achang/archive/2013/03/22/2975161.html","categories":[{"name":"MySQL","slug":"MySQL","permalink":"http://apparition957.github.io/categories/MySQL/"}],"tags":[]},{"title":"设计模式笔记 - 观察者模式(Observer Pattern)","slug":"设计模式笔记-观察者模式(Observer-Pattern)","date":"2017-03-12T16:22:33.000Z","updated":"2017-07-31T12:55:01.000Z","comments":true,"path":"2017/03/13/设计模式笔记-观察者模式(Observer-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/13/设计模式笔记-观察者模式(Observer-Pattern)/","excerpt":"","text":"模式定义 观察者模式(Observer Pattern)定了对象间的一种一对多的依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并自动更新。观察者模式又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式。 模式结构观察者模式包含如下角色: Subject:目标 ConcreteSubject:具体目标 Observer:观察者 ConcreteObserver:具体观察者 模式案例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384// Subject-目标(主题)public abstract class Subject { public List<Observer> observers; public Subject() { observers = new ArrayList<Observers>(); } public void attach(Observer observer) { observers.add(observer); } public boolean detach(Observer observer) { return observers.remove(observer); } public abstract void notify();}// Observer-观察者 -> 定义共同约定的接口public interface Observer { public void update();}// ConcreteSubject-具体目标(主题)public class ConcreteSubject extends Subject { public void notify(String text) { for (Observer observer: observers) { observer.update(text); } }}// ConcreteObserver-具体观察者// 观察者1public class ConcreteObserver1 implements Observer { private ConcreteSubject concreteSubject; public ConcreteObserver1(ConcreteSubject concreteSubject) { this.concreteSubject = concreteSubject; } public void update(String text) { display(text); } public void display(String text) { System.out.println("ConcreteObserver1 get message: " + text); }}// 观察者2public class ConcreteObserver2 implements Observer { private ConcreteSubject concreteSubject; public ConcreteObserver2(ConcreteSubject concreteSubject) { this.concreteSubject = concreteSubject; } public void update(String text) { display(text); } public void display(String text) { System.out.println("ConcreteObserver2 get message: " + text + ", also can individuation."); }}// 测试类public class Test { public static void main(String[] args) { ConcreateSubject subject = new ConcreteSubject(); Observer observer1 = new ConcreteObserver1(); Observer observer2 = new ConcreteObserver2(); // 向目标(主体)注册观察者 subject.attach(observer1); subject.attach(observer2); // 向已注册的观察者发送消息 subject.notify("test message - A"); subject.notify("test message - B"); } } 模式分析 观察者模式描述了如何建立对象与对象之间的依赖关系,如果构造满足这种需求的系统。 这一模式中的关键对象是被观察目标与观察者两者,一个目标可以有任意数目的与之相依赖的观察者,一旦目标的状态发生改变,所有观察者都将得到通知。 作为对这个通知的响应,每个观察者都可以即时更新自己的状态。 观察者模式的优点: 观察者模式可以实现表现层与数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样的表现层作为具体的观察者角色(前提是实现了更新接口)。 观察者模式在观察目标与观察者之间建立一个抽象的耦合。 观察者模式支持广播通信。 观察者模式符合“开闭原则”的要求。 观察者模式的缺点: 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。 适用环境在以下情况可以使用观察者模式: 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。 一个对象的改变将导致一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象….,可以使用观察者模式创建一种链式触发机制。 参考资料: [1] http://blog.csdn.net/hguisu/article/details/7556625 [2] https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/observer.html","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 策略模式(Strategy Pattern)","slug":"设计模式笔记-策略模式(Strategy-Pattern)","date":"2017-03-12T04:36:13.000Z","updated":"2017-07-31T12:54:03.000Z","comments":true,"path":"2017/03/12/设计模式笔记-策略模式(Strategy-Pattern)/","link":"","permalink":"http://apparition957.github.io/2017/03/12/设计模式笔记-策略模式(Strategy-Pattern)/","excerpt":"","text":"模式定义 策略模式(Strategy Pattern)定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法独立于使用算法的客户而变化。 模式结构 Context:环境类 Strategy:抽象策略类 ConcreteStrategy:具体策略类 模式案例不同的鸭子或许会有不同的飞行模式,又或许两种不同的鸭子有相似的飞行模式。对此我们可以使用策略模式来设计不同的飞行模式,即可提高代码的复用,也易于飞行模式的扩展。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950// Context-环境类 -> 鸭子public class Duck { FlyBehavior flyBehavior; public void performFly() { this.flyBehavior.fly(); } public void setFlyBehavior(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; }}// Strategy-抽象策略类 -> 飞行行为public interface FlyBehavior { public void fly();}// ConcreteStrategy-具体策略类// 算法1public class FlyWithWings implements FlyBehavior { @Override public void fly() { System.out.println("I'm flying with wings"); }}// 算法2public class FlyNoWay implements FlyBehavior { @Override public void fly() { System.out.println("I can't fly"); }}// test-测试类public class DuckTest { public static void main(String[] args) { Duck duck = new Duck(); duck.setFlyBehavior(new FlyWithWings()); // "I'm flying with wings" duck.performFly(); duck.setFlyBehavior(new FlyNoWay()); // "I can't fly" duck.performFly(); }} 模式分析优点: 策略模式提供了“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。 策略模式提供了管理相关的算法则的办法。 策略模式提供了可以利用组合替换继承的办法。 策略模式可以避免使用多重条件(if..else..)语句。 缺点: 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。即在设计接口时,必须适当的暴露出全部的接口。 策略模式将造成产生很多策略类,但相对于未使用设计模式管理算法族来说,比较容易管理。 参考资料: [1] http://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/strategy.html [2] http://blog.csdn.net/hguisu/article/details/7558249/","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"设计模式笔记 - 概述","slug":"设计模式笔记-概述","date":"2017-03-11T16:08:28.000Z","updated":"2017-07-31T12:50:21.000Z","comments":true,"path":"2017/03/12/设计模式笔记-概述/","link":"","permalink":"http://apparition957.github.io/2017/03/12/设计模式笔记-概述/","excerpt":"","text":"所有设计模式笔记基本上依据图说设计模式以及《Head First Design Pattern》总结写的,深入部分依然需要根据它们来仔细钻研。 设计模式(Design Pattern)是对软件设计中普遍存在的各种问题,所提出的具有针对性的解决方案。 设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象(OO)设计模式通常以类(class)或对象(object)来描述其中的关系和互相作用,但不涉及用来完成应用程序的特定类或对象。 设计模式能使不稳定趋于相对稳定、具体趋于相对抽象,降低代码间的耦合程度,提高软件设计的可扩张性,易于维护。设计模式也可以把开发人员的思想架构的层次提高至模式层面,而不是仅仅停留在琐碎的对象上。 设计模式与现有的库、框架有什么区别? 库和框架为我们提供了某些特定的方法,让我们的代码可以轻易地引用。而设计模式是提供开发人员如何更加有效的组织代码,提高代码的复用、使代码更加容易维护。某些库和框架当中也会使用设计模式,但是它们不是设计模式。 设计模式中遵循以下设计原则: 找出应用中可能需要变化的地方,把它们独立出来,不要和那些不需要变化的代码混在一起。即把需要变化的部分进行封装,以便以后进行修改或扩展,和其他稳定的代码相区分,使系统更有弹性。 针对接口编程,而不是针对实现编程面对接口编程,使用接口作为基类而非抽象类的原因,而为什么使用接口替代抽象类,接口代表的是一种行为规范,而抽象类代表的具体类的雏形,两者代表的意义不同。 多用组合, 少用继承 参考资料: [1] https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F","categories":[{"name":"设计模式","slug":"设计模式","permalink":"http://apparition957.github.io/categories/设计模式/"}],"tags":[]},{"title":"范式设计与反范式设计","slug":"范式设计与反范式设计","date":"2017-03-11T03:57:56.000Z","updated":"2017-07-31T12:49:00.000Z","comments":true,"path":"2017/03/11/范式设计与反范式设计/","link":"","permalink":"http://apparition957.github.io/2017/03/11/范式设计与反范式设计/","excerpt":"","text":"范式设计关系规范化是在基于关系数据库中分将各个数据库表之间存在访问异常的关系分解为结构良好的关系的过程,使得这些关系只存在最小的冗余或没有冗余。 而规范化范式(Normal Forma, NF)则是在关系规范化的前提下符合某一种级别的关系模式的集合。关系数据库中的关系必须满足一定的要求,即满足不同的范式。目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、第四范式(4NF)、第五范式(5NF)和第六范式(6NF)。满足最低要求的范式是第一范式(1NF)。在第一范式的基础上进一步满足更多要求的称为第二范式(2NF),其余范式以次类推。一般说来,数据库只需满足第三范式(3NF)就行了。 以下解释均以该图做为基础 第一范式如果关系R中的所有属性不可再细分为更加基本的数据单位时,则该关系R满足第一范式。 第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式就是无重复的列。 图a)中学生关系中不满足第一范式,因为“联系方式”属性可以在细分为“手机号码”、“电子邮箱”等颗粒度更细的属性。图b)为满足第一范式的其中一种解决方式。 第二范式如果关系R满足第一范式,并消除了关系中属性的部分依赖,则该关系满足第二范式。 第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是属性完全依赖于主键。 分析图b)中学生关系中,学生的成绩可以是若干个的。如果通过复合主键(学号、课程编号)来确定成绩的唯一性,就会造成数据的冗余。图c)为满足第二范式的其中一种解决方式。 第三范式如果关系R满足第二范式,并切断了关系中的属性传递依赖,则该关系满足第三范式。 第三范式(3NF)要求实体的属性之间不存在间接关系,即不存在实体中的某一字段依赖于其他非主键字段,而该非主键字段又依赖于主键字段这一关系。简而言之,第三范式就是不存在传递依赖。 分析图c)中学生关系,(学号)->专业,(专业)->班级,故(学生)->班级之间存在传递函数依赖。图d)为满足第三范式的其中一种解决方式。 反范式设计范式设计可以有效避免数据的冗余,降低维护数据完整性的成本,易于数据库表拓展设计。但是完整严格基于范式设计(一般为第三范式以上的更高范式)会导致数据库表的增多,查询数据时需要联合多表,对数据操作的性能降低。 反范式设计即是提出反对范式设计的一种设计模式。 在反范式设计模式中,允许在数据库表中适当的数据冗余,尽可能减少数据库表的数量,从而提高数据操作的性能,从本质上就是利用空间来换时间,把数据冗余在多个表中,进行查询时可以减少或者避免表之间的关联。 此图中学生模型可以从第三范式退化到第二范式中(此举仅用于说明,其实不建议) 最后:范式设计是基于关系型数据库中较良好的设计模型,可以建立冗余较小、结构合理的数据库。但在面对数据量较大的数据库中,单单依靠范式来设计数据库表容易造成较大的性能损失。所以在设计过程中应当结合反范式设计,尽可能构造出性能与空间均衡(或者根据需求偏向于某一方面)的数据库。 参考资料: [1] http://kimi.it/418.html [2] http://www.cnblogs.com/knowledgesea/p/3667395.html [3] http://www.cnblogs.com/binyue/p/4519858.html","categories":[{"name":"MySQL","slug":"MySQL","permalink":"http://apparition957.github.io/categories/MySQL/"}],"tags":[]},{"title":"数据库语言的五大分类","slug":"数据库语言的五大分类","date":"2017-03-10T15:34:01.000Z","updated":"2017-07-31T12:42:00.000Z","comments":true,"path":"2017/03/10/数据库语言的五大分类/","link":"","permalink":"http://apparition957.github.io/2017/03/10/数据库语言的五大分类/","excerpt":"","text":"SQL语言分类有五大类:数据定义语言(DDL)、数据操纵语言(DML)、数据查询语言(DQL)、数据控制语言(DCL)和事务处理语言(TPL)。 数据定义语言 数据定义语言(Data Definition Language,DDL)是SQL语言中用于创建或删除数据库对象的语句。这类语句也可以定义数据表对象的主外键、索引等要素。主要语句如下: CREATE DATABASE - 创建数据库 DROP DATABASE - 删除数据库 ALTER DATABASE - 修改数据库属性 CREATE TABLE - 创建数据库表 DROP TABLE - 删除数据库表 CREATE INDEX - 创建索引 DROP INDEX - 删除索引 数据操纵语言 数据操纵语言(Data Manipulation Language, DML)是SQL语言中用于添加、修改、删除数据的语句,需要进行事务提交(commit)。主要语句如下: INSERT - 向数据库表中插入数据 UPDATE - 更新数据库表中的数据 DELETE - 从数据库表中删除数据 数据查询语言 数据查询语言(Data Query Language, DQL)是SQL语言中用于对数据库进行查询的语句。主要语句如下: SELECT - 从至少一个数据库表中查询数据 数据控制语言 数据控制语言(Data Control Language, DCL)是SQL语言用于对数据对象访问权进行控制的语句。主要语句如下: GRANT - 授予用户对数据库对象的权限 DENY - 拒绝授予用户对数据库对象的权限 REVOKE - 撤销用户对数据库对象的权限 事务处理语言 事务处理语言(Transaction Process Language, TPL)是SQL语言用于数据库内部事务处理的语句。主要语句如下: BEGIN TRANSACTION - 开始事务 COMMIT - 提交事务 ROLLBACK - 回滚事务","categories":[{"name":"MySQL","slug":"MySQL","permalink":"http://apparition957.github.io/categories/MySQL/"}],"tags":[]},{"title":"Eureka客户端强制关闭, 注册中心界面中客户端依然以UP状态存在","slug":"Eureka客户端强制关闭-注册中心界面中客户端依然以UP状态存在","date":"2017-03-09T12:50:46.000Z","updated":"2017-07-31T12:39:08.000Z","comments":true,"path":"2017/03/09/Eureka客户端强制关闭-注册中心界面中客户端依然以UP状态存在/","link":"","permalink":"http://apparition957.github.io/2017/03/09/Eureka客户端强制关闭-注册中心界面中客户端依然以UP状态存在/","excerpt":"","text":"当手动强制关闭服务时,而非调用程序关闭时,发现Eureka提供的注册中心界面中仍然保留有已关闭的客户端信息,但无法正常访问。大约在三个心跳周期(90s)后出现警告提醒: 1EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE. 以上代码说明Eureka进入了自我保护模式。 产生原因在于,Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。 以上问题,可以通过关闭自我保护模式来注销已关闭的客户端。 12345# 关闭自我保护功能eureka.server.enableSelfPreservation=false# 配置心跳检测时长eureka.instance.leaseRenewalIntervalSeconds=1eureka.instance.leaseExpirationDurationInSeconds=2 在当出现以上情况后,注册中心界面会出现以下文字,并发现被关闭的客户端已成功注销。 1THE SELF PRESERVATION MODE IS TURNED OFF.THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS. 可以通过Log文件查看客户端被注销的过程: 12342017-03-09 10:56:09.872 INFO 4132 --- [nio-1111-exec-2] c.n.e.registry.AbstractInstanceRegistry : Registered instance COMPUTE-SERVICE/localhost:compute-service:2223 with status UP (replication=false)2017-03-09 10:59:57.768 INFO 4132 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : Evicting 1 items (expired=1, evictionLimit=1)2017-03-09 10:59:57.770 WARN 4132 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : DS: Registry: expired lease for COMPUTE-SERVICE/localhost:compute-service:22232017-03-09 10:59:57.771 INFO 4132 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : Cancelled instance COMPUTE-SERVICE/localhost:compute-service:2223 (replication=false) 参考资料: [1] http://www.cnblogs.com/moonandstar08/p/6435710.html","categories":[{"name":"Spring","slug":"Spring","permalink":"http://apparition957.github.io/categories/Spring/"}],"tags":[]},{"title":"Robbin中 No instances available for XX 亦或者遇到 NULL 错误","slug":"Robbin中-No-instances-available-for-XX-亦或者遇到-NULL-错误","date":"2017-03-08T15:06:35.000Z","updated":"2017-07-31T12:37:56.000Z","comments":true,"path":"2017/03/08/Robbin中-No-instances-available-for-XX-亦或者遇到-NULL-错误/","link":"","permalink":"http://apparition957.github.io/2017/03/08/Robbin中-No-instances-available-for-XX-亦或者遇到-NULL-错误/","excerpt":"","text":"错误如下所示: 第一, 检查maven下的pom文件是否正确导入所有的包 1234567891011121314151617181920212223242526272829303132333435363738394041<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.5.RELEASE</version> <relativePath></relativePath></parent><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Brixton.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement><dependencies> <!-- 导入ribbon依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <!-- 导入eureka依赖, 进行服务注册 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <!-- 导入spring-boot依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies> 第二, 检查是否有BeanFactory中是否存在RestTemplate, 并有无加载注解@LoadBalanced 12345@Bean@LoadBalancedpublic RestTemplate restTemplate() { return new RestTemplate();} 第三, 检查是否填写错误路由 12345@RequestMapping(value="/add", method=RequestMethod.GET)public String addService() { // 正确代码: "http://COMPUTE-SERVICE/add?a=10&b=30" return restTemplate.getForEntity("COMPUTE-SERVICE/add?a=10&b=30", String.class).getBody();}","categories":[{"name":"Spring","slug":"Spring","permalink":"http://apparition957.github.io/categories/Spring/"}],"tags":[]},{"title":"PO/VO/BO/DTO/DAO/POJO相关概念","slug":"PO-VO-BO-DTO-DAO-POJO相关概念","date":"2017-03-07T01:38:32.000Z","updated":"2017-07-31T12:32:16.000Z","comments":true,"path":"2017/03/07/PO-VO-BO-DTO-DAO-POJO相关概念/","link":"","permalink":"http://apparition957.github.io/2017/03/07/PO-VO-BO-DTO-DAO-POJO相关概念/","excerpt":"","text":"PO(Persistant Object) - 持久化对象在ORM(Object-Relation Mapping,对象关系映射)中,PO通常对应数据库当中的数据模型,是与数据库中的表映射出来的java对象。最简单的PO对应于数据库中的某表的一条记录,多条记录可以作为PO的集合。PO当中不应该含有任何对数据库的操作。 VO(Value Object) - 值对象通过new创建的Java对象,可以根据业务的需求进行个性化定制。 VO通常用于业务层不同模块之间进行数据传递,和PO一样仅仅包含数据。 PO用于数据层,VO用于业务逻辑层,两者所负责的区域不同。 BO(Business Object) - 业务对象封装业务逻辑的Java对象,通过调用DAO方法,结合PO、VO进行业务操作。 表示应用程序领域内“事物”的所有实体类。这些实体类驻留在服务器上,并利用服务类来协助完成它们的职责。 DTO(Data Transfer Object) - 数据传输对象根据业务逻辑获取相应的PO集合,对集合中PO内数据进行校验/封装,返回调用方需要的数据。 通常用于业务逻辑层与表现层之间通过网络进行数据传递,可以有效减少请求次数,降低延迟。 DAO(Data Access Object) - 数据访问对象封装对数据库的访问,结合PO/POJO对数据库进行相应的操作,为业务逻辑层提供相应的接口。 POJO(Plain Old Java Object) - 普通Java对象传统意义的 JavaBean 对象,只有属性以及相应的getter/setter方法,不包含任何逻辑代码。 参考资料: [1] http://blog.csdn.net/gaoyunpeng/article/details/2093211 [2] http://www.cnblogs.com/bluestorm/archive/2012/09/26/2703234.html [3] https://my.oschina.net/liting/blog/354077","categories":[{"name":"Spring","slug":"Spring","permalink":"http://apparition957.github.io/categories/Spring/"}],"tags":[]},{"title":"MySQL基本概念 -- 主键/外键/索引","slug":"MySQL基本概念-主键-外键-索引","date":"2017-03-06T10:06:30.000Z","updated":"2017-07-31T12:26:13.000Z","comments":true,"path":"2017/03/06/MySQL基本概念-主键-外键-索引/","link":"","permalink":"http://apparition957.github.io/2017/03/06/MySQL基本概念-主键-外键-索引/","excerpt":"","text":"主键(PRIMARY KEY)主键能够唯一标识表中某一行的属性或属性组。 主键是索引的一种,并且是唯一性索引的一种。 主键约束唯一标识数据库表中的每条记录。 主键必须包含唯一的值。 主键列不能包含NULL值。 主键常常与外键构成参照完整性约束,防止出现数据不一致。 每个表应当有一个主键,但每个表只能有一个主键。 1234567-- 每一笔订单对应一个唯一的订单编号-- 若字段使用AUTO_INCREMENT, 其字段类型必须是TINYINT/SMALLINT/INT/BIGINT其中一种CREATE TABLE order ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(40) NOT NULL, PRIMARY KEY(id)); 外键(FOREIGN KEY)如果一个实体的某个字段指向另一个实体的主键,就称为外键。 其中,被指向的实体,称之为主实体(主表)。负责指向的实体,称之为从实体(从表),也叫子实体(子表)。 主要作用维护两个表之间的数据一致性,加强两个表数据之间的链接的一列或多列。 每当子表中的字段需要添加记录时,必须查询父表中是否存在该字段记录。需要删除父表中的主键时,则必须查询是否有其他表的外键与其绑定。 1234567891011121314151617181920212223-- 每样商品都有特定的商品种类(电脑对应电子产品, 口红对应化妆品)CREATE TABLE mb_item_type ( id INT NOT NULL AUTO_INCREMENT, type VARCHAR(200) NOT NULL, PRIMARY KEY(id));INSERT INTO mb_item_type (type) VALUES ('电子产品'), ('化妆品');CREATE TABLE item ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(200) NOT NULL, type_id INT NOT NULL, PRIMARY KEY(id), FOREIGN KEY(type_id) REFERENCES mb_item_type(id));-- 对应相应的商品序号INSERT INTO item (name, type_id) VALUES ('thinkpad', 1);INSERT INTO item (name, type_id) VALUES ('口红', 2);-- 错误输入, 因为在mb_item_type中不存在序号为3的记录INSERT INTO item (name, type_id) VALUES ('相机', 3); 是用来快速地寻找那些具有特定值的记录。主要是为了检索的方便,是为了加快访问速度, 按一定的规则创建的,一般起到排序作用。 唯一性索引(UNIQUE)UNIQUE 约束唯一标识数据库表中的每条记录。 UNIQUE 和 PRIMARY KEY 约束均为列或列集合提供了唯一性的保证。 PRIMARY KEY 拥有自动定义的 UNIQUE 约束。 每个表可以有多个 UNIQUE 约束,但是每个表只能有一个 PRIMARY KEY 约束。 12345678-- 一名学生对应一个唯一学号CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(40) NOT NULL, student_id INT NOT NULL, PRIMARY KEY(id), UNIQUE KEY student_unique_id (student_id) USING BTREE); 索引(KEY)根据特定的算法,来快速地查找到具有特定值(按一定的规则创建)的记录。 主要作用是为了检索的方便,是为了加快访问速度。 详细参见http://www.cnblogs.com/hustcat/archive/2009/10/28/1591648.html 参考文章: [1] http://blog.csdn.net/duck_arrow/article/details/8264686 [2] http://www.cnblogs.com/zunpeng/p/3878459.html [3] http://www.w3school.com.cn/sql/","categories":[{"name":"MySQL","slug":"MySQL","permalink":"http://apparition957.github.io/categories/MySQL/"}],"tags":[]},{"title":"Spring框架中@Controller类下的返回值","slug":"Spring框架中-Controller类下的返回值","date":"2017-02-27T16:51:56.000Z","updated":"2017-07-31T12:24:59.000Z","comments":true,"path":"2017/02/28/Spring框架中-Controller类下的返回值/","link":"","permalink":"http://apparition957.github.io/2017/02/28/Spring框架中-Controller类下的返回值/","excerpt":"","text":"Spring中Controller返回值 返回String 根据字符串名返回需要相应在/templates下的html文件 需要同时返回数据时, 只需要在Controller内部声明Model/ModelMap<String, Object>添加数据即可 12345@RequestMapping(value=\"/save\", method=RequestMethod.POST)public void save(Demo demo, Model model) { model.addAttribute(\"msg\", \"Success\"); return \"success\"} 重定向(redirect) 浏览器中url会进行改变, 但是重定向的页面无法传递request请求, 只能够重新创建request并提交 1234@RequestMapping(value=\"/save\", method=RequestMethod.POST)public void save(Demo demo) { return \"redirect:/demo/detail\"} 页面转发(forward) 浏览器中url不会改变, 但是会将到这个页面(/save)的request传递到指定页面(/demo/detail), 保证数据一致性 1234@RequestMapping(value=\"/save\", method=RequestMethod.POST)public void save(Demo demo) { return \"forward:/demo/detail\"} @ResponseBody 方法上声明了@ResponseBody注解, 则会将返回的数据(String/Model)直接输出到页面中(String/JSON) 123456@RequestMapping(value=\"/print\")@ResponseBodypublic String print(){ String message = \"Hello World, Spring MVC!\"; return message;} 返回ModelAndView 定义ModelAndView对象并返回, 对象中可添加model数据、指定view ModelAndView中可以设置view, 同时也可以设置model对视图进行渲染 123456@RequestMapping(value=\"/save\", method=RequestMethod.POST)public ModelAndView save(Demo demo) { ModelAndView mav = new ModelAndView(\"hello\");//实例化一个VIew的ModelAndView实例 mav.addObject(\"message\", \"Hello World!\");//添加一个带名的model对象 return mav; } 返回void 对应返回的视图就是路由路径 1234@RequestMapping(value=\"/demo/get\", method=RequestMethod.GET)public void getDemo() { return; // 返回的视图名称为demo文件夹下的get.html } @Controller注释的类中的方法都可以直接通过HttpServletRequest和HttpServletResponse控制request和response 12345@RequestMapping(value=\"/demo/get\", method=RequestMethod.GET)public void getDemo(HttpServletRequest request, HttpServletResponse response) { // 根据HTTP协议获取及传递 return; } 参考资料: [1] http://www.cnblogs.com/liuwt365/p/5686659.html [2] http://www.cnblogs.com/xiepeixing/p/4243801.html [3] http://itroop.iteye.com/blog/263845","categories":[{"name":"Spring","slug":"Spring","permalink":"http://apparition957.github.io/categories/Spring/"}],"tags":[]},{"title":"Spring框架中的注解","slug":"Spring框架中的注解","date":"2017-02-27T10:33:52.000Z","updated":"2017-07-31T12:23:25.000Z","comments":true,"path":"2017/02/27/Spring框架中的注解/","link":"","permalink":"http://apparition957.github.io/2017/02/27/Spring框架中的注解/","excerpt":"","text":"@RestController @RestController结合了@Controller和@ResponseBody方便实现RESTful服务的注解 当返回的是对象时, 会以JSON数据进行传输 @Controller 用于标注控制层组件 @RequestMapping 用于处理请求地址映射的注解, 可用于类或方法上 当作用于类上时, 表示类中的所有响应请求的方法以该注解上的地址作为父级路径 value @RequestMapping(value="/")(显示声明)或 @RequestMapping("/")(隐式声明) 指定请求的实际路由, 可分为以下三类: 普通的URI value=”/demo” 含有某变量的URI 1value=\"/get/{year}/{month}/{day}\" 含有正则表达式的URI 1value=\"/spring-web/{symbolicName: [a-z-]+}-{version: \\d.\\d.\\d}.{extension: \\\\.[a-z]}\" method @RequestMapping(method="POST")或 @RequestMapping(method=RequestMethod.GET) 指定请求的method类型 consumes @RequestMapping(consumes="text/html") 指定处理请求的提交内容类型(Content-Type), 例如:application/json, text/html produces @RequestMapping(produces="text/html") 指定返回的内容类型, 当且仅当request请求头里面Accept类型中包含该指定类型才返回 params @RequestMapping(params={"username=kobe", "password=123456"}) 指定request中必须包含某些指定的参数值, 才会正常处理该响应 headers @RequestMapping(headers={}) 指定request中必须包含某些指定的header值 , 才会正常处理该响应 常用的辅助注解: @RequestParam 针对于提交的表格信息进行直接获取, 也可以通过HttpServletRequest对表格信息进行间接获取(request.getParameter(“name”)) 1234<!-- 前端代码 --><form action=\"/getForm\"> <input name=\"name\" value=\"jianpeng\"/></form> 12345// 后端代码@RequestMapping(\"/getForm\")public void getForm(@RequestForm String name) { ...} @RequestBody 接收JSON对象的字符串形式, 而非JSON对象 1234@RequestMapping(\"/getJson\")public void getJson(@RequestBody List<User> users) { ...} @PathVariable 当@RequestMapping(“/{day}”)中含有动态变量时, 可在响应方法的参数列表中声明该形式参数为路由中的参数 1234@RequestMapping(\"/{day}\")public void getDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day) { ...} @ResponseBody 表示处理函数直接将函数的返回值传回到浏览器端显示 @Resource与@Autowired 都可用于装配bean, 即可写在字段上, 也可写在setter方法上. 123456789101112/* * 通过在变量上进行注解优先级大于在setter方法上进行注解 * 因为可以在不暴露setter方法的情况下, 使代码更加紧凑, 提高封装性 */@Resourceprivate String name;// 通过@Resource注解注入与setter形参上变量名称相同的bean@Resourcepublic void setName(String name) { this.name = name;} @Autowired 默认按类型装配(该注解属于Spring框架中), 默认情况下必须要求依赖对象存在, 如果允许null值, 则可以设置它的required属性为false, 如: @Autowired(required=”false”) @Autowired也可指定name来装配bean, 但必须与@Qualifier一起搭配使用 12@Autowired @Qualifier(\"baseDao\")private BaseDao baseDao; @Resource 默认按变量名进行装配(该注解属于JDK中), 名称可以通过name制定, 如: @Resource(name=”baseName”), 但如果一旦按照name制定, 则只会通过name进行装配工作 12@Resourceprivate BaseDao baseDao; 虽然两者作用的同一地方的不同部分, 但实际开发工作中的作用是一样的, 最好进行统一即可 @Service 用于标注业务(服务)层组件 @Repository 用于标注持久层, 即数据访问层(DAO)组件 @Component 泛指组件, 在spring2.5前@Service、@Controller和@Repository三种不同层的注解统称为@Component 在含义不清时可以暂时给一个应用层进行分类时可使用 @EnableWebMvc 完全控制Spring MVC, 你可以在@Configuration注解的配置类上增加@EnableWebMvc, 增加该注解以后WebMvcAutoConfiguration中配置就不会生效, 你需要自己来配置需要的每一项**. 这种情况下的配置方法建议参考WebMvcAutoConfiguration类 @Bean 用于该类/方法/变量可注册为Bean并交由Spring管理(可通过name指定需要管理的Bean的名字) @Value 用于从配置文件中获取值 12@Value({spring.database.username})private String username = 'root'; 当@Value无法获取值且被注解的变量中存在赋值时, 则使用设计者直接主动赋予的变量值作为默认值 @Import 用于导入不同包下java类文件, 并交由Spring管理其中的已注册的Bean @ImportResource 用于引入基于XML的配置文件, 同理向上 locations 从外部导入文件的路径, 有两种形式: classpath - 从工程/resource下导入 1locations={\"classpath:application-bean1.xml\", \"classpath:application-bean2.xml\"} file - 从系统中以绝对路径的形式导入 1locations={\"file:d:/demo/application-bean3.xml\"} @EnableAutoConfiguration 用于表明Spring Boot可以根据添加的jar依赖猜测你想如何配置Spring 作用于@Configuration下的常见的@EnableXX注解 @EnableAspectJAutoProxy 开启对AspectJ自动代理的支持@EnableAsync 开启异步方法的支持@EnableScheduling 开启计划任务的支持@EnableWebMvc 开启Web MVC的配置支持.@EnableConfigurationProperties开启对@ConfigurationProperties注解配置Bean的支持.@EnableJpaRepositories开启对Spring Data JPA Repository的支持.@EnableTransactionManagement 开启注解式事务的支持.@EnableCaching开启注解式的缓存支持 @ComponentScan 作用于配置类上, 用于扫描主程序位于的包以及子包下的所有组件(@Component/@Controller/@Service/@Repository) Spring集成了基于JSR-303的Bean Validation框架, 支持设计者进行数据校验工作 JSR-303是一个数据验证的规范 Bean Validation是一个通过配置注解来验证参数的框架, 它包含两部分Bean Validation API和Hibernate Validator. Bean Validation API是Java定义的一个验证参数的规范. Hibernate Validator是Bean Validation API的一个实现扩展. Bean Validation 中的 constraint表 1. Bean Validation 中内置的 constraint Constraint 详细信息 @Null 被注释的元素必须为 null @NotNull 被注释的元素必须不为 null @AssertTrue 被注释的元素必须为 true @AssertFalse 被注释的元素必须为 false @Min(value) 被注释的元素必须是一个数字, 其值必须大于等于指定的最小值 @Max(value) 被注释的元素必须是一个数字, 其值必须小于等于指定的最大值 @DecimalMin(value) 被注释的元素必须是一个数字, 其值必须大于等于指定的最小值 @DecimalMax(value) 被注释的元素必须是一个数字, 其值必须小于等于指定的最大值 @Size(max, min) 被注释的元素的大小必须在指定的范围内 @Digits (integer, fraction) 被注释的元素必须是一个数字, 其值必须在可接受的范围内 @Past 被注释的元素必须是一个过去的日期 @Future 被注释的元素必须是一个将来的日期 @Pattern(value) 被注释的元素必须符合指定的正则表达式 表 2. Hibernate Validator 附加的 constraint Constraint 详细信息 @Email 被注释的元素必须是电子邮箱地址 @Length 被注释的字符串的大小必须在指定的范围内 @NotEmpty 被注释的字符串的必须非空 @Range 被注释的元素必须在合适的范围内 @Valid - 注释要求要对该参数/变量通过校验器进行校验** Spring一旦发现@Valid注解, 就会根据被注解类中的注解规则进行数据校验, 所以如果当数据不符合校验规则, 会出现如下错误提示: 1Whitelabel Error PageThis application has no explicit mapping for /error, so you are seeing this as a fallback.Fri Aug 21 10:09:54 CST 2015There was an unexpected error (type=Bad Request, status=400).Validation failed for object='post'. Error count: 2 但为了让用户体验更加良好, 我们需要将自定义的错误页面呈现出来, 以下为有错误时的处理代码: 12345678@RequestMapping(value = \"/\", method = RequestMethod.POST)public String create(@Valid Demo demo, BindingResult result) { if (result.hasErrors()) { return \"error\"; } return \"demo\";} @ControllerAdvice 可视为一种增强版拦截器. 能把@ControllerAdvice注解内部所使用的@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到所有(或特定)的 @RequestMapping注解的方法. 12345678910@ControllerAdvicepublic class GlobalDefaultExceptionHandler { // 可返回任意数据/页面, 与Controller同样用法 @ExceptionHanlder(value=Exception.class) public voud defaultErrorHandler(HttpServletRequest req, Exception e) { e.printStackTrace(); System.out.println(\"GlobalDefaultExceptionHandler.defaultErrorHandler() raise\"); }} 默认情况 - 全局拦截 1@ControllerAdvice 仅在内部添加字符串(不声明属性名) - 作用于指定的包 1@ControllerAdvice(\"com.example.contoller\") assignableTypes - 作用于指定的类 1@ControllerAdvice(assignableTypes={ControllerA.class, ControllerB.class}) annotations - 作用于指定的注解 1@ControllerAdvice(annotations=RestController.class) @ManyToOne 与 @OneToMany @ManyToOne 从字面意思可知为 多对一, @OneToMany 为 一对多. 以上解释好比于书单与图书的关系, 一张书单可以对应多本不同图书, 一本图书可以对应多张书单. JPA通过其中的映射关系将获取的值注入到被注释的变量中 12345678910111213141516171819202122232425@Entitypublic class Order { @Id @GeneratedValue private long id; /* * @OneToMany: 指明Order与OrderItem关联关系为一对多关系 * * mappedBy: 定义类之间的双向关系. 如果类之间是单向关系, 不需要提供定义, 如果类和类之间形成双向关系, 我们就需要使用这个属性进行定义, 否则可能引起数据一致性的问题. * * * cascade: CascadeType[]类型. 该属性定义类和类之间的级联关系. 定义的级联关系将被容器视为对当前类对象及其关联类对象采取相同的操作, * 而且这种关系是递归调用的. 举个例子:Order 和OrderItem有级联关系, 那么删除Order 时将同时删除它所对应的OrderItem对象. * 而如果OrderItem还和其他的对象之间有级联关系, 那么这样的操作会一直递归执行下去. cascade的值只能从CascadeType.PERSIST(级联新建)、CascadeType.REMOVE(级联删除)、CascadeType.REFRESH(级联刷新)、CascadeType.MERGE(级联更新)中选择一个或多个. * 还有一个选择是使用CascadeType.ALL, 表示选择全部四项. * * * fatch: 可选择项包括:FetchType.EAGER 和FetchType.LAZY. 前者表示关系类(本例是OrderItem类)在主类(本例是Order类)加载的时候同时加载; 后者表示关系类在被访问时才加载,默认值是FetchType.LAZY. * */ @OneToMany(mappedBy=\"order\", cascade=CascadeType.ALL, fetch=FetchType.LAZY) @Order(value=\"id ASC\") private List<OrderItem> OrderItems;} 123456789101112131415161718192021222324@Entitypublic class OrderItem { @Id @GeneratedValue private int id; /* * @ManyToOne指明OrderItem和Order之间为多对一关系, 多个OrderItem实例关联的都是同一个Order对象. * 其中的属性和@OneToMany基本一样, 但@ManyToOne注释的fetch属性默认值是FetchType.EAGER. * * optional 属性是定义该关联类对是否必须存在. * 值为false时, 关联类双方都必须存在, 如果关系被维护端不存在, 查询的结果为null. * 值为true 时, 关系被维护端可以不存在, 查询的结果仍然会返回关系维护端, 在关系维护端中指向关系被维护端的属性为null. * optional 属性的默认值是true. 举个例:某项订单(Order)中没有订单项(OrderItem), 如果optional 属性设置为false, 获取该项订单(Order)时, 得到的结果为null, 如果optional 属性设置为true, 仍然可以获取该项订单, 但订单中指向订单项的属性为null. * 实际上在解释Order与OrderItem的关系成SQL时, optional 属性指定了他们的联接关系optional=false联接关系为inner join, * optional=true联接关系为left join. * * * @JoinColumn:指明了被维护端(OrderItem)的外键字段为order_id, 它和维护端的主键(order_id)连接, unique=true指明order_id列的值不可重复. */ @ManyToOne(cascade=CascadeType.REFRESH, optional=false) @JoinColumn(name=\"order_id\", referencedColumnName=\"order_id\") private Order order; } @Transactional 表明指定类(通常为Service类)下的所有方法或表明某些指定的方法需要进行事务管理 12345@Service@Transactionalpublic class DemoServiceImpl implements DemoService { ... } Transactional注解中常用参数说明 参数名称 功能描述 readOnly 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true) rollbackFor 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class}) rollbackForClassName 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称:@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”}) noRollbackFor 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class}) noRollbackForClassName 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”}) propagation 该属性用于设置事务的传播行为, 例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true) isolation 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置 timeout 该属性用于设置事务的超时秒数,默认值为-1表示永不超时 propagation @Transactional(propagation=Propagation.REQUIRED) :如果有事务, 那么加入事务, 没有的话新建一个(默认情况下) @Transactional(propagation=Propagation.NOT_SUPPORTED) :容器不为这个方法开启事 @Transactional(propagation=Propagation.REQUIRES_NEW) :不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务 @Transactional(propagation=Propagation.MANDATORY) :必须在一个已有的事务中执行,否则抛出异常 @Transactional(propagation=Propagation.NEVER) :必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反) @Transactional(propagation=Propagation.SUPPORTS) :如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务. @Async - 异步 @Cacheable 创建 @CachePut 更新 @CacheEvict 删除 @Scheduled @NamedQuery @Modifying @Query @EnableEurekaClient @EnableDiscoveryClient @Profile 参考文章: [1] http://www.importnew.com/18561.html [2] https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/ [3] https://course.tianmaying.com/web-development+form-validation#0 [4] http://blog.longjiazuo.com/archives/1366 [5] http://jinnianshilongnian.iteye.com/blog/1866350 [6] http://lym6520.iteye.com/blog/312125 [7] http://www.cnblogs.com/caoyc/p/5632963.html","categories":[{"name":"Spring","slug":"Spring","permalink":"http://apparition957.github.io/categories/Spring/"}],"tags":[]},{"title":"利用服务器快速搭建Wordpress","slug":"利用服务器快速搭建Wordpress","date":"2017-02-06T14:17:00.000Z","updated":"2017-08-04T08:49:32.000Z","comments":true,"path":"2017/02/06/利用服务器快速搭建Wordpress/","link":"","permalink":"http://apparition957.github.io/2017/02/06/利用服务器快速搭建Wordpress/","excerpt":"","text":"虽然是一篇傻瓜式安装Wordpress的文章, 但其中也包含了不少搭建时碰到的困难. 配置服务器现有的操作系统 选择适合Wordpress的操作系统(一般都是CentOS), 上面会有配置相关所需环境 打开wordpress安装指引 根据服务器的公网IP在浏览器打开页面, 会显示wordpress的著名”五分钟安装”页面 在安装页面中输入相关信息 密码: 使用各大服务商推荐的系统进行安装, 它们都会设置好mysql的初始密码, 此时需要根据下面步骤 1234// 1. 查看根目录是否存在default.pass类似的文本文档ls// 2. 若存在则打开该文件, 查看内部记录的数据库信息vim default.pass 数据库主机: localhost不联网不使用网卡,不受防火墙和网卡限制本机访问 127.0.0.1(这是我们需要正确填上的信息)不联网网卡传输,受防火墙和网卡限制本机访问 本机IP联网网卡传输 ,受防火墙和网卡限制本机或外部访问 完成 相关资料来源于: http://blog.csdn.net/dqjyong/article/details/21238257 http://blog.csdn.net/dqjyong/article/details/21238257","categories":[{"name":"生活技能","slug":"生活技能","permalink":"http://apparition957.github.io/categories/生活技能/"}],"tags":[]}]}