<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">iBit程序猿</title>
            <subtitle type="text">茫茫猿海中一只寂寂无闻的程序猿👨🏻‍💻</subtitle>
    <updated>2026-03-15T01:18:13+08:00</updated>
        <id>https://ibit.tech</id>
        <link rel="alternate" type="text/html" href="https://ibit.tech" />
        <link rel="self" type="application/atom+xml" href="https://ibit.tech/atom.xml" />
    <rights>Copyright © 2026, iBit程序猿</rights>
    <generator uri="https://halo.run/" version="1.3.2">Halo</generator>
            <entry>
                <title><![CDATA[gdb的操作运行中的mysql实例的小技巧]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/mysq-gdb-skills" />
                <id>tag:https://ibit.tech,2023-07-12:mysq-gdb-skills</id>
                <published>2023-07-12T08:21:27+08:00</published>
                <updated>2023-07-12T08:21:27+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="1回收内存基于默认的glibc的环境malloc-trim--release-free-memory-from-the-heap">1、回收内存(基于默认的glibc的环境)（malloc_trim--release free memory from the heap）</h3><pre><code>gdb --batch --pid $(pidof mysqld) --ex 'call malloc_trim(0)'</code></pre><h3 id="2gdb调整最大连接数连接数耗尽的情况下">2、gdb调整最大连接数(连接数耗尽的情况下)</h3><pre><code>gdb -p $(pidof mysqld) -ex &quot;set max_connections=1500&quot; -batch</code></pre><h3 id="参考资料">参考资料</h3><ul><li><a href="https://cloud.tencent.com/developer/article/2195587">gdb操作运行中的MySQL实例--&quot;黑科技&quot;</a></li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Nginx日常踩坑指南]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/nginx-daily-pit-guide" />
                <id>tag:https://ibit.tech,2023-07-12:nginx-daily-pit-guide</id>
                <published>2023-07-12T07:56:20+08:00</published>
                <updated>2023-07-12T08:26:05+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="踩坑1nginx-location-和-proxy-pass-末尾问题">踩坑1：Nginx location 和 proxy_pass 末尾<code>/</code>问题</h2><table><thead><tr><th>序号</th><th>访问URL</th><th>location配置</th><th>proxy_pass配置</th><th>后端接收的请求</th><th>备注</th></tr></thead><tbody><tr><td>1</td><td><a href="http://ibit.tech/user/test.html">http://ibit.tech/user/test.html</a></td><td>/user/</td><td><a href="http://test1/">http://test1/</a></td><td>/test.html</td><td> </td></tr><tr><td>2</td><td><a href="http://ibit.tech/user/test.html">http://ibit.tech/user/test.html</a></td><td>/user/</td><td><a href="http://test1">http://test1</a></td><td>/user/test.html</td><td> </td></tr><tr><td>3</td><td><a href="http://ibit.tech/user/test.html">http://ibit.tech/user/test.html</a></td><td>/user</td><td><a href="http://test1">http://test1</a></td><td>/user/test.html</td><td> </td></tr><tr><td>4</td><td><a href="http://ibit.tech/user/test.html">http://ibit.tech/user/test.html</a></td><td>/user</td><td><a href="http://test1/">http://test1/</a></td><td>//test.html</td><td> </td></tr><tr><td>5</td><td><a href="http://ibit.tech/user/test.html">http://ibit.tech/user/test.html</a></td><td>/user/</td><td><a href="http://test1/haha/">http://test1/haha/</a></td><td>/haha/test.html</td><td> </td></tr><tr><td>6</td><td><a href="http://ibit.tech/user/test.html">http://ibit.tech/user/test.html</a></td><td>/user/</td><td><a href="http://test1/haha">http://test1/haha</a></td><td>/hahatest.html</td><td> </td></tr></tbody></table><h2 id="踩坑2nginx-路径中多个斜杆问题">踩坑2：nginx 路径中多个斜杆问题</h2><p>如果访问 <code>https://ibit.tech//test.html</code> 重定向到 <code>https://ibit.tech/test.html</code></p><pre><code>merge_slashes off;rewrite (.*)//(.*) $1/$2 permanent;</code></pre><h2 id="参考资料">参考资料</h2><ul><li><a href="https://www.cnblogs.com/operationhome/p/15212801.html">Nginx location 和 proxy_pass路径配置详解</a></li><li><a href="https://blog.csdn.net/lyl117363/article/details/100655136">nginx处理路径中的多个斜杠/</a></li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[opentelemetry-python 运行机制]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/opentelemetry-python" />
                <id>tag:https://ibit.tech,2022-03-13:opentelemetry-python</id>
                <published>2022-03-13T17:54:10+08:00</published>
                <updated>2022-03-14T11:58:28+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="概述">概述</h3><p>OpenTelemetry 是一组标准和工具的集合，旨在管理观测类数据，如 trace、metrics、logs 等 (未来可能有新的观测类数据类型出现)。</p><p>OpenTelemetry 提供与 vendor 无关的实现，根据用户的需要将观测类数据导出到不同的后端，如开源的 Prometheus、Jaeger 或云厂商的服务中。</p><p>本文介绍 opentelemetry 在 python 应用中的使用和运行机制。</p><h3 id="示例">示例</h3><p>文档：<a href="https://opentelemetry-python.readthedocs.io/en/latest/examples/auto-instrumentation/README.html">https://opentelemetry-python.readthedocs.io/en/latest/examples/auto-instrumentation/README.html</a></p><p>安装好相关的依赖：</p><pre><code class="language-shell">$ pip install opentelemetry-sdk$ pip install opentelemetry-instrumentation$ pip install opentelemetry-instrumentation-flask$ pip install flask$ pip install requests</code></pre><p>启动应用程序：opentelemetry-instrument python server_uninstrumented.py</p><p>应用程序仅需要进行简单的trace初始化，不需任何的手工instrument，或者创建span的代码。opentelemetry-instrumentation-flask库会自动插入处理trace的相关逻辑。</p><pre><code class="language-python">from flask import Flask, requestfrom opentelemetry import tracefrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import (    BatchSpanProcessor,    ConsoleSpanExporter,)app = Flask(__name__)trace.set_tracer_provider(TracerProvider())trace.get_tracer_provider().add_span_processor(    BatchSpanProcessor(ConsoleSpanExporter()))@app.route(&quot;/server_request&quot;)def server_request():    print(request.args.get(&quot;param&quot;))    return &quot;served&quot;if __name__ == &quot;__main__&quot;:    app.run(port=8082)</code></pre><p><strong>opentelemetry-instrument命令</strong></p><p>上面示例中使用了 opentelemetry-instrument 命令进行应用程序启用，那么 opentelemetry-instrument 的作用是什么呢？</p><p>opentelemetry-instrument核心功能是将 sitecustomize 模块的路径添加到PYTHONPATH头部，以便sitecustomize可以再程序启动时被执行。</p><p>这个脚本的代码：</p><pre><code>import reimport sysfrom opentelemetry.instrumentation.auto_instrumentation import runif __name__ == '__main__':    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])    sys.exit(run())</code></pre><p>它主要是调用<code>auto_instrumentation的run</code>函数，在run函数中主要是解析参数，设置环境变量，最重要的环境变量为PYTHONPATH，他将opentelemetry.instrumentation.auto_instrumentation添加为 PYTHONPATH 的头部。</p><p><strong>sitecustomize</strong></p><p>sitecustomize机制用于解决python启动时自动执行instrument的问题。</p><p>在opentelemetry.instrumentation.auto_instrumentation 目录下有一个 sitecustomize.py文件，这个文件中的内容就是auto_instrumentation的处理逻辑。那么sitecustomize是何时调用的呢？site模块会在初始化阶段自动导入，详细请查看：<a href="https://docs.python.org/3/library/site.html">https://docs.python.org/3/library/site.html</a></p><p>在sitecustomize中会通过pkg_resources.iter_entry_points扫描 opentelemetry_instrumentor 加载那些已经安装的instrumentor，比如：django、flask等。</p><p><strong>entry_points</strong></p><p>entry_points机制用于解决如何自动找到依赖库的instrumentation库</p><p>例如：</p><p><a href="https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-django">opentelemetry-instrumentation-django</a></p><pre><code>[options.entry_points]opentelemetry_instrumentor =    django = opentelemetry.instrumentation.django:DjangoInstrumentor</code></pre><p>文档：</p><ul><li><a href="https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html">entry_point</a></li><li><a href="https://setuptools.readthedocs.io/en/latest/pkg_resources.html">pkg_resources</a></li></ul><p><strong>distro</strong></p><p>上面的示例中，应用程序还需要自己进行 TracerProvider 的初始化，否则不会输出任何span记录。那么是否可以由 opentelemetry-instrument 完成这部分工作呢？</p><p>可以的，distro就是用于解决初始化TracerProvider和配置SpanExporter的问题的。</p><p>在sitecustomize 中加载的entry_points除了opentelemetry_instrumentor还有 opentelemetry_distro和opentelemetry_configurator。</p><p><a href="https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-distro">opentelemetry-distro</a></p><pre><code>[options.entry_points]opentelemetry_distro =    distro = opentelemetry.distro:OpenTelemetryDistroopentelemetry_configurator =    configurator = opentelemetry.distro:OpenTelemetryConfigurator</code></pre><p>从上面的setup.cfg可以知道<code>opentelemetry-distro</code> 会增加opentelemetry_distro 和 opentelemetry_configurator 两个entry_points。</p><p>代码如下：</p><pre><code>import osfrom opentelemetry.environment_variables import OTEL_TRACES_EXPORTERfrom opentelemetry.instrumentation.distro import BaseDistrofrom opentelemetry.sdk._configuration import _OTelSDKConfiguratorclass OpenTelemetryConfigurator(_OTelSDKConfigurator):    passclass OpenTelemetryDistro(BaseDistro):    &quot;&quot;&quot;    The OpenTelemetry provided Distro configures a default set of    configuration out of the box.    &quot;&quot;&quot;    # pylint: disable=no-self-use    def _configure(self, **kwargs):        os.environ.setdefault(OTEL_TRACES_EXPORTER, &quot;otlp_proto_grpc_span&quot;)</code></pre><p>OpenTelemetryDistro 区别于 DefaultDistro 仅仅是设置了默认的 “OTEL_TRACES_EXPORTER”环境变量。</p><p>而 OpenTelemetryConfigurator 的作用就是进行trace初始化（完成 TracerProvider的设置），代码如下：</p><pre><code>def _init_tracing(    exporters: Sequence[SpanExporter], id_generator: IdGenerator):    # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name    # from the env variable else defaults to &quot;unknown_service&quot;    provider = TracerProvider(        id_generator=id_generator(),    )    trace.set_tracer_provider(provider)    for _, exporter_class in exporters.items():        exporter_args = {}        provider.add_span_processor(            BatchSpanProcessor(exporter_class(**exporter_args))        )</code></pre><h3 id="综上">综上</h3><p>安装以下库：</p><pre><code>$ pip install opentelemetry-sdk$ pip install opentelemetry-instrumentation$ pip install opentelemetry-instrumentation-flask$ pip install opentelemetry-distro$ pip install opentelemetry-exporter-otlp</code></pre><p>应用程序代码如下：</p><pre><code>from flask import Flask, requestapp = Flask(__name__)@app.route(&quot;/server_request&quot;)def server_request():    print(request.args.get(&quot;param&quot;))    return &quot;served&quot;if __name__ == &quot;__main__&quot;:    app.run(port=8082)</code></pre><p>启动opentelemetry-collector：</p><pre><code>docker run -p 4317:4317 \    -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \    otel/opentelemetry-collector:latest \    --config=/etc/otel-collector-config.yaml</code></pre><p>运行应用程序：</p><pre><code>opentelemetry-instrument  python server.py</code></pre><p>运行客户端程序：</p><pre><code>python client.py testing</code></pre><p>测试结果：opentelemetry-collector会输出收到的span</p><p>这样整个应用程序就完全不需要写任何代码了。</p><h3 id="版权申明"><font color='Red'>版权申明</font></h3><p>本文转载于【Timmy Yuan 伏特加空间】<a href="https://mp.weixin.qq.com/s/d5_4QsVVipuWu12UMTqc2A">5分钟实现用docker搭建Redis集群模式和哨兵模式</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[OpenTelemetry 项目解读]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/opentelemetry-project" />
                <id>tag:https://ibit.tech,2022-03-13:opentelemetry-project</id>
                <published>2022-03-13T17:31:31+08:00</published>
                <updated>2022-03-13T17:32:37+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>随着分布式应用越来越普遍，分布式应用需要依赖强大的可观测性设施来提供监控保障，强大的可观测性设施需要依赖高质量的遥测数据。虽然已经有许多开源或者商业供应商提供了遥测数据监测采集方案。但是在没有统一标准的情况下，采集的遥测数据兼容性差，维护监测客户端也给使用者带来沉重的负担。</p><p>Opentelemetry可以为开发者们提供统一的，与第三方无关的遥测数据采集方案，以解决上述的各种问题。</p><h2 id="一源起">一、源起</h2><p>Opentelemetry源自于OpenTracing与OpenCensus两大开源社区的合并。OpenTracing在2016年由Ben Sigelman发起，旨在解决开源Tracing实现重复开发监测客户端，数据模型不统一，Tracing后端兼容成本高的问题。<strong>OpenCensus则是由Google内部实践而来，结合了Tracing和 Metrics的监测客户端开源工具包</strong>。</p><p>由于两大开源社区各自的影响力都不小，而存在两个或多个Tracing的标准这个事情本身跟社区组建的主旨相违背。于是两大开源社区一拍即合，成立了OpenTelemetry。</p><h2 id="二为什么需要">二、为什么需要</h2><p>从使用者角度来看，接入Tracing监测客户端，对业务代码有一定入侵性。一旦接入了一个供应商的监测客户端，就很难切换到其他供应商提供的监测客户端。而从Tracing服务端供应商的角度来说，服务端除了要能够处理自己Tracing客户端的数据外，还需要兼容其他供应商Tracing客户端产生的数据，维护成本越来越高。尤其是在分布式应用逐渐普及的情况下，如文章开头所说，<strong>Opentelemetry的价值更加明显</strong>。</p><h2 id="三opentelemetry项目组成">三、Opentelemetry项目组成</h2><p>Opentelemetry的项目主要分为<strong>四个部分</strong>内容：</p><ul><li>跨语言规范说明</li><li>收集、转换、转发遥测数据的工具Collector</li><li>各语言监测客户端API&amp;SDK</li><li>自动监测客户端与第三方库Instrumentation&amp;Contrib</li></ul><h3 id="一跨语言规范说明">（一）跨语言规范说明</h3><p>规范说明包含话题内容比较广泛。其中有包含遥测客户端内部实现所需要的规范，也有包含遥测客户端与外部通信所需要实现的协议规范。</p><p><strong>代码仓库</strong>：<a href="https://github.com/open-telemetry/opentelemetry-specification">opentelemetry-specification</a></p><p>遥测客户端<strong>内部实现</strong>所需要的规范，如监测客户端基本架构、设计原则，遥测信号（Traces/Metrics/Logs）与辅助对象（Baggage/Context/Propagator）的概念与模型定义，实现遥测客户端需要实现的类与功能的设计等。这部分内容本文就不做详细介绍，可以在<code>specification/overview</code>.md以及相应对象文件夹下面的<code>datamodel.md/ api.md/sdk.md</code>可以进行查阅。</p><p>遥测客户端与<strong>外部通信</strong>所需要实现的协议规范主要是指OpenTelemetry Protocol（简称OTLP）。OTLP是Opentelemetry原生的遥测信号传递协议，虽然在Opentelemetry的项目中组件支持了Zipkin v2或Jaeger Thrift的协议格式的实现，但是都是以第三方贡献库的形式提供的。只有 OTLP是Opentelemetry官方原生支持的格式。</p><p>OTLP的数据模型定义是基于ProtoBuf完成的，如果你需要实现一套可以收集OTLP遥测数据的后端服务，那就需要了解里面的内容，对应可以参考代码仓库：</p><p><strong>代码仓库</strong>：<a href="https://github.com/open-telemetry/opentelemetry-proto">opentelemetry-proto</a></p><h3 id="二收集转换转发遥测数据的工具collector">（二）收集、转换、转发遥测数据的工具Collector</h3><p>在Tracing实践中有一个原则，遥测数据收集过程需要与业务逻辑处理正交。意思就是遥测数据收集并传递到遥测后端服务的过程不占用业务逻辑的信道/线程，尽量较少监测客户端对原有业务逻辑的影响。Collector是基于这个原则实践的产物。</p><p><strong>代码仓库</strong>：<a href="https://github.com/open-telemetry/opentelemetry-collector">opentelemetry-collector</a></p><p>从架构层面来说，Collector有<strong>两种****模式</strong>。一种是把Collector部署在应用相同的主机内（如K8S的DaemonSet），或者部署在应用相同的Pod里面 （如K8S中的Sidecar），应用采集到的遥测数据，直接通过回环网络传递给Collector。这种模式统称为<strong>Agent模式</strong>。</p><p>另一种模式是把Collector当作一个独立的中间件，应用把采集到的遥测数据往这个中间件里面传递。这种模式称之为<strong>Gateway模式</strong>。</p><p>两种模式既可以单独使用，也可以组合使用，只需要数据出口的数据协议格式跟数据入口的数据协议格式保持一致。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1647163455596.png" alt="image.png" /></p><p>Opentelemetry Architecture</p><p>在Collector内部设计中，一套数据的流入、处理、流出的过程称为pipeline。一个pipeline有三部分组件组合而成，它们分别是receiver/processor/exporter。</p><ul><li><strong>receiver</strong></li></ul><p>负责按照对应的协议格式监听接收遥测数据，并把数据转给一个或者多个processor。</p><ul><li><strong>processor</strong></li></ul><p>负责做遥测数据加工处理，如丢弃数据，增加信息，转批处理等，并把数据传递给下一个processor或者传递给一个或多个exporter。</p><ul><li><strong>exporter</strong></li></ul><p>负责把数据往下一个接收端发送（一般是遥测后端），exporter可以定义同时从多个不同的processor中获取遥测数据。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1647163502186.png" alt="Collector Pipeline" /></p><p>从上面的设计可以看出，Collector除了提供让遥测数据收集与业务逻辑处理正交的能力，还充当了遥测数据对接遥测后端的适配器。Collector可以接收otlp、zipkin、jaeger等任意格式的数据，然后以otlp、zipkin、jaeger等任意格式的数据转发出去。这一切只取决于你需要输入或输出的格式是否有对应的receiver和exporter实现。otlp相关实现都是在opentelemetry-collector仓库中。而otlp以外的协议实现，则是可以参考下面代码仓库。</p><p><strong>代码仓库</strong>：<a href="https://github.com/open-telemetry/opentelemetry-collector-contrib">opentelemetry-collector-contrib</a></p><h3 id="三各语言监测客户端apisdk">（三）各语言监测客户端API&amp;SDK</h3><p>Opentelemetry为每种语言提供了基础的监测客户端API&amp;SDK包。这些包一般都是根据opentelemetry-specification里面的建议与定义，以及结合语言自身的特点，实现了在客户端采集遥测数据的基本能力。如<strong>元数据在服务间、进程间的传递，Trace添加监测与数据导出，Metrics指标的创建、使用及数据导出</strong>等。以下为各语言监测客户端API&amp;SDK包对应的代码仓库表。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1647163533815.png" alt="" /></p><p>按照Opentelemetry项目的规划，2021年上半年大部分组件完成Tracing的支持。以目前的时间点（2021年12月）来看，C++/.NET/Golang/Java/Javascript/Python/Ruby监测客户端对Tracing支持已经进入Stable状态。Erlang/Rust/Swift监测客户端对Tracing支持则是进入了Beta测试阶段。</p><p>而Opentelemetry项目规划对于Mertics的支持则晚一些。希望在2021年下半年大部分组件能够完成Metrics的支持。以目前情况来看，各语言客户端包对于Metrics支持还处在Alpha测试阶段。而对于Logs的支持，则是计划在2022年开始。</p><h3 id="四instrumentationcontrib">（四）Instrumentation&amp;Contrib</h3><p>如果单纯使用监测客户端API&amp;SDK包，许多的操作是需要修改应用代码的。如添加Tracing监测点，记录字段信息，元数据在进程/服务间传递的装箱拆箱等。这种方式具有代码侵入性，不易解耦，而且操作成本高，增加用户使用门槛。这个时候就可以利用公共组件的设计模式或语言特性等来降低用户使用门槛。</p><p><strong>利用公共组件的设计模式</strong>，例如Golang的Gin组件，实现了Middleware责任链设计模式。我们可以引用<code>github.com/gin-gonic/gin</code>库，创建一个otelgin.Middleware，手动添加到Middleware链中，实现Gin的快速监测。</p><p><strong>利用语言特性</strong>，例如Java使用Java Agent的能力与bytebuddy字节码织入技术，在Java应用启动之前找到对应类和方法，修改字节码注入监测，实现对指定类的自动监测。</p><p>理论上来说，快速监测依赖于客户端API&amp;SDK，自动监测则依赖于快速监测。但是实际操作却并没有按理论的来。如Java语言利用Java Agent与bytebuddy技术可以实现对指定开源组件实现全自动监测的，所以就没有单独提供快速监测（在OpenTracing里面是有分开的）。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1647163562431.png" alt="" /></p><h2 id="四总结">四、总结</h2><p>Opentelemetry的使命是实现收集<strong>高质量</strong>、<strong>大范围</strong>、<strong>便携</strong>的遥测数据，让有效的可观测性设施成为可能。它本身并不提供完整的可观测性解决方案，而是提供了统一的遥测数据采集方案。而如果是需要搭建一套完整的可观测性设施，还需要搭配相应的监测后端做数据持久化与数据查询，如Tracing后端zipkin/jaeger/tempo/、metrics后端prometheus、logs后端loki等。</p><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【周东科 云加社区】<a href="https://mp.weixin.qq.com/s/c1S40-b1Y3zSWZp-bS4H0w">OpenTelemetry项目解读</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[5分钟实现用docker搭建Redis集群模式和哨兵模式]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/docker-redis-pattern" />
                <id>tag:https://ibit.tech,2022-03-06:docker-redis-pattern</id>
                <published>2022-03-06T00:29:09+08:00</published>
                <updated>2022-03-06T00:36:02+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>如果让你为开发、测试环境分别搭一套哨兵和集群模式的redis，你最快需要多久，或许你需要一天？2小时？事实是可以更短。 是的，你已经猜到了，用docker部署，真的只需要十几分钟。</p><h2 id="一准备工作">一.准备工作</h2><h3 id="拉取redis镜像">拉取redis镜像</h3><p><strong>运行如下命令：</strong></p><pre><code class="language-ebnf">docker pull redis</code></pre><p>该命令拉取的镜像是官方镜像，当然你可以搜索其他的镜像，这里不做深入<br /><strong>查看镜像情况：</strong><br /><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497861529.png" alt="image.png" /></p><h2 id="二部署redis哨兵主从模式">二.部署redis哨兵主从模式</h2><p>什么是哨兵模式？--请自行百度</p><h3 id="1什么是docker-compose">1、什么是docker compose？</h3><p>Docker Compose 可以理解为将多个容器运行的方式和配置固化下来！</p><p>就拿最简单的例子来说吧，如果我们要为我们的应用容器准备一个 MySQL 容器和一个 Redis 容器，那么在每次启动时，我们先要将 MySQL 容器和 Redis 容器启动起来，再将应用容器运行起来。这其中还不要忘了在创建应用容器时将容器网络连接到 MySQL 容器和 Redis 容器上，以便应用连接上它们并进行数据交换。</p><p>这还不够，如果我们还对容器进行了各种配置，我们最好还得将容器创建和配置的命令保存下来，以便下次可以直接使用。</p><p>针对这种情况，我们就不得不引出在我们开发中最常使用的多容器定义和运行软件，也就是 Docker Compose 了。<br /><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497880905.png" alt="image.png" /></p><h3 id="2编写reids主从docker-composeyml">2、编写reids主从docker-compose.yml</h3><pre><code class="language-yaml">version: '3.7'services:  master:    image: redis    container_name: redis-master    restart: always    command: redis-server --requirepass redispwd  --appendonly yes    ports:      - 6379:6379    volumes:      - ./data1:/data  slave1:    image: redis    container_name: redis-slave-1    restart: always    command: redis-server --slaveof redis-master 6379  --requirepass redispwd --masterauth redispwd  --appendonly yes    ports:      - 6380:6379    volumes:      - ./data2:/data  slave2:    image: redis    container_name: redis-slave-2    restart: always    command: redis-server --slaveof redis-master 6379  --requirepass redispwd --masterauth redispwd  --appendonly yes    ports:      - 6381:6379    volumes:      - ./data3:/data</code></pre><p><strong>名词解释：</strong><br /><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497901820.png" alt="image.png" /></p><h3 id="3启动主从redis">3、启动主从redis</h3><p>进入redis对应的docker-compose.yml的目录，执行命令：</p><pre><code class="language-ebnf">docker-compose up -d</code></pre><p>-d表示后台运行<br />使用命令docker ps命令查看启动结果：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497911954.png" alt="image.png" /></p><p>出现截图所示，表示运行成功</p><h3 id="4编写哨兵docker-composeyml">4.编写哨兵docker-compose.yml</h3><pre><code class="language-yaml">version: '3.7'services:  sentinel1:    image: redis    container_name: redis-sentinel-1    restart: always    ports:      - 26379:26379    command: redis-sentinel /usr/local/etc/redis/sentinel.conf    volumes:      - ./sentinel1.conf:/usr/local/etc/redis/sentinel.conf  sentinel2:    image: redis    container_name: redis-sentinel-2    restart: always    ports:    - 26380:26379    command: redis-sentinel /usr/local/etc/redis/sentinel.conf    volumes:      - ./sentinel2.conf:/usr/local/etc/redis/sentinel.conf  sentinel3:    image: redis    container_name: redis-sentinel-3    ports:      - 26381:26379    command: redis-sentinel /usr/local/etc/redis/sentinel.conf    volumes:      - ./sentinel3.conf:/usr/local/etc/redis/sentinel.confnetworks:  default:    external:      name: redis_default</code></pre><h3 id="5编写哨兵sentinelconf">5.编写哨兵sentinel.conf</h3><pre><code class="language-smali"># 自定义集群名，其中172.19.0.3 为 redis-master 的 ip，6379 为 redis-master 的端口，2 为最小投票数（因为有 3 台 Sentinel 所以可以设置成 2）port 26379dir /tmpsentinel monitor mymaster 172.19.0.3 6379 2sentinel down-after-milliseconds mymaster 30000sentinel parallel-syncs mymaster 1sentinel auth-pass mymaster redispwdsentinel failover-timeout mymaster 180000sentinel deny-scripts-reconfig yes</code></pre><p>将上述文件分别拷贝3份分别命名为sentinel1.conf、sentinel2.conf、sentinel3.conf与docker-compose.yml中的配置文件对应，然后放置和哨兵的docker-compose.yml在同一目录</p><h3 id="6启动哨兵">6.启动哨兵</h3><p>进入哨兵docker-compose.yml所在目录，执行命令：</p><pre><code class="language-ebnf">docker-compose up -d</code></pre><p>查看容器，可以看到哨兵和主从redis都起来了<br /><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497935624.png" alt="image.png" /></p><h4 id="61哨兵启动日志">6.1哨兵启动日志</h4><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497953713.png" alt="image.png" /></p><p>上述日志中可以看出，哨兵监听master和slave节点</p><h4 id="62关掉master节点">6.2关掉master节点</h4><p>通过命令停止redis的master节点</p><pre><code class="language-crmsh">docker stop redis-master</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497996780.png" alt="image.png" /></p><p>通过上述日志，我们可以看到sdown，odown，他们是什么意思呢？<br /><strong>sdown是主观宕机</strong>，就一个哨兵如果自己觉得一个master宕机了，那么就是主观宕机<br /><strong>odown是客观宕机</strong>，如果quorum数量的哨兵都觉得一个master宕机了，那么就是客观宕机<br />然后就是开始选举，从日志可以看出两个哨兵选择了同一个slave节点，这时候满足了我们配置最小投票数，那么这台slave就被选为新的master。</p><h4 id="63重开master节点">6.3重开master节点</h4><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646498031875.png" alt="image.png" /></p><p>上述日志表明哨兵检测到原master重新启动，将原master节点变成新master的从节点</p><h2 id="三部署redis集群模式">三.部署redis集群模式</h2><h3 id="1创建目录和文件">1、创建目录和文件</h3><pre><code class="language-haskell">├── docker-compose.yml├── redis-6371│   ├── conf│   │   └── redis.conf│   └── data├── redis-6372│   ├── conf│   │   └── redis.conf│   └── data├── redis-6373│   ├── conf│   │   └── redis.conf│   └── data├── redis-6374│   ├── conf│   │   └── redis.conf│   └── data├── redis-6375│   ├── conf│   │   └── redis.conf│   └── data└── redis-6376    ├── conf    │   └── redis.conf    └── data</code></pre><h3 id="2redisconf-配置文件">2、redis.conf 配置文件</h3><pre><code class="language-stata">port 6371cluster-enabled yescluster-config-file nodes-6371.confcluster-node-timeout 5000appendonly yesprotected-mode norequirepass 1234masterauth 1234cluster-announce-ip 10.12.12.10 # 这里是宿主机IPcluster-announce-port 6371cluster-announce-bus-port 16371</code></pre><p>每个节点的配置只需改变端口。</p><h3 id="3docker-compose-配置文件">3、docker-compose 配置文件</h3><pre><code class="language-awk">version: &quot;3&quot;# 定义服务，可以多个services:  redis-6371: # 服务名称    image: redis # 创建容器时所需的镜像    container_name: redis-6371 # 容器名称    restart: always # 容器总是重新启动    volumes: # 数据卷，目录挂载      - ./redis-6371/conf/redis.conf:/usr/local/etc/redis/redis.conf      - ./redis-6371/data:/data    ports:      - 6371:6371      - 16371:16371    command:      redis-server /usr/local/etc/redis/redis.conf  redis-6372:    image: redis    container_name: redis-6372    volumes:      - ./redis-6372/conf/redis.conf:/usr/local/etc/redis/redis.conf      - ./redis-6372/data:/data    ports:      - 6372:6372      - 16372:16372    command:      redis-server /usr/local/etc/redis/redis.conf  redis-6373:    image: redis    container_name: redis-6373    volumes:      - ./redis-6373/conf/redis.conf:/usr/local/etc/redis/redis.conf      - ./redis-6373/data:/data    ports:      - 6373:6373      - 16373:16373    command:      redis-server /usr/local/etc/redis/redis.conf        redis-6374:    image: redis    container_name: redis-6374    restart: always    volumes:      - ./redis-6374/conf/redis.conf:/usr/local/etc/redis/redis.conf      - ./redis-6374/data:/data    ports:      - 6374:6374      - 16374:16374    command:      redis-server /usr/local/etc/redis/redis.conf  redis-6375:    image: redis    container_name: redis-6375    volumes:      - ./redis-6375/conf/redis.conf:/usr/local/etc/redis/redis.conf      - ./redis-6375/data:/data    ports:      - 6375:6375      - 16375:16375    command:      redis-server /usr/local/etc/redis/redis.conf  redis-6376:    image: redis    container_name: redis-6376    volumes:      - ./redis-6376/conf/redis.conf:/usr/local/etc/redis/redis.conf      - ./redis-6376/data:/data    ports:      - 6376:6376      - 16376:16376    command:      redis-server /usr/local/etc/redis/redis.conf</code></pre><p>编写完成后使用docker-compose up -d启动容器 ,这里没有使用主机模式（host），而是使用 NAT 模式，因为主机模式可能导致外部客户端无法连接。</p><h3 id="4进入容器创建集群">4、进入容器，创建集群</h3><p>上面只是启动了 6 个 Redis 实例，并没有构建成 Cluster 集群。<br />执行docker exec -it redis-6371 bash进入一个 Redis 节点容器，随便哪个都行。<br />继续执行以下命令创建集群：</p><pre><code class="language-dns"># 集群创建命令redis-cli -a 1234 --cluster create 10.35.30.39:6371 10.35.30.39:6372 10.35.30.39:6373 10.35.30.39:6374 10.35.30.39:6375 10.35.30.39:6376 --cluster-replicas 1# 执行过后会有以下输出Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.&gt;&gt;&gt; Performing hash slots allocation on 6 nodes...Master[0] -&gt; Slots 0 - 5460Master[1] -&gt; Slots 5461 - 10922Master[2] -&gt; Slots 10923 - 16383Adding replica 10.35.30.39:6375 to 10.35.30.39:6371Adding replica 10.35.30.39:6376 to 10.35.30.39:6372Adding replica 10.35.30.39:6374 to 10.35.30.39:6373&gt;&gt;&gt; Trying to optimize slaves allocation for anti-affinity[WARNING] Some slaves are in the same host as their masterM: e9a35d6a9d203830556de89f06a3be2e2ab4eee1 10.35.30.39:6371   slots:[0-5460] (5461 slots) masterM: 0c8755144fe6a200a46716371495b04f8ab9d4c8 10.35.30.39:6372   slots:[5461-10922] (5462 slots) masterM: fcb83b0097d2a0a87a76c0d782de12147bc86291 10.35.30.39:6373   slots:[10923-16383] (5461 slots) masterS: b9819797e98fcd49f263cec1f77563537709bcb8 10.35.30.39:6374   replicates fcb83b0097d2a0a87a76c0d782de12147bc86291S: f4660f264f12786d81bcf0b18bc7287947ec8a1b 10.35.30.39:6375   replicates e9a35d6a9d203830556de89f06a3be2e2ab4eee1S: d2b9f265ef7dbb4a612275def57a9cc24eb2fd5d 10.35.30.39:6376   replicates 0c8755144fe6a200a46716371495b04f8ab9d4c8Can I set the above configuration? (type 'yes' to accept): yes # 这里输入 yes 并回车 确认节点 主从身份 以及 哈希槽的分配&gt;&gt;&gt; Nodes configuration updated&gt;&gt;&gt; Assign a different config epoch to each node&gt;&gt;&gt; Sending CLUSTER MEET messages to join the clusterWaiting for the cluster to join.&gt;&gt;&gt; Performing Cluster Check (using node 10.35.30.39:6371)M: e9a35d6a9d203830556de89f06a3be2e2ab4eee1 10.35.30.39:6371   slots:[0-5460] (5461 slots) master   1 additional replica(s)M: 0c8755144fe6a200a46716371495b04f8ab9d4c8 10.35.30.39:6372   slots:[5461-10922] (5462 slots) master   1 additional replica(s)S: b9819797e98fcd49f263cec1f77563537709bcb8 10.35.30.39:6374   slots: (0 slots) slave   replicates fcb83b0097d2a0a87a76c0d782de12147bc86291M: fcb83b0097d2a0a87a76c0d782de12147bc86291 10.35.30.39:6373   slots:[10923-16383] (5461 slots) master   1 additional replica(s)S: f4660f264f12786d81bcf0b18bc7287947ec8a1b 10.35.30.39:6375   slots: (0 slots) slave   replicates e9a35d6a9d203830556de89f06a3be2e2ab4eee1S: d2b9f265ef7dbb4a612275def57a9cc24eb2fd5d 10.35.30.39:6376   slots: (0 slots) slave   replicates 0c8755144fe6a200a46716371495b04f8ab9d4c8[OK] All nodes agree about slots configuration.&gt;&gt;&gt; Check for open slots...&gt;&gt;&gt; Check slots coverage...[OK] All 16384 slots covered.</code></pre><p>看到上面的输出即为 Cluster 集群配置完成。且为 3 主 3 从。</p><h2 id="总结">总结：</h2><p>以上就是通过docker compose方式部署哨兵模式和集群模式的全过程，redis部署在docker中，适用于本地、开发、测试等环境，生产环境请慎用，除非你对docker有很强的掌控力。</p><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【程序员阿牛 segmentfault】<a href="https://segmentfault.com/a/1190000040755506">5分钟实现用docker搭建Redis集群模式和哨兵模式</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Redis主从模式、哨兵模式、集群模式]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/redis-pattern" />
                <id>tag:https://ibit.tech,2022-03-06:redis-pattern</id>
                <published>2022-03-06T00:21:48+08:00</published>
                <updated>2022-03-06T00:21:48+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="一主从模式">一、主从模式</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497184089.png" alt="image.png" /></p><p>主从模式即一主(master)一从(slave)或一主多从，从节点定期备份主节点的数据。<strong>主节点可提供读写操作，从节点只提供读操作。</strong></p><p><strong>优点：</strong></p><p>1、分担了主节点的读压力</p><p>2、实现了数据备份</p><p><strong>缺点：</strong></p><p>1、如果主节点失效，需要人工干预才可恢复</p><p>2、写能力和存储能力都受到单机限制</p><h3 id="二哨兵模式">二、哨兵模式</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497197936.png" alt="image.png" /></p><p>哨兵模式即在主从模式的基础上，添加一个或多个哨兵，实现主节点宕机后<strong>自动化恢复</strong>。</p><p>哨兵：特殊的redis节点，不存储数据，用于监控各个主从节点的存活状态，当主节点宕机时负责选举新的主节点。</p><p>哨兵监控原理：心跳包。当有足够多的哨兵发送心跳包却没有收到响应时，该节点会被标记为下线状态。如果是主节点下线，之后会通过选举机制在从节点中选取一个自动升级为主节点</p><p><strong>优点：</strong></p><p>1、包含主从模式的所有优点</p><p>2、实现了自动化恢复</p><p><strong>缺点：</strong></p><p>写能力和存储能力仍受到单机限制</p><h3 id="三集群模式">三、集群模式</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1646497219299.png" alt="image.png" /></p><p>上面两种模式都没有解决单机限制，所以集群模式要解决的就是这个问题</p><p>类似于数据库的水平分库分表，将数据以某种规则放置在不同的库/表中，以实现集群</p><p>集群模式可以看做是在哨兵模式的基础上进行了水平扩展</p><p><strong>优点：</strong></p><p>1、包含哨兵模式的所有优点</p><p>2、突破单机限制</p><p><strong>缺点：</strong></p><p>大概就是实现比较复杂吧</p><h3 id="版权申明"><font color='Red'>版权申明</font></h3><p>本文转载于【python】<a href="https://python.iitter.com/other/125901.html">Redis主从模式、哨兵模式、集群模式</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[docker-compose.yaml文件详解]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/docker-composeyaml文件详解" />
                <id>tag:https://ibit.tech,2022-03-05:docker-composeyaml文件详解</id>
                <published>2022-03-05T23:51:48+08:00</published>
                <updated>2022-03-05T23:52:46+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="compose和docker兼容性">Compose和Docker兼容性：</h3><p>​    Compose 文件格式有3个版本,分别为1, 2.x 和 3.x<br />​    目前主流的为 3.x 其支持 docker 1.13.0 及其以上的版本</p><h3 id="常用参数">常用参数</h3><pre><code>version           # 指定 compose 文件的版本services          # 定义所有的 service 信息, services 下面的第一级别的 key 既是一个 service 的名称     build                 # 指定包含构建上下文的路径, 或作为一个对象，该对象具有 context 和指定的 dockerfile 文件以及 args 参数值        context               # context: 指定 Dockerfile 文件所在的路径        dockerfile            # dockerfile: 指定 context 指定的目录下面的 Dockerfile 的名称(默认为 Dockerfile)        args                  # args: Dockerfile 在 build 过程中需要的参数 (等同于 docker container build --build-arg 的作用)        cache_from            # v3.2中新增的参数, 指定缓存的镜像列表 (等同于 docker container build --cache_from 的作用)        labels                # v3.3中新增的参数, 设置镜像的元数据 (等同于 docker container build --labels 的作用)        shm_size              # v3.5中新增的参数, 设置容器 /dev/shm 分区的大小 (等同于 docker container build --shm-size 的作用)     command               # 覆盖容器启动后默认执行的命令, 支持 shell 格式和 [] 格式     configs               # 不知道怎么用     cgroup_parent         # 不知道怎么用     container_name        # 指定容器的名称 (等同于 docker run --name 的作用)     credential_spec       # 不知道怎么用     deploy                # v3 版本以上, 指定与部署和运行服务相关的配置, deploy 部分是 docker stack 使用的, docker stack 依赖 docker swarm        endpoint_mode         # v3.3 版本中新增的功能, 指定服务暴露的方式            vip                   # Docker 为该服务分配了一个虚拟 IP(VIP), 作为客户端的访问服务的地址            dnsrr                 # DNS轮询, Docker 为该服务设置 DNS 条目, 使得服务名称的 DNS 查询返回一个 IP 地址列表, 客户端直接访问其中的一个地址        labels                # 指定服务的标签，这些标签仅在服务上设置        mode                  # 指定 deploy 的模式            global                # 每个集群节点都只有一个容器            replicated            # 用户可以指定集群中容器的数量(默认)        placement             # 不知道怎么用        replicas              # deploy 的 mode 为 replicated 时, 指定容器副本的数量        resources             # 资源限制            limits                # 设置容器的资源限制                cpus: &quot;0.5&quot;           # 设置该容器最多只能使用 50% 的 CPU                 memory: 50M           # 设置该容器最多只能使用 50M 的内存空间             reservations          # 设置为容器预留的系统资源(随时可用)                cpus: &quot;0.2&quot;           # 为该容器保留 20% 的 CPU                memory: 20M           # 为该容器保留 20M 的内存空间        restart_policy        # 定义容器重启策略, 用于代替 restart 参数            condition             # 定义容器重启策略(接受三个参数)                none                  # 不尝试重启                on-failure            # 只有当容器内部应用程序出现问题才会重启                any                   # 无论如何都会尝试重启(默认)            delay                 # 尝试重启的间隔时间(默认为 0s)            max_attempts          # 尝试重启次数(默认一直尝试重启)            window                # 检查重启是否成功之前的等待时间(即如果容器启动了, 隔多少秒之后去检测容器是否正常, 默认 0s)        update_config         # 用于配置滚动更新配置            parallelism           # 一次性更新的容器数量            delay                 # 更新一组容器之间的间隔时间            failure_action        # 定义更新失败的策略                continue              # 继续更新                rollback              # 回滚更新                pause                 # 暂停更新(默认)            monitor               # 每次更新后的持续时间以监视更新是否失败(单位: ns|us|ms|s|m|h) (默认为0)            max_failure_ratio     # 回滚期间容忍的失败率(默认值为0)            order                 # v3.4 版本中新增的参数, 回滚期间的操作顺序                stop-first            #旧任务在启动新任务之前停止(默认)                start-first           #首先启动新任务, 并且正在运行的任务暂时重叠        rollback_config       # v3.7 版本中新增的参数, 用于定义在 update_config 更新失败的回滚策略            parallelism           # 一次回滚的容器数, 如果设置为0, 则所有容器同时回滚            delay                 # 每个组回滚之间的时间间隔(默认为0)            failure_action        # 定义回滚失败的策略                continue              # 继续回滚                pause                 # 暂停回滚            monitor               # 每次回滚任务后的持续时间以监视失败(单位: ns|us|ms|s|m|h) (默认为0)            max_failure_ratio     # 回滚期间容忍的失败率(默认值0)            order                 # 回滚期间的操作顺序                stop-first            # 旧任务在启动新任务之前停止(默认)                start-first           # 首先启动新任务, 并且正在运行的任务暂时重叠         注意：            支持 docker-compose up 和 docker-compose run 但不支持 docker stack deploy 的子选项            security_opt  container_name  devices  tmpfs  stop_signal  links    cgroup_parent            network_mode  external_links  restart  build  userns_mode  sysctls     devices               # 指定设备映射列表 (等同于 docker run --device 的作用)     depends_on            # 定义容器启动顺序 (此选项解决了容器之间的依赖关系， 此选项在 v3 版本中 使用 swarm 部署时将忽略该选项)        示例：            docker-compose up 以依赖顺序启动服务，下面例子中 redis 和 db 服务在 web 启动前启动            默认情况下使用 docker-compose up web 这样的方式启动 web 服务时，也会启动 redis 和 db 两个服务，因为在配置文件中定义了依赖关系             version: '3'            services:                web:                    build: .                    depends_on:                        - db                              - redis                  redis:                    image: redis                db:                    image: postgres                                  dns                   # 设置 DNS 地址(等同于 docker run --dns 的作用)     dns_search            # 设置 DNS 搜索域(等同于 docker run --dns-search 的作用)     tmpfs                 # v2 版本以上, 挂载目录到容器中, 作为容器的临时文件系统(等同于 docker run --tmpfs 的作用, 在使用 swarm 部署时将忽略该选项)     entrypoint            # 覆盖容器的默认 entrypoint 指令 (等同于 docker run --entrypoint 的作用)     env_file              # 从指定文件中读取变量设置为容器中的环境变量, 可以是单个值或者一个文件列表, 如果多个文件中的变量重名则后面的变量覆盖前面的变量, environment 的值覆盖 env_file 的值        文件格式：            RACK_ENV=development      environment           # 设置环境变量， environment 的值可以覆盖 env_file 的值 (等同于 docker run --env 的作用)     expose                # 暴露端口, 但是不能和宿主机建立映射关系, 类似于 Dockerfile 的 EXPOSE 指令     external_links        # 连接不在 docker-compose.yml 中定义的容器或者不在 compose 管理的容器(docker run 启动的容器, 在 v3 版本中使用 swarm 部署时将忽略该选项)     extra_hosts           # 添加 host 记录到容器中的 /etc/hosts 中 (等同于 docker run --add-host 的作用)     healthcheck           # v2.1 以上版本, 定义容器健康状态检查, 类似于 Dockerfile 的 HEALTHCHECK 指令        test                  # 检查容器检查状态的命令, 该选项必须是一个字符串或者列表, 第一项必须是 NONE, CMD 或 CMD-SHELL, 如果其是一个字符串则相当于 CMD-SHELL 加该字符串            NONE                  # 禁用容器的健康状态检测            CMD                   # test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost&quot;]            CMD-SHELL             # test: [&quot;CMD-SHELL&quot;, &quot;curl -f http://localhost || exit 1&quot;] 或者　test: curl -f https://localhost || exit 1        interval: 1m30s       # 每次检查之间的间隔时间        timeout: 10s          # 运行命令的超时时间        retries: 3            # 重试次数        start_period: 40s     # v3.4 以上新增的选项, 定义容器启动时间间隔        disable: true         # true 或 false, 表示是否禁用健康状态检测和　test: NONE 相同     image                 # 指定 docker 镜像, 可以是远程仓库镜像、本地镜像     init                  # v3.7 中新增的参数, true 或 false 表示是否在容器中运行一个 init, 它接收信号并传递给进程     isolation             # 隔离容器技术, 在 Linux 中仅支持 default 值     labels                # 使用 Docker 标签将元数据添加到容器, 与 Dockerfile 中的 LABELS 类似     links                 # 链接到其它服务中的容器, 该选项是 docker 历史遗留的选项, 目前已被用户自定义网络名称空间取代, 最终有可能被废弃 (在使用 swarm 部署时将忽略该选项)     logging               # 设置容器日志服务        driver                # 指定日志记录驱动程序, 默认 json-file (等同于 docker run --log-driver 的作用)        options               # 指定日志的相关参数 (等同于 docker run --log-opt 的作用)            max-size              # 设置单个日志文件的大小, 当到达这个值后会进行日志滚动操作            max-file              # 日志文件保留的数量     network_mode          # 指定网络模式 (等同于 docker run --net 的作用, 在使用 swarm 部署时将忽略该选项)              networks              # 将容器加入指定网络 (等同于 docker network connect 的作用), networks 可以位于 compose 文件顶级键和 services 键的二级键        aliases               # 同一网络上的容器可以使用服务名称或别名连接到其中一个服务的容器        ipv4_address      # IP V4 格式        ipv6_address      # IP V6 格式         示例:            version: '3.7'            services:                 test:                     image: nginx:1.14-alpine                    container_name: mynginx                    command: ifconfig                    networks:                         app_net:                                # 调用下面 networks 定义的 app_net 网络                        ipv4_address: 172.16.238.10            networks:                app_net:                    driver: bridge                    ipam:                        driver: default                        config:                            - subnet: 172.16.238.0/24     pid: 'host'           # 共享宿主机的 进程空间(PID)     ports                 # 建立宿主机和容器之间的端口映射关系, ports 支持两种语法格式        SHORT 语法格式示例:            - &quot;3000&quot;                            # 暴露容器的 3000 端口, 宿主机的端口由 docker 随机映射一个没有被占用的端口            - &quot;3000-3005&quot;                       # 暴露容器的 3000 到 3005 端口, 宿主机的端口由 docker 随机映射没有被占用的端口            - &quot;8000:8000&quot;                       # 容器的 8000 端口和宿主机的 8000 端口建立映射关系            - &quot;9090-9091:8080-8081&quot;            - &quot;127.0.0.1:8001:8001&quot;             # 指定映射宿主机的指定地址的            - &quot;127.0.0.1:5000-5010:5000-5010&quot;               - &quot;6060:6060/udp&quot;                   # 指定协议         LONG 语法格式示例:(v3.2 新增的语法格式)            ports:                - target: 80                    # 容器端口                  published: 8080               # 宿主机端口                  protocol: tcp                 # 协议类型                  mode: host                    # host 在每个节点上发布主机端口,  ingress 对于群模式端口进行负载均衡     secrets               # 不知道怎么用     security_opt          # 为每个容器覆盖默认的标签 (在使用 swarm 部署时将忽略该选项)     stop_grace_period     # 指定在发送了 SIGTERM 信号之后, 容器等待多少秒之后退出(默认 10s)     stop_signal           # 指定停止容器发送的信号 (默认为 SIGTERM 相当于 kill PID; SIGKILL 相当于 kill -9 PID; 在使用 swarm 部署时将忽略该选项)     sysctls               # 设置容器中的内核参数 (在使用 swarm 部署时将忽略该选项)     ulimits               # 设置容器的 limit     userns_mode           # 如果Docker守护程序配置了用户名称空间, 则禁用此服务的用户名称空间 (在使用 swarm 部署时将忽略该选项)     volumes               # 定义容器和宿主机的卷映射关系, 其和 networks 一样可以位于 services 键的二级键和 compose 顶级键, 如果需要跨服务间使用则在顶级键定义, 在 services 中引用        SHORT 语法格式示例:            volumes:                - /var/lib/mysql                # 映射容器内的 /var/lib/mysql 到宿主机的一个随机目录中                - /opt/data:/var/lib/mysql      # 映射容器内的 /var/lib/mysql 到宿主机的 /opt/data                - ./cache:/tmp/cache            # 映射容器内的 /var/lib/mysql 到宿主机 compose 文件所在的位置                - ~/configs:/etc/configs/:ro    # 映射容器宿主机的目录到容器中去, 权限只读                - datavolume:/var/lib/mysql     # datavolume 为 volumes 顶级键定义的目录, 在此处直接调用         LONG 语法格式示例:(v3.2 新增的语法格式)            version: &quot;3.2&quot;            services:                web:                    image: nginx:alpine                    ports:                        - &quot;80:80&quot;                    volumes:                        - type: volume                  # mount 的类型, 必须是 bind、volume 或 tmpfs                            source: mydata              # 宿主机目录                            target: /data               # 容器目录                            volume:                     # 配置额外的选项, 其 key 必须和 type 的值相同                                nocopy: true                # volume 额外的选项, 在创建卷时禁用从容器复制数据                        - type: bind                    # volume 模式只指定容器路径即可, 宿主机路径随机生成; bind 需要指定容器和数据机的映射路径                            source: ./static                            target: /opt/app/static                            read_only: true             # 设置文件系统为只读文件系统            volumes:                mydata:                                 # 定义在 volume, 可在所有服务中调用     restart               # 定义容器重启策略(在使用 swarm 部署时将忽略该选项, 在 swarm 使用 restart_policy 代替 restart)        no                    # 禁止自动重启容器(默认)        always                # 无论如何容器都会重启        on-failure            # 当出现 on-failure 报错时, 容器重新启动     其他选项：        domainname, hostname, ipc, mac_address, privileged, read_only, shm_size, stdin_open, tty, user, working_dir        上面这些选项都只接受单个值和 docker run 的对应参数类似     对于值为时间的可接受的值：        2.5s        10s        1m30s        2h32m        5h34m56s        时间单位: us, ms, s, m， h    对于值为大小的可接受的值：        2b        1024kb        2048k        300m        1gb        单位: b, k, m, g 或者 kb, mb, gbnetworks          # 定义 networks 信息    driver                # 指定网络模式, 大多数情况下, 它 bridge 于单个主机和 overlay Swarm 上        bridge                # Docker 默认使用 bridge 连接单个主机上的网络        overlay               # overlay 驱动程序创建一个跨多个节点命名的网络        host                  # 共享主机网络名称空间(等同于 docker run --net=host)        none                  # 等同于 docker run --net=none    driver_opts           # v3.2以上版本, 传递给驱动程序的参数, 这些参数取决于驱动程序    attachable            # driver 为 overlay 时使用, 如果设置为 true 则除了服务之外，独立容器也可以附加到该网络; 如果独立容器连接到该网络，则它可以与其他 Docker 守护进程连接到的该网络的服务和独立容器进行通信    ipam                  # 自定义 IPAM 配置. 这是一个具有多个属性的对象, 每个属性都是可选的        driver                # IPAM 驱动程序, bridge 或者 default        config                # 配置项            subnet                # CIDR格式的子网，表示该网络的网段    external              # 外部网络, 如果设置为 true 则 docker-compose up 不会尝试创建它, 如果它不存在则引发错误    name                  # v3.5 以上版本, 为此网络设置名称</code></pre><h3 id="示例代码">示例代码</h3><pre><code>文件格式示例：version: &quot;3&quot;services:  redis:    image: redis:alpine    ports:      - &quot;6379&quot;    networks:      - frontend    deploy:      replicas: 2      update_config:        parallelism: 2        delay: 10s      restart_policy:        condition: on-failure  db:    image: postgres:9.4    volumes:      - db-data:/var/lib/postgresql/data    networks:      - backend    deploy:      placement:        constraints: [node.role == manager]</code></pre><h3 id="版权申明"><font color='Red'>版权申明</font></h3><p>本文转载于【cnwinds 简书】<a href="https://www.jianshu.com/p/0e25ebffd197">docker compose yaml文件详解</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[使用jsoup获取maven仓库所有版本信息]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/use-jsoup-query-all-maven-version-info" />
                <id>tag:https://ibit.tech,2021-11-14:use-jsoup-query-all-maven-version-info</id>
                <published>2021-11-14T23:54:06+08:00</published>
                <updated>2021-11-15T23:45:27+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>最新项目需要获取maven仓库中开源的组件版本信息，原以为使用wget命令，就可以从 <a href="https://repo.maven.apache.org/maven2/">Maven Repo</a> 轻松获取。可惜，理想很丰满，现实很有骨感。既然wget获取不到，那就自己简单实现个爬虫获取吧。</p><h2 id="分析过程">分析过程</h2><h3 id="打开页面">打开页面</h3><p>打开仓库页面：<a href="https://repo.maven.apache.org/maven2/">https://repo.maven.apache.org/maven2/</a></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1636905311156.png" alt="" /></p><p>页面上都是以目录和文件的方式展示的。</p><h3 id="查看页面源码">查看页面源码</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1636905540623.png" alt="" /></p><p>可以轻易的发现目录和文件的内容都是在id为“contents”下的<code>a</code>标签中。</p><h3 id="版本信息查看在maven-metadataxml">版本信息查看（在maven-metadata.xml）</h3><p>不断深入某个目录，可以轻易的发现组件的版本信息都在<code>maven-metadata.xml</code>中进行描述。eg:</p><p><a href="https://repo.maven.apache.org/maven2/tech/ibit/sql-builder/maven-metadata.xml">https://repo.maven.apache.org/maven2/tech/ibit/sql-builder/maven-metadata.xml</a> 的内容</p><pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;metadata&gt;    &lt;groupId&gt;tech.ibit&lt;/groupId&gt;    &lt;artifactId&gt;sql-builder&lt;/artifactId&gt;    &lt;versioning&gt;        &lt;latest&gt;2.0&lt;/latest&gt;        &lt;release&gt;2.0&lt;/release&gt;        &lt;versions&gt;            &lt;version&gt;1.0&lt;/version&gt;            &lt;version&gt;1.1&lt;/version&gt;            &lt;version&gt;2.0&lt;/version&gt;        &lt;/versions&gt;        &lt;lastUpdated&gt;20201130115230&lt;/lastUpdated&gt;    &lt;/versioning&gt;&lt;/metadata&gt;</code></pre><p><code>maven-metadata.xml</code>中包含<code>groupId</code>，<code>artifactId</code>和<code>version</code>信息。</p><h3 id="综合上述过程获取maven所有版本信息可以做以下操作">综合上述过程，获取maven所有版本信息，可以做以下操作</h3><ul><li>遍历 maven repo 所有目录信息，并获取 <code>maven-metadata.xml</code> 文件</li><li>解析 <code>maven-metadata.xml</code>，获取 <code>groupId</code>，<code>artifactId</code>和<code>version</code>。</li></ul><h2 id="示例代码">示例代码:</h2><h3 id="爬取所有的-maven-metadataxml文件和目录">爬取所有的 <code>maven-metadata.xml</code>文件和目录</h3><pre><code>package tech.ibit.crawler;import org.apache.commons.lang.StringUtils;import org.jsoup.Jsoup;import org.jsoup.nodes.Document;import org.jsoup.nodes.Element;import org.jsoup.select.Elements;import java.io.File;import java.io.FileWriter;import java.io.IOException;import java.util.Scanner;/** * Maven爬虫 * * @author IBIT程序猿 */public class MavenCrawler {    /**     * 爬取跟目录     */    private static final String ROOT = &quot;https://repo.maven.apache.org/&quot;;    /**     * maven-metadata.xml文件名     */    private static final String MAVEN_METADATA_XML_FILENAME = &quot;maven-metadata.xml&quot;;    public static void main(String[] args) {        // 参数说明        // args[0]: 爬取目录        // args[1]: sleep毫秒数        // args[2]: 开始层级（可选）        // args[3]: 开始行（可选）        String dirPath = args[0];        File dir = new File(dirPath);        if (!dir.exists() || !dir.isDirectory()) {            System.err.println(&quot;爬取目录不存在，dir: &quot; + dirPath);            System.exit(1);        }        int sleepMillis = Integer.parseInt(args[1]);        int level = 0;        if (args.length &gt; 2) {            level = Integer.parseInt(args[2]);        }        String beginLine = null;        if (args.length &gt; 3) {            beginLine = args[3];        }        File urlFile;        boolean begin = null == beginLine;        while ((urlFile = getLevelFile(dir, level)).exists()) {            level++;            boolean fileEmpty = true;            File subFile = getLevelFile(dir, level);            try (Scanner scanner = new Scanner(urlFile);                 FileWriter writer = new FileWriter(subFile)) {                while (scanner.hasNext()) {                    String line = scanner.nextLine();                    if (StringUtils.isNotBlank(line)) {                        fileEmpty = false;                        if (!begin &amp;&amp; line.equals(beginLine)) {                            begin = true;                        }                        if (begin) {                            String url = ROOT + line;                            findSubUrl(url, sleepMillis, writer);                        }                    }                }            } catch (IOException e) {                e.printStackTrace();            }            if (fileEmpty) {                urlFile.deleteOnExit();                subFile.deleteOnExit();                break;            }        }    }    /**     * 获取文件     *     * @param dir   目录     * @param level 等级     * @return 文件     */    private static File getLevelFile(File dir, int level) {        return new File(dir.getAbsolutePath() + File.separator + &quot;level_&quot; + level + &quot;.txt&quot;);    }    /**     * 查询子url     *     * @param url         当前url     * @param sleepMillis 睡眠毫秒数     * @param writer      writer     */    private static void findSubUrl(String url, int sleepMillis, FileWriter writer) {        try {            if (url.endsWith(MAVEN_METADATA_XML_FILENAME)) {                return;            }            Thread.sleep(sleepMillis);            Document doc = Jsoup.connect(url).get();            Elements links = doc.select(&quot;#contents a&quot;);            for (Element link : links) {                String absUrl = link.absUrl(&quot;href&quot;);                // 非子目录                if (!absUrl.contains(url) || url.equals(absUrl)) {                    continue;                }                String relativePath = absUrl.substring(url.length());                if (MAVEN_METADATA_XML_FILENAME.equals(relativePath) || !relativePath.contains(&quot;.&quot;)) {                    String path = absUrl.substring(ROOT.length());                    writer.write(path + &quot;\n&quot;);                    writer.flush();                    System.out.println(path);                }            }        } catch (IOException | InterruptedException e) {            e.printStackTrace();        }    }}</code></pre><blockquote><p>说明：</p><ol><li>需要在保存的文件夹中新建level_0.txt文件，并将初始url <a href="https://repo.maven.apache.org/maven2/">https://repo.maven.apache.org/maven2/</a> 放置于文件中。执行过程中，会按照遍历目录的深度，生成level_1.txt, level_2.txt等。。</li><li>当前示例代码使用单线程，并设置睡眠时间（避免ip被封），如果需要改为多线程，自行设计。</li></ol></blockquote><h3 id="解析-maven-metadataxml-示例代码">解析 <code>maven-metadata.xml</code> 示例代码</h3><pre><code>package tech.ibit.crawler;import org.apache.commons.collections4.CollectionUtils;import org.apache.commons.io.IOUtils;import org.apache.commons.lang.StringUtils;import org.w3c.dom.Document;import org.w3c.dom.Node;import org.w3c.dom.NodeList;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.DocumentBuilderFactory;import java.io.ByteArrayInputStream;import java.io.File;import java.io.FileWriter;import java.io.IOException;import java.net.URL;import java.nio.charset.StandardCharsets;import java.util.LinkedHashSet;import java.util.Scanner;import java.util.Set;/** * Maven meta * * @author IBIT程序猿 */public class MavenMetaDataParser {    /**     * 爬取跟目录     */    private static final String ROOT = &quot;https://repo.maven.apache.org/&quot;;    /**     * maven-metadata.xml文件名     */    private static final String MAVEN_METADATA_XML_FILENAME = &quot;maven-metadata.xml&quot;;    public static void main(String[] args) {        // 参数说明        // args[0]: 爬取目录        // args[1]: sleep毫秒数        // args[2]: 开始层级        // args[3]: 结束层级        // args[4]: 开始行（可选）        if (args.length &lt; 4) {            System.err.println(&quot;参数：爬取目录 sleep毫秒数 开始层级 结束层级 开始行（可选）&quot;);            System.exit(1);        }        String dirPath = args[0];        File dir = new File(dirPath);        if (!dir.exists() || !dir.isDirectory()) {            System.err.println(&quot;爬取目录不存在，dir: &quot; + dirPath);            System.exit(1);        }        int sleepMillis = Integer.parseInt(args[1]);        int beginLevel = Integer.parseInt(args[2]);        int endLevel = Integer.parseInt(args[3]);        String beginLine = null;        if (args.length &gt; 4) {            beginLine = args[4];        }        boolean begin = null == beginLine;        for (int i = beginLevel; i &lt;= endLevel; i++) {            File urlFile = getLevelFile(dir, i);            if (!urlFile.exists()) {                break;            }            try (Scanner scanner = new Scanner(urlFile);                 FileWriter writer = new FileWriter(getVersionLevelFile(dir, i))) {                while (scanner.hasNext()) {                    String line = scanner.nextLine();                    if (StringUtils.isNotBlank(line)) {                        if (!begin &amp;&amp; line.equals(beginLine)) {                            begin = true;                        }                        if (begin &amp;&amp; line.endsWith(MAVEN_METADATA_XML_FILENAME)) {                            String url = ROOT + line;                            appendVersions(url, sleepMillis, writer);                        }                    }                }            } catch (IOException e) {                e.printStackTrace();            }        }    }    /**     * 生成版本     *     * @param url         url     * @param sleepMillis 睡眠毫秒数     * @param writer      writer     */    private static void appendVersions(String url, int sleepMillis, FileWriter writer) {        try {            Thread.sleep(sleepMillis);            String xmlContent = IOUtils.toString(new URL(url), StandardCharsets.UTF_8);            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();            DocumentBuilder builder = factory.newDocumentBuilder();            try (ByteArrayInputStream in = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8))) {                Document doc = builder.parse(in);                String groupId = getSingleValue(doc, &quot;groupId&quot;);                if (StringUtils.isBlank(groupId)) {                    return;                }                String artifactId = getSingleValue(doc, &quot;artifactId&quot;);                if (StringUtils.isBlank(artifactId)) {                    return;                }                Set&lt;String&gt; versions = getMultiValues(doc, &quot;version&quot;);                if (CollectionUtils.isEmpty(versions)) {                    return;                }                String versionLine = groupId + &quot;:&quot; + artifactId + &quot;:&quot; + StringUtils.join(versions, &quot;,&quot;);                writer.write(versionLine + &quot;\n&quot;);                writer.flush();                System.out.println(versionLine);            }        } catch (Exception e) {            e.printStackTrace();        }    }    /**     * 获取文件     *     * @param dir   目录     * @param level 等级     * @return 文件     */    private static File getLevelFile(File dir, int level) {        return new File(dir.getAbsolutePath() + File.separator + &quot;level_&quot; + level + &quot;.txt&quot;);    }    /**     * 获取文件     *     * @param dir   目录     * @param level 等级     * @return 文件     */    private static File getVersionLevelFile(File dir, int level) {        return new File(dir.getAbsolutePath() + File.separator + &quot;version_level_&quot; + level + &quot;.txt&quot;);    }    /**     * 获取单个值     *     * @param document 文档     * @param tagName  标签名称     * @return 单个值     */    private static String getSingleValue(Document document, String tagName) {        NodeList nodeList = document.getElementsByTagName(tagName);        if (nodeList.getLength() == 0) {            return null;        }        return getNodeValue(nodeList.item(0));    }    /**     * 获取多个值     *     * @param document 文档     * @param tagName  标签名称     * @return 值集合     */    private static Set&lt;String&gt; getMultiValues(Document document, String tagName) {        Set&lt;String&gt; values = new LinkedHashSet&lt;&gt;();        NodeList nodeList = document.getElementsByTagName(tagName);        for (int i = 0; i &lt; nodeList.getLength(); i++) {            String value = getNodeValue(nodeList.item(i));            if (null != value) {                values.add(value);            }        }        return values;    }    /**     * 获取节点值     *     * @param node 节点     * @return 节点值     */    private static String getNodeValue(Node node) {        if (null == node) {            return null;        }        return node.getFirstChild().getNodeValue();    }}</code></pre><blockquote><p>说明</p><ol><li>该示例代码就是读取爬虫生成的level_x.txt文件中的maven-metadata.xml文件，并解析出对应的groupId, artifactId, version</li><li>当前示例代码使用单线程，并设置睡眠时间（避免ip被封），如果需要改为多线程，自行设计。</li></ol></blockquote><h3 id="其他说明pomxml引入依赖说明">其他说明，pom.xml引入依赖说明</h3><pre><code>    &lt;dependencies&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.jsoup&lt;/groupId&gt;            &lt;artifactId&gt;jsoup&lt;/artifactId&gt;            &lt;version&gt;1.14.3&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;commons-lang&lt;/groupId&gt;            &lt;artifactId&gt;commons-lang&lt;/artifactId&gt;            &lt;version&gt;2.6&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;            &lt;artifactId&gt;commons-collections4&lt;/artifactId&gt;            &lt;version&gt;4.4&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;commons-io&lt;/groupId&gt;            &lt;artifactId&gt;commons-io&lt;/artifactId&gt;            &lt;version&gt;2.11.0&lt;/version&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[jsoup介绍]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/jsoup-introdution" />
                <id>tag:https://ibit.tech,2021-11-14:jsoup-introdution</id>
                <published>2021-11-14T23:23:52+08:00</published>
                <updated>2021-11-14T23:32:18+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>jsoup 是一个用于处理 HTML 的 Java 库。 它使用 HTML5 最佳 DOM 方法 和 CSS 选择器，为提取 URL 以及提取和处理数据提供了非常方便的API。</p><p>jsoup 实现 <a href="https://whatwg.org/html">WHATWG HTML5</a> 规范，并将HTML解析为与现代浏览器相同的DOM。</p><ul><li>从URL、文件或字符串中抓取并<a href="https://jsoup.org/cookbook/input/parse-document-from-string">解析</a>HTML</li><li>使用 DOM 遍历或 CSS 选择器<a href="https://jsoup.org/cookbook/extracting-data/selector-syntax">查找</a>和提取数据</li><li><a href="https://jsoup.org/cookbook/modifying-data/set-html">处理</a> HTML 元素、属性和文本</li><li>根据安全的白名单<a href="https://jsoup.org/cookbook/cleaning-html/whitelist-sanitizer">清除</a>用户提交的内容，以防止XSS攻击</li><li><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#html">输出</a>整洁的 HTML</li></ul><p>jsoup旨在处理和发现的所有各种HTML； 从原始和验证到无效的 tag； jsoup将创建一个明智的解析树。</p><p><strong>示例</strong></p><p>获取 <a href="https://en.wikipedia.org/wiki/Main_Page">Wikipedia</a> 主页，将其解析为DOM，然后从“新闻中”部分的标题中选择<a href="https://jsoup.org/apidocs/index.html?org/jsoup/select/Elements.html">元素</a>列表（<a href="https://try.jsoup.org/~LGB7rk_atM2roavV0d-czMt3J_g">在线示例</a>，<a href="https://github.com/jhy/jsoup/blob/master/src/main/java/org/jsoup/examples/Wikipedia.java">完整源代码</a>）：</p><pre><code>Document doc = Jsoup.connect(&quot;https://en.wikipedia.org/&quot;).get();log(doc.title());Elements newsHeadlines = doc.select(&quot;#mp-itn b a&quot;);for (Element headline : newsHeadlines) {  log(&quot;%s\n\t%s&quot;,     headline.attr(&quot;title&quot;), headline.absUrl(&quot;href&quot;));}</code></pre><p><strong>开源的</strong></p><p>jsoup 是一个开放源代码项目，根据 <a href="https://jsoup.org/license">MIT的自由许可证</a>进行分发。源代码可从 <a href="https://github.com/jhy/jsoup/">GitHub</a> 获得。</p><h2 id="下载和安装-jsoup">下载和安装 jsoup</h2><p>jsoup 是可下载的 <strong>.jar</strong> Java 库。 当前发行版本是 <strong>1.13.1</strong>。</p><ul><li><a href="https://jsoup.org/packages/jsoup-1.13.1.jar">jsoup-1.13.1.jar</a> 核心类库</li><li><a href="https://jsoup.org/packages/jsoup-1.13.1-sources.jar">jsoup-1.13.1-sources.jar</a> 可选源代码 jar</li><li><a href="https://jsoup.org/packages/jsoup-1.13.1-javadoc.jar">jsoup-1.13.1-javadoc.jar</a> 可选javadoc jar 包</li></ul><p><strong>更新内容</strong></p><p>有关最新更改，请参见<a href="https://jsoup.org/news/release-1.13.1">1.13.1发行公告</a>；有关完整历史记录，请参见<a href="https://github.com/jhy/jsoup/blob/master/CHANGES">更改日志</a>。</p><p>使用<a href="https://github.com/jhy/jsoup/releases">早期版本</a>的 jsoup。</p><p><strong>Maven</strong></p><p>如果你使用Maven来管理 Java 项目的依赖关系，你无需下载；只需将以下内容放入POM的<dependencies>部分：</p><pre><code>&lt;dependency&gt;  &lt;!-- jsoup HTML parser library @ https://jsoup.org/ --&gt;  &lt;groupId&gt;org.jsoup&lt;/groupId&gt;  &lt;artifactId&gt;jsoup&lt;/artifactId&gt;  &lt;version&gt;1.13.1&lt;/version&gt;&lt;/dependency&gt;</code></pre><p><strong>Gradle</strong></p><pre><code>// jsoup HTML parser library @ https://jsoup.org/compile 'org.jsoup:jsoup:1.13.1'</code></pre><p><strong>源码构建</strong></p><p>如果你想尝试尚未发布的更改，或者想要进行自己的更改，则需要从源代码构建一个jar。 这很简单。 最好使用git，以便你可以保持最新状态，并能够将所做的更改反馈给你：</p><pre><code>git clone https://github.com/jhy/jsoup.gitcd jsoupmvn install</code></pre><p>这将运行单元测试和集成测试，并在通过后将快照 jar 安装到本地Maven存储库中。</p><p>如果您不想使用git，则可以下载一个zip文件：</p><pre><code>curl -Lo jsoup.zip https://github.com/jhy/jsoup/archive/master.zipunzip jsoup.zipcd jsoup-mastermvn install</code></pre><p><strong>依赖</strong></p><p>jsoup 完全是自包含的，没有依赖性。</p><p>jsoup 可在Java 7及更高版本，Scala，Kotlin，Android，OSGi，Lambda 和 Google App Engine 上运行。</p><h2 id="cookbook-内容">Cookbook 内容</h2><h3 id="简介">简介</h3><p><strong>解析 HTML 文档</strong></p><pre><code>String html = &quot;&lt;html&gt;&lt;head&gt;&lt;title&gt;First parse&lt;/title&gt;&lt;/head&gt;&quot;  + &quot;&lt;body&gt;&lt;p&gt;Parsed HTML into a doc.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&quot;;Document doc = Jsoup.parse(html);</code></pre><p>查看 从字符串解析文档 获取更多信息。</p><p>解析器将尽一切努力从您提供的 HTML 创建干净的解析，无论HTML是否格式正确。 它处理：</p><ul><li>未关闭的标签（例如 “&lt;p&gt; Lorem &lt;p&gt; Ipsum” 解析为 “&lt;p&amp;gt; Lorem &lt;/p&gt; &lt;p&gt; Ipsum &lt;/p&gt;”）</li><li>隐式标签（例如，将裸 “&lt;td&gt; Table数据&lt;/td&gt;” 包装到 “&lt;table&gt; &lt;tr&gt; &lt;td&gt; ...” 中）</li><li>可靠地创建文档结构（<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Document.OutputSettings.Syntax.html#html">html</a> 包含头部和主体，并且头部中仅包含适当的元素）</li></ul><p><strong>文档的对象模型</strong></p><ul><li>文档由 Element 和 TextNode（以及几个其他节点：请参见<a href="https://jsoup.org/apidocs/?org/jsoup/nodes/package-tree.html">节点包树</a>）组成。</li><li>继承链为：<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Document.html">Document</a> 继承 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html">Element</a> 继承 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Node.html">Node</a>。<a href="https://jsoup.org/apidocs/org/jsoup/nodes/TextNode.html">TextNode</a> 继承 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Node.html">Node</a>。</li><li>一个 Element 包含一个子节点列表，并具有一个父 Element。 它们还仅提供子 Element 的过滤列表。</li></ul><h3 id="输入">输入</h3><p><strong>从字符串解析文档</strong></p><p>静态 <a href="https://jsoup.org/apidocs/org/jsoup/Jsoup.html#parse(java.lang.String)">Jsoup.parse(String html)</a> 方法, 或如果页面来自网络并且你要获取绝对URL <a href="https://jsoup.org/apidocs/org/jsoup/Jsoup.html#parse(java.lang.String,java.lang.String)">Jsoup.parse(String html, String baseUri)</a> 。</p><pre><code>String html = &quot;&lt;html&gt;&lt;head&gt;&lt;title&gt;First parse&lt;/title&gt;&lt;/head&gt;&quot;  + &quot;&lt;body&gt;&lt;p&gt;Parsed HTML into a doc.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&quot;;Document doc = Jsoup.parse(html);</code></pre><p><strong>解析 body 片段</strong></p><p>使用 <a href="https://jsoup.org/apidocs/org/jsoup/Jsoup.html#parseBodyFragment(java.lang.String)">Jsoup.parseBodyFragment(String html)</a> 方法</p><pre><code>String html = &quot;&lt;div&gt;&lt;p&gt;Lorem ipsum.&lt;/p&gt;&quot;;Document doc = Jsoup.parseBodyFragment(html);Element body = doc.body();</code></pre><p><strong>从 URL 加载文档</strong></p><p>使用 <a href="https://jsoup.org/apidocs/org/jsoup/Jsoup.html#connect(java.lang.String)">Jsoup.connect(String url)</a> 方法。</p><pre><code>Document doc = Jsoup.connect(&quot;http://example.com/&quot;).get();String title = doc.title();</code></pre><p>设置其他参数：</p><pre><code>Document doc = Jsoup.connect(&quot;http://example.com&quot;)  .data(&quot;query&quot;, &quot;Java&quot;)  .userAgent(&quot;Mozilla&quot;)  .cookie(&quot;auth&quot;, &quot;token&quot;)  .timeout(3000)  .post();</code></pre><p><strong>从文件加载文档</strong></p><p>使用静态方法 <a href="https://jsoup.org/apidocs/org/jsoup/Jsoup.html#parse(java.io.File,java.lang.String,java.lang.String)">Jsoup.parse(File in, String charsetName, String baseUri)</a>。</p><pre><code>File input = new File(&quot;/tmp/input.html&quot;);Document doc = Jsoup.parse(input, &quot;UTF-8&quot;, &quot;http://example.com/&quot;);</code></pre><h3 id="提取数据">提取数据</h3><p><strong>使用DOM方法浏览文档</strong></p><p>将HTML解析为 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Document.html">Document</a> 后，请使用类似DOM的方法。</p><pre><code>File input = new File(&quot;/tmp/input.html&quot;);Document doc = Jsoup.parse(input, &quot;UTF-8&quot;, &quot;http://example.com/&quot;);Element content = doc.getElementById(&quot;content&quot;);Elements links = content.getElementsByTag(&quot;a&quot;);for (Element link : links) {  String linkHref = link.attr(&quot;href&quot;);  String linkText = link.text();}</code></pre><p>查找 Element</p><ul><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#getElementById(java.lang.String)">getElementById(String id)</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#getElementsByTag(java.lang.String)">getElementsByTag(String tag)</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#getElementsByClass(java.lang.String)">getElementsByClass(String className)</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#getElementsByAttribute(java.lang.String)">getElementsByAttribute(String key)</a> （和关联方法）</li><li>同级 Element : <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#siblingElements()">siblingElements()</a>，<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#firstElementSibling()">firstElementSibling()</a>，<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#lastElementSibling()">lastElementSibling()</a>；<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#nextElementSibling()">nextElementSibling()</a>，<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#previousElementSibling()">previousElementSibling()</a></li><li>图: <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Node.html#parent()">parent()</a>，<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#children()">children()</a>，<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#child(int)">child(int index)</a></li></ul><p>处理 HTML 和 文本</p><ul><li><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#attr(java.lang.String)">attr(String key)</a> 获取 和 <a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#attr(java.lang.String,java.lang.String)">attr(String key, String value)</a> 设置属性</li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Node.html#attributes()">attributes()</a> 获取所有属性</li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#id()">id()</a>, <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#className()">className()</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#classNames()">classNames()</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#text()">text()</a> 获取 和 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/TextNode.html#text(java.lang.String)">text(String value)</a> 设置文本内容</li><li><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#html()">html()</a> 获取 <a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#html(java.lang.String)">html(String value)</a> 设置内部 HTML 内容</li><li><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#outerHtml()">outerHtml()</a> 获取外部 HTML 值</li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#data()">data()</a> 获取数据内容 (如. tag 的脚本和样式)</li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#tag()">tag()</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#tagName()">tagName()</a></li></ul><p>Element 数据</p><ul><li><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#append(java.lang.String)">append(String html)</a>，<a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#prepend(java.lang.String)">prepend(String html)</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#appendText(java.lang.String)">appendText(String text)</a>，<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#prependText(java.lang.String)">prependText(String text)</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#appendElement(java.lang.String)">appendElement(String tagName)</a>，<a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#prependElement(java.lang.String)">prependElement(String tagName)</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#html(java.lang.String)">html(String value)</a></li></ul><p><strong>使用选择器语法查找元素</strong></p><p>使用 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#select(java.lang.String)">Element.select(String selector)</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#select(java.lang.String)">Elements.select(String selector)</a> 方法:</p><pre><code>File input = new File(&quot;/tmp/input.html&quot;);Document doc = Jsoup.parse(input, &quot;UTF-8&quot;, &quot;http://example.com/&quot;);Elements links = doc.select(&quot;a[href]&quot;); // a with hrefElements pngs = doc.select(&quot;img[src$=.png]&quot;);  // img with src ending .pngElement masthead = doc.select(&quot;div.masthead&quot;).first();  // div with class=mastheadElements resultLinks = doc.select(&quot;h3.r &gt; a&quot;); // direct a after h3</code></pre><p>jsoup 元素支持类似选择器语法的 <a href="https://www.w3.org/TR/2009/PR-css3-selectors-20091215/">CSS</a>（或<a href="https://jquery.com/">jquery</a>）来查找匹配的元素，从而允许非常强大和健壮的查询。</p><p>选择器概述</p><ul><li><strong>tagname</strong>：通过 tag 查找元素，如：&quot;a&quot;</li><li><strong>ns|tag</strong>：通过 tag 在命名空间内查找元素，如：&quot;fb|name&quot; 查找 &quot;<a href="fb:name">fb:name</a>&quot; 元素</li><li><strong>#id</strong>：通过 ID 查找元素，如：&quot;#logo&quot;</li><li><strong>.class</strong>：通过 class 名称查找元素，如：&quot;.masthead&quot;</li><li><strong>[attribute]</strong>：带属性元素，如：&quot;[href]&quot;</li><li><strong>[^attr]</strong>：带属性名前缀的元素，如：&quot;[^data-]&quot; 查找带 HTML5 数据集属性的元素</li><li><strong>[attr=value]</strong>：带属性值的元素，如：&quot;[width=500]&quot;（也可以引用，如：&quot;[data-name='launch sequence']&quot;）</li><li><strong>[attr^=value]</strong>，<strong>[attr$=value]</strong>，<strong>[attr*=value]</strong>：带属性值满足开始于、结束于或包含的元素，如：&quot;[href*=/path/]&quot;</li><li><strong>[attr~=regex]</strong>：带属性值满足正则表达式的元素，如：&quot;img[src~=(?i).(png|jpe?g)]&quot;</li></ul><p>选择器组合</p><ul><li><strong>el#id</strong>：带 ID 的元素，如：&quot;div#logo&quot;</li><li><strong>el.class</strong>：带 class 的元素，如：&quot;div.masthead&quot;</li><li><strong>el[attr]</strong>：带属性的元素，如：&quot;a[href]&quot;</li><li>任何组合，如 &quot;a[href].highlight&quot;</li><li>祖先-子：指定祖先查找子元素，如：&quot;.body p&quot;，在 class 为 &quot;body&quot; 的块下的任意位置查找tag 为 p 的元素</li><li><strong>parent&gt; child</strong>：父元素的直接子元素，如：&quot;div.content&gt; p&quot; 查找 &quot;div.content&quot; 为 p 直接子元素； &quot;body &gt; *&quot; 查找 &quot;body&quot; 下所有直接子元素</li><li><strong>siblingA + siblingB</strong>：查找紧随兄弟 A 的兄弟 B 元素，如：&quot;div.head + div&quot;</li><li><strong>siblingA〜siblingX</strong>：查找在兄弟 A 之前的兄弟 X 元素，如：&quot;h1 ~ p&quot;</li><li><strong>el, el, el</strong>：将多个选择器组合在一起，找到与任何选择器匹配的唯一元素； 如：&quot;div.masthead，div.logo&quot;</li></ul><p>伪选择器</p><ul><li><strong>:lt(n)</strong>：查找其兄弟索引（即其在DOM树中相对于其父节点的位置）小于n的元素；如 &quot;td:lt(3)&quot;</li><li><strong>:gt(n)</strong>：查找兄弟索引大于n的元素；如：&quot;div p:gt(2)&quot;</li><li><strong>:eq(n)</strong>：查找同级索引等于 n 的元素；如：&quot;form input:eq(1)&quot;</li><li><strong>:has(selector)</strong>：查找包含与选择器匹配的元素的元素；如：&quot;div:has(p)&quot;</li><li><strong>:not(selector)</strong>：查找与选择器不匹配的元素；如：&quot;div:not(.logo)&quot;</li><li><strong>:contains(text)</strong>：查找包含给定文本的元素。 搜索不区分大小写；如：&quot;p:contains(jsoup)&quot;</li><li><strong>:containsOwn(text)</strong>：查找直接包含给定文本的元素</li><li><strong>:matches(regex)</strong>：查找文本与指定正则表达式匹配的元素；如：&quot;div:matches((?i)login)&quot;</li><li><strong>:matchesOwn(regex)</strong>：查找其文本与指定的正则表达式匹配的元素</li><li><strong>请注意</strong>，上面索引的伪选择器基于0，即第一个元素位于索引0，第二个元素位于1，依此类推。</li></ul><p>有关完整的支持列表和详细信息，请参见 <a href="https://jsoup.org/apidocs/org/jsoup/select/Selector.html">Selector</a> API参考。</p><p><strong>从元素中提取属性、文本和 HTML</strong></p><ul><li>若要获取属性的值，请使用 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Node.html#attr(java.lang.String)">Node.attr(String key)</a> 方法</li><li>对于元素（及其组合的子元素）上的文本，请使用 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#text()">Element.text()</a></li><li>对于HTML，请根据需要使用 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#html()">Element.html()</a> 或 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Node.html#outerHtml()">Node.outerHtml()</a></li></ul><pre><code>String html = &quot;&lt;p&gt;An &lt;a href='http://example.com/'&gt;&lt;b&gt;example&lt;/b&gt;&lt;/a&gt; link.&lt;/p&gt;&quot;;Document doc = Jsoup.parse(html);Element link = doc.select(&quot;a&quot;).first();String text = doc.body().text(); // &quot;An example link&quot;String linkHref = link.attr(&quot;href&quot;); // &quot;http://example.com/&quot;String linkText = link.text(); // &quot;example&quot;&quot;String linkOuterH = link.outerHtml();     // &quot;&lt;a href=&quot;http://example.com&quot;&gt;&lt;b&gt;example&lt;/b&gt;&lt;/a&gt;&quot;String linkInnerH = link.html(); // &quot;&lt;b&gt;example&lt;/b&gt;&quot;</code></pre><p>其他方法</p><ul><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#id()">Element.id()</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#tagName()">Element.tagName()</a></li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#className()">Element.className()</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#hasClass(java.lang.String)">Element.hasClass(String className)</a></li></ul><p><strong>使用 URL</strong></p><ol><li>确保在解析文档时指定基本URI（从URL加载时是隐式的）</li><li>使用 <strong>abs:</strong> 属性前缀可从属性解析绝对URL</li></ol><pre><code>Document doc = Jsoup.connect(&quot;http://jsoup.org&quot;).get();Element link = doc.select(&quot;a&quot;).first();String relHref = link.attr(&quot;href&quot;);  // == &quot;/&quot;String absHref = link.attr(&quot;abs:href&quot;);  // &quot;http://jsoup.org/&quot;</code></pre><h3 id="提取数据示例--列举链接">提取数据示例 —— 列举链接</h3><p>该示例程序演示了如何从URL提取页面。提取链接、图像和其他指向；并检查其 URL 和文本。</p><p>指定要获取的URL作为程序的唯一参数。</p><pre><code>package org.jsoup.examples;import org.jsoup.Jsoup;import org.jsoup.helper.Validate;import org.jsoup.nodes.Document;import org.jsoup.nodes.Element;import org.jsoup.select.Elements;import java.io.IOException;/** * Example program to list links from a URL. */public class ListLinks {    public static void main(String[] args) throws IOException {        Validate.isTrue(args.length == 1, &quot;usage: supply url to fetch&quot;);        String url = args[0];        print(&quot;Fetching %s...&quot;, url);        Document doc = Jsoup.connect(url).get();        Elements links = doc.select(&quot;a[href]&quot;);        Elements media = doc.select(&quot;[src]&quot;);        Elements imports = doc.select(&quot;link[href]&quot;);        print(&quot;\nMedia: (%d)&quot;, media.size());        for (Element src : media) {            if (src.normalName().equals(&quot;img&quot;))                print(&quot; * %s: &lt;%s&gt; %sx%s (%s)&quot;,                        src.tagName(), src.attr(&quot;abs:src&quot;), src.attr(&quot;width&quot;), src.attr(&quot;height&quot;),                        trim(src.attr(&quot;alt&quot;), 20));            else                print(&quot; * %s: &lt;%s&gt;&quot;, src.tagName(), src.attr(&quot;abs:src&quot;));        }        print(&quot;\nImports: (%d)&quot;, imports.size());        for (Element link : imports) {            print(&quot; * %s &lt;%s&gt; (%s)&quot;, link.tagName(),link.attr(&quot;abs:href&quot;), link.attr(&quot;rel&quot;));        }        print(&quot;\nLinks: (%d)&quot;, links.size());        for (Element link : links) {            print(&quot; * a: &lt;%s&gt;  (%s)&quot;, link.attr(&quot;abs:href&quot;), trim(link.text(), 35));        }    }    private static void print(String msg, Object... args) {        System.out.println(String.format(msg, args));    }    private static String trim(String s, int width) {        if (s.length() &gt; width)            return s.substring(0, width-1) + &quot;.&quot;;        else            return s;    }}</code></pre><p>示例输入（已修剪）</p><pre><code>Fetching http://news.ycombinator.com/...Media: (38) * img: &lt;http://ycombinator.com/images/y18.gif&gt; 18x18 () * img: &lt;http://ycombinator.com/images/s.gif&gt; 10x1 () * img: &lt;http://ycombinator.com/images/grayarrow.gif&gt; x () * img: &lt;http://ycombinator.com/images/s.gif&gt; 0x10 () * script: &lt;http://www.co2stats.com/propres.php?s=1138&gt; * img: &lt;http://ycombinator.com/images/s.gif&gt; 15x1 () * img: &lt;http://ycombinator.com/images/hnsearch.png&gt; x () * img: &lt;http://ycombinator.com/images/s.gif&gt; 25x1 () * img: &lt;http://mixpanel.com/site_media/images/mixpanel_partner_logo_borderless.gif&gt; x (Analytics by Mixpan.) Imports: (2) * link &lt;http://ycombinator.com/news.css&gt; (stylesheet) * link &lt;http://ycombinator.com/favicon.ico&gt; (shortcut icon) Links: (141) * a: &lt;http://ycombinator.com&gt;  () * a: &lt;http://news.ycombinator.com/news&gt;  (Hacker News) * a: &lt;http://news.ycombinator.com/newest&gt;  (new) * a: &lt;http://news.ycombinator.com/newcomments&gt;  (comments) * a: &lt;http://news.ycombinator.com/leaders&gt;  (leaders) * a: &lt;http://news.ycombinator.com/jobs&gt;  (jobs) * a: &lt;http://news.ycombinator.com/submit&gt;  (submit) * a: &lt;http://news.ycombinator.com/x?fnid=JKhQjfU7gW&gt;  (login) * a: &lt;http://news.ycombinator.com/vote?for=1094578&amp;dir=up&amp;whence=%6e%65%77%73&gt;  () * a: &lt;http://www.readwriteweb.com/archives/facebook_gets_faster_debuts_homegrown_php_compiler.php?utm_source=feedburner&amp;utm_medium=feed&amp;utm_campaign=Feed%3A+readwriteweb+%28ReadWriteWeb%29&amp;utm_content=Twitter&gt;  (Facebook speeds up PHP) * a: &lt;http://news.ycombinator.com/user?id=mcxx&gt;  (mcxx) * a: &lt;http://news.ycombinator.com/item?id=1094578&gt;  (9 comments) * a: &lt;http://news.ycombinator.com/vote?for=1094649&amp;dir=up&amp;whence=%6e%65%77%73&gt;  () * a: &lt;http://groups.google.com/group/django-developers/msg/a65fbbc8effcd914&gt;  (&quot;Tough. Django produces XHTML.&quot;) * a: &lt;http://news.ycombinator.com/user?id=andybak&gt;  (andybak) * a: &lt;http://news.ycombinator.com/item?id=1094649&gt;  (3 comments) * a: &lt;http://news.ycombinator.com/vote?for=1093927&amp;dir=up&amp;whence=%6e%65%77%73&gt;  () * a: &lt;http://news.ycombinator.com/x?fnid=p2sdPLE7Ce&gt;  (More) * a: &lt;http://news.ycombinator.com/lists&gt;  (Lists) * a: &lt;http://news.ycombinator.com/rss&gt;  (RSS) * a: &lt;http://ycombinator.com/bookmarklet.html&gt;  (Bookmarklet) * a: &lt;http://ycombinator.com/newsguidelines.html&gt;  (Guidelines) * a: &lt;http://ycombinator.com/newsfaq.html&gt;  (FAQ) * a: &lt;http://ycombinator.com/newsnews.html&gt;  (News News) * a: &lt;http://news.ycombinator.com/item?id=363&gt;  (Feature Requests) * a: &lt;http://ycombinator.com&gt;  (Y Combinator) * a: &lt;http://ycombinator.com/w2010.html&gt;  (Apply) * a: &lt;http://ycombinator.com/lib.html&gt;  (Library) * a: &lt;http://www.webmynd.com/html/hackernews.html&gt;  () * a: &lt;http://mixpanel.com/?from=yc&gt;  ()</code></pre><h3 id="修改数据">修改数据</h3><p><strong>设置属性数据</strong></p><ul><li><p>使用属性设置器方法 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#attr(java.lang.String,java.lang.String)">Element.attr(String key, String value)</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html#attr(java.lang.String,java.lang.String)">Elements.attr(String key, String value)</a>。</p></li><li><p>如果需要修改元素的 class 属性，请使用 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#addClass(java.lang.String)">Element.addClass(String className)</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#removeClass(java.lang.String)">Element.removeClass(String className)</a> 方法。</p></li><li><p><a href="https://jsoup.org/apidocs/org/jsoup/select/Elements.html">Elements</a> <a href="https://wiki.ibit.tech/d/集合">集合</a>具有批量属性和类方法。 如：要向 div 中的每个 a 元素添加 rel=&quot;nofollow&quot; 属性：</p></li></ul><pre><code>doc.select(&quot;div.comments a&quot;).attr(&quot;rel&quot;, &quot;nofollow&quot;);</code></pre><p><strong>设置元素的 HTML</strong></p><p>使用 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html">Element</a> HTML 设置方法：</p><ul><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#html(java.lang.String)">Element.html(String html)</a> 清除元素已存在的内部 HTML，并将其替换为已解析的 HTML。</li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#prepend(java.lang.String)">Element.prepend(String first)</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#append(java.lang.String)">Element.append(String last)</a> 往元素的内部 HTML 开始或结束位置增加 HTML。</li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#wrap(java.lang.String)">Element.wrap(String around)</a> 将HTML包装在元素的外部HTML周围。</li></ul><pre><code>Element div = doc.select(&quot;div&quot;).first(); // &lt;div&gt;&lt;/div&gt;div.html(&quot;&lt;p&gt;lorem ipsum&lt;/p&gt;&quot;); // &lt;div&gt;&lt;p&gt;lorem ipsum&lt;/p&gt;&lt;/div&gt;div.prepend(&quot;&lt;p&gt;First&lt;/p&gt;&quot;);div.append(&quot;&lt;p&gt;Last&lt;/p&gt;&quot;);// now: &lt;div&gt;&lt;p&gt;First&lt;/p&gt;&lt;p&gt;lorem ipsum&lt;/p&gt;&lt;p&gt;Last&lt;/p&gt;&lt;/div&gt;Element span = doc.select(&quot;span&quot;).first(); // &lt;span&gt;One&lt;/span&gt;span.wrap(&quot;&lt;li&gt;&lt;a href='http://example.com/'&gt;&lt;/a&gt;&lt;/li&gt;&quot;);// now: &lt;li&gt;&lt;a href=&quot;http://example.com&quot;&gt;&lt;span&gt;One&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;</code></pre><p><strong>设置元素的文本内容</strong></p><p>使用 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html">Element</a> 文本设置方法</p><ul><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#text(java.lang.String)">Element.text(String text)</a> 清除元素中所有已存在的内部 HTML，并将其替换为提供的文本。</li><li><a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#prepend(java.lang.String)">Element.prepend(String first)</a> 和 <a href="https://jsoup.org/apidocs/org/jsoup/nodes/Element.html#append(java.lang.String)">Element.append(String last)</a> 往元素的内部 HTML 开始或结束位置增加文本节点。</li></ul><pre><code>Element div = doc.select(&quot;div&quot;).first(); // &lt;div&gt;&lt;/div&gt;div.text(&quot;five &gt; four&quot;); // &lt;div&gt;five &amp;gt; four&lt;/div&gt;div.prepend(&quot;First &quot;);div.append(&quot; Last&quot;);// now: &lt;div&gt;First five &amp;gt; four Last&lt;/div&gt;</code></pre><h3 id="清除-html">清除 HTML</h3><p>将 jsoup HTML <a href="https://jsoup.org/apidocs/org/jsoup/safety/Cleaner.html">Cleaner</a> 与 <a href="https://jsoup.org/apidocs/org/jsoup/safety/Whitelist.html">Whitelist</a> 指定的配置一起使用。</p><pre><code class="language-java">String unsafe =   &quot;&lt;p&gt;&lt;a href='http://example.com/' onclick='stealCookies()'&gt;Link&lt;/a&gt;&lt;/p&gt;&quot;;String safe = Jsoup.clean(unsafe, Whitelist.basic());// now: &lt;p&gt;&lt;a href=&quot;http://example.com/&quot; rel=&quot;nofollow&quot;&gt;Link&lt;/a&gt;&lt;/p&gt;</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[关于HttpClient上传中文乱码的解决办法]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/httpclient-upload-chinese-name-problem" />
                <id>tag:https://ibit.tech,2021-11-03:httpclient-upload-chinese-name-problem</id>
                <published>2021-11-03T23:43:22+08:00</published>
                <updated>2021-11-03T23:43:22+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>使用过HttpClient的人都知道可以通过addTextBody方法来添加要上传的文本信息，但是，如果要上传中文的话，或还有中文名称的文件会出现乱码的问题，解决办法其实很简单：</p><p>**第一步：**设置MultipartEntityBuilder的编码方式为UTF-8。</p><pre><code class="language-java">//设置请求的编码格式builder.setCharset(Charset.forName(HTTP.UTF_8));</code></pre><p>**第二步：**创建ContentType对象，指定UTF-8编码。</p><pre><code>ContentType contentType= ContentType.create(HTTP.PLAIN_TEXT_TYPE, HTTP.UTF_8); </code></pre><p>**第三步：**使用 addPart + StringBody 代替 addTextBody。如：</p><pre><code>StringBody stringBody=new StringBody(&quot;中文乱码&quot;, contentType);builder.addPart(&quot;test&quot;,stringBody);</code></pre><p><strong>附上完整代码：</strong></p><pre><code>ContentType contentType = ContentType.create(HTTP.PLAIN_TEXT_TYPE, HTTP.UTF_8);// 开启一个客户端 HTTP 请求 HttpClient client=new DefaultHttpClient();//创建 HTTP POST 请求HttpPost post = new HttpPost(url);MultipartEntityBuilder builder = MultipartEntityBuilder.create();//设置请求的编码格式builder.setCharset(Charset.forName(HTTP.UTF_8));//设置浏览器兼容模式builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);int count=0;for (File file : files) {builder.addBinaryBody(&quot;file&quot; + count, file);count++;}//设置请求参数builder.addTextBody(&quot;method&quot;, params.get(&quot;method&quot;));//设置请求参数builder.addTextBody(&quot;fileTypes&quot;, params.get(&quot;fileTypes&quot;));StringBody stringBody=new StringBody(&quot;中文乱码&quot;,contentType);builder.addPart(&quot;test&quot;, stringBody);// 生成 HTTP POST 实体 HttpEntity entity = builder.build(); //设置请求参数post.setEntity(entity);// 发起请求 并返回请求的响应HttpResponse response = client.execute(post);if (response.getStatusLine().getStatusCode()==200) {return true;}return false;</code></pre><p>**版权申明：**本文转载于【CSDN博主「CrazyCodeBoy」】<a href="https://blog.csdn.net/fengyuzhengfan/article/details/40792529">关于HttpClient上传中文乱码的解决办法</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[linux awk命令详解]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/linux-awk-command-usage" />
                <id>tag:https://ibit.tech,2021-11-02:linux-awk-command-usage</id>
                <published>2021-11-02T23:18:19+08:00</published>
                <updated>2021-11-03T00:18:52+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="简介"><strong>简介</strong></h2><p>awk是一个强大的文本分析工具，相对于grep的查找，sed的编辑，awk在其对数据分析并生成报告时，显得尤为强大。简单来说awk就是把文件逐行的读入，以空格为默认分隔符将每行切片，切开的部分再进行各种分析处理。</p><p>awk有3个不同版本: awk、nawk和gawk，未作特别说明，一般指gawk，gawk 是 AWK 的 GNU 版本。</p><p>awk其名称得自于它的创始人 Alfred Aho 、Peter Weinberger 和 Brian Kernighan 姓氏的首个字母。实际上 AWK 的确拥有自己的语言： AWK 程序设计语言 ， 三位创建者已将它正式定义为“样式扫描和处理语言”。它允许您创建简短的程序，这些程序读取输入文件、为数据排序、处理数据、对输入执行计算以及生成报表，还有无数其他的功能。</p><h2 id="使用方法"><strong>使用方法</strong></h2><pre><code>awk '{pattern + action}' {filenames}</code></pre><p>尽管操作可能会很复杂，但语法总是这样，其中 pattern 表示 AWK 在数据中查找的内容，而 action 是在找到匹配内容时所执行的一系列命令。花括号（{}）不需要在程序中始终出现，但它们用于根据特定的模式对一系列指令进行分组。 pattern就是要表示的正则表达式，用斜杠括起来。</p><p>awk语言的最基本功能是在文件或者字符串中基于指定规则浏览和抽取信息，awk抽取信息后，才能进行其他文本操作。完整的awk脚本通常用来格式化文本文件中的信息。</p><p>通常，awk是以文件的一行为处理单位的。awk每接收文件的一行，然后执行相应的命令，来处理文本。</p><h2 id="调用awk"><strong>调用awk</strong></h2><p>有三种方式调用awk</p><p><strong>命令行方式</strong></p><pre><code>awk [-F  field-separator]  'commands'  input-file(s)其中，commands 是真正awk命令，[-F域分隔符]是可选的。 input-file(s) 是待处理的文件。在awk中，文件的每一行中，由域分隔符分开的每一项称为一个域。通常，在不指名-F域分隔符的情况下，默认的域分隔符是空格。</code></pre><p><strong>shell脚本方式</strong></p><pre><code>将所有的awk命令插入一个文件，并使awk程序可执行，然后awk命令解释器作为脚本的首行，一遍通过键入脚本名称来调用。相当于shell脚本首行的：#!/bin/sh可以换成：#!/bin/awk</code></pre><p><strong>将所有的awk命令插入一个单独文件，然后调用：</strong></p><pre><code>awk -f awk-script-file input-file(s)其中，-f选项加载awk-script-file中的awk脚本，input-file(s)跟上面的是一样的。</code></pre><p>本章重点介绍命令行方式。</p><h2 id="入门实例"><strong>入门实例</strong></h2><p>假设last -n 5的输出如下</p><pre><code>[root@www ~]# last -n 5 &lt;==仅取出前五行root     pts/1   192.168.1.100  Tue Feb 10 11:21   still logged inroot     pts/1   192.168.1.100  Tue Feb 10 00:46 - 02:28  (01:41)root     pts/1   192.168.1.100  Mon Feb  9 11:41 - 18:30  (06:48)dmtsai   pts/1   192.168.1.100  Mon Feb  9 11:41 - 11:41  (00:00)root     tty1                   Fri Sep  5 14:09 - 14:10  (00:01)</code></pre><p>如果只是显示最近登录的5个帐号</p><pre><code>#last -n 5 | awk  '{print $1}'rootrootrootdmtsairoot</code></pre><p>awk工作流程是这样的：读入有'\n'换行符分割的一条记录，然后将记录按指定的域分隔符划分域，填充域，$0则表示所有域,$1表示第一个域,$n表示第n个域。默认域分隔符是&quot;空白键&quot; 或 &quot;[tab]键&quot;,所以$1表示登录用户，$3表示登录用户ip,以此类推。</p><p>如果只是显示/etc/passwd的账户</p><pre><code>#cat /etc/passwd |awk  -F ':'  '{print $1}'  rootdaemonbinsys</code></pre><p>这种是awk+action的示例，每行都会执行action{print $1}。</p><p>-F指定域分隔符为':'。</p><p>如果只是显示/etc/passwd的账户和账户对应的shell,而账户与shell之间以tab键分割</p><pre><code>#cat /etc/passwd |awk  -F ':'  '{print $1&quot;\t&quot;$7}'root    /bin/bashdaemon  /bin/shbin     /bin/shsys     /bin/sh</code></pre><p>如果只是显示/etc/passwd的账户和账户对应的shell,而账户与shell之间以逗号分割,而且在所有行添加列名name,shell,在最后一行添加&quot;blue,/bin/nosh&quot;。</p><pre><code>cat /etc/passwd |awk  -F ':'  'BEGIN {print &quot;name,shell&quot;}  {print $1&quot;,&quot;$7} END {print &quot;blue,/bin/nosh&quot;}'name,shellroot,/bin/bashdaemon,/bin/shbin,/bin/shsys,/bin/sh....blue,/bin/nosh</code></pre><p>awk工作流程是这样的：先执行BEGING，然后读取文件，读入有/n换行符分割的一条记录，然后将记录按指定的域分隔符划分域，填充域，$0则表示所有域,$1表示第一个域,$n表示第n个域,随后开始执行模式所对应的动作action。接着开始读入第二条记录······直到所有的记录都读完，最后执行END操作。</p><p>搜索/etc/passwd有root关键字的所有行</p><pre><code>#awk -F: '/root/' /etc/passwdroot:x:0:0:root:/root:/bin/bash</code></pre><p>这种是pattern的使用示例，匹配了pattern(这里是root)的行才会执行action(没有指定action，默认输出每行的内容)。</p><p>搜索支持正则，例如找root开头的: awk -F: '/^root/' /etc/passwd</p><p>搜索/etc/passwd有root关键字的所有行，并显示对应的shell</p><pre><code># awk -F: '/root/{print $7}' /etc/passwd             /bin/bash这里指定了action{print $7}</code></pre><h2 id="awk内置变量"><strong>awk内置变量</strong></h2><p>awk有许多内置变量用来设置环境信息，这些变量可以被改变，下面给出了最常用的一些变量。</p><pre><code>ARGC               命令行参数个数ARGV               命令行参数排列ENVIRON            支持队列中系统环境变量的使用FILENAME           awk浏览的文件名FNR                浏览文件的记录数FS                 设置输入域分隔符，等价于命令行 -F选项NF                 浏览记录的域的个数NR                 已读的记录数OFS                输出域分隔符ORS                输出记录分隔符RS                 控制记录分隔符</code></pre><p>此外,$0变量是指整条记录。$1表示当前行的第一个域,$2表示当前行的第二个域,......以此类推。</p><p>统计/etc/passwd:文件名，每行的行号，每行的列数，对应的完整行内容:</p><pre><code>#awk  -F ':'  '{print &quot;filename:&quot; FILENAME &quot;,linenumber:&quot; NR &quot;,columns:&quot; NF &quot;,linecontent:&quot;$0}' /etc/passwdfilename:/etc/passwd,linenumber:1,columns:7,linecontent:root:x:0:0:root:/root:/bin/bashfilename:/etc/passwd,linenumber:2,columns:7,linecontent:daemon:x:1:1:daemon:/usr/sbin:/bin/shfilename:/etc/passwd,linenumber:3,columns:7,linecontent:bin:x:2:2:bin:/bin:/bin/shfilename:/etc/passwd,linenumber:4,columns:7,linecontent:sys:x:3:3:sys:/dev:/bin/sh</code></pre><p>使用printf替代print,可以让代码更加简洁，易读</p><pre><code> awk  -F ':'  '{printf(&quot;filename:%10s,linenumber:%s,columns:%s,linecontent:%s\n&quot;,FILENAME,NR,NF,$0)}' /etc/passwd</code></pre><h2 id="print和printf"><strong>print和printf</strong></h2><p>awk中同时提供了print和printf两种打印输出的函数。</p><p>其中print函数的参数可以是变量、数值或者字符串。字符串必须用双引号引用，参数用逗号分隔。如果没有逗号，参数就串联在一起而无法区分。这里，逗号的作用与输出文件的分隔符的作用是一样的，只是后者是空格而已。</p><p>printf函数，其用法和c语言中printf基本相似,可以格式化字符串,输出复杂时，printf更加好用，代码更易懂。</p><h2 id="awk编程"><strong>awk编程</strong></h2><p><strong>变量和赋值</strong></p><p>除了awk的内置变量，awk还可以自定义变量。</p><p>下面统计/etc/passwd的账户人数</p><pre><code>awk '{count++;print $0;} END{print &quot;user count is &quot;, count}' /etc/passwdroot:x:0:0:root:/root:/bin/bash......user count is  40</code></pre><p>count是自定义变量。之前的action{}里都是只有一个print,其实print只是一个语句，而action{}可以有多个语句，以;号隔开。</p><p>这里没有初始化count，虽然默认是0，但是妥当的做法还是初始化为0:</p><pre><code>awk 'BEGIN {count=0;print &quot;[start]user count is &quot;, count} {count=count+1;print $0;} END{print &quot;[end]user count is &quot;, count}' /etc/passwd[start]user count is  0root:x:0:0:root:/root:/bin/bash...[end]user count is  40</code></pre><p>统计某个文件夹下的文件占用的字节数</p><pre><code>ls -l |awk 'BEGIN {size=0;} {size=size+$5;} END{print &quot;[end]size is &quot;, size}'[end]size is  8657198</code></pre><p>如果以M为单位显示:</p><pre><code>ls -l |awk 'BEGIN {size=0;} {size=size+$5;} END{print &quot;[end]size is &quot;, size/1024/1024,&quot;M&quot;}' [end]size is  8.25889 M</code></pre><p>注意，统计不包括文件夹的子目录。</p><p><strong>条件语句</strong></p><p>awk中的条件语句是从C语言中借鉴来的，见如下声明方式：</p><pre><code>if (expression) {    statement;    statement;    ... ...}if (expression) {    statement;} else {    statement2;}if (expression) {    statement1;} else if (expression1) {    statement2;} else {    statement3;}</code></pre><p>统计某个文件夹下的文件占用的字节数,过滤4096大小的文件(一般都是文件夹):</p><pre><code>ls -l |awk 'BEGIN {size=0;print &quot;[start]size is &quot;, size} {if($5!=4096){size=size+$5;}} END{print &quot;[end]size is &quot;, size/1024/1024,&quot;M&quot;}' [end]size is  8.22339 M</code></pre><p><strong>循环语句</strong></p><p>awk中的循环语句同样借鉴于C语言，支持while、do/while、for、break、continue，这些关键字的语义和C语言中的语义完全相同。</p><p><strong>数组</strong></p><p>因为awk中数组的下标可以是数字和字母，数组的下标通常被称为关键字(key)。值和关键字都存储在内部的一张针对key/value应用hash的表格里。由于hash不是顺序存储，因此在显示数组内容时会发现，它们并不是按照你预料的顺序显示出来的。数组和变量一样，都是在使用时自动创建的，awk也同样会自动判断其存储的是数字还是字符串。一般而言，awk中的数组用来从记录中收集信息，可以用于计算总和、统计单词以及跟踪模板被匹配的次数等等。</p><p>显示/etc/passwd的账户</p><pre><code>awk -F ':' 'BEGIN {count=0;} {name[count] = $1;count++;}; END{for (i = 0; i &lt; NR; i++) print i, name[i]}' /etc/passwd0 root1 daemon2 bin3 sys4 sync5 games......</code></pre><p>这里使用for循环遍历数组</p><p>awk编程的内容极多，这里只罗列简单常用的用法，更多请参考 <a href="http://www.gnu.org/software/gawk/manual/gawk.html">http://www.gnu.org/software/gawk/manual/gawk.html</a></p><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【ggjucheng 博客园】<a href="https://www.cnblogs.com/ggjucheng/archive/2013/01/13/2858470.html">linux awk命令详解</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringBoot + MyBatis使用多数据源]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/springboot-mybatis-multi-datasource" />
                <id>tag:https://ibit.tech,2021-11-01:springboot-mybatis-multi-datasource</id>
                <published>2021-11-01T22:15:03+08:00</published>
                <updated>2021-11-01T22:19:00+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>有时候我们需要在一个项目里面集成一个或者多个数据源。</p><h3 id="实现的思想">实现的思想</h3><ul><li>使用mybatis持久层框架</li><li>mybatis的运行需要依赖于几个组件<ul><li><code>DataSource</code> 数据源</li><li><code>DataSourceTransactionManager</code> 事务管理器</li><li><code>SqlSessionFactory</code> SqlSession工厂，负责创建SqlSession</li><li><code>SqlSessionTemplate</code> SqlSession，负责执行crud</li></ul></li><li>多数据源，就是使用多个数据源，多个事务管理器，多个SqlSession工厂，就有不同的SqlSession</li></ul><h3 id="maven以来">Maven以来</h3><blockquote><p>使用Druid作为数据源</p></blockquote><pre><code class="language-xml">&lt;dependencies&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;mysql&lt;/groupId&gt;&lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;&lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;&lt;version&gt;2.0.0&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;com.alibaba&lt;/groupId&gt;&lt;artifactId&gt;druid&lt;/artifactId&gt;&lt;version&gt;1.1.14&lt;/version&gt;&lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><h3 id="applicationyml">application.yml</h3><blockquote><p>配置多个数据源，这里仅仅配置了基本必须的属性</p></blockquote><pre><code>logging:  level:    root: debugdatasource:  test1:    driver-class-name: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/springboot1?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;allowMultiQueries=true&amp;serverTimezone=GMT%2b8    username: root    password: root    test2:    driver-class-name: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/springboot2?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;allowMultiQueries=true&amp;serverTimezone=GMT%2b8    username: root    password: root</code></pre><h3 id="多个数据源的configuration-配置">多个数据源的@Configuration 配置</h3><h4 id="第一个数据源以及持久层需要的组件">第一个数据源以及持久层需要的组件</h4><pre><code class="language-java">package io.springboot.multidatasource.configuration;import javax.sql.DataSource;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.SqlSessionTemplate;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import com.alibaba.druid.pool.DruidDataSource;@Configurationpublic class DataSourceConfiguration1 {@Bean(name = &quot;dataSource1&quot;)@Primary@ConfigurationProperties(prefix = &quot;datasource.test1&quot;)public DataSource dataSource() {return new DruidDataSource();}@Bean(name = &quot;dataSourceTransactionManager1&quot;)@Primarypublic DataSourceTransactionManager dataSourceTransactionManager(@Qualifier(&quot;dataSource1&quot;)DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Bean(name = &quot;sqlSessionFactory1&quot;)@Primarypublic SqlSessionFactory sessionFactory(@Qualifier(&quot;dataSource1&quot;)DataSource dataSource) throws Exception {SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();sessionFactoryBean.setDataSource(dataSource);return sessionFactoryBean.getObject();}@Bean(&quot;sqlSessionTemplate1&quot;)@Primarypublic SqlSessionTemplate sqlSessionTemplate(@Qualifier(&quot;sqlSessionFactory1&quot;) SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}}</code></pre><h4 id="第二个数据源以及持久层需要的组件">第二个数据源以及持久层需要的组件</h4><pre><code class="language-java">package io.springboot.multidatasource.configuration;import javax.sql.DataSource;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.SqlSessionTemplate;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import com.alibaba.druid.pool.DruidDataSource;@Configurationpublic class DataSourceConfiguration2 {@Bean(name = &quot;dataSource2&quot;)@ConfigurationProperties(prefix = &quot;datasource.test2&quot;)public DataSource dataSource() {return new DruidDataSource();}@Bean(name = &quot;dataSourceTransactionManager2&quot;)public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier(&quot;dataSource2&quot;)DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Bean(name = &quot;sqlSessionFactory2&quot;)public SqlSessionFactory sessionFactory(@Qualifier(&quot;dataSource2&quot;)DataSource dataSource) throws Exception {SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();sessionFactoryBean.setDataSource(dataSource);return sessionFactoryBean.getObject();}@Bean(&quot;sqlSessionTemplate2&quot;)public SqlSessionTemplate sqlSessionTemplate(@Qualifier(&quot;sqlSessionFactory2&quot;) SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}}</code></pre><p>可以看到，配置1中的bean，比配置2中的bean多了一个 <code>@Primary</code> 注解。<br />当其他的组件通过@Autowired等方式注入一个类的时候。而IOC中有多个该的实现。那么标注了<code>@Primary</code> 注解的Bean会优先注入。详细可以参阅官方文档</p><p><a href="https://docs.spring.io/spring/docs/5.1.5.RELEASE/spring-framework-reference/core.html#beans-autowired-annotation-primary">https://docs.spring.io/spring/docs/5.1.5.RELEASE/spring-framework-reference/core.html#beans-autowired-annotation-primary 31</a></p><h3 id="启动类">启动类</h3><pre><code class="language-java">package io.springboot.multidatasource;import org.mybatis.spring.annotation.MapperScan;import org.mybatis.spring.annotation.MapperScans;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@MapperScans(value = {@MapperScan(basePackages = &quot;io.springboot.multidatasource.mapper1&quot;,sqlSessionFactoryRef = &quot;sqlSessionFactory1&quot;,sqlSessionTemplateRef = &quot;sqlSessionTemplate1&quot;),@MapperScan(basePackages = &quot;io.springboot.multidatasource.mapper2&quot;,sqlSessionFactoryRef = &quot;sqlSessionFactory2&quot;,sqlSessionTemplateRef = &quot;sqlSessionTemplate2&quot;),})public class MultidatasourceApplication {public static void main(String[] args) {SpringApplication.run(MultidatasourceApplication.class, args);}}</code></pre><p>重点在于 <code>@MapperScan</code>注解。该注解会去扫描指定包下的接口。并且动态的生成实现类。<br />通过 <code>sqlSessionFactoryRe</code>f 和 <code>sqlSessionTemplateRef</code>指定它们生成代理对象时。使用的SqlSessionFactory 和 SqlSession。（值就是定义在了IOC中的bean名称）。</p><h3 id="版权申明"><font color='Red'>版权申明</font></h3><p>本文转载于 <a href="https://springboot.io/t/topic/109">SpringBoot + MyBatis使用多数据源</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Spring中经典的9种设计模式]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/spring-9-design-pattern" />
                <id>tag:https://ibit.tech,2021-11-01:spring-9-design-pattern</id>
                <published>2021-11-01T00:26:05+08:00</published>
                <updated>2021-11-01T00:30:56+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="简单工厂">简单工厂</h3><p>实现方式：BeanFactory。Spring中的BeanFactory就是简单工厂模式的体现，根据传入一个唯一的标识来获得Bean对象，但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。</p><p>实质：由一个工厂类根据传入的参数，动态决定应该创建哪一个产品类。</p><p>实现原理：</p><ul><li><p>bean容器的启动阶段：</p><ul><li>读取bean的xml配置文件，将bean元素分别转换成一个BeanDefinition对象。</li><li>然后通过BeanDefinitionRegistry将这些bean注册到beanFactory中，保存在它的一个ConcurrentHashMap中。</li><li>将BeanDefinition注册到了beanFactory之后，在这里Spring为我们提供了一个扩展的切口，允许我们通过实现接口BeanFactoryPostProcessor 在此处来插入我们定义的代码。典型的例子就是：PropertyPlaceholderConfigurer，我们一般在配置数据库的dataSource时使用到的占位符的值，就是它注入进去的。</li></ul></li><li><p>容器中bean的实例化阶段，实例化阶段主要是通过反射或者CGLIB对bean进行实例化，在这个阶段Spring又给我们暴露了很多的扩展点：</p><ul><li>各种的Aware接口，比如 BeanFactoryAware，对于实现了这些Aware接口的bean，在实例化bean时Spring会帮我们注入对应的BeanFactory的实例。</li><li>BeanPostProcessor接口，实现了BeanPostProcessor接口的bean，在实例化bean时Spring会帮我们调用接口中的方法。</li><li>InitializingBean接口，实现了InitializingBean接口的bean，在实例化bean时Spring会帮我们调用接口中的方法。</li><li>DisposableBean接口，实现了BeanPostProcessor接口的bean，在该bean死亡时Spring会帮我们调用接口中的方法。</li></ul></li></ul><p>设计意义：</p><ul><li>松耦合。可以将原来硬编码的依赖，通过Spring这个beanFactory这个工长来注入依赖，也就是说原来只有依赖方和被依赖方，现在我们引入了第三方——Spring这个beanFactory，由它来解决bean之间的依赖问题，达到了松耦合的效果。</li><li>bean的额外处理。通过Spring接口的暴露，在实例化bean的阶段我们可以进行一些额外的处理，这些额外的处理只需要让bean实现对应的接口即可，那么spring就会在bean的生命周期调用我们实现的接口来处理该bean。</li></ul><h3 id="工厂方法">工厂方法</h3><p>实现方式：FactoryBean接口。</p><p>实现原理：实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是，spring会在使用getBean()调用获得该bean时，会自动调用该bean的getObject()方法，所以返回的不是factory这个bean，而是这个bean.getOjbect()方法的返回值。</p><p>例子：</p><ul><li><p>典型的例子有Spring与MyBatis的结合。</p></li><li><p>代码示例：</p></li></ul><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1635697605241.png" alt="" /></p><ul><li>说明：我们看上面该bean，因为实现了FactoryBean接口，所以返回的不是SqlSessionFactoryBean的实例，而是它的 SqlSessionFactoryBean.getObject()的返回值。</li></ul><h3 id="单例模式">单例模式</h3><p>Spring依赖注入Bean实例默认是单例的。</p><p>Spring的依赖注入（包括lazy-init方式）都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。</p><p>分析getSingleton()方法：</p><pre><code>public Object getSingleton(String beanName){    //参数true设置标识允许早期依赖    return getSingleton(beanName,true);}protected Object getSingleton(String beanName, boolean allowEarlyReference) {    //检查缓存中是否存在实例    Object singletonObject = this.singletonObjects.get(beanName);    if (singletonObject == null &amp;&amp; isSingletonCurrentlyInCreation(beanName)) {        //如果为空，则锁定全局变量并进行处理。        synchronized (this.singletonObjects) {            //如果此bean正在加载，则不处理            singletonObject = this.earlySingletonObjects.get(beanName);            if (singletonObject == null &amp;&amp; allowEarlyReference) {                  //当某些方法需要提前初始化的时候则会调用addSingleFactory 方法将对应的ObjectFactory初始化策略存储在singletonFactories                ObjectFactory&lt;?&gt; singletonFactory = this.singletonFactories.get(beanName);                if (singletonFactory != null) {                    //调用预先设定的getObject方法                    singletonObject = singletonFactory.getObject();                    //记录在缓存中，earlysingletonObjects和singletonFactories互斥                    this.earlySingletonObjects.put(beanName, singletonObject);                    this.singletonFactories.remove(beanName);                }            }        }    }    return (singletonObject != NULL_OBJECT ? singletonObject : null);}</code></pre><p>getSingleton()过程图（ps：Spring依赖注入时，使用了双重判断加锁的单例模式）：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1635697626166.png" alt="" /></p><p>单例模式定义：保证一个类仅有一个实例，并提供一个访问它的全局访问点。</p><p>Spring对单例的实现：Spring中的单例模式完成了后半句话，即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例，这是因为Spring管理的是任意的Java对象。</p><h3 id="适配器模式">适配器模式</h3><p>实现方式：SpringMVC中的适配器HandlerAdatper。</p><p>实现原理：HandlerAdatper根据Handler规则执行不同的Handler。</p><p>实现过程：DispatcherServlet根据HandlerMapping返回的handler，向HandlerAdatper发起请求，处理Handler。HandlerAdapter根据规则找到对应的Handler并让其执行，执行完毕后Handler会向HandlerAdapter返回一个ModelAndView，最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。</p><p>实现意义：HandlerAdatper使得Handler的扩展变得容易，只需要增加一个新的Handler和一个对应的HandlerAdapter即可。因此Spring定义了一个适配接口，使得每一种Controller有一种对应的适配器实现类，让适配器代替controller执行相应的方法。这样在扩展Controller时，只需要增加一个适配器类就完成了SpringMVC的扩展了。</p><h3 id="装饰器模式">装饰器模式</h3><p>实现方式：Spring中用到的包装器模式在类名上有两种表现：一种是类名中含有Wrapper，另一种是类名中含有Decorator。</p><p>实质：</p><ul><li>动态地给一个对象添加一些额外的职责。</li><li>就增加功能来说，Decorator模式相比生成子类更为灵活。</li></ul><h3 id="代理模式">代理模式</h3><p>实现方式：AOP底层，就是动态代理模式的实现。</p><ul><li>动态代理：在内存中构建的，不需要手动编写代理类</li><li>静态代理：需要手工编写代理类，代理类引用被代理对象。</li></ul><p>实现原理：切面在应用运行的时刻被织入。一般情况下，在织入切面时，AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。</p><p>织入：把切面应用到目标对象并创建新的代理对象的过程。</p><h3 id="观察者模式">观察者模式</h3><p>实现方式：Spring的事件驱动模型使用的是观察者模式 ，Spring中Observer模式常用的地方是listener的实现。</p><p>具体实现：事件机制的实现需要三个部分，即：事件源、事件、事件监听器。</p><p><strong>ApplicationEvent抽象类[事件]</strong></p><p>继承自JDK的EventObject，所有的事件都需要继承ApplicationEvent，并且通过构造器参数source得到事件源。</p><p>该类的实现类ApplicationContextEvent表示ApplicaitonContext的容器事件。</p><p>代码：</p><pre><code>public abstract class ApplicationEvent extends EventObject {    private static final long serialVersionUID = 7099057708183571937L;    private final long timestamp;    public ApplicationEvent(Object source) {    super(source);    this.timestamp = System.currentTimeMillis();    }    public final long getTimestamp() {        return this.timestamp;    }}</code></pre><p><strong>ApplicationListener接口[事件监听器]</strong></p><p>继承自JDK的EventListener，所有的监听器都要实现这个接口。</p><p>这个接口只有一个onApplicationEvent()方法，该方法接受一个ApplicationEvent或其子类对象作为参数，在方法体中，可以通过不同对Event类的判断来进行相应的处理。</p><p>当事件触发时所有的监听器都会收到消息。</p><p>代码：</p><pre><code>    public interface ApplicationListener&lt;E extends ApplicationEvent&gt; extends                EventListener {                 void onApplicationEvent(E event);} </code></pre><p><strong>ApplicationContext接口[事件源]</strong></p><p>ApplicationContext是Spring中的全局容器，翻译过来是“应用上下文”。</p><p>实现了ApplicationEventPublisher接口。</p><p>职责：负责读取bean的配置文档，管理bean的加载，维护bean之间的依赖关系，可以说是负责bean的整个生命周期，再通俗一点就是我们平时所说的IOC容器。</p><p>代码：</p><pre><code>public interface ApplicationEventPublisher {        void publishEvent(ApplicationEvent event);}   public void publishEvent(ApplicationEvent event) {    Assert.notNull(event, &quot;Event must not be null&quot;);    if (logger.isTraceEnabled()) {         logger.trace(&quot;Publishing event in &quot; + getDisplayName() + &quot;: &quot; + event);    }    getApplicationEventMulticaster().multicastEvent(event);    if (this.parent != null) {    this.parent.publishEvent(event);    }}</code></pre><p><strong>ApplicationEventMulticaster抽象类[事件源中publishEvent方法需要调用其方法getApplicationEventMulticaster]</strong></p><p>属于事件广播器，它的作用是把Applicationcontext发布的Event广播给所有的监听器。</p><p>代码：</p><pre><code>public abstract class AbstractApplicationContext extends DefaultResourceLoader    implements ConfigurableApplicationContext, DisposableBean {      private ApplicationEventMulticaster applicationEventMulticaster;      protected void registerListeners() {      // Register statically specified listeners first.      for (ApplicationListener&lt;?&gt; listener : getApplicationListeners()) {      getApplicationEventMulticaster().addApplicationListener(listener);      }      // Do not initialize FactoryBeans here: We need to leave all regular beans      // uninitialized to let post-processors apply to them!      String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);      for (String lisName : listenerBeanNames) {      getApplicationEventMulticaster().addApplicationListenerBean(lisName);      }    }  }</code></pre><h3 id="策略模式">策略模式</h3><p>实现方式：Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力，Spring框架本身大量使用了Resource接口来访问底层资源。</p><p><strong>Resource接口介绍</strong></p><p>source接口是具体资源访问策略的抽象，也是所有资源访问类所实现的接口。</p><p>Resource接口主要提供了如下几个方法：</p><ul><li>getInputStream()：定位并打开资源，返回资源对应的输入流。每次调用都返回新的输入流。调用者必须负责关闭输入流。</li><li>exists()：返回Resource所指向的资源是否存在。</li><li>isOpen()：返回资源文件是否打开，如果资源文件不能多次读取，每次读取结束应该显式关闭，以防止资源泄漏。</li><li>getDescription()：返回资源的描述信息，通常用于资源处理出错时输出该信息，通常是全限定文件名或实际URL。</li><li>getFile：返回资源对应的File对象。</li><li>getURL：返回资源对应的URL对象。</li></ul><p>最后两个方法通常无须使用，仅在通过简单方式访问无法实现时，Resource提供传统的资源访问的功能。</p><p>Resource接口本身没有提供访问任何底层资源的实现逻辑，针对不同的底层资源，Spring将会提供不同的Resource实现类，不同的实现类负责不同的资源访问逻辑。</p><p>Spring为Resource接口提供了如下实现类：</p><ul><li>UrlResource：访问网络资源的实现类。</li><li>ClassPathResource：访问类加载路径里资源的实现类。</li><li>FileSystemResource：访问文件系统里资源的实现类。</li><li>ServletContextResource：访问相对于ServletContext路径里的资源的实现类.</li><li>InputStreamResource：访问输入流资源的实现类。</li><li>ByteArrayResource：访问字节数组资源的实现类。</li></ul><p>这些Resource实现类，针对不同的的底层资源，提供了相应的资源访问逻辑，并提供便捷的包装，以利于客户端程序的资源访问。</p><h3 id="模版方法模式">模版方法模式</h3><p>经典模板方法定义：</p><p>父类定义了骨架（调用哪些方法及顺序），某些特定方法由子类实现。</p><p>最大的好处：代码复用，减少重复代码。除了子类要实现的特定方法，其他方法及方法调用顺序都在父类中预先写好了。</p><p>所以父类模板方法中有两类方法：</p><ul><li>共同的方法：所有子类都会用到的代码</li><li>不同的方法：子类要覆盖的方法，分为两种：</li><li>抽象方法：父类中的是抽象方法，子类必须覆盖</li><li>钩子方法：父类中是一个空方法，子类继承了默认也是空的</li></ul><p>**注：**为什么叫钩子，子类可以通过这个钩子（方法），控制父类，因为这个钩子实际是父类的方法（空方法）！</p><p>Spring模板方法模式实质：是模板方法模式和回调模式的结合，是Template Method不需要继承的另一种实现方式。Spring几乎所有的外接扩展都采用这种模式。</p><p>具体实现：JDBC的抽象和对Hibernate的集成，都采用了一种理念或者处理方式，那就是模板方法模式与相应的Callback接口相结合。</p><p>采用模板方法模式是为了以一种统一而集中的方式来处理资源的获取和释放，以JdbcTemplate为例：</p><pre><code>public abstract class JdbcTemplate {       public final Object execute（String sql）{          Connection con=null;          Statement stmt=null;          try{              con=getConnection（）;              stmt=con.createStatement（）;              Object retValue=executeWithStatement（stmt,sql）;              return retValue;          }catch（SQLException e）{               ...          }finally{              closeStatement（stmt）;              releaseConnection（con）;          }      }       protected abstract Object executeWithStatement（Statement   stmt, String sql）;  }</code></pre><p>引入回调原因：JdbcTemplate是抽象类，不能够独立使用，我们每次进行数据访问的时候都要给出一个相应的子类实现,这样肯定不方便，所以就引入了回调。</p><p>回调代码：</p><pre><code>public interface StatementCallback{      Object doWithStatement（Statement stmt）;  }</code></pre><p>利用回调方法重写JdbcTemplate方法：</p><pre><code>public class JdbcTemplate {      public final Object execute（StatementCallback callback）{          Connection con=null;          Statement stmt=null;          try{              con=getConnection（）;              stmt=con.createStatement（）;              Object retValue=callback.doWithStatement（stmt）;              return retValue;          }catch（SQLException e）{              ...          }finally{              closeStatement（stmt）;              releaseConnection（con）;          }      }      ...//其它方法定义  }</code></pre><p>Jdbc使用方法如下：</p><pre><code>JdbcTemplate jdbcTemplate=...;      final String sql=...;      StatementCallback callback=new StatementCallback(){      public Object=doWithStatement(Statement stmt){          return ...;      }  }    jdbcTemplate.execute(callback);</code></pre><p>为什么JdbcTemplate没有使用继承？</p><p>因为这个类的方法太多，但是我们还是想用到JdbcTemplate已有的稳定的、公用的数据库连接，那么我们怎么办呢？我们可以把变化的东西抽出来作为一个参数传入JdbcTemplate的方法中。但是变化的东西是一段代码，而且这段代码会用到JdbcTemplate中的变量。怎么办？那我们就用回调对象吧。在这个回调对象中定义一个操纵JdbcTemplate中变量的方法，我们去实现这个方法，就把变化的东西集中到这里了。然后我们再传入这个回调对象到JdbcTemplate，从而完成了调用。</p><h3 id="版权申明"><font color='Red'>版权申明</font></h3><p>本文转载于【iCoding91 架构文摘】<a href="https://mp.weixin.qq.com/s/XrW32GfyWArWS_KNaY8ebQ">Spring中经典的9种设计模式</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[为什么 MySQL 中 Delete 表数据后，磁盘空间却还是被占用？]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/mysql-why-space-not-release-after-deleted" />
                <id>tag:https://ibit.tech,2021-11-01:mysql-why-space-not-release-after-deleted</id>
                <published>2021-11-01T00:15:55+08:00</published>
                <updated>2021-11-01T00:15:55+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>最近有个上位机获取下位机上报数据的项目，由于上报频率比较频繁且数据量大，导致数据增长过快，磁盘占用多。为了节约成本，定期进行数据备份，并通过delete删除表记录。明明已经执行了delete，可表文件的大小却没减小，令人费解！项目中使用Mysql作为数据库，对于表来说，一般为表结构和表数据。表结构占用空间都是比较小的，一般都是表数据占用的空间。当我们使用 delete删除数据时，确实删除了表中的数据记录，但查看表文件大小却没什么变化。</p><h3 id="1mysql数据结构">1.Mysql数据结构</h3><p>凡是使用过mysql，对B+树肯定是有所耳闻的，MySQL InnoDB 中采用了 B+ 树作为存储数据的结构，也就是常说的索引组织表，并且数据时按照页来存储的。因此在删除数据时，会有两种情况：</p><ul><li>删除数据页中的某些记录</li><li>删除整个数据页的内容</li></ul><h3 id="2表文件大小未更改和mysql设计有关">2.表文件大小未更改和mysql设计有关</h3><p>比如想要删除 R4 这条记录：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1635696707818.png" alt="image.png" /></p><p>InnoDB 直接将 R4 这条记录标记为删除，称为可复用的位置。如果之后要插入 ID 在 300 到 700 间的记录时，就会复用该位置。由此可见，<strong>磁盘文件的大小并不会减少</strong>。</p><p>通用删除整页数据也将记录标记删除，数据就复用用该位置，与删除默写记录不同的是，删除整页记录，当后来插入的数据不在原来的范围时，都可以复用位置，而如果只是删除默写记录，是需要插入数据符合删除记录位置的时候才能复用。另外关注公号“终码一生”，回复关键词“资料”，获取视频教程和最新的面试资料！</p><p>因此，无论是数据行的删除还是数据页的删除，都是将其标记为删除的状态，用于复用，所以文件并不会减小。</p><h3 id="3那怎么才能让表大小变小">3.那怎么才能让表大小变小</h3><p>DELETE只是将数据标识位删除，并没有整理数据文件，当插入新数据后，会再次使用这些被置为删除标识的记录空间，可以使用OPTIMIZE TABLE来回收未使用的空间，并整理数据文件的碎片。</p><pre><code>OPTIMIZE TABLE 表名;</code></pre><p>**注意：**OPTIMIZE TABLE只对MyISAM, BDB和InnoDB表起作用。</p><p>另外，也可以执行通过ALTER TABLE重建表</p><pre><code>ALTER TABLE 表名 ENGINE=INNODB</code></pre><p>有人会问OPTIMIZE TABLE和ALTER TABLE有什么区别？</p><pre><code>alter table t engine = InnoDB（也就是recreate），而optimize table t 等于recreate+analyze</code></pre><h3 id="4online-ddl">4.Online DDL</h3><p>最后，再说一下Online DDL，dba的日常工作肯定有一项是ddl变更，ddl变更会锁表，这个可以说是dba心中永远的痛，特别是执行ddl变更，导致库上大量线程处于“Waiting for meta data lock”状态的时候。因此在 5.6 版本后引入了 Online DDL。</p><p>Online DDL推出以前，执行ddl主要有两种方式copy方式和inplace方式，inplace方式又称为(fast index creation)。相对于copy方式，inplace方式不拷贝数据，因此较快。但是这种方式仅支持添加、删除索引两种方式，而且与copy方式一样需要全程锁表，实用性不是很强。Online方式与前两种方式相比，不仅可以读，还可以支持写操作。另外关注公号“终码一生”，回复关键词“资料”，获取视频教程和最新的面试资料！</p><p>执行online DDL语句的时候，使用ALGORITHM和LOCK关键字，这两个关键字在我们的DDL语句的最后面，用逗号隔开即可。示例如下：</p><pre><code>ALTER TABLE tbl_name ADD COLUMN col_name col_type, ALGORITHM=INPLACE, LOCK=NONE;</code></pre><p><strong>ALGORITHM选项</strong></p><ul><li><strong>INPLACE</strong>：替换：直接在原表上面执行DDL的操作。</li><li><strong>COPY</strong>：复制：使用一种临时表的方式，克隆出一个临时表，在临时表上执行DDL，然后再把数据导入到临时表中，在重命名等。这期间需要多出一倍的磁盘空间来支撑这样的 操作。执行期间，表不允许DML的操作。</li><li><strong>DEFAULT</strong>：默认方式，有MySQL自己选择，优先使用INPLACE的方式。</li></ul><p><strong>LOCK选项</strong></p><ul><li><strong>SHARE</strong>：共享锁，执行DDL的表可以读，但是不可以写。</li><li><strong>NONE</strong>：没有任何限制，执行DDL的表可读可写。</li><li><strong>EXCLUSIVE</strong>：排它锁，执行DDL的表不可以读，也不可以写。</li><li><strong>DEFAULT</strong>：默认值，也就是在DDL语句中不指定LOCK子句的时候使用的默认值。如果指定LOCK的值为DEFAULT，那就是交给MySQL子句去觉得锁还是不锁表。不建议使用，如果你确定你的DDL语句不会锁表，你可以不指定lock或者指定它的值为default，否则建议指定它的锁类型。</li></ul><p>执行DDL操作时，ALGORITHM选项可以不指定，这时候MySQL按照INSTANT、INPLACE、COPY的顺序自动选择合适的模式。也可以指定ALGORITHM=DEFAULT，也是同样的效果。如果指定了ALGORITHM选项，但不支持的话，会直接报错。</p><p>OPTIMIZE TABLE 和 ALTER TABLE 表名 ENGINE=INNODB都支持Oline DDL，但依旧建议在业务访问量低的时候使用</p><h3 id="5总结">5.总结</h3><p>delete 删除数据时，其实对应的数据行并不是真正的删除，仅仅是将其标记成可复用的状态，所以表空间不会变小。</p><p>可以重建表的方式，快速将delete数据后的表变小（<strong>OPTIMIZE TABLE</strong> 或<strong>ALTER TABLE</strong>），在 5.6 版本后，创建表已经支持 Online 的操作，但最好是在业务低峰时使用。</p><h3 id="版权申明"><font color='Red'>版权申明</font></h3><p>本文转载于【康熙 终码一生】<a href="https://mp.weixin.qq.com/s/th80JXu3mNwah9KlZ6w5DQ">为什么 MySQL 中 Delete 表数据后，磁盘空间却还是被占用？</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[MySQL使用技巧-查看数据库所占空间大小]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/mysql-use-skill-lookup-database-space" />
                <id>tag:https://ibit.tech,2021-10-31:mysql-use-skill-lookup-database-space</id>
                <published>2021-10-31T23:52:54+08:00</published>
                <updated>2021-10-31T23:52:54+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>在Mysql中会有一个默认的数据库：<code>information_schema</code>，里面有一个<code>Tables</code>表记录了所有表的信息。使用该表来看数据库所占空间大小的代码如下：</p><pre><code>USE information_schema;SELECT TABLE_SCHEMA, SUM(DATA_LENGTH) FROM TABLES GROUP BY TABLE_SCHEMA;</code></pre><p>可看到各个数据库的所占空间大小，如果想要看到以<code>k</code>为单位的大小，代码如下：</p><pre><code>USE information_schema;SELECT TABLE_SCHEMA, SUM(DATA_LENGTH)/1024 FROM TABLES GROUP BY TABLE_SCHEMA;</code></pre><p><strong>就是字节数除以1024</strong>，同理，<code>M</code>和<code>G</code>分别是再除一个1024和再除两个1024.</p><p><code>TABLES</code>表中还有很多其它的数据，有需要的同学可以通过<code>SHOW COLUMNS FROM TABLES</code>查看表的字段。</p><p>本文转载于<a href="https://zgljl2012.com/mysql-cha-kan-shu-ju-ku-suo-zhan-kong-jian-da-xiao/">【MySQL】查看数据库所占空间大小</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Spring基本概念]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/spring-basic-concept" />
                <id>tag:https://ibit.tech,2021-10-31:spring-basic-concept</id>
                <published>2021-10-31T23:27:05+08:00</published>
                <updated>2021-10-31T23:39:54+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="spring概述"><font color='green'>Spring概述</font></h2><h3 id="什么是spring">什么是spring?</h3><p>Spring是<strong>一个轻量级Java开发框架</strong>，最早有<strong>Rod Johnson</strong>创建，目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的JavaSE/JavaEE full-stack（一站式）轻量级开源框架，为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构，因此Java开发者可以专注于应用程序的开发。</p><p>Spring最根本的使命是<strong>解决企业级应用开发的复杂性，即简化Java开发</strong>。</p><p>Spring可以做很多事情，它为企业级开发提供给了丰富的功能，但是这些功能的底层都依赖于它的两个核心特性，也就是<strong>依赖注入（dependency injection，DI）和面向切面编程（aspect-oriented programming，AOP）</strong>。</p><p>为了降低Java开发的复杂性，Spring采取了以下4种关键策略</p><ul><li>基于POJO的轻量级和最小侵入性编程；</li><li>通过依赖注入和面向接口实现松耦合；</li><li>基于切面和惯例进行声明式编程；</li><li>通过切面和模板减少样板式代码。</li></ul><h3 id="spring框架的设计目标设计理念和核心是什么">Spring框架的设计目标，设计理念，和核心是什么</h3><p><strong>Spring设计目标</strong>：Spring为开发者提供一个一站式轻量级应用开发平台；</p><p><strong>Spring设计理念</strong>：在JavaEE开发中，支持POJO和JavaBean开发方式，使应用面向接口开发，充分支持OO（面向对象）设计方法；Spring通过IoC容器实现对象耦合关系的管理，并实现依赖反转，将对象之间的依赖关系交给IoC容器，实现解耦；</p><p><strong>Spring框架的核心</strong>：IoC容器和AOP模块。通过IoC容器管理POJO对象以及他们之间的耦合关系；通过AOP以动态非侵入的方式增强服务。</p><p>IoC让相互协作的组件保持松散的耦合，而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。</p><h3 id="spring的优缺点是什么">Spring的优缺点是什么？</h3><p>优点</p><ul><li><p>方便解耦，简化开发</p><p>Spring就是一个大工厂，可以将所有对象的创建和依赖关系的维护，交给Spring管理。</p></li><li><p>AOP编程的支持</p><p>Spring提供面向切面编程，可以方便的实现对程序进行权限拦截、运行监控等功能。</p></li><li><p>声明式事务的支持</p><p>只需要通过配置就可以完成对事务的管理，而无需手动编程。</p></li><li><p>方便程序的测试</p><p>Spring对Junit4支持，可以通过注解方便的测试Spring程序。</p></li><li><p>方便集成各种优秀框架</p><p>Spring不排斥各种优秀的开源框架，其内部提供了对各种优秀框架的直接支持（如：Struts、Hibernate、MyBatis等）。</p></li><li><p>降低JavaEE API的使用难度</p><p>Spring对JavaEE开发中非常难用的一些API（JDBC、JavaMail、远程调用等），都提供了封装，使这些API应用难度大大降低。</p></li></ul><p>缺点</p><ul><li>Spring明明一个很轻量级的框架，却给人感觉大而全</li><li>Spring依赖反射，反射影响性能</li><li>使用门槛升高，入门Spring需要较长时间</li></ul><h3 id="spring有哪些应用场景">Spring有哪些应用场景</h3><p><strong>应用场景</strong>：JavaEE企业应用开发，包括SSH、SSM等</p><p><strong>Spring价值</strong>：</p><ul><li>Spring是非侵入式的框架，目标是使应用程序代码对框架依赖最小化；</li><li>Spring提供一个一致的编程模型，使应用直接使用POJO开发，与运行环境隔离开来；</li><li>Spring推动应用设计风格向面向对象和面向接口开发转变，提高了代码的重用性和可测试性；</li></ul><h3 id="spring由哪些模块组成">Spring由哪些模块组成？</h3><p>Spring 总共大约有 20 个模块， 由 1300 多个不同的文件构成。而这些组件被分别整合在<code>核心容器（Core Container）</code>、 <code>AOP（Aspect Oriented Programming）和设备支持（Instrmentation）</code> 、<code>数据访问与集成（Data Access/Integeration）</code> 、 <code>Web</code>、 <code>消息（Messaging）</code> 、 <code>Test</code>等 6 个模块中。以下是 Spring 5 的模块结构图：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1635694354884.png" alt="" /></p><ul><li>spring core：提供了框架的基本组成部分，包括控制反转（Inversion of Control，IOC）和依赖注入（Dependency Injection，DI）功能。</li><li>spring beans：提供了BeanFactory，是工厂模式的一个经典实现，Spring将管理对象称为Bean。</li><li>spring context：构建于 core 封装包基础上的 context 封装包，提供了一种框架式的对象访问方法。</li><li>spring jdbc：提供了一个JDBC的抽象层，消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析， 用于简化JDBC。</li><li>spring aop：提供了面向切面的编程实现，让你可以自定义拦截器、切点等。</li><li>spring Web：提供了针对 Web 开发的集成特性，例如文件上传，利用 servlet listeners 进行 ioc 容器初始化和针对 Web 的 ApplicationContext。</li><li>spring test：主要为测试提供支持的，支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。</li></ul><h3 id="spring-框架中都用到了哪些设计模式">Spring 框架中都用到了哪些设计模式？</h3><ol><li>工厂模式：BeanFactory就是简单工厂模式的体现，用来创建对象的实例；</li><li>单例模式：Bean默认为单例模式。</li><li>代理模式：Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术；</li><li>模板方法：用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。</li><li>观察者模式：定义对象键一种一对多的依赖关系，当一个对象的状态发生改变时，所有依赖于它的对象都会得到通知被制动更新，如Spring中listener的实现–ApplicationListener。</li></ol><h3 id="详细讲解一下核心容器spring-context应用上下文-模块">详细讲解一下核心容器（spring context应用上下文) 模块</h3><p>这是基本的Spring模块，提供spring 框架的基础功能，BeanFactory 是 任何以spring为基础的应用的核心。Spring 框架建立在此模块之上，它使Spring成为一个容器。</p><p>Bean 工厂是工厂模式的一个实现，提供了控制反转功能，用来把应用的配置和依赖从真正的应用代码中分离。最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ，它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。</p><h3 id="spring框架中有哪些不同类型的事件">Spring框架中有哪些不同类型的事件</h3><p>Spring 提供了以下5种标准的事件：</p><ol><li>上下文更新事件（ContextRefreshedEvent）：在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。</li><li>上下文开始事件（ContextStartedEvent）：当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。</li><li>上下文停止事件（ContextStoppedEvent）：当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。</li><li>上下文关闭事件（ContextClosedEvent）：当ApplicationContext被关闭时触发该事件。容器被关闭时，其管理的所有单例Bean都被销毁。</li><li>请求处理事件（RequestHandledEvent）：在Web应用中，当一个http请求（request）结束触发该事件。如果一个bean实现了ApplicationListener接口，当一个ApplicationEvent 被发布以后，bean会自动被通知。</li></ol><h3 id="spring-应用程序有哪些不同组件">Spring 应用程序有哪些不同组件？</h3><p>Spring 应用一般有以下组件：</p><ul><li>接口 - 定义功能。</li><li>Bean 类 - 它包含属性，setter 和 getter 方法，函数等。</li><li>Bean 配置文件 - 包含类的信息以及如何配置它们。</li><li>Spring 面向切面编程（AOP） - 提供面向切面编程的功能。</li><li>用户程序 - 它使用接口。</li></ul><h3 id="使用-spring-有哪些方式">使用 Spring 有哪些方式？</h3><p>使用 Spring 有以下方式：</p><ul><li>作为一个成熟的 Spring Web 应用程序。</li><li>作为第三方 Web 框架，使用 Spring Frameworks 中间层。</li><li>作为企业级 Java Bean，它可以包装现有的 POJO（Plain Old Java Objects）。</li><li>用于远程使用。</li></ul><h2 id="spring控制反转ioc"><font color='green'>Spring控制反转(IOC)</font></h2><h3 id="什么是spring-ioc-容器">什么是Spring IOC 容器？</h3><p>控制反转即IoC (Inversion of Control)，它把传统上由程序代码直接操控的对象的调用权交给容器，通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移，从程序代码本身转移到了外部容器。</p><p>Spring IOC 负责创建对象，管理对象（通过依赖注入（DI），装配对象，配置对象，并且管理这些对象的整个生命周期。</p><h3 id="控制反转ioc有什么作用">控制反转(IoC)有什么作用</h3><ul><li>管理对象的创建和依赖关系的维护。对象的创建并不是一件简单的事，在对象关系比较复杂时，如果依赖关系需要程序猿来维护的话，那是相当头疼的</li><li>解耦，由容器去维护具体的对象</li><li>托管了类的产生过程，比如我们需要在类的产生过程中做一些处理，最直接的例子就是代理，如果有容器程序可以把这部分处理交给容器，应用程序则无需去关心类是如何完成代理的</li></ul><h3 id="ioc的优点是什么">IOC的优点是什么？</h3><ul><li>IOC 或 依赖注入把应用的代码量降到最低。</li><li>它使应用容易测试，单元测试不再需要单例和JNDI查找机制。</li><li>最小的代价和最小的侵入性使松散耦合得以实现。</li><li>IOC容器支持加载服务时的饿汉式初始化和懒加载。</li></ul><h3 id="spring-ioc-的实现机制">Spring IoC 的实现机制</h3><p>Spring 中的 IoC 的实现原理就是工厂模式加反射机制。</p><p>示例：</p><pre><code>interface Fruit {   public abstract void eat(); }class Apple implements Fruit {    public void eat(){        System.out.println(&quot;Apple&quot;);    }}class Orange implements Fruit {    public void eat(){        System.out.println(&quot;Orange&quot;);    }}class Factory {    public static Fruit getInstance(String ClassName) {        Fruit f=null;        try {            f=(Fruit)Class.forName(ClassName).newInstance();        } catch (Exception e) {            e.printStackTrace();        }        return f;    }}class Client {    public static void main(String[] a) {        Fruit f=Factory.getInstance(&quot;io.github.dunwu.spring.Apple&quot;);        if(f!=null){            f.eat();        }    }}</code></pre><h3 id="spring-的-ioc支持哪些功能">Spring 的 IoC支持哪些功能</h3><p>Spring 的 IoC 设计支持以下功能：</p><ul><li>依赖注入</li><li>依赖检查</li><li>自动装配</li><li>支持集合</li><li>指定初始化方法和销毁方法</li><li>支持回调某些方法（但是需要实现 Spring 接口，略有侵入）</li></ul><p>其中，最重要的就是依赖注入，从 XML 的配置上说，即 ref 标签。对应 Spring RuntimeBeanReference 对象。</p><p>对于 IoC 来说，最重要的就是容器。容器管理着 Bean 的生命周期，控制着 Bean 的依赖注入。</p><h3 id="beanfactory-和-applicationcontext有什么区别">BeanFactory 和 ApplicationContext有什么区别？</h3><p>BeanFactory和ApplicationContext是Spring的两大核心接口，都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。</p><p>依赖关系</p><p>BeanFactory：是Spring里面最底层的接口，包含了各种Bean的定义，读取bean配置文档，管理bean的加载、实例化，控制bean的生命周期，维护bean之间的依赖关系。</p><p>ApplicationContext接口作为BeanFactory的派生，除了提供BeanFactory所具有的功能外，还提供了更完整的框架功能：</p><ul><li>继承MessageSource，因此支持国际化。</li><li>统一的资源文件访问方式。</li><li>提供在监听器中注册bean的事件。</li><li>同时加载多个配置文件。</li><li>载入多个（有继承关系）上下文 ，使得每一个上下文都专注于一个特定的层次，比如应用的web层。</li></ul><p>加载方式</p><p>BeanFactroy采用的是延迟加载形式来注入Bean的，即只有在使用到某个Bean时(调用getBean())，才对该Bean进行加载实例化。这样，我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入，BeanFacotry加载后，直至第一次使用调用getBean方法才会抛出异常。</p><p>ApplicationContext，它是在容器启动时，一次性创建了所有的Bean。这样，在容器启动时，我们就可以发现Spring中存在的配置错误，这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean，通过预载入单实例bean ,确保当你需要的时候，你就不用等待，因为它们已经创建好了。</p><p>相对于基本的BeanFactory，ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时，程序启动较慢。</p><p>创建方式</p><p>BeanFactory通常以编程的方式被创建，ApplicationContext还能以声明的方式创建，如使用ContextLoader。</p><p>注册方式</p><p>BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用，但两者之间的区别是：BeanFactory需要手动注册，而ApplicationContext则是自动注册。</p><h3 id="spring-如何设计容器的beanfactory和applicationcontext的关系详解">Spring 如何设计容器的，BeanFactory和ApplicationContext的关系详解</h3><p>Spring 作者 Rod Johnson 设计了两个接口用以表示容器。</p><ul><li>BeanFactory</li><li>ApplicationContext</li></ul><p>BeanFactory 简单粗暴，可以理解为就是个 HashMap，Key 是 BeanName，Value 是 Bean 实例。通常只提供注册（put），获取（get）这两个功能。我们可以称之为 <strong>“低级容器”</strong>。</p><p>ApplicationContext 可以称之为 <strong>“高级容器”</strong>。因为他比 BeanFactory 多了更多的功能。他继承了多个接口。因此具备了更多的功能。例如资源的获取，支持多种消息（例如 JSP tag 的支持），对 BeanFactory 多了工具级别的支持等待。所以你看他的名字，已经不是 BeanFactory 之类的工厂了，而是 “应用上下文”， 代表着整个大容器的所有功能。该接口定义了一个 refresh 方法，此方法是所有阅读 Spring 源码的人的最熟悉的方法，用于刷新整个容器，即重新加载/刷新所有的 bean。</p><p>当然，除了这两个大接口，还有其他的辅助接口，这里就不介绍他们了。</p><p>BeanFactory和ApplicationContext的关系</p><p>为了更直观的展示 “低级容器” 和 “高级容器” 的关系，这里通过常用的 ClassPathXmlApplicationContext 类来展示整个容器的层级 UML 关系。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1635694399762.png" alt="" /></p><p>有点复杂？先不要慌，我来解释一下。</p><p>最上面的是 BeanFactory，下面的 3 个绿色的，都是功能扩展接口，这里就不展开讲。</p><p>看下面的隶属 ApplicationContext 粉红色的 “高级容器”，依赖着 “低级容器”，这里说的是依赖，不是继承哦。他依赖着 “低级容器” 的 getBean 功能。而高级容器有更多的功能：支持不同的信息源头，可以访问文件资源，支持应用事件（Observer 模式）。</p><p>通常用户看到的就是 “高级容器”。但 BeanFactory 也非常够用啦！</p><p>左边灰色区域的是 “低级容器”， 只负载加载 Bean，获取 Bean。容器其他的高级功能是没有的。例如上图画的 refresh 刷新 Bean 工厂所有配置，生命周期事件回调等。</p><p>小结</p><p>说了这么多，不知道你有没有理解Spring IoC？这里小结一下：IoC 在 Spring 里，只需要低级容器就可以实现，2 个步骤：</p><ol><li>加载配置文件，解析成 BeanDefinition 放在 Map 里。</li><li>调用 getBean 的时候，从 BeanDefinition 所属的 Map 里，拿出 Class 对象进行实例化，同时，如果有依赖关系，将递归调用 getBean 方法 —— 完成依赖注入。</li></ol><p>上面就是 Spring 低级容器（BeanFactory）的 IoC。</p><p>至于高级容器 ApplicationContext，他包含了低级容器的功能，当他执行 refresh 模板方法的时候，将刷新整个容器的 Bean。同时其作为高级容器，包含了太多的功能。一句话，他不仅仅是 IoC。他支持不同信息源头，支持 BeanFactory 工具类，支持层级容器，支持访问文件资源，支持事件发布通知，支持接口回调等等。</p><h3 id="applicationcontext通常的实现是什么">ApplicationContext通常的实现是什么？</h3><p><strong>FileSystemXmlApplicationContext</strong> ：此容器从一个XML文件中加载beans的定义，XML Bean 配置文件的全路径名必须提供给它的构造函数。</p><p><strong>ClassPathXmlApplicationContext</strong>：此容器也从一个XML文件中加载beans的定义，这里，你需要正确设置classpath因为这个容器将在classpath里找bean配置。</p><p><strong>WebXmlApplicationContext</strong>：此容器加载一个XML文件，此文件定义了一个WEB应用的所有bean。</p><h3 id="什么是spring的依赖注入">什么是Spring的依赖注入？</h3><p>控制反转IoC是一个很大的概念，可以用不同的方式来实现。其主要实现方式有两种：依赖注入和依赖查找</p><p>依赖注入：相对于IoC而言，依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入（Dependency Injection），即组件之间的依赖关系由容器在应用系统运行期来决定，也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询，只提供普通的Java方法让容器去决定依赖关系。</p><h3 id="依赖注入的基本原则">依赖注入的基本原则</h3><p>依赖注入的基本原则是：应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责，“查找资源”的逻辑应该从应用组件的代码中抽取出来，交给IoC容器负责。容器全权负责组件的装配，它会把符合依赖关系的对象通过属性（JavaBean中的setter）或者是构造器传递给需要的对象。</p><h3 id="依赖注入有什么优势">依赖注入有什么优势</h3><p>依赖注入之所以更流行是因为它是一种更可取的方式：让容器全权负责依赖查询，受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口，使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相比，主要优势为：</p><ul><li>查找定位操作与应用代码完全无关。</li><li>不依赖于容器的API，可以很容易地在任何容器以外使用应用对象。</li><li>不需要特殊的接口，绝大多数对象可以做到完全不必依赖容器。</li></ul><h3 id="有哪些不同类型的依赖注入实现方式">有哪些不同类型的依赖注入实现方式？</h3><p>依赖注入是时下最流行的IoC实现方式，依赖注入分为接口注入（Interface Injection），Setter方法注入（Setter Injection）和构造器注入（Constructor Injection）三种方式。其中接口注入由于在灵活性和易用性比较差，现在从Spring4开始已被废弃。</p><p><strong>构造器依赖注入</strong>：构造器依赖注入通过容器触发一个类的构造器来实现的，该类有一系列参数，每个参数代表一个对其他类的依赖。</p><p><strong>Setter方法注入</strong>：Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后，调用该bean的setter方法，即实现了基于setter的依赖注入。</p><h3 id="构造器依赖注入和-setter方法注入的区别">构造器依赖注入和 Setter方法注入的区别</h3><table><thead><tr><th>构造函数注入</th><th>setter 注入</th></tr></thead><tbody><tr><td>没有部分注入</td><td>有部分注入</td></tr><tr><td>不会覆盖 setter 属性</td><td>会覆盖 setter 属性</td></tr><tr><td>任意修改都会创建一个新实例</td><td>任意修改不会创建一个新实例</td></tr><tr><td>适用于设置很多属性</td><td>适用于设置少量属性</td></tr></tbody></table><p>两种依赖方式都可以使用，构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖，setter方法实现可选依赖。</p><h2 id="spring-beans"><font color='green'>Spring Beans</font></h2><h3 id="什么是spring-beans">什么是Spring beans？</h3><p>Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化，装配，和管理。这些beans通过容器中配置的元数据创建。比如，以XML文件中 的形式定义。</p><h3 id="一个-spring-bean-定义-包含什么">一个 Spring Bean 定义 包含什么？</h3><p>一个Spring Bean 的定义包含容器必知的所有配置元数据，包括如何创建一个bean，它的生命周期详情及它的依赖。</p><h3 id="如何给spring-容器提供配置元数据spring有几种配置方式">如何给Spring 容器提供配置元数据？Spring有几种配置方式</h3><p>这里有三种重要的方法给Spring 容器提供配置元数据。</p><ul><li>XML配置文件。</li><li>基于注解的配置。</li><li>基于java的配置。</li></ul><h3 id="spring配置文件包含了哪些信息">Spring配置文件包含了哪些信息</h3><p>Spring配置文件是个XML 文件，这个文件包含了类信息，描述了如何配置它们，以及如何相互调用。</p><h3 id="spring基于xml注入bean的几种方式">Spring基于xml注入bean的几种方式</h3><ol><li>Set方法注入；</li><li>构造器注入：①通过index设置参数的位置；②通过type设置参数类型；</li><li>静态工厂注入；</li><li>实例工厂；</li></ol><h3 id="你怎样定义类的作用域">你怎样定义类的作用域？</h3><p>当定义一个 在Spring里，我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定义。如，当Spring要在需要的时候每次生产一个新的bean实例，bean的scope属性被指定为prototype。另一方面，一个bean每次使用的时候必须返回同一个实例，这个bean的scope 属性 必须设为 singleton。</p><h3 id="解释spring支持的几种bean的作用域">解释Spring支持的几种bean的作用域</h3><p>Spring框架支持以下五种bean的作用域：</p><ul><li><strong>singleton :</strong> bean在每个Spring ioc 容器中只有一个实例。</li><li><strong>prototype</strong>：一个bean的定义可以有多个实例。</li><li><strong>request</strong>：每次http请求都会创建一个bean，该作用域仅在基于web的Spring ApplicationContext情形下有效。</li><li><strong>session</strong>：在一个HTTP Session中，一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。</li><li><strong>global-session</strong>：在一个全局的HTTP Session中，一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。</li></ul><p><strong>注意：</strong> 缺省的Spring bean 的作用域是Singleton。使用 prototype 作用域需要慎重的思考，因为频繁创建和销毁 bean 会带来很大的性能开销。</p><h3 id="spring框架中的单例bean是线程安全的吗">Spring框架中的单例bean是线程安全的吗？</h3><p>不是，Spring框架中的单例bean不是线程安全的。</p><p>spring 中的 bean 默认是单例模式，spring 框架并没有对单例 bean 进行多线程的封装处理。</p><p>实际上大部分时候 spring bean 无状态的（比如 dao 类），所有某种程度上来说 bean 也是安全的，但如果 bean 有状态的话（比如 view model 对象），那就要开发者自己去保证线程安全了，最简单的就是改变 bean 的作用域，把“singleton”变更为“prototype”，这样请求 bean 相当于 new Bean()了，所以就可以保证线程安全了。</p><ul><li>有状态就是有数据存储功能。</li><li>无状态就是不会保存数据。</li></ul><h3 id="spring如何处理线程并发问题">Spring如何处理线程并发问题？</h3><p>在一般情况下，只有无状态的Bean才可以在多线程环境下共享，在Spring中，绝大部分Bean都可以声明为singleton作用域，因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理，解决线程安全问题。</p><p>ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式，仅提供一份变量，不同的线程在访问前需要获取锁，没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。</p><p>ThreadLocal会为每一个线程提供一个独立的变量副本，从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本，从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象，在编写多线程代码时，可以把不安全的变量封装进ThreadLocal。</p><h3 id="解释spring框架中bean的生命周期">解释Spring框架中bean的生命周期</h3><p>在传统的Java应用中，bean的生命周期很简单。使用Java关键字new进行bean实例化，然后该bean就可以使用了。一旦该bean不再被使用，则由Java自动进行垃圾回收。相比之下，Spring容器中的bean的生命周期就显得相对复杂多了。正确理解Spring bean的生命周期非常重要，因为你或许要利用Spring提供的扩展点来自定义bean的创建过程。下图展示了bean装载到Spring应用上下文中的一个典型的生命周期过程。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1635694433592.png" alt="" /></p><p>bean在Spring容器中从创建到销毁经历了若干阶段，每一阶段都可以针对Spring如何管理bean进行个性化定制。</p><p>正如你所见，在bean准备就绪之前，bean工厂执行了若干启动步骤。</p><p>我们对上图进行详细描述：</p><p>Spring对bean进行实例化；</p><p>Spring将值和bean的引用注入到bean对应的属性中；</p><p>如果bean实现了BeanNameAware接口，Spring将bean的ID传递给setBean-Name()方法；</p><p>如果bean实现了BeanFactoryAware接口，Spring将调用setBeanFactory()方法，将BeanFactory容器实例传入；</p><p>如果bean实现了ApplicationContextAware接口，Spring将调用setApplicationContext()方法，将bean所在的应用上下文的引用传入进来；</p><p>如果bean实现了BeanPostProcessor接口，Spring将调用它们的post-ProcessBeforeInitialization()方法；</p><p>如果bean实现了InitializingBean接口，Spring将调用它们的after-PropertiesSet()方法。类似地，如果bean使用initmethod声明了初始化方法，该方法也会被调用；</p><p>如果bean实现了BeanPostProcessor接口，Spring将调用它们的post-ProcessAfterInitialization()方法；</p><p>此时，bean已经准备就绪，可以被应用程序使用了，它们将一直驻留在应用上下文中，直到该应用上下文被销毁；</p><p>如果bean实现了DisposableBean接口，Spring将调用它的destroy()接口方法。同样，如果bean使用destroy-method声明了销毁方法，该方法也会被调用。</p><p>现在你已经了解了如何创建和加载一个Spring容器。但是一个空的容器并没有太大的价值，在你把东西放进去之前，它里面什么都没有。为了从Spring的DI(依赖注入)中受益，我们必须将应用对象装配进Spring容器中。</p><h3 id="哪些是重要的bean生命周期方法你能重载它们吗">哪些是重要的bean生命周期方法？你能重载它们吗？</h3><p>有两个重要的bean 生命周期方法，第一个是setup ， 它是在容器加载bean的时候被调用。第二个方法是 teardown 它是在容器卸载类的时候被调用。</p><p>bean 标签有两个重要的属性（init-method和destroy-method）。用它们你可以自己定制初始化和注销方法。它们也有相应的注解（@PostConstruct和@PreDestroy）。</p><h3 id="什么是spring的内部bean什么是spring-inner-beans">什么是Spring的内部bean？什么是Spring inner beans？</h3><p>在Spring框架中，当一个bean仅被用作另一个bean的属性时，它能被声明为一个内部bean。内部bean可以用setter注入“属性”和构造方法注入“构造参数”的方式来实现，内部bean通常是匿名的，它们的Scope一般是prototype。</p><h3 id="在-spring中如何注入一个java集合">在 Spring中如何注入一个java集合？</h3><p>Spring提供以下几种集合的配置元素：</p><p>类型用于注入一列值，允许有相同的值。</p><p>类型用于注入一组值，不允许有相同的值。</p><p>类型用于注入一组键值对，键和值都只能为String类型。</p><h3 id="什么是bean装配">什么是bean装配？</h3><p>装配，或bean 装配是指在Spring 容器中把bean组装到一起，前提是容器需要知道bean的依赖关系，如何通过依赖注入来把它们装配到一起。</p><h3 id="什么是bean的自动装配">什么是bean的自动装配？</h3><p>在Spring框架中，在配置文件中设定bean的依赖关系是一个很好的机制，Spring 容器能够自动装配相互合作的bean，这意味着容器不需要和配置，能通过Bean工厂自动处理bean之间的协作。这意味着 Spring可以通过向Bean Factory中注入的方式自动搞定bean之间的依赖关系。自动装配可以设置在每个bean上，也可以设定在特定的bean上。</p><h3 id="解释不同方式的自动装配spring-自动装配-bean-有哪些方式">解释不同方式的自动装配，spring 自动装配 bean 有哪些方式？</h3><p>在spring中，对象无需自己查找或创建与其关联的其他对象，由容器负责把需要相互协作的对象引用赋予各个对象，使用autowire来配置自动装载模式。</p><p>在Spring框架xml配置中共有5种自动装配：</p><ul><li>no：默认的方式是不进行自动装配的，通过手工设置ref属性来进行装配bean。</li><li>byName：通过bean的名称进行自动装配，如果一个bean的 property 与另一bean 的name 相同，就进行自动装配。</li><li>byType：通过参数的数据类型进行自动装配。</li><li>constructor：利用构造函数进行装配，并且构造函数的参数通过byType进行装配。</li><li>autodetect：自动探测，如果有构造方法，通过 construct的方式自动装配，否则使用 byType的方式自动装配。</li></ul><h3 id="使用autowired注解自动装配的过程是怎样的">使用@Autowired注解自动装配的过程是怎样的？</h3><p>使用@Autowired注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置，&lt;context:annotation-config /&gt;。</p><p>在启动spring IoC时，容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器，当容器扫描到@Autowied、@Resource或@Inject时，就会在IoC容器自动查找需要的bean，并装配给该对象的属性。在使用@Autowired时，首先在容器中查询对应类型的bean：</p><ul><li>如果查询结果刚好为一个，就将该bean装配给@Autowired指定的数据；</li><li>如果查询的结果不止一个，那么@Autowired会根据名称来查找；</li><li>如果上述查找的结果为空，那么会抛出异常。解决方法时，使用required=false。</li></ul><h3 id="自动装配有哪些局限性">自动装配有哪些局限性？</h3><p>自动装配的局限性是：</p><p><strong>重写</strong>：你仍需用 和 配置来定义依赖，意味着总要重写自动装配。</p><p><strong>基本数据类型</strong>：你不能自动装配简单的属性，如基本数据类型，String字符串，和类。</p><p><strong>模糊特性</strong>：自动装配不如显式装配精确，如果有可能，建议使用显式装配。</p><h3 id="你可以在spring中注入一个null-和一个空字符串吗">你可以在Spring中注入一个null 和一个空字符串吗？</h3><p>可以。</p><h2 id="spring注解"><font color='green'>Spring注解</font></h2><h3 id="什么是基于java的spring注解配置-给一些注解的例子">什么是基于Java的Spring注解配置? 给一些注解的例子</h3><p>基于Java的配置，允许你在少量的Java注解的帮助下，进行你的大部分Spring配置而非通过XML文件。</p><p>以@Configuration 注解为例，它用来标记类可以当做一个bean的定义，被Spring IOC容器使用。</p><p>另一个例子是@Bean注解，它表示此方法将要返回一个对象，作为一个bean注册进Spring应用上下文。</p><pre><code>@Configurationpublic class StudentConfig {    @Bean    public StudentBean myStudent() {        return new StudentBean();    }}</code></pre><h3 id="怎样开启注解装配">怎样开启注解装配？</h3><p>注解装配在默认情况下是不开启的，为了使用注解装配，我们必须在Spring配置文件中配置 <code>&lt;context:annotation-config/&gt;</code>元素。</p><h3 id="component-controller-repository-service-有何区别">@Component, @Controller, @Repository, @Service 有何区别？</h3><p>@Component：这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。</p><p>@Controller：这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。</p><p>@Service：此注解是组件注解的特化。它不会对 @Component 注解提供任何其他行为。您可以在服务层类中使用 @Service 而不是 @Component，因为它以更好的方式指定了意图。</p><p>@Repository：这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器，并使未经检查的异常有资格转换为 Spring DataAccessException。</p><h3 id="required-注解有什么作用">@Required 注解有什么作用</h3><p>这个注解表明bean的属性必须在配置的时候设置，通过一个bean定义的显式的属性值或通过自动装配，若@Required注解的bean属性未被设置，容器将抛出BeanInitializationException。示例：</p><pre><code>public class Employee {    private String name;    @Required    public void setName(String name){        this.name=name;    }    public string getName(){        return name;    }}</code></pre><h3 id="autowired-注解有什么作用">@Autowired 注解有什么作用</h3><p>@Autowired默认是按照类型装配注入的，默认情况下它要求依赖对象必须存在（可以设置它required属性为false）。@Autowired 注解提供了更细粒度的控制，包括在何处以及如何完成自动装配。它的用法和@Required一样，修饰setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。</p><pre><code>public class Employee {    private String name;    @Autowired    public void setName(String name) {        this.name=name;    }    public string getName(){        return name;    }}</code></pre><h3 id="autowired和resource之间的区别">@Autowired和@Resource之间的区别</h3><p>@Autowired可用于：构造函数、成员变量、Setter方法</p><p>@Autowired和@Resource之间的区别</p><ul><li>@Autowired默认是按照类型装配注入的，默认情况下它要求依赖对象必须存在（可以设置它required属性为false）。</li><li>@Resource默认是按照名称来装配注入的，只有当找不到与名称匹配的bean才会按照类型来装配注入。</li></ul><h3 id="qualifier-注解有什么作用">@Qualifier 注解有什么作用</h3><p>当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时，您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。</p><h3 id="requestmapping-注解有什么用">@RequestMapping 注解有什么用？</h3><p>@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注释可应用于两个级别：</p><ul><li>类级别：映射请求的 URL</li><li>方法级别：映射 URL 以及 HTTP 请求方法</li></ul><h2 id="spring数据访问"><font color='green'>Spring数据访问</font></h2><h3 id="解释对象关系映射集成模块">解释对象/关系映射集成模块</h3><p>Spring 通过提供ORM模块，支持我们在直接JDBC之上使用一个对象/关系映射映射(ORM)工具，Spring 支持集成主流的ORM框架，如Hiberate，JDO和 iBATIS，JPA，TopLink，JDO，OJB 。Spring的事务管理同样支持以上所有ORM框架及JDBC。</p><h3 id="在spring框架中如何更有效地使用jdbc">在Spring框架中如何更有效地使用JDBC？</h3><p>使用Spring JDBC 框架，资源管理和错误处理的代价都会被减轻。所以开发者只需写statements 和 queries从数据存取数据，JDBC也可以在Spring框架提供的模板类的帮助下更有效地被使用，这个模板叫JdbcTemplate</p><h3 id="解释jdbc抽象和dao模块">解释JDBC抽象和DAO模块</h3><p>通过使用JDBC抽象和DAO模块，保证数据库代码的简洁，并能避免数据库资源错误关闭导致的问题，它在各种不同的数据库的错误信息之上，提供了一个统一的异常访问层。它还利用Spring的AOP 模块给Spring应用中的对象提供事务管理服务。</p><h3 id="spring-dao-有什么用">spring DAO 有什么用？</h3><p>Spring DAO（数据访问对象） 使得 JDBC，Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时，无需考虑捕获每种技术不同的异常。</p><h3 id="spring-jdbc-api-中存在哪些类">spring JDBC API 中存在哪些类？</h3><p>JdbcTemplate</p><p>SimpleJdbcTemplate</p><p>NamedParameterJdbcTemplate</p><p>SimpleJdbcInsert</p><p>SimpleJdbcCall</p><h3 id="jdbctemplate是什么">JdbcTemplate是什么</h3><p>JdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象，执行写好的或可调用的数据库操作语句，提供自定义的数据错误处理。</p><h3 id="使用spring通过什么方式访问hibernate使用-spring-访问-hibernate-的方法有哪些">使用Spring通过什么方式访问Hibernate？使用 Spring 访问 Hibernate 的方法有哪些？</h3><p>在Spring中有两种方式访问Hibernate：</p><ul><li>使用 Hibernate 模板和回调进行控制反转</li><li>扩展 HibernateDAOSupport 并应用 AOP 拦截器节点</li></ul><h3 id="如何通过hibernatedaosupport将spring和hibernate结合起来">如何通过HibernateDaoSupport将Spring和Hibernate结合起来？</h3><p>用Spring的 SessionFactory 调用 LocalSessionFactory。集成过程分三步：</p><ul><li>配置the Hibernate SessionFactory</li><li>继承HibernateDaoSupport实现一个DAO</li><li>在AOP支持的事务中装配</li></ul><h3 id="spring支持的事务管理类型-spring-事务实现方式有哪些">Spring支持的事务管理类型， spring 事务实现方式有哪些？</h3><p>Spring支持两种类型的事务管理：</p><p><strong>编程式事务管理</strong>：这意味你通过编程的方式管理事务，给你带来极大的灵活性，但是难维护。</p><p><strong>声明式事务管理</strong>：这意味着你可以将业务代码和事务管理分离，你只需用注解和XML配置来管理事务。</p><h3 id="spring事务的实现方式和实现原理">Spring事务的实现方式和实现原理</h3><p>Spring事务的本质其实就是数据库对事务的支持，没有数据库的事务支持，spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。</p><h3 id="说一下spring的事务传播行为">说一下Spring的事务传播行为</h3><p>spring事务的传播行为说的是，当多个事务同时存在的时候，spring如何处理这些事务的行为。</p><blockquote><p>① PROPAGATION_REQUIRED：如果当前没有事务，就创建一个新事务，如果当前存在事务，就加入该事务，该设置是最常用的设置。</p><p>② PROPAGATION_SUPPORTS：支持当前事务，如果当前存在事务，就加入该事务，如果当前不存在事务，就以非事务执行。</p><p>③ PROPAGATION_MANDATORY：支持当前事务，如果当前存在事务，就加入该事务，如果当前不存在事务，就抛出异常。</p><p>④ PROPAGATION_REQUIRES_NEW：创建新事务，无论当前存不存在事务，都创建新事务。</p><p>⑤ PROPAGATION_NOT_SUPPORTED：以非事务方式执行操作，如果当前存在事务，就把当前事务挂起。</p><p>⑥ PROPAGATION_NEVER：以非事务方式执行，如果当前存在事务，则抛出异常。</p><p>⑦ PROPAGATION_NESTED：如果当前存在事务，则在嵌套事务内执行。如果当前没有事务，则按REQUIRED属性执行。</p></blockquote><h3 id="说一下-spring-的事务隔离">说一下 spring 的事务隔离？</h3><p>spring 有五大隔离级别，默认值为 ISOLATION_DEFAULT（使用数据库的设置），其他四个隔离级别和数据库的隔离级别一致：</p><ol><li>ISOLATION_DEFAULT：用底层数据库的设置隔离级别，数据库设置的是什么我就用什么；</li><li>ISOLATION_READ_UNCOMMITTED：未提交读，最低隔离级别、事务未提交前，就可被其他事务读取（会出现幻读、脏读、不可重复读）；</li><li>ISOLATION_READ_COMMITTED：提交读，一个事务提交后才能被其他事务读取到（会造成幻读、不可重复读），SQL server 的默认级别；</li><li>ISOLATION_REPEATABLE_READ：可重复读，保证多次读取同一个数据时，其值都和事务开始时候的内容是一致，禁止读取到别的事务未提交的数据（会造成幻读），MySQL 的默认级别；</li><li>ISOLATION_SERIALIZABLE：序列化，代价最高最可靠的隔离级别，该隔离级别能防止脏读、不可重复读、幻读。</li></ol><p><strong>脏读</strong> ：表示一个事务能够读取另一个事务中还未提交的数据。比如，某个事务尝试插入记录 A，此时该事务还未提交，然后另一个事务尝试读取到了记录 A。</p><p><strong>不可重复读</strong> ：是指在一个事务内，多次读同一数据。</p><p><strong>幻读</strong> ：指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录，但是第二次同等条件下查询却有 n+1 条记录，这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据，同一个记录的数据内容被修改了，所有数据行的记录就变多或者变少了。</p><h3 id="spring框架的事务管理有哪些优点">Spring框架的事务管理有哪些优点？</h3><ul><li>为不同的事务API 如 JTA，JDBC，Hibernate，JPA 和JDO，提供一个不变的编程模式。</li><li>为编程式事务管理提供了一套简单的API而不是一些复杂的事务API</li><li>支持声明式事务管理。</li><li>和Spring各种数据访问抽象层很好得集成。</li></ul><h3 id="你更倾向用那种事务管理类型">你更倾向用那种事务管理类型？</h3><p>大多数Spring框架的用户选择声明式事务管理，因为它对应用代码的影响最小，因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理，虽然比编程式事务管理（这种方式允许你通过代码控制事务）少了一点灵活性。唯一不足地方是，最细粒度只能作用到方法级别，无法做到像编程式事务那样可以作用到代码块级别。</p><h2 id="spring面向切面编程aop"><font color='green'>Spring面向切面编程(AOP)</font></h2><h3 id="什么是aop">什么是AOP</h3><p>OOP(Object-Oriented Programming)面向对象编程，允许开发者定义纵向的关系，但并适用于定义横向的关系，导致了大量代码的重复，而不利于各个模块的重用。</p><p>AOP(Aspect-Oriented Programming)，一般称为面向切面编程，作为面向对象的一种补充，用于将那些与业务无关，但却对多个对象产生影响的公共行为和逻辑，抽取并封装为一个可重用的模块，这个模块被命名为“切面”（Aspect），减少系统中的重复代码，降低了模块间的耦合度，同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。</p><h3 id="spring-aop-and-aspectj-aop-有什么区别aop-有哪些实现方式">Spring AOP and AspectJ AOP 有什么区别？AOP 有哪些实现方式？</h3><p>AOP实现的关键在于 代理模式，AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ；动态代理则以Spring AOP为代表。</p><p>（1）AspectJ是静态代理的增强，所谓静态代理，就是AOP框架会在编译阶段生成AOP代理类，因此也称为编译时增强，他会在编译阶段将AspectJ(切面)织入到Java字节码中，运行的时候就是增强之后的AOP对象。</p><p>（2）Spring AOP使用的动态代理，所谓的动态代理就是说AOP框架不会去修改字节码，而是每次运行时在内存中临时为方法生成一个AOP对象，这个AOP对象包含了目标对象的全部方法，并且在特定的切点做了增强处理，并回调原对象的方法。</p><h3 id="jdk动态代理和cglib动态代理的区别">JDK动态代理和CGLIB动态代理的区别</h3><p>Spring AOP中的动态代理主要有两种方式，JDK动态代理和CGLIB动态代理：</p><ul><li>JDK动态代理只提供接口的代理，不支持类的代理。核心InvocationHandler接口和Proxy类，InvocationHandler 通过invoke()方法反射来调用目标类中的代码，动态地将横切逻辑和业务编织在一起；接着，Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。</li><li>如果代理类没有实现 InvocationHandler 接口，那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB（Code Generation Library），是一个代码生成的类库，可以在运行时动态的生成指定类的一个子类对象，并覆盖其中特定方法并添加增强代码，从而实现AOP。CGLIB是通过继承的方式做的动态代理，因此如果某个类被标记为final，那么它是无法使用CGLIB做动态代理的。</li></ul><p>静态代理与动态代理区别在于生成AOP代理对象的时机不同，相对来说AspectJ的静态代理方式具有更好的性能，但是AspectJ需要特定的编译器进行处理，而Spring AOP则无需特定的编译器处理。</p><blockquote><p>InvocationHandler 的 invoke(Object proxy,Method method,Object[] args)：proxy是最终生成的代理实例; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。</p></blockquote><h3 id="如何理解-spring-中的代理">如何理解 Spring 中的代理？</h3><p>将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下，目标对象和代理对象是相同的。</p><p>Advice + Target Object = Proxy</p><h3 id="解释一下spring-aop里面的几个名词">解释一下Spring AOP里面的几个名词</h3><p>（1）切面（Aspect）：切面是通知和切点的结合。通知和切点共同定义了切面的全部内容。在Spring AOP中，切面可以使用通用类（基于模式的风格） 或者在普通类中以 @AspectJ 注解来实现。</p><p>（2）连接点（Join point）：指方法，在Spring AOP中，一个连接点 总是 代表一个方法的执行。应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中，并添加新的行为。</p><p>（3）通知（Advice）：在AOP术语中，切面的工作被称为通知。</p><p>（4）切入点（Pointcut）：切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称，或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。</p><p>（5）引入（Introduction）：引入允许我们向现有类添加新方法或属性。</p><p>（6）目标对象（Target Object）：被一个或者多个切面（aspect）所通知（advise）的对象。它通常是一个代理对象。也有人把它叫做 被通知（adviced） 对象。既然Spring AOP是通过运行时代理实现的，这个对象永远是一个 被代理（proxied） 对象。</p><p>（7）织入（Weaving）：织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多少个点可以进行织入：</p><ul><li>编译期：切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的。</li><li>类加载期：切面在目标类加载到JVM时被织入。需要特殊的类加载器，它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。</li><li>运行期：切面在应用运行的某个时刻被织入。一般情况下，在织入切面时，AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。</li></ul><h3 id="spring在运行时通知对象">Spring在运行时通知对象</h3><p>通过在代理类中包裹切面，Spring在运行期把切面织入到Spring管理的bean中。代理封装了目标类，并拦截被通知方法的调用，再把调用转发给真正的目标bean。当代理拦截到方法调用时，在调用目标bean方法之前，会执行切面逻辑。</p><p>直到应用需要被代理的bean时，Spring才创建代理对象。如果使用的是ApplicationContext的话，在ApplicationContext从BeanFactory中加载所有bean的时候，Spring才会创建被代理的对象。因为Spring运行时才创建代理对象，所以我们不需要特殊的编译器来织入SpringAOP的切面。</p><h3 id="spring只支持方法级别的连接点">Spring只支持方法级别的连接点</h3><p>因为Spring基于动态代理，所以Spring只支持方法连接点。Spring缺少对字段连接点的支持，而且它不支持构造器连接点。方法之外的连接点拦截功能，我们可以利用Aspect来补充。</p><h3 id="在spring-aop-中关注点和横切关注的区别是什么在-spring-aop-中-concern-和-cross-cutting-concern-的不同之处">在Spring AOP 中，关注点和横切关注的区别是什么？在 spring aop 中 concern 和 cross-cutting concern 的不同之处</h3><p>关注点（concern）是应用中一个模块的行为，一个关注点可能会被定义成一个我们想实现的一个功能。</p><p>横切关注点（cross-cutting concern）是一个关注点，此关注点是整个应用都会使用的功能，并影响整个应用，比如日志，安全和数据传输，几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。</p><h3 id="spring通知有哪些类型">Spring通知有哪些类型？</h3><p>在AOP术语中，切面的工作被称为通知，实际上是程序执行时要通过SpringAOP框架触发的代码段。</p><p>Spring切面可以应用5种类型的通知：</p><ol><li>前置通知（Before）：在目标方法被调用之前调用通知功能；</li><li>后置通知（After）：在目标方法完成之后调用通知，此时不会关心方法的输出是什么；</li><li>返回通知（After-returning ）：在目标方法成功执行之后调用通知；</li><li>异常通知（After-throwing）：在目标方法抛出异常后调用通知；</li><li>环绕通知（Around）：通知包裹了被通知的方法，在被通知的方法调用之前和调用之后执行自定义的行为。</li></ol><blockquote><p>同一个aspect，不同advice的执行顺序：</p><p>①没有异常情况下的执行顺序：</p><p>around before advice<br />before advice<br />target method 执行<br />around after advice<br />after advice<br />afterReturning</p><p>②有异常情况下的执行顺序：</p><p>around before advice<br />before advice<br />target method 执行<br />around after advice<br />after advice<br />afterThrowing:异常发生<br />java.lang.RuntimeException: 异常发生</p></blockquote><h3 id="什么是切面-aspect">什么是切面 Aspect？</h3><p>aspect 由 pointcount 和 advice 组成，切面是通知和切点的结合。它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑编织到切面所指定的连接点中.<br />AOP 的工作重心在于如何将增强编织目标对象的连接点上, 这里包含两个工作:</p><ul><li>如何通过 pointcut 和 advice 定位到特定的 joinpoint 上</li><li>如何在 advice 中编写切面代码.</li></ul><p>可以简单地认为, 使用 @Aspect 注解的类就是切面.</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1635694477206.png" alt="" /></p><h3 id="解释基于xml-schema方式的切面实现">解释基于XML Schema方式的切面实现</h3><p>在这种情况下，切面由常规类以及基于XML的配置实现。</p><h3 id="解释基于注解的切面实现">解释基于注解的切面实现</h3><p>在这种情况下(基于@AspectJ的实现)，涉及到的切面声明的风格与带有java5标注的普通java类一致。</p><h3 id="有几种不同类型的自动代理">有几种不同类型的自动代理？</h3><p>BeanNameAutoProxyCreator</p><p>DefaultAdvisorAutoProxyCreator</p><p>Metadata autoproxying</p><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【java1234】<a href="https://mp.weixin.qq.com/s/M0KnJvDWG17cPdfxy5OmTw">流弊！2021最新 Spring 面试题</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[如何优雅地停止Java进程]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/how-to-stop-the-java-process-gracefully" />
                <id>tag:https://ibit.tech,2021-09-12:how-to-stop-the-java-process-gracefully</id>
                <published>2021-09-12T23:26:51+08:00</published>
                <updated>2021-09-13T08:26:13+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="1-理解停止java进程的本质">1. 理解停止Java进程的本质</h2><p>我们知道，Java程序的运行需要一个运行时环境，即：JVM，启动Java进程即启动了一个JVM。<br />因此，所谓停止Java进程，本质上就是关闭JVM。<br />那么，哪些情况会导致JVM关闭呢？</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631460254874.png" alt="image.png" /></p><h2 id="2-应该如何正确地停止java进程">2. 应该如何正确地停止Java进程</h2><p>通常来讲，停止一个进程只需要杀死进程即可。<br />但是，在某些情况下可能需要在JVM关闭之前执行一些数据保存或者资源释放的工作，此时就不能直接强制杀死Java进程。</p><ul><li>对于正常关闭或异常关闭的几种情况，JVM关闭前，都会调用已注册的关闭钩子，基于这种机制，我们可以将扫尾的工作放在关闭钩子中，进而使我们的应用程序安全的退出。而且，基于平台通用性的考虑，更推荐应用程序使用System.exit(0)这种方式退出JVM。</li><li>对于强制关闭的几种情况：<code>系统关机</code>，操作系统会通知JVM进程等待关闭，一旦等待超时，系统会强制中止JVM进程；而<code>kill -9</code>、<code>Runtime.halt()</code>、<code>断电</code>、<code>系统crash</code>这些方式会直接无商量中止JVM进程，JVM完全没有执行扫尾工作的机会。</li></ul><p>综上所述：</p><ul><li>除非非常确定不需要在Java进程退出之前执行收尾的工作，否则强烈不建议使用<code>kill -9</code>这种简单暴力的方式强制停止Java进程（除了<code>系统关机</code>，<code>系统Crash</code>，<code>断电</code>，和<code>Runtime.halt()</code>我们无能为力之外）。</li><li>不论如何，都应该在Java进程中注册关闭钩子，尽最大可能地保证在Java进程退出之前做一些善后的事情（实际上，大多数时候都需要这样做）。</li></ul><h3 id="21-如何注册关闭钩子">2.1 如何注册关闭钩子</h3><p>在Java中注册关闭钩子通过Runtime类实现：</p><pre><code class="language-java">Runtime.getRuntime().addShutdownHook(new Thread(){    @Override    public void run() {        // 在JVM关闭之前执行收尾工作        // 注意事项：        // 1.在这里执行的动作不能耗时太久        // 2.不能在这里再执行注册，移除关闭钩子的操作        // 3 不能在这里调用System.exit()        System.out.println(&quot;do shutdown hook&quot;);    }});</code></pre><p>为JVM注册关闭钩子的时机不固定，可以在启动Java进程之前，也可以在Java进程之后（如：在监听到操作系统信号量之后再注册关闭钩子也是可以的）。</p><h3 id="22-使用关闭钩子的注意事项">2.2 使用关闭钩子的注意事项</h3><ul><li>关闭钩子本质上是一个线程（也称为Hook线程），对于一个JVM中注册的多个关闭钩子它们将会并发执行，所以JVM并不保证它们的执行顺序；由于是并发执行的，那么很可能因为代码不当导致出现竞态条件或死锁等问题，为了避免该问题，强烈建议只注册一个钩子并在其中执行一系列操作。</li><li>Hook线程会延迟JVM的关闭时间，这就要求在编写钩子过程中必须要尽可能的减少Hook线程的执行时间，避免hook线程中出现耗时的计算、等待用户I/O等等操作。</li><li>关闭钩子执行过程中可能被强制打断，比如在操作系统关机时，操作系统会等待进程停止，等待超时，进程仍未停止，操作系统会强制的杀死该进程，在这类情况下，关闭钩子在执行过程中被强制中止。</li><li>在关闭钩子中，不能执行注册、移除钩子的操作，JVM将关闭钩子序列初始化完毕后，不允许再次添加或者移除已经存在的钩子，否则JVM抛出IllegalStateException异常。</li><li>不能在钩子调用System.exit()，否则卡住JVM的关闭过程，但是可以调用Runtime.halt()。</li><li>Hook线程中同样会抛出异常，对于未捕捉的异常，线程的默认异常处理器处理该异常（将异常信息打印到System.err），不会影响其他hook线程以及JVM正常退出。</li></ul><h3 id="23-信号量机制">2.3 信号量机制</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631460275162.png" alt="image.png" /></p><p>注册关闭钩子的目的是为了在JVM关闭之前执行一些收尾的动作，而从上述描述可以知道，触发关闭钩子动作的执行需要满足JVM正常关闭或异常关闭的情形。<br />显然，我们应该正常关闭JVM（异常关闭JVM的情形不希望发生，也无法百分之百地完全杜绝），即执行：<code>System.exit()</code>，<code>Ctrl + C</code>， <code>kill -15 进程ID</code>。</p><ul><li>System.exit()：通常我们在程序运行完毕之后调用，这是在应用代码中写死的，无法在进程外部进行调用。</li><li>Ctrl + C：如果Java进程运行在操作系统前台，可以通过键盘中断的方式结束运行；但是当进程在后台运行时，就无法通过<code>Ctrl + C</code>方式退出了。</li><li>Kill (-15)SIGTERM信号：使用kill命令结束进程是使用操作系统的信号量机制，不论进程运行在操作系统前台还是后台，都可以通过kill命令结束进程，这也是结束进程使用得最多的方式。</li></ul><p>实际上，大多数情况下的进程结束操作通常是在进程运行过程中需要停止进程或者重启进程，而不是等待进程自己运行结束（服务程序都是一直运行的，并不会主动结束）。也就是说，针对JVM正常关闭的情形，大多数情况是使用<code>kill -15 进程ID</code>的方式实现的。那么，我们是否可以结合操作系统的信号量机制和JVM的关闭钩子实现优雅地关闭Java进程呢？答案是肯定的，具体实现步骤如下：</p><p><strong>第一步：在应用程序中监听信号量</strong></p><p>由于不通的操作系统类型实现的信号量动作存在差异，所以监听的信号量需要根据Java进程实际运行的环境而定（如：Windows使用SIGINT，Linux使用SIGTERM）。</p><pre><code class="language-java">Signal sg = new Signal(&quot;TERM&quot;); // kill -15 pidSignal.handle(sg, new SignalHandler() {    @Override    public void handle(Signal signal) {        System.out.println(&quot;signal handle: &quot; + signal.getName());        // 监听信号量，通过System.exit(0)正常关闭JVM，触发关闭钩子执行收尾工作        System.exit(0);    }});</code></pre><p><strong>第二步：注册关闭钩子</strong></p><pre><code class="language-java">Runtime.getRuntime().addShutdownHook(new Thread(){    @Override    public void run() {        // 执行进程退出前的工作        // 注意事项：        // 1.在这里执行的动作不能耗时太久        // 2.不能在这里再执行注册，移除关闭钩子的操作        // 3 不能在这里调用System.exit()        System.out.println(&quot;do something&quot;);    }});</code></pre><p>完整示例如下：</p><pre><code class="language-java">public class ShutdownTest {    public static void main(String[] args) {        System.out.println(&quot;Shutdown Test&quot;);        Signal sg = new Signal(&quot;TERM&quot;); // kill -15 pid        // 监听信号量        Signal.handle(sg, new SignalHandler() {            @Override            public void handle(Signal signal) {                System.out.println(&quot;signal handle: &quot; + signal.getName());                System.exit(0);            }        });        // 注册关闭钩子        Runtime.getRuntime().addShutdownHook(new Thread(){            @Override            public void run() {                // 在关闭钩子中执行收尾工作                // 注意事项：                // 1.在这里执行的动作不能耗时太久                // 2.不能在这里再执行注册，移除关闭钩子的操作                // 3 不能在这里调用System.exit()                System.out.println(&quot;do shutdown hook&quot;);            }        });        mockWork();        System.out.println(&quot;Done.&quot;);        System.exit(0);    }    // 模拟进程正在运行    private static void mockWork() {        //mockRuntimeException();        //mockOOM();        try {            Thread.sleep(120 * 1000);        } catch (InterruptedException e) {            e.printStackTrace();        }     }    // 模拟在应用中抛出RuntimeException时会调用注册钩子    private static void mockRuntimeException() {        throw new RuntimeException(&quot;This is a mock runtime ex&quot;);    }    // 模拟应用运行出现OOM时会调用注册钩子    // -xms10m -xmx10m    private static void mockOOM() {        List list = new ArrayList();        for(int i = 0; i &lt; 1000000; i++) {            list.add(new Object());        }    }}</code></pre><h2 id="3-总结">3. 总结</h2><p>网上有文章总结说可以直接使用监听信号量的机制来实现优雅地关闭Java进程（详见：<a href="https://blog.csdn.net/carlislelee/article/details/52688693">Java程序优雅关闭的两种方法</a>)，实际上这是有问题的。因为单纯地监听信号量，并不能覆盖到异常关闭JVM的情形（如：RuntimeException或OOM），这种方式与注册关闭钩子的区别在于：</p><ul><li>关闭钩子是在独立线程中运行的，当应用进程被kill的时候main函数就已经结束了，仅会运行ShutdownHook线程中run()方法的代码。</li><li>监听信号量方法中handle函数会在进程被kill时收到TERM信号，但对main函数的运行不会有任何影响，需要使用别的方式结束main函数（如：在main函数中添加布尔类型的flag，当收到TERM信号时修改该flag，程序便会正常结束；或者在handle函数中调用System.exit())。</li></ul><p><strong>【参考】</strong></p><ul><li><a href="https://blog.csdn.net/u011001084/article/details/73480432">https://blog.csdn.net/u011001084/article/details/73480432</a> JVM安全退出（如何优雅的关闭java服务）</li><li><a href="http://yuanke52014.iteye.com/blog/2306805">http://yuanke52014.iteye.com/blog/2306805</a> Java保证程序结束时调用释放资源函数</li><li><a href="https://tessykandy.iteye.com/blog/2005767">https://tessykandy.iteye.com/blog/2005767</a> 基于kill信号优雅的关闭JAVA程序</li><li><a href="https://www.cnblogs.com/taobataoma/archive/2007/08/30/875743.html">https://www.cnblogs.com/taobataoma/archive/2007/08/30/875743.html</a> Linux 信号signal处理机制</li></ul><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【编程随笔】<a href="https://www.cnblogs.com/nuccch/p/10903162.html">如何优雅地停止Java进程</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[持续集成（Continuous integration）]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/continuous-integration" />
                <id>tag:https://ibit.tech,2021-09-12:continuous-integration</id>
                <published>2021-09-12T19:05:45+08:00</published>
                <updated>2021-09-13T08:26:24+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="一基本概念">一、基本概念</h2><h3 id="1持续集成">1、持续集成</h3><p>　　持续集成（Continuous integration，简称CI），简单来说持续集成就是<strong>频繁地</strong>（一天多次）<strong>将代码集成到主干</strong>。</p><p>　　每次集成都通过<strong>自动化的构建</strong>（包括编译、发布、自动化测试）来验证，从而尽快地发现集成错误。</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631444694315.png" alt="image.png" /></p><p>　　持续集成强调开发人员提交了新代码之后，立刻进行构建、（单元）测试。根据测试结果，可以确定新代码和原有代码能否正确地集成在一起。</p><p>　　<strong>持续集成的好处：</strong></p><ul><li>快速发现错误，每完成一点更新，就集成到主干，可以快速发现错误，定位错误也比较容易；</li><li>防止分支大幅偏离主干，如果不经常集成，主干又在不断更新，会导致以后集成的难度变大，甚至难以集成。</li></ul><p>　　<strong>持续集成的目的：</strong>　　</p><p>　　让产品可以快速迭代，同时还能保持高质量。它的核心措施是，代码集成到主干之前，必须通过自动化测试。只要有一个测试用例失败，就不能集成。</p><p>　　持续集成并不能消除 Bug，而是让它们非常容易的发现和改正。</p><h3 id="2持续交付">2、持续交付</h3><p>　　持续交付（Continuous delivery）指的是：频繁地将软件的新版本，交付给质量团队或者用户，以供评审。如果评审通过，代码就进入生产阶段。</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/2_1631445003746.png" alt="2.png" /></p><p>　　持续交付可以看作持续集成的下一步。它强调的是，不管怎么更新，软件是随时随地可以交付的。</p><h3 id="3持续部署">3、持续部署</h3><p>　　持续部署（Continuous deployment）是持续交付的下一步，指的是代码通过评审以后，<strong>自动部署到生产环境</strong>。</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/3_1631445154944.png" alt="3.png" /></p><p>　　持续部署的目标是：代码在任何时候都是可以部署的，可以进入生产阶段，持续部署的前提是能<strong>自动化完成测试、构建、部署</strong>等步骤。</p><h3 id="4持续交付和持续部署的区别">4、持续交付和持续部署的区别</h3><p>　　CD是持续交付和持续部署，但是持续交付不等于持续部署。持续部署则是在持续交付的基础上，把部署到生产环境的过程自动化。具体区别参考下图：</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/4_1631445171390.png" alt="4.png" /></p><h2 id="二持续集成的一般流程">二、持续集成的一般流程</h2><p>　　根据持续集成的设计，代码从提交到生产，整个过程有以下几步：</p><h4 id="1提交">（1）提交</h4><p>　　流程的第一步，是开发者向代码仓库提交代码。所有后面的步骤都始于本地代码的一次提交（commit）。</p><h4 id="2测试第一轮">（2）测试（第一轮）</h4><p>　　代码仓库对commit操作配置了钩子（hook），只要提交代码或者合并进主干，就会跑自动化测试。</p><h4 id="3构建">（3）构建</h4><p>　　通过第一轮测试，代码就可以合并进主干，就算可以交付了。</p><p>　　交付后，就先今夕构建（build），再进入第二轮测试。所谓构建，指的是将源码转换为可以运行的实际代码，比如安装依赖，配置各种资源（样式表、JS脚本、图片）等等。</p><p>　　常用的构建工具主要是：jeknins、Travis、codeship等。</p><h4 id="4测试第二轮">（4）测试（第二轮）</h4><p>　　构建完成，就要进行第二轮测试。如果第一轮已经涵盖了所有测试内容，第二轮可以省略，当然，这时构建步骤也要移到第一轮测试前面。</p><p>　　第二轮是全面测试，单元测试和继承测试都会跑，有条件的话，也要做端对端测试。所有测试以自动化为主，少数无法自动化的测试用例，就要人工跑。</p><h4 id="5部署">（5）部署</h4><p>　　通过第二轮测试，当前代码就是一个可以直接部署的版本（artifact）。将这个版本的所有文件打包（tar filename.tar *）存档，发到生产服务器。</p><p>　　生产服务器将打包文件，解包成本地的一个目录，再讲运行路径的符号链接（symlink）指向这个目录，然后重新启动应用。</p><p>　　这方面的部署工具有Ansible、Chef、Puppet等。</p><h4 id="6回滚">（6）回滚</h4><p>　　一旦当前版本发生问题，就要回滚到上一个版本的构建结果。最简单的做法就是修改一下符号链接，指向上一个版本的目录。</p><h2 id="三认识devops">三、认识DevOps</h2><h3 id="1devops是什么">1、DevOps是什么？</h3><p>　　DevOps 一词的来自于 <strong>Development（开发）</strong> 和 <strong>Operations（运维）</strong> 的组合，突出重视软件开发人员和运维人员的沟通合作，通过自动化流程来使得软件构建、测试、发布更加快捷、频繁和可靠。</p><p>　　目前对 DevOps 有太多的说法和定义，不过它们都有一个共同的思想：“解决开发者与运维者之间曾经不可逾越的鸿沟，增强开发者与运维者之间的沟通和交流”。</p><p>　　DevOps可以用一个公式表示：<strong>文化观念的改变+自动化工具 = 不断适应快速变化的市场</strong>。</p><p>　　<strong>强调：DevOps是一个框架，是一种方法论，并不是一套工具，它包括一系列的基本原则和实践。</strong></p><p>　　<strong>DevOps的核心价值：</strong></p><ul><li>更快速地交付，响应市场的变化；</li><li>更多地关注业务的改进和提升。</li></ul><h3 id="2为什么需要devops">2、为什么需要DevOps?</h3><h4 id="1产品迭代">（1）产品迭代</h4><p>　　在现实工作中，往往都是用户不知道自己想要什么，但是当我们设计完一个产品后，他会告诉我们他们不需要什么，这样我们的产品需要反复的迭代，而且过程可能是曲折的，那我们有什么好办法快速的交付价值，灵活的响应变化呢？</p><p>　　答案就是DevOps，因为DevOps是<strong>面向业务目标，助力业务成功</strong>的最佳实践。</p><h4 id="2技术革新">（2）技术革新</h4><p>　　现在的IT技术架构随着系统的复杂化不断的革新，从最初所有服务在一个系统中，发展到现在的微服务架构、从纯手动操作到全自动流程、从单台物理机到云平台。</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/22_1631445233465.png" alt="22.png" /></p><h3 id="3devops如何落地">3、DevOps如何落地？</h3><p>　　<strong>落实DevOps的指导思想</strong></p><p>　　高效的协作和沟通、自动化流程和工具、迅速敏捷的开发、持续交付和部署、不断学习和创新。</p><p>　　下面是一张来自 devops 经典著作《success with enterprise dev-ops whitepaper》的介绍图。</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/30_1631445244947.png" alt="30.png" /></p><p>　　<strong>敏捷管理</strong>：一支训练有素的敏捷开发团队是成功实施DevOps的关键。</p><p>　　根据<strong>康威定律：软件团队开发的产品是对公司组织架构的反映</strong>。</p><p>　　持续交付部署：实现应用程序的自动化构建、部署、测试和发布。</p><p>　　通过技术工具，把传统的手工操作转变为自动化流程，这不仅有利于提高产品开发、运维部署的效率，还将减少人为因素引起的失误和事故，提早发现问题并及时地解决问题，这样也保证了产品的质量。下图展示了DevOps自动化的流程：</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/301_1631445258784.png" alt="301.png" /></p><p>　　<strong>IT服务管理（ITSM）</strong>，它是传统的“IT管理”转向为“IT服务”为主的一种模式，前者可能更关注具体服务器管理、网络管理和系统软件安装部署等工作；而后者更关注流程的规范化、标准化，明确定义各个流程的目标和范围、成本和效益、运营步骤、关键成功因素和绩效指标、有关人员的责权利，以及各个流程之间的关系等，比如建立线上事故解决流程、服务配置管理流程等；</p><p>　　<strong>精益管理</strong>：建立一个流水线式的 IT 服务链，打通开发与运维的鸿沟，实现开发运维一体化的敏捷模式。</p><p>　　精益生产主要来源于<strong>丰田生产方式（TPS）<strong>的生产哲学，它以降低浪费、提升整体客户价值而闻名，它主要利用优化自动化流程来提高生产率、降低浪费。所以精益生产的精髓是</strong>即时制（JIT）<strong>和</strong>自动化（Jidoka）</strong>。</p><p>　　精益管理贯穿于整个DevOps阶段，它鼓励主动发现问题，不断的优化流程，从而达到<strong>持续交付、快速反馈、降低风险和保障质量</strong>的目的。</p><h3 id="4实施devops的具体方法">4、实施DevOps的具体方法</h3><h4 id="1建立快速敏捷的团队">（1）建立快速敏捷的团队</h4><p>　　按照业务功能划分团队，建立沟通群组，设置产品负责人（一个策划人员）、Scrum Master（我们一般选择测试人员担任，测试驱动开发模式）和开发者团队（前端工程师、后端工程师、测试各一名），形成如下的组织结构和系统构架：</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/41_1631445275061.png" alt="41.png" /></p><h4 id="2实现自动化的流程">（2）实现自动化的流程</h4><p>　　直接看图说话吧，以下为一个完整DevOps的Pipeline：</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/42_1631445285172.png" alt="42.png" /></p><ul><li>提交：工程师将代码在本地测试后，提交到版本控制系统，如Git代码仓库中。</li><li>构建：持续整合系统（如Jenkins CI），在检测到版本控制系统更新时，便自动从Git代码仓库里拉取最新的代码，进行编译、构建。</li><li>单元测试：Jenkins完成编译构建后，会自动执行指定的单元测试代码。</li><li>部署到测试环境：在完成单元测试后，Jenkins可以将应用程序部署到与生产环境相近的测试环境中进行测试。</li><li>预生产环境测试：在预生产测试环境里，可以进行一些最后的自动化测试，例如使用Appium自动化测试工具进行测试，以及与实际情况类似的一些测试可由开发人员或客户人员手动进行测试。</li><li>部署到生产环境：通过所有测试后，便可以使用灰度更新将最新的版本部署到实际生产环境里。</li></ul><h4 id="3devops在落地实施过程中经常会遇到的问题">（3）DevOps在落地实施过程中经常会遇到的问题</h4><p>　　人手紧缺；跨部门协作，前期沟通培训成本高；前期投入工作量大见效少。</p><h3 id="5devops技术栈">5、DevOps技术栈</h3><h4 id="1敏捷管理工具">（1）敏捷管理工具</h4><p>　　Trello、Teambition、Worktile、Tower</p><h4 id="2产品质量管理">（2）产品&amp;质量管理</h4><p>　　confluence、禅道、Jira、Bugzila。</p><p>　　其中 confluence 和禅道主要是产品的需求、定义、依赖和推广等的全面管理工具；而 Jira 和 Bugzilla 是产品的质量管理和监控能力，包括测试用例、缺陷跟踪和质量监控等。</p><p>　　目前使用Jira 和 禅道较多。</p><h4 id="3代码仓库管理">（3）代码仓库管理</h4><p>　　Git、Gitlab、Github.</p><p>　　Git是一个开源的分布式版本控制系统；</p><p>　　Gitlab 和 Github 是用于仓库管理系统的开源项目，它们使用Git作为代码管理工具，并在此基础上搭建起来的web服务。</p><h4 id="4自动化构建脚本">（4）自动化构建脚本</h4><p>　　Gradle、Maven、SBT、ANT</p><h4 id="5虚拟机和容器化">（5）虚拟机和容器化　　</h4><p>　　VMware、VirtualBox、Vagrant、Docker</p><h4 id="6持续集成ci持续部署cd">（6）持续集成(CI)&amp;持续部署(CD)</h4><p>　　Jenkins、Hudson、Travis CI、CircleCI。</p><p>　　Jenkins 是一个开源软件项目，是基于 Java 开发的一种持续集成工具，用于监控持续重复的工作，旨在提供一个开放易用的软件平台，使软件的持续集成变成可能，它的前身为Hudson。</p><p>　　Travis CI 是目前新兴的开源持续集成构建项目，它与 jenkins 很明显的区别在于采用 yaml 格式，简洁清新独树一帜。</p><p>　　CircleCI 是一个 web 应用开发者提供服务的持续集成平台，主要为开发团队提供测试，持续集成，以及代码部署等服务。</p><h4 id="7自动化测试">（7）自动化测试</h4><ul><li><p>Appium</p><p>Appium是一个移动端的自动化框架，可用于测试原生应用，移动网页应用和混合型应用，且是跨平台的。可用于IOS和Android以及firefox的操作系统。</p></li><li><p>Selenium</p><p>Selenium 测试直接在浏览器中运行，就像真实用户所做的一样。Selenium 测试可以在 Windows、Linux 和 Macintosh上的 Internet Explorer、Mozilla 和 Firefox 中运行。</p></li><li><p>Mock测试</p><p>Mock测试就是在测试过程中，对于某些不容易构造或者不容易获取的对象，用一个虚拟的对象来创建以便测试的测试方法。这个虚拟的对象就是Mock对象，Mock对象就是真实对象在调试期间的代替品。Java中的Mock框架常用的有EasyMock和Mockito等。</p></li><li><p>消费者驱动契约测试</p><p>契约测试是一种针对外部服务的接口进行的测试，它能够验证服务是否满足消费方期待的契约。当一些消费方通过接口使用某个组件的提供的行为时，它们之间就产生了契约。这个契约包含了对输入和输出的数据结构的期望，性能以及并发性。而PACT是目前比较流的消费者驱动契约测试框架。</p></li></ul><h4 id="8自动化运维工具">（8）自动化运维工具</h4><ul><li>Ansible</li><li>Puppet</li><li>Chef</li></ul><p>　　IT运维自动化是指将IT运维中日常的、大量的重复性工作自动化，把过去的手工执行转为自动化操作。自动化是IT运维工作的升华，IT运维自动化不单纯是一个维护过程，更是一个管理的提升过程，是IT运维的最高层次，也是未来的发展趋势。下图为常用自动化运维工具对比：</p><p>　　<img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/51_1631445317305.png" alt="51.png" /></p><h4 id="9监控管理工具">（9）监控管理工具</h4><ul><li><p>Zabbix</p><p>Zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级开源解决方案。</p></li><li><p>ELK Stack日志分析系统</p><p>ELK Stack是开源日志处理平台解决方案，背后的商业公司是Elastic。它由日志采集解析工具 Logstash、基于 Lucene 的全文搜索引擎 Elasticsearch、分析可视化平台 Kibana三部分组成。</p></li><li><p>云监控（如Amazon CloudWatch）</p><p>Amazon CloudWatch 是一项针对 AWS 云资源和在 AWS 上运行的应用程序进行监控的服务。您可以使用 Amazon CloudWatch 收集和跟踪各项指标、收集和监控日志文件、设置警报以及自动应对 AWS 资源的更改</p></li></ul><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【休耕】<a href="https://www.cnblogs.com/xiugeng/p/10555847.html">持续集成（Continuous integration）</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Nginx 安装 Lua 支持]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/nginx-lua-supported" />
                <id>tag:https://ibit.tech,2021-09-12:nginx-lua-supported</id>
                <published>2021-09-12T15:14:12+08:00</published>
                <updated>2021-09-13T08:26:38+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>Nginx 支持 Lua 需要安装 lua-nginx-module 模块，一般常用有 2 种方法:</p><ul><li><p>编译 Nginx 的时候带上 lua-nginx-module 模块一起编译</p></li><li><p>使用 OpenResty: Nginx + 一些模块，默认启用了 Lua 支持(推荐使用此方式)</p><blockquote><p><a href="https://openresty.org/cn/openresty.html">OpenResty</a> is just an enhanced version of <a href="https://openresty.org/cn/nginx.html">Nginx</a> by means of addon modules anyway. You can take advantage of all the exisitng goodies in the <a href="https://openresty.org/cn/nginx.html">Nginx</a> world.</p><p>OpenResty® 是一个基于 <a href="https://openresty.org/cn/nginx.html">Nginx</a> 与 Lua 的高性能 Web 平台，其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。</p><p>OpenResty® 通过汇聚各种设计精良的 <a href="https://openresty.org/cn/nginx.html">Nginx</a> 模块（主要由 OpenResty 团队自主开发），从而将 <a href="https://openresty.org/cn/nginx.html">Nginx</a> 有效地变成一个强大的通用 Web 应用平台。这样，Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 <a href="https://openresty.org/cn/nginx.html">Nginx</a> 支持的各种 C 以及 Lua 模块，快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。</p><p>OpenResty® 的目标是让你的Web服务直接跑在 <a href="https://openresty.org/cn/nginx.html">Nginx</a> 服务内部，充分利用 <a href="https://openresty.org/cn/nginx.html">Nginx</a> 的非阻塞 I/O 模型，不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。</p></blockquote></li></ul><h2 id="openresty">OpenResty</h2><p>OpenResty 的安装很方便，Mac 里使用 brew 安装，对于一些常见的 Linux 发行版本，OpenResty® 提供 <a href="https://openresty.org/cn/linux-packages.html">官方预编译包</a>，CentOS 使用 yum，Ubuntu 使用 apt-get，具体请参考 <a href="https://openresty.org/cn/installation.html，以下以">https://openresty.org/cn/installation.html，以下以</a> Mac 和 CentOS 7 中安装 OpenResty 为例。</p><h3 id="mac-使用-openresty">Mac 使用 OpenResty</h3><ul><li><p>终端执行 <code>brew install homebrew/nginx/openresty</code> 把 OpenResty 安装到 <strong>/usr/local/Cellar/openresty</strong></p></li><li><p>配置文件位于 <strong>/usr/local/etc/openresty/nginx.conf</strong> (可执行 <code>openresty -V</code> 从输出中找到)</p></li><li><p>启动 Nginx，2 种启动方式都可以</p><ul><li><code>sudo openresty</code> (openresty 其实就是 nginx 的软连接)</li><li><code>sudo nginx</code> (把 /usr/local/Cellar/openresty/1.11.2.5/nginx/sbin 添加到 PATH 里，注意不同版本时的路径不同)</li><li>查看是否启动了 nginx: <code>ps aux | grep nginx</code></li></ul></li><li><p>测试是否支持 Lua</p><ol><li><p><strong>/usr/local/etc/openresty/nginx.conf</strong> 中添加</p><pre><code>location /lua {    default_type 'text/html';    content_by_lua 'ngx.say(&quot;hello world&quot;);';}</code></pre></li><li><p><code>sudo nginx -t</code> 测试配置没问题，然后 <code>sudo nginx -s reload</code> 重新加载配置 (<code>sudo nginx -s stop</code> 关闭 Nginx)</p></li><li><p><code>curl http://localhost/lua</code> 输出 <strong>hello world</strong> 则说明 Nginx 支持 Lua</p></li></ol></li></ul><h3 id="centos-7-使用-openresty">CentOS 7 使用 OpenResty</h3><ul><li><p>终端执行下面 3 条命令把 OpenResty 安装到 <strong>/usr/local/openresty</strong></p><p><code>sudo yum install yum-utils</code></p><p><code>sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo</code></p><p><code>sudo yum install openresty</code></p></li><li><p>Nginx 的配置文件位于 <strong>/usr/local/openresty/nginx/conf/nginx.conf</strong> (openresty -V 中没有指定)</p></li><li><p>启动 Nginx，2 种启动方式都可以</p><ul><li><code>sudo openresty</code></li><li><code>sudo nginx</code></li><li>查看是否启动了 nginx: <code>ps -ef | grep nginx</code></li></ul></li><li><p>测试是否支持 Lua: 参考上面的方法</p></li></ul><h2 id="编译-nginx--lua">编译 Nginx + Lua</h2><blockquote><p>编译 Nginx 需要先准备好下面的这些工具，如果不确定是否已安装，可以在编译的时候根据出现的错误提示再进行安装</p><ul><li><code>yum install -y gcc g++ gcc-c++</code></li><li><code>yum -y install zlib zlib-devel openssl openssl--devel pcre pcre-devel</code></li></ul></blockquote><p>Nginx 支持 Lua 需要依赖 LuaJIT-2.0.4.tar.gz，ngx_devel_kit，lua-nginx-module，下面介绍具体的编译过程 (都下载到 /root 目录)</p><ol><li><p>下载安装 <strong>LuaJIT-2.0.4.tar.gz</strong></p><pre><code>wget -c http://luajit.org/download/LuaJIT-2.0.4.tar.gztar xzvf LuaJIT-2.0.4.tar.gzcd LuaJIT-2.0.4make install PREFIX=/usr/local/luajit# 添加环境变量export LUAJIT_LIB=/usr/local/luajit/libexport LUAJIT_INC=/usr/local/luajit/include/luajit-2.0</code></pre></li><li><p>下载解压 <strong>ngx_devel_kit</strong></p><pre><code>wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gztar -xzvf v0.3.0.tar.gz</code></pre></li><li><p>下载解压 <strong>lua-nginx-module</strong></p><pre><code>wget https://github.com/openresty/lua-nginx-module/archive/v0.10.8.tar.gztar -xzvf v0.10.8.tar.gz</code></pre></li><li><p>下载安装 <strong>nginx-1.10.3.tar.gz</strong></p><pre><code>wget http://nginx.org/download/nginx-1.10.3.tar.gztar -xzvf nginx-1.10.3.tar.gzcd nginx-1.10.3# 注意ngx_devel_kit和lua-nginx-module 以实际解压路径为准./configure --add-module=/root/ngx_devel_kit-0.3.0 --add-module=/root/lua-nginx-module-0.10.8make -j2make install</code></pre></li><li><p>支持 Nginx 被安装到了 <strong>/usr/local/nginx</strong>，配置文件为 <strong>/usr/local/nginx/conf/nginx.conf</strong></p></li><li><p>验证</p><ul><li><p>将 nginx 做成命令: <code>ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx</code></p></li><li><p><strong>/usr/local/nginx/conf/nginx.conf</strong> 中添加 Lua 测试代码</p><pre><code>location /lua {    default_type 'text/html';    content_by_lua 'ngx.say(&quot;hello world&quot;);';}</code></pre></li><li><p>启动 Nginx: <code>sudo nginx</code></p></li><li><p><code>curl http://localhost/lua</code> 输出 <strong>hello world</strong> 则说明 Nginx 支持 Lua</p></li></ul></li></ol><p>上面编译 Nginx 的内容来源于 <a href="http://www.cnblogs.com/aoeiuv/p/6856056.html，编译">http://www.cnblogs.com/aoeiuv/p/6856056.html，编译</a> Nginx 相对使用 OpenResty 麻烦一些，不过也不难，根据自己的喜好选择即可。</p><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【公孙二狗】<a href="https://qtdebug.com/mac-nginx-lua/">Nginx 安装 Lua 支持</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[互联网公司使用 Redis 的16个应用场景]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/redis-16-usage-situation" />
                <id>tag:https://ibit.tech,2021-09-10:redis-16-usage-situation</id>
                <published>2021-09-10T23:31:56+08:00</published>
                <updated>2021-09-13T08:26:48+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="1缓存">1、缓存</h3><p>String类型</p><p>例如：热点数据缓存（例如报表、明星出轨），对象缓存、全页缓存、可以提升热点数据的访问数据。</p><h3 id="2数据共享分布式">2、数据共享分布式</h3><p>String 类型，因为 Redis 是分布式的独立服务，可以在多个应用之间共享</p><p>例如：分布式Session</p><pre><code>&lt;dependency&gt;  &lt;groupId&gt;org.springframework.session&lt;/groupId&gt;  &lt;artifactId&gt;spring-session-data-redis&lt;/artifactId&gt; &lt;/dependency&gt;</code></pre><h3 id="3分布式锁">3、分布式锁</h3><p>String 类型setnx方法，只有不存在时才能添加成功，返回true</p><pre><code>public static boolean getLock(String key) {    Long flag = jedis.setnx(key, &quot;1&quot;);    if (flag == 1) {        jedis.expire(key, 10);    }    return flag == 1;}public static void releaseLock(String key) {    jedis.del(key);}</code></pre><h3 id="4全局id">4、全局ID</h3><p>int类型，incrby，利用原子性</p><pre><code>incrby userid 1000</code></pre><p>分库分表的场景，一次性拿一段。</p><h3 id="5计数器">5、计数器</h3><p>int类型，incr方法</p><p>例如：文章的阅读量、微博点赞数、允许一定的延迟，先写入Redis再定时同步到数据库</p><h3 id="6限流">6、限流</h3><p>int类型，incr方法</p><p>以访问者的ip和其他信息作为key，访问一次增加一次计数，超过次数则返回false</p><h3 id="7位统计">7、位统计</h3><p>String类型的bitcount（1.6.6的bitmap数据结构介绍）</p><p>字符是以8位二进制存储的</p><pre><code>set k1 asetbit k1 6 1setbit k1 7 0get k1 /* 6 7 代表的a的二进制位的修改a 对应的ASCII码是97，转换为二进制数据是01100001b 对应的ASCII码是98，转换为二进制数据是01100010因为bit非常节省空间（1 MB=8388608 bit），可以用来做大数据量的统计。*/</code></pre><p>例如：在线用户统计，留存用户统计</p><pre><code>setbit onlineusers 01 setbit onlineusers 11 setbit onlineusers 20</code></pre><p>支持按位与、按位或等等操作</p><pre><code>BITOPANDdestkeykey[key...] ，对一个或多个 key 求逻辑并，并将结果保存到 destkey 。       BITOPORdestkeykey[key...] ，对一个或多个 key 求逻辑或，并将结果保存到 destkey 。 BITOPXORdestkeykey[key...] ，对一个或多个 key 求逻辑异或，并将结果保存到 destkey 。 BITOPNOTdestkeykey ，对给定 key 求逻辑非，并将结果保存到 destkey 。BITOP &quot;AND&quot; &quot;7_days_both_online_users&quot; &quot;day_1_online_users&quot; &quot;day_2_online_users&quot; ...  &quot;day_7_online_users&quot;</code></pre><h3 id="8购物车">8、购物车</h3><p>String 或hash。所有String可以做的hash都可以做</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631287758044.png" alt="image.png" /></p><pre><code>key：用户id；field：商品id；value：商品数量。+1：hincr。-1：hdecr。删除：hdel。全选：hgetall。商品数：hlen。</code></pre><h3 id="9用户消息时间线timeline">9、用户消息时间线timeline</h3><p>list，双向链表，直接作为timeline就好了。插入有序</p><h3 id="10消息队列">10、消息队列</h3><p>List提供了两个阻塞的弹出操作：blpop/brpop，可以设置超时时间</p><ul><li>blpop：blpop key1 timeout 移除并获取列表的第一个元素，如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。</li><li>brpop：brpop key1 timeout 移除并获取列表的最后一个元素，如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。</li></ul><p>上面的操作。其实就是java的阻塞队列。学习的东西越多。学习成本越低</p><ul><li>队列：先进先除：rpush blpop，左头右尾，右边进入队列，左边出队列</li><li>栈：先进后出：rpush brpop</li></ul><h3 id="11抽奖">11、抽奖</h3><p>自带一个随机获得值</p><pre><code>spop myset</code></pre><h3 id="12点赞签到打卡">12、点赞、签到、打卡</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631287783610.png" alt="image.png" /></p><p>假如上面的微博ID是t1001，用户ID是u3001</p><p>用 like:t1001 来维护 t1001 这条微博的所有点赞用户</p><ul><li>点赞了这条微博：sadd like:t1001 u3001</li><li>取消点赞：srem like:t1001 u3001</li><li>是否点赞：sismember like:t1001 u3001</li><li>点赞的所有用户：smembers like:t1001</li><li>点赞数：scard like:t1001</li></ul><h3 id="13商品标签">13、商品标签</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631287831547.png" alt="image.png" /></p><p>老规矩，用 tags:i5001 来维护商品所有的标签。</p><ul><li>sadd tags:i5001 画面清晰细腻</li><li>sadd tags:i5001 真彩清晰显示屏</li><li>sadd tags:i5001 流程至极</li></ul><h3 id="14商品筛选">14、商品筛选</h3><pre><code>// 获取差集sdiff set1 set2// 获取交集（intersection ）sinter set1 set2// 获取并集sunion set1 set2</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631287869851.png" alt="image.png" /></p><p>假如：iPhone11 上市了</p><pre><code>sadd brand:apple iPhone11sadd brand:ios iPhone11sad screensize:6.0-6.24 iPhone11sad screentype:lcd iPhone 11</code></pre><p>赛选商品，苹果的、ios的、屏幕在6.0-6.24之间的，屏幕材质是LCD屏幕</p><pre><code>sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd</code></pre><h3 id="15用户关注推荐模型">15、用户关注、推荐模型</h3><p>follow 关注 fans 粉丝</p><p>相互关注：</p><pre><code>sadd 1:follow 2sadd 2:fans 1sadd 1:fans 2sadd 2:follow 1</code></pre><p>我关注的人也关注了他(取交集)：</p><pre><code>sinter 1:follow 2:fans</code></pre><p>可能认识的人：</p><pre><code>用户1可能认识的人(差集)：sdiff 2:follow 1:follow用户2可能认识的人：sdiff 1:follow 2:follow</code></pre><h3 id="16排行榜">16、排行榜</h3><p>id 为6001 的新闻点击数加1：zincrby hotNews:20190926 1 n6001</p><p>获取今天点击最多的15条：zrevrange hotNews:20190926 0 15 withscores</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631287888518.png" alt="image.png" /></p><h3 id="版权说明"><font color='Red'>版权说明</font></h3><p>本文转载于【DevOps技术栈】<a href="https://mp.weixin.qq.com/s/uHUqDWj1IcZ8jgzZP-lI2A">互联网公司使用 Redis 的16个应用场景</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Nginx的location、root、alias指令用法和区别]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/nginx-location-root-alias" />
                <id>tag:https://ibit.tech,2021-09-05:nginx-location-root-alias</id>
                <published>2021-09-05T18:45:55+08:00</published>
                <updated>2021-09-13T08:27:14+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>nginx指定文件路径有两种方式root和alias，指令的使用方法和作用域：</p><p><strong>[root]</strong></br><br />语法：<font color='Green'>root path</font></br><br />默认值：<font color='Green'>root html</font></br><br />配置段：<font color='Green'>http、server、location、if</font></p><p><strong>[alias]</strong></br><br />语法：<font color='Green'>alias path</font></br><br />配置段：<font color='Green'>location</font></p><p>root与alias主要区别在于nginx如何解释location后面的uri，这会使两者分别以不同的方式将请求映射到服务器文件上。</p><ul><li>root的处理结果是：root路径＋location路径</li><li>alias的处理结果是：使用alias路径替换location路径</li><li>alias是一个目录别名的定义，root则是最上层目录的定义。 还有一个重要的区别是alias后面必须要用“/”结束，否则会找不到文件的。而root则可有可无~~</li></ul><p>root实例：</p><pre><code class="language-nginx">location ^~ /t/ {     root /www/root/html/;}</code></pre><p>如果一个请求的URI是/t/a.html时，web服务器将会返回服务器上的/www/root/html/t/a.html的文件。</p><p>alias实例：</p><pre><code class="language-nginx">location ^~ /t/ { alias /www/root/html/new_t/;}</code></pre><p>如果一个请求的URI是<code>/t/a.html</code>时，web服务器将会返回服务器上的<code>/www/root/html/new_t/a.html</code>的文件。注意这里是<code>new_t</code>，因为 alias 会把 location 后面配置的路径丢弃掉，把当前匹配到的目录指向到指定的目录。</p><p><strong>注意：</strong></p><ul><li>使用alias时，目录名后面一定要加&quot;/&quot;。</li><li>alias在使用正则匹配时，必须捕捉要匹配的内容并在指定的内容处使用。</li><li>alias只能位于location块中。（root可以不放在location中）</li></ul><p><font color='Red'>版权申明</font></p><p>本文转载于【华创信息技术】<a href="https://www.cnblogs.com/lywJ/p/10710361.html">nginx安装及其配置详细教程</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[nginx安装及其配置详细教程]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/nginx-setup-and-config" />
                <id>tag:https://ibit.tech,2021-09-04:nginx-setup-and-config</id>
                <published>2021-09-04T18:26:09+08:00</published>
                <updated>2021-09-13T08:27:01+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="1-nginx-介绍">1 nginx 介绍</h2><h3 id="11-什么是nginx">1.1 什么是nginx</h3><p>Nginx是一款高性能的http 服务器/反向代理服务器及电子邮件（IMAP/POP3）代理服务器。</p><p>由俄罗斯的程序设计师Igor Sysoev所开发，官方测试nginx能够支支撑5万并发链接，</p><p>并且cpu、内存等资源消耗却非常低，运行非常稳定。</p><h3 id="12-应用场景">1.2 应用场景</h3><ol><li><p>http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。</p></li><li><p>虚拟主机。可以实现在一台服务器虚拟出多个网站。例如个人网站使用的虚拟主机。</p></li><li><p>反向代理，负载均衡。当网站的访问量达到一定程度后，单台服务器不能满足用户的请求时，需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载，不会因为某台服务器负载高宕机而某台服务器闲置的情况。</p></li></ol><h2 id="2-nginx安装">2 nginx安装</h2><h3 id="22-下载">2.2 下载</h3><p>官方网址：<a href="http://nginx.org/en/download.html">http://nginx.org/en/download.html</a></p><p>官网提供三种版本：</p><p>Nginx官网提供了三个类型的版本<br />Mainline version：Mainline 是 Nginx 目前主力在做的版本，可以说是开发版<br />Stable version：最新稳定版，生产环境上建议使用的版本<br />Legacy versions：遗留的老版本的稳定版</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631440664407.png" alt="image.png" /></p><p>我们这里下载的是Stable version下面的</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631440685756.png" alt="image.png" /></p><p>使用的版本是1.14.0.tar.gz.</p><h3 id="22-安装要求的环境">2.2 安装要求的环境</h3><p>下面的环境需要视自己的系统情况而定，没有的环境安装以下就好。</p><p><strong>1. 需要安装gcc环境</strong></p><pre><code># yum install gcc-c++</code></pre><p><strong>2. 第三方的开发包</strong></p><p><strong>a. PERE</strong></p><p>PCRE(Perl Compatible Regular Expressions)是一个Perl库，包括 perl 兼容的正则表达式库。</p><p>nginx的http模块使用pcre来解析正则表达式，所以需要在linux上安装pcre库。</p><p><strong>注：pcre-devel是使用pcre开发的一个二次开发库。nginx****也需要此库</strong>。</p><pre><code># yum install -y pcre pcre-devel</code></pre><p><strong>b. zlib</strong></p><p>zlib库提供了很多种压缩和解压缩的方式，nginx使用zlib对http包的内容进行gzip，所以需要在linux上安装zlib库。</p><pre><code># yum install -y zlib zlib-devel</code></pre><p><strong>c. openssl</strong></p><p>OpenSSL 是一个强大的安全套接字层密码库，囊括主要的密码算法、常用的密钥和证书封装管理功能及SSL协议，</p><p>并提供丰富的应用程序供测试或其它目的使用。</p><p>nginx不仅支持http协议，还支持https（即在ssl协议上传输http），所以需要在linux安装openssl库。</p><pre><code># yum -y install pcre  pcre-devel zlib  zlib-devel openssl openssl-devel</code></pre><h3 id="23-nginx安装过程">2.3 nginx安装过程</h3><p><strong>1. 把nginx源码包上传到linux系统上</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631440712607.png" alt="image.png" /></p><p><strong>2. 解压到/usr/local下面</strong></p><pre><code># tar -xvf nginx-1.14.0.tar.gz -C /usr/local</code></pre><p><strong>3. 使用cofigure命令创建一个makeFile文件</strong></p><p><strong>执行下面的命令的时候，一定要进入到nginx-1.14.0目录里面去。</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631440928656.png" alt="image.png" /></p><pre><code>./configure \--prefix=/usr/local/nginx \--pid-path=/var/run/nginx/nginx.pid \--lock-path=/var/lock/nginx.lock \--error-log-path=/var/log/nginx/error.log \--http-log-path=/var/log/nginx/access.log \--with-http_gzip_static_module \--http-client-body-temp-path=/var/temp/nginx/client \--http-proxy-temp-path=/var/temp/nginx/proxy \--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \--http-scgi-temp-path=/var/temp/nginx/scgi \--with-http_stub_status_module \--with-http_ssl_module \--with-file-aio \--with-http_realip_module</code></pre><p>如果没有makeFile文件，编译的时候会报错</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631440776131.png" alt="image.png" /></p><p><strong>\</strong> 表示命令还没有输入完，换行的意思。</p><pre><code>--prefix=/usr/local/nginx  表示软件安装到/usr/local/nginx下面。这个make install 的时候就不用在指定安装路径。执行完成后查看目录里面已经多了一个Makefile文件</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441188553.png" alt="image.png" /></p><p><strong>注意：启动nginx之前，上边将临时文件目录指定为/var/temp/nginx，</strong></p><pre><code>需要在/var下创建temp及nginx目</code></pre><p><strong>4. 创建目录/var/temp/nginx/</strong></p><pre><code># mkdir /var/temp/nginx -p</code></pre><p>-p 表示级联创建的意思</p><p><strong>5. 进入nginx-1.14.0里面执行make命令进行编译</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441205634.png" alt="image.png" /></p><p><strong>6. 进入nginx-1.14.0里面执行make install 命令进行安装</strong></p><p>这里不需要再次执行安装路径，创建makefile文件的时候已经指定了。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441222750.png" alt="image.png" /></p><p><strong>7. 进入安装位置/usr/local/nginx查看目录结构</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441235983.png" alt="image.png" /></p><p>其中html是里面首页html文件。conf里面是配置文件。sbin里面只执行文件。</p><h2 id="3-启动nginx">3 启动nginx</h2><p>进入sbin目录，执行命令./nginx</p><pre><code>[root@admin sbin]# ./nginx</code></pre><h2 id="4-查看nginx是否启动">4 查看nginx是否启动</h2><pre><code>[root@admin sbin]# ps -aux | grep nginx</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441256933.png" alt="image.png" /></p><p><strong>ps命令</strong>用于报告当前系统的进程状态。</p><p>-a：显示所有终端机下执行的程序，除了阶段作业领导者之外。</p><p><strong>参数说明：</strong></p><pre><code>a：显示现行终端机下的所有程序，包括其他用户的程序。u：以用户为主的格式来显示程序状况。x：显示所有程序，不以终端机来区分。</code></pre><h2 id="5-关闭nginx">5 关闭nginx</h2><pre><code>[root@admin sbin]#  ./nginx -s stop</code></pre><p>或者</p><pre><code>[root@admin sbin]# ./nginx -s quit</code></pre><h2 id="6-重启nginx">6 重启nginx</h2><p>先关闭，然后启动</p><h2 id="7-刷新配置文件">7 刷新配置文件</h2><pre><code>[root@admin sbin]# ./nginx -s reload</code></pre><h2 id="8-关闭防火墙开启远程访问">8 关闭防火墙，开启远程访问</h2><p>首先需要关闭防火墙：默认端口是80</p><p><strong>方法一：永久开放80端口</strong></p><pre><code>/sbin/iptables -I INPUT -p tcp --dport 80 -j ACCEPT/etc/rc.d/init.d/iptables save</code></pre><p><strong>方法二：临时关闭系统防火墙</strong></p><pre><code># service iptables stop  </code></pre><p><strong>方法三：永久关闭修改配置开机不启动防火墙</strong></p><pre><code># chkconfig iptables off </code></pre><p><strong>特殊：针对阿里云</strong></p><p>需要添加安全组规则</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441317157.png" alt="image.png" /></p><h2 id="9-访问nginx">9 访问nginx</h2><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441329685.png" alt="image.png" /></p><h2 id="10-配置虚拟主机">10 配置虚拟主机</h2><p>就是在一台服务器启动多个网站。</p><p>如何区分不同的网站：主要有以下两种方式</p><p>方式一：端口不同</p><p>方式二：域名不同</p><h2 id="11-通过端口区分不同的主机">11 通过端口区分不同的主机</h2><p>nginx配置文件的位置：/usr/local/nginx/conf/nginx.conf</p><p>原始配置文件的内容如下：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441357636.png" alt="image.png" /></p><p>我们可以通过配置多个server,从而配置多个虚拟机</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441396321.png" alt="image.png" /></p><p>下面测试以下：复制原来的html目录，改名为html-81</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441426580.png" alt="image.png" /></p><p>修改以下里面的index.html文件，方便区分</p><pre><code>[root@admin nginx]# vim html-81/index.html</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441499507.png" alt="image.png" /></p><p>修改完成之后刷新以下配置文件</p><pre><code>[root@admin sbin]# ./nginx -s reload</code></pre><p>然后分别访问192.168.204.131:80 和192.168.204.131:81</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441566900.png" alt="image.png" /></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441597065.png" alt="image.png" /></p><h2 id="12-多个域名区分虚拟主机">12 多个域名区分虚拟主机</h2><h3 id="121-什么是域名">12.1 什么是域名</h3><p>域名就是网站：<a href="http://www.baidu.com就是域名">www.baidu.com就是域名</a></p><p>DNS域名解析服务器，把域名解析为ip地址。保存的就是域名和ip地址的映射关系。</p><p>一级域名：baidu.com</p><p>二级域名：<a href="http://www.baidu.com">www.baidu.com</a></p><p>三级域名：image.baidu.com</p><p>一个域名对应与一个ip地址，一个ip地址可以被多个域名绑定。</p><p>只需要买一个一级域名，后面的二级，三级域名你自己可以随便定义。</p><p>本地测试我们可以通过修改hosts配置文件来完成：</p><p>hosts文件的位置：C:\Windows\System32\drivers\etc</p><p>可以自己手动配置域名和ip的映射关系，如果hosts文件中配置了域名和ip的对应关系，不需要走DNS域名解析服务器。</p><p>因为拿到一个域名，首先是到hosts文件里面查找，没有才有去DNS域名解析器查找。</p><h3 id="122-nginx配置">12.2 nginx配置</h3><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441653485.png" alt="image.png" /></p><h3 id="123-测试">12.3 测试</h3><p><strong>1. 修改本地hosts配置文件</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441677733.png" alt="image.png" /></p><p><strong>2. 复制html目录，分别改名为html-taobao和html-baidu</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441688078.png" alt="image.png" /></p><p><strong>3. 分别修改html-baidu和html-taobao里面的index.html文件，方便区分</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441701761.png" alt="image.png" /></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441711419.png" alt="image.png" /></p><p><strong>4. 刷新配置文件</strong></p><pre><code>[root@admin sbin]# ./nginx -s reload</code></pre><p><strong>5. 然后使用浏览器分别访问：<a href="http://www.taobao.com">www.taobao.com</a> 和 <a href="http://www.baidu.com">www.baidu.com</a></strong></p><h2 id="13-正向代理">13 正向代理</h2><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441724987.png" alt="image.png" /></p><h2 id="14-反向代理">14 反向代理</h2><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441735629.png" alt="image.png" /></p><p>反向代理服务器决定那台服务器提供服务</p><h2 id="15-nginx实现反向代理">15 nginx实现反向代理</h2><p>两个域名指向同一台nginx服务器，用户访问不同的域名显示不同的网页内容。</p><p>两个域名是<a href="http://www.baidu.com和www.taobao.com">www.baidu.com和www.taobao.com</a></p><p>nginx代理服务器使用虚拟机192.168.204.131</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441747347.png" alt="image.png" /></p><p>第一步：安装两个tomcat，分别运行在8080和8081端口。</p><p>第二步：启动两个tomcat。</p><p>第三步：反向代理服务器的配置</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441759191.png" alt="image.png" /></p><p>第四步：nginx重新加载配置文件</p><p>第五步：配置域名</p><p>在hosts文件中添加域名和ip的映射关系</p><pre><code>192.168.204.131 www.baidu.com192.168.204.131 www.taobao.com</code></pre><h2 id="16-负载均衡">16 负载均衡</h2><p>如果一个服务由多个服务器提供，需要把负载分配到不同的服务器处理，需要负载均衡。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441787458.png" alt="image.png" /></p><p>可以根据服务器的实际情况调整服务器权重。权重越高分配的请求越多，权重越低，请求越少。默认是都是1</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631441799820.png" alt="image.png" /></p><h2 id="17-1-设置nginx开机自启动centos65">17-1 设置nginx开机自启动（centos6.5）</h2><p>每次启动nginx服务都需要到安装目录下的/sbin下面，感觉挺麻烦的。</p><p>下面介绍一下如何在Linux(CentOS)系统上，设置nginx开机自启动。</p><h3 id="171-用脚本管理nginx服务">17.1 用脚本管理nginx服务</h3><p><strong>第一步：在/etc/init.d/目录下创建nginx文件，命令如下：</strong></p><pre><code># touch /etc/init.d/nginx</code></pre><p><strong>第二步：在创建的nginx文件中加入下面的内容</strong></p><p>首先执行命令：</p><pre><code># vim /etc/init.d/nginx</code></pre><p>然后加下面的内容复制到nginx配置文件中</p><pre><code>#!/bin/sh## nginx - this script starts and stops the nginx daemon## chkconfig:   - 85 15# description:  NGINX is an HTTP(S) server, HTTP(S) reverse \#               proxy and IMAP/POP3 proxy server# processname: nginx# config:      /etc/nginx/nginx.conf# config:      /etc/sysconfig/nginx# pidfile:     /var/run/nginx.pid# Source function library.. /etc/rc.d/init.d/functions# Source networking configuration.. /etc/sysconfig/network# Check that networking is up.[ &quot;$NETWORKING&quot; = &quot;no&quot; ] &amp;&amp; exit 0nginx=&quot;/usr/sbin/nginx&quot;prog=$(basename $nginx)NGINX_CONF_FILE=&quot;/etc/nginx/nginx.conf&quot;[ -f /etc/sysconfig/nginx ] &amp;&amp; . /etc/sysconfig/nginxlockfile=/var/lock/subsys/nginxmake_dirs() {   # make required directories   user=`$nginx -V 2&gt;&amp;1 | grep &quot;configure arguments:&quot; | sed 's/[^*]*--user=\([^ ]*\).*/\1/g' -`   if [ -z &quot;`grep $user /etc/passwd`&quot; ]; then       useradd -M -s /bin/nologin $user   fi   options=`$nginx -V 2&gt;&amp;1 | grep 'configure arguments:'`   for opt in $options; do       if [ `echo $opt | grep '.*-temp-path'` ]; then           value=`echo $opt | cut -d &quot;=&quot; -f 2`           if [ ! -d &quot;$value&quot; ]; then               # echo &quot;creating&quot; $value               mkdir -p $value &amp;&amp; chown -R $user $value           fi       fi   done}start() {    [ -x $nginx ] || exit 5    [ -f $NGINX_CONF_FILE ] || exit 6    make_dirs    echo -n $&quot;Starting $prog: &quot;    daemon $nginx -c $NGINX_CONF_FILE    retval=$?    echo    [ $retval -eq 0 ] &amp;&amp; touch $lockfile    return $retval}stop() {    echo -n $&quot;Stopping $prog: &quot;    killproc $prog -QUIT    retval=$?    echo    [ $retval -eq 0 ] &amp;&amp; rm -f $lockfile    return $retval}restart() {    configtest || return $?    stop    sleep 1    start}reload() {    configtest || return $?    echo -n $&quot;Reloading $prog: &quot;    killproc $nginx -HUP    RETVAL=$?    echo}force_reload() {    restart}configtest() {  $nginx -t -c $NGINX_CONF_FILE}rh_status() {    status $prog}rh_status_q() {    rh_status &gt;/dev/null 2&gt;&amp;1}case &quot;$1&quot; in    start)        rh_status_q &amp;&amp; exit 0        $1        ;;    stop)        rh_status_q || exit 0        $1        ;;    restart|configtest)        $1        ;;    reload)        rh_status_q || exit 7        $1        ;;    force-reload)        force_reload        ;;    status)        rh_status        ;;    condrestart|try-restart)        rh_status_q || exit 0            ;;    *)        echo $&quot;Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}&quot;        exit 2esac</code></pre><p>上面的脚本文件并不是自己写的，是nginx官方提供的。</p><p>地址：<a href="http://wiki.nginx.org/RedHatNginxInitScript">http://wiki.nginx.org/RedHatNginxInitScript</a></p><p>注意：如果是自定义安装的nginx,修改根据实际情况修改安装路和配置文件。</p><pre><code>nginx=&quot;/usr/sbin/nginx&quot; 修改成你的nginx执行程序的路径。比如我的是nginx=&quot;/usr/local/nginx/sbin/nginx&quot;NGINX_CONF_FILE=&quot;/etc/nginx/nginx.conf&quot; 修改成你的配置文件的路径例如：NGINX_CONF_FILE=&quot;/usr/local/nginx/nginx.conf</code></pre><p>修改完成后保存脚本文件，wq 保存并退出</p><p><strong>第三步：设置nginx文件的权限</strong></p><pre><code># chmod a+x /etc/init.d/nginx</code></pre><p>解释：a+x==&gt;all user can execute 所有用户可执行）的意思</p><p><strong>第四步：管理脚本</strong></p><p>到这里，我们就可以使用nginx脚本对服务进行管理了</p><pre><code># /etc/init.d/nginx start      启动服务# /etc/init.d/nginx stop       停止服务  # /etc/init.d/nginx restart    重启服务# /etc/init.d/nginx status     查看服务的状态# /etc/init.d/nginx reload     刷新配置文件</code></pre><h3 id="172-使用chkconfig管理">17.2 使用chkconfig管理</h3><p>上面的方法完成了用脚本管理nginx服务的功能，但是还是不太方便，比如要设置nginx开机启动等。</p><p>这个时候我们可以使用chkconfig来进行管理。</p><p><strong>第一步：将nginx服务加入chkconfig管理列表</strong></p><pre><code># chkconfig --add /etc/init.d/nginx</code></pre><p><strong>第二步：使用service管理服务</strong></p><pre><code># service nginx start    启动服务# service nginx stop     停止服务# service nginx restart  重启服务# service nginx status   查询服务的状态# service nginx relaod   刷新配置文</code></pre><p><strong>第三步：设置终端模式开机启动</strong></p><pre><code># chkconfig nginx on</code></pre><h2 id="17-2-设置nginx开机自启动centos74">17-2 设置nginx开机自启动（centos7.4）</h2><p><strong>第一步：进入到/lib/systemd/system/目录</strong></p><pre><code>[root@iz2z init.d]# cd /lib/systemd/system/</code></pre><p><strong>第二步：创建nginx.service文件，并编辑</strong></p><pre><code># vim nginx.service</code></pre><p>内如如下：</p><pre><code>[Unit]Description=nginx serviceAfter=network.target    [Service] Type=forking ExecStart=/usr/local/nginx/sbin/nginxExecReload=/usr/local/nginx/sbin/nginx -s reloadExecStop=/usr/local/nginx/sbin/nginx -s quitPrivateTmp=true    [Install] WantedBy=multi-user.target</code></pre><p><strong>[Unit]: 服务的说明</strong></p><pre><code>Description:描述服务After:描述服务类别</code></pre><p><strong>[Service]服务运行参数的设置</strong></p><pre><code>Type=forking是后台运行的形式ExecStart为服务的具体运行命令ExecReload为重启命令ExecStop为停止命令PrivateTmp=True表示给服务分配独立的临时空间</code></pre><p><strong>注意：</strong>[Service]的启动、重启、停止命令全部要求使用绝对路径<br />[Install]运行级别下服务安装的相关设置，可设置为多用户，即系统运行级别为3</p><p>保存退出。</p><p><strong>第三步：加入开机自启动</strong></p><pre><code># systemctl enable nginx</code></pre><p>如果不想开机自启动了，可以使用下面的命令取消开机自启动</p><pre><code># systemctl disable nginx</code></pre><p><strong>第四步：服务的启动/停止/刷新配置文件/查看状态</strong></p><pre><code># systemctl start nginx.service　         启动nginx服务# systemctl stop nginx.service　          停止服务# systemctl restart nginx.service　       重新启动服务# systemctl list-units --type=service     查看所有已启动的服务# systemctl status nginx.service          查看服务当前状态# systemctl enable nginx.service          设置开机自启动# systemctl disable nginx.service         停止开机自启动</code></pre><h3 id="一个常见的错误">一个常见的错误</h3><p><strong>Warning: nginx.service changed on disk. Run 'systemctl daemon-reload' to reload units.</strong></p><p>直接按照提示执行命令systemctl daemon-reload 即可。</p><pre><code># systemctl daemon-reload</code></pre><h2 id="18-重启系统再次启动nginx报错">18 重启系统，再次启动nginx报错</h2><h3 id="181-故障现场">18.1 故障现场</h3><p>之前在虚拟机centos6.5上面设置自启动之后，重新启动系统可以正常启动，也不会出错。</p><p>centos6.5的自启动设置见16部分知识点。</p><p>但是在centos7.4(阿里云上面），参照第17部分配置好了自启动。重启系统发现nginx并没有自启动</p><p>使用命名systemctl status nginx查看了一下状态，内容如下：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442015567.png" alt="image.png" /></p><p>然后我直接进入/usr/local/nginx/sbin目录下面，执行./nginx，出现了下面的错误提示：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442034083.png" alt="image.png" /></p><p>从这两个提示信息，可以大概看出告诉我们的就是找不到/var/run/nginx/目录下面的nginx.pid文件。</p><h3 id="182-故障解决">18.2 故障解决</h3><p><strong>第一步：进入 cd /usr/local/nginx/conf/ 目录，编辑配置文件nginx.conf ；</strong></p><p>在配置文件中找到：#pid    logs/nginx.pid;</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442088565.png" alt="image.png" /></p><p>将其修改为：去掉注释，修改成自己的路径</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442099539.png" alt="image.png" /></p><p>修改完成保存退出</p><p><strong>第二步：创建目录/var/run/nginx/</strong></p><pre><code># mkdir /var/run/nginx -p</code></pre><p><strong>第三步：启动nginx服务</strong></p><pre><code># /usr/local/nginx/sbin/nginx</code></pre><p>可以查看一下是否成功启动了</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442145870.png" alt="image.png" /></p><h3 id="183-故障重现">18.3 故障重现</h3><p><strong>[emerg] open() &quot;/var/run/nginx/nginx.pid&quot; failed (2: No such file or directory)处理</strong></p><p>测试发现，只要执行reboot命令重启，var/run/nginx，nginx这个文件夹都会被删除，</p><p>搞得每一次都要去建立nginx这个文件夹，简直麻烦到了极点，实在受不了。下面</p><p>继续来解决这个问题。</p><p><strong>第一步：进入 cd /usr/local/nginx/conf/ 目录，编辑配置文件nginx.conf ；</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442168742.png" alt="image.png" /></p><p><strong>第二步：在/usr/local/nginx目录下建立logs文件夹</strong></p><pre><code># mkdir /usr/local/nginx/logs</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442196166.png" alt="image.png" /></p><p><strong>第三步：把/var/run/nginx/目录下的nginx.pid这个文件拷贝到第二步创建的logs文件夹里面。</strong></p><pre><code># cp nginx.pid /usr/local/nginx/logs/</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442216207.png" alt="image.png" /></p><p><strong>第四步：把logs这个文件夹在conf下也拷贝一份</strong></p><pre><code># cp -r logs conf</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442241254.png" alt="image.png" /></p><p><strong>第五步：修改权限/usr/local/nginx/logs/目录下面的nginx.pid文件的权限。</strong></p><pre><code>[root@iz2logs]# chmod 755 nginx.pid</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442255673.png" alt="image.png" /></p><p><strong>第六步：重启reboot</strong></p><pre><code># reboot</code></pre><p><strong>第六步：启动nginx</strong></p><pre><code># /usr/local/nginx/sbin/nginx</code></pre><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442268966.png" alt="image.png" /></p><p>这次是终于成功解决了，一边安装一边解决问题，到这里nginx总是算是可以自启动了，并且也不会重启后找不到nginx.pid文件。真的太不容易了。</p><p><strong>解决的原理：就是让它去另外一个地方找nginx.pid文件，</strong></p><p><strong>因为/var/run/nginx/nginx.pid这个文件总是重启就删除了</strong>。</p><h3 id="简单解决方案">简单解决方案</h3><p>上面的过程有点繁琐了，实际可以直接按照下面的这个简单方法解决</p><p>修改nginx.conf文件如下：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442284864.png" alt="image.png" /></p><p>在/usr/local/nginx/目录下创建一个logs目录。</p><p>然后启动就可以了，并且重启也不会被删除。</p><p>这样下面的日志文件的配置也可以简化为去掉# error_log logs/error.log info; 前面的“#”就可以了</p><p>error_log logs/error.log info;</p><h2 id="19-配置日志文件的位置">19 配置日志文件的位置</h2><p><strong>第一步：进入 cd /usr/local/nginx/conf/ 目录，编辑配置文件nginx.conf ；</strong></p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631442301968.png" alt="image.png" /></p><p><strong>第二步：保证肯定有这个路径，可以直接创建一下这个配置的目录</strong></p><pre><code># mkdir -p /var/log/nginx/</code></pre><p><strong>第三步：刷新配置文件</strong></p><pre><code># /usr/local/nginx/sbin/nginx -s reload</code></pre><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【lywJee】<a href="https://www.cnblogs.com/lywJ/p/10710361.html">nginx安装及其配置详细教程</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Lombok介绍]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/lombok-introduction" />
                <id>tag:https://ibit.tech,2021-09-02:lombok-introduction</id>
                <published>2021-09-02T21:31:41+08:00</published>
                <updated>2021-09-13T08:27:24+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="1-前言">1. 前言</h2><p>在过往的Java项目中，充斥着太多不友好的代码：POJO的getter/setter/toString；异常处理；I/O流的关闭操作等等，这些样板代码既没有技术含量，又影响着代码的美观，Lombok应运而生。</p><p>首先说明一下：任何技术的出现都是为了解决某一类问题的，如果在此基础上再建立奇技淫巧，不如回归Java本身。应该保持合理使用而不滥用。</p><h2 id="2-lombok介绍">2. Lombok介绍</h2><p>Lombok的使用非常简单，下面我们一起来看下：</p><h3 id="21-引入相应的maven包">2.1 引入相应的maven包</h3><pre><code>&lt;dependency&gt;  &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;  &lt;artifactId&gt;lombok&lt;/artifactId&gt;  &lt;version&gt;1.16.18&lt;/version&gt;  &lt;scope&gt;provided&lt;/scope&gt;&lt;/dependency&gt;复制代码</code></pre><p>Lombok的scope=provided，说明它只在编译阶段生效，不需要打入包中。事实正是如此，Lombok在编译期将带Lombok注解的Java文件正确编译为完整的Class文件。</p><p>###2.2 添加IDE工具对Lombok的支持</p><p>IDEA中引入Lombok支持如下：</p><ul><li>点击File-- Settings设置界面，安装Lombok插件：</li></ul><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453214810.png" alt="" /></p><ul><li>点击File-- Settings设置界面，开启Annocation Processors：</li></ul><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453236527.png" alt="" /></p><p>开启该项是为了让Lombok注解在编译阶段起到作用。</p><p>Eclipse的Lombok插件安装可以自行百度，也比较简单，值得一提的是，由于Eclipse内置的编译器不是Oracle javac，而是eclipse自己实现的Eclipse Compiler for Java (ECJ).要让ECJ支持Lombok，需要在eclipse.ini配置文件中添加如下两项内容：</p><p>-Xbootclasspath/a:[lombok.jar所在路径]</p><p>-javaagent:[lombok.jar所在路径]</p><h3 id="23-lombok实现原理">2.3 Lombok实现原理</h3><p>自从Java 6起，javac就支持“JSR 269 Pluggable Annotation Processing API”规范，只要程序实现了该API，就能在javac运行的时候得到调用。</p><p>Lombok就是一个实现了&quot;JSR 269 API&quot;的程序。在使用javac的过程中，它产生作用的具体流程如下：</p><ul><li>javac对源代码进行分析，生成一棵抽象语法树(AST)</li><li>javac编译过程中调用实现了JSR 269的Lombok程序</li><li>此时Lombok就对第一步骤得到的AST进行处理，找到Lombok注解所在类对应的语法树(AST)，然后修改该语法树(AST)，增加Lombok注解定义的相应树节点</li><li>javac使用修改后的抽象语法树(AST)生成字节码文件</li></ul><h3 id="24-lombok注解的使用">2.4 Lombok注解的使用</h3><p><em><strong>POJO类常用注解：</strong></em></p><h4 id="gettersetter">@Getter/@Setter</h4><p>作用类上，生成所有成员变量的getter/setter方法；作用于成员变量上，生成该成员变量的getter/setter方法。可以设定访问权限及是否懒加载等。</p><pre><code>package com.trace;import lombok.AccessLevel;import lombok.Getter;import lombok.Setter;/** * Created by Trace on 2018/5/19.&lt;br/&gt; * DESC: 测试类 */@SuppressWarnings(&quot;unused&quot;)public class TestClass {    public static void main(String[] args) {    }    @Getter(value = AccessLevel.PUBLIC)    @Setter(value = AccessLevel.PUBLIC)    public static class Person {        private String name;        private int age;        private boolean friendly;    }    public static class Animal {        private String name;        private int age;        @Getter @Setter private boolean funny;    }    }</code></pre><p>在Structure视图中，可以看到已经生成了getter/setter等方法：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453283541.png" alt="image.png" /></p><p>编译后的代码如下：[这也是传统Java编程需要编写的样板代码]</p><pre><code>//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package com.trace;public class TestClass {    public TestClass() {    }    public static void main(String[] args) {    }    public static class Animal {        private String name;        private int age;        private boolean funny;        public Animal() {        }        public boolean isFunny() {            return this.funny;        }        public void setFunny(boolean funny) {            this.funny = funny;        }    }    public static class Person {        private String name;        private int age;        private boolean friendly;        public Person() {        }        public String getName() {            return this.name;        }        public int getAge() {            return this.age;        }        public boolean isFriendly() {            return this.friendly;        }        public void setName(String name) {            this.name = name;        }        public void setAge(int age) {            this.age = age;        }        public void setFriendly(boolean friendly) {            this.friendly = friendly;        }    }}</code></pre><h4 id="tostring">@ToString</h4><p>作用于类，覆盖默认的toString()方法，可以通过of属性限定显示某些字段，通过exclude属性排除某些字段。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453327541.png" alt="" /></p><h4 id="equalsandhashcode">@EqualsAndHashCode</h4><p>作用于类，覆盖默认的equals和hashCode</p><h4 id="nonnull">@NonNull</h4><p>主要作用于成员变量和参数中，标识不能为空，否则抛出空指针异常。</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453341056.png" alt="" /></p><h4 id="noargsconstructor-requiredargsconstructor-allargsconstructor">@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor</h4><p>作用于类上，用于生成构造函数。有staticName、access等属性。</p><p>staticName属性一旦设定，将采用静态方法的方式生成实例，access属性可以限定访问权限。</p><ul><li>@NoArgsConstructor：生成无参构造器；</li><li>@RequiredArgsConstructor：生成包含final和@NonNull注解的成员变量的构造器；</li><li>@AllArgsConstructor：生成全参构造器。</li></ul><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453357142.png" alt="image.png" /></p><p>编译后结果：</p><pre><code>public static class Person {    @NonNull    private String name;    private int age;    private boolean friendly;    public String toString() {        return &quot;TestClass.Person(name=&quot; + this.getName() + &quot;, age=&quot; + this.getAge() + &quot;)&quot;;    }    @NonNull    public String getName() {        return this.name;    }    public int getAge() {        return this.age;    }    public boolean isFriendly() {        return this.friendly;    }    public void setName(@NonNull String name) {        if(name == null) {            throw new NullPointerException(&quot;name&quot;);        } else {            this.name = name;        }    }    public void setAge(int age) {        this.age = age;    }    public void setFriendly(boolean friendly) {        this.friendly = friendly;    }    private Person() {    }    private static TestClass.Person of() {        return new TestClass.Person();    }    @ConstructorProperties({&quot;name&quot;})    Person(@NonNull String name) {        if(name == null) {            throw new NullPointerException(&quot;name&quot;);        } else {            this.name = name;        }    }    @ConstructorProperties({&quot;name&quot;, &quot;age&quot;, &quot;friendly&quot;})    public Person(@NonNull String name, int age, boolean friendly) {        if(name == null) {            throw new NullPointerException(&quot;name&quot;);        } else {            this.name = name;            this.age = age;            this.friendly = friendly;        }    }}</code></pre><h4 id="data">@Data</h4><p>作用于类上，是以下注解的集合：@ToString @EqualsAndHashCode @Getter @Setter @RequiredArgsConstructor</p><h4 id="builder">@Builder</h4><p>作用于类上，将类转变为建造者模式</p><h4 id="log">@Log</h4><p>作用于类上，生成日志变量。针对不同的日志实现产品，有不同的注解：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453396707.png" alt="" /></p><p>*<strong>其他重要注解*</strong></p><h4 id="cleanup">@Cleanup：</h4><p>自动关闭资源，针对实现了java.io.Closeable接口的对象有效，如：典型的IO流对象</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453432763.png" alt="" /></p><p>编译后结果如下：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453448590.png" alt="" /></p><p><strong>是不是简洁了太多。</strong></p><h4 id="sneakythrows">@SneakyThrows</h4><p>可以对受检异常进行捕捉并抛出，可以改写上述的main方法如下：</p><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1631453494791.png" alt="" /></p><h4 id="synchronized">@Synchronized</h4><p>作用于方法级别，可以替换synchronize关键字或lock锁，用处不大</p><h2 id="版权申明"><font color='Red'>版权申明</font></h2><p>本文转载于【LiWenD正在掘金】<a href="https://juejin.cn/post/6844903607985242120">Java效率工具之Lombok</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[生产环境之Nginx高可用方案]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/nginx-production-high-availability" />
                <id>tag:https://ibit.tech,2021-08-28:nginx-production-high-availability</id>
                <published>2021-08-28T18:41:26+08:00</published>
                <updated>2021-09-13T08:27:33+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="准备工作"><font color='Green'>准备工作</font></h3><p><strong>虚拟机准备</strong></p><p>192.168.16.128</p><p>192.168.16.129</p><p>两台虚拟机。安装好<code>Nginx</code></p><p><strong>安装Nginx</strong></p><p>更新<code>yum</code>源文件：</p><pre><code class="language-shell"># rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm# wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo</code></pre><p>安装Nginx:</p><pre><code class="language-shell"># yum -y install  nginx</code></pre><p>操作命令：</p><pre><code class="language-shell"># 启动Nginx# systemctl start nginx; #停止Nginx# systemctl stop nginx; </code></pre><h3 id="什么是高可用"><font color='Green'>什么是高可用？</font></h3><p>高可用HA（High Availability）是分布式系统架构设计中必须考虑的因素之一，它通常是指，通过设计减少系统不能提供服务的时间。如果一个系统能够一直提供服务，那么这个可用性则是百分之百，但是天有不测风云。所以我们只能尽可能的去减少服务的故障。</p><h3 id="解决的问题"><font color='Green'>解决的问题？</font></h3><p>在生产环境上很多时候是以<code>Nginx</code>做反向代理对外提供服务，但是一天Nginx难免遇见故障，如：服务器宕机。当<code>Nginx</code>宕机那么所有对外提供的接口都将导致无法访问。</p><p>虽然我们无法保证服务器百分之百可用，但是也得想办法避免这种悲剧，今天我们使用<code>keepalived</code>来实现<code>Nginx</code></p><p>的高可用。</p><h3 id="双机热备方案"><font color='Green'>双机热备方案</font></h3><p>这种方案是国内企业中最为普遍的一种高可用方案，双机热备其实就是指一台服务器在提供服务，另一台为某服务的备用状态，当一台服务器不可用另外一台就会顶替上去。</p><p><strong>keepalived是什么？</strong></p><p><code>Keepalived</code>软件起初是专为<code>LVS</code>负载均衡软件设计的，用来管理并监控LVS集群系统中各个服务节点的状态，后来又加入了可以实现高可用的<code>VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议）</code>功能。因此，<code>Keepalived</code>除了能够管理LVS软件外，还可以作为其他服务<code>（例如：Nginx、Haproxy、MySQL等）</code>的高可用解决方案软件</p><p><strong>故障转移机制</strong></p><p><code>Keepalived</code>高可用服务之间的故障切换转移，是通过<code>VRRP</code> 来实现的。<br />在 <code>Keepalived</code>服务正常工作时，主 <code>Master</code>节点会不断地向备节点发送（多播的方式）心跳消息，用以告诉备<code>Backup</code>节点自己还活着，当主 <code>Master</code>节点发生故障时，就无法发送心跳消息，备节点也就因此无法继续检测到来自主 <code>Master</code>节点的心跳了，于是调用自身的接管程序，接管主Master节点的 IP资源及服务。而当主 Master节点恢复时，备Backup节点又会释放主节点故障时自身接管的IP资源及服务，恢复到原来的备用角色。</p><h3 id="实现过程"><font color='Green'>实现过程</font></h3><p><strong>安装keepalived</strong></p><p><code>yum</code>方式直接安装即可，该方式会自动安装依赖：</p><pre><code class="language-shell"># yum -y install keepalived</code></pre><p><strong>修改主机（192.168.16.128）keepalived配置文件</strong></p><p><code>yum</code>方式安装的会生产配置文件在<code>/etc/keepalived</code>下：</p><pre><code class="language-shell">vi keepalived.confkeepalived.conf:#检测脚本vrrp_script chk_http_port {    script &quot;/usr/local/src/check_nginx_pid.sh&quot; #心跳执行的脚本，检测nginx是否启动    interval 2                          #（检测脚本执行的间隔，单位是秒）    weight 2                            #权重}#vrrp 实例定义部分vrrp_instance VI_1 {    state MASTER            # 指定keepalived的角色，MASTER为主，BACKUP为备    interface ens33         # 当前进行vrrp通讯的网络接口卡(当前centos的网卡) 用ifconfig查看你具体的网卡    virtual_router_id 66    # 虚拟路由编号，主从要一直    priority 100            # 优先级，数值越大，获取处理请求的优先级越高    advert_int 1            # 检查间隔，默认为1s(vrrp组播周期秒数)    #授权访问    authentication {        auth_type PASS #设置验证类型和密码，MASTER和BACKUP必须使用相同的密码才能正常通信        auth_pass 1111    }    track_script {        chk_http_port            #（调用检测脚本）    }    virtual_ipaddress {        192.168.16.130            # 定义虚拟ip(VIP)，可多设，每行一个    }}</code></pre><p><code>virtual_ipaddress</code> 里面可以配置vip,在线上通过vip来访问服务。</p><pre><code>interface`需要根据服务器网卡进行设置通常查看方式`ip addr</code></pre><p><code>authentication</code>配置授权访问后备机也需要相同配置</p><p><strong>修改备机（192.168.16.129）keepalived配置文件</strong></p><pre><code>keepalived.conf:#检测脚本vrrp_script chk_http_port {    script &quot;/usr/local/src/check_nginx_pid.sh&quot; #心跳执行的脚本，检测nginx是否启动    interval 2                          #（检测脚本执行的间隔）    weight 2                            #权重}#vrrp 实例定义部分vrrp_instance VI_1 {    state BACKUP                        # 指定keepalived的角色，MASTER为主，BACKUP为备    interface ens33                      # 当前进行vrrp通讯的网络接口卡(当前centos的网卡) 用ifconfig查看你具体的网卡    virtual_router_id 66                # 虚拟路由编号，主从要一直    priority 99                         # 优先级，数值越大，获取处理请求的优先级越高    advert_int 1                        # 检查间隔，默认为1s(vrrp组播周期秒数)    #授权访问    authentication {        auth_type PASS #设置验证类型和密码，MASTER和BACKUP必须使用相同的密码才能正常通信        auth_pass 1111    }    track_script {        chk_http_port                   #（调用检测脚本）    }    virtual_ipaddress {        192.168.16.130                   # 定义虚拟ip(VIP)，可多设，每行一个    }}</code></pre><p><strong>检测脚本：</strong></p><pre><code class="language-sh">#!/bin/bash#检测nginx是否启动了A=`ps -C nginx --no-header |wc -l`        if [ $A -eq 0 ];then    #如果nginx没有启动就启动nginx                              systemctl start nginx                #重启nginx      if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then    #nginx重启失败，则停掉keepalived服务，进行VIP转移              killall keepalived                          fifi</code></pre><p>脚本授权:<code>chmod 775 check_nginx_pid.sh</code></p><p>说明：脚本必须通过授权，不然没权限访问啊，在这里我们两条服务器执行、<code>VIP(virtual_ipaddress:192.168.16.130)</code>,我们在生产环境是直接通过vip来访问服务。</p><p>模拟<code>nginx</code>故障：</p><p>修改两个服务器默认访问的<code>Nginx</code>的<code>html</code>页面作为区别。</p><p>首先访问<code>192.168.16.130</code>,通过<code>vip</code>进行访问，页面显示<code>192.168.16.128</code>；说明当前是主服务器提供的服务。</p><p>这个时候<code>192.168.16.128</code>主服务器执行命令：</p><pre><code class="language-sh">systemctl stop nginx; #停止nginx</code></pre><p>再次访问<code>vip(192.168.16.130)</code>发现这个时候页面显示的还是：<code>192.168.16.128</code>，这是脚本里面自动重启。</p><p>现在直接将<code>192.168.16.128</code>服务器关闭，在此访问<code>vip(192.168.16.130)</code>现在发现页面显示<code>192.168.16.129</code>这个时候<code>keepalived</code>就自动故障转移了，一套企业级生产环境的高可用方案就搭建好了。</p><p><code>keepalived</code>中还有许多功能比如：邮箱提醒啊等等，就不操作了，可以去官网看看文档。</p><h3 id="版权说明"><font color='Red'>版权说明</font></h3><p>本文转载于【SimpleWu】<a href="https://www.cnblogs.com/SimpleWu/p/11004902.html">生产环境之Nginx高可用方案</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[这次终于懂了，InnoDB的七种锁]]></title>
                <link rel="alternate" type="text/html" href="https://ibit.tech/archives/mysql-innodb-7-locks" />
                <id>tag:https://ibit.tech,2021-08-28:mysql-innodb-7-locks</id>
                <published>2021-08-28T14:55:02+08:00</published>
                <updated>2021-09-13T08:27:43+08:00</updated>
                <author>
                    <name>iBit程序猿</name>
                    <uri>https://ibit.tech</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="前言"><font color='Green'>前言</font></h3><p>MySQL是目前世界上最流行的数据库，InnoDB是MySQL最流行的存储引擎，它在大数据量高并发量的业务场景下，有着非常良好的性能表现，之所以如此，是和InnoDB的<strong>锁机制</strong>相关。</p><p>总的来说，InnoDB共有<strong>七种类型的锁</strong>：</br><br />（1）自增锁(Auto-inc Locks)；</br><br />（2）共享/排它锁(Shared and Exclusive Locks)；</br><br />（3）意向锁(Intention Locks)；</br><br />（4）插入意向锁(Insert Intention Locks)；</br><br />（5）记录锁(Record Locks)；</br><br />（6）间隙锁(Gap Locks)；</br><br />（7）临键锁(Next-key Locks)；</br></p><h3 id="第一种自增锁auto-inc-locks"><font color='Green'>第一种，自增锁（Auto-inc Locks）</font></h3><p><strong>【案例说明】</strong></p><p>MySQL，InnoDB，默认的隔离级别(RR)，假设有数据表：</p><pre><code>t(id AUTO_INCREMENT, name);</code></pre><p>数据表中有数据：</p><table><thead><tr><th>id</th><th>name</th></tr></thead><tbody><tr><td>1</td><td>shenjian</td></tr><tr><td>2</td><td>zhangsan</td></tr><tr><td>3</td><td>lisi</td></tr></tbody></table><p>事务A<strong>先</strong>执行，还<strong>未提交</strong>：</p><pre><code>insert into t(name) values(xxx);</code></pre><p>事务B<strong>后</strong>执行：</p><pre><code>insert into t(name) values(ooo);*</code></pre><p><strong>问：事务B会不会被阻塞？</strong></p><p><strong>【案例分析】</strong></p><p>InnoDB在RR隔离级别下，尝试解决幻读问题，上面这个案例中：</p><p>（1）事务A先执行insert，会得到一条<font color='Salmon'>(4, xxx)</font>的记录，由于是自增列，故不用显示指定id为 4，InnoDB会自动增长，注意此时事务并未提交；</br><br />（2）事务B后执行insert，<strong>假设不会被阻塞</strong>，那会得到一条<font color='Salmon'>(5, ooo)</font>的记录；</br>此时，并未有什么不妥，但<font color='Salmon'>如果，</font></br><br /><font color='Salmon'>（3）事务A继续insert：</font></br></p><pre><code>insert into t(name) values(xxoo);</code></pre><p>会得到一条<font color='Salmon'>(6, xxoo)</font>的记录。</br><br />（4）事务A再select：</p><pre><code>select * from t where id&gt;3;</code></pre><p>得到的结果是：</p><table><thead><tr><th>id</th><th>name</th></tr></thead><tbody><tr><td>4</td><td>xxx</td></tr><tr><td>6</td><td>xxoo</td></tr></tbody></table><p><font color='Blue'>画外音：不可能查询到5的记录，在RR的隔离级别下，不可能读取到还未提交事务生成的数据。</font></p><p>这对于事务A来说，就很奇怪了，AUTO_INCREMENT的列，<strong>连续插入了两条记录</strong>，<font color='Salmon'>一条是4，接下来一条变成了6</font>，就像莫名其妙的幻影。</p><p><strong>【自增锁】</strong></p><p>自增锁是一种特殊的<strong>表级别锁</strong>（table-level lock），<font color='Salmon'>专门针对事务插入AUTO_INCREMENT类型的列</font>。</p><p>最简单的情况，如果一个事务正在往表中插入记录，所有其他事务的插入必须等待，以便第一个事务插入的行，是连续的主键值。</p><p><font color='Blue'>画外音：官网是这么说的 </br><br />An AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.</font></p><p>与此同时，InnoDB提供了innodb_autoinc_lock_mode配置，可以调节与改变该锁的模式与行为。</p><p><strong>【假如不是自增列】</strong></p><p>上面的案例，<strong>假设不是自增列</strong>，又会是什么样的情形呢？</p><pre><code>t(id unique PK, name);</code></pre><p>数据表中有数据：</p><table><thead><tr><th>id</th><th>name</th></tr></thead><tbody><tr><td>10</td><td>shenjian</td></tr><tr><td>20</td><td>zhangsan</td></tr><tr><td>30</td><td>lisi</td></tr></tbody></table><p>事务A<strong>先</strong>执行，在<font color='Salmon'>10与20两条记录中</font>插入了一行，还<strong>未提交</strong>：</p><pre><code>insert into t values(11, xxx);</code></pre><p>事务B<strong>后</strong>执行，也在<font color='Salmon'>10与20两条记录中</font>插入了一行：</p><pre><code>insert into t values(12, ooo);</code></pre><p>这里，便<font color='Salmon'>不再使用自增锁</font>，那：</br><br />（1）会使用什么锁？</br><br />（2）事务B会不会被阻塞呢？</p><p>先卖个关子，下文再解答。</p><h3 id="第二种共享排它锁shared-and-exclusive-locks"><font color='Green'>第二种，共享/排它锁(Shared and Exclusive Locks)</font></h3><p>《<a href="https://ibit.tech/archives/mysql-innodb-high-concurrency-reason">InnoDB并发如此高，原因竟然在这？</a>》一文介绍了通用的共享/排它锁，在InnoDB里当然也实现了<strong>标准的行级锁</strong>(row-level locking)，<font color='Salmon'>共享/排它锁</font>：</br><br />（1）事务拿到某一行记录的共享S锁，才可以<font color='Salmon'>读取</font>这一行；</br><br />（2）事务拿到某一行记录的排它X锁，才可以<font color='Salmon'>修改</font>或者<font color='Salmon'>删除</font>这一行；</p><p>其<strong>兼容互斥表</strong>如下：</p><table><thead><tr><th> </th><th>S</th><th>X</th></tr></thead><tbody><tr><td>S</td><td>兼容</td><td>互斥</td></tr><tr><td>X</td><td>互斥</td><td>互斥</td></tr></tbody></table><p>即：</br><br />（1）多个事务可以拿到一把S锁，<font color='Salmon'>读读可以并行</font>；</br><br />（2）而只有一个事务可以拿到X锁，<font color='Salmon'>写写/读写必须互斥</font>；</p><p>共享/排它锁的潜在问题是，<font color='Salmon'>不能充分的并行</font>，解决思路是<strong>数据多版本</strong>，具体思路在《<a href="https://ibit.tech/archives/mysql-innodb-high-concurrency-reason">InnoDB并发如此高，原因竟然在这？</a>》介绍过，这里不再深入展开。</p><h3 id="第三种意向锁intention-locks"><font color='Green'>第三种，意向锁(Intention Locks)</font></h3><p>InnoDB支持多粒度锁(multiple granularity locking)，它允许行级锁与表级锁共存，实际应用中，InnoDB使用的是意向锁。</p><p><strong>意向锁</strong>是指，未来的某个时刻，事务可能要加共享/排它锁了，先提前声明一个意向。</p><p>意向锁有这样一些特点：</br><br />（1）首先，意向锁，是一个表级别的锁(table-level locking)；</br><br />（2）意向锁分为：</br></p><ul><li><strong>意向共享锁</strong>(intention shared lock, IS)，它预示着，事务有意向对表中的<font color='Salmon'>某些行</font>加共享S锁</li><li><strong>意向排它锁</strong>(intention exclusive lock, IX)，它预示着，事务有意向对表中的<font color='Salmon'>某些行</font>加排它X锁</li></ul><p>举个例子：</p><p><code>select ... lock in share mode</code>，要设置<strong>IS锁</strong>；</p><p><code>select ... for update</code>，要设置<strong>IX锁</strong>；</p><p>（3）意向锁协议(intention locking protocol)并不复杂：</p><ul><li>事务要获得某些行的S锁，必须先获得表的IS锁</li><li>事务要获得某些行的X锁，必须先获得表的IX锁</li></ul><p>（4）由于意向锁仅仅表明意向，它其实是比较弱的锁，意向锁之间并不相互互斥，而是可以并行，其<strong>兼容互斥表</strong>如下：</p><table><thead><tr><th> </th><th>IS</th><th>IX</th></tr></thead><tbody><tr><td>IS</td><td>兼容</td><td>兼容</td></tr><tr><td>IX</td><td>兼容</td><td>兼容</td></tr></tbody></table><p>（5）额，既然意向锁之间都相互兼容，那其意义在哪里呢？它会与共享锁/排它锁互斥，其<strong>兼容互斥表</strong>如下：</p><table><thead><tr><th> </th><th>S</th><th>X</th></tr></thead><tbody><tr><td>IS</td><td>兼容</td><td>互斥</td></tr><tr><td>IX</td><td>互斥</td><td>互斥</td></tr></tbody></table><p><font color='Blue'>画外音：排它锁是很强的锁，不与其他类型的锁兼容。这也很好理解，修改和删除某一行的时候，必须获得强锁，禁止这一行上的其他并发，以保障数据的一致性。</font></p><h3 id="第四种插入意向锁insert-intention-locks"><font color='Green'>第四种，插入意向锁(Insert Intention Locks)</font></h3><p>对已有数据行的<strong>修改与删除</strong>，必须<font color='Salmon'>加强互斥锁X锁</font>，那对于<strong>数据的插入</strong>，是否还需要加这么强的锁，来实施互斥呢？插入意向锁，孕育而生。</p><p><strong>插入意向锁</strong>，是间隙锁(Gap Locks)的一种（所以，也是实施在索引上的），它是专门针对insert操作的。</p><p><font color='Blue'>画外音：有点尴尬，间隙锁下文才会介绍，暂且理解为，它是一种实施在索引上，锁定索引某个区间范围的锁。</font></p><p>它的玩法是：</p><p>多个事务，在同一个索引，同一个范围区间插入记录时，<font color='Salmon'>如果插入的位置不冲突，不会阻塞彼此</font>。</p><p><font color='Blue'>画外音：官网的说法是</br><br />Insert Intention Lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.</font></p><p>这样，之前挖坑的例子，就能够解答了。</p><p>在MySQL，InnoDB，RR下：</p><pre><code>t(id unique PK, name);</code></pre><p>数据表中有数据：</p><table><thead><tr><th>id</th><th>name</th></tr></thead><tbody><tr><td>10</td><td>shenjian</td></tr><tr><td>20</td><td>zhangsan</td></tr><tr><td>30</td><td>lisi</td></tr></tbody></table><p>事务A先执行，在<font color='Salmon'>10与20两条记录中插入了一行</font>，还未提交：</p><pre><code>insert into t values(11, xxx);</code></pre><p>事务B后执行，也在<font color='Salmon'>10与20两条记录中插入了一行</font>：</p><pre><code>insert into t values(12, ooo);</code></pre><p>（1）会使用什么锁？</br><br />（2）事务B会不会被阻塞呢？</p><p><strong>回答</strong>：虽然事务隔离级别是RR，虽然是同一个索引，虽然是同一个区间，但插入的记录并不冲突，故这里：</p><p>（1）使用的是<font color='Salmon'>插入意向锁；</font></br><br />（2）并<font color='Salmon'>不会阻塞</font>事务B；</p><p><strong>【思路小结】</strong></p><p>（1）InnoDB使用<strong>共享锁</strong>，可以提高<font color='Salmon'>读读并发</font>；</br><br />（2）为了保证<font color='Salmon'>数据强一致</font>，InnoDB使用强<strong>互斥锁</strong>，保证同一行记录修改与删除的串行性；</p><p>（3）InnoDB使用<strong>插入意向锁</strong>，可以提高<font color='Salmon'>插入并发</font>；</p><p><strong>【另一个案例】</strong></p><p>假设不是插入并发，而是读写并发，又会是什么样的结果呢？</p><p>MySQL，InnoDB，默认的隔离级别(RR)。</p><pre><code>t(id unique PK, name);</code></pre><p>数据表中有数据：</p><table><thead><tr><th>id</th><th>name</th></tr></thead><tbody><tr><td>10</td><td>shenjian</td></tr><tr><td>20</td><td>zhangsan</td></tr><tr><td>30</td><td>lisi</td></tr></tbody></table><p>事务A先执行，<font color='Salmon'>查询了一些记录</font>，还未提交：</p><pre><code>select * from t where id&gt;10;</code></pre><p>事务B后执行，在<font color='Salmon'>10与20两条记录中插入</font>了一行：</p><pre><code>insert into t values(11, xxx);</code></pre><p>这里：</br><br />（1）会使用什么锁？</br><br />（2）事务B会不会被阻塞呢？</p><p>继续卖关子，下文解答。</p><p><strong>【继续插入，知识铺垫】</strong></p><p>InnoDB的细粒度锁，是实现在索引记录上的，如果查询没有命中索引，也将退化为表锁。</p><p><strong>【InnoDB的索引】</strong></p><p>InnoDB的索引有两类索引，<strong>聚集索引</strong>(Clustered Index)与<strong>普通索引</strong>(Secondary Index)。</p><p>InnoDB的<font color='Salmon'>每一个表都会有聚集索引</font>：</br><br />（1）如果表定义了PK，则<strong>PK</strong>就是聚集索引；</br><br />（2）如果表没有定义PK，则<strong>第一个非空unique列</strong>是聚集索引；</br><br />（3）否则，InnoDB会<strong>创建一个隐藏的row-id</strong>作为聚集索引；</br><br />为了方便说明，后文都将以PK说明。</p><p>索引的结构<strong>是B+树</strong>，这里不展开B+树的细节，说几个结论：</br><br />（1）在索引结构中，非叶子节点存储key，叶子节点存储value；</p><p>（2）<strong>聚集索引</strong>，<font color='Salmon'>叶子节点存储行记录(row)</font>；</p><p><font color='Blue'>    画外音：所以，InnoDB索引和记录是存储在一起的，而MyISAM的索引和记录是分开存储的。</font></p><p>（3）<strong>普通索引</strong>，<font color='Salmon'>叶子节点存储了PK的值</font>；</p><p><font color='Blue'>    画外音：</p><p>    所以，InnoDB的普通索引，如果未满足索引覆盖，实际上会扫描两遍：</p><p>    第一遍，由普通索引找到PK；</p><p>    第二遍，由PK找到行记录；</p><p>    索引结构，InnoDB/MyISAM的索引结构，如果大家感兴趣，未来撰文详述。</font></p><p>举个例子，假设有InnoDB表：</p><pre><code>t(id PK, name KEY, sex, flag);</code></pre><p>表中有四条记录：</p><table><thead><tr><th>id</th><th>name</th><th>sex</th><th>flag</th></tr></thead><tbody><tr><td>1</td><td>shenjian</td><td>m</td><td>A</td></tr><tr><td>3</td><td>zhangsan</td><td>m</td><td>A</td></tr><tr><td>5</td><td>lisi</td><td>m</td><td>A</td></tr><tr><td>9</td><td>wangwu</td><td>f</td><td>B</td></tr></tbody></table><p><img src="https://x-halo.oss-cn-beijing.aliyuncs.com/halo/image_1630133638328.png" alt="image.png" /></p><p>可以看到：</br><br />（1）第一幅图，id PK的聚集索引，叶子存储了所有的行记录；</br><br />（2）第二幅图，name上的普通索引，叶子存储了PK的值；</p><p>对于：</p><pre><code>select \* from t where name=’shenjian’;</code></pre><p>（1）会先在name普通索引上查询到PK=1；</br><br />（2）再在聚集索引上查询到(1,shenjian, m, A)的行记录；</p><p>有了上面的铺垫，下文继续介绍InnoDB剩下三种锁：</br><br />（1）记录锁(Record Locks)；</br><br />（2）间隙锁(Gap Locks)；</br><br />（3）临键锁(Next-Key Locks)；</br></p><p>为了方便讲述，如无特殊说明，后文中，默认的事务隔离级别为<strong>可重复读</strong>(Repeated Read, RR)。</p><h3 id="第五种记录锁record-locks"><font color='Green'>第五种，记录锁(Record Locks)</font></h3><p><strong>记录锁</strong>，它封锁索引记录，例如：</p><pre><code>select * from t where id=1 for update;</code></pre><p>它会在id=1的索引记录上加锁，以阻止其他事务插入，更新，删除id=1的这一行。</p><p>需要说明的是：</p><pre><code>select * from t where id=1;</code></pre><p>是<strong>快照读</strong>(SnapShot Read)，它并不加锁，具体在《<a href="https://ibit.tech/archives/mysql-innodb-high-concurrency-reason">InnoDB并发如此高，原因竟然在这？</a>》中做了详细阐述。</p><h3 id="第六种间隙锁gap-locks"><font color='Green'>第六种，间隙锁(Gap Locks)</font></h3><p><strong>间隙锁</strong>，它封锁索引记录中的间隔，或者第一条索引记录之前的范围，又或者最后一条索引记录之后的范围。</p><p>依然是上面的例子，InnoDB，RR：</p><pre><code>t(id PK, name KEY, sex, flag);</code></pre><p>表中有四条记录：</p><table><thead><tr><th>id</th><th>name</th><th>sex</th><th>flag</th></tr></thead><tbody><tr><td>1</td><td>shenjian</td><td>m</td><td>A</td></tr><tr><td>3</td><td>zhangsan</td><td>m</td><td>A</td></tr><tr><td>5</td><td>lisi</td><td>m</td><td>A</td></tr><tr><td>9</td><td>wangwu</td><td>f</td><td>B</td></tr></tbody></table><p>这个SQL语句</p><pre><code>select * from t where id between 8 and 15 for update;</code></pre><p>会封锁区间，以阻止其他事务id=10的记录插入。</p><p><font color='Blue'>   画外音：</p><p>    为什么要阻止id=10的记录插入？</p><p>如果能够插入成功，头一个事务执行相同的SQL语句，会发现结果集多出了一条记录，即幻影数据。<br /></font></p><p>间隙锁的<strong>主要目的</strong>，就是为了<font color='Salmon'>防止其他事务在间隔中插入数据</font>，以导致“不可重复读”。</p><p>如果把事务的隔离级别降级为<strong>读提交</strong>(Read Committed, RC)，间隙锁则会自动失效。</p><h3 id="第七种临键锁next-key-locks"><font color='Green'>第七种，临键锁(Next-Key Locks)</font></h3><p><strong>临键锁</strong>，是<font color='Salmon'>记录锁与间隙锁的组合</font>，它的封锁范围，既包含索引记录，又包含索引区间。</p><p>更具体的，临键锁会封锁索引记录本身，以及索引记录之前的区间。</p><p>如果一个会话占有了索引记录R的共享/排他锁，其他会话不能立刻在R之前的区间插入新的索引记录。</p><p><font color='Blue'>  画外音：原文是说</p><p>    If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.</font></p><p>依然是上面的例子，InnoDB，RR：</p><pre><code>t(id PK, name KEY, sex, flag);</code></pre><p>表中有四条记录：</p><table><thead><tr><th>id</th><th>name</th><th>sex</th><th>flag</th></tr></thead><tbody><tr><td>1</td><td>shenjian</td><td>m</td><td>A</td></tr><tr><td>3</td><td>zhangsan</td><td>m</td><td>A</td></tr><tr><td>5</td><td>lisi</td><td>m</td><td>A</td></tr><tr><td>9</td><td>wangwu</td><td>f</td><td>B</td></tr></tbody></table><p>PK上潜在的临键锁为：</p><pre><code>(-infinity, 1](1, 3](3, 5](5, 9](9, +infinity)</code></pre><p>临键锁的主要目的，也是为了避免<strong>幻读</strong>(Phantom Read)。如果把事务的隔离级别降级为RC，临键锁则也会失效。</p><p><font color='Blue'>  画外音：关于事务的隔离级别，以及幻读，之前的文章一直没有展开说明，如果大家感兴趣，后文详述。</font></p><h3 id="总结"><font color='Green'>【总结】</font></h3><p>（1）<strong>自增锁</strong>(Auto-inc Locks)：<font color='Salmon'>表级锁</font>，专门针对事务插入AUTO_INC的列，如果插入位置冲突，<font color='Salmon'>多个事务会阻塞</font>，以保证数据一致性；</br><br />（2）<strong>共享/排它锁</strong>(Shared and Exclusive Locks)：<font color='Salmon'>行级锁</font>，S锁与X锁，<font color='Salmon'>强锁</font>；</br><br />（3）<strong>意向锁</strong>(Intention Locks)：<font color='Salmon'>表级锁</font>，IS锁与IX锁，弱锁，仅仅表明意向；</br><br />（4）<strong>插入意向锁</strong>(Insert Intention Locks)：<font color='Salmon'>针对insert的，如果插入位置不冲突，多个事务不会阻塞</font>，以提高插入并发；</br><br />（5）<strong>记录锁</strong>(Record Locks)：<font color='Salmon'>索引记录上加锁</font>，对索引记录实施互斥，以保证数据一致性；</br><br />（6）<strong>间隙锁</strong>(Gap Locks)：<font color='Salmon'>封锁索引记录中间的间隔</font>，在RR下有效，防止间隔中被其他事务插入；</br><br />（7）<strong>临键锁</strong>(Next-key Locks)：<font color='Salmon'>封锁索引记录，以及索引记录中间的间隔</font>，在RR下有效，防止幻读。</p><h3 id="版权说明"><font color='Red'>版权说明</font></h3><p>本文转载于【58沈剑-架构师之路】<a href="https://mp.weixin.qq.com/s/f4_o-6-JEbIzPCH09mxHJg">这次终于懂了，InnoDB的七种锁</a>，仅用于学习，版权归作者所有，如有侵权烦请告知，我会立即删除并表示歉意，<a href="mailto:xiaobenma020@gmail.com">联系邮箱</a>。</p>]]>
                </content>
            </entry>
</feed>
