<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>小温之家—行走在猿类世界</title>
            <link>http://blog.wenqy.com</link>
        <generator>Halo 1.6.0</generator>
        <lastBuildDate>Tue, 17 Nov 2020 10:42:24 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[记一次试用阿里公有云ARMS（应用实时监控服务）产品]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/记一次试用阿里公有云arms应用实时监控服务产品</link>
                    <description>
                            <![CDATA[<p>在阿里公有云上手抖买了云服务器ECS（Elastic Compute Service）好久了，花了几百大洋，肉痛。。。那时刚好正值阿里云ARMS（应用实时监控服务）开通试用，那就玩一把吧。ARMS是基于Agent的无侵入式APM（应用性能管理）方案，它对性能监控、分布式链路追踪、故障诊断都有很强的支持，包含应用监控、前端监控、容器监控甚至是自定义监控等非常大而全的功能，简直是一站式全家桶服务。</p><h3 id="准备ecs环境">准备ECS环境</h3><p>只有一台ECS，那就要物尽其用，体验ECS环境下的ARMS。对ECS进行操作，需先配置安全组规则，定义出入方向的端口等等，第一次SSH登录，需重置密码。考虑只有一台ECS环境，可能要测试多应用，安装Docker和Nginx环境。</p><p>1、安装docker环境，启动测试，判断是否安装成功</p><pre><code class="language-sh"># 修改软件源sudo yum-config-manager \    --add-repo \    https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.reposudo sed -i 's/download.docker.com/mirrors.ustc.edu.cn\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo</code></pre><pre><code class="language-sh">sudo yum makecache fastsudo yum install docker-ce</code></pre><pre><code class="language-sh">sudo systemctl enable dockersudo systemctl start docker</code></pre><pre><code class="language-bash"># hello world镜像测试服务docker run hello-world</code></pre><p>参考：<a href="https://yeasy.gitbooks.io/docker_practice/content/install/centos.html">https://yeasy.gitbooks.io/docker_practice/content/install/centos.html</a></p><p>2、docker下安装nginx</p><pre><code class="language-sh"># 安装Nginx，并挂载资源和配置docker run -d -p 80:80 --name nginx-web \  -v /home/nginx/www:/usr/share/nginx/html \  -v /home/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \  -v /home/nginx/logs:/var/log/nginx \  nginx</code></pre><p><code>vi /home/nginx/www/index.html</code> 编写测试首页</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;title&gt;Nginx test !!!&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h1&gt;Nginx&lt;/h1&gt;    &lt;p&gt;Hello,wenqy!&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</code></pre><p><code>vi /home/nginx/conf/nginx.conf</code>配置路由到首页</p><pre><code>server {        listen       80;        #server_name  localhost;        #charset koi8-r;        #access_log  logs/host.access.log  main;        location / {            root   /usr/share/nginx/html;            index  index.html index.htm;        }}</code></pre><p><code>curl 127.0.0.1:80</code> 测试访问</p><p>3、docker镜像加速</p><p>开通镜像服务器：<a href="https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors">https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors</a></p><pre><code class="language-sh">sudo mkdir -p /etc/dockersudo tee /etc/docker/daemon.json &lt;&lt;-'EOF'{  &quot;registry-mirrors&quot;: [&quot;https://xxxxxx.mirror.aliyuncs.com&quot;]}EOFsudo systemctl daemon-reloadsudo systemctl restart docker</code></pre><p>公网访问</p><p>ECS安全组规则里，配置入口方向，开放端口，0.0.0.0/0</p><p><code>curl 123.56.xxx.xxx</code>访问</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20201113151306820-d2b3126db90d43fb9614d82feb6eefd3.png" alt="image-20201113151306820" /></p><h3 id="rds环境">RDS环境</h3><p>准备RDS环境，有条件的同学也可以购买RDS（关系型数据库服务），进行RDS操作时，需添加白名单和安全组，申请外网地址，创建数据库账号等等。</p><h3 id="arms环境">ARMS环境</h3><p>阿里云官网，开通ARMS试用，注意不要选择专业版，专业版按量付费，不差钱的那就另说了。</p><h4 id="单应用测试">单应用测试</h4><h5 id="docker安装demo应用">Docker安装demo应用</h5><p>准备应用，从<code>start.aliyun.com</code> （类似<a href="https://start.spring.io/在线项目初始化器）下载">https://start.spring.io/在线项目初始化器）下载</a> demo</p><pre><code class="language-sh">mvn clean installjava -jar demo-0.0.1-SNAPSHOT.jar </code></pre><p>启动失败，提示没有清单属性，META-INF没有定义主类</p><p>修改插件配置</p><pre><code class="language-xml">&lt;plugin&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;&lt;version&gt;2.2.6.RELEASE&lt;/version&gt;&lt;executions&gt;         &lt;execution&gt;           &lt;goals&gt;              &lt;goal&gt;repackage&lt;/goal&gt;           &lt;/goals&gt;         &lt;/execution&gt;    &lt;/executions&gt;&lt;configuration&gt;&lt;mainClass&gt;com.wenqy.demo.DemoApplication&lt;/mainClass&gt;&lt;/configuration&gt;&lt;/plugin&gt;</code></pre><p>还要写controller类，来暴露REST服务，本想体验一把<code>start.aliyun.com</code>的便捷性，可能我操作不对，直接放弃，改用之前写的demo</p><p><a href="https://github.com/wenqy/java-study/tree/master/spring-security-study/securing-web">https://github.com/wenqy/java-study/tree/master/spring-security-study/securing-web</a></p><p>准备好应用和Dockerfile文件后，构建demo镜像</p><pre><code class="language-sh">[root@iZ2ze7666pvq2l4otgrbseZ securing-web]# docker build -t wenqy/securing-web:1.0.0 .Sending build context to Docker daemon  20.98MBStep 1/4 : FROM openjdk:8-jdk-alpine8-jdk-alpine: Pulling from library/openjdke7c96db7181b: Pull complete f910a506b6cb: Pull complete c2274a1a0e27: Pull complete Digest: sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3Status: Downloaded newer image for openjdk:8-jdk-alpine ---&gt; a3562aa0b991Step 2/4 : COPY securing-web-0.0.1-SNAPSHOT.jar /securing-web-0.0.1-SNAPSHOT.jar ---&gt; d4b27dd36821Step 3/4 : ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/securing-web-0.0.1-SNAPSHOT.jar&quot;] ---&gt; Running in fbc474735d0bRemoving intermediate container fbc474735d0b ---&gt; 13efd600069cStep 4/4 : EXPOSE 8080 ---&gt; Running in d11d875488b3Removing intermediate container d11d875488b3 ---&gt; ea41dcbbfac0Successfully built ea41dcbbfac0Successfully tagged wenqy/securing-web:1.0.0[root@iZ2ze7666pvq2l4otgrbseZ securing-web]# docker imagesREPOSITORY           TAG                 IMAGE ID            CREATED             SIZEwenqy/securing-web   1.0.0               ea41dcbbfac0        9 seconds ago       126MBnginx                latest              ed21b7a8aee9        3 weeks ago         127MBopenjdk              8-jdk-alpine        a3562aa0b991        11 months ago       105MBhello-world          latest              fce289e99eb9        15 months ago       1.84kB</code></pre><p>运行容器应用，测试是否启动成功</p><pre><code class="language-sh">[root@iZ2ze7666pvq2l4otgrbseZ securing-web]# docker run -d -p 8080:8080 --name securing-web  wenqy/securing-web:1.0.002f8d461e6b9566ccd84d225f8d6d301a057650d2334bda3b5b5174ec80d04cd[root@iZ2ze7666pvq2l4otgrbseZ securing-web]# docker psCONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                    NAMES02f8d461e6b9        wenqy/securing-web:1.0.0   &quot;java -jar /securing…&quot;   5 seconds ago       Up 5 seconds        0.0.0.0:8080-&gt;8080/tcp   securing-web3a5f171848e0        nginx                      &quot;nginx -g 'daemon of…&quot;   8 days ago          Up 7 days           0.0.0.0:80-&gt;80/tcp       nginx-web[root@iZ2ze7666pvq2l4otgrbseZ securing-web]# curl localhost:8080&lt;!DOCTYPE html&gt;&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xmlns:sec=&quot;https://www.thymeleaf.org/thymeleaf-extras-springsecurity3&quot;&gt;    &lt;head&gt;        &lt;title&gt;Spring Security Example&lt;/title&gt;    &lt;/head&gt;    &lt;body&gt;        &lt;h1&gt;Welcome!&lt;/h1&gt;        &lt;p&gt;Click &lt;a href=&quot;/hello&quot;&gt;here&lt;/a&gt; to see a greeting.&lt;/p&gt;    &lt;/body&gt;&lt;/html&gt;[root@iZ2ze7666pvq2l4otgrbseZ securing-web]# </code></pre><p>配置Nginx，将请求转发至应用端口，其中ifconfig 使用docker0的ip</p><pre><code class="language-nginx">location / {     proxy_pass http://172.17.0.1:8080;}</code></pre><p><code>docker restart nginx</code>重启nginx后，查看效果，访问成功</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20201113154057488-374c6a8840574301ac49578f79a1591d.png" alt="image-20201113154057488" /></p><h5 id="docker安装demo应用agent">Docker安装demo应用Agent</h5><p>1、准备ARMS的licenseKey</p><p>在<a href="https://arms.console.aliyun.com/#/home">ARMS控制台</a>要接入的应用里获取licenseKey</p><p>2、集成已有镜像</p><p>编辑Dockerfile文件来集成镜像，注意修改应用和licenseKey。</p><pre><code class="language-dockerfile">##       RMS APM DEMO Docker    #####          For Java            #####      withAgent   V0.1        #####                              ######################################FROM wenqy/securing-web:1.0.0WORKDIR /root/# 请根据所在地域替换探针的下载地址。RUN wget &quot;http://arms-apm-hangzhou.oss-cn-hangzhou.aliyuncs.com/ArmsAgent.zip&quot; -O ArmsAgent.zipRUN unzip ArmsAgent.zip -d /root/# LicenseKey 在控制台应用监控接入页面查看。# AppName 为用户自定义 ARMS 监控应用名称，用户名暂不支持中文。# 若所有镜像都接入同一个应用监控任务，配置此处的 arms_licenseKey 和 arms_appName 即可。# 若需将镜像接入其他应用监控任务，可在 docker run 中使用 -e 参数指定该应用的 arms_licenseKey 和 arms_appName 参数，以覆盖此处的配置。ENV arms_licenseKey=ARMS控制台分配的KeyENV arms_appName=ARMS控制台接入应用ENV JAVA_TOOL_OPTIONS ${JAVA_TOOL_OPTIONS} '-javaagent:/root/ArmsAgent/arms-bootstrap-1.7.0-SNAPSHOT.jar -Darms.licenseKey='${arms_licenseKey}' -Darms.appName='${arms_appName}### for check the argsRUN env | grep JAVA_TOOL_OPTIONS### 下面可加入用户 自定义 dockerfile 逻辑。### ......</code></pre><p>3、构建并启动新镜像</p><p>构建Agent镜像</p><pre><code>docker build -t wenqy/arms-springboot:1.0.0 -f Dockerfile .</code></pre><p>启动Agent镜像</p><pre><code>docker run -d -e &quot;arms_licenseKey=xxxxxx&quot; -e &quot;arms_appName=application&quot; -p 8081:8080 wenqy/arms-springboot:1.0.0</code></pre><p>启动成功后，登录ARMS控制台，进行结果验证</p><p>编写脚本，对应用进行访问，预热</p><pre><code class="language-sh">#!/bin/bashwhile truedocurl localhost:8081sleep 1done</code></pre><p>监控信息有一定延迟，一段时间后查看控制台</p><p>查看接入应用列表</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424151739176-57761e3834f141e2b8be3c40b97d4ad9.png" alt="image-20200424151739176" /></p><p>查看应用metric概览统计分析</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424152057519-f56e8b325c8d45d1ad6f4d63620d5e19.png" alt="image-20200424152057519" /></p><p>查看应用详情监控信息，JVM监控</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424152203039-9ece5e27dac541388686a9a285c2b7d3.png" alt="image-20200424152203039" /></p><p>查看应用详情监控信息，CPU、内存、磁盘等主机监控</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424152321428-9a94836a50d24413a58b2fc3329f550a.png" alt="image-20200424152321428" /></p><p>应用诊断，线程分析，分析线程栈信息</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424152807520-b7ba95d4e5044fd9a0b5688ac606036b.png" alt="image-20200424152807520" /></p><p>docker安装探针，参考：<a href="https://help.aliyun.com/document_detail/85906.html">https://help.aliyun.com/document_detail/85906.html</a>?<a href="https://help.aliyun.com/document_detail/85906.html?spm=a2c4g.11186623.6.588.58b8e108Khh65I">spm=a2c4g.11186623.6.588.58b8e108Khh65I</a></p><p>决定应用上下文id的名称org.springframework.boot.context.ContextIdApplicationContextInitializer.getApplicationId(ConfigurableEnvironment)</p><h4 id="微服务应用测试">微服务应用测试</h4><h5 id="准备应用">准备应用</h5><p>像单应用测试一样准备多个微服务应用demo</p><p>account-demo <a href="https://github.com/wenqy/springcloud-study/tree/master/account-demo">https://github.com/wenqy/springcloud-study/tree/master/account-demo</a></p><p>order-demo <a href="https://github.com/wenqy/springcloud-study/tree/master/order-demo">https://github.com/wenqy/springcloud-study/tree/master/order-demo</a></p><p>inventory-demo <a href="https://github.com/wenqy/springcloud-study/tree/master/inventory-demo">https://github.com/wenqy/springcloud-study/tree/master/inventory-demo</a></p><p>eureka-server <a href="https://github.com/wenqy/springcloud-study/tree/master/eureka-server">https://github.com/wenqy/springcloud-study/tree/master/eureka-server</a></p><p>涉及数据库的，对RDS数据库表初始化</p><h5 id="构建并启动镜像">构建并启动镜像</h5><p>Docker构建微服务demo镜像</p><pre><code class="language-sh">docker build -t weny/account-demo:1.0.0 .docker build -t weny/order-demo:1.0.0 .docker build -t weny/inventory-demo:1.0.0 .docker build -t weny/eureka-server:1.0.0 .</code></pre><p>然后像单应用测试一样为每个微服务demo应用安装Agent镜像，最后启动Agent镜像</p><pre><code>docker run -d -p 8400:8400 --name order-demo wenqy/agent-order-demo:1.0.0docker run -d -p 8401:8401 --name inventory-demo wenqy/agent-inventory-demo:1.0.0docker run -d -p 8402:8402 --name account-demo wenqy/agent-account-demo:1.0.0docker run -d -p 9000:9000 --name eureka-server wenqy/eureka-server:1.0.0</code></pre><p>测试应用链路是否正常</p><pre><code>[root@iZ2ze7666pvq2l4otgrbseZ ~]# curl -X POST &quot;http://172.17.0.1:8400/order/orderPay?amount=1&amp;count=1&quot;success[root@iZ2ze7666pvq2l4otgrbseZ ~]# curl -X POST &quot;http://172.17.0.1:8400/order/orderPay?amount=1&amp;count=1&quot;success[root@iZ2ze7666pvq2l4otgrbseZ ~]# </code></pre><p>服务调用关系</p><pre><code>order-service -&gt; account-service      -&gt; inventory-service</code></pre><p>成功插入订单，服务调用正常</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424210440614-488d8a4d1aa74b46ab79bef4052b81eb.png" alt="image-20200424210440614" /></p><p>编写访问请求的脚本，让监控探针采集一段时间</p><pre><code class="language-shell">#!/bin/bashwhile truedocurl -X POST &quot;http://172.17.0.1:8400/order/orderPay?amount=1&amp;count=1&quot;sleep 2done</code></pre><p>登录控制台，等待一段时间查看结果</p><h5 id="arms图例">ARMS图例</h5><p>调用链路查询，查询调用链路列表</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424212804528-b6f77775b9b04803b6694755bdc74e30.png" alt="image-20200424212804528" /></p><p>调用链路详情，查看调用关系和耗时</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424212847358-f03fe3ffcd0f409aba38a1ec4cc9a1fb.png" alt="image-20200424212847358" /></p><p>调用链路方法栈，查看方法栈耗时</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424212906055-c73dee545b4e47ae834d97a6cb2354be.png" alt="image-20200424212906055" /></p><p>应用列表，加入探针的应用列表</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213006101-41d8c3920ea6481aa9c675375242a8d4.png" alt="image-20200424213006101" /></p><p>应用总览，CPU、内存等系统信息</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213202626-fed50cdc572c4d808ee693e3fef74e64.png" alt="image-20200424213202626" /></p><p>应用总览，慢调用统计分析信息</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213120089-add697a272f649d3b4230a47caae756e.png" alt="image-20200424213120089" /></p><p>应用总览，服务拓扑图</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213248959-496eb7e76d2d44459e2610225607a8a3.png" alt="image-20200424213248959" /></p><p>应用详情，SQL分析，SQL调用次数和平均耗时</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213353984-573507cc420345db955b7cd7f8e15f85.png" alt="image-20200424213353984" /></p><p>应用详情，异常分析，异常统计及异常堆栈</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213410635-01cf0ec14e524cb1b08f3d3f06fede9c.png" alt="image-20200424213410635" /></p><p>应用详情，错误分析</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213421806-c002da12b3da4457a74dc3d282204594.png" alt="image-20200424213421806" /></p><p>应用详情，接口快照</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213454698-53adce9b9ca748e4b268fdc855314860.png" alt="image-20200424213454698" /></p><p>接口调用，错误分析</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213547982-d560ae180b1d4172a503437ae395526d.png" alt="image-20200424213547982" /></p><p>接口调用，链路下游</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213612952-cbc6720651834db5bba64411c3c9deb0.png" alt="image-20200424213612952" /></p><p>数据库调用，QPS、响应时间统计</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213755250-996b098bcd374ac19dfb2d170344dd4a.png" alt="image-20200424213755250" /></p><p>外部调用，HTTP状态统计、错误数统计等等</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213821169-970497ec28e4420cb752af0ed3ec6d68.png" alt="image-20200424213821169" /></p><p>应用诊断，异常分析</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200424213944981-025402c8dd734a2196fb655e642d00e9.png" alt="image-20200424213944981" /></p><p>进入Agent容器，查出探针日志</p><pre><code>~/diamond/fixed-139.196.135.144_diamond/snapshot-tenant/c845a7b4-23a1-4f28-a380-5ab30d8a280f/cn-hangzhou # cat arms.trace.cj44pq0njo-3a979069aa8c619 {&quot;profiler&quot;:{&quot;agent&quot;:{&quot;logger&quot;:{&quot;level&quot;:&quot;WARN&quot;}},&quot;enable&quot;:true,&quot;sampling&quot;:{&quot;enable&quot;:true,&quot;rate&quot;:100,&quot;deadline&quot;:1587738635558},&quot;dubbo&quot;:{&quot;enable&quot;:true},&quot;googlehttpclient&quot;:{&quot;enable&quot;:true},&quot;hsf&quot;:{&quot;enable&quot;:true},&quot;httpclient3&quot;:{&quot;enable&quot;:true},&quot;httpclient4&quot;:{&quot;enable&quot;:true},&quot;jdkhttp&quot;:{&quot;enable&quot;:true},&quot;jetty&quot;:{&quot;enable&quot;:true},&quot;mybatis&quot;:{&quot;enable&quot;:true},&quot;mysql&quot;:{&quot;enable&quot;:true},&quot;okhttp&quot;:{&quot;enable&quot;:true},&quot;oracle&quot;:{&quot;enable&quot;:true},&quot;postgresql&quot;:{&quot;enable&quot;:true},&quot;redis&quot;:{&quot;enable&quot;:true},&quot;spring&quot;:{&quot;enable&quot;:true},&quot;springboot&quot;:{&quot;enable&quot;:true},&quot;tomcat&quot;:{&quot;enable&quot;:true},&quot;mongodb&quot;:{&quot;enable&quot;:true},&quot;lettuce&quot;:{&quot;enable&quot;:true},&quot;grpc&quot;:{&quot;enable&quot;:true},&quot;thrift&quot;:{&quot;enable&quot;:true},&quot;defined&quot;:{&quot;excludeurl&quot;:&quot;&quot;},&quot;thresholds&quot;:{&quot;apdex&quot;:500,&quot;sql&quot;:500,&quot;interface&quot;:500,&quot;limit&quot;:100},&quot;callstack&quot;:{&quot;maxLength&quot;:128},&quot;callsql&quot;:{&quot;maxLength&quot;:1024}}}~/diamond/fixed-139.196.135.144_diamond/snapshot-tenant/c845a7b4-23a1-4f28-a380-5ab30d8a280f/cn-hangzhou # ~/diamond/fixed-139.196.135.144_diamond/snapshot-tenant/c845a7b4-23a1-4f28-a380-5ab30d8a280f/cn-hangzhou # cd ~~ # ls -ltrtotal 43816-rw-r--r--    1 root     root      44865284 Apr 24 12:14 ArmsAgent.zipdrwxr-xr-x    1 root     root            18 Apr 24 12:23 ArmsAgentdrwxr-xr-x    3 root     root            43 Apr 24 13:40 diamonddrwxr-xr-x    5 root     root           104 Apr 26 02:34 logs~ # cd logs/~/logs # ls -ltrtotal 2028drwxr-xr-x    3 root     root            23 Apr 24 12:23 armsdrwxr-xr-x    2 root     root            32 Apr 24 12:23 diamond-clientdrwxr-xr-x    2 root     root            57 Apr 26 02:34 spas-rw-r--r--    1 root     root         44448 Apr 26 02:34 spring.log.2020-04-24.0.gz-rw-r--r--    1 root     root       1350554 Apr 26 02:47 spring.log</code></pre><p>查看镜像容器的依赖包，你会发现它依赖许多第三方开源组件，如：pinpoint、zipkin、opencensus等链路跟踪和其他RPC组件等等。</p><p>ARMS提供性能监控、用户体验监控、调用链追踪以及故障诊断等功能，在故障排查，分析和诊断性能瓶颈具重大作用。置于ARMS的Agent方式是否会对应用性能本身造成影响，还有待观察。类似Agent无侵入方法的开源产品有<a href="https://github.com/pinpoint-apm/pinpoint">Pinpoint</a>、<a href="https://skywalking.apache.org/">Skywalking</a>，但很显然，没有商用ARMS整合的大而全。如果应用有ARMS组件的加持，在服务监控预警、故障排查和性能诊断等多方面确实提供很大便利和帮助。</p><p><strong>最后的最后，如果有想买阿里云ECS服务，想自己玩玩的胖友，那就顺手使用我的推广链接吧，买过的直接略过。</strong></p><p><a href="https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=n85o6zdm">https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=n85o6zdm</a></p><h3 id="参考">参考</h3><p><a href="https://help.aliyun.com/product/34364.html?spm=a2c4g.11186623.6.540.167d31d3gH64oQ">https://help.aliyun.com/product/34364.html?spm=a2c4g.11186623.6.540.167d31d3gH64oQ</a></p><p><a href="https://yq.aliyun.com/articles/721312?spm=5176.7946893.1411534..7cfa75baEBYWPa">https://yq.aliyun.com/articles/721312?spm=5176.7946893.1411534..7cfa75baEBYWPa</a></p>]]>
                    </description>
                    <pubDate>Sat, 14 Nov 2020 10:19:06 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Effective-Java-代码如诗]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/effective-java-代码如诗</link>
                    <description>
                            <![CDATA[<p>读一本好书，就像和高尚的人谈话。终于向Java界的经典著作《Effective Java》下手了。《Effective Java》不仅是JDK源码设计的指南，更是上层编程领域的金科玉律。</p><p>你是否想过如何写好一个注释，是否会为了注释而注释？你是否会忽视方法的访问级别？你是否会为了某个类、方法或变量的命名而烦恼？你是否会滥用继承，而不考虑它们之间的关系？你是否会忽视线程安全问题？你是否会随意实现Serializable接口，是否有认真考虑过序列化？你是否注意到自己的代码可能会发生内存泄漏？你是否考虑过怎样运用设计模式？你的API设计是否考虑版本兼容？你是否急于优化代码？你是否会在代码里做了隐藏规则的约定？你是否会复审自己的代码，觉得怎样实现才合理？你是否想过如何写出诗一般的代码，怎么样才能让人赏心悦目？你是否想过强如JDK源码也有着一些不合理的设计？太多太多。。。</p><p>一门语言，无外乎语法、词汇和用法，在用法上推敲琢磨也是司空见惯的事，这用法用的好就是诗，体现了对这门语言的驾驭水平，计算机语言亦是如此，代码可读性，高效性也是开发者的目标。《Effective Java》可以作为开发者的指导方针，引领你走向前方。通用的编程思想、设计原则和模式放之四海而皆准，并不仅仅局限于一门语言。</p><p>它就像一位长者，娓娓道来，诉说着曾经的风雨，分享着经验和教训，教你避开障碍，少走弯路，码出高效。它提出了许多准则和建议，值得我们去遵循或者思考。如果你对这本书能如会贯通，或许写出诗一般的代码就不会这么难了。下面整理了自己的非完全阅读笔记，备忘。诚然，有许多事情没有做到，但每个人都应该向诗人进军</p><ul><li><a href="#effective-java">Effective-Java</a><ul><li><a href="#创建和销毁对象">创建和销毁对象</a></li><li><a href="#对象通用方法">对象通用方法</a></li><li><a href="#类和接口">类和接口</a></li><li><a href="#泛型">泛型</a></li><li><a href="#枚举和注解">枚举和注解</a></li><li><a href="#lambda和stream">Lambda和Stream</a></li><li><a href="#方法">方法</a></li><li><a href="#通用程序设计">通用程序设计</a></li><li><a href="#异常">异常</a></li><li><a href="#并发">并发</a></li><li><a href="#序列化">序列化</a></li></ul></li></ul><h3 id="effective-java">Effective-Java</h3><p><img src="http://blog.wenqy.com/upload/2020/11/image-20201113100915594-6b416bf15d3642bf915900cedd1ac651.png" alt="image-20201113100915594" /></p><h4 id="创建和销毁对象">创建和销毁对象</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E5%88%9B%E5%BB%BA%E5%92%8C%E9%94%80%E6%AF%81%E5%AF%B9%E8%B1%A1-b52aa22c17a3401490790f93b3056beb.png" alt="创建和销毁对象" /></p><h4 id="对象通用方法">对象通用方法</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E5%AF%B9%E8%B1%A1%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95-872dde921b7b4cb7b41db766e91beae2.png" alt="对象通用方法" /></p><h4 id="类和接口">类和接口</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3-5d865212bc9b4d789094a268e1ce9c7a.png" alt="类和接口" /></p><h4 id="泛型">泛型</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E6%B3%9B%E5%9E%8B-d4a6ddc728234b75b010cfbbdf8fc914.png" alt="泛型" /></p><h4 id="枚举和注解">枚举和注解</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E6%9E%9A%E4%B8%BE%E5%92%8C%E6%B3%A8%E8%A7%A3-688cb63bb8ad4de3ade3b581f9395657.png" alt="枚举和注解" /></p><h4 id="lambda和stream">Lambda和Stream</h4><p><img src="http://blog.wenqy.com/upload/2020/11/Lambda%E5%92%8CStream-e9fa60099ac54c1e834bf5140447d8ac.png" alt="Lambda和Stream" /></p><h4 id="方法">方法</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E6%96%B9%E6%B3%95-2991d81312dc49119327ce42092df214.png" alt="方法" /></p><h4 id="通用程序设计">通用程序设计</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E9%80%9A%E7%94%A8%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1-f3c8140e80aa48bc88605245147265c7.png" alt="通用程序设计" /></p><h4 id="异常">异常</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E5%BC%82%E5%B8%B8-0b14e7c6f18f406aba238acde7c121ee.png" alt="异常" /></p><h4 id="并发">并发</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E5%B9%B6%E5%8F%91-3892bf55166c4f77bd150f62bb8f8f10.png" alt="并发" /></p><h4 id="序列化">序列化</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E5%BA%8F%E5%88%97%E5%8C%96-f8c30ead113a422bb75ba75a96abeaf7.png" alt="序列化" /></p>]]>
                    </description>
                    <pubDate>Fri, 13 Nov 2020 10:46:53 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[人性的弱点—这口心灵鸡汤我干了（附带思维导图）]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/人性的弱点md</link>
                    <description>
                            <![CDATA[<p>《人性的弱点》这本书听说过很久了，可我迟迟都没有阅读它的欲望，我曾一度以为，它是像《丑陋的中国人》那样，想要道尽人类脆弱的一面和不足之处，以反省自身，一度怀疑它有标题党之嫌。再者，每个人从小到大，谁都听过无数道理，没有理由去听成功学的“心灵鸡汤”，认为最终都会难免流于说教。</p><p>马云成功了，连马云自己都不知道有没有说过的话的马云成功学出来了，借着人性对成功的本能贪婪，多少人趋之若鹜，当多少人还陷在了追求成功的泥淖里无法自拔的时候，而兜售成功的成功学他们成功了，他们挣得盆满钵满。每一个人的成功都无法复制。如果可以，要看也要看成功者本人说的话，做过的事。（PS：话说，马云老师，做过演员，唱过歌，当过各种职业，就还没出过书。当周鸿祎、刘强东、李彦宏这些互联网大佬都已经出过书，如果马老师自己出一本书，那场面就火爆了，我一定追随一本）。风靡全球，畅销近百年的卡耐基成功学，到底是帮助他人成功，然后他也成功了，还是只有他成功了？</p><p>我无法考证，然而，这搁置许久的书我还是读了，记得是从当当网上下书单，搞促销赠品活动，我顺手把《人性的弱点》赠品勾上了，没想到还是一个选译本。本着不要浪费的原则，我还是读起了它。读完之后，你会发现他并没有像《丑陋的中国人》那样，有着直接丑陋行径的详例描写，更多的是引导你的人生，教你如何更好的把握自己，如何经营的你的人生，提出了许多案例和建议。</p><p>如何看待人际交往，为人处世，如何规划人生，怎样树立自己的金钱观、消费观，如何对待自己的工作，如何经营好自己的家庭等等。对于个人，衣着要干净整洁，仪表得体，笑口常开；与人交往，需热情真诚，以心换心，张弛有度；每个人都要树立自己的目标并为之计划，不断实践调整；对于金钱，取之有道，用之有度，节俭而不吝啬，合理消费，把钱花在刀刃上；对待工作要热忱，充满激情，看到背后的成长机会，不要一味地抱怨；对于婚姻，要相互坦诚，家人之间，鼓励永远比批评重要，还要经营好夫妻生活。</p><p>读完发现太难了，发现好多自己都没有做到。格物致知、修身齐家，做人永远是一个学问，一个难题。一时做到，却难保一世都能做到。这就需要不断地与自己博弈，不断地实践调整。人不可能听尽所有道理，每个人都有自己的知识盲区，适当的心灵鸡汤喝一喝，还是有耳目一新的感觉的。</p><p>对自己进行“盘点”，思考，适时调整自己，如能引发你如何经营自己一生的思考，那它便是成功的。成功对于每个人都可能有不同的定义，但无疑都是每个人的渴望。绝大多数人中没有人可以随随便便成功，我们大都是平凡人，我们如何做好自己？就像阿波罗登月一样，无法根据事先预测的弹道轨迹将飞船送往太空，而是在飞行过程中根据反馈不断调整自己，最终到达终点，完成了人月神话的伟大壮举。人亦是如此，面对未来，尤其是信息时代的今天，复杂可变的因素太多，根本无法考虑全部的可能性，所以，我们需要一个基点，那就是人生目标，根据目标不断地动态控制自己的心态和行为，经营好自己的人生，而不让自己偏离航道而走得越来越远。但是找到一个适合自己的人生目标，却不是一件容易的事情，有可能终其一生都在找寻自己的目标。那就对照自观吧，纸上得来终觉浅，我也一样，先写下这篇读后感，勉励一下自己。</p><p>附录贴下思维导图：</p><h3 id="附录">附录</h3><p><img src="http://blog.wenqy.com/upload/2020/11/image-20201105155639139-3a2cb482837442f899a41ba574801b54.png" alt="image-20201105155639139" /></p><h4 id="把握人际交往的关键">把握人际交往的关键</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E6%8A%8A%E6%8F%A1%E4%BA%BA%E9%99%85%E4%BA%A4%E5%BE%80%E7%9A%84%E5%85%B3%E9%94%AE-5c20c1d8cbe448dea4bcd1d13d7c3d0b.png" alt="把握人际交往的关键" /></p><h4 id="把别人吸引到身边来">把别人吸引到身边来</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E6%8A%8A%E5%88%AB%E4%BA%BA%E5%90%B8%E5%BC%95%E5%88%B0%E8%BA%AB%E8%BE%B9%E6%9D%A5-c46d68cfa0234b4782f58e2b6cf5103b.png" alt="把别人吸引到身边来" /></p><h4 id="做好一生的规划">做好一生的规划</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E5%81%9A%E5%A5%BD%E4%B8%80%E7%94%9F%E7%9A%84%E8%A7%84%E5%88%92-0e259ad0937044e49e5d83a37177e708.png" alt="做好一生的规划" /></p><h4 id="与金钱和睦相处">与金钱和睦相处</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E4%B8%8E%E9%87%91%E9%92%B1%E5%92%8C%E7%9D%A6%E7%9B%B8%E5%A4%84-1604564205848-e51277cb7ae247cdb52e77f326119e71.png" alt="与金钱和睦相处-1604564205848" /></p><h4 id="学会享受工作">学会享受工作</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E5%AD%A6%E4%BC%9A%E4%BA%AB%E5%8F%97%E5%B7%A5%E4%BD%9C-b310befc7f974e5d867cec18395ab711.png" alt="学会享受工作" /></p><h4 id="营造幸福家庭">营造幸福家庭</h4><p><img src="http://blog.wenqy.com/upload/2020/11/%E8%90%A5%E9%80%A0%E5%B9%B8%E7%A6%8F%E5%AE%B6%E5%BA%AD-c81f8ca0f0364225beb8d2f5e9503fa7.png" alt="营造幸福家庭" /></p>]]>
                    </description>
                    <pubDate>Fri, 06 Nov 2020 09:41:40 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[记一次Gitlab-CI集成K8S实录]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/记一次gitlab-ci集成k8s实录md</link>
                    <description>
                            <![CDATA[<p>2019年号称云原生元年，企业全面上云，上云就上云原生。各大云厂商云原生事业如火如荼的进行着。Gitlab也不甘人后，很好的支持和构建云原生项目。部署环境的搭建和配置向来繁杂，云原生之前的年代，搭建和配置部署环境还存在大量人工而且重复地劳动，浪费了大量时间和精力在环境部署上，而且环境难以移植，微服务的兴起更是加剧了环境搭建和配置的难度，对运维也是一大挑战。容器及其编排技术因此而孕育而生，宿主环境的无感知，极易地扩缩容，容器技术存在巨大优势。但容器及其编排环境搭建本身也不是一件容易的事情，各种套件你方唱罢我登场。Iass、Pass层领域容器云环境搭建和维护成本问题都摆在那里，各大云厂商云原生事业才有了广阔的市场前景，都想在这一领域独占鳌头。Docker、Kubernetes、Harbor、Prometheus等集群环境不是本文关注的重点，这里只是记录Gitlab-CI集成K8S的试验，依赖现成的K8S集群环境，但曾经被还原过一次，导致一些配置丢失。CI集成K8S跟集成其他环境步骤相同，同样需要两步，第一步，安装<code>Gitlab-Runner</code>及注册授权，需要注意的是，需要选择Kubernetes执行器；第二步是还是编写<code>.gitlab-ci.yaml</code>文件，只是需要Docker等容器命令的相关知识。此外，由于Kubernetes是基于RBAC角色权限设计，需要有<code>Kubernetes Service Account</code>具有操纵K8S集群的权限。</p><h4 id="gitlab-ci绑定k8s集群">Gitlab-ci绑定k8s集群</h4><p>我们可以先手动一个K8S集群的集成，预置好具有对K8S集群有操纵权限的账号，供后续使用。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200803105705828-95014ad5db4c4a1aa61d094b193dab28.png" alt="image-20200803105705828" /></p><p>然后在K8S环境里准备好集群环境配置参数</p><p>API，URL 指向k8s集群地址</p><pre><code class="language-sh">kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'</code></pre><p>CA certificate， 需要k8s认证</p><pre><code class="language-sh">[root@wenqy gitlab]# kubectl get secretsNAME                  TYPE                                  DATA   AGEdefault-token-xxlhq   kubernetes.io/service-account-token   3      47d[root@wenqy gitlab]# kubectl get secret default-token-xxlhq -o jsonpath=&quot;{['data']['ca\.crt']}&quot; | base64 --decode-----BEGIN CERTIFICATE-----MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl......gINmxCg80ZwsEXEUo8UE2ybYVd/GmhPShuW/UriJcj6Ncx9viX2EsyoviVU=-----END CERTIFICATE-----[root@wenqy gitlab]# </code></pre><p>Token， K8S授予具有<a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles"><code>cluster-admin</code></a>账号权限的token</p><p>K8S集群环境创建至少有<code>container.clusterRoleBindings.create</code>权限的账号</p><pre><code class="language-bash">[root@wenqy gitlab]# cat gitlab-admin-service-account.yaml apiVersion: v1kind: ServiceAccountmetadata:  name: gitlab-admin  namespace: kube-system---apiVersion: rbac.authorization.k8s.io/v1beta1kind: ClusterRoleBindingmetadata:  name: gitlab-adminroleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: cluster-adminsubjects:- kind: ServiceAccount  name: gitlab-admin  namespace: kube-system[root@wenqy gitlab]# kubectl apply -f gitlab-admin-service-account.yamlserviceaccount/gitlab-admin createdclusterrolebinding.rbac.authorization.k8s.io/gitlab-admin created</code></pre><p>K8S集群环境获取Token</p><pre><code class="language-sh">[root@wenqy gitlab]# kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep gitlab-admin | awk '{print $1}')Name:         gitlab-admin-token-xsknqNamespace:    kube-systemLabels:       &lt;none&gt;Annotations:  kubernetes.io/service-account.name: gitlab-admin              kubernetes.io/service-account.uid: d67285d1-d538-11ea-ba67-000c29b409baType:  kubernetes.io/service-account-tokenData====ca.crt:     1025 bytesnamespace:  11 bytestoken:      eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWN......5qcaEOsXRuVrmG0hAHqVHpAvryuQ3CA[root@wenqy gitlab]# </code></pre><p>如果出现<code>is blocked: Requests to the local network are not allowed</code>错误，需要用管理员账号开启配置，允许访问本地网络</p><p><img src="http://blog.wenqy.com/upload/2020/11/clip_image001-d4712f3aefc04c98a58e4c4764319c32.png" alt="clip_image001" /></p><p>填入上述参数后，创建成功</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200803113646082-c0e2732758c94ee0a19754824279bca6.png" alt="image-20200803113646082" /></p><p>这一步可能不是必须的，需要注意是对哪个名称空间创建服务账号，需要部署在哪个名称空间下，否则可能导致部署失败</p><p>操作页面本身有链接参考</p><p><a href="http://gitlab.wenqy.com/help/user/project/clusters/add_remove_clusters.md#add-existing-cluster">http://gitlab.wenqy.com/help/user/project/clusters/add_remove_clusters.md#add-existing-cluster</a></p><h4 id="k8s安装gitlab-runner">K8S安装Gitlab-Runner</h4><p>安装Gitlab-Runner可以有多种形式，或者使用不同的执行器等等。这里采用Helm+Tiller的形式，直接将Gitlab-Runner部署到K8S集群中。</p><h5 id="安装helm">安装Helm</h5><p>Helm是K8S的应用包管理器，主要用于 Kubernetes 应用程序 Chart 的创建、打包、发布以及创建和管理本地和远程的 Chart 仓库。Helm官方提供一键安装脚本</p><pre><code class="language-sh">curl https://raw.githubusercontent.com/helm/helm/master/scripts/get &gt; get_helm.sh$ chmod 700 get_helm.sh$ ./get_helm.sh</code></pre><h5 id="安装tiller">安装Tiller</h5><p>Tiller是 Helm 的服务端，部署在 Kubernetes 集群中。Tiller 用于接收 Helm 的请求，并根据 Chart 生成 Kubernetes 的部署文件（Helm 称为 Release），然后提交给 Kubernetes 创建应用。官方Tiller镜像被墙，利用dockerhub第三方镜像打tag成官方镜像，可以推送到私有镜像仓库，或者找国内的源</p><pre><code class="language-sh">docker pull fishead/gcr.io.kubernetes-helm.tiller:v2.12.3docker tag fishead/gcr.io.kubernetes-helm.tiller:v2.12.3 gcr.io/kubernetes-helm/tiller:v2.12.3</code></pre><p>helm部署Tiller</p><pre><code class="language-sh">helm init --service-account tiller --tiller-image gcr.io/kubernetes-helm/tiller:v2.12.3 --skip-refresh</code></pre><p>使用国内源</p><pre><code class="language-sh">helm init --service-account tiller --upgrade -i registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.12.3  --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts</code></pre><p>查看Tiller是否部署成功</p><pre><code class="language-sh">kubectl get pods -n kube-system|grep tiller</code></pre><p>如果出现<code>ImagePullBackOff</code></p><pre><code class="language-sh">[root@wenqy gitlab]# kubectl get pods -n kube-system|grep tillertiller-deploy-85d786df69-l6ch4            0/1     ImagePullBackOff   0          12d</code></pre><p>可以先拉取tiller镜像到本地，编辑配置文件，把拉取策略改为<code>imagePullPolicy:Never</code>，只从本地拉取</p><pre><code class="language-sh">#使用命令编辑配置，IfNotPresent ：如果本地存在镜像就优先使用本地镜像kubectl edit deployment tiller-deploy -n kube-system</code></pre><p>重启Tiller pod</p><pre><code class="language-sh">kubectl get pods -n kube-system|grep tillerkubectl get pod tiller-deploy-d87494c4-qnr7t -n kube-system -o yaml | kubectl replace --force -f -</code></pre><p>给Tiller授权</p><p>创建 Kubernetes 的服务帐号和绑定角色</p><pre><code class="language-sh">kubectl create serviceaccount --namespace kube-system tillerkubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller</code></pre><p>使用 kubectl patch 更新 API 对象</p><pre><code class="language-sh">kubectl patch deploy --namespace kube-system tiller-deploy -p '{&quot;spec&quot;:{&quot;template&quot;:{&quot;spec&quot;:{&quot;serviceAccount&quot;:&quot;tiller&quot;}}}}'</code></pre><p>查看是否授权成功</p><pre><code class="language-sh">kubectl get deploy --namespace kube-system   tiller-deploy  --output yaml|grep  serviceAccount</code></pre><p>查看是否安装成功</p><pre><code class="language-sh">[root@wenqy gitlab]# helm versionClient: &amp;version.Version{SemVer:&quot;v2.16.1&quot;, GitCommit:&quot;bbdfe5e7803a12bbdf97e94cd847859890cf4050&quot;, GitTreeState:&quot;clean&quot;}Server: &amp;version.Version{SemVer:&quot;v2.16.1&quot;, GitCommit:&quot;bbdfe5e7803a12bbdf97e94cd847859890cf4050&quot;, GitTreeState:&quot;clean&quot;}</code></pre><h5 id="安装gitlab-runner">安装Gitlab-Runner</h5><p>准备url、token等信息</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200803160710387-8d9c16d57f9941799c75df02e23b4165.png" alt="image-20200803160710387" /></p><p>准备Helm <code>values.yaml</code>配置，注意填写gitlabUrl、token、tag、harbor secret和hostAliases等信息或者限制资源</p><pre><code class="language-yaml">imagePullPolicy: IfNotPresentgitlabUrl: http://gitlab.wenqy.com/runnerRegistrationToken: &quot;aYT_P3W8qddRy6hpFf7a&quot;terminationGracePeriodSeconds: 3600concurrent: 10checkInterval: 30rbac:  create: false  clusterWideAccess: false  # serviceAccountName: default  podSecurityPolicy:         enabled: false    resourceNames:            - gitlab-runnermetrics:                                                                                                       enabled: truerunners:  image: ubuntu:18.04  tags: &quot;k8s-runner&quot;  privileged: true  pollTimeout: 180  outputLimit: 4096  cache: {}  builds: {}  services: {}  helpers: {}securityContext:  fsGroup: 65533  runAsUser: 100resources: {}affinity: {}nodeSelector: {}tolerations: []hostAliases:                                                                                                 - ip: &quot;192.168.1.106&quot;    hostnames: [&quot;dockerhub.wenqy.com&quot;]                                                                       podAnnotations: {}podLabels: {}</code></pre><p>由于K8S集群环境没有配置VPC，这里没有开启缓存，需要注意的是，使用<code>docker in docker</code>构建镜像 时<code>privileged</code>必须为true，开启特权模式，即<code>privileged: true</code>，否则会报无法连接docker daemon的错误，开启特权意味着Pod对docker的资源限制可能失效</p><p>参考</p><p><a href="https://gitlab.com/gitlab-org/charts/gitlab-runner/-/blob/master/values.yaml">https://gitlab.com/gitlab-org/charts/gitlab-runner/-/blob/master/values.yaml</a></p><p>这里总是按照最新的Gitlab-runner版本，利用Helm安装Gitlab-runner</p><pre><code class="language-sh">helm repo add gitlab https://charts.gitlab.iohelm install --namespace gitlab --name gitlab-runner -f  values.yaml gitlab/gitlab-runner</code></pre><p>如果提示报错:</p><pre><code class="language-sh">[root@wenqy gitlab]# helm install --namespace gitlab --name gitlab-runner -f values.yaml gitlab/gitlab-runnerError: Could not get apiVersions from Kubernetes: unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request</code></pre><p>删除无用api，再次安装Gitlab-Runner</p><pre><code class="language-sh">[root@wenqy gitlab]# kubectl get apiserviceNAME                                   SERVICE                      AVAILABLE                  AGEv1beta1.extensions                     Local                        True                       47dv1beta1.metrics.k8s.io                 kube-system/metrics-server   False (MissingEndpoints)   47dv2beta2.autoscaling                    Local                        True                       47d[root@wenqy gitlab]# kubectl delete apiservice v1beta1.metrics.k8s.ioapiservice.apiregistration.k8s.io &quot;v1beta1.metrics.k8s.io&quot; deleted</code></pre><p>查看Gitlab-Runner pod状态</p><pre><code class="language-sh">kubectl describe pod gitlab-runner-gitlab-runner-9ffb46694-d62rc -n gitlab</code></pre><p>校验Gitlab-Runner是否成功</p><pre><code class="language-sh">[root@wenqy gitlab]# kubectl get pod -n gitlabNAME                                          READY   STATUS    RESTARTS   AGEgitlab-runner-gitlab-runner-9ffb46694-d62rc   1/1     Running   0          27m</code></pre><p>回到gitlab，gitlab-runner出现绿色实心圈，说明注册成功了</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200803153119363-aebfcc429d0244c08c92718594bbcafa.png" alt="image-20200803153119363" /></p><h4 id="ci流水线">CI流水线</h4><h5 id="变量定义">变量定义</h5><p>编写<code>.gitlab-ci.yaml</code>文件前，可以先定义一些变量，保存kubeConfig或者私有镜像仓库的账号信息等等</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200803154739696-87b333179e85439b97626f874957cc5f.png" alt="image-20200803154739696" /></p><p>获取kube_config编码串，定义到变量中，利用kube_config访问集群，在deploy阶段可有用</p><pre><code class="language-sh">echo $(cat ~/.kube/config | base64) | tr -d &quot; &quot;</code></pre><p>gitlab-ci部署阶段，将编码串解码写入配置</p><pre><code>variables:  KUBECONFIG: /etc/deploy/config  ....deploy_k8s_job:  image: dockerhub.wenqy.com/gitlab/kubectl:1.0.0  stage: deploy_k8s  tags:    - k8s-runner  before_script:    - mkdir -p /etc/deploy    - echo $kube_config |base64 -d &gt; $KUBECONFIG</code></pre><p>如果Gitlab-ci绑定k8s集群使用的Service Account对指定的namespace拥有create权限的话，是可以跳过这步kube_config配置的。</p><h5 id="重新分配service-account">重新分配Service Account</h5><p>由于k8s环境被人还原过一次，很多配置都丢失了，gitlab-ci绑定的Service Account不一致，gitlab-ci报错：</p><pre><code class="language-sh">ERROR: Job failed (system failure): pods is forbidden: User &quot;system:serviceaccount:gitlab:default&quot; cannot create resource &quot;pods&quot; in API group &quot;&quot; in the namespace &quot;gitlab&quot;</code></pre><p>新增namespace为gitlab,name为default的Service Account</p><pre><code class="language-yaml"># Source: gitlab-runner/templates/service-account.yamlapiVersion: v1kind: ServiceAccountmetadata:  name: default  namespace: gitlab---# Source: gitlab-runner/templates/role.yamlapiVersion: rbac.authorization.k8s.io/v1kind: &quot;ClusterRole&quot;metadata:  name: default  namespace: gitlabrules:- apiGroups: [&quot;*&quot;]  resources: [&quot;*&quot;]  verbs: [&quot;*&quot;]---# Source: gitlab-runner/templates/role-binding.yamlapiVersion: rbac.authorization.k8s.io/v1kind: &quot;ClusterRoleBinding&quot;metadata:  name: default  namespace: gitlabroleRef:  apiGroup: rbac.authorization.k8s.io  kind: &quot;ClusterRole&quot;  name: defaultsubjects:- kind: ServiceAccount  name: default  namespace: gitlab</code></pre><p>删除原有gitlab-runner</p><pre><code>[root@wenqy gitlab]# helm lsNAME            REVISION        UPDATED                         STATUS          CHART                   APP VERSION     NAMESPACEgitlab-runner   1               Mon Aug  3 18:11:11 2020        DEPLOYED        gitlab-runner-0.19.1    13.2.1          gitlab   [root@wenqy gitlab]# helm del --purge gitlab-runner</code></pre><p>可以修改<code>values.yaml</code>，添加serviceAccountName属性，指定gitlab-runner使用Service Account账号角色权限操纵</p><pre><code>runners:  serviceAccountName: default</code></pre><p>参考</p><p><a href="https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3841">https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3841</a></p><p>重建Gitlab-Runner,可以指定Gitlab-Runner版本</p><pre><code class="language-sh">helm install --namespace gitlab --name gitlab-runner -f  values.yaml gitlab/gitlab-runner --version 0.14.0</code></pre><h5 id="分配imagepullsecrets">分配imagePullSecrets</h5><p>重新注册Gitlab-Runner后，gitlab-ci报错，无法拉取自定义镜像</p><pre><code> ERROR: Job failed (system failure): prepare environment: image pull failed: rpc error: code = Unknown desc = Error response from daemon: pull access denied for xxxxxxx/gitlab/maven_deps, repository does not exist or may require 'docker login'. Check https://docs.gitlab.com/runner/shells/index.html#shell-profile-loading for more information</code></pre><p>可以暂时先在K8S集群宿主机里手动拉取私服上的自定义镜像，默认拉取策略是<code>IfNotPresent</code>，优先从本地拉取，而出现问题原因没有配置拉取镜像的秘钥，指向docker registry的域名映射，在<code>values.yaml</code>分配秘钥，重新安装Gitlab-Runner</p><pre><code class="language-yaml">runners:                                                                                                     imagePullSecrets:[&quot;harbor-secret&quot;]hostAliases:                                                                                                 - ip: &quot;192.168.181.106&quot;    hostnames: [&quot;dockerhub.wenqy.com&quot;]</code></pre><h5 id="开启特权模式">开启特权模式</h5><p>继续执行gitlab-ci流水线，问题来到了下面：</p><pre><code class="language-sh">68 $ docker login -u $REGISTRY_USERNAME -p $REGISTRY_PASSWORD dockerhub.wenqy.com69 WARNING! Using --password via the CLI is insecure. Use --password-stdin.70 time=&quot;2020-08-03T10:47:18Z&quot; level=info msg=&quot;Error logging in to v2 endpoint, trying next endpoint: Get https://dockerhub.wenqy.com/v2/: dial tcp xx.xx.xx.xx:443: connect: connection refused&quot;71 Get https://dockerhub.wenqy.com/v2/: dial tcp xx.xx.xx.xx:443: connect: connection refused72 ERROR: Job failed: command terminated with exit code 1</code></pre><p>这次在k8s集群环境还原之前，出现过类似问题，当时是<code>docker in docker</code>版本问题所导致，采用版本降级处理。那现在为什么不行了呢？因为使用的docker image是<code>stable</code>,稳定版可能升级了，我把矛头还是指向了docker版本问题，使用具体的低版本，还在<code>docker login</code>之前使用<code>docker info</code>，还是出现问题：</p><pre><code class="language-sh">docker: Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?.</code></pre><p>太多人碰到了这个问题，官方文档为此都出了故障排除集锦。问题虽已解决过，还是纠结于其中。基础环境已经一致，应当刹车才对。查看官方文档，docker-dind需要开启特权模式。由于k8s环境还原，helm <code>values.xml</code>文件也丢失了，凭借印象，还特地把privileged改为false，不知其所以然，实在是尴尬。按照说法，docker需要挂载<code>/var/run/docker.sock</code>与docker daemon通信，docker容器内默认是普通用户权限，这时需要提升到root权限。</p><pre><code class="language-yaml"># values.yamlrunners:               privileged: true </code></pre><p>特权模式也可能带来相应的安全隐患，据官网说法还可能带来一定的性能损失，<code>docker-dind</code>应该不是一个值得推荐的build方式。另外一种是使用google的<code>kaniko</code>，但由于众所周知的The Great Firewall of China，kaniko是被屏蔽的。知识终究是有国界的，因为知识最终是以人为载体的。可以考虑第三方kaniko，这里不做尝试。</p><p>参考</p><p><a href="https://docs.gitlab.com/runner/executors/kubernetes.html#configuring-executor-service-account">https://docs.gitlab.com/runner/executors/kubernetes.html#configuring-executor-service-account</a></p><p><a href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-workflow-with-docker-executor">https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-workflow-with-docker-executor</a></p><h5 id="kube-config配置">kube_config配置</h5><p>因为使用了自定义变量kube_config配置，k8s环境还原后，配置已然不对，导致报错：</p><pre><code class="language-sh">unable to recognize &quot;deployment.yaml&quot;: Get https://192.168.1.141:6443/api?timeout=32s: x509: certificate signed by unknown authority (possibly because of &quot;crypto/rsa: verification error&quot; while trying to verify candidate authority certificate &quot;kubernetes&quot;)</code></pre><p>在k8s宿主机上重新获取配置，定义变量值，解决问题</p><pre><code class="language-sh">sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configecho $(cat ~/.kube/config | base64) | tr -d &quot; &quot;</code></pre><h5 id="自定义镜像">自定义镜像</h5><p>由于没有启动缓存，项目每次构建时需要从阿里云私服下载依赖都非常慢，先迫不得已做个妥协，做一个黑盒镜像，先把依赖缓存到镜像，在推送到镜像私服上去，加快构建步骤，这步并不是必须的。</p><pre><code class="language-sh">docker run it maven:3.6.2-jdk-14 sh# 容器内创建maven工程，跑pommvn -s ci_settings.xml --batch-mode package -B -DskipTests# 退出容器后提交一个新的镜像docker psdocker commit 065b4f9d20ac maven_deps:3.6.2-jdk-14docker tag  maven_deps:3.6.2-jdk-14 dockerhub.wenqy.com/gitlab/maven_deps:3.6.2-jdk-14</code></pre><h5 id="dockerdind版本问题">docker:dind版本问题</h5><p>因为之前说过docker in docker 版本问题，在构建应用镜像的阶段也会导致下面错误：</p><pre><code class="language-sh">docker: Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?.</code></pre><p>问题的原因在于docker:dind的19.03版本以后，默认会开启TLS通信，由于k8s集群环境没有开启TLS，需要关闭TLS，或者降级dind版本到18.09。</p><p><strong>关闭TLS</strong></p><p>将TLS证书目录路径设空<code>DOCKER_TLS_CERTDIR: ''</code></p><pre><code class="language-yaml">docker_build_job:  #image: docker:stable  image: docker:19.03.0  stage: docker_build  variables:    DOCKER_DRIVER: overlay2    DOCKER_TLS_CERTDIR: ''    #CI_DEBUG_TRACE: &quot;true&quot;  services:    - name: docker:19.03.0-dind</code></pre><p><strong>降级版本</strong></p><p>将docker:dind版本降级到18.09</p><pre><code class="language-yaml">docker_build_job:  image: docker:18.09.7  stage: docker_build  variables:    DOCKER_DRIVER: overlay2    DOCKER_HOST: tcp://localhost:2375    #CI_DEBUG_TRACE: &quot;true&quot;  services:    - name: docker:18.09.7-dind      entrypoint: [&quot;dockerd-entrypoint.sh&quot;]      command: [&quot;--insecure-registry&quot;, &quot;dockerhub.wenqy.com&quot;]</code></pre><p>因为Harbor Registry服务器是开启Https的，需要设置<code>insecure-registry</code>参数，在Https不可用的情况下，docker可以回退到http。否则可能会出现x509等类似的问题。</p><pre><code class="language-sh">WARNING! Using --password via the CLI is insecure. Use --password-stdin. time=&quot;2020-04-02T04:09:23Z&quot; level=info msg=&quot;Error logging in to v2 endpoint, trying next endpoint: Get https://dockerhub.wenqy.com/v2/: x509: certificate is valid for www.wenqy.com, wenqy.com, not dockerhub.wenqy.com&quot; Get https://dockerhub.wenqy.com/v2/: x509: certificate is valid for www.wenqy.com, wenqy.com, not dockerhub.wenqy.com</code></pre><h5 id="指定node运行">指定node运行</h5><p>因为k8s环境是别人的，只是挂了一个节点到k8s环境，k8s环境还原后，node节点label信息也丢失了，匹配不到node节点运行pod</p><pre><code class="language-sh">Events:  Type     Reason            Age                From               Message  ----     ------            ----               ----               -------  Warning  FailedScheduling  13m (x3 over 13m)  default-scheduler  0/3 nodes are available: 3 node(s) didn't match node selector.</code></pre><p>k8s利用<code>label</code>标签来绑定到特定node运行pod</p><pre><code class="language-sh">[root@wenqy .kube]# [root@wenqy .kube]# kubectl get nodes --show-labelsNAME              STATUS   ROLES    AGE   VERSION   LABELSwenqy   Ready    master   49d   v1.13.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=wenqy,node-role.kubernetes.io/master=harbornode01         Ready    &lt;none&gt;   49d   v1.13.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=harbornode01,vsftp=ftpharbornode03         Ready    &lt;none&gt;   49d   v1.13.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=harbornode03[root@wenqy .kube]#  [root@wenqy .kube]# kubectl label nodes harbornode03 disktype=ssdnode/harbornode03 labeled[root@wenqy .kube]# kubectl get nodes --show-labelsNAME              STATUS   ROLES    AGE   VERSION   LABELSwenqy   Ready    master   49d   v1.13.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=wenqy,node-role.kubernetes.io/master=harbornode01         Ready    &lt;none&gt;   49d   v1.13.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=harbornode01,vsftp=ftpharbornode03         Ready    &lt;none&gt;   49d   v1.13.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/hostname=harbornode03</code></pre><p>指定node运行pod</p><pre><code class="language-yaml">#deployment.yamlspec:  ...  template:    ...    spec:      nodeSelector:        disktype: ssd # lable 指定node节点跑</code></pre><h5 id="k8s生成secrets">K8s生成Secrets</h5><p>CI流水线成功部署，pod启动出现<code>ImagePullBackOff</code>，报错详情提示需要docker login</p><p>k8s还原，secret也丢失了，创建secret，指定名称空间</p><pre><code>[root@wenqy .kube]# kubectl create secret docker-registry harbor-secret --docker-server=dockerhub.wenqy.com --docker-username=gitlab --docker-password=Gitlab123456 -n gitlabsecret/harbor-secret created[root@wenqy .kube]# kubectl  get secretNAME                  TYPE                                  DATA   AGEdefault-token-xxlhq   kubernetes.io/service-account-token   3      49dharbor-secret         kubernetes.io/dockerconfigjson        1      7s[root@wenqy .kube]# </code></pre><h5 id="资源限制">资源限制</h5><p>k8s环境资源有限，还是借挂节点，需要对CPU、memory等资源进行限制，防止对集群的崩溃</p><pre><code class="language-yaml">spec:  ...  template:    ...    spec:      ...      containers:      - name: springboot-hsf-test        image: dockerhub.wenqy.com/gitlab/springboot-hsf-test:IMAGE_TAG        imagePullPolicy: Always        resources: #资源管理          requests: #容器运行时，最低资源需求，也就是说最少需要多少资源容器才能正常运行            cpu: 0.1 #CPU资源（核数），两种方式，浮点数或者是整数+m，0.1=100m，最少值为0.001核（1m）            memory: 1024Mi #内存使用量          limits: #资源限制            cpu: 0.5            memory: 2048Mi</code></pre><h5 id="查看logs">查看logs</h5><p>继续，还是出现<code>CrashLoopBackOff</code>，项目启动失败，查看日志</p><pre><code>kubectl logs springboot-hsf-test-89775f989-tt65l -n gitlab</code></pre><p>java启动报错，出现打开jar zip流报错，判断依赖jar包损坏，项目采用阿里云hsf框架jar形式构建，项目启动需要指定Pandora容器的位置，构建docker镜像时，需要从Nginx服务器中拉取<code>Pandora sar</code>包，域名和Nginx转发规则有被人改过，无法下载sar包，到此，发现原因，将sar包传至正确的域名指向位置</p><h5 id="配置ingress">配置ingress</h5><p>流水线执行成功，成功部署至k8s，直接访问内网IP健康检查地址，说明项目启动成功</p><pre><code class="language-sh">[root@wenqy ~]# kubectl get pod -n gitlab -o wideNAME                                          READY   STATUS    RESTARTS   AGE     IP           NODE        NOMINATED NODE   READINESS GATESgitlab-runner-gitlab-runner-df75bf48d-wg644   1/1     Running   0          16h     10.96.1.55   harbornode01   &lt;none&gt;           &lt;none&gt;springboot-hsf-test-54cd4ff877-7n8sp          1/1     Running   0          9m50s   10.96.2.56   harbornode03   &lt;none&gt;           &lt;none&gt;[root@wenqy ~]# [root@wenqy ~]# curl 10.96.2.56:18081/healthok[root@wenqy ~]# </code></pre><p>配置项目K8S Service类型为<code>ClusterIP</code>，通过集群的内部 IP 暴露服务，服务只能够在集群内部可以访问，这也是默认的 <code>ServiceType</code></p><pre><code class="language-yaml">#deployment.yaml---apiVersion: v1kind: Servicemetadata:  name: springboot-hsf-testspec:  ports:  - port: 18081    targetPort: 18081    name: springboot-hsf-test  selector:    app: springboot-hsf-test  type: ClusterIP</code></pre><p>利用Ingress暴露服务给外部，相当于外部负载均衡器，配置网址映射规则和路径匹配会路由到一个或多个后端服务。</p><pre><code class="language-yaml">[root@wenqy gitlab]# cat ingress.yaml apiVersion: extensions/v1beta1kind: Ingressmetadata:  name: gitlab-ingress  namespace: gitlab  annotations:    nginx.ingress.kubernetes.io/rewrite-target: /spec: rules: - host: busi.com   http:     paths:     - path: /springboot-hsf-test       backend:         serviceName: springboot-hsf-test         servicePort: 18081[root@wenqy gitlab]# kubectl apply -f ingress.yaml </code></pre><p>host配置为域名，不能配置为IP，会报错</p><pre><code class="language-sh">[root@wenqy gitlab]# kubectl get ingress -n gitlabNAME             HOSTS      ADDRESS   PORTS   AGEgitlab-ingress   busi.com             80      25m[root@wenqy gitlab]# kubectl describe ingress gitlab-ingress -n gitlabName:             gitlab-ingressNamespace:        gitlabAddress:          Default backend:  default-http-backend:80 (&lt;none&gt;)Rules:  Host      Path  Backends  ----      ----  --------  busi.com              /springboot-hsf-test   springboot-hsf-test:18081 (&lt;none&gt;)Annotations:  kubectl.kubernetes.io/last-applied-configuration:  {&quot;apiVersion&quot;:&quot;extensions/v1beta1&quot;,&quot;kind&quot;:&quot;Ingress&quot;,&quot;metadata&quot;:{&quot;annotations&quot;:{&quot;nginx.ingress.kubernetes.io/rewrite-target&quot;:&quot;/&quot;},&quot;name&quot;:&quot;gitlab-ingress&quot;,&quot;namespace&quot;:&quot;gitlab&quot;},&quot;spec&quot;:{&quot;rules&quot;:[{&quot;host&quot;:&quot;busi.com&quot;,&quot;http&quot;:{&quot;paths&quot;:[{&quot;backend&quot;:{&quot;serviceName&quot;:&quot;springboot-hsf-test&quot;,&quot;servicePort&quot;:18081},&quot;path&quot;:&quot;/springboot-hsf-test&quot;}]}}]}}  nginx.ingress.kubernetes.io/rewrite-target:  /Events:  Type    Reason  Age   From                      Message  ----    ------  ----  ----                      -------  Normal  CREATE  25m   nginx-ingress-controller  Ingress gitlab/gitlab-ingress</code></pre><p>本机配置DNS，修改<code>/etc/hosts</code>配置</p><pre><code class="language-sh">192.168.1.142 busi.com</code></pre><p>用域名访问健康检查URL，成功</p><pre><code class="language-sh">[root@wenqy gitlab]# curl busi.com/springboot-hsf-test/healthok[root@wenqy gitlab]#</code></pre><p>如果有部署类似360开源的wayne这种devops部署平台或者Kubernetes 的DashBoard，可以在管理平台直接创建ingress，进行路由</p><h5 id="运行结果">运行结果</h5><p>流水线最终运行效果</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200806140733583-4d2d7cfd40ea4579b2091a586f68e0a7.png" alt="image-20200806140733583" /></p><p>Pineline执行完后，构建镜像推送到私有仓库，登录Harbor查看</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200814143851535-0784a2d9acb24da9933bada9f9b235b6.png" alt="image-20200814143851535" /></p><p>访问健康检查URL也返回正确结果，项目已经在k8s集群环境中启动成功。</p><p>我们可以自建Maven私服、镜像私服等减少网络带来的延迟进行优化等等。测试环境暂且告一段落了，这里也可以推送部署到阿里云容器服务等第三方云服务厂商</p><h4 id="总结">总结</h4><p>Gitlab CI将应用自动化部署到K8S集群环境步骤还是简单的，但与K8S环境的对接过程还是有点曲折，基础镜像被还原，由于问题和数据没有及时的记录、备份、整理和理解，让人措手不及，凭借对当时的印象和模糊经验，可能反而让你记忆混淆，南辕北辙。英语烂，不解其意，知识储备和理解不够也是问题出现的根源。需要及时总结出现的问题和解决方案，好记性不如烂笔头，需要形成自己的知识库，一知半解的东西更需要记录和备忘，以待备查。Gitlab-Runner版本、docker版本间可能存在较大的差异，需要甄别版本特性，不要使用latest或stable版本，防止自动升级导致意外。提醒我们版本应保持向后兼容，实在重大版本无法兼容，应提供升级方案或者版本回退方案。虽然历史不是简单的重复，但是人总是踏入同一条错误的河流，不然，错误为什么无法避免，战争为何无法避免。</p><p>测试项目和资料：<a href="https://github.com/wenqy/springboot-hsf-test">https://github.com/wenqy/springboot-hsf-test</a></p><h4 id="参考">参考</h4><p><a href="https://help.aliyun.com/document_detail/99943.html?spm=a2c4g.11186623.6.618.69556f171qQLGo">https://help.aliyun.com/document_detail/99943.html?spm=a2c4g.11186623.6.618.69556f171qQLGo</a></p><p><a href="https://docs.docker.com/registry/insecure/#deploy-a-plain-http-registry">https://docs.docker.com/registry/insecure/#deploy-a-plain-http-registry</a></p><p><a href="https://gitlab.com/gitlab-org/gitlab-runner/-/tree/master/">https://gitlab.com/gitlab-org/gitlab-runner/-/tree/master/</a></p><p><a href="https://help.aliyun.com/document_detail/106968.html?spm=5176.11065259.1996646101.searchclickresult.15237c2bLJVndo">https://help.aliyun.com/document_detail/106968.html?spm=5176.11065259.1996646101.searchclickresult.15237c2bLJVndo</a> 使用GitLab CI在Kubernetes服务上运行GitLab Runner并执行Pipeline</p><p><a href="https://about.gitlab.com/releases/2019/07/31/docker-in-docker-with-docker-19-dot-03/">https://about.gitlab.com/releases/2019/07/31/docker-in-docker-with-docker-19-dot-03/</a> Update: Changes to GitLab CI/CD and Docker in Docker with Docker 19.03</p><p><a href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#docker-cannot-connect-to-the-docker-daemon-at-tcpdocker2375-is-the-docker-daemon-running">https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#docker-cannot-connect-to-the-docker-daemon-at-tcpdocker2375-is-the-docker-daemon-running</a></p><p><a href="https://kubernetes.io/zh/docs/concepts/services-networking/ingress/">https://kubernetes.io/zh/docs/concepts/services-networking/ingress/</a></p><p><a href="https://kubernetes.io/zh/docs/concepts/services-networking/service/">https://kubernetes.io/zh/docs/concepts/services-networking/service/</a></p><p><a href="http://360yun.org/wayne/">http://360yun.org/wayne/</a></p><p><a href="https://github.com/cnych/gitlab-ci-k8s-demo">https://github.com/cnych/gitlab-ci-k8s-demo</a></p><p><a href="https://gitee.com/linlion/gitlab-docker-k8s">https://gitee.com/linlion/gitlab-docker-k8s</a></p>]]>
                    </description>
                    <pubDate>Thu, 05 Nov 2020 11:43:25 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Gitlab-CI初识]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/gitlab-ci初识md</link>
                    <description>
                            <![CDATA[<p>GitLab CI (<strong>Continuous Integration</strong>)是GitLab内置的进行持续集成的工具。基于特征分支开发后，需要发起<code>Merge Requests</code>合并共享代码库。<code>Merge Requests</code>总是频繁发生，合并请求过来后，可以触发流水线自动去构建、测试、验证新代码功能，及早发现错误，减少集成问题。我们也总是希望在任何时候都能发布稳定版本的软件，自动推送功能变更到演示环境，甚至是生产环境，完成持续地交付（<strong>Continuous Delivery</strong>）和部署（<strong>Continuous Deployment</strong>），以减少部署带来的风险和及时得到客户的反馈。通过Gitlab CI减少了人为错误的机会和跨团队沟通成本，提高整个团队的效率。</p><p><img src="http://blog.wenqy.com/upload/2020/11/cicd_pipeline_infograph-b09983b0619e4bf9b56f14038869d600.png" alt="cicd_pipeline_infograph" /></p><p><strong>GitLab CI/CD</strong>作为Gitlab整个DevOps生命周期的重要组成部分，对应用的构建、测试、部署和监控都起到重要作用。对于Docker、Kubernetes等容器化技术有着天然的集成，简直就是为云原生而生。</p><p>对于Gitlab项目集成CI/CD很容易上手，像之前【Gitlab概览】提到过的那样，只需两个步骤：1、安装<code>Gitlab-Runner</code>及配置，2、工程目录下创建和编写<code>.gitlab-ci.yaml</code>文件。</p><h4 id="gitlab-runner">Gitlab-Runner</h4><p><code>GitLab-Runner</code>是一个Go语言编写的开源项目，用于运行Job并将结果发送回GitLab，它与GitLab CI/CD一起，协调持续集成服务。它能部署到任何一个地方、任何一个平台，只要它能ping通Gitlab。多个项目可以使用同一个<code>Gitlab-Runner</code>，一个项目也可以使用多个<code>Gitlab-Runner</code>，而<code>Gitlab-Runner</code>还可以并发地执行多个Job，它是灵活的，减轻了Gitlab构建项目的负担。<code>Gitlab-Runner</code>对Docker、Kubernetes的等天然支持，使得项目与容器云服务有很好的集成。</p><h5 id="gitlab-runner类型">Gitlab-Runner类型</h5><p>主要有三种类型的Runner:<a href="https://docs.gitlab.com/ee/ci/runners/#shared-runners">Shared</a>、<a href="https://docs.gitlab.com/ee/ci/runners/#group-runners">Group</a> 、<a href="https://docs.gitlab.com/ee/ci/runners/#specific-runners">Specific</a>。</p><table><thead><tr><th>Shared Runners</th><th>适用于所有应用，且只有系统管理员能够创建</th></tr></thead><tbody><tr><td>Create a group Runner</td><td>项目组下的所有应用，具有组权限的人才能创建</td></tr><tr><td>Specific Runners</td><td>特定项目使用</td></tr></tbody></table><p>我们可以用管理员账号登录，注册<code>Shared Runner</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813163435694-c3a363adb491479eb130b610f725918f.png" alt="image-20200813163435694" /></p><h5 id="安装gitlab-runner">安装Gitlab-Runner</h5><p>安装<code>Gitlab-Runner</code>的版本要注意与Gitlab的版本一致。可能由于新版本引入新功能特性或者Bug，存在版本差异，版本不一致可能造成无法预知的后果。</p><p>以CentOS7，<code>Shell</code>执行器为例，可以利用官方的一键脚本安装最新版本的<code>Gitlab-Runner</code></p><pre><code class="language-sh"># For RHEL/CentOS/Fedoracurl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash</code></pre><p>也可以使用软件源，安装特定版本，注意修改软件源</p><pre><code class="language-sh"># For RHEL/CentOS/Fedorasudo yum install gitlab-runner# for RPM based systemsyum list gitlab-runner --showduplicates | sort -rsudo yum install gitlab-runner-10.0.0-1</code></pre><p>安装时会自动创建<code>Gitlab-Runner</code>的用户，并用这个用户账号运行CI任务。需要注意权限问题。对应Linux root用户，Runner使用的默认配置路径<code>/etc/gitlab-runner/config.toml</code>，对于修改配置，如，并发数参数等，大多数选项，可以不需要重启。默认每5分钟检查一次文件，自动获取所有更改。</p><h5 id="gitlab-runner注册">Gitlab-Runner注册</h5><p><code>Gitlab-Runner</code>拉取Gitlab代码需要授权，这时需要注册URL和Token。以CentOS7，<code>Shell</code>执行器为例，键入命令</p><pre><code class="language-sh">#sudo gitlab-ci-multi-runner registersudo gitlab-runner register</code></pre><p>按步骤，交互式访问需要依次输入GitLab URL、Gitlab token、Description for the Runner、<a href="https://docs.gitlab.com/ee/ci/runners/#using-tags">Tags associated with the Runner</a>、<a href="https://docs.gitlab.com/runner/executors/README.html">Runner executor</a>等等。非容器化项目，Executor这里选<code>Shell</code>，如果想将项目部署到容器，可以选择相应的Docker、Kubernetes执行器。而利用<code>Tags</code>可以指定项目运行的<code>Gitlab-Runner</code>。URL和Token可以在Gitlab上分配，以<code>Specitic Runner</code>为例</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813154011388-4e9a8f9b4dcc4e0bac89df9bb7e6a72e.png" alt="image-20200813154011388" /></p><p>注册成功后，运行<code>Gitlab-Runner</code></p><pre><code class="language-sh">gitlab-runner run</code></pre><p>当出现<strong>绿色实心圈</strong>则表示<code>Gitlab-Runner</code>注册成功，而且正在运行。这时，只要在项目下编写<code>.gitlab-ci.yaml</code>文件就可以啦</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813155020665-56c7ee51fa794790b9c3115992f86219.png" alt="image-20200813155020665" /></p><h4 id="编写gitlab-ciyml">编写.gitlab-ci.yml</h4><p>GitLab CI使用<code>YAML</code>文件(<code>.gitlab-ci.yml</code>)来管理项目配置。默认存放于项目仓库的根目录，它包含了项目如何被编译的描述语句。<code>YAML</code>文件使用一系列约束定义了Job启动时所要做的事情。</p><p>如果<code>.gitlab-ci.yml</code>未指定Runner的<code>tag</code>，可能一直处于<code>Pending</code>状态，这时，可以编辑Runner配置，勾上<code>Run untagged jobs</code>。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813161014290-4fa1e418ecf344b1ac1cbd071d17666d.png" alt="image-20200813161014290" /></p><p><code>Pipelines</code>是整个Gitlab CI最顶层的组件，是一个分成不同阶段(<code>Stage</code>)的作业(<code>Job</code>)的集合。每个推送到 Gitlab 的提交都会产生一个与该提交关联的<code>Pipeline</code>，若一次推送包含了多个提交，则<code>Pipeline</code>与最后那个提交相关联。我们很容易利用每个<code>Pipeline</code>的ID标识做资源隔离，或者作为镜像的版本等等。根据配置可以展现出不同类型的<code>Pipeline</code>，可能是有向无环图(DAG)的，可能是父子结构的等等。</p><p>以<code>Java Maven Jar</code>项目为例，通常包含package、test、deploy这三个步骤。先上效果图：</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813170713024-c2f9064d24564b5b91b4e7a57875869f.png" alt="image-20200813170713024" /></p><p>Gitlab CI Pipeline定义了<code>Stage</code>这个阶段概念，对批量的作业进行了一个逻辑上划分。多个<code>Stage</code>间是顺序执行的，一个 <code>Stage</code>失败，后续的 <code>Stage</code>不会被执行，整个 CI 过程会被认为失败。</p><pre><code class="language-yaml">stages:  - build  - test  - deploy</code></pre><p>上述Maven样例中整个 CI 环节包含三个<code>Stage</code>：build、test 和deploy。分别对应maven的打包、测试、部署阶段。其中一个阶段失败，都会导致后面的阶段不可用。</p><p><code>Job</code>是Runner要执行的指令集合，<code>Job</code> 可以被关联到一个<code>Stage</code>，一个<code>Stage</code>可能包含多个<code>Job</code> 。当一个<code>Stage</code>执行的时候，与其关联的所有<code>Job</code>都会被执行。同一个<code>Stage</code>下的<code>Job</code>是可以并发执行的，至于并发能力，这取决于Runner配置的并发数和服务器并行能力。</p><p>从官网截了一个图出来，比较直观，<code>Stage</code>间是串行的，同个<code>Stage</code>下<code>Job</code>间是并行的。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813182442502-baafa7df6f164b60a90a1e9c20b9de9d.png" alt="image-20200813182442502" /></p><p>我们可以先看下deploy Job的描述片段：</p><pre><code class="language-yaml">deploy:  stage: deploy  script:    - mvn $MAVEN_CLI_OPTS deploy  only:    - master</code></pre><p><code>Job</code>包含<code>script</code>，一段由Runner执行的shell脚本。在script中，对于直接定义<code>|</code>、<code>&gt;</code>等管道和重定向的shell复合命令，有可能会出现莫名问题，需要甄别。我们可以定义<code>before_script</code>，<code>after_script</code>来辅助<code>Job</code>执行<code>script</code>前后需要准备或者收尾的事情。</p><pre><code class="language-yaml">#定义全局 before_script:default:  before_script:    - global before script#覆盖全局before_scriptjob:  before_script:    - execute this instead of global before script  script:    - my command  after_script:    - execute this after my script</code></pre><p>对于多个Job，有些步骤可能是重复的，如Maven的打包和测试阶段都需要寻找依赖，这时，可以利用缓存机制，将依赖缓存下来，加快Job的执行时间。</p><pre><code class="language-yaml">variables:  MAVEN_CLI_OPTS: &quot;-s ci_settings.xml --batch-mode&quot;  MAVEN_OPTS: &quot;-Dmaven.repo.local=.m2/repository&quot;  cache:  paths:    - .m2/repository/    - target/</code></pre><p>很多时候我们并不希望每次push都触发<code>Pipeline</code>，希望定义触发规则还触发<code>Pipeline</code>，我们可能希望指定某个分支，或者以分支名正则关联，或者排除某些分支，或者某个变量的值，或者某个<code>commit</code>的信息，或者某一类代码的变化，或者希望手动触发，或者定时触发，甚至是自定义触发规则等等。这时我们可以利用<code>only</code>和<code>except</code>定义怎么触发<code>Pipeline</code>。</p><pre><code class="language-yaml">  only:    refs:      - branches    variables:      - $ENABLE_VERIFY == &quot;true&quot;    changes:      - &quot;**/*.java&quot;</code></pre><p>在编辑<code>.gitlab-ci.yaml</code>文件中可以使用预置的变量，如：<code>$CI_PIPELINE_ID</code>表示正在构建的<code>Pipeline</code>ID，可以在<code>script</code>中使用这个变量。将预置变量<code>CI_DEBUG_TRACE</code>值设为true，则进入debug模式，将打印更多的CI日志。</p><pre><code class="language-yaml">  variables:    CI_DEBUG_TRACE: &quot;true&quot;</code></pre><p>我们有可能需要<code>Gitlab-Runner</code>生成的结果回传给Gitlab，提供下载。</p><pre><code class="language-yaml">  artifacts:    #expire_in: 1 week    paths:      - target/*.jar</code></pre><p>如果上传打包太大会报错:</p><pre><code class="language-sh">ERROR: Uploading artifacts to coordinator... too large archive  id=407 responseStatus=413 Request Entity Too Large status=413 Request Entity Too Large token=EQ-TLvUS FATAL: too large</code></pre><p>需要调整允许上传附件的最大大小</p><p><img src="http://blog.wenqy.com/upload/2020/11/clip_image001-c2608bbcaa58452f8ab157916620a394.png" alt="clip_image001" /></p><p>修改<code>/etc/gitlab/gitlab.rb</code>Gitlab配置，将<code>client_max_body_size</code>值设为0，表示不限制大小</p><pre><code class="language-yaml">nginx['client_max_body_size'] = 0</code></pre><p>我们有可能引入手动，以便更好的控制部署，有时候会只希望负责人手动批准，触发任务部署。</p><pre><code class="language-yaml">  when: mannual  allow_failure: false</code></pre><p>对于微服务这种项目，我们可能还要打通上下游项目，需要<strong>触发多项目</strong>的<code>Pipeline</code>。而且多个项目间权限不同，有可能还需要引入批准步骤等等，这些高级特性还有待挖掘。语法中还有<strong>模板</strong>的功能，这些就详见官方文档了。</p><p>以<code>Shell</code>为执行器的<code>Gitlab Runner</code>服务器，在运行Maven Job时，需要提前准备JDK\Maven环境。万事俱备之后，Push代码后，直接自动触发<code>Pipeline</code>。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813182226498-bc5c4ddab8f6496293ce6d9815ba5ffe.png" alt="image-20200813182226498" /></p><p>值得注意的是，<code>Gitlab Runner</code>跑<code>Pipeline</code>拉取代码到仓库构建目录，存在一定规则：</p><blockquote><p builds_dir="">/$RUNNER_TOKEN_KEY/$CONCURRENT_ID/$NAMESPACE/$PROJECT_NAME</p></blockquote><p>我们也很容易从<code>Pipeline</code>的日志里看出端倪:<code>/etc/profile.d/builds/53GgNRQd/0/wenqy/simple-maven-dep</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813181255935-6576a01a8ea44754b1cc1f576f98f85a.png" alt="image-20200813181255935" /></p><p>配置邮箱后，构建失败会发起邮箱通知：</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200814141455180-4ba3bc0dd3d8478da580cd67f299a3ce.png" alt="image-20200814141455180" /></p><p>此外<code>YAML</code>格式文件编写还是易出错的，Gitlab嵌入了校验<code>.gitlab-ci.yml</code>内容格式的调试工具，还是比较人性化的。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200813180659907-c16f60ea44af4545a614ce3c359b89f7.png" alt="image-20200813180659907" /></p><p>总之，Gitlab项目要启用CI/CD还是相当容易，<code>.gitlab-c.yaml</code>文件提供了很大的灵活度，可以根据具体项目的需要定制属于自己团队特殊的CI/CD，Gitlab CI/CD无疑为团队的整个效益提供一大助力，为敏捷开发提供了支持。</p><h4 id="参考">参考</h4><p><a href="https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/">https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/</a></p><p><a href="https://docs.gitlab.com/ee/ci/quick_start/">https://docs.gitlab.com/ee/ci/quick_start/</a></p><p><a href="https://docs.gitlab.com/runner/install/linux-repository.html">https://docs.gitlab.com/runner/install/linux-repository.html</a></p><p><a href="https://docs.gitlab.com/runner/install/kubernetes.html">https://docs.gitlab.com/runner/install/kubernetes.html</a></p><p><a href="https://docs.gitlab.com/runner/commands/README.html">https://docs.gitlab.com/runner/commands/README.html</a></p><p><a href="https://docs.gitlab.com/ee/ci/runners/#using-tags">https://docs.gitlab.com/ee/ci/runners/#using-tags</a></p><p><a href="https://docs.gitlab.com/ee/ci/yaml/README.html">https://docs.gitlab.com/ee/ci/yaml/README.html</a></p><p><a href="https://docs.gitlab.com/ee/ci/examples/artifactory_and_gitlab/">https://docs.gitlab.com/ee/ci/examples/artifactory_and_gitlab/</a></p><p><a href="https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Maven.gitlab-ci.yml">https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Maven.gitlab-ci.yml</a></p><p><a href="https://gitlab.com/gitlab-org/gitlab/-/pipelines/177402875">https://gitlab.com/gitlab-org/gitlab/-/pipelines/177402875</a></p>]]>
                    </description>
                    <pubDate>Wed, 04 Nov 2020 20:21:45 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Gitlab安装及使用]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/gitlab安装及使用md</link>
                    <description>
                            <![CDATA[<p><code>Gitlab</code>是基于Ruby on Rails开源的项目管理和代码托管平台，可以很方便的管理公司私有库，所以通常是自建Gitlab。下面来看下Gitlab服务的安装。</p><p>官方硬件需求至少需要<code>4GB</code>内存。在CentOS 7安装为例，安装Gitlab。</p><h4 id="安装配置依赖包">安装配置依赖包</h4><p>Gitlab服务需要系统防火墙打开<code>HTTP</code>, <code>HTTPS</code> 和 <code>SSH</code>访问。这些依赖通常是通用的，运维人员可能已经安装好</p><pre><code class="language-shell">sudo yum install -y curl policycoreutils-python openssh-serversudo systemctl enable sshdsudo systemctl start sshdsudo firewall-cmd --permanent --add-service=httpsudo firewall-cmd --permanent --add-service=httpssudo systemctl reload firewalld</code></pre><h4 id="安装邮局服务">安装邮局服务</h4><p>我们可以安装<code>Postfix</code>进行邮件通知，注意DNS解析。如果安装有其他邮局服务，可以跳过。此外，安装Gitlab后，还需配置smtp。</p><pre><code class="language-shell">sudo yum install postfixsudo systemctl enable postfixsudo systemctl start postfix</code></pre><h4 id="安装gitlab软件包">安装Gitlab软件包</h4><p>我们可以使用官方提供的一键脚本安装包，安装最新稳定版</p><pre><code class="language-sh">//输出到文件里是为了看下下载的脚本内容curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.rpm.sh &gt; rpm.shchmod +x rpm.sh./rpm.sh</code></pre><p>或者在线下载官方包进行安装</p><pre><code class="language-shell">wget -O gitlab.rpm https://packages.gitlab.com/gitlab/gitlab-ce/packages/el/7/gitlab-ce-12.10.4-ce.0.el7.x86_64.rpm/download.rpmrpm -ivh gitlab.rpm#可以查看安装了哪些的文件，看到gitlab安装在/opt/gitlab目录下#rpm -ql gitlab-ce-12.10.4-ce.0.el7.x86_64</code></pre><p>也可以使用软件源安装</p><pre><code class="language-sh"># 配置yum源cat &lt;&lt; EOF &gt;&gt; /etc/yum.repos.d/gitlab-ce.repo[gitlab-ce]name=gitlab-cebaseurl=http://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el6Repo_gpgcheck=0Enabled=1Gpgkey=https://packages.gitlab.com/gpg.keyEOF#更新本地yum缓存sudo yum makecache#安装Gitlab-ce社区版sudo yum install gitlab-ce        #自动安装最新版sudo yum install gitlab-ce-x.x.x    #安装指定版本</code></pre><p>我们还可以将gitlab安装在docker、kubernetes等容器服务里</p><h4 id="修改配置">修改配置</h4><p>默认配置文件路径：<code>/etc/gitlab/gitlab.rb</code>，修改配置后，需要重启服务方可生效。</p><h5 id="配置url">配置URL</h5><pre><code class="language-properties">external_url 'http://gitlab.wenqy.com:80'nginx['listen_port'] = 80</code></pre><p>默认nginx 监听的是80 端口，如果nginx 修改端口，external_url 里面也必须带端口。gitlab安装内置默认用户root，首次访问<code>external_url</code>时，需要root用户登陆，设置密码。如果配置不修改重启，可能会出现502错误。</p><h5 id="配置unicorn端口">配置unicorn端口</h5><p>unicorn是Ruby语言领域的一款<code>http server</code>软件。默认端口是8080。如果是非独享服务器，Tomcat默认端口也是8080，容易发生端口占用冲突，也会导致502错误。此时，可以需要unicorn端口。</p><pre><code class="language-properties">unicorn['port'] = 8901</code></pre><h5 id="配置ssh">配置SSH</h5><p>新暴露了ssh的端口8902</p><pre><code class="language-properties">gitlab_rails['gitlab_ssh_host'] = '192.168.1.34'gitlab_rails['gitlab_shell_ssh_port'] = 8902</code></pre><p>修改ssh协议后，<code>/etc/ssh/sshd_config</code>配置文件新增ssh端口</p><pre><code class="language-properties">#Port 22Port 22port 8902</code></pre><p>防火墙开放端口</p><pre><code class="language-shell">iptables -I INPUT -p tcp -m state --state NEW -m tcp --dport 8902 -j ACCEPTiptables-saveiptables -nL --line-number</code></pre><p>重启ssh服务</p><pre><code class="language-sh">systemctl restart sshd.service</code></pre><h5 id="配置邮箱">配置邮箱</h5><p>填写配置好的smtp信息</p><pre><code class="language-properties">gitlab_rails['smtp_enable'] = truegitlab_rails['smtp_address'] = &quot;smtp.wenqy.com&quot;gitlab_rails['smtp_port'] = 25gitlab_rails['smtp_user_name'] = &quot;wenqy@wenqy.com&quot;gitlab_rails['smtp_password'] = &quot;123456&quot;gitlab_rails['smtp_domain'] = &quot;smtp.uf-tobacco.com&quot;gitlab_rails['smtp_authentication'] = &quot;login&quot;gitlab_rails['smtp_tls'] = falsegitlab_rails['smtp_enable_starttls_auto'] = truegitlab_rails['smtp_openssl_verify_mode'] = 'none'gitlab_rails['smtp_ssl'] = falsegitlab_rails['smtp_force_ssl'] = falsegitlab_rails['gitlab_email_enabled'] = truegitlab_rails['gitlab_email_display_name'] = 'Gitlab'gitlab_rails['gitlab_email_from'] = 'wenqy@wenqy.com'</code></pre><p>重启后进入console测试邮箱发送</p><pre><code class="language-sh">gitlab-rails console#进入控制台，然后发送邮件Notify.test_email('wen.qy@qq.com', '邮件标题', '邮件正文').deliver_now</code></pre><h5 id="重启gitlab">重启gitlab</h5><p>修改配置后，重启生效</p><pre><code class="language-sh">#重器gitlab配置服务sudo gitlab-ctl reconfigure#重启gitlabgitlab-ctl restart </code></pre><h4 id="常用命令">常用命令</h4><pre><code class="language-sh">gitlab-ctl start #启动全部服务gitlab-ctl restart #重启全部服务gitlab-ctl stop #停止全部服务gitlab-ctl restart nginx #重启单个服务gitlab-ctl status #查看全部组件的状态gitlab-ctl show-config #验证配置文件gitlab-ctl uninstall #删除gitlab(保留数据）gitlab-ctl cleanse #删除所有数据，重新开始gitlab-ctl tail &lt;svc_name&gt;  #查看服务的日志gitlab-rails console production #进入控制台 ，可以修改root 的密码gitlab-ctl --help #查看gitlab-ctl命令的帮助信息</code></pre><h4 id="汉化">汉化</h4><p>如果需要汉化，汉化包与gitlab软件包版本要一致，否则可能发送意想不到的错误，汉化之前更应该先备份gitlab数据。未尝试，不建议汉化。</p><pre><code class="language-sh"># 暂停gitlabgitlab-ctl stop# 查看gitlab版本cat /opt/gitlab/embedded/service/gitlab-rails/VERSION12.8.1-ee# 下载对应版本汉化包git clone https://gitlab.com/xhang/gitlab.git# 备份cp -r /opt/gitlab/embedded/service/gitlab-rails{,.ori}# 汉化包覆盖cp -rf gitlab-10-3-stable-zh/* /opt/gitlab/embedded/service/gitlab-rails/# 启动gitlab-ctl start</code></pre><h4 id="安装git">安装git</h4><p>Gitlab是基于git做版本控制，访问gitlab需要安装git，在linux用yum源安装</p><pre><code class="language-sh"># 修改yum源，指向163mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup# 下载对应版本repo文件, 放入/etc/yum.repos.d/wget http://mirrors.163.com/.help/CentOS7-Base-163.repo# 如果yum命令直接卡死，造成rpm数据库异常，杀进程# 删除rpm数据文件 rm -f /var/lib/rpm/__db.00*# 重建rpm数据文件 rpm -vv --rebuilddb# 清除缓存yum clean allyum makecache# 安装gityum -y install git</code></pre><h4 id="groups">Groups</h4><p>一个 GitLab 的组是一些项目的集合，连同关于多少用户可以访问这些项目的数据。 每一个组都有一个项目命名空间（与用户一样），所以如果一个叫 <code>training</code> 的组拥有一个名称是 <code>materials</code> 的项目，那么这个项目的 url 会是 <code>http://server/training/materials</code>。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811224510385-cc71c8273987486f871d3966c4cf4550.png" alt="image-20200811224510385" /></p><p>每一个组都有许多用户与之关联，每一个用户对组中的项目以及组本身的权限都有级别区分。 权限的范围从 “访客”（仅能提问题和讨论） 到 “拥有者”（完全控制组、成员和项目）。 权限的种类太多以至于难以在这里一一列举，不过在 GitLab 的管理界面上有帮助链接。</p><h4 id="项目">项目</h4><p>一个 GitLab 的项目相当于 git 的版本库。 每一个项目都属于一个用户或者一个组的单个命名空间。 如果这个项目属于一个用户，那么这个拥有者对所有可以获取这个项目的人拥有直接管理权； 如果这个项目属于一个组，那么该组中用户级别的权限也会起作用。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811224833453-3714220545a24bdc968aa39a5ba41c17.png" alt="image-20200811224833453" /></p><p>每一个用户账号都有一个 <strong>命名空间</strong> ，即该用户项目的逻辑集合。 如果一个叫 <code>wenqy</code> 的用户拥有一个名称是 springcloudstudy的项目，那么这个项目的 url 会是 <code>http://server/wenqy/springcloudstudy</code> 。</p><p>每一个项目都有一个可视级别，控制着谁可以看到这个项目页面和仓库。 如果一个项目是 <em>私有</em> 的，这个项目的拥有者必须明确授权从而使特定的用户可以访问。 一个 <em>内部</em> 的项目可以被所有登录的人看到，而一个 <em>公开</em> 的项目则是对所有人可见的。 注意，这种控制既包括 <code>git fetch</code> 的使用也包括对项目 web 用户界面的访问。</p><h4 id="配置ssh-keys">配置SSH Keys</h4><p>我们需要经常免登录git账号进行推送代码，这时需要配置SSH Keys，将生成的RSA 公钥填入。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811225630116-33cbcc7e5bdb445bb41f6653fa55cfe9.png" alt="image-20200811225630116" /></p><p>当通常情况下，我们已经在GitHub、码云上已经分配了账号，可能是相同的ID，也可能在一个平台上有多个账号，我们可能需要区分不同的平台和账号和一个平台的多个账号。我们可以看下同时配置GitHub两个账号，用命令生成公钥私钥对</p><blockquote><p>ssh-keygen -t rsa -C &quot;<a href="mailto:one@github.com">one@github.com</a>&quot;</p><p>ssh-keygen -t rsa -C &quot;<a href="mailto:two@github.com">two@github.com</a>&quot;</p></blockquote><p>运行命令后不要一路回车，分别在第一次对话出现“<code>Enter file in which to save the key</code>”的时候输入不同的文件名，避免覆盖。两份包含私钥和公钥的4个文件，后缀为<code>.pub</code>的文件为公钥文件。linux或mac用户一定要在<code>~/.ssh</code>路径下运行命令行，不然生成的文件不会出现在当前目录，Windows用户则在“<code>C:\Users\用户名\.ssh</code>”目录下运行命令行。</p><h5 id="配置config文件">配置config文件</h5><p>在<code>.ssh</code>目录下创建config文件，在config文件中添加以下内容:</p><pre><code class="language-properties">#github server oneHost one.github.comHostname github.comUser onePreferredAuthentications publickeyIdentityFile C:\\Users\\xmn-wenqy.HELLO-WORLD\\.ssh\\id_rsa#github server twoHost two.github.comHostname github.comUser twoPreferredAuthentications publickeyIdentityFile C:\\Users\\xmn-wenqy.HELLO-WORLD\\.ssh\\id_rsa_cloud</code></pre><p>每个账号单独配置一个Host，每个Host要取一个别名，一般为每个Host主要配置HostName和IdentityFile两个属性，配置完保存即可。Host的名字可以自定义名字，不过这个会影响git相关命令，例如：Host one.github.com这样定义的话，使用命令git clone <a href="mailto:git@one.github.com">git@one.github.com</a>:<a href="mailto:one/project.git，git@后面紧跟的名字改为one.github.com">one/project.git，git@后面紧跟的名字改为one.github.com</a></p><p>为仓库设置局部的用户名和邮箱</p><pre><code class="language-sh"># 取消全局 用户名/邮箱 配置git config --global --unset user.namegit config --global --unset user.email    # 单独为每个repo设置 用户名/邮箱git config user.name &quot;one_name&quot; ; git config user.email &quot;one_email&quot;git config user.name &quot;two_name&quot; ; git config user.email &quot;two_email&quot;</code></pre><p>配置好SSH Keys后就可以免登录推送代码到gitlab了。</p><h4 id="钩子">钩子</h4><p>GitLab 在项目和系统级别上都支持钩子程序。 对任意级别，当有相关事件发生时，GitLab 的服务器会执行一个包含描述性 JSON 数据的 HTTP 请求。 这是自动化连接你的 git 版本库和 GitLab 实例到其他的开发工具，比如 CI 服务器，聊天室，或者部署工具的一个极好方法。</p><p>对应root超级用户，有更多的管理权限，有管理面板，可以对使用者、项目进行管理和统计分析等等。</p><h4 id="架构">架构</h4><p>gitlab 依赖的组件默认安装在<code>/var/opt/gitlab/</code>目录下</p><p><img src="http://blog.wenqy.com/upload/2020/11/architecture_simplified-94a742e7c49c48e59c417c2f34f9dbeb.png" alt="architecture_simplified" /></p><p>Gitlab利用Nginx或者Apache作为web前端代理到Unicorn web服务端。默认情况下，Unicorn和前端之间的通信是通过<code>Unix domain socket</code>进行的，但也支持通过TCP转发请求。利用<code>Sidekiq</code>从Redis队列中拉取job并处理，是Ruby后台任务处理器；利用<code>Redis</code>缓存作业、会话等信息；利用PostgreSQL做持久化数据库，保存用户、权限、issues等元信息；利用<code>GitLab Shell</code>接收SSH请求，<code>GitLab Shell</code>通过<code>Gitaly</code>访问存储库，为Git对象提供服务，并与<code>Redis</code>通信，将作业提交给<code>Sidekiq</code>供GitLab处理。<code>GitLab Shell</code>通过<code>GitLab API</code>查询以确定授权和访问；<code>Gitaly</code>从<code>GitLab Shell</code>和GitLab web app执行Git操作，并为GitLab web app提供一个API，以从Git获取属性（例如titile、、branches、tags、其他元数据），并获取blob（例如diff、commits、files）;<code>GitLab Workhorse</code>是由GitLab设计的程序，可帮助缓解<code>Unicorn</code>的压力，它旨在充当智能反向代理，以帮助加快整个GitLab的运行速度。</p><p>此外，官方推荐服务器CPU核数至少4核，存储空间不少于gitlab所有存储库之和，磁盘最好采用逻辑卷管理方便扩展硬盘驱动器，gitlab占用太多内存，容易导致服务器崩溃，可以启用swap分区，缓解内存压力，例如，阿里云ECS默认是没有开启swap的，这值得注意一下。日志目录默认安装在<code>/var/log/gitlab</code>路径下。还应该注意<strong>Gitlab的性能优化、数据备份和迁移</strong>。</p><p>Gitlab安装和部署还是相对容易的，只是依赖的组件相对庞杂，太过吃内存。对于熟悉Git和GitHub的人也是极易上手，甚至是无感知使用的。</p><h4 id="参考">参考</h4><p><a href="https://about.gitlab.com/install/#centos-7">https://about.gitlab.com/install/#centos-7</a></p><p><a href="https://gitlab.com/gitlab-org/gitlab-foss/">https://gitlab.com/gitlab-org/gitlab-foss/</a></p><p><a href="https://packages.gitlab.com/gitlab/gitlab-ce">https://packages.gitlab.com/gitlab/gitlab-ce</a></p><p><a href="https://docs.gitlab.com/omnibus/settings/smtp.html">https://docs.gitlab.com/omnibus/settings/smtp.html</a></p><p><a href="https://docs.gitlab.com/ee/install/requirements.html">https://docs.gitlab.com/ee/install/requirements.html</a></p><p><a href="https://docs.gitlab.com/ce/development/architecture.html">https://docs.gitlab.com/ce/development/architecture.html</a></p><p><a href="https://blog.csdn.net/ouyang_peng/article/details/84066417">https://blog.csdn.net/ouyang_peng/article/details/84066417</a>  解决GitLab内存消耗大的问题</p>]]>
                    </description>
                    <pubDate>Tue, 03 Nov 2020 22:40:10 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Gitlab概览]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/gitlab概览md</link>
                    <description>
                            <![CDATA[<p><code>Gitlab</code>是开源的基于Git的仓库管理系统，也可以管理软件开发的整个生命周期，是项目管理和代码托管平台，支撑着整个DevOps的生命周期。Gitlab很容易选为GitHub\码云\gogs\gitea的替代品，作为公司私有库管理的工具。我们可以用<code>Gitlab Workflow</code>来协同整个团队的软件开发管理过程。</p><h4 id="软件开发阶段">软件开发阶段</h4><p><img src="http://blog.wenqy.com/upload/2020/11/wps1-27a4c9b642a74a9f92225ec4a7622747.jpg" alt="wps1" /></p><p>Gitlab工作流将软件开发定义为10个阶段，并提供相应的解决方案，帮助团队高效协作，完成项目开发。</p><table><thead><tr><th>IDEA</th><th>项目的通常来自一个idea的诞生，idea可能来自某次闲聊。Gitlab集成Mattermost，提供内部通讯工具。（默认未开启）</th></tr></thead><tbody><tr><td>ISSUE</td><td>为IDEA建个ISSUE讨论，团队成员追踪、讨论、提升</td></tr><tr><td>PLAN</td><td>讨论达成一致，需要优化和组织工作流，可以利用ISSUE Board</td></tr><tr><td>CODE</td><td>准备就绪，进入编码阶段</td></tr><tr><td>COMMIT</td><td>利用版本控制，提交代码到功能分支</td></tr><tr><td>TEST</td><td>利用<a href="https://about.gitlab.com/gitlab-ci/">GitLab CI</a>，可以运行脚本来构建和测试应用</td></tr><tr><td>REVIEW</td><td>构建和测试成功后，可以进行代码复审</td></tr><tr><td>STAGING</td><td>部署代码到演示环境，检查是否符合期望，以便及时调整</td></tr><tr><td>PRODUCTION</td><td>如预期顺畅，部署应用到生产环境</td></tr><tr><td>FEEDBACK</td><td>回顾项目，利用Cycle Analytics对关键部分反馈，总结和提升</td></tr></tbody></table><h4 id="gitlab-flow">Gitlab flow</h4><p>区别于SVN等版本控制系统，基于Git的版本管理对于创建分支和合并变得更加容易，也方便项目的管理。</p><p><img src="http://blog.wenqy.com/upload/2020/11/wps1-1597111300349-23f3df2a86244f779d2d3464ff86aabf.jpg" alt="wps1-1597111300349" /></p><p>利用工单追踪，将功能特征驱动开发和功能特征分支有机结合在一起。受保护的分支对于权限较低开发者等角色需要通过发起合并请求，复核评审通过后最终才能合并到保护分支。GitLab flow是GitLab官方推荐的分支管理策略。我们可以在原则上约定：分支分为<strong>永久分支</strong>和<strong>临时分支</strong>，都是在master分支以外建立。永久分支不会被删除，包括主分支<code>master</code>，准生产分支<code>pre-production</code>，生产分支<code>production</code>。临时分支分为<strong>Feature特征分支</strong>和<strong>Fix修复分支</strong>，在开发完成会被删除。一种简单的方案，通常可以基于最新的master主分支，创建特征分支，进行特征开发，最后合并请求到master分支。</p><p><img src="http://blog.wenqy.com/upload/2020/11/wps2-070a5309903e46f2b54385b8841a1dc1.jpg" alt="wps2" /></p><p>只有一个master主分支，这样的好处在于，可以代码及时合并，可以是本地代码库存量最小，也可以更快的持续交付。当然，与此同时，部署、环境、集成等相关问题也没得到很好的解决。</p><p>对于每一次分支合并，我们总是期望可以部署一个稳定的版本。我们可以考虑多环境分支的情况。</p><p><img src="http://blog.wenqy.com/upload/2020/11/wps3-b458ecff368b42df9473c2ad0071a14a.jpg" alt="wps3" /></p><p>Gitlab flow 的最大原则叫做&quot;<strong>上游优先</strong>&quot;（upsteam first）。代码的变化，必须由“上游”向“下游”发展。Feature和Fix是master的“上游”，master是<code>pre-production</code>的上游，<code>pre-production</code>是<code>production</code>的“上游”。比如，生产环境出现了bug 或者要开发新的feature，这时就要建一个临时Hotfix或Feature分支，开发完成把它合并到master，确认没有问题，再<code>cherry-pick</code>到<code>pre-production</code>，这一步也没有问题，才进入<code>production</code>。Feature和Fix在开发人员在开发环境部署测试，master在测试环境中部署测试，<code>pre-production</code>在演示环境中部署测试，<code>production</code>在生产环境发布，一条流水线下来，可以持续完成项目的部署交付。</p><p>此外，我们可以有多稳定版本的分支策略，用于将软件发布给外界。</p><p><img src="http://blog.wenqy.com/upload/2020/11/wps4-0518e14803b54f49b99700846171565e.jpg" alt="wps4" /></p><p>版本发布适用于APP、小程序等有版本规划的项目。稳定分支以master为起点，并尽可能晚地创建。通过尽可能晚的分支，可以最大程度地减少将错误修正应用于多个分支的时间。在master上打出稳定的分支版本。如果稳定分支发行后，出现bug，可以先将错误修复到master，然后<code>cherry-picked</code>到稳定分支，我们可以通过设置<code>Tag</code>来提高补丁版本，可以维护一个稳定分支专门指向最新版本发布的分支提交。</p><p>此外，我们可以对分支名称做个约定，在Gitlab上 基于issue创建的分支默认是<strong>issue编号+issue标题</strong>等等。</p><h4 id="issue">Issue</h4><p>GitLab 有一个强大的工单追溯系统，在使用过程中，允许你和你的团队，以及你的合作者分享和讨论建议。所有的开发工作都应该以<strong>工单任务为导向</strong>。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811114523568-dd28e37650874e998259e781a9fe73af.png" alt="image-20200811114523568" /></p><p>我们可以填写<code>是否私密</code>、<code>受托人</code>、<code>截止日期</code>、<code>标签</code> 、<code>里程碑</code>等信息，利用这些信息可以更加方便地组织活动和安排优先级。</p><h4 id="labels">Labels</h4><p>GitLab 标签也是Gitlab flow的重要组成部分，可以对issue工单进行分类和定位。也可以通过定义<strong>优先级标签</strong>组织它们。通常和<code>Boards</code>一起，来组织计划和工作流程。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811115423963-1ef8a1aa8cba4a64bc15f4e7b9bc2baf.png" alt="image-20200811115423963" /></p><p>我们可以根据需要设计一些常用标签，如待办类，Bug类，功能改进类等进行定位、统计和跟踪。</p><h4 id="boards">Boards</h4><p>Gitlab 工单面板是一个用于计划以及组织工单，使之符合项目工作流的工具。Boards包含了与其相关的对应标签，每一个列表包含了相关的被标记的issue工单，并且以卡片的形式展示出来。这些issue卡片可以在列表之间推拽移动，被移动的卡片，其标签将会依据你移动的位置更新到相应列表上。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811120241243-58abb005432541118eb298c4438b1f6b.png" alt="image-20200811120241243" /></p><h4 id="milestones">Milestones</h4><p>Milestones 是GitLab 中基于共同目标、时间进度追踪团队工作的最好工具。它定义了目标阶段对应的工单集合和合并请求。通常是团队协作在截止日期完成某个目标。例如，发布一个新的版本，启动一个新的产品，在某个日期前完成，或者按季度收尾一些项目。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811120523006-bc823ada25404d13b723b235d1b842d6.png" alt="image-20200811120523006" /></p><h4 id="merge-request">Merge Request</h4><p>我们可以根据<code>issue</code>创建branch和发起<code>Merge Requests(MR)</code>合并到目标分支。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811135052067-e16e000ef77f4a80a9db424bd3082f0d.png" alt="image-20200811135052067" /></p><p>开发者检出特征分支开发，提交后并推送到远程。功能分支和修复分支合并进master分支，必须通过<code>Merge Requests</code>。审核员可以对这些提交和变化进行评审，可以反复这个过程，直到符合代码规范，才合并到目标分支。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811140550289-6a92a5c0218c477ab977e9d29be47cb2.png" alt="image-20200811140550289" /></p><p>master分支应该受到保护，不是每个人都可以直接修改这个分支，以及拥有审批 <code>Merge Request</code>的权力。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811140802812-82715cdc71fc44fda94e5e6e70dc7c8a.png" alt="image-20200811140802812" /></p><p>上图展示了保护分支的权限设置，规定了哪些角色具有Branch的<code>MR</code>和<code>push</code>的权限。否则，推送无权限的分支时可能会出现以下错误：</p><blockquote><p><strong>you are not allowed to push code to protected branches on this project</strong></p></blockquote><p>每一次 <code>MR</code> 都会有一个标题（这个标题总结了这次的改动）并且一个用 <a href="https://docs.gitlab.com/ee/user/markdown.html">Markdown</a> 书写的描述。在描述中，你可以简单的描述该 <code>MR</code> 做了什么，涉及的任何工单和 <code>MR</code>（在它们之间创建联系），并且，你也可以添加个<a href="https://docs.gitlab.com/ce/administration/issue_closing_pattern.html"><strong>关闭工单模式</strong></a>，当该<code>MR</code> 被合并的时候，相关联的工单就会自动被关闭。在描述里添加 <code>Closes #xxx</code>，xxx为对应的<code>issue编号</code>。Merge成功后，自动关闭工单。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811141514015-6342fc2faabb4e5f9e2d9b347aa5773f.png" alt="image-20200811141514015" /></p><p><strong>有权限角色在<code>Merge Request</code>Merge 前，注意下Merge的内容，评审通过后，最好能确保每一次Merge后对应Master分支都是稳定的</strong>。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811141812996-ae3aa1a04b734d7587c75c648c0429db.png" alt="image-20200811141812996" /></p><h5 id="wip-mr">WIP MR</h5><p><code>WIP (Work in Process) MR</code>,避免 MR 在准备就绪前被合并。只需要添加 <strong>WIP:</strong> 在 MR 的标题开头，它将不会被合并，除非你把 <strong>WIP:</strong> 删除。当你改动已经准备好被合并，编辑工单来手动删除 <strong>WIP:</strong> 。或者使用快捷方式，只需要在评论或者 MR 描述中输入斜线命令**/wip**并提交即可快速添加到合并请求中。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811142229007-b62d9035efdd4764bad1527bfc1965e1.png" alt="image-20200811142229007" /></p><h5 id="code-review">Code Review</h5><p>一旦你创建一个合并请求，审核方收到反馈，就可以决定是否合并这个请求了。审核者可以进行差异比较，回复或者解决它们。在图形界面中可以看到提交历史，通过提交历史，你可以追踪文件的每一次改变。你可以以行内差异或左右对比的方式浏览它们,甚至评论他们。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811143449656-2cd68fd6829049018c50ec6da335cbd8.png" alt="image-20200811143449656" /></p><p>如果有合并冲突，甚至可以在线编辑解决。</p><h4 id="memebers">Memebers</h4><p>项目拥有者和维护者需要添加协作的团队成员，进行项目协同，可以选择已注册gitlab的有效成员进行邀请。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811143601297-b0cccaa06bb3419987a7d31111e2cc9a.png" alt="image-20200811143601297" /></p><p>受邀人会受到电子邮件，同意后就可以加入团队了。低权限角色只能开发，甚至只能浏览，无权限维护。</p><h4 id="cicd">CI/CD</h4><p><a href="https://about.gitlab.com/gitlab-ci/">GitLab CI/CD</a>是构建项目持续集成、持续部署和持续交付的有效工具。首先需要在项目根目录下创建文件<code>.gitlab-ci.yml</code>。</p><p><img src="http://blog.wenqy.com/upload/2020/11/wps1-1597128155459-59bfe130c823454e97340d1a28199b68.jpg" alt="wps1-1597128155459" /></p><p>例如，我们可以在项目中每一次<code>Merge Request</code>，都可以触发<code>pipeline</code> 去构建、测试和部署。而具体的构建规则可以根据编写不同的<code>.gitlab-ci.yaml</code>脚本实现。<code>CI/CD pipelines</code>的运行离不开<code>Gitlab Runner</code>。所以，还需要安装<code>GitLab Runner</code>，它可以在任何地方（能ping通Gitlab）运行，也可以是以<code>Shell</code>、<code>docker</code>、<code>kubernetes</code>等不同的形式运行。<code>GitLab Runner</code>向Gitlab注册，需要Gitlab授权。注册成功后，<code>GitLab Runners</code>负责从Gitlab拉取代码进行构建、测试和部署。区别于<code>Jenkins</code>，Gitlab-CI更加适合<code>DevOps</code>人员，开发与运维是同一个人，非常适合敏捷开发。<code>Jenkins</code>通过Hook可以使编译服务和代码仓库分离，耦合度低，适合多角色团队，职责分明。将另有篇幅概述，这一块暂不赘述。</p><h4 id="周期分析">周期分析</h4><p>我们可以通过周期分析，可以查看各个阶段的统计，及时反馈项目的进展和改进项目的不足之处。</p><table><thead><tr><th>Issue</th><th>从创建一个工单，到分配这个工单给一个里程碑或者添加工单到你的工单看板的时间</th></tr></thead><tbody><tr><td>Plan</td><td>从给工单分配一个里程碑或者把它添加到工单看板，到推送第一次提交的时间</td></tr><tr><td>Code</td><td>从第一次提交到提出该合并请求的时间</td></tr><tr><td>Test</td><td>CI 为了相关合并请求而运行整个过程的时间</td></tr><tr><td>Review</td><td>从创建一个合并请求到合并它的时间</td></tr><tr><td>Staging</td><td>从合并到发布成为产品的时间</td></tr><tr><td>Production（Total）</td><td>从创建工单到把代码发布成产品的时间</td></tr></tbody></table><p><img src="http://blog.wenqy.com/upload/2020/11/image-20200811152625429-a70ee6dd4dc54b2c9ae90dc2b19df767.png" alt="image-20200811152625429" /></p><h4 id="markdown-tips">Markdown Tips</h4><p>在<code>Issue</code> 或 <code>Merge Request</code> 等对应的markdown描述或者<code>git commit -m &quot;&quot;</code>注释信息中，可以添加特殊标识来实现一些特性。可以看下简单的例子：</p><pre><code class="language-markdown">## 增加一个新页面这个 MR 将会为这个项目创建一个包含该 app 概览的 `readme.md`。Closes #1,#2 and https://gitlab.wenqy.com/group/project/issues/&lt;8&gt;related to #3:smile:/cc @wenqy @all</code></pre><p>上面是MR的一段描述。合并时，会关闭编号为1,2的issue工单，以及url指向的项目工单。这还做了一个编号为3的issue工单关联，不会关闭。并邮件通知用户。还支持<code>emoji</code>表情。</p><p>添加<a href="https://docs.gitlab.com/ce/administration/issue_closing_pattern.html">关闭工单样式</a>到你的 MR 以便可以使用 【GitLab周期分析】追踪你的项目进展，是十分重要的。它将会追踪“CODE”阶段，衡量第一次提交及创建一个相关的合并请求所间隔的时间。</p><h5 id="第一次提交">第一次提交</h5><p>第一次提交时添加一个关联的Issue编号，利用<code>Ref #</code>关联</p><blockquote><p>git commit -m &quot;this is my commit message. Ref #xxx&quot;</p></blockquote><p>或者使用全称<code>Related to</code>关联</p><blockquote><p>git commit -m &quot;this is my commit message. Related to <a href="https://gitlab.com/">https://gitlab.com/</a><username>/<projectname>/issues/<xxx>&quot;</p></blockquote><p>链接第一次提交与Issue，将有助于【GitLab周期分析】跟踪工作流程，它将度量计划该Issue的实现所用的时间，即从创建Issue到进行第一次提交之间的时间。</p><h5 id="任务列表">任务列表</h5><p>在描述里添加 <strong>- []</strong> 子任务，划分子任务</p><pre><code class="language-markdown">- [x] Completed task - [ ] Incomplete task - [ ] Sub-task 1 - [x] Sub-task 2 - [ ] Sub-task</code></pre><p>描述里面体现子任务的列表</p><p><img src="http://blog.wenqy.com/upload/2020/11/wps2-1597131562201-17a839c612dc40ad917f95ac3afc88dc.jpg" alt="wps2-1597131562201" /></p><p>此外，markdown描述里输入<code>#</code>，触发已有issue下拉；输入<code>！</code>，触发已有MR下拉；输入<code>/</code>，触发命令；输入<code>:</code>，触发emoji</p><p>在CI中还可以在git commit 注释里添加<code>[ci skip]</code>跳过CI等等。</p><p>此外，还可以对Issue和<code>Merge Request</code>等描述定义描述模板，规范描述。</p><p>Gitlab还有wiki和代码片段等知识库的管理和分享功能，方便团队规范行为。</p><h4 id="总结">总结</h4><p>Gitlab开源免费，可以自建gitlab。它清晰且直观地划分了软件开发管理的各个阶段，可以无缝衔接过渡到每个阶段。它能预测和控制项目的生命周期，整个平台很容易上手和使用，以自己的工作流管理项目的整个生命周期非常方便，轻量而敏捷，甚至可以说是一体化管理，为敏捷而生，是一个值得推荐的协同开发项目管理工具。</p><h4 id="参考">参考</h4><p><a href="https://about.gitlab.com/blog/2016/10/25/gitlab-workflow-an-overview/">https://about.gitlab.com/blog/2016/10/25/gitlab-workflow-an-overview/</a></p><p><a href="https://about.gitlab.com/blog/2014/09/29/gitlab-flow/">https://about.gitlab.com/blog/2014/09/29/gitlab-flow/</a></p><p><a href="https://docs.gitlab.com/ee/topics/gitlab_flow.html">https://docs.gitlab.com/ee/topics/gitlab_flow.html</a></p><p><a href="https://docs.gitlab.com/ee/user/markdown.html">https://docs.gitlab.com/ee/user/markdown.html</a></p><p><a href="https://gitlab.com/gitlab-org/omnibus-gitlab/-/labels">https://gitlab.com/gitlab-org/omnibus-gitlab/-/labels</a></p>]]>
                    </description>
                    <pubDate>Sat, 24 Oct 2020 11:48:44 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[java8并发学习]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/java8并发学习</link>
                    <description>
                            <![CDATA[<p>这里继续学习记录java8的并发知识。关于什么是并发，什么是并行，什么是进程，什么是线程，有什么关系区别等等就不贴出来啦。</p><p>并发在Java5中首次被引入并在后续的版本中不断得到增强。Java从JDK1.0开始执行线程。在开始一个新的线程之前，你必须指定由这个线程执行的代码，通常称为task。</p><h3 id="线程与执行器">线程与执行器</h3><h4 id="runnable">Runnable</h4><p>我们可以通过实现<code>Runnable</code>--一个定义了一个无返回值无参数的run()方法的函数接口，来实现task。</p><pre><code class="language-java">/**     * Runnable     *      * @author wenqy     * @date 2020年1月17日 下午3:19:58     */    private void testRunnable() {        Runnable task = () -&gt; {            String threadName = Thread.currentThread().getName();            System.out.println(“Hello “ + threadName);        };        task.run(); // 非线程方式调用，还在主线程里        Thread thread = new Thread(task);        thread.start();        System.out.println(“Done!”);  // runnable是在打印’done’前执行还是在之后执行,顺序是不确定的    }</code></pre><p>我们可以将线程休眠确定的时间。通过这种方法来模拟长时间运行的任务。</p><pre><code class="language-java">/**     * 设置线程休眠时间，模拟长任务     *      * @author wenqy     * @date 2020年1月17日 下午3:25:01     */    private void testRunnableWithSleep() {        Runnable runnable = () -&gt; {            try {                String name = Thread.currentThread().getName();                System.out.println(“Foo “ + name);                TimeUnit.SECONDS.sleep(1); // 休眠1s                System.out.println(“Bar “ + name);            }            catch (InterruptedException e) {                e.printStackTrace();            }        };        Thread thread = new Thread(runnable);        thread.start();    }</code></pre><h4 id="executor">Executor</h4><p>并发API引入了<code>ExecutorService</code>作为一个在程序中直接使用Thread的高层次的替换方案。<code>Executors</code>支持运行异步任务，通常管理一个线程池，这样一来我们就不需要手动去创建新的线程。在不断地处理任务的过程中，线程池内部线程将会得到复用，Java进程从没有停止！<code>Executors</code>必须显式的停止-否则它们将持续监听新的任务。</p><p><code>ExecutorService</code>提供了两个方法来达到这个目的--<code>shutdwon()</code>会等待正在执行的任务执行完而<code>shutdownNow()</code>会终止所有正在执行的任务并立即关闭executor。</p><pre><code class="language-java">ExecutorService executor = Executors.newSingleThreadExecutor(); // 单线程线程池        executor.submit(() -&gt; {            String threadName = Thread.currentThread().getName();            System.out.println(“Hello “ + threadName);            try {                TimeUnit.SECONDS.sleep(6);            } catch (InterruptedException e) {                System.err.println(“my is interrupted”);            } // 休眠1s        });        // =&gt; Hello pool-1-thread-1        // Executors必须显式的停止-否则它们将持续监听新的任务        try {            System.out.println(“attempt to shutdown executor”);            executor.shutdown(); // 等待正在执行的任务执行完            executor.awaitTermination(5, TimeUnit.SECONDS); // 等待指定时间优雅关闭executor。在等待最长5s的时间后，executor最终会通过中断所有的正在执行的任务关闭            System.out.println(“wait for 5s to shutdown”);        } catch (InterruptedException e) {            System.err.println(“tasks interrupted”);        } finally {            if (!executor.isTerminated()) {                System.err.println(“cancel non-finished tasks”);            }            executor.shutdownNow(); // 终止所有正在执行的任务并立即关闭executor            System.out.println(“shutdown finished”);        }</code></pre><h4 id="callable">Callable</h4><p>Callables也是类似于runnables的函数接口，不同之处在于，Callable返回一个值。一样提交给 executor services。在调用<code>get()</code>方法时，当前线程会阻塞等待，直到callable在返回实际的结果 123 之前执行完成。</p><pre><code class="language-java">Callable&lt;Integer&gt; task = () -&gt; {            try {                TimeUnit.SECONDS.sleep(5); // 休眠5s后返回整数                return 123;            }            catch (InterruptedException e) {                throw new IllegalStateException(“task interrupted”, e);            }        };        ExecutorService executor = Executors.newFixedThreadPool(1); // 固定线程池        Future&lt;Integer&gt; future = executor.submit(task);        System.out.println(“future done? “ + future.isDone());//      executor.shutdownNow(); // 如果关闭executor，所有的未中止的future都会抛出异常。        Integer result = future.get(); // 在调用get()方法时，当前线程会阻塞等待，直到callable在返回实际的结果123之前执行完成        System.out.println(“future done? “ + future.isDone());        System.out.println(“result: “ + result);        executor.shutdownNow(); // 需要显式关闭        System.out.println(“result: “ + future.get());    }</code></pre><p>任何<code>future.get()</code>调用都会阻塞，然后等待直到callable中止。在最糟糕的情况下，一个callable持续运行--因此使你的程序将没有响应。我们可以简单的传入一个时长来避免这种情况。</p><pre><code class="language-java">ExecutorService executor = Executors.newFixedThreadPool(1);        Future&lt;Integer&gt; future = executor.submit(() -&gt; {            try {                TimeUnit.SECONDS.sleep(2);                return 123;            }            catch (InterruptedException e) {                throw new IllegalStateException(“task interrupted”, e);            }        });        // 任何future.get()调用都会阻塞，然后等待直到callable中止,传入超时时长终止        future.get(1, TimeUnit.SECONDS);  // 抛出 java.util.concurrent.TimeoutException</code></pre><h4 id="invokeall">invokeAll</h4><p>Executors支持通过<code>invokeAll()</code>一次批量提交多个callable。这个方法结果一个callable的集合，然后返回一个future的列表。</p><pre><code class="language-java">ExecutorService executor = Executors.newWorkStealingPool(); // ForkJoinPool 一个并行因子数来创建，默认值为主机CPU的可用核心数        List&lt;Callable&lt;String&gt;&gt; callables = Arrays.asList(                () -&gt; “task1”,                () -&gt; “task2”,                () -&gt; “task3”);        executor.invokeAll(callables)            .stream()            .map(future -&gt; { // 返回的所有future，并每一个future映射到它的返回值                try {                    return future.get();                }                catch (Exception e) {                    throw new IllegalStateException(e);                }            })            .forEach(System.out::println);</code></pre><h4 id="invokeany">invokeAny</h4><p>批量提交callable的另一种方式就是<code>invokeAny()</code>，它的工作方式与<code>invokeAll()</code>稍有不同。在等待future对象的过程中，这个方法将会阻塞直到第一个callable中止然后返回这一个callable的结果。</p><pre><code class="language-java">private static Callable&lt;String&gt; callable(String result, long sleepSeconds) {        return () -&gt; {            TimeUnit.SECONDS.sleep(sleepSeconds);            return result;        };    }    public static void main(String[] args) throws InterruptedException, ExecutionException {        ExecutorService executor = Executors.newWorkStealingPool();        List&lt;Callable&lt;String&gt;&gt; callables = Arrays.asList(        callable(“task1”, 2),        callable(“task2”, 1),        callable(“task3”, 3));        String result = executor.invokeAny(callables);        System.out.println(result); // task2    }</code></pre><p>这个例子又使用了另一种方式来创建executor--调用<code>newWorkStealingPool()</code>。这个工厂方法是Java8引入的，返回一个<code>ForkJoinPool</code>类型的 executor，它的工作方法与其他常见的execuotr稍有不同。与使用一个固定大小的线程池不同，<code>ForkJoinPools</code>使用一个并行因子数来创建，默认值为主机CPU的可用核心数。</p><h4 id="scheduledexecutor">ScheduledExecutor</h4><p>为了持续的多次执行常见的任务，我们可以利用调度线程池。<code>ScheduledExecutorService</code>支持任务调度，持续执行或者延迟一段时间后执行。调度一个任务将会产生一个专门的future类型--<code>ScheduleFuture</code>，它除了提供了Future的所有方法之外，他还提供了<code>getDelay()</code>方法来获得剩余的延迟。在延迟消逝后，任务将会并发执行。</p><p>为了调度任务持续的执行，executors 提供了两个方法<code>scheduleAtFixedRate()</code>和<code>scheduleWithFixedDelay()</code>。第一个方法用来以固定频率来执行一个任务，另一个方法等待时间是在一次任务的结束和下一个任务的开始之间</p><pre><code class="language-java">/**     * 获取剩余延迟     * @throws InterruptedException     * @author wenqy     * @date 2020年1月17日 下午4:56:58     */    private static void scheduleDelay() throws InterruptedException {        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);        Runnable task = () -&gt; System.out.println(“Scheduling: “ + System.nanoTime());        ScheduledFuture&lt;?&gt; future = executor.schedule(task, 3, TimeUnit.SECONDS); // 3s后执行        TimeUnit.MILLISECONDS.sleep(1337);        long remainingDelay = future.getDelay(TimeUnit.MILLISECONDS);        System.out.printf(“Remaining Delay: %sms\n”, remainingDelay); // 剩余的延迟        executor.shutdown();    }    /**     * 以固定频率来执行一个任务     *      * @throws InterruptedException     * @author wenqy     * @date 2020年1月17日 下午4:57:45     */    private static void scheduleAtFixedRate() throws InterruptedException {        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);        Runnable task = () -&gt; {            System.out.println(“at fixed rate Scheduling: “ + System.nanoTime());            try {                TimeUnit.SECONDS.sleep(2);            } catch (InterruptedException e) {                e.printStackTrace();            }        };        int initialDelay = 0; // 初始化延迟，用来指定这个任务首次被执行等待的时长        int period = 1;        executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS); // 不考虑任务的实际用时    }    /**     * 以固定延迟来执行一个任务     *  等待时间 period 是在一次任务的结束和下一个任务的开始之间     * @throws InterruptedException     * @author wenqy     * @date 2020年1月17日 下午5:03:28     */    private static void scheduleWithFixedDelay() throws InterruptedException {        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);        Runnable task = () -&gt; {            try {                TimeUnit.SECONDS.sleep(2);                System.out.println(“WithFixedDelay Scheduling: “ + System.nanoTime());            }            catch (InterruptedException e) {                System.err.println(“task interrupted”);            }        };        executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);    }</code></pre><h3 id="同步与锁">同步与锁</h3><p>我们学到了如何通过执行器服务同时执行代码。当我们编写这种多线程代码时，我们需要特别注意共享可变变量的并发访问。</p><pre><code class="language-java">int count = 0;    void increment() {        count = count + 1;    }</code></pre><p>我们在不同的线程上共享可变变量，并且变量访问没有同步机制，这会产生竞争条件。上面例子被多个线程同时访问，就会出现未知的错误。</p><p>我们可以用<code>synchronized</code>关键字支持线程同步</p><pre><code class="language-java">synchronized void incrementSync() {      count = count + 1;}</code></pre><p><code>synchronized</code>关键字也可用于语句块</p><pre><code class="language-java">void incrementSync2() {        synchronized (this) {            count = count + 1;        }    }</code></pre><p>java在内部使用所谓的**&quot;监视器&quot;**（monitor），也称为监视器锁（monitor lock）或内在锁（ intrinsic lock）来管理同步。监视器绑定在对象上，例如，当使用同步方法时，每个方法都共享相应对象的相同监视器。</p><p>所有隐式的监视器都实现了重入（<code>reentrant</code>）特性。重入的意思是锁绑定在当前线程上。线程可以安全地多次获取相同的锁，而不会产生死锁（例如，同步方法调用相同对象的另一个同步方法）</p><p>并发API支持多种显式的锁，它们由<code>Lock</code>接口规定，用于代替<code>synchronized</code>的隐式锁。锁对细粒度的控制支持多种方法，因此它们比隐式的监视器具有更大的开销。</p><h4 id="reentrantlock">ReentrantLock</h4><p><code>ReentrantLock</code>类是互斥锁，与通过<code>synchronized</code>访问的隐式监视器具有相同行为，但是具有扩展功能。就像它的名称一样，这个锁实现了重入特性，就像隐式监视器一样。</p><p>锁可以通过<code>lock()</code>来获取，通过<code>unlock()</code>来释放。把你的代码包装在try-finally代码块中来确保异常情况下的解锁非常重要。</p><pre><code class="language-java">/** * 可重入锁 *  * @author wenqy * @date 2020年1月18日 下午3:41:50 */private void safeIncreByLock() {    count = 0;    ExecutorService executor = Executors.newFixedThreadPool(2);    executor.submit(() -&gt; {        lock.lock();        try {            ConcurrentUtils.sleep(1);        } finally {            lock.unlock();        }    });    executor.submit(() -&gt; {        System.out.println(“Locked: “ + lock.isLocked());        System.out.println(“Held by me: “ + lock.isHeldByCurrentThread());        boolean locked = lock.tryLock(); // 尝试拿锁而不阻塞当前线程        // 在访问任何共享可变变量之前，必须使用布尔值结果来检查锁是否已经被获取        System.out.println(“Lock acquired: “ + locked);    });    ConcurrentUtils.stop(executor);}</code></pre><h4 id="readwritelock">ReadWriteLock</h4><p><code>ReadWriteLock</code>接口规定了锁的另一种类型，包含用于读写访问的一对锁。读写锁的理念是，只要没有任何线程写入变量，并发读取可变变量通常是安全的。所以读锁可以同时被多个线程持有，只要没有线程持有写锁。这样可以提升性能和吞吐量，因为读取比写入更加频繁。</p><pre><code class="language-java">/** * 读写锁 *  * @author wenqy * @date 2020年1月18日 下午3:41:21 */private void readWriteLock() {    ExecutorService executor = Executors.newFixedThreadPool(2);    Map&lt;String, String&gt; map = new HashMap&lt;&gt;();    ReadWriteLock lock = new ReentrantReadWriteLock();    executor.submit(() -&gt; {        lock.writeLock().lock();        try {            ConcurrentUtils.sleep(1);            map.put(“foo”, “bar”);        } finally {            lock.writeLock().unlock();        }    });    Runnable readTask = () -&gt; {        lock.readLock().lock();        try {            System.out.println(map.get(“foo”));            ConcurrentUtils.sleep(1);        } finally {            lock.readLock().unlock();        }    };    executor.submit(readTask);    executor.submit(readTask);    ConcurrentUtils.stop(executor);    // 两个读任务需要等待写任务完成。在释放了写锁之后，两个读任务会同时执行，并同时打印结果。    // 它们不需要相互等待完成，因为读锁可以安全同步获取}</code></pre><h4 id="stampedlock">StampedLock</h4><p>Java 8 自带了一种新的锁，叫做<code>StampedLock</code>，它同样支持读写锁，就像上面的例子那样。与<code>ReadWriteLock</code>不同的是，<code>StampedLock</code>的锁方法会返回表示为long的标记。你可以使用这些标记来释放锁，或者检查锁是否有效。</p><pre><code class="language-java">/**     * java8 StampedLock     *      tampedLock并没有实现重入特性，相同线程也要注意死锁     * @author wenqy     * @date 2020年1月18日 下午3:40:46     */    private void stampedLock() {        ExecutorService executor = Executors.newFixedThreadPool(2);        Map&lt;String, String&gt; map = new HashMap&lt;&gt;();        StampedLock lock = new StampedLock();        executor.submit(() -&gt; {            long stamp = lock.writeLock(); // 读锁或写锁会返回一个标记            try {                ConcurrentUtils.sleep(1);                map.put(“foo”, “bar”);            } finally {                lock.unlockWrite(stamp);            }        });        Runnable readTask = () -&gt; {            long stamp = lock.readLock();            try {                System.out.println(map.get(“foo”));                ConcurrentUtils.sleep(1);            } finally {                lock.unlockRead(stamp);            }        };        executor.submit(readTask);        executor.submit(readTask);        ConcurrentUtils.stop(executor);    }</code></pre><p>此外，StampedLock支持另一种叫做<strong>乐观锁</strong>（optimistic locking）的模式。乐观的读锁通过调用<code>tryOptimisticRead()</code>获取，它总是返回一个标记而不阻塞当前线程，无论锁是否真正可用。如果已经有写锁被拿到，返回的标记等于0。你需要总是通过<code>lock.validate(stamp)</code>检查标记是否有效。</p><pre><code class="language-java">/** * 乐观锁 *  乐观锁在刚刚拿到锁之后是有效的。和普通的读锁不同的是，乐观锁不阻止其他线程同时获取写锁。 *  在第一个线程暂停一秒之后，第二个线程拿到写锁而无需等待乐观的读锁被释放。 *  此时，乐观的读锁就不再有效了。甚至当写锁释放时，乐观的读锁还处于无效状态。 *  所以在使用乐观锁时，你需要每次在访问任何共享可变变量之后都要检查锁，来确保读锁仍然有效。 *  * @author wenqy * @date 2020年1月18日 下午3:49:31 */private void optimisticLock() {    System.out.println(“—–&gt;optimisticLock—-&gt;”);    ExecutorService executor = Executors.newFixedThreadPool(2);    StampedLock lock = new StampedLock();    executor.submit(() -&gt; {        long stamp = lock.tryOptimisticRead();        try {            System.out.println(“Optimistic Lock Valid: “ + lock.validate(stamp));            ConcurrentUtils.sleep(1);            System.out.println(“Optimistic Lock Valid: “ + lock.validate(stamp));            ConcurrentUtils.sleep(2);            System.out.println(“Optimistic Lock Valid: “ + lock.validate(stamp));        } finally {            lock.unlock(stamp);        }    });    executor.submit(() -&gt; {        long stamp = lock.writeLock();        try {            System.out.println(“Write Lock acquired”);            ConcurrentUtils.sleep(2);        } finally {            lock.unlock(stamp);            System.out.println(“Write done”);        }    });    ConcurrentUtils.stop(executor);}/** * 读锁转换为写锁 *  * @author wenqy * @date 2020年1月18日 下午4:00:10 */private void convertToWriteLock() {    count = 0;    System.out.println(“—–&gt;convertToWriteLock—-&gt;”);    ExecutorService executor = Executors.newFixedThreadPool(2);    StampedLock lock = new StampedLock();    executor.submit(() -&gt; {        long stamp = lock.readLock();        try {            if (count == 0) {                stamp = lock.tryConvertToWriteLock(stamp); // 读锁转换为写锁而不用再次解锁和加锁                if (stamp == 0L) { // 调用不会阻塞，但是可能会返回为零的标记，表示当前没有可用的写锁                    System.out.println(“Could not convert to write lock”);                    stamp = lock.writeLock(); // 阻塞当前线程，直到有可用的写锁                }                count = 23;            }            System.out.println(count);        } finally {            lock.unlock(stamp);        }    });    ConcurrentUtils.stop(executor);}</code></pre><h4 id="信号量">信号量</h4><p>除了锁之外，并发API也支持计数的信号量。不过锁通常用于变量或资源的互斥访问，信号量可以维护整体的准入许可。</p><pre><code class="language-java">/**     * 信号量     *      * @author wenqy     * @date 2020年1月18日 下午4:13:11     */    private void doSemaphore() {        ExecutorService executor = Executors.newFixedThreadPool(10);        Semaphore semaphore = new Semaphore(5); // 并发访问总数        Runnable longRunningTask = () -&gt; {            boolean permit = false;            try {                permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);                if (permit) {                    System.out.println(“Semaphore acquired”);                    ConcurrentUtils.sleep(5);                } else { // 等待超时之后，会向控制台打印不能获取信号量的结果                    System.out.println(“Could not acquire semaphore”);                }            } catch (InterruptedException e) {                throw new IllegalStateException(e);            } finally {                if (permit) {                    semaphore.release();                }            }        };        IntStream.range(0, 10)            .forEach(i -&gt; executor.submit(longRunningTask));        ConcurrentUtils.stop(executor);    }</code></pre><h3 id="原子变量">原子变量</h3><p><code>java.concurrent.atomic</code>包包含了许多实用的类，用于执行原子操作。如果你能够在多线程中同时且安全地执行某个操作，而不需要<code>synchronized</code>关键字或锁，那么这个操作就是原子的。</p><p>本质上，原子操作严重依赖于比较与交换（CAS），它是由多数现代CPU直接支持的原子指令。这些指令通常比同步块要快。所以在只需要并发修改单个可变变量的情况下，我建议你优先使用原子类，而不是锁。可以看下java包提供的一些原子变量例子AtomicInteger、<span class="comment">LongAdder</span> 、LongAccumulator、concurrentHashMap等等。</p><pre><code class="language-java">/**     * AtomicInteger     *      incrementAndGet     * @author wenqy     * @date 2020年1月18日 下午4:27:10     */    private void atomicIntegerIncre() {        AtomicInteger atomicInt = new AtomicInteger(0);        ExecutorService executor = Executors.newFixedThreadPool(2);        IntStream.range(0, 1000)            .forEach(i -&gt; executor.submit(atomicInt::incrementAndGet)); // ++        ConcurrentUtils.stop(executor);        System.out.println(atomicInt.get());    // =&gt; 1000    }    /**     * AtomicInteger     *  updateAndGet     * @author wenqy     * @date 2020年1月18日 下午4:29:26     */    private void atomicIntegerUpdateAndGet() {        AtomicInteger atomicInt = new AtomicInteger(0);        ExecutorService executor = Executors.newFixedThreadPool(2);        IntStream.range(0, 1000)            .forEach(i -&gt; {                Runnable task = () -&gt;                    atomicInt.updateAndGet(n -&gt; n + 2); // 结果累加2                executor.submit(task);            });        ConcurrentUtils.stop(executor);        System.out.println(atomicInt.get());    // =&gt; 2000    }    /**     * LongAdder     *      AtomicLong的替代，用于向某个数值连续添加值     *      内部维护一系列变量来减少线程之间的争用，而不是求和计算单一结果     *      当多线程的更新比读取更频繁时，这个类通常比原子数值类性能更好。     *      这种情况在抓取统计数据时经常出现，例如，你希望统计Web服务器上请求的数量。     *      LongAdder缺点是较高的内存开销，因为它在内存中储存了一系列变量。     * @author wenqy     * @date 2020年1月18日 下午4:33:29     */    private void longAdder() {        LongAdder adder = new LongAdder();        ExecutorService executor = Executors.newFixedThreadPool(2);        IntStream.range(0, 1000)            .forEach(i -&gt; executor.submit(adder::increment));        ConcurrentUtils.stop(executor);        System.out.println(adder.sumThenReset());   // =&gt; 1000    }    /**     * LongAccumulator     *      LongAccumulator是LongAdder的更通用的版本     *      内部维护一系列变量来减少线程之间的争用     * @author wenqy     * @date 2020年1月18日 下午4:35:11     */    private void longAccumulator() {        LongBinaryOperator op = (x, y) -&gt; 2 * x + y;        LongAccumulator accumulator = new LongAccumulator(op, 1L);        ExecutorService executor = Executors.newFixedThreadPool(2);        // i=0  2 * 1 + 0 = 2;        // i=2  2 * 2 + 2 = 6;        // i=3  2 * 6 + 3 = 15;        // i=4  2 * 15 + 4 = 34;        IntStream.range(0, 10)            .forEach(i -&gt; executor.submit(() -&gt; {                        accumulator.accumulate(i);                        System.out.println(“i:” + i + ” result:” + accumulator.get());                    })                );        // 初始值为1。每次调用accumulate(i)的时候，当前结果和值i都会作为参数传入lambda表达式。        ConcurrentUtils.stop(executor);        System.out.println(accumulator.getThenReset());     // =&gt; 2539    }    /**     * concurrentMap     *      * @author wenqy     * @date 2020年1月18日 下午4:38:09     */    private void concurrentMap() {        System.out.println(“—–&gt;concurrentMap—–&gt;”);        ConcurrentMap&lt;String, String&gt; map = new ConcurrentHashMap&lt;&gt;();        map.put(“foo”, “bar”);        map.put(“han”, “solo”);        map.put(“r2”, “d2”);        map.put(“c3”, “p0”);        map.forEach((key, value) -&gt; System.out.printf(“%s = %s\n”, key, value));        String value = map.putIfAbsent(“c3”, “p1”);        System.out.println(value);    // p0  提供的键不存在时，将新的值添加到映射        System.out.println(map.getOrDefault(“hi”, “there”));    // there 传入的键不存在时，会返回默认值        map.replaceAll((key, val) -&gt; “r2”.equals(key) ? “d3” : val);        System.out.println(map.get(“r2”));    // d3        map.compute(“foo”, (key, val) -&gt; val + val);        System.out.println(map.get(“foo”));   // barbar 转换单个元素，而不是替换映射中的所有值        map.merge(“foo”, “boo”, (oldVal, newVal) -&gt; newVal + ” was “ + oldVal);        System.out.println(map.get(“foo”));   // boo was foo    }    /**     * concurrentHashMap     *      * @author wenqy     * @date 2020年1月18日 下午4:38:42     */    private void concurrentHashMap() {        System.out.println(“—–&gt;concurrentHashMap—–&gt;”);        ConcurrentHashMap&lt;String, String&gt; map = new ConcurrentHashMap&lt;&gt;();        map.put(“foo”, “bar”);        map.put(“han”, “solo”);        map.put(“r2”, “d2”);        map.put(“c3”, “p0”);        map.forEach(1, (key, value) -&gt;        System.out.printf(“key: %s; value: %s; thread: %s\n”,            key, value, Thread.currentThread().getName())); // 可以并行迭代映射中的键值对        String result = map.search(1, (key, value) -&gt; {            System.out.println(Thread.currentThread().getName());            if (“foo”.equals(key)) { // 当前的键值对返回一个非空的搜索结果                return value; // 只要返回了非空的结果，就不会往下搜索了            }            return null;        }); // ConcurrentHashMap是无序的。搜索函数应该不依赖于映射实际的处理顺序        System.out.println(“Result: “ + result);        String searchResult = map.searchValues(1, value -&gt; {            System.out.println(Thread.currentThread().getName());            if (value.length() &gt; 3) {                return value;            }            return null;        }); // 搜索映射中的值        System.out.println(“Result: “ + searchResult);        String reduceResult = map.reduce(1,            (key, value) -&gt; {                System.out.println(“Transform: “ + Thread.currentThread().getName());                return key + “=” + value;            },            (s1, s2) -&gt; {                System.out.println(“Reduce: “ + Thread.currentThread().getName());                return s1 + “, “ + s2;            });        // 第一个函数将每个键值对转换为任意类型的单一值。        // 第二个函数将所有这些转换后的值组合为单一结果，并忽略所有可能的null值        System.out.println(“Result: “ + reduceResult);    }</code></pre><p>我们学了java8的一些新特性和并发编程例子，暂且告一段落了，demo已上传至github：<a href="https://github.com/wenqy/java-study">https://github.com/wenqy/java-study</a></p><h3 id="参考">参考</h3><p><u><a href="https://github.com/winterbe/java8-tutorial">https://github.com/winterbe/java8-tutorial</a></u> java8教程</p><p><u><a href="https://wizardforcel.gitbooks.io/modern-java/content/">https://wizardforcel.gitbooks.io/modern-java/content/</a></u> 中文译站</p><p><a href="https://github.com/wenqy/java-study">https://github.com/wenqy/java-study</a> 学习例子</p>]]>
                    </description>
                    <pubDate>Sun, 09 Feb 2020 17:17:44 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[java8数据流再识]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/java8数据流再识md</link>
                    <description>
                            <![CDATA[<p>数据流操作要么是衔接操作，要么是终止操作。当一个函数不修改数据流的底层数据源，它就是无干扰的。当一个函数的操作的执行是确定性的，它就是无状态的。</p><p>数据流可以从多种数据源创建，尤其是集合。可以有不同类型的数据流。</p><pre><code class="language-java">/**     * 从多种数据源创建数据流     *      * @author wenqy     * @date 2020年1月17日 上午11:00:15     */    private void diffStreamType() {        System.out.println(“—–&gt;diffStreamType—–&gt;”);        Arrays.asList(“a1”, “a2”, “a3”)            .stream()            .findFirst()            .ifPresent(System.out::println);  // a1        Stream.of(“a1”, “a2”, “a3”)            .findFirst()            .ifPresent(System.out::println);  // a1        IntStream.range(1, 4)   // 基本数据类型 // 1 2 3            .forEach(System.out::println);    }</code></pre><p>基本数据流和对象数据流间也可以转换</p><pre><code class="language-java">/**     * 基本数据流操作     *      * @author wenqy     * @date 2020年1月17日 上午11:01:26     */    private void baseStream() {        System.out.println(“—–&gt;baseStream—–&gt;”);        Arrays.stream(new int[] {1, 2, 3})            .map(n -&gt; 2 * n + 1)            .average() // 终止操作，求平均值            .ifPresent(System.out::println);  // 5.0        Stream.of(“a1”, “a2”, “a3”)            .map(s -&gt; s.substring(1))            .mapToInt(Integer::parseInt) // 对象数据流转换为基本数据流            .max()            .ifPresent(System.out::println);  // 3        IntStream.range(1, 4)            .mapToObj(i -&gt; “a” + i) // 基本数据流转换为对象数据流            .forEach(System.out::println); // a1 a2 a3    }</code></pre><p>数据流处理时，衔接操作的一个重要特性就是延迟性，在调用链上是垂直移动的，减少每个元素上所执行的实际操作数量。</p><pre><code class="language-java">/**     * 处理顺序     *      * @author wenqy     * @date 2020年1月17日 上午11:08:33     */    private void streamHandleSort() {        System.out.println(“—–&gt;streamHandleSort—–&gt;”);        Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)            .filter(s -&gt; {                System.out.println(“filter: “ + s);                return true;            })            .forEach(s -&gt; System.out.println(“forEach: “ + s));        // filter:  d2 forEach: d2 … 每个元素在调用链上垂直移动        Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)            .map(s -&gt; {                System.out.println(“map: “ + s);                return s.toUpperCase();            })            .anyMatch(s -&gt; {                System.out.println(“anyMatch: “ + s);                return s.startsWith(“A”);            });        // map:d2 anyMatch:D2 map:a2 anyMatch:A2 anyMatch返回true时终止        Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)            .map(s -&gt; {                System.out.println(“map: “ + s);                return s.toUpperCase();            })            .filter(s -&gt; {                System.out.println(“filter: “ + s);                return s.startsWith(“A”);            })            .forEach(s -&gt; System.out.println(“forEach: “ + s));        // map和filter会对底层集合的每个字符串调用五次，而forEach只会调用一次        Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)            .filter(s -&gt; {                System.out.println(“filter: “ + s);                return s.startsWith(“a”);            })            .map(s -&gt; {                System.out.println(“map: “ + s);                return s.toUpperCase();            })            .forEach(s -&gt; System.out.println(“forEach: “ + s));        // filter移动到调用链的顶端  map只会调用一次 执行更快        Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)            .sorted((s1, s2) -&gt; {                System.out.printf(“sort: %s; %s\n”, s1, s2);                return s1.compareTo(s2);            })            .filter(s -&gt; {                System.out.println(“filter: “ + s);                return s.startsWith(“a”);            })            .map(s -&gt; {                System.out.println(“map: “ + s);                return s.toUpperCase();            })            .forEach(s -&gt; System.out.println(“forEach: “ + s));        // 排序是一类特殊的衔接操作。它是有状态的操作，因为你需要在处理中保存状态来对集合中的元素排序        Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)            .filter(s -&gt; {                System.out.println(“filter: “ + s);                return s.startsWith(“a”);            })            .sorted((s1, s2) -&gt; {                System.out.printf(“sort: %s; %s\n”, s1, s2);                return s1.compareTo(s2);            })            .map(s -&gt; {                System.out.println(“map: “ + s);                return s.toUpperCase();            })            .forEach(s -&gt; System.out.println(“forEach: “ + s));        // 重排调用链来优化性能,这个例子中sorted永远不会调用,极大提升性能    }</code></pre><p>java8的数据流不能被复用。一旦你调用了任何终止操作，数据流就关闭了，要克服这个限制，我们需要为每个我们想要执行的终止操作创建新的数据流调用链。例如，我们创建一个数据流供应器，来构建新的数据流，并且设置好所有衔接操作。</p><pre><code class="language-java">/**     * 复用数据流     *      * @author wenqy     * @date 2020年1月17日 上午11:40:02     */    private void streamReuse() {        System.out.println(“—–&gt;streamReuse—–&gt;”);        Stream&lt;String&gt; stream =                Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)                    .filter(s -&gt; s.startsWith(“a”));        stream.anyMatch(s -&gt; true);    // ok//      stream.noneMatch(s -&gt; true);   // exception  java.lang.IllegalStateException: stream has already been operated upon or closed        Supplier&lt;Stream&lt;String&gt;&gt; streamSupplier =                () -&gt; Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)                        .filter(s -&gt; s.startsWith(“a”));        // 每次对get()的调用都构造了一个新的数据流，我们将其保存来调用终止操作        streamSupplier.get().anyMatch(s -&gt; true);   // ok        streamSupplier.get().noneMatch(s -&gt; true);  // ok    }</code></pre><p><strong>collect</strong></p><p><code>collect</code>是非常有用的终止操作，将流中的元素存放在不同类型的结果中，例如List、Set或者Map。<code>collect</code>接受收集器（<code>Collector</code>），它由四个不同的操作组成：供应器（<code>supplier</code>）、累加器（<code>accumulator</code>）、组合器（<code>combiner</code>）和终止器（<code>finisher</code>）。</p><pre><code class="language-java">/**     * collect是非常有用的终止操作，将流中的元素存放在不同类型的结果中，例如List、Set或者Map。     * collect接受收集器（Collector），它由四个不同的操作组成：     *  供应器（supplier）、累加器（accumulator）、组合器（combiner）和终止器（finisher）     *      * @author wenqy     * @date 2020年1月17日 上午11:45:18     */    private void streamCollect() {        System.out.println(“—–&gt;streamCollect—–&gt;”);        List&lt;Person&gt; persons = getPersionList();        List&lt;Person&gt; filtered =            persons                .stream()                .filter(p -&gt; p.getFirstName().startsWith(“P”))                .collect(Collectors.toList()); // 构造list        System.out.println(filtered);    // [Person [firstName=Peter, lastName=null, age=23], Person [firstName=Pamela, lastName=null, age=23]]        Map&lt;Integer, List&lt;Person&gt;&gt; personsByAge = persons            .stream()            .collect(Collectors.groupingBy(p -&gt; p.getAge())); // 构造map key: age        personsByAge            .forEach((age, p) -&gt; System.out.format(“age %s: %s\n”, age, p));        IntSummaryStatistics ageSummary =            persons                .stream()                .collect(Collectors.summarizingInt(p -&gt; p.getAge()));        System.out.println(ageSummary); // 统计：简单计算最小年龄、最大年龄、算术平均年龄、总和和数量        String phrase = persons            .stream()            .filter(p -&gt; p.getAge() &gt;= 18)            .map(p -&gt; p.getFirstName()) // 键必须是唯一的，否则会抛出IllegalStateException异常            .collect(Collectors.joining(” and “, “In China “, ” are of legal age.”));        System.out.println(phrase); // 所有人连接为一个字符串        Collector&lt;Person, StringJoiner, String&gt; personNameCollector =            Collector.of(                () -&gt; new StringJoiner(” | “),          // supplier                (j, p) -&gt; j.add(p.getFirstName().toUpperCase()),  // accumulator                (j1, j2) -&gt; j1.merge(j2),               // combiner                StringJoiner::toString);                // finisher        String names = persons            .stream()            .collect(personNameCollector);        System.out.println(names);  // MAX | PETER | PAMELA | DAVID        // 构建自己特殊收集器。将流中的所有人转换为一个字符串，包含所有大写的名称，并以|分割。    }</code></pre><p><strong>flatMap</strong></p><p>我们已经了解了如何通过使用map操作，将流中的对象转换为另一种类型。map有时十分受限，因为每个对象只能映射为一个其它对象。但如何我希望将一个对象转换为多个或零个其他对象呢？<code>flatMap</code>这时就会派上用场。</p><p><code>flatMap</code>将流中的每个元素，转换为其它对象的流。所以每个对象会被转换为零个、一个或多个其它对象，以流的形式返回。这些流的内容之后会放进<code>flatMap</code>所返回的流中。</p><pre><code class="language-java">/** * flatMap将流中的每个元素，转换为其它对象的流。 * 所以每个对象会被转换为零个、一个或多个其它对象，以流的形式返回。 * 这些流的内容之后会放进flatMap所返回的流中 *  * @author wenqy * @date 2020年1月17日 下午1:38:09 */private void streamFlatMap() {    System.out.println(“—–&gt;streamFlatMap—–&gt;”);    List&lt;Foo&gt; foos = new ArrayList&lt;&gt;();    // create foos    IntStream        .range(1, 4)        .forEach(i -&gt; foos.add(new Foo(“Foo” + i)));    // create bars    foos.forEach(f -&gt;        IntStream            .range(1, 4)            .forEach(i -&gt; f.bars.add(new Bar(“Bar” + i + ” &lt;- “ + f.name))));    foos.stream()        .flatMap(f -&gt; f.bars.stream())        .forEach(b -&gt; System.out.println(b.name)); // 将含有三个foo对象中的流转换为含有九个bar对象的流    IntStream.range(1, 4)        .mapToObj(i -&gt; new Foo(“Foo” + i))        .peek(f -&gt; IntStream.range(1, 4)            .mapToObj(i -&gt; new Bar(“Bar” + i + ” &lt;- “ + f.name))            .forEach(f.bars::add)) // 简化为流式操作的单一流水线        .flatMap(f -&gt; f.bars.stream())        .forEach(b -&gt; System.out.println(b.name));    Optional.of(new Outer())        .flatMap(o -&gt; Optional.ofNullable(o.nested))        .flatMap(n -&gt; Optional.ofNullable(n.inner))        .flatMap(i -&gt; Optional.ofNullable(i.foo))        .ifPresent(System.out::println);    // 如果存在的话，每个flatMap的调用都会返回预期对象的Optional包装，    // 否则为null的Optional包装,避免潜在NullPointerException}</code></pre><p><strong>reduce</strong></p><p>归约操作将所有流中的元素组合为单一结果。Java8支持三种不同类型的<code>reduce</code>方法。</p><pre><code class="language-java">/**     * 归约操作将所有流中的元素组合为单一结果     *      * @author wenqy     * @date 2020年1月17日 下午2:01:23     */    private void streamReduce() {        System.out.println(“—–&gt;streamReduce—–&gt;”);        List&lt;Person&gt; persons = getPersionList();        persons            .stream()            .reduce((p1, p2) -&gt; p1.getAge() &gt; p2.getAge() ? p1 : p2)            .ifPresent(System.out::println);    // 计算年龄最大的人 Pamela        Person result =            persons                .stream()                .reduce(new Person(“”, 0), (p1, p2) -&gt; {                    p1.setAge(p1.getAge() + p2.getAge());                    p1.setFirstName(p1.getFirstName() + p2.getFirstName());                    return p1;  // 构造带有聚合后名称和年龄的新Person对象                });        // name=MaxPeterPamelaDavid; age=76        System.out.format(“name=%s; age=%s.\n”, result.getFirstName(), result.getAge());        Integer ageSum = persons            .stream()            .reduce(0, (sum, p) -&gt; sum += p.getAge(), (sum1, sum2) -&gt; sum1 + sum2);        System.out.println(ageSum);  // 计算所有人的年龄总和 76        Integer ageSum2 = persons            .stream()            .reduce(0,                (sum, p) -&gt; {                    System.out.format(“accumulator: sum=%s; person=%s\n”, sum, p);                    return sum += p.getAge();                },                (sum1, sum2) -&gt; {                    System.out.format(“combiner: sum1=%s; sum2=%s\n”, sum1, sum2);                    return sum1 + sum2;                });        System.out.println(ageSum2); // 输出调试信息，combiner并没有输出        Integer ageSum3 = persons            .parallelStream()            .reduce(0,                (sum, p) -&gt; {                    System.out.format(“accumulator: sum=%s; person=%s [%s]\n”, sum, p, Thread.currentThread().getName());                    return sum += p.getAge();                },                (sum1, sum2) -&gt; {                    System.out.format(“combiner: sum1=%s; sum2=%s [%s]\n”, sum1, sum2, Thread.currentThread().getName());                    return sum1 + sum2;                });        System.out.println(ageSum3); // 并行方式    }</code></pre><p><strong>parallelStream</strong></p><p>流可以并行执行，在大量输入元素上可以提升运行时的性能。并行流使用公共的<code>ForkJoinPool</code>，由<code>ForkJoinPool.commonPool()</code>方法提供。底层线程池的大小最大为五个线程 -- 取决于CPU的物理核数。</p><p>组合器函数只在并行流中调用，而不在串行流中调用</p><pre><code class="language-java">/**     * 并行流     *      * @author wenqy     * @date 2020年1月17日 下午2:31:57     */    private void streamParallel() {        System.out.println(“—–&gt;streamParallel—–&gt;”);        ForkJoinPool commonPool = ForkJoinPool.commonPool();        System.out.println(commonPool.getParallelism());    // 底层线程池的大小 — 取决于CPU的物理核数 本机 默认 7         // 可用JVM参数增减 -Djava.util.concurrent.ForkJoinPool.common.parallelism=5        Arrays.asList(“a1”, “a2”, “b1”, “c2”, “c1”)            .parallelStream()            .filter(s -&gt; {                System.out.format(“filter: %s [%s]\n”,                    s, Thread.currentThread().getName());                return true;            })            .map(s -&gt; {                System.out.format(“map: %s [%s]\n”,                    s, Thread.currentThread().getName());                return s.toUpperCase();            })            .forEach(s -&gt; System.out.format(“forEach: %s [%s]\n”,                s, Thread.currentThread().getName()));        // 并行流使用了所有公共的ForkJoinPool中的可用线程来执行流式操作        Arrays.asList(“a1”, “a2”, “b1”, “c2”, “c1”)            .parallelStream()            .filter(s -&gt; {                System.out.format(“filter: %s [%s]\n”,                    s, Thread.currentThread().getName());                return true;            })            .map(s -&gt; {                System.out.format(“map: %s [%s]\n”,                    s, Thread.currentThread().getName());                return s.toUpperCase();            })            .sorted((s1, s2) -&gt; {                System.out.format(“sort: %s &lt;&gt; %s [%s]\n”,                    s1, s2, Thread.currentThread().getName());                return s1.compareTo(s2);            })            .forEach(s -&gt; System.out.format(“forEach: %s [%s]\n”,                s, Thread.currentThread().getName()));        // sort看起来只在主线程上串行执行。实际上，并行流上的sort在背后使用了Java8中新的方法Arrays.parallelSort()。        // 如javadoc所说，这个方法会参照数据长度来决定以串行或并行来执行,如果指定数据的长度小于最小粒度，它使用相应的Arrays.sort方法来排序        // 所有并行流操作都共享相同的JVM相关的公共ForkJoinPool。所以你可能需要避免实现又慢又卡的流式操作，因为它可能会拖慢你应用中严重依赖并行流的其它部分。    }</code></pre><p><strong>参考</strong></p><p><u><a href="https://github.com/winterbe/java8-tutorial">https://github.com/winterbe/java8-tutorial</a></u> java8教程</p><p><u><a href="https://wizardforcel.gitbooks.io/modern-java/content/">https://wizardforcel.gitbooks.io/modern-java/content/</a></u> 中文译站</p>]]>
                    </description>
                    <pubDate>Sat, 08 Feb 2020 17:18:52 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[品茗Java8新特性]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/品茗java8新特性</link>
                    <description>
                            <![CDATA[<p>看了下openjdk官网 <a href="http://openjdk.java.net/projects/jdk/14/"><u>http://openjdk.java.net/projects/jdk/14/</u></a> jdk14今年就要发布稳定版本了，连java8都没有系统的学习过。那就来学习下java8吧，这是一个长期维护的版本。这里主要来学习java8的一些新特性。对应demo可以下载链接：</p><p><a href="https://github.com/wenqy/java-study">https://github.com/wenqy/java-study</a></p><h3 id="接口默认方法">接口默认方法</h3><p>使用<code>default</code>关键字，为接口声明添加非抽象的方法实现接口默认方法</p><pre><code class="language-java">public interface DefaultInterfaceMethod {    double calc(int a);    /**     * 接口使用 &lt;b&gt;default&lt;/b&gt;关键字，添加非抽象方法实现（扩展方法）     * @param a     * @return     * @author wenqy     * @date 2020年1月16日 上午9:55:20     */    default double sqrt(int a) {        return Math.sqrt(a);    }}</code></pre><p>然后实现匿名内部类，并调用默认方法</p><pre><code class="language-java">DefaultInterfaceMethod interfaceMethod = new DefaultInterfaceMethod() {    @Override    public double calc(int a) {        return sqrt(a * 100);    }};System.out.println(“calc：” + interfaceMethod.calc(100)); // calc：100.0System.out.println(“sqrt：” + interfaceMethod.sqrt(16)); // sqrt：4.0</code></pre><h3 id="lambda表达式">Lambda表达式</h3><p>来看下排序的例子，静态工具方法<code>Collections.sort</code>接受一个list，和一个<code>Comparator</code>接口作为输入参数，java8之前需要一个匿名对象，java8可以用Lambda表达式变得简洁</p><pre><code class="language-java">    /**     * Java8之前方法（匿名对象）实现排序     * @param names     * @author wenqy     * @date 2020年1月16日 上午10:07:45     */    private static void sortByBefore8Method(List&lt;String&gt; names) {        Collections.sort(names, new Comparator&lt;String&gt;() {            @Override            public int compare(String a, String b) {                return b.compareTo(a);            }        });    }    /**     * 用Lambda表达式排序，Java编译器能够自动识别参数类型     * @param names     * @author wenqy     * @date 2020年1月16日 上午10:09:40     */    private static void sortByLambda(List&lt;String&gt; names) {//      可以这样写//      Collections.sort(names, (String a, String b) -&gt; {//          return b.compareTo(a);//      });//      也可以这样写//      Collections.sort(names, (String a, String b) -&gt; b.compareTo(a));//      还可以这样写        Collections.sort(names, (a, b) -&gt; b.compareTo(a));    }</code></pre><p>Java编译器能够自动识别参数的类型，所以可以省略掉类型不写。</p><h3 id="函数式接口">函数式接口</h3><p>每一个lambda都能够通过一个特定的接口，与一个给定的类型进行匹配。一个所谓的函数式接口必须要有且仅有一个抽象方法声明。每个与之对应的lambda表达式必须要与抽象方法的声明相匹配。由于默认方法不是抽象的，因此你可以在你的函数式接口里任意添加默认方法。</p><p>任意只包含一个抽象方法的接口，我们都可以用来做成lambda表达式。为了让你定义的接口满足要求，你应当在接口前加上<code>@FunctionalInterface</code> 标注。编译器会注意到这个标注，如果你的接口中定义了第二个抽象方法的话，编译器会抛出异常。</p><pre><code class="language-java">/** *  * 每一个lambda都能够通过一个特定的接口，与一个给定的类型进行匹配。 * 一个所谓的函数式接口必须要有且仅有一个抽象方法声明。 * 每个与之对应的lambda表达式必须要与抽象方法的声明相匹配。 *  * @FunctionalInterface 标注。 *  编译器会注意到这个标注，如果接口中定义了第二个抽象方法的话，编译器会抛出异常 *  * @version V5.0 * @author wenqy * @date   2020年1月16日 */@FunctionalInterfacepublic interface Converter&lt;F, T&gt; {    /**     * 类型转换  F-&gt;T     * @param from     * @return     * @author wenqy     * @date 2020年1月16日 上午10:22:32     */    T convert(F from);//  T mapping(F from);}</code></pre><h3 id="方法和构造函数引用">方法和构造函数引用</h3><p>函数式接口可以通过方法或构造函数引用使代码变得更简洁。Java 8 允许你通过<code>::</code>关键字获取方法或者构造函数的的引用。</p><pre><code class="language-java">static class Something {    String startsWith(String s) {        return String.valueOf(s.charAt(0));    }}private static &lt;F, T&gt; void convert(Converter&lt;F,T&gt; converter, F from) {    T converted = converter.convert(from);    System.out.println(converted);}public static void main(String[] args) {    Converter&lt;String, Integer&gt; converter1 = Integer::valueOf; // 静态方法引用    convert(converter1,“123”); // “1”    Something something = new Something();    Converter&lt;String, String&gt; converter2 = something::startsWith; // 对象方法引用    convert(converter2,“Java”); // “J”    PersonFactory&lt;Person&gt; personFactory = Person::new;  // 构造函数引用(构造参数需一致)    Person person = personFactory.create(“Peter”, “Parker”);    System.out.println(person); // Person [firstName=Peter, lastName=Parker]}</code></pre><h3 id="lambda访问范围">Lambda访问范围</h3><p>对于lambda表达式外部的变量，其访问权限的粒度与匿名对象的方式非常类似。你能够访问局部对应的外部区域的局部final变量，以及成员变量和静态变量。默认方法无法在lambda表达式内部被访问。</p><p>我们可以访问lambda表达式外部的<code>final</code>局部变量，但是与匿名对象不同的是，变量num并不需要一定是<code>final</code>，然而，num在编译的时候被隐式地当做<code>final</code>变量来处理。</p><pre><code class="language-java"> static class Lambda {        static int outerStaticNum;        int outerNum;        /**         * 访问成员变量         *          * @author wenqy         * @date 2020年1月16日 上午11:15:31         */        void accessOuterNum() {            Converter&lt;Integer, String&gt; converter = (from) -&gt; {                outerNum = 23;                return String.valueOf(outerNum + from);            };            System.out.println(“accessOuterNum：” + converter.convert(2)); // 25//          outerNum = 3;        }        /**         * 访问静态变量         *          * @author wenqy         * @date 2020年1月16日 上午11:18:41         */        void accessOuterStaticNum() {            Converter&lt;Integer, String&gt; converter = (from) -&gt; {                outerStaticNum = 72;                return String.valueOf(outerStaticNum + from);            };            System.out.println(“accessOuterStaticNum：” + converter.convert(2)); // 74//          outerStaticNum = 3;        }        /**         * 访问局部变量         *      可以访问lambda表达式外部的final局部变量         *      但是与匿名对象不同的是，变量num并不需要一定是final         * @author wenqy         * @date 2020年1月16日 上午11:06:09         */        void accessLocalVariable() {//          final int num = 1;            int num = 1;            Converter&lt;Integer, String&gt; stringConverter =                    (from) -&gt; String.valueOf(from + num);            System.out.println(“accessLocalVariable：” + stringConverter.convert(2));     // 3//          num = 3; // 编译的时候被隐式地当做final变量来处理，无法改变值        }    }    public static void main(String[] args) {        Lambda lambda = new Lambda();        lambda.accessLocalVariable();        lambda.accessOuterNum();        lambda.accessOuterStaticNum();//      默认方法无法在lambda表达式内部被访问,无法通过编译//      DefaultInterfaceMethod formula = (a) -&gt; sqrt( a * 100);    }</code></pre><h3 id="内置函数式接口">内置函数式接口</h3><p>Java 8 API 还提供了很多新的函数式接口，来降低程序员的工作负担。让我们来看下几个内置的函数式接口</p><p><code>Predicate</code>是一个布尔类型的函数，该函数只有一个输入参数。Predicate接口包含了多种默认方法，用于处理复杂的逻辑动词（and, or，negate）</p><p><code>Function</code>接口接收一个参数，并返回单一的结果。默认方法可以将多个函数串在一起（compse, andThen）</p><p><code>Supplier</code>接口产生一个给定类型的结果。与Function不同的是，Supplier没有输入参数</p><p><code>Consumer</code>代表了在一个输入参数上需要进行的操作</p><p><code>Comparator</code>接口在早期的Java版本中非常著名。Java 8 为这个接口添加了不同的默认方法</p><p><code>Optional</code>不是一个函数式接口，而是一个精巧的工具接口，用来防止<code>NullPointerException</code>产生。他是一个简单的值容器，这个值可以是null，也可以是non-null。考虑到一个方法可能会返回一个non-null的值，也可能返回一个空值。为了不直接返回null，我们在Java 8中就返回一个Optional</p><pre><code class="language-java">/**     * 布尔类型函数     *      * @author wenqy     * @date 2020年1月16日 上午11:39:26     */    private static void testPredicates() {        System.out.println(“——&gt;testPredicates——&gt;”);        Predicate&lt;String&gt; predicate = (s) -&gt; s.length() &gt; 0;        System.out.println(predicate.test(“foo”)); // true        System.out.println(predicate.negate().test(“foo”)); // false        Predicate&lt;Boolean&gt; nonNull = Objects::nonNull;        Predicate&lt;Boolean&gt; isNull = Objects::isNull;        System.out.println(nonNull.test(null)); // false        System.out.println(isNull.test(null)); // true        Predicate&lt;String&gt; isEmpty = String::isEmpty;        Predicate&lt;String&gt; isNotEmpty = isEmpty.negate();        System.out.println(isEmpty.test(“”)); // true        System.out.println(isNotEmpty.test(“”)); // false    }    /**     * 内置 Function 函数，将多个函数串联     *      * @author wenqy     * @date 2020年1月16日 上午11:41:35     */    private static void testFunctions() {        System.out.println(“——&gt;testFunctions——&gt;”);        Function&lt;String, Integer&gt; toInteger = Integer::valueOf;        Function&lt;String, String&gt; backToString = toInteger.andThen(String::valueOf);        System.out.println(backToString.apply(“123”));     // “123”    }    /**     * 内置 Supplier 函数     *  产生一个给定类型的结果     * @author wenqy     * @date 2020年1月16日 上午11:52:51     */    private static void testSuppliers() {        System.out.println(“——&gt;testSuppliers——&gt;”);        Supplier&lt;Person&gt; personSupplier = Person::new;        Person person = personSupplier.get();   // new Person        person.setFirstName(“wen”);        person.setLastName(“qy”);        System.out.println(person);    }    /**     * 内置 Consumer 函数     *  输入参数上需要进行操作     * @author wenqy     * @date 2020年1月16日 下午1:48:33     */    private static void testConsumers() {        System.out.println(“——&gt;testConsumers——&gt;”);        Consumer&lt;Person&gt; greeter = (p) -&gt; System.out.println(“Hello, “ + p.getFirstName());        greeter.accept(new Person(“Luke”, “Skywalker”));    }    /**     *      * 内置 Comparator 函数     *      用于比较     * @author wenqy     * @date 2020年1月16日 下午1:54:29     */    private static void testComparators() {        System.out.println(“——&gt;testComparators——&gt;”);        Comparator&lt;Person&gt; comparator = (p1, p2) -&gt; p1.getFirstName().compareTo(p2.getFirstName());        Person p1 = new Person(“John”, “Doe”);        Person p2 = new Person(“Alice”, “Wonderland”);        System.out.println(comparator.compare(p1, p2)); // &gt; 0        System.out.println(comparator.reversed().compare(p1, p2)); // &lt; 0    }    /**     * Optional     *  值容器，用来防止NullPointerException产生     * @author wenqy     * @date 2020年1月16日 下午1:58:44     */    private static void testOptionals() {        System.out.println(“——&gt;testOptionals——&gt;”);        Optional&lt;String&gt; optional = Optional.of(“bam”);        System.out.println(optional.isPresent()); // true        System.out.println(optional.get()); // “bam”        System.out.println(optional.orElse(“fallback”)); // “bam”        optional.ifPresent((s) -&gt; System.out.println(s.charAt(0)));     // “b”    }</code></pre><h3 id="流stream">流Stream</h3><p><code>java.util.Stream</code>表示了某一种元素的序列，在这些元素上可以进行各种操作。Stream操作可以是中间操作，也可以是完结操作。完结操作会返回一个某种类型的值，而中间操作会返回流对象本身，并且你可以通过多次调用同一个流操作方法来将操作结果串起来（就像<code>StringBuffer</code>的append方法一样）。Stream是在一个源的基础上创建出来的，例如<code>java.util.Collection</code>中的list或者set（map不能作为Stream的源）。Stream操作往往可以通过顺序或者并行两种方式来执行。Stream并不会改变原有的序列。</p><pre><code class="language-java">List&lt;String&gt; stringCollection = new ArrayList&lt;&gt;();public StreamsMain() {    init();}private void init() {    if (stringCollection.isEmpty()) {        stringCollection.add(“ddd2”);        stringCollection.add(“aaa2”);        stringCollection.add(“bbb1”);        stringCollection.add(“aaa1”);        stringCollection.add(“bbb3”);        stringCollection.add(“ccc”);        stringCollection.add(“bbb2”);        stringCollection.add(“ddd1”);    }}/** *  * Filter接受一个predicate接口类型的变量，并将所有流对象中的元素进行过滤。 * 该操作是一个中间操作，因此它允许我们在返回结果的基础上再进行其他的流操作（forEach）。 * ForEach接受一个function接口类型的变量，用来执行对每一个元素的操作。 * ForEach是一个中止操作。它不返回流，所以我们不能再调用其他的流操作。 * @author wenqy * @date 2020年1月16日 下午2:34:58 */private void testFilter() {    System.out.println(“—–&gt;testFilter—–&gt;”);    stringCollection        .stream()        .filter((s) -&gt; s.startsWith(“a”)) // 过滤以“a”开头的集合        .forEach(System.out::println);}/** * Sorted是一个中间操作，能够返回一个排过序的流对象的视图。 * 流对象中的元素会默认按照自然顺序进行排序，除非你自己指定一个Comparator接口来改变排序规则 *  * @author wenqy * @date 2020年1月16日 下午2:38:28 */private void testSorted() {    System.out.println(“—–&gt;testSorted—–&gt;”);    stringCollection        .stream()        .sorted() // 自然排序        .filter((s) -&gt; s.startsWith(“a”)) // 过滤以“a”开头的集合        .forEach(System.out::println);    // sorted不会改变原来集合中元素的顺序。原来string集合中的元素顺序是没有改变的。    System.out.println(stringCollection);}/** * map是一个对于流对象的中间操作，通过给定的方法，它能够把流对象中的每一个元素对应到另外一个对象上。 * 下面的例子就演示了如何把每个string都转换成大写的string.  * 不但如此，你还可以把每一种对象映射成为其他类型。 * 对于带泛型结果的流对象，具体的类型还要由传递给map的泛型方法来决定。 *  * @author wenqy * @date 2020年1月16日 下午2:43:38 */private void testMap() {    System.out.println(“—–&gt;testMap—–&gt;”);    stringCollection        .stream()        .map(String::toUpperCase) // 每个元素转大写        .sorted((a, b) -&gt; b.compareTo(a))        .forEach(System.out::println);    // sorted不会改变原来集合中元素的顺序。原来string集合中的元素顺序是没有改变的。    System.out.println(stringCollection);}/** * 匹配操作有多种不同的类型，都是用来判断某一种规则是否与流对象相互吻合的。 * 所有的匹配操作都是终结操作，只返回一个boolean类型的结果。 *  * @author wenqy * @date 2020年1月16日 下午2:47:48 */private void testMatch() {    System.out.println(“—–&gt;testMatch—–&gt;”);    boolean anyStartsWithA =        stringCollection            .stream()            .anyMatch((s) -&gt; s.startsWith(“a”));    System.out.println(anyStartsWithA);      // true    boolean allStartsWithA =        stringCollection            .stream()            .allMatch((s) -&gt; s.startsWith(“a”));    System.out.println(allStartsWithA);      // false    boolean noneStartsWithZ =        stringCollection            .stream()            .noneMatch((s) -&gt; s.startsWith(“z”));    System.out.println(noneStartsWithZ);      // true}/** * Count是一个终结操作，它的作用是返回一个数值，用来标识当前流对象中包含的元素数量。 *  * @author wenqy * @date 2020年1月16日 下午2:51:40 */private void testCount() {    System.out.println(“—–&gt;testCount—–&gt;”);    long startsWithB =        stringCollection            .stream()            .filter((s) -&gt; s.startsWith(“b”))            .count();    System.out.println(startsWithB);    // 3}/** * 该操作是一个终结操作，它能够通过某一个方法，对元素进行削减操作。 * 该操作的结果会放在一个Optional变量里返回。 *  * @author wenqy * @date 2020年1月16日 下午2:54:00 */private void testReduce() {    System.out.println(“—–&gt;testReduce—–&gt;”);    Optional&lt;String&gt; reduced =        stringCollection            .stream()            .sorted()            .reduce((s1, s2) -&gt; s1 + “#” + s2);    reduced.ifPresent(System.out::println); // “aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2”}</code></pre><h3 id="并发流stream">并发流Stream</h3><p>数量量大时，可以使用并行流进行操作来提高运行效率。</p><pre><code class="language-java">//  int max = 1000000; // 大数据量并发才有优势    int max = 400000;    List&lt;String&gt; values = new ArrayList&lt;&gt;(max);    public ParallelStreamsMain() {        init();    }    private void init() {        for (int i = 0; i &lt; max; i++) {            UUID uuid = UUID.randomUUID();            values.add(uuid.toString());        }    }    /**     * 顺序排序     *      * @author wenqy     * @date 2020年1月16日 下午3:20:22     */    private void sequenceSorted() {        System.out.println(“——&gt;sequenceSorted—–&gt;”);        long t0 = System.nanoTime();        long count = values.stream().sorted().count();        System.out.println(count);        long t1 = System.nanoTime();        long millis = TimeUnit.NANOSECONDS.toMillis(t1 – t0);        System.out.println(String.format(“sequential sort took: %d ms”, millis));    }    /**     * 并行排序     *      * @author wenqy     * @date 2020年1月16日 下午3:21:56     */    private void parallelSorted() {        System.out.println(“——&gt;parallelSorted—–&gt;”);        long t0 = System.nanoTime();        long count = values.parallelStream().sorted().count();        System.out.println(count);        long t1 = System.nanoTime();        long millis = TimeUnit.NANOSECONDS.toMillis(t1 – t0);        System.out.println(String.format(“parallel sort took: %d ms”, millis));    }</code></pre><h3 id="map">Map</h3><p>map是不支持流操作的。而更新后的map现在则支持多种实用的新方法</p><pre><code class="language-java">Map&lt;Integer, String&gt; map = new HashMap&lt;&gt;();for (int i = 0; i &lt; 10; i++) {    map.putIfAbsent(i, “val” + i); // 旧值存在时返回旧值，不进行替换}map.forEach((id, val) -&gt; System.out.println(val));map.computeIfPresent(3, (num, val) -&gt; val + num);map.get(3);             // val33map.computeIfPresent(9, (num, val) -&gt; null); // 返回null时，remove(key)map.containsKey(9);     // falsemap.computeIfAbsent(23, num -&gt; “val” + num);map.containsKey(23);    // truemap.computeIfAbsent(3, num -&gt; “bam”); // 旧值存在时返回旧值，不进行替换map.get(3);             // val33map.remove(3, “val3”);  // 校验value删除map.get(3);             // val33map.remove(3, “val33”);map.get(3);             // nullmap.getOrDefault(42, “not found”);  // not found 不存在则返回默认值map.merge(9, “val9”, (value, newValue) -&gt; value.concat(newValue)); // 先前key 9 已经删除了，不存在key，则返回 valueSystem.out.println(map.get(9));             // val9map.merge(9, “concat”, (value, newValue) -&gt; value.concat(newValue)); // 存在key,才进行合并System.out.println(map.get(9));             // val9concat</code></pre><h3 id="时间日期api">时间日期API</h3><p>Java 8 包含了全新的时间日期API，这些功能都放在了<code>java.time</code>包下。新的时间日期API是基于Joda-Time库开发的，但是也不尽相同。</p><pre><code class="language-java">/** * Clock提供了对当前时间和日期的访问功能。Clock是对当前时区敏感的 *  * @author wenqy * @date 2020年1月16日 下午4:36:49 */private static void testClock() {    System.out.println(“—–&gt;testClock—–&gt;”);    Clock clock = Clock.systemDefaultZone();    long millis = clock.millis();    System.out.println(millis); // 获取当前毫秒时间    Instant instant = clock.instant();    Date date = Date.from(instant);   // java.util.Date    System.out.println(date); // 获取日期 Thu Jan 16 16:38:01 CST 2020}/** * 时区类可以用一个ZoneId来表示。 * 时区类还定义了一个偏移量，用来在当前时刻或某时间与目标时区时间之间进行转换。 *  * @author wenqy * @date 2020年1月16日 下午4:40:38 */private static void testTimezones() {    System.out.println(“—–&gt;testTimezones—–&gt;”);    System.out.println(ZoneId.getAvailableZoneIds());    // prints all available timezone ids    ZoneId zone1 = ZoneId.of(“Europe/Berlin”);    ZoneId zone2 = ZoneId.of(“Brazil/East”);    ZoneId zone3 = ZoneId.of(“Asia/Shanghai”);    System.out.println(zone1.getRules());    System.out.println(zone2.getRules());    System.out.println(zone3.getRules());}/** * 本地时间类表示一个没有指定时区的时间 *  * @author wenqy * @date 2020年1月16日 下午4:49:11 */private static void testLocalTime() {    System.out.println(“—–&gt;testLocalTime—–&gt;”);    LocalTime now1 = LocalTime.now(ZoneId.of(“Europe/Berlin”));    LocalTime now2 = LocalTime.now(ZoneId.of(“Asia/Shanghai”));    // 比较两个时间    System.out.println(now1.isBefore(now2));  // true    long hoursBetween = ChronoUnit.HOURS.between(now1, now2);    long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);    // 柏林时区在东一区，上海时区在东八区   柏林比北京时间慢7小时    System.out.println(hoursBetween);       // 7    System.out.println(minutesBetween);     // 420    // 时间字符串解析操作    LocalTime late = LocalTime.of(23, 59, 59);    System.out.println(late);       // 23:59:59    DateTimeFormatter germanFormatter =        DateTimeFormatter            .ofLocalizedTime(FormatStyle.SHORT)            .withLocale(Locale.GERMAN);    LocalTime leetTime = LocalTime.parse(“13:37”, germanFormatter);    System.out.println(leetTime);   // 13:37}/** * 本地日期 *  每一次操作都会返回一个新的时间对象 *  * @author wenqy * @date 2020年1月16日 下午5:05:52 */private static void testLocalDate() {    System.out.println(“—–&gt;testLocalDate—–&gt;”);    LocalDate today = LocalDate.now(); // 今天    LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); // 明天=今天+1    LocalDate yesterday = tomorrow.minusDays(2); // 昨天=明天-2    System.out.println(yesterday);    LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);    DayOfWeek dayOfWeek = independenceDay.getDayOfWeek(); // 获取周    System.out.println(dayOfWeek); // FRIDAY}/** * 日期-时间 *  final对象 * @author wenqy * @date 2020年1月16日 下午5:12:37 */private static void testLocalDateTime() {    System.out.println(“—–&gt;testLocalDateTime—–&gt;”);    LocalDateTime sylvester = LocalDateTime.of(2019, Month.DECEMBER, 31, 23, 59, 59);    DayOfWeek dayOfWeek = sylvester.getDayOfWeek();    System.out.println(dayOfWeek);      // TUESDAY    Month month = sylvester.getMonth();    System.out.println(month);          // DECEMBER    long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);    System.out.println(minuteOfDay);    // 1439    // 加上时区，转日期Date    Instant instant = sylvester            .atZone(ZoneId.systemDefault())            .toInstant();    Date legacyDate = Date.from(instant);    System.out.println(legacyDate);     // Tue Dec 31 23:59:59 CST 2019    // 自定义格式对象    DateTimeFormatter formatter =        DateTimeFormatter            .ofPattern(“yyyy-MM-dd HH:mm:ss”);  // 线程安全，不可变    LocalDateTime parsed = LocalDateTime.parse(“2020-01-16 17:13:00”, formatter);    String string = formatter.format(parsed);    System.out.println(string);     // 2020-01-16 17:13:00}</code></pre><h3 id="可重复注解">可重复注解</h3><p>Java 8中的注解是可重复的</p><pre><code class="language-java">/** * @Repeatable，Java 8 允许我们对同一类型使用多重注解 *  * @version V5.0 * @author wenqy * @date   2020年1月16日 */@Repeatable(Hints.class)@Retention(RetentionPolicy.RUNTIME) // 需要指定runtime,否则测试会失败public @interface Hint {    String value();}</code></pre><p>在Person类添加注解，尝试获取类注解</p><pre><code class="language-java">/** * @Hint 可重复注解（新方法） * 使用注解容器（老方法）: @Hints({@Hint(“hint1”), @Hint(“hint2”)}) * @version V5.0 * @author wenqy * @date   2020年1月16日 */@Hint(“hint1”)@Hint(“hint2”)//@Hints({@Hint(“hint1”), @Hint(“hint2”)})public class Person {    // …}Hint hint = Person.class.getAnnotation(Hint.class);System.out.println(hint);                   // nullHints hints1 = Person.class.getAnnotation(Hints.class);System.out.println(hints1.value().length);  // 2Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);System.out.println(hints2.length);          // 2</code></pre><h3 id="string">String</h3><p>java8添加了对字符数据流的流式操作</p><pre><code class="language-java">System.out.println(String.join(“:”, “foobar”, “foo”, “bar”)); // foobar:foo:bar        // 指定分割符，将字符串拼接        System.out.println(                “foobar:foo:bar”                .chars() // 字符数据流                .distinct()                .mapToObj(c -&gt; String.valueOf((char)c))                .sorted()                .collect(Collectors.joining())                ); // :abfor        System.out.println(                Pattern.compile(“:”)                .splitAsStream(“foobar:foo:bar”)                .filter(s -&gt; s.contains(“bar”))                .sorted()                .collect(Collectors.joining(“:”))            ); // bar:foobar        System.out.println(                Stream.of(“bob@gmail.com”, “alice@hotmail.com”)                    .filter(Pattern.compile(“.*@gmail\\.com”).asPredicate())                    .count()            ); // 1</code></pre><h3 id="数值处理">数值处理</h3><p>Java8添加了对无符号数的额外支持。新增了一些方法来处理数值溢出</p><pre><code class="language-java">System.out.println(Integer.MAX_VALUE);      // 2147483647        System.out.println(Integer.MAX_VALUE + 1);  // -2147483648        long maxUnsignedInt = (1l &lt;&lt; 32) – 1; //         String string = String.valueOf(maxUnsignedInt);        int unsignedInt = Integer.parseUnsignedInt(string, 10); // 无符号转换        String string2 = Integer.toUnsignedString(unsignedInt, 10);        System.out.println(string2);        try {            Integer.parseInt(string, 10);        } catch (NumberFormatException e) {            System.err.println(“could not parse signed int of “ + maxUnsignedInt);        }        try {            Math.addExact(Integer.MAX_VALUE, 1);        }        catch (ArithmeticException e) {            System.err.println(e.getMessage());            // =&gt; integer overflow        }        try {            Math.toIntExact(Long.MAX_VALUE);        }        catch (ArithmeticException e) {            System.err.println(e.getMessage());            // =&gt; integer overflow        }</code></pre><h3 id="文件处理">文件处理</h3><p>java8新增了对文件处理的方法</p><pre><code class="language-java">/** * 文件查找 * @throws IOException * @author wenqy * @date 2020年1月18日 下午3:02:05 */private static void findFiles() throws IOException {    Path start = Paths.get(“”);    int maxDepth = 5;    try (Stream&lt;Path&gt; stream = Files.find(start, maxDepth, (path, attr) -&gt;            String.valueOf(path).endsWith(“.js”))) {        String joined = stream            .sorted()            .map(String::valueOf)            .collect(Collectors.joining(“; “));        System.out.println(“Found: “ + joined);    }}/** * 获取文件列表 * @throws IOException * @author wenqy * @date 2020年1月18日 下午3:02:26 */private static void listFiles() throws IOException {    try (Stream&lt;Path&gt; stream = Files.list(Paths.get(“”))) {        String joined = stream            .map(String::valueOf)            .filter(path -&gt; !path.startsWith(“.”))            .sorted()            .collect(Collectors.joining(“; “));        System.out.println(“List: “ + joined);        // 列出了当前工作目录的所有文件，之后将每个路径都映射为它的字符串表示。之后结果被过滤、排序，最后连接为一个字符串    }}/** * 文件读写处理 * @throws IOException * @author wenqy * @date 2020年1月18日 下午3:02:52 */private static void handleFiles() throws IOException {    List&lt;String&gt; lines = Files.readAllLines(Paths.get(“res/nashorn1.js”)); // 整个文件都会读进内存,不高效。文件越大，所用的堆区也就越大    lines.add(“print(‘foobar’);”);    Files.write(Paths.get(“res/nashorn1-modified.js”), lines);    try (Stream&lt;String&gt; stream = Files.lines(Paths.get(“res/nashorn1.js”))) { // 行读取，高效点        stream            .filter(line -&gt; line.contains(“print”))            .map(String::trim)            .forEach(System.out::println);    }    try (BufferedReader reader = Files.newBufferedReader(Paths.get(“res/nashorn1.js”))) { // BufferedReader 更精细        System.out.println(reader.readLine());    }    Path pathOut = Paths.get(“res/output.js”);    try (BufferedWriter writer = Files.newBufferedWriter(pathOut)) { // BufferedWriter 写入文件        writer.write(“print(‘Hello World’);”);    }    try (BufferedReader reader = Files.newBufferedReader(Paths.get(“res/nashorn1.js”))) {        long countPrints = reader            .lines() // 流式处理            .filter(line -&gt; line.contains(“print”))            .count();        System.out.println(countPrints);    }}</code></pre><h3 id="避免-null-检查">避免 Null 检查</h3><p>java8之前，如果是多层内嵌对象，需要多次判空，引入Optional 类型提高安全性</p><pre><code class="language-java">public static void main(String[] args) {    Outer outer = new Outer();    if (outer != null &amp;&amp; outer.nested != null &amp;&amp; outer.nested.inner != null) {        System.out.println(outer.nested.inner.foo);    }    Optional.of(new Outer())        .map(Outer::getNested)        .map(Nested::getInner)        .map(Inner::getFoo)        .ifPresent(System.out::println);    Outer obj = new Outer();    resolve(() -&gt; obj.getNested().getInner().getFoo())        .ifPresent(System.out::println);    // 这两个解决方案可能没有传统 null 检查那么高的性能}private static &lt;T&gt; Optional&lt;T&gt; resolve(Supplier&lt;T&gt; resolver) {    try {        T result = resolver.get();        return Optional.ofNullable(result);    }    catch (NullPointerException e) {        return Optional.empty();    }}</code></pre><p>初识java8新特性，java8语法糖还是挺香的，java8之前方法调用一堆的回调，让人看的眼花缭乱，不整洁。但让人想吐槽的是调试，加难了调试力度，可能是新手错觉。。。</p><h3 id="参考">参考</h3><p><u><a href="https://github.com/winterbe/java8-tutorial">https://github.com/winterbe/java8-tutorial</a></u> java8教程</p><p><u><a href="https://wizardforcel.gitbooks.io/modern-java/content/">https://wizardforcel.gitbooks.io/modern-java/content/</a></u> 中文译站</p><p><a href="https://github.com/wenqy/java-study">https://github.com/wenqy/java-study</a></p><p><a href="https://github.com/wenqy/java-study">https://github.com/wenqy/java-study</a></p>]]>
                    </description>
                    <pubDate>Fri, 07 Feb 2020 17:51:14 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Git学步]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/git学步</link>
                    <description>
                            <![CDATA[<p>Git 是一款开源的分布式版本控制系统，是Linux之父Linus开发的版本控制软件，为帮助管理Linux内核开发而诞生，这期间有故事，当然，这不是我记录文章的重点，我只是感慨：牛逼！</p><p>这里只是记录Git学习的常用命令，话说20%的命令可以涵盖80%的场景，二八定律套一套，就可少学一套！这里不会记录Git的安装，也不会记载与SVN等其他版本控制系统的区别，只是学一学别人的步伐，具体深究详情可参考链接。</p><h3 id="创建版本库">创建版本库</h3><p>初始化本地仓库，当前目录下多了一个<code>.git</code>的目录</p><pre><code>git init</code></pre><p>将新建文件加入到Index暂存区</p><pre><code>git add testgitaddfile.txt</code></pre><p>将暂存区提交到当前分支版本库，创建Git版本库时，Git自动为我们创建了唯一一个<code>master</code>分支</p><pre><code>git commit -m &quot;add file&quot;</code></pre><p>提交流程可以由下面简图标识：</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_add_commit_flow-d20528d082ab46e6a62624ffc77a7a0c.png" alt="git_add_commit_flow" /></p><p>推送到远程关联的主分支</p><pre><code>git push -u origin master</code></pre><p>这里我已经绑定了自己安装的Gitlab，同步到Gitlab服务器</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_push_gitlab-538068c0f0ad4af9a843e23ff158c654.png" alt="git_push_gitlab" /></p><p>登录Gitlab查看，查看确实提交上去了</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_push_gitlab_detail-f0ea9c19dbc84a169e9a4da0a87f26ac.png" alt="git_push_gitlab_detail" /></p><h3 id="状态与比较">状态与比较</h3><p>修改<code>testgitaddfile.txt</code>文件内容后，查看状态，如看文件是否修改过</p><pre><code>git status</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_status_add_and_commit-5d4da3f427f94218b12956c05ab1823e.png" alt="git_status_add_and_commit" /></p><p>上图结果输出，可知，<code>testgitaddfile.txt</code>被修改过了，但还没有准备提交的修改。</p><p>我们可以进行文件比较，查看修改内容</p><pre><code>git diff testgitaddfile.txt</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_diff_imissyouverymuch-0cd62dd5b35748e5b5fb41ff7783c0d2.png" alt="git_diff_imissyouverymuch" /></p><p>可以看出距离上次文件修改添加了内容&quot;I miss you very much!&quot;</p><p>再次添加文件到暂存区<code>git add testgitaddfile.txt</code>后，查看状态，变成未提交：</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_status_add_not_committed-0b264175733e46d8a67de8130d5b3e9f.png" alt="git_status_add_not_committed" /></p><p>再次<code>git commit testgitaddfile.txt</code>后，状态变成没有未提交的文件，工作区是干净的：</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_status_add_and_commit-5d4da3f427f94218b12956c05ab1823e.png" alt="git_status_add_and_commit" /></p><p><code>git diff</code> 是只比较比较工作区和暂存区（最后一次add）的区别，<code>git diff --cached</code> 是只比较暂存区和版本库的区别，<code>git diff HEAD -- filename</code> 是只比较工作区和版本库（最后一次commit）的区别。</p><p>修改工作区<code>testgitaddfile.txt</code>文件内容后，再次比较：</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_diff_head_cached-030e2a0968ad496fada28c84ac1f975a.png" alt="git_diff_head_cached" /></p><p>暂存区和版本库当前分支比较没有变化，工作区和暂存区、工作区和版本库当前分支比较都发生变化了。</p><p><code>git add</code> 文件，提交到暂存区后，继续比较：</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_diff_after_add-43227c61267f425992b3b020583a2b87.png" alt="git_diff_after_add" /></p><p>工作区和暂存区比较没有变化，版本库当前分支和暂存区、工作区和版本库当前分支比较都发生变化了。</p><h3 id="回退与撤销">回退与撤销</h3><p>我们可以先查看提交的版本历史记录，<code>--pretty=oneline</code> 进行单行输出，比较方便</p><pre><code>git log --pretty=oneline</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git-log-552d7b350ef9477abe4382450b2bd9a1.png" alt="git-log" /></p><p>commit id版本号计算出一串十六进制数字。</p><p><code>HEAD</code>表示当前版本  上一个版本就是<code>HEAD^</code>  上上一个版本就是<code>HEAD^^</code>，当然往上100个版本写100个^比较容易数不过来，所以写成<code>HEAD~100</code></p><p>回退上一个版本，这样历史就可以重写了</p><pre><code>git reset --hard HEAD^</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git-reset-e36effe52b8d46739991123d0b13898e.png" alt="git-reset" /></p><p>历史是可以重写 了，但是难免后悔，总想回到最后发生的剧本，蓦然回首才发现，最初注定的结局才是最香的。查看命令历史，并回到最新版本</p><pre><code>git reloggit reset --hard ff9e036</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_relog-f4d4428024b441b3bac11a12410946f8.png" alt="git_relog" /></p><p>人无完人，犯错总是难免的，工作区内容改错了，就原谅你，权当没有发生。我们需要撤销修改，回到最后一次add状态，直接丢弃工作区的修改</p><pre><code>git checkout -- testgitaddfile.txt</code></pre><p>没有<code>--</code>，就变成了&quot;切换到另一个分支&quot;的命令，撤销工作区修改注意加上<code>--</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/git_checkout__-b0e9684d470a46dda2472bc2eb2e955d.png" alt="git_checkout__" /></p><p><code>git reset</code>命令既可以回退版本，也可以把暂存区的修改回退到工作区。当我们用<code>HEAD</code>时，表示最新的版本</p><p>当你不仅改乱了工作区文件的内容，还添加到了暂存区时，想丢弃修改，分两步，第一步用命令<code>git reset HEAD &lt;file&gt;</code>，回退暂存区，第二步操作<code>git checkout -- &lt;file&gt;</code>回退工作区。</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_reset_head-120fbd30c7f649118bfc88ce7d3e1126.png" alt="git_reset_head" /></p><p>已经提交了不合适的修改到版本库时，直接版本回退，体验不同的平行空间吧</p><h3 id="删除文件">删除文件</h3><p>当某些不和适宜的文件需要丢弃时，就需要删除，也要提交到版本库</p><p>用<code>rm &lt;file&gt;</code>命令删掉文件后，查看文件状态</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-0b83a2ea53c8474a88f2230c92526170.png" alt="image.png" /></p><p>文件处于删除状态，需要把版本库的文件也删了</p><pre><code>git rm test.txtgit commit -m &quot;remove test.txt&quot;</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_rm_deleted-15a2be21145d4e8ea70a97d33338c492.png" alt="git_rm_deleted" /></p><p>在Git世界了后悔药总是可以吃的，可以恢复版本库中被删除的文件</p><pre><code>git checkout -- test.txt</code></pre><p>我们只能恢复到文件的最新版本，你会丢失<strong>最近一次提交后修改的内容</strong></p><h3 id="远程仓库">远程仓库</h3><p>我们可以在码云gitee创建新的仓库，如testproject</p><p><img src="http://blog.wenqy.com/upload/2020/11/gitee_testproject-5d0b301e2ac84faf9a3b3e7b9e062ca8.png" alt="gitee_testproject" /></p><p>对于如何在gitee或github上创建账号，生成ssh key并把公钥key放至对应的账号配置中就自行参考了，关联远程仓库并推送</p><pre><code>git remote add gitee git@gitee.com:wenqygitee/testproject.gitgit push -u gitee master</code></pre><p><code>-u</code>参数，Git不但会把本地的master分支内容推送的远程新的master分支，把本地的master分支和远程的master分支关联起来</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_remote_add-gitee-6dada0652fbc4cceb1488163bf6ee952.png" alt="git_remote_add-gitee" /></p><p>登录Gitee查看，发现推送成功啦</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_push_gitee-2c5bfe380e124f9896354d8eb93969b4.png" alt="git_push_gitee" /></p><p>查看远程库地址</p><pre><code>git remote -v</code></pre><p>取消远程库关联</p><pre><code>git remote rm origin</code></pre><p>关联多个远程库时，可以未不同的远程库关联不同的名，也可以取消关联，重新绑定。</p><p><strong>克隆远程库</strong></p><p>克隆自建Gitlab的仓库项目</p><pre><code>git clone git@xx.xxx.xxx.xxx:root/testproject.git</code></pre><h3 id="分支管理">分支管理</h3><p>创建并切换分支，<code>-b</code>参数表示创建并切换</p><pre><code>git checkout -b dev</code></pre><p>相当于两条命令</p><pre><code>git branch devgit checkout dev</code></pre><p>列出所有分支，当前分支前面会标一个<code>*</code>号</p><pre><code>git branch</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_checkout_b_dev-1e6de0605f0a49eaa6b5474fc5f427f6.png" alt="git_checkout_b_dev" /></p><p><code>git merge dev</code> 指定分支<strong>合并</strong>到当前分支</p><pre><code>git merge --no-ff -m &quot;merge with no-ff&quot; dev</code></pre><p>默认<code>Fast forward</code>模式，但这种模式下，删除分支后，会丢掉分支信息，<code>--no-ff</code>参数，表示禁用<code>Fast forward</code>模式，<code>git log</code>分支历史上就可以看出合并的分支信息</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-98482af52fb44115aca31aa8a59272d0.png" alt="image.png" /></p><p>删除分支</p><pre><code>git branch -d dev</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_branch_d_dev-05d800a52f5743cd8e9b9910d6a52063.png" alt="git_branch_d_dev" /></p><p>新版本还可以用<code>switch</code>切换分支</p><pre><code>git switch -c dev</code></pre><p>多人协作，对同一份内容同时修改，多分支合并等，难免会发生冲突：</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_merge_conflict-a49155a587db4ee29a06221cae33fa82.png" alt="git_merge_conflict" /></p><p>Git用<code>&lt;&lt;&lt;&lt;&lt;&lt;&lt;</code>，<code>=======</code>，<code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code>标记出冲突文件里不同分支的内容，修改冲突的文件内容后，再次提交，解决冲突</p><p>可以用<code>git log</code>查看分支合并情况</p><pre><code>git log --graph --pretty=oneline --abbrev-commit</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_log_graph-11ae82f86144434cbdab437a87ec8ae1.png" alt="git_log_graph" /></p><p><strong>Bug分支</strong></p><p>通常手工工作进行中时，会插入紧急bug任务需要修复，这时可以先保存开发现场，创建修复分支修复bug，然后在切回开发分支，保存dev分支修改，切换master分支后，创建issue分支，并在issue分支上进行bug修复，并合并到master分支</p><p>先在dev分支上保存现场</p><pre><code>git stash</code></pre><p>切换master分支后，创建issue分支</p><pre><code>git checkout mastergit checkout -b issue</code></pre><p>修复bug后，切回master分支，并将issue分支合并到master分支</p><pre><code>git checkout mastergit merge --no-ff -m &quot;merged bug fixed&quot; issue</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_merge_issue-1b528b4d6f1b48aabbf8a55e3a84ce1f.png" alt="git_merge_issue" /></p><p>合并完后，切换回dev分支，恢复之前进行到一半的现场，可以继续开发啦</p><pre><code>git stash listgit stash pop</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_stash_pop-d83b7061e71d44c0baed9a1bd6cb67ee.png" alt="git_stash_pop" /></p><p>dev分支是早期从master分支分出来的，这个bug在dev分支上也可能存在。我们可以快速复制一个特定的提交到当前分支来修复bug，复制指定commit id的修改</p><pre><code>git cherry-pick a82f58c</code></pre><p>操作时，由于dev分支修改的是同一份内容，发生冲突了，修改内容后重新提交</p><p><img src="http://blog.wenqy.com/upload/2020/11/git_cherry-pick-cc0a539bff5f43409da3a298a66a161c.png" alt="git_cherry-pick" /></p><p>通常开发新特性，新功能，我们可以新建feature分支上开发</p><p>有时候分支还没合并就需要删除，提示删除不了，这时需要强制删除分支，-D 强制删除</p><pre><code>git branch -D feature-one</code></pre><p><strong>推送分支</strong></p><pre><code>git push gitee dev</code></pre><p>推送失败时可能你的小伙伴的最新提交和你试图推送的提交有<strong>冲突</strong>，可以先<code>git pull</code> 抓取最新分支，解决冲突，</p><p><code>git pull</code>也失败了，原因是没有指定本地<code>dev</code>分支与远程<code>gitee/dev</code>分支的链接，<strong>关联链接</strong></p><pre><code>git branch --set-upstream-to=gitee/dev dev</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_branch_set_upstream-2719fdbb755c4a539660c544c3700ba7.png" alt="git_branch_set_upstream" /></p><h3 id="标签管理">标签管理</h3><p>发布一个版本时，我们通常先在版本库中打一个标签（tag），标签也是版本库的一个快照。</p><p><code>git tag &lt;name&gt;</code>就可以打一个新标签</p><pre><code>git tag v1.0.0</code></pre><p>查看所有标签</p><pre><code>git tag</code></pre><p>默认标签是打在最新提交的commit上的。有时候，如果忘了打标签，可以先找到历史提交的commit id，然后打上就可以</p><pre><code>git tag v0.0.9 42dee2a</code></pre><p>查看标签信息</p><pre><code>git show v1.0.0</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_tag-57cebd49dda14e5193b1cd1276d0ecc0.png" alt="git_tag" /></p><p>删除标签</p><pre><code>git tag -d v0.0.9</code></pre><p>推送标签到远程</p><pre><code>git push gitee v1.0.0</code></pre><p>推送所有标签到远程</p><pre><code>git push origin --tags</code></pre><p>删除远程标签，本地也需先删</p><pre><code>git tag -d v1.0.0git push gitee :refs/tags/v1.0.0</code></pre><p>查看远程分支</p><pre><code>git ls-remote</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_tag_d-c5f44bd9c8d741f9951303f9a279e1cd.png" alt="git_tag_d" /></p><h3 id="忽略文件">忽略文件</h3><p>忽略某些文件，不需要加入到版本控制时，需要编写<code>.gitignore</code>文件，<code>.gitignore</code>文件本身要放到版本库里，并且可以对<code>.gitignore</code>做版本管理！</p><p><img src="http://blog.wenqy.com/upload/2020/11/gitignore-4a328b97942d40e184e25a13a77fe66b.png" alt="gitignore" /></p><p>可以查看别人整理的gitignore文件：<a href="https://github.com/github/gitignore">https://github.com/github/gitignore</a></p><h3 id="配置别名">配置别名</h3><p>对于git命令，有些难记的，可以偷懒取巧，取个别名</p><pre><code>git config --global alias.st statusgit config --global alias.co checkoutgit config --global alias.ci commitgit config --global alias.br branchgit config --global alias.last 'log -1'git config --global alias.lg &quot;log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&lt;%an&gt;%Creset' --abbrev-commit&quot;</code></pre><p><img src="http://blog.wenqy.com/upload/2020/11/git_alias-156a0db5f18b4e73a6f9c9a2a0c02a0b.png" alt="git_alias" /></p><p>配置Git的时候，加上--global是针对当前用户起作用的，如果不加，那只针对当前的仓库起作用</p><p>每个仓库的Git配置文件都放在<code>.git/config</code>文件中</p><p><img src="http://blog.wenqy.com/upload/2020/11/gitconfig-343f8f53733c4fc384287f87032453ed.jpg" alt="gitconfig" /></p><p>我们学习了常用的Git命令，知道了查看状态和比较，版本的回退和撤销，关联远程库，对分支和标签进行管理，还编写了忽略版本的文件，配置命令别名等等。Git学习，需要深究可以查看官网手册。</p><h3 id="参考">参考</h3><p><a href="https://git-scm.com/docs">https://git-scm.com/docs</a> 官方文档<br /><a href="https://www.liaoxuefeng.com/wiki/896043488029600">https://www.liaoxuefeng.com/wiki/896043488029600</a> 廖雪峰Git教程<br /><a href="https://www.runoob.com/git/git-tutorial.html">https://www.runoob.com/git/git-tutorial.html</a> 菜鸟教程<br /><a href="https://time.geekbang.org/course/intro/100021601">https://time.geekbang.org/course/intro/100021601</a> 玩转Git三剑客（付费视频教程）<br /><a href="https://learngitbranching.js.org/">https://learngitbranching.js.org/</a> Git教学网<br /><a href="https://backlog.com/git-tutorial/cn/">https://backlog.com/git-tutorial/cn/</a> 猴子都能懂的GIT入门</p>]]>
                    </description>
                    <pubDate>Thu, 06 Feb 2020 23:37:16 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[伤心总是难免的，你又何必一往情深]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/伤心总是难免的你又何必一往情深</link>
                    <description>
                            <![CDATA[<p>网站是很久没维护的，这是事实，幸运的是，总有人提醒我，你曾经拥有自己网站。跟往常一样的夜晚，刷着视频，突然间，一条不速之短信消息弹了出来，抬头就是【万网】，心里顿时咯噔了下，难道阿里催着续费了？细细一看，&quot;因超标消耗资源已关停，请及时处理&quot;这消息显得格外的醒目和刺眼。访问量应该不大啊，网站是很久没更新了，没理由挂掉啊。访问下首页，心碎了一地：</p><p><img src="http://blog.wenqy.com/upload/2020/11/aliyun-no-resource-35dafe9f22bc4043ae9216c45569f8d5.png" alt="aliyun-no-resource" /></p><p>算了，停了就停了，拖延症让我度过了一星期，让【小温之家】自身自灭了一星期。终于一星期后我登录了阿里云万网控制台，重新点亮了网站，稍微看了下访问页，资源异常当天，原来是我上传的jdk8 api访问量异常高，没想到还挺有用的，呵呵。没在意，闪人。</p><p><img src="http://blog.wenqy.com/upload/2020/11/jdk8-access-2da4f50d9099441aac9b7eec59791e06.png" alt="jdk8-access" /></p><p>就这样云淡风轻的过了几天，然而该来的终究又还是来了，不速之客还是敲响了我的手机。&quot;见鬼了，怎么又不行了，都想骂娘了都&quot;。珊珊地又等到了周末，仔细看了些访问请求，原来，我是攻击了啊，还好没种码。这些都是异常的访问请求：</p><p><a href="http://blog.wenqy.com/upload/2020/11/execption-access-request-ecf152659ed941a6afa620075533b8f5.png">http://blog.wenqy.com/upload/2020/11/execption-access-request-ecf152659ed941a6afa620075533b8f5.png</a></p><p>这是另外一些：</p><p><img src="http://blog.wenqy.com/upload/2020/11/execption-access-request-2-8cb305d95ba44f64876f397eda0d3ada.png" alt="execption-access-request-2" /></p><p>再看下当时的访问请求日志：</p><p><img src="http://blog.wenqy.com/upload/2020/11/access-request-log-a8a7cff5e3cb41b5ae03c1c00fc62e60.png" alt="access-request-log" /></p><p>还有其他的：</p><p><img src="http://blog.wenqy.com/upload/2020/11/access-request-log-2-1e1ad2207ab140988fa42550e7f3e988.png" alt="access-request-log-2" /></p><p>三番五次的被搞，我已无能为力。鄙人不才也无法深究，刨根问底。姑且认为你是南京小妹妹吧，不是肉鸡，真不愿相信自己是被殃及池鱼的那个，你成功的吸引了我的注意，让我写下这篇无聊的回忆，愿你被这个世界温柔以待，我是爱你的。发布的今天是举国同庆的节日，愿你与世界不可分割！</p><p>PS：who can help me（程序猿何苦为难程序猿）?</p>]]>
                    </description>
                    <pubDate>Tue, 01 Oct 2019 23:21:07 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Elasticjob安装与入门]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/elasticjob安装与入门</link>
                    <description>
                            <![CDATA[<p>Elasticjob是一个开源的分布式调度解决方案。Elasticjob由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成。Elastic-Job-Lite定位为<strong>轻量级无中心化解决方案</strong>，使用jar包的形式提供分布式任务的协调服务；Elastic-Job-Cloud采用自研Mesos Framework的解决方案，额外提供资源治理、应用分发以及进程隔离等功能。</p><p>值得八卦的是，github上Elasticjob的Title已经隐去了当当的商标，而author的Title也已转向了京东，难道跟当当的&quot;卖身&quot;有关？而github版本已由release版2.1.5大跃进成3.0.0.M1开发版，是要断舍离，再出发的节奏吗？</p><p>现在主要介绍Elastic-Job-Lite，它支持多种任务类型，支持任务分片，可以将任务拆分成多个子任务，像原先<strong>介绍的Quartz集群方案</strong>中可能存在的任务处理的单机极限问题，可以利用Elastic-Job-Lite解决，可以通过预先分配分片项参数，任务分片后，每个分片任务只处理跟自己相关的分片数据，这样就可以将单机处理的数据，分散成多机处理，缓解压力。Elastic-Job还支持分片级别的故障切换，具有容错性，一个分片任务故障后，可以被其他机器的任务接管，继续执行分片任务。</p><h3 id="实现原理">实现原理</h3><h4 id="作业启动">作业启动</h4><p>下面摘自官网链接的实现原理解析图。Elastic-Job-Lite依赖注册中心来感知作业节点间的关系，选举主节点来分配分片项。作业服务器启动后，会<strong>持久化作业任务</strong>到注册中心，作业任务的添加是由作业服务器启动来完成。它的控制台页面也没有作业添加的功能。</p><p><img src="http://blog.wenqy.com/upload/2020/11/job_start-ecf70e78e6f447289973620cd5bde756.jpg" alt="job_start" /></p><h4 id="作业执行">作业执行</h4><p>Elastic-Job-Lite内核是quartz，它<strong>默认实现了quartz job接口的任务适配器</strong>，抓取分片后，从JobDataMap中取出不同的任务类型，进行分发处理。</p><p><img src="http://blog.wenqy.com/upload/2020/11/job_exec-f42da982eb9048f8a813c00372dad724.jpg" alt="job_exec" /></p><h3 id="下载安装">下载安装</h3><p>从github官网上<a href="https://github.com/elasticjob/elastic-job-lite下载源码，可以选择导入elastic-job-lite工程到IDE开发环境，子模块elastic-job-lite-console为控制台界面管理工程，用maven管理，">https://github.com/elasticjob/elastic-job-lite下载源码，可以选择导入elastic-job-lite工程到IDE开发环境，子模块elastic-job-lite-console为控制台界面管理工程，用maven管理，</a><code>mvn install</code>在idea是可以直接编译成功的，但eclipse却提示了gpg plugin报错，可以选择去掉<code>maven-gpg-plugin</code>插件</p><p><img src="http://blog.wenqy.com/upload/2020/11/maven-gpg-plugin-ccb5b6cdb24f452b9a375cf374d8ba93.png" alt="maven-gpg-plugin" /></p><p>也可以选择编译时跳过插件<code>mvn install -Dgpg.skip</code></p><p><span version="">解压缩mvn install生成的elastic-job-lite-console-$</span>.tar.gz并执行其中的启动脚本<code>bin\start.bat</code>。打开浏览器访问<code>http://localhost:8899/</code>即可访问控制台</p><p>默认提供了访问账号</p><table width="484"><tbody><tr><td style="font-weight: 400;" width="104">类型</td><td style="font-weight: 400;" width="380">账号</td></tr><tr><td style="font-weight: 400;" width="104">管理员</td><td style="font-weight: 400;" width="380">root/root</td></tr><tr><td style="font-weight: 400;" width="104">访客</td><td style="font-weight: 400;" width="380">guest/guest</td></tr></tbody></table>可以在elastic-job-lite-console的配置文件conf\auth.properties中修改账号<p><img src="http://blog.wenqy.com/upload/2020/11/elastic-job-lite-console-680a0e13da064a7bbeb9eb165471b0e2.png" alt="elastic-job-lite-console" /></p><h4 id="配置注册中心">配置注册中心</h4><p>配置注册中心地址，<strong>暂时只能是zookeeper</strong>。可以使用Elastic内置的zookeeper，也可以自己单独启动，这里的指向地址要与项目启动指向的注册中心地址一致。</p><p><img src="http://blog.wenqy.com/upload/2020/11/elasticjob-zookeeper-a28951f55d2c452895bece3dc85791a7.png" alt="elasticjob-zookeeper" /></p><h4 id="配置数据源">配置数据源</h4><p>事件追踪数据源配置，用于记录任务历史，只能是mysql类型数据库，向地址要与项目启动指向的事件追踪数据源地址一致</p><p><img src="http://blog.wenqy.com/upload/2020/11/elasticjob-log-database-d2d30860745b46398f920f0e45f9129b.png" alt="elasticjob-log-database" /></p><h4 id="项目启动">项目启动</h4><p>下载<a href="https://github.com/elasticjob/elastic-job-example示例工程">https://github.com/elasticjob/elastic-job-example示例工程</a></p><p>elastic-job-example-lite-springboot工程，<code>application.yml</code>修改注册中心地址，事件追踪数据源地址，配置作业任务的分片项参数和分片总数以及Cron表达式等等，<code>com.dangdang.ddframe.job.example.SpringBootMain</code> 项目启动：</p><p><img src="http://blog.wenqy.com/upload/2020/11/elastic-job-example-springboot-a821dfb3957547f0ba99060545c87f77.png" alt="elastic-job-example-springboot" /></p><p>在控制台上可以查看已经注册的作业任务和作业服务器</p><p><img src="http://blog.wenqy.com/upload/2020/11/elasticjob-springboot-start-fc3eca9de9324ebb81c54ae30407d0e1.png" alt="elasticjob-springboot-start" /></p><h4 id="作业详情">作业详情</h4><p>查看作业详情，可以查看分片项执行的情况</p><p><img src="http://blog.wenqy.com/upload/2020/11/elasticjob-detail-45d0a055638b495fb330aa8c2a1f5ed3.png" alt="elasticjob-detail" /></p><h4 id="任务失效">任务失效</h4><p>可以将任务失效为暂停状态，已失效任务可以重新生效。</p><p><img src="http://blog.wenqy.com/upload/2020/11/elsticjob-disable-cbebb001a42d4e299dd787c3b846624d.png" alt="elsticjob-disable" /></p><h4 id="作业修改">作业修改</h4><p>作业修改，可以修改作业的cron表达式，作业分片数等等。</p><p><img src="http://blog.wenqy.com/upload/2020/11/elasticjob-update-d03479de249a4811879ff9fe12c88090.png" alt="elasticjob-update" /></p><p>对于已失效或生效任务，<strong>可以选择触发任务，进行手动触发任务一次，而无需等待触发时间的到来，但对于终止任务，一旦终止，任务就完全作废了</strong>。</p><h4 id="作业日志">作业日志</h4><p>可以查看任务触发的日志，查看任务执行结果。</p><p><img src="http://blog.wenqy.com/upload/2020/11/elasticjob-log-history-d1b4c84bb5bb4fea82b5064796712618.jpg" alt="elasticjob-log-history" /></p><p>可以查看任务的历史状态，是等待运行，还是运行中，还是已完成等待。</p><p><img src="http://blog.wenqy.com/upload/2020/11/elaticjob-log-status-ae3d95d0fd914c12a7389e0bbc66f6f6.png" alt="elaticjob-log-status" /></p><p>总之，Elasticjob解决了quartz的单机极限痛点，是对quartz的一种扩展和优化。</p><h3 id="参考">参考</h3><p>官网：<a href="http://elasticjob.io/docs/elastic-job-lite/00-overview">http://elasticjob.io/docs/elastic-job-lite/00-overview</a></p><p>Github地址：<a href="https://github.com/elasticjob">https://github.com/elasticjob</a></p><p> </p>]]>
                    </description>
                    <pubDate>Sat, 30 Jun 2018 23:17:32 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Quartz管中窥豹之集群高可用]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/quartz管中窥豹之集群高可用</link>
                    <description>
                            <![CDATA[<p>Quartz在集群模式下通过故障切换和任务负载均衡来实现任务的高可用（HA <code>High Available</code>）。而集群模式是通过争用数据库悲观锁来实现必须使用JdbcStore持久化存储任务。这个可以先阅读之前集群管理文章<a href="http://wenqy.com/2018/04/03/quartz%e7%ae%a1%e4%b8%ad%e7%aa%a5%e8%b1%b9%e4%b9%8b%e9%9b%86%e7%be%a4%e7%ae%a1%e7%90%86.html">Quartz管中窥豹之集群管理</a></p><h3 id="故障切换">故障切换</h3><p>当其中一个节点在执行一个或多个作业期间失败时发生故障切换（<code>Fail Over</code>）。当节点出现故障时，其他节点会检测到该状况并识别数据库中在故障节点内正在进行的作业。任何标记为恢复的作业（在JobDetail上都具有&quot;请求恢复(<code>requests recovery</code>)&quot;属性）将被剩余的节点重新执行，已达到失效任务 转移。没有标记为恢复的作业将在下一次相关的Triggers触发时简单地被释放以执行。</p><p>1、每个节点Scheduler实例由集群管理线程ClusterManager周期性（配置文件中检测周期属性<code>clusterCheckinInterval</code>默认值是 15000 (即15 秒)）定时检测CHECKIN数据库，遍历集群各兄弟节点的实例状态，检测集群各个兄弟节点的健康情况。</p><p>2、当集群中一个节点的Scheduler实例执行CHECKIN时，它会查看是否有其他节点的Scheduler实例在到达它们所预期的时间还未CHECKIN。若检测到有节点在预期时间未CHECKIN，则认为该节点故障。判断节点是否故障与节点Scheduler实例最后CHECKIN的时间有关，而判断条件：</p><pre>LAST_CHECKIN_TIME + Max(检测周期，检测节点现在距上次最后CHECKIN的时间) + 7500ms < currentTime。</pre><p>3、集群管理线程检测到故障节点，就会更新触发器状态，状态更新如下。</p><table><tbody><tr><td width="426">故障节点触发器更新前状态</td><td width="426">更新后状态</td></tr><tr><td width="426">BLOCKED</td><td width="426">WAITING</td></tr><tr><td width="426">PAUSED_BLOCKED</td><td width="426">PAUSED</td></tr><tr><td width="426">ACQUIRED</td><td width="426">WAITING</td></tr><tr><td width="426">COMPLETE</td><td width="426">无，删除Trigger</td></tr></tbody></table>`org.quartz.impl.jdbcjobstore.Constants`常量类定义了触发器的几种状态<p>4、集群管理线程删除故障节点的实例状态（<code>qrtz_scheduler_state</code>表），即重置了所有故障节点触发任务一般。原先故障任务和正常任务一样就交由调度处理线程处理了。</p><p><strong>代码参见</strong></p><p>下图是集群管理线程CHECKIN时第一次CHECKIN或者发现故障节点后需获取实例状态访问行锁，才能更新触发器状态，删除故障节点实例状态等等。</p><p><img src="http://blog.wenqy.com/upload/2020/11/firstCheckIn_LOCK_STATE_ACCESS-8a1b8a66d87e478d9d095bb3b599674e.png" alt="firstCheckIn_LOCK_STATE_ACCESS" /></p><p>下面两张图是查找故障节点时，会查找所有集群节点的实例状态，然后遍历判断是否故障。</p><p><img src="http://blog.wenqy.com/upload/2020/11/selectSchedulerStateRecords-ec1765bac122480c9477d8999263d131.png" alt="selectSchedulerStateRecords" /></p><p>查找集群状态语句</p><p><img src="http://blog.wenqy.com/upload/2020/11/select_from_quartz_scheduler_state-c27916db186b49e2a86038c0db34deed.png" alt="select_from_quartz_scheduler_state" /></p><p>下图是根据节点实例最后CHECKIN时间判断是否节点故障的方法。</p><p><img src="http://blog.wenqy.com/upload/2020/11/calcFailedOver-fcac23daabb449fdac61dcb5a33bf862.png" alt="calcFailedOver" /></p><p>下图可知，集群管理线程是单独的线程实例。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-1eeb77e2cdb747739eefa8eae5d8e406.png" alt="image.png" /></p><p><img src="http://blog.wenqy.com/upload/2020/11/defaultTreadExecutor-92c5a6cff4da4afb9b69941c3325fc10.png" alt="defaultTreadExecutor" /></p><p>下图可知，集群管理线程检测完后要更新最后检测时间</p><p><img src="http://blog.wenqy.com/upload/2020/11/updatescheduerstate-fb7f4865dadd48c6aee98bace72748e4.png" alt="updatescheduerstate" /></p><h3 id="负载均衡">负载均衡</h3><p>负载平衡自动发生，群集的每个节点都尽可能快地触发Jobs。当Triggers的触发时间发生时，获取它的第一个节点（通过在其上放置一个锁定）是将触发它的节点。</p><p>它不一定是每次相同的节点 - 哪个节点运行它或多或少是随机的。负载平衡机制对于繁忙的调度器（大量的Triggers）是近似随机的，<strong>但是对于非忙（例如，很少的Triggers）调度器而言，有利于同一个节点下执行（Why?）</strong>。</p><p>集群下任务的调度存在一定的随机性，谁先拥有触发器行锁TRIGGER_ACCESS，谁就先可能触发任务。当某一个机子的调度线程拿到该锁（别的机子只能等待）时，</p><p>1、 <code>acquireNextTriggers</code>获取待触发队列，查询Trigger表的判断条件：</p><pre><code>NEXT_FIRE_TIME &lt; now + idleWaitTime + timeWindow and TRIGGER_STATE = 'WAITING'</code></pre><p>然后更新触发器状态为ACQUIRE</p><p>2、触发待触发队列，修改 Trigger 表中的 NEXT_FIRE_TIME 字段，也就是下次触发时间，计算下次触发时间的方法与具体的触发器实现有关，如Cron表达式触发器，计算触发时间与Cron表达式有关。参见：</p><p><code>org.quartz.impl.triggers.CronTriggerImpl.triggered(Calendar)</code></p><p>触发待触发队列后及时释放触发器行锁。</p><p>3、这样，别的机子拿到该锁，也查询 Trigger 表，但是由于任务触发器的下次触发时间或者状态已经修改，所以不会被查找出来。这时拿到的任务就可能是别的触发任务。这样就实现了多个节点的应用在某一时刻对任务只进行一次调度。对于重复任务每次都不一定是相同的节点,它或多或少会随机节点运行它。</p><p><strong>代码参见</strong></p><p>如下面，如果是集群，则会开启基于数据库行锁的集群任务调度机制。</p><p><code>org.quartz.impl.jdbcjobstore.JobStoreSupport.initialize(ClassLoadHelper, SchedulerSignaler)</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/cluster_initialize_dblocks-a6a8564f902c46e080651c9527d4ce85.png" alt="cluster_initialize_dblocks" /></p><p>获取待触发队列的方法</p><p><img src="http://blog.wenqy.com/upload/2020/11/selectTriggerToAcquire-736d26d868cf4e2dbdf8f2a4d23c1825.png" alt="selectTriggerToAcquire" /></p><p>集群功能最适合扩展长时间运行或cpu密集型作业（通过多个节点分配工作负载）。如果需要扩展以支持数千个短期运行（例如1秒）作业，则可以考虑通过使用多个不同的调度程序（包括HA的多个群集调度程序）对作业集进行分区。调度程序使用集群范围的锁，这种模式会在添加更多节点（超过三个节点 - 取决于数据库的功能等）时降低性能。</p><p><strong>注意：</strong></p><pre><code>Never run clustering on separate machines, unless their clocks are synchronized using some form of time-sync service (daemon) that runs very regularly (the clocks must be within a second of each other). See http://www.boulder.nist.gov/timefreq/service/its.htm if you are unfamiliar with how to do this.</code></pre><pre><code>Never start (scheduler.start()) a non-clustered instance against the same set of database tables that any other instance is running (start()ed) against. You may get serious data corruption, and will definitely experience erratic behavior.</code></pre><h3 id="参考">参考</h3><p>Quartz教程 <a href="https://www.w3cschool.cn/quartz_doc/quartz_doc-2put2clm.html">https://www.w3cschool.cn/quartz_doc/quartz_doc-2put2clm.html</a></p><p><a href="http://www.quartz-scheduler.org/documentation/quartz-2.2.x/configuration/ConfigJDBCJobStoreClustering.html">http://www.quartz-scheduler.org/documentation/quartz-2.2.x/configuration/ConfigJDBCJobStoreClustering.html</a></p><p>调度系统入门和调度高可用实现方案 <a href="https://www.jianshu.com/p/810400e6a274Quartz">https://www.jianshu.com/p/810400e6a274Quartz</a></p>]]>
                    </description>
                    <pubDate>Sat, 05 May 2018 23:40:35 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Quartz管中窥豹之其他特性初识]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/quartz管中窥豹之其他特性初识</link>
                    <description>
                            <![CDATA[<p>由先前的文章，我们已经验证了Quartz到集群模式、触发器优先级、任务错过触发处理和任务有状态与并发等场景特性。</p><h3 id="删除正运行任务">删除正运行任务</h3><p><strong>测试结果</strong></p><p>任务删除将删除还未触发的任务及触发器信息。但不会强行中断已经触发运行的任务。</p><p><strong>测试步骤</strong></p><p>1、定义一个任务为休眠10秒。</p><p><img src="http://blog.wenqy.com/upload/2020/11/waitingremovingjob_10s-69cb66d8836045f99413c8b8b2081213.png" alt="waitingremovingjob_10s" /></p><p>2、测试用例中，任务每2秒触发。等待6秒后删除任务。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testRemoveRuningJob-3a85f9d6f386479587f97a74eb98f2bf.png" alt="testRemoveRuningJob" /></p><p>查看结果，加入任务后，数据库保存了任务信息。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_jobdetail_test_result-a986dc27b868490886a2f7d27f04507f.png" alt="quartz_jobdetail_test_result" /></p><p>由于任务每2秒触发，而6秒后删除任务，共触发了4次，即6秒后删除任务，没有新的触发任务。删除任务也不会强行中断正在触发的任务。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testRemoveRuningJob_test_result-7f211f653149418dbaca82e0c9a996f0.png" alt="testRemoveRuningJob_test_result" /></p><p>最后，数据库任务信息也删除了。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_jobdetail_test_result_delete-9b951357441842a48760323ab39427cd.png" alt="quartz_jobdetail_test_result_delete" /></p><p><strong>代码参见</strong></p><p>调度器根据JobKey删除任务及其相关触发器，并通知调度线程</p><p><code>org.quartz.core.QuartzScheduler.deleteJob(JobKey)</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_deletejob-bebef1d4a85f40aa976df7dafa2763a6.png" alt="quartz_deletejob" /></p><h3 id="集群下调度实例id生成">集群下调度实例ID生成</h3><p><strong>测试结果</strong></p><p>集群模式下，调度实例ID可配置自动生成，生成方式为：当前主机名 + 当前时间毫秒数</p><p><strong>测试步骤</strong></p><p>1、配置文件将调度实例ID设为AUTO，并加入集群</p><pre><code>org.quartz.scheduler.instanceId: AUTOorg.quartz.jobStore.isClustered=true</code></pre><p>2、跟踪调度工厂<code>org.quartz.impl.StdSchedulerFactory</code>加载配置启动过程</p><p><img src="http://blog.wenqy.com/upload/2020/11/stdschedulerfactory_init_scheId-7d2e486dd1374a27836386603c80c0ca.png" alt="stdschedulerfactory_init_scheId" /></p><p>3、实例ID生成器<code>org.quartz.spi.InstanceIdGenerator.generateInstanceId()</code>生成ID</p><p><img src="http://blog.wenqy.com/upload/2020/11/InstanceIdGenerator.generateInstanceId-6f142cda00c44282aa3cb739200cd924.png" alt="InstanceIdGenerator.generateInstanceId" /></p><p>可以看出默认生成器ID生成规则：<code>HOSTNAME + CURRENT_TIME</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/SIMPLEGENERATOR_HOSTNAME_CURRENT_TIME-292ef44551284583a3cb63c4a1b3e713.png" alt="SIMPLEGENERATOR_HOSTNAME_CURRENT_TIME" /></p><p>打印日志可以看出实例ID</p><p><img src="http://blog.wenqy.com/upload/2020/11/print_scheduler_id-074753549fc14ed48f60af224d8f69e4.png" alt="print_scheduler_id" /></p><p>配置文件可以设置<code>org.quartz.scheduler.instanceIdGenerator.class</code>属性，指向自定义的ID生成器，自行实现ID生成算法。</p><h3 id="优雅关机">优雅关机</h3><p><strong>测试结果</strong></p><p>手动关闭Quartz调度，即调用Shutdown方法时，会停止正在运行的触发器和清除所有调度相关的资源。关闭调度线程、关闭错过触发线程、关闭集群管理线程、关闭连接池、关闭线程池，设为True时，关闭时会等待到所有任务线程执行结束。</p><p><strong>测试步骤</strong></p><p>定义一个任务休眠20s，任务触发后等待15s，关闭调度。发现调度关闭会等待正在执行的任务直到任务线程结束。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_shutdown_smartly-5bce71c97f714b22847b60af2f71ffab.png" alt="quartz_shutdown_smartly" /></p><p>发现触发器还是持久化到数据库。</p><p><img src="http://blog.wenqy.com/upload/2020/11/image-1bbce0d23eae4b88aadf4380e4cb8d38.png" alt="image.png" /></p><p><strong>代码参见</strong></p><p>调度器关闭方法，关闭线程、关闭触发器、关闭连接池、关闭线程池等等</p><p><code>org.quartz.core.QuartzScheduler.shutdown(boolean)</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/QuartzScheduler.shutdown-938fbddfa4224db583438eec4fcb9b83.png" alt="QuartzScheduler.shutdown" /></p><h3 id="设计模式">设计模式</h3><p>quartz内部也应用了一些设计模式，贴下代码吧</p><h4 id="工厂模式">工厂模式</h4><p>quartz采用了工厂方法。调度工厂SchedulerFactory的实现类有两个标准调度工厂StdSchedulerFactory和直接调度工厂DirectSchedulerFactory，StdSchedulerFactory可通过quartz配置文件初始化调度器Scheduler，而DirectSchedulerFactory可通过创建调度器方法来获取不同类型调度器。</p><p><img src="http://blog.wenqy.com/upload/2020/11/interface_SchedulerFactory-80464ec2f4c046108b2afcda5a84f39f.png" alt="interface_SchedulerFactory" /></p><p>任务运行壳工厂JobRunShellFactory，主要有两种实现类，创建有JTA事务和没有事务的JobRunShell。JTAJobRunShellFactory会负责创建含有JTA事务的JTAJobRunShell线程，用来反射执行真正的Job任务，并包裹了一层JTA事务；而StdJobRunShellFactory负责创建简单的JobRunShell，不处理事务相关的东西</p><p><img src="http://blog.wenqy.com/upload/2020/11/interface_JobRunShellFactory-d40c057eae1c4400bbcb84e27a6e2940.png" alt="interface_JobRunShellFactory" /></p><h4 id="建造者模式">建造者模式</h4><p>Quartz提供了不同触发器类型的触发器建造者，建造者支持属性设值的链式写法</p><p><img src="http://blog.wenqy.com/upload/2020/11/scheduleBuilder-f95a1d83a5b642618c921fad6e9e1bc7.png" alt="scheduleBuilder" /></p><p>触发器建造者，可以创建不同触发器类型的触发器</p><p><img src="http://blog.wenqy.com/upload/2020/11/triggerBuilder-7160ef81c8324c4bb028f2dee7128551.png" alt="triggerBuilder" /></p><p><code>org.quartz.JobBuilder</code> 任务建造者，负责创建JobDetail实例</p><p><code>org.quartz.DateBuilder</code> 日期建造者，负责创建与任务相关的具体日期</p><h4 id="观察者模式">观察者模式</h4><p><code>org.quartz.JobListener</code> 任务监听器，任务执行会通知改监听器</p><p><code>org.quartz.TriggerListener</code> 触发器执行会通知触发器监听器</p><p><code>org.quartz.SchedulerListener</code> 调度器监听器，调度器启动和关闭等动态变化、触发器动态变化、任务动态变化会通知调度器监听器</p><p><code>org.quartz.ListenerManager</code> 监听器管理类，对任务监听器、触发器监听器和调度器监听器管理</p><p>QuartzScheduler任务调度后，会负责通知监听器</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_notify-1d7efcb804604227ab7dde1f85008a0c.png" alt="quartz_notify" /></p><h4 id="单例模式">单例模式</h4><p>DirectSchedulerFactory.getInstance()</p><p>验证了删除正在运行任务，集群模式下调度实例ID生成和优雅关机等场景，和贴了quartz应用到的一些设计模式。quartz作为一款比较成熟的定时调度开源框架，受到广泛的应用和扩展不是没有道理的。</p>]]>
                    </description>
                    <pubDate>Tue, 01 May 2018 23:13:49 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Quartz管中窥豹之任务有状态与并发]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/quartz管中窥豹之任务有状态与并发</link>
                    <description>
                            <![CDATA[<p><strong>Quartz定时任务默认都是并发执行</strong>,不仅仅是不同任务Job，还可以是同个Job的不同实例（JobDetail），意味着一次任务并不会等待上一次任务执行完毕，只要触发时间到达就会执行, 如果定时任执行太长，会长时间占用线程资源，导致其它任务堵塞。</p><p><strong>Quartz定时任务默认也是无状态的</strong>，也就是每个Job实例都是独立的，每个Job实例的JobDataMap都是独有的，数据的改变互不影响。</p><p>我们可以在自定义任务Job类加上<strong>类注解@DisallowConcurrentExecution</strong>来表示同个Job的不同实例（JobDetail）不允许并发（不同job还是可以并发），这样子，一次任务执行会等待上次任务执行完毕，才会继续执行，否则会阻塞。</p><p>我们还可以在自定义任务Job类加上<strong>类注解@PersistJobDataAfterExecution</strong>让同个Job的不同实例（JobDetail）是有状态的，他们的数据是共享的，已存在的数据可能会被新的数据覆盖掉。值得注意的是，让任务变成有状态，最好是同时不允许并发，在并发情况下，数据读写是不确定的，可能造成不一致，是不可靠的。加上@PersistJobDataAfterExecution的同时，最好加上@PersistJobDataAfterExecution。</p><h3 id="单server">单Server</h3><p><strong>测试结果</strong></p><p>这里的有状态和并发是指同一个Job的不同JobDetail实例。在默认情况下多个JobDetail实例之间是相互独立，互不影响的，即便是同个JobDetail实例的不同时间触发也是互不干扰。是支持并发的。而JobDetail中JobDataMap的数据初始化后也不会更新，是无状态的。</p><p>在任务Job类中加入注解@DisallowConcurrentExecution，则表示同一个JobDetail实例前后触发是相互影响的，下次触发要等待上次触发执行完毕，不同实例间则不需等待。</p><p>在任务Job类中加入注解@PersistJobDataAfterExecution，则表示同一个JobDetail实例前后触发JobDataMap是共享的，数据会发生更新。上次触发更新到JobDataMap的数据可以在下次触发时取到，为了防止并发竞争造成的不确定性，往往和@DisallowConcurrentExecution并发控制注解一起使用。</p><p><strong>测试步骤</strong></p><p>1、配置文件设置线程池大小为5，在线程充足的情形测试</p><pre><code>org.quartz.threadPool.threadCount: 5</code></pre><p>2、定义任务类ColorJob，把执行次数累加到JobDataMap里，休眠12s。</p><p><img src="http://blog.wenqy.com/upload/2020/11/colorjob_with_jobdatamap-46457ab1227d41f395685800845ef7b4.png" alt="colorjob_with_jobdatamap" /></p><p>3、定义两个任务实例，每个任务实例都是10s后开始运行，每10s重复执行，重复4次，总运行5次</p><p><img src="http://blog.wenqy.com/upload/2020/11/2jobdetail_withrepeating10s-6f08abe3a0294dd7ad92e70704e7e9af.png" alt="2jobdetail_withrepeating10s" /></p><p>默认job是无状态，JobDataMap的数据不会发生更新，如测试场景结果，任务执行次数一直为1。支持并发，也在线程充足的情况下，同个任务实例触发也是相互独立，互不影响。如测试场景结果，job1每10s执行，但任务执行需要12s，下次触发时间到了就立即执行，不会等待。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withconcurrent-2d6d8bbbd00c4fd79316d21f90a1563f.png" alt="testjob_withconcurrent" /></p><p>4、在任务类ColorJob加入注解@DisallowConcurrentExecution，不会并发执行同一个job定义（ColorJob）的多个实例（JobDetail）。</p><p><img src="http://blog.wenqy.com/upload/2020/11/disallow_concurrent_job-a87736d9b3eb4503a67d94d2d1c3a926.png" alt="disallow_concurrent_job" /></p><p>重复执行第3步的测试用例，发现同一个ColorJob的两个实例是可以并发的，但同一时刻只允许一个相同实例执行。如测试场景结果，job1每10s执行，但任务执行需要休眠12s，下次触发时间到了要等待上次触发执行结束。这里比较诡异，没有休眠完12s就结束了。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withdisallowconcurrent-6187c62d77e74312b1d424dac262c804.png" alt="testjob_withdisallowconcurrent" /></p><p>5、在任务类ColorJob的注解换成@PersistJobDataAfterExecution，成功执行了job类的execute方法后（没有发生任何异常），更新JobDetail中JobDataMap的数据下次触发时，JobDataMap中是更新后的数据，Job的状态就体现在持续的JobDataMap数据。</p><p><img src="http://blog.wenqy.com/upload/2020/11/colorjob_with_persist-15c1e7a1a002481b80b4ddf1213a935c.png" alt="colorjob_with_persist" /></p><p>重复执行第3步的测试用例，发现打印的执行次数发生变化，即JobDetail实例的JobDataMap数据更新了。但在并发情形下，JobDataMap数据发生了不确定。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withpersist-e5acb81e986d4b51b530df60a5a71056.png" alt="testjob_withpersist" /></p><p>6、在任务类ColorJob加入注解@PersistJobDataAfterExecution和</p><p>@DisallowConcurrentExecution，<strong>如果需要JobDataMap状态更新，就应该变成同步，防止并发导致的竞争，造成数据脏乱</strong>。重复执行第3步的测试用例，发现打印的执行次数发生变化，禁止并发后，与测试场景，每10s重复执行，执行5次，发生吻合。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withdisallowconcurrent_persist-dab9fa58aec4459ab5b4c1849575cf6c.png" alt="testjob_withdisallowconcurrent_persist" /></p><h3 id="集群">集群</h3><p>测试结果</p><p>集群下任务实例间的并发控制与【单server】一致，但故障情况下，故障期间未完成任务丢失，状态更新可能会发生不确定，可设置为可恢复的任务，集群可以重跑某一节点故障期间丢失的任务。</p><p>测试步骤</p><p>1、与【单server】步骤6的测试用例，启用两个环境，构造集群测试。相继启动两个环境，发现会自动负载均衡任务实例。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withdisallowconcurrent_persist_cluster-c3495e168f9f4081aeccf81f7f40e79f.png" alt="testjob_withdisallowconcurrent_persist_cluster" /></p><p>2、手动关闭其中一个应用（job2此时执行第四次还未结束），集群节点会检测到其他节点的健康状况，<strong>如果某节点丢失了，则接管其节点的Job任务，等待下次触发，故障时间的任务却丢失了</strong>。而检测频率由配置<code>org.quartz.jobStore.clusterCheckinInterval</code>参数决定，默认15000ms。但JobDataMap的数据更新状态由于故障发生了不确定性（因为要任务执行结束才更新状态）。如下图，job2的最终状态保持在4，与实际的执行次数5不一致。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withdisallowconcurrent_persist_cluster_shutdown-d7900f448bff43a8a5c240a83d47d885.png" alt="testjob_withdisallowconcurrent_persist_cluster_shutdown" /></p><p>3、将job2设为可恢复的任务</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withdisallowconcurrent_persist_cluster_shutdown_reqrecover-5c21d5756dd24fd6ab6da5927cb540ae.png" alt="testjob_withdisallowconcurrent_persist_cluster_shutdown_reqrecover" /></p><p>重跑集群测试，关闭其中一个应用（job2此时执行第四次还未结束），但另外一个调度会接管恢复异常故障时间丢失的任务，重新执行。</p><p><img src="http://blog.wenqy.com/upload/2020/11/testjob_withdisallowconcurrent_persist_cluster_shutdown_reqrecover_result-ccdf7f46ef6543d1a9073ccf1ad21087.png" alt="testjob_withdisallowconcurrent_persist_cluster_shutdown_reqrecover_result" /></p><h3 id="跨集群">跨集群</h3><p>测试结果</p><p>不同集群的任务实例相互之间没有任何关联。</p><p>测试步骤</p><p>1、运行两个测试用例，启用不同的配置。用例采用【单server】步骤6的测试用例</p><p><img src="http://blog.wenqy.com/upload/2020/11/cover_cluster_with_stdschedulerfactory-fad7c317b3234f13af726a3e955f1e73.png" alt="cover_cluster_with_stdschedulerfactory" /></p><p>其中，设置不同调度实例</p><p><img src="http://blog.wenqy.com/upload/2020/11/cover_cluster_with_instancename-92acfe174d134b5086c6b53d5e3e89a4.png" alt="cover_cluster_with_instancename" /></p><p>用两个环境分别测试，发现两个实例互不影响。与【单server】步骤6测试结果一致。</p><p><img src="http://blog.wenqy.com/upload/2020/11/cover_cluster_with_instancename_result-15e46a164b9a444d894e5d572eb61413.png" alt="cover_cluster_with_instancename_result" /></p><h3 id="代码参见">代码参见</h3><p>job_detail任务表结构定义可以看出有状态位标示是否可并发，是否有状态，是否可恢复等等，也就意味着任务的并发控制和是否有状态也会持久化，任务动态操纵也伴随着这些标志位的改变。</p><p><img src="http://blog.wenqy.com/upload/2020/11/job_detail_ddl-274f7a0220e848c093891d2c48998500.png" alt="job_detail_ddl" /></p><p>对于同个Job不允许同时并发的不同JobDetail实例，它会过滤掉其他JobDetail的触发器</p><p><code>org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTrigger(Connection, long, int, long)</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/acquirenexttrigger_with_disallowconcurrent-9f02fa0e819645568b73127de721343e.png" alt="acquirenexttrigger_with_disallowconcurrent" /></p><p>任务触发完成后，根据会根据是否运行并发和是否有状态，进行触发器状态和Job数据更新</p><p><code>org.quartz.impl.jdbcjobstore.JobStoreSupport.triggeredJobComplete(Connection, OperableTrigger, JobDetail, CompletedExecutionInstruction)</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/triggerjobcomplete_withdisallow-125ebd1e3d8d4308a2b368f70a6ae19f.png" alt="triggerjobcomplete_withdisallow" /></p><p><code>org.quartz.impl.jdbcjobstore.JobStoreSupport.triggerFired(Connection, OperableTrigger)</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/triggerfired_withdisallowconcurrent-66994bb0f50842d2a045d8e6fe4873bf.png" alt="triggerfired_withdisallowconcurrent" /></p><h3 id="总结">总结</h3><p>quartz定时任务默认是并发和无状态的，可通过使用类注解@DisallowConcurrentExecution和@PersistJobDataAfterExecution让同个Job的不同JobDetail实例可以进行控制并发执行和数据共享有状态，这种结果，在集群情况下也同样试用，只是默认故障期间未完成任务状态会丢失，可以设置为可恢复（requestRecovery），任务应用重启，可故障恢复。</p>]]>
                    </description>
                    <pubDate>Mon, 23 Apr 2018 22:27:18 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Quartz管中窥豹之触发器优先级]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/quartz管中窥豹之触发器优先级</link>
                    <description>
                            <![CDATA[<p>根据先前的概念简介可知，一个Job可以有多个相同或不同的Trigger触发，甚至是同时触发。不同的Job也可能同时触发。这时就出现了触发时间相同的触发器触发先后顺序问题，这就是触发器优先级要考虑的问题。这里直接上结果</p><h3 id="测试结果">测试结果：</h3><p>默认情况是触发时间先后顺序排列，触发时间比较前的先执行任务，但如果一个或多个任务同时在相同时间触发下，触发器设置优先级越高越先执行。如果优先级相同，则跟任务的存储方式有关，<code>RAMJobStore</code>时与<code>TriggerKey</code>排序有关，即按触发器名的字母序；如果是<code>JdbcStore</code>则跟数据库查询的默认排序有关了。Trigger优先级默认为5，数值越大优先级越高。</p><h3 id="测试步骤">测试步骤：</h3><p>1、定义了3个任务，都是在17:12:00秒触发，优先级大小分别为10、100、1</p><p><img src="http://blog.wenqy.com/upload/2020/11/define_tree_job-7f1d3af036cf48be82c178a72094a0ae.png" alt="define_tree_job" /></p><p>测试结果为：SimpleJob数值最大，优先级最大，最先执行。</p><p><img src="http://blog.wenqy.com/upload/2020/11/simple_job_first-a0b41ee0590642f58ded395eddb7696c.png" alt="simple_job_first" /></p><p>调换下优先级大小，FirstJob为1，SimpleJob为5，HelloJob为10。定点触发，结果是优先级最大的HelloJob最先执行。</p><p><img src="http://blog.wenqy.com/upload/2020/11/hello_job_first-b08535ae9d304040828f0d606726189e.png" alt="hello_job_first" /></p><p>2、优先级相同时，都设为5时，多测试几遍，FirstJob总是先执行。</p><p><img src="http://blog.wenqy.com/upload/2020/11/the_same_time_trigger-fbc53bf0a2444569bf70650f2953c3bb.png" alt="the_same_time_trigger" /></p><h3 id="代码参见"><strong>代码参见</strong></h3><p>查看Quartz调度线程可知，与触发器的存储方式有关。QuartzSchedulerThread定义了获取下一个触发器队列的方法。</p><p><code>org.quartz.core.QuartzSchedulerThread#org.quartz.spi.JobStore.acquireNextTriggers(long, int, long)</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/JobStore_acquireNextTriggers_trigger_priority-6f3d663f23ae45e191fff46ba4f78fe4.png" alt="JobStore_acquireNextTriggers_trigger_priority" /></p><p>如果是RAMJobStore，用TreeSet存储待触发的线程。Trigger类里定义了触发器时间比较器</p><p><img src="http://blog.wenqy.com/upload/2020/11/trigger_time_comparator-045deeed81b44e91ad2fb2d6aaff68d9.png" alt="trigger_time_comparator" /></p><p>可以看出，优先级按触发时间、优先级和TriggerKey（String的字母序）排序的。</p><p>而JdbcStore可以看下Jdbc委托类，查看触发器查询语句</p><p><code>org.quartz.impl.jdbcjobstore.StdJDBCDelegate</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/select_trigger_to_acquire-b4e4072c528b4d2ab7272d5396a014c8.png" alt="select_trigger_to_acquire" /></p><p>挑出触发器查询语句：</p><pre><code>select * from qrtz_triggers ORDER BY NEXT_FIRE_TIME ASC, PRIORITY DESC</code></pre><p>发现是按触发时间升序，优先级降序排列，如果都相同，采用数据库自己默认的排序了，跟具体的DB有关了。MySQL中SELECT 默认排序是按照物理存储顺序显示。</p><p>**Trigger可以定义一个优先级，默认为5。**当多个trigger同时触发job时，线程池可能不够用，此时根据优先级来决定谁先触发。数值越大优先级越高。这个可以考虑为配置项，为优先执行的Trigger设置较大的优先级数值。</p><p>所以所有获取待触发触发器列表，跟触发时间有关，其次是触发器优先级大小，最后是具体任务的存储方式有关。触发时间较前先触发，触发时间相同，触发器优先级大的先触发，如果触发器优先级相同，则看任务存储方式，若为内存存储，则与TriggerKey排序有关，即按触发器名的字母序，若为DB存储，则与DB默认排序规则有关。</p>]]>
                    </description>
                    <pubDate>Thu, 19 Apr 2018 23:30:53 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Quartz管中窥豹之错过触发处理策略]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/quartz管中窥豹之错过触发处理策略</link>
                    <description>
                            <![CDATA[<p>在应用运行中，任务错过触发的事情也是时有发生，比如，系统宕机重启，在关闭至重启的时间，任务可能Misfired；线程池线程满了，没有空闲线程执行任务，导致无法触发，等待超时造成Misfired；不允许并发任务（<code>@DisallowConcurrentExecution</code>）在下次触发时间到达时，上次执行还未结束，只能等待，可能造成Misfired；触发器Trigger被暂停suspend时间段里，有些任务可能被Misfired等等。当然Quartz提供了一定的容忍度。配置文件可配置容忍错过触发阈值：<code>org.quartz.jobStore.misfireThreshold=60000</code>，默认为1分钟。意味着本因触发的任务未能及时执行可以延迟1分钟执行。超过1分钟后，即认为任务&quot;Misfired&quot;。这时就需要有错过触发的调度策略来决定如何这些&quot;Misfired&quot;任务。</p><h3 id="错过触发处理策略">错过触发处理策略</h3><p>不同的触发器类型有自己的&quot;Misfired&quot;错过触发处理策略。可以查看各个触发器实现策略。</p><p><code>org.quartz.impl.triggers.AbstractTrigger.updateAfterMisfire(Calendar)</code></p><p>每个类型触发器要实现上述抽象类方法，进行&quot;Misfired&quot;任务处理。错过触发处理策略由MisfireHandler错过触发处理线程处理错过触发任务。总结如下：</p><table><thead><tr><th align="left">序号</th><th align="left">策略</th><th align="left">值</th><th align="left">描述</th><th align="left">适用触发类型</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">MISFIRE_INSTRUCTION _IGNORE_MISFIRE_POLICY</td><td align="left">-1</td><td align="left">这种策略会在资源适合的时候尽快的重新触发所有“Misfired”错过触发任务。不会影响现有的调度时间。例如，SimpleTrigger每15秒执行一次，而中间有5分钟时间它都“Misfired”了，一共错失了20个，5分钟后，假设资源充足了，并且任务允许并发，它会被一次性触发。</td><td align="left">所有触发器</td></tr><tr><td align="left">2</td><td align="left">MISFIRE_INSTRUCTION _SMART_POLICY</td><td align="left">0</td><td align="left">触发器默认策略。如果当前触发器是CronTrigger则走序号【3】策略。如果当前触发器是SimpleTrigger，当重复次数为0时，走序号【5】策略；当重复次数为-1，即无限次数时，走序号【8】策略；否则走序号【6】策略。如果当前触发器是 CalendarIntervalTrigger，则走序号【3】策略。如果当前触发器是 DailyTimeIntervalTrigger，则走序号【3】策略。</td><td align="left">所有触发器</td></tr><tr><td align="left">3</td><td align="left">MISFIRE_INSTRUCTION _FIRE_ONCE_NOW</td><td align="left">1</td><td align="left">立即触发“Misfired”错过触发任务。</td><td align="left">CronTriggerCalendarIntervalTriggerDailyTimeIntervalTrigger</td></tr><tr><td align="left">4</td><td align="left">MISFIRE_INSTRUCTION _DO_NOTHING</td><td align="left">2</td><td align="left">不立即触发执行，等待下次触发时间到达时刻才触发执行，重新开始调度任务。</td><td align="left">CronTriggerCalendarIntervalTriggerDailyTimeIntervalTrigger</td></tr><tr><td align="left">5</td><td align="left">MISFIRE_INSTRUCTION _FIRE_NOW</td><td align="left">1</td><td align="left">立即触发执行。此指令通常只适用于“一次性”（非重复）触发器。如果用于重复次数大于0的触发器，则走需要【7】策略</td><td align="left">SimpleTrigger</td></tr><tr><td align="left">6</td><td align="left">MISFIRE_INSTRUCTION _RESCHEDULE_NOW_WITH _EXISTING_REPEAT_COUNT</td><td align="left">2</td><td align="left">以当前时间为开始触发时间，立即触发执行。重复时间间隔不变，重复次数更新为原先重复次数-已触发次数。然后已触发次数重置为0。如果当前时间是在结束时间之后，触发器将不会再次触发。该策略无法记住最初设置的“开始时间”和“重复次数”</td><td align="left">SimpleTrigger</td></tr><tr><td align="left">7</td><td align="left">MISFIRE_INSTRUCTION _RESCHEDULE_NOW_WITH _REMAINING_REPEAT_COUNT</td><td align="left">3</td><td align="left">与序号【6】策略一致，区别在于重复次数的计算，重复次数更新为原先重复次数-已触发次数-下次触发时间到当前时间区间触发的次数。</td><td align="left">SimpleTrigger</td></tr><tr><td align="left">8</td><td align="left">MISFIRE_INSTRUCTION _RESCHEDULE_NEXT_WITH _REMAINING_COUNT</td><td align="left">4</td><td align="left">等待下次触发时间到达时刻才触发执行，重新开始调度任务。会更新已触发次数为已触发次数+下个触发时间到当前时间的下个触发时间区间的次数。</td><td align="left">SimpleTrigger</td></tr><tr><td align="left">9</td><td align="left">MISFIRE_INSTRUCTION _RESCHEDULE_NEXT_WITH _EXISTING_COUNT</td><td align="left">5</td><td align="left">等待下次触发时间到达时刻才触发执行，重新开始调度任务。</td><td align="left">SimpleTrigger</td></tr></tbody></table><h3 id="错过触发处理策略测试">错过触发处理策略测试</h3><h4 id="线程不足相同时间同时触发多任务">线程不足，相同时间同时触发多任务</h4><p>测试结果：</p><p>当线程池满了，相同时间触发的任务只能等待有可用线程，或者非并发的任务上次触发执行太久，下次触发时间超过配置里设置的容忍错过触发阈值时，将被置为错过触发状态。由错过触发处理线程采用错过触发处理策略处理，或立即执行，或等待下一次触发等等，错过触发处理线程只会更新下次触发时间，并通知触发器监听器，最终还是由调度线程处理；如果没有超过阈值，则意味着已有可用线程可用，正常运行。因为线程池是按触发器的触发时间，然后才是触发器优先级来排任务队列的。一种错过触发任务一直等待的情形，只能是线程不足，而且线程都执行了长时间、甚至是死循环不会任务的这种情形。而线程饥饿是JVM进程调度的范畴，而Quartz调度处理线程中调度任务使用的线程池是SimpleThreadPool，默认的线程优先级是5，每个线程都是有机会参与CPU竞争的。而且Java优先级较高的线程不一定每一次都优先执行，有一定随机性。</p><p>测试步骤：</p><p>1、配置文件中设置任务线程池大小为1。改造FirstJob让它休眠60s。</p><p><img src="http://blog.wenqy.com/upload/2020/11/FirstJob-0d47cc1dc52b43f2ba635ed693e98529.png" alt="FirstJob" /></p><p>2、还是定点相同时间触发多个任务。FistJob优先级最高优先执行。</p><p><img src="http://blog.wenqy.com/upload/2020/11/trigger_on_the_same_time-8025d8ab773540fa83a5cd73f82819a5.png" alt="trigger_on_the_same_time" /></p><p>3、SimpleJob和HelloJob处于等待状态。等待FirstJob线程结束时，另外两线程才相继触发。org.quartz.jobStore.misfireThreshold: 60000 设置容忍错过触发阈值为60s。如：SimpleJob本因55分触发的，如果56分还未触发，就会错过触发。由于线程池满了，SimpleJob只能等待FirstJob执行结束（即休眠60s）。60s后，FirstJob刚刚执行完毕，此时刚好在容忍错过触发阈值，错过触发。由MisfireHandler错过触发线程根据错过触发策略处理。如下图，处理了两个错过触发的任务。（这时候的策略是立即处理）</p><p><img src="http://blog.wenqy.com/upload/2020/11/Misfired_handler-599838e030bd496892606575a7ec12eb.png" alt="Misfired_handler" /></p><p>4、将容忍错过触发阈值加大为61s。即：org.quartz.jobStore.misfireThreshold: 61000</p><p>FirstJob线程结束后。其他两个任务此时未超过容忍错过触发阈值。按触发优先级争用线程池，正常处理。</p><p><img src="http://blog.wenqy.com/upload/2020/11/normal_trigger-e0c9cfcdbc9f4134a5024dc476339e52.png" alt="normal_trigger" /></p><h4 id="crontrigger处理策略">CronTrigger处理策略</h4><p>使用Cron表达式，创建的触发器为CronTrigger，他采用的默认策略：</p><p>MISFIRE_INSTRUCTION_SMART_POLICY。对于CronTrigger，采用策略即立即触发。</p><p><img src="http://blog.wenqy.com/upload/2020/11/MISFIRE_INSTRUCTION_SMART_POLICY-f0dc69b3aa7448a59adddf720bf905d3.png" alt="MISFIRE_INSTRUCTION_SMART_POLICY" /><br />而CronTrigger暴露的错过触发策略：</p><p><img src="http://blog.wenqy.com/upload/2020/11/CronTrigger_misfired_handler-20a41b565ad3421aa8a49f64c6979538.png" alt="CronTrigger_misfired_handler" /></p><p>即对应表格：</p><table><tbody><tr><td width="426">CronTrigger方法</td><td width="426">对应策略</td></tr><tr><td width="426">withMisfireHandlingInstructionIgnoreMisfires</td><td width="426">MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY 序号【1】</td></tr><tr><td width="426">withMisfireHandlingInstructionFireAndProceed</td><td width="426">MISFIRE_INSTRUCTION_FIRE_ONCE_NOW 序号【3】</td></tr><tr><td width="426">withMisfireHandlingInstructionDoNothing</td><td width="426">MISFIRE_INSTRUCTION_DO_NOTHING 序号【4】</td></tr></tbody></table>观察其实现类org.quartz.impl.triggers.CronTriggerImpl，默认策略就是立即触发。<p><img src="http://blog.wenqy.com/upload/2020/11/CronTriggerImpl.trigger_immediately-7e3db789438646b9a1fbeab587cdf84c.png" alt="CronTriggerImpl.trigger_immediately" /></p><p>1、测试withMisfireHandlingInstructionDoNothing 即序号【4】策略。</p><p>创建触发器，带有withMisfireHandlingInstructionDoNothing</p><p><img src="http://blog.wenqy.com/upload/2020/11/create_trigger_handlerwithnothing-6c3162ac0b2e42699f829e4ee26dd691.png" alt="create_trigger_handlerwithnothing" /></p><p>2、设置容忍错过触发阈值为60s，让任务错过触发。改下触发时间，继续测试。</p><p><img src="http://blog.wenqy.com/upload/2020/11/handlerwithnothing_result-c9310578729f41c1bd89a16963751602.png" alt="handlerwithnothing_result" /></p><p>3、根据打印日志可知，错过触发处理线程已经处理错过触发任务。但此时错过触发任务没有立即执行，要等待下一次执行。（下次执行时间是下一天对于的时间点，无法重现）</p><h4 id="simpletrigger处理策略">SimpleTrigger处理策略</h4><p>1、测试SimpleTrigger的错过触发策略，定义StatefulDumbJob任务类，使用有状态和并发控制的Job，【有状态和并发Job测试】。将容忍错过触发阈值设为7000ms。线程数为2。</p><p><img src="http://blog.wenqy.com/upload/2020/11/statefuldumbjob-cd4fa285ffce45fca1f079cd8fb2dea2.png" alt="statefuldumbjob" /></p><p>2、定义两个任务，都是休眠10s，调度启动15s后，开始任务，后每3s触发，无限重复执行。Job1使用默认策略，因为定义的触发器SimpleTrigger及无限重复，由序号【1】策略，可知，最终走序号【8】策略</p><p>MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT；job2使用序号【6】策略MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT</p><p><img src="http://blog.wenqy.com/upload/2020/11/MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT-08c38fadaa3b4377a786c548339eb81a.png" alt="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT" /></p><p>3、查看测试结果，05分15秒开始两个任务触发。下个触发时间按周期3s，本应18s触发，但任务休眠10s后才能执行，刚好超过容忍错过触发的阈值，变成&quot;Misfired&quot;。由错过触发处理线程检测处理，05分27秒处理错过触发任务。发现job2立即执行，但job1在05分30秒才执行。第二次处理错过触发任务时，job2立即执行，但job1在05分42秒才执行。05分30秒、05分42秒、05分57秒都落在05分15秒开始，每3秒的周期点上，即会等待检测当前时间的下个触发时间触发错过触发任务。验证了序号【8】和【6】策略。</p><p><img src="http://blog.wenqy.com/upload/2020/11/simpletrigger_result-7fb3aa18b849442a8af6b75771c68ff1.png" alt="simpletrigger_result" /></p><h3 id="代码参见">代码参见</h3><p>不同类型触发器定义了不同的公有静态常量来表示错过触发指令，用于错过触发任务处理，过触发指令可由以下接口找到</p><p>org.quartz.SimpleTrigger</p><p>org.quartz.CronTrigger</p><p>org.quartz.CalendarIntervalTrigger</p><p>org.quartz.DailyTimeIntervalTrigger</p><p>不同类型触发器实现AbstractTrigger抽象方法进行错过触发任务处理</p><p>org.quartz.impl.triggers.AbstractTrigger.updateAfterMisfire(Calendar)</p><h3 id="总结">总结</h3><p>Quartz定义了容忍错过触发阈值和错过触发处理策略，不同类型的触发器可以有不同的触发策略，根据可能错过触发的场景，配置线程池大小也选的比较重要了，Quartz配置应该要能适应应用业务需要，动态调整，比如，可以上配置中心，等等。</p>]]>
                    </description>
                    <pubDate>Tue, 17 Apr 2018 23:21:03 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Quartz管中窥豹之触发器状态]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/quartz管中窥豹之触发器状态</link>
                    <description>
                            <![CDATA[<p>Quartz随着任务的创建，触发和销毁，触发器状态也做出改变，流转成相应的变化。我们可以通过触发器状态变化观察到定时任务的生命周期。</p><h3 id="触发器状态">触发器状态</h3><p>我们可以看下<code>Quartz org.quartz.impl.jdbcjobstore.Constants</code>常量类中定义的触发器的几种状态:</p><table><tbody><tr><td width="426">Triggers表状态</td><td width="426">描述</td></tr><tr><td width="426">WAITING</td><td width="426">创建任务触发器默认状态</td></tr><tr><td width="426">ACQUIRED</td><td width="426">当到达触发时间时，获得状态</td></tr><tr><td width="426">EXECUTING</td><td width="426">运行中，firedTrigger表中</td></tr><tr><td width="426">COMPLETE</td><td width="426">完成状态，任务结束</td></tr><tr><td width="426">BLOCKED</td><td width="426">阻塞状态</td></tr><tr><td width="426">ERROR</td><td width="426">错误状态</td></tr><tr><td width="426">PAUSED</td><td width="426">暂停状态</td></tr><tr><td width="426">PAUSED_BLOCKED</td><td width="426">暂停阻塞状态，非并发下</td></tr><tr><td width="426">DELETED</td><td width="426">删除状态</td></tr></tbody></table>下图是**触发器状态图**：描述调度线程QuartzSchedulerThread自动状态切换和手动状态切换（动态调用定时任务CRUD方法）的过程。<p><img src="http://blog.wenqy.com/upload/2020/11/trigger_status_diagram-a342b16de9e44ae0925d9812b0517be5.png" alt="trigger_status_diagram" /></p><p>1、在创建JobDetail和Trigger并schedueJob初始化调度后默认状态为等待<code>WAITING</code>状态</p><p>2、在调度线程QuartzSchedulerThread 获取下次触发队列acquireNextTriggers时更新为已获得<code>ACQUIRED</code>状态</p><p>3、 JobStoreSupport.triggerFired的时候会根据是否取得绑定的Job判断，如果无法取得直接进入错误<code>ERROR</code>状态，否则根据Job是否运行并发，Job体现为自定义Job实现类是否加类注解<code>@DisallowConcurrentExecution</code>。如果允许并发，则判断下次触发时间NextFireTime，如果为空，则意味着任务生命周期已经结束，状态标记为完成<code>COMPLETE</code>状态，如果下次触发时间NextFireTime不为空，则回到WAITING状态，等待下一次任务触发时间的到来；如果Job不允许并发，也会根据下次触发时间NextFireTime判断，如果下次触发时间NextFireTime不为空，不为空，则进入阻塞<code>BLOCKED</code>状态，同时也会更新绑定相同Job实例的其他触发器的状态</p><p>4、对于动态任务管理，如暂定任务pauseJob，则会将触发器状态置为<code>PAUSED</code>或<code>PAUSED_BLOCKED</code>，相应地，调用resumeJob恢复任务，则触发器状态重置成WAITING状态。</p><p>这些生命周期跟线程状态很像，熟悉线程生命周期，就很容易理解。</p><h3 id="代码参见">代码参见</h3><p>1、常量类定义的触发器状态值</p><p><code>Quartz org.quartz.impl.jdbcjobstore.Constants</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/jdbcjobstore.Constants-6e70e30905d04d279c929afc432c19c2.png" alt="jdbcjobstore.Constants" /></p><p>2、Quartz提供了定时任务动态改变的方法</p><p><code>org.quartz.core.QuartzScheduler</code></p><p><img src="http://blog.wenqy.com/upload/2020/11/core.QuartzScheduler-651f4d9ebce442a39cd7db265aa4fed7.png" alt="core.QuartzScheduler" /></p><p>对于用可视化界面动态进行定时任务CRUD管理可以利用这些方法，可以在这基础上再统一封装一层定时任务管理的API</p><p>对于触发器生命周期的状态流转，并没有一一的跟进验证，只是看着源码画出自己理解的状态图，难免跟实际情况有些出入。</p>]]>
                    </description>
                    <pubDate>Sun, 15 Apr 2018 22:46:08 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Quartz管中窥豹之集群管理]]>
                    </title>
                    <link>http://blog.wenqy.com/archives/quartz管中窥豹之集群管理</link>
                    <description>
                            <![CDATA[<p>Quartz是原生支持应用集群下的任务调度，查下摘自官网的架构图：</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_cluster-88ca304b67ee4ee08cc5a7736fa541d9.png" alt="quartz_cluster" /></p><p>Quartz集群中的每个节点是一个独立的Quartz任务应用，它又管理着其他的节点。该集群需要分别对每个节点分别启动或停止，不像一些应用服务器的集群需要彼此通信，独立的Quartz节点并不与另一个节点或是管理节点通信。Quartz应用是通过共用相同数据库表来感知到另一应用。也就是说只有使用****持久化JobStore<strong>存储Job和Trigger</strong>才能完成Quartz集群。</p><h3 id="数据库核心表">数据库核心表</h3><p>在Quartz源码的<code>docs/dbTables</code>目录下，存放了针对不同数据库Quartz所需要的数据库表。以Mysql为例，看下需要的表：</p><table><tbody><tr><td width="285">表名</td><td width="566">描述</td></tr><tr><td width="285">QRTZ_CALENDARS</td><td width="566">存储Quartz的Calendar信息</td></tr><tr><td width="285">QRTZ_CRON_TRIGGERS</td><td width="566">存储CronTrigger，包括Cron表达式和时区信息</td></tr><tr><td width="285">QRTZ_FIRED_TRIGGERS</td><td width="566">存储与已触发的Trigger相关的状态信息，以及相联Job的执行信息</td></tr><tr><td width="285">QRTZ_PAUSED_TRIGGER_GRPS</td><td width="566">存储已暂停的Trigger组的信息</td></tr><tr><td width="285">QRTZ_SCHEDULER_STATE</td><td width="566">存储少量的有关Scheduler的状态信息，和别的Scheduler实例</td></tr><tr><td width="285">QRTZ_LOCKS</td><td width="566">存储程序的悲观锁的信息</td></tr><tr><td width="285">QRTZ_JOB_DETAILS</td><td width="566">存储每一个已配置的Job的详细信息</td></tr><tr><td width="285">QRTZ_JOB_LISTENERS</td><td width="566">存储有关已配置的JobListener的信息</td></tr><tr><td width="285">QRTZ_SIMPLE_TRIGGERS</td><td width="566">存储SimpleTrigger，包括重复次数、间隔、以及已触的次数</td></tr><tr><td width="285">QRTZ_BLOG_TRIGGERS</td><td width="566">Trigger作为Blob类型存储</td></tr><tr><td width="285">QRTZ_TRIGGER_LISTENERS</td><td width="566">存储已配置的TriggerListener的信息</td></tr><tr><td width="285">QRTZ_TRIGGERS</td><td width="566">存储已配置的Trigger的信息</td></tr></tbody></table>Quartz的集群部署方案在架构上是分布式的，没有负责集中管理的节点，而是利用****数据库行锁****的方式来实现集群环境下进行并发控制。<p>一个调度器实例在集群模式下首先要获取 <code>{0}LOCKS</code> 表中对应的行级锁，</p><p>向MySQL获取行锁语句为</p><pre><code>select * from {0}LOCKS where sched_name = ? and lock_name = ? for update</code></pre><p>{0}会替换为配置文件默认配置的<code>QRTZ_</code>。<code>sched_name</code>为应用集群的实例名，<code>lock_name</code>就是行级锁名。Quartz主要由两个行级锁。</p><table><tbody><tr><td width="426">lock_name</td><td width="426">desc</td></tr><tr><td width="426">STATE_ACCESS</td><td width="426">状态访问锁</td></tr><tr><td width="426">TRIGGER_ACCESS</td><td width="426">触发器访问锁</td></tr></tbody></table>Quartz集群争用触发器行锁，锁被占用只能等待。获取触发器行锁后，先获取需要待触发的其他触发器信息。数据库更新触发器状态信息，及时释放触发器行锁，供其他调度实例获取，然后在进行触发器任务调度操作。对数据库操作就要先获取行锁。<h3 id="集群配置">集群配置</h3><p>定义<code>quartz.properties</code>配置文件默认放在应用classpath路径下，其他路径只能自己手动加载properties。下面是集群配置参考</p><pre><code class="language-properties">#集群中应用采用相同的Scheduler实例org.quartz.scheduler.instanceName: wenqyScheduler#集群节点的ID必须唯一，可由quartz自动生成org.quartz.scheduler.instanceId: AUTO#通知Scheduler实例要它参与到一个集群当中org.quartz.jobStore.isClustered: true#需持久化存储org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTXorg.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate#数据源org.quartz.jobStore.dataSource=myDS#quartz表前缀org.quartz.jobStore.tablePrefix=QRTZ_#数据源配置org.quartz.dataSource.myDS.driver: com.mysql.jdbc.Driverorg.quartz.dataSource.myDS.URL: jdbc:mysql://localhost:3306/ncdborg.quartz.dataSource.myDS.user: rootorg.quartz.dataSource.myDS.password: 123456org.quartz.dataSource.myDS.maxConnections: 5org.quartz.dataSource.myDS.validationQuery: select 0</code></pre><h3 id="集群测试">集群测试</h3><p>测试结果：<strong>Quartz集群管理可以避免应用在集群环境下重复执行相同的Job。</strong></p><p>测试步骤：</p><p>在两个IDE环境下测试同一份demo应用，运行相同的任务。应用采用集群配置。</p><p>一个应用先添加任务，每秒触发任务。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_add_test_job-dda52bb0588e4d14b57b845f3158aabb.png" alt="quartz_add_test_job" /></p><p>集群自动分配实例ID，避免冲突，如看打印日志。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_cluster_id-f80cfc6c26294710b61a3b46febc9629.png" alt="quartz_cluster_id" /></p><p>另外一个环境执行相同添加任务，则提示已经存在无法添加任务。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_job_exists-81fd303c948047c6a5d3d709255685a6.png" alt="quartz_job_exists" /></p><p>查看数据库，此时，任务信息持久化在表中</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_jobstore-59b6ca9979884d649420f4862e124c30.png" alt="quartz_jobstore" /></p><p>应用再次运行时会自动触发库中持久化的任务信息。模拟启动。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_test_run_job-795114d7f66e422e8e2233b6773e9ad1.png" alt="quartz_test_run_job" /></p><p>一个环境先启动，已经触发了任务。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_trigger_job-5a77121494b242af8160d48432ad958a.png" alt="quartz_trigger_job" /></p><p>另一个实例，没有触发任务就结束了。</p><p><img src="http://blog.wenqy.com/upload/2020/11/quartz_job_is_runover-a081c06e425841e48a2a400e7f0bb879.png" alt="quartz_job_is_runover" /></p><p>验证了任务集群下是可以避免任务重复执行的。</p><h3 id="总结">总结</h3><p><strong>Quartz虽然强大，任务调度极其方便，易用，集群下也可以避免任务重复执行。但还是有些不足之处：节点任务调度会跟系统当前时间做比较，集群各个节点的系统时间尽可能一致，Quartz集群还发生争用数据库的情况，存在数据库单点故障，任务处理也存在应用集群下单机处理极限问题。Quartz原生也没有支持可视化监控的任务管理端。有不少分布式调度开源框架都是基于Quartz做了扩展。</strong></p><h3 id="参考">参考</h3><p>Quartz应用与集群原理分析 <a href="https://tech.meituan.com/mt-crm-quartz.html">https://tech.meituan.com/mt-crm-quartz.html</a></p><p>基于 Quartz 开发企业级任务调度应用 <a href="https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/">https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/</a></p>]]>
                    </description>
                    <pubDate>Tue, 03 Apr 2018 21:43:23 CST</pubDate>
                </item>
    </channel>
</rss>