SegmentFault 全栈在路上最新的文章
2020-12-09T10:37:28+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
webpack项目如何正确打包引入的自定义字体?
https://segmentfault.com/a/1190000038422245
2020-12-09T10:37:28+08:00
2020-12-09T10:37:28+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<h2>一. 如何在Vue或React项目中使用自定义字体</h2><blockquote>在开发前端项目时,经常会遇到UI同事希望在项目中使用一个炫酷字体的需求。那么怎么在项目中使用自定义字体呢?<p>其实实现起来并不复杂,可以借用CSS3 @font-face 来实现。</p><p>本文着重介绍一下 webpack 项目如何正确打包引入的自定义字体。</p></blockquote><h3>@font-face有什么用</h3><p>总结一下就是:用户借助该规则,可以为引入的字体包命一个名字,并指定在哪里可以找到它(指定字体包的存储路径)后,就可以像使用通用字体那样去使用它了。</p><h3>具体实现步骤</h3><p>例如现在的需求是:需要在项目中使用 <strong>KlavikaMedium-Italic</strong> 字体。</p><p>则只需以下三个步骤即可。</p><h4>1. 将字体包放入项目目录下</h4><p>这里放到根目录下的 tool/fonts 文件夹里。</p><h4>2. 在index.css文件中定义</h4><pre><code>@font-face {
font-family: 'myFont';
src: url(tool/fonts/KlavikaMedium-Italic.otf);
}</code></pre><h4>3. 使用自定义字体</h4><p>新建一个index.vue文件,引入样式:</p><pre><code>import './index.css'
<template>
<h1>使用自定义字体</h1>
<style>
h1 {
font-family: 'myFont'
}
</style>
</template></code></pre><p>效果如下:</p><p><img src="/img/bVcLnuR" alt="image" title="image"></p><h2>二. webpack项目如何正确打包自定义的字体</h2><h3>1. 打包时报错</h3><p>既然在本地开发环境实现了效果,于是就使用 webpack 打包准备上线,却发现 webpack 在打包过程中报错:</p><p><img src="/img/bVcLnuV" alt="image" title="image"></p><h3>2. 打包时为什么会报错</h3><p>我们在定义自定义字体时使用URL指定了字体包的路径,由于 webpack 默认是无法处理 css 中的 url 地址的,因此这里会报错。</p><h3>3. 解决报错</h3><h4>3.1 认识file-loader</h4><p>这时就需要借助 loader 来大显身手了,解决这个问题需要使用 <strong>file-loader</strong>,它主要干了两件事儿:</p><ul><li>根据配置修改打包后图片、字体包的存放路径;</li><li>再根据配置修改我们引用的路径,使之对应引入。</li></ul><h4>3.2 安装file-loader</h4><p>yarn add file-loader</p><h4>3.3 配置file-loader</h4><p>在 webpack.config.js 中,配置file-loader:</p><pre><code>module.exports = {
module: {
rules: [
{
// 命中字体包
test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
// 只命中指定 目录下的文件,加快Webpack 搜索速度
include: [paths.toolSrc],
// 排除 node_modules 目录下的文件
exclude: /(node_modules)/,
loader: 'file-loader',
},
]
}
}</code></pre><p>再次执行打包命令,不再报错。</p><h3>4. 自定义字体为什么不生效</h3><p>于是将打包出来的 dist 目录重新部署到服务器上后访问页面,却发现由于找不到字体导致没有生效:</p><p><img src="/img/bVcLnvf" alt="image" title="image"></p><p>从图中可以看出,http请求字体包的路径为:<strong>根目录下(打包出来的静态文件index.html所在目录)的 css/620db1b997cd78cd373003282ee4453f.otf</strong>。</p><h4>4.1 字体不生效的原因</h4><p>看了一下打包命令生成的 dist 目录结构:</p><pre><code>├── 620db1b997cd78cd373003282ee4453f.otf
├── css
│ ├── backend.66a35.css
│ └── backend.66a35.css.map
├── favicon.ico
├── images
│ ├── bg.5825f.svg
│ ├── data-baseTexture.c2963.jpg
│ ├── data-heightTexture.6f50d.jpg
│ └── logo.7227a.png
├── index.html
└── js
├── backend.66a35.js</code></pre><p>却发现,字体包和 index.html 是在同一级。因此字体无法生效的原因就很明朗了:</p><ul><li>由于http请求的字体包路径与实际的存放路径一致,就导致了404;</li><li>找不到字体包的实际路径,因此使用的字体无法生效。</li></ul><h4>4.2 字体不生效的解决办法</h4><p>可以通过修改字体包打包后的实际存储路径去解决这个问题,在 webpack.config.js 中,借助 options 参数可以继续给 file-loader 设置更多的配置项:</p><pre><code>module.exports = {
module: {
rules: [
{
// 命中字体包
test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
// 只命中指定 目录下的文件,加快Webpack 搜索速度
include: [paths.toolSrc],
// 排除 node_modules 目录下的文件
exclude: /(node_modules)/,
loader: 'file-loader',
// 新增options配置参数:关于file-loader的配置项
options: {
limit: 10000,
// 定义打包完成后最终导出的文件路径
outputPath: 'css/fonts/',
// 文件的最终名称
name: '[name].[hash:7].[ext]'
}
},
]
}
}
</code></pre><p>再次打包,生成的 dist 目录结构如下:</p><pre><code>
├── css
│ ├── backend.66a35.css
│ ├── backend.66a35.css.map
│ └── fonts
│ └── KlavikaMedium-Italic.620db1b.otf
├── favicon.ico
├── images
│ ├── bg.5825f.svg
│ ├── data-baseTexture.c2963.jpg
│ ├── data-heightTexture.6f50d.jpg
│ └── logo.7227a.png
├── index.html
└── js
├── backend.66a35.js</code></pre><p>可以看到字体包正如配置时预期的那样存储在 <strong>css/fonts</strong> 目录下面。</p><p>重新部署项目,再次查看:</p><p>这一次 http 请求的字体包路径与实际的存放路径一致,因此自定义字体生效。</p><p>可以通过下面这个梳理流程图看的更清楚一些:<br><img src="/img/bVcLnvP" alt="image" title="image"></p><h2>三. 总结</h2><p>为什么本地开发的时候可以看到字体,部署到服务器后却看不到了呢?</p><ul><li>由于 webpack 项目在本地开发中使用的是 webpack-dev-server,实时编译后的文件都保存到了内存当中,引用字体包的时候使用的是绝对路径,因此在本地开发中使用的自定义字体能够生效;</li><li>使用webpack打包后的 dist 目录,字体包的实际存储路径与 http 请求字体包的路径不一致,因此导致找不到字体包;</li><li>借助 file-loader 解决 webpack 打包报错,通过使用 options 参数去设置字体包在打包后的实际存储路径,从而解决问题。</li></ul><h2>四. 更多文章</h2><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=orvEq2KpVXVUshVHnQzivg%3D%3D.eZ%2B1jHZCDKm5%2F89Ui9Y7u7BSsVjByDHTEn8My7uD4IQJDvf7pnxQGWqINUJIY8fDbEFkntudk9YBlOTAFDqzoOUrOiM934UrZpZMqyNIvHwiMQsKYulKbvb2zAx23hS5hU3r52uqL7VUkcpkp4a4rQ47b0E6f%2FiNMtZpK19G5Q1BOixeTocusAKw%2Bt2keecvEt2Oat5o1iPjLvxCMMdCPdAu2JvNo4aSjmO8Gh5ihb4zHjdCqpvzXWXX1ZcLWvFhjnt2KMKyMDxKrp5he9l6yQvzdLB6vtiYho4r0mrJsDRZVgAZUc2HGlsSCavhKBGJNgBdmb8mjh9VvHEcyLp2fw%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=%2FpBhJsmWzr7e%2FKnIr6HHNQ%3D%3D.TVsPMAC%2BpJW%2BlnefEONJNVgBDSw%2F%2BaOjRlQMC6mVdzdNKK3RTValEGOObeov82A4y6jLbABXHfCGcmQ4bLnBK%2Fc1L8lPS4tXdPrRgZa9fNliK9vvjTBDfXsss1EeRaWR40Zw3Tc7pEWJeSnz%2BxCOpyFJJ4MMgq4n6ejQdCuf4K3DQWZYGp4n6picZPCdSCerIqju2fL56zv%2FyGkjG%2Fewsge9ZHk73LkKY4uf32bWIiIYzzCfrbXzalY8KtPmfLtVqqlgQF%2FU8vaJa3lPkOAQ6RKXI3Wkn8ow76%2FwvURNsxFpjaVP6ORpz21lS5gfiJ4vDirDHIiNT4FHzNLMYFLQSg%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=jpIE16b89ick83C%2FU9LzGQ%3D%3D.sfAdQxSa0%2BEN10D0oLV5wraYIKBiEPKugUXuOUDKNB%2FmiYnhAC%2BHH8purLO8M1ypCuqPSsVJXJMJAfhrl2MQgpmfxt%2B1Kqj%2FcumTZWXdFUWSW3zmWOYssVW%2FV9RCQfXx4B74GFdxVIBKLc1mXRyYDPQ0tkpVffYO2m5LBmVlUkUXkvgJttd9hNdsPbl6yiBmWCeupbp%2FZ%2FZvg3vyila1sqjViwFWiYSU51F5lOXGY1fGuhoIeOyutoIfkcYRxcXQH69WntJ40q3URMeF%2BRHQgzehQqJoubUen9AolLfUbUiFR2eMj6TyYEnJKsWUC6Yv4Z7yD%2FoQmdA2b223Rw5jcw%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=Gz1grKImohpwLEzRjTcyBQ%3D%3D.kdYXCNg2MKBspp0OTPfJTAVjmFPgB%2Bw0lRdEuu44XUh4MEN5iTpJ%2FDoWQrjawtLDUElsm2t1o9cLI6qF3MtUBQVxBqD6tQ1aXd8Qjz%2Bif59WMAOOWYXce2%2Bmg5jT%2Bl15VhYS8Z%2FgE3b6jD36Bm9Sg1vNyjSFe4uXDOxpawgwa71v5uJva%2FeKhtn9SPZEQNgwGqfEvIirPncFLuyXVHmfENjB4yPgjA%2FTnDKoR1y38Ol9eeaCNsNDxvuDxukAPqm7NTw0dA4g8Dt94QQtajSSMJ0%2Ft%2BWSqsEpsokOFhYVueLLaWCMFE8ofLBP%2BggEf5fBdUGyc7Qr1KxCzsjor3%2BuXA%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多原创文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
使用easyexcel时遇到Could not initialize class cglib.beans.BeanMap怎么解决
https://segmentfault.com/a/1190000023890615
2020-09-04T11:59:36+08:00
2020-09-04T11:59:36+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<blockquote><ol><li>上一篇文章 Maven项目为什么会产生NoClassDefFoundError的jar包冲突 结合了大量的图解,详细介绍了Maven项目产生jar包冲突的原因,以及为什么在编译的时候不报错,在运行的时候会报错的场景分析;</li><li>本篇记录一下在项目开发中使用Alibaba的开源组件easyexcel做excel文件上传和下载功能时,遇到的一个jar包冲突问题的排查思路和解决办法。</li></ol></blockquote><h3>一. 问题现象</h3><p>在使用alibaba的easyexcel工具开发excel的上传和下载功能时,本机环境测试没有问题,部署到测试环境时,却发现下载excel文件的功能一直异常,查看后台服务报错日志如下:</p><pre><code>nested exception is com.alibaba.excel.exception.ExcelGenerateException: java.lang.NoClassDefFoundError: Could not initialize class net.sf.cglib.beans.BeanMap$Generator] with root cause]
java.lang.NoClassDefFoundError: Could not initialize class net.sf.cglib.beans.BeanMap$Generator</code></pre><p>上一篇<strong>Maven项目为什么会产生NoClassDefFoundError的jar包冲突</strong>时 已经介绍过,看到 <code>NoClassDefFoundError</code>类似的异常时,大多数都是因为<strong>jar包冲突</strong>引起的。</p><h3>二. 问题排查流程</h3><p>由于本地开发环境无法浮现,所以只能从测试环境着手排查。</p><h4>1. 解压jar包</h4><p>登录项目部署的docker容器,解压项目jar包,将解压后的文件放入 app 文件夹里:</p><pre><code>[root@08e08117bd99 /]# cd /data/application/java
[root@08e08117bd99 /data/application/java]# unzip app.jar -d app/</code></pre><h4>2. 查找jar包里有没有cglib.beans.BeanMap类</h4><p>进入解压后的 app 目录,查找 cglib 包:</p><pre><code>[root@08e08117bd99 /data/application/java]# cd /app/BOOT-INF/lib
$ ls -l | grep cglib
-rw-r--r-- 1 root root 283080 Dec 7 2013 cglib-3.1.jar
# 继续解压cglib包
[root@08e08117bd99 /data/application/java/app/BOOT-INF/lib]# unzip -l cglib-3.1.jar | grep BeanMap
336 12-07-2013 11:28 net/sf/cglib/beans/BeanMap$Generator$BeanMapKey.class
3219 12-07-2013 11:28 net/sf/cglib/beans/BeanMap$Generator.class
5008 12-07-2013 11:28 net/sf/cglib/beans/BeanMap.class
1825 12-07-2013 11:28 net/sf/cglib/beans/BeanMapEmitter$1.class
2090 12-07-2013 11:28 net/sf/cglib/beans/BeanMapEmitter$2.class
1546 12-07-2013 11:28 net/sf/cglib/beans/BeanMapEmitter$3.class
6339 12-07-2013 11:28 net/sf/cglib/beans/BeanMapEmitter.class</code></pre><p>发现,解压后的 jar包里是存在 <code>cglib.beans.BeanMap$Generator</code> 这个类。</p><h4>3. 继续看其它错误日志</h4><p>继续看错误日志,发现这段:</p><pre><code>class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class</code></pre><p>大概意思是:</p><blockquote>ClassVisitor 定义的是一个 interface,但是现在却作为一个父类(super class)被其它类继承了。</blockquote><p>这种情况表明,此时应该存在jar包冲突了:</p><ul><li>ClassVisitor 在一个jar包中是一个 interface;</li><li>在另一个jar包中却是一个 class。</li></ul><h4>4. 从刚刚解压的项目jar包里查找asm包</h4><p>由于这个报错信息里包含asm,所以尝试查找包含asm的jar包:<br><img src="/img/bVbMo9h" alt="image" title="image"></p><p><strong>发现竟然有两个不同版本的jar包。。。</strong></p><h3>三. 问题原因</h3><p>为什么一个项目打出的 jar包里会有两个 asm 包呢?</p><h4>1.项目哪些jar包依赖了asm包</h4><p>使用 Maven helper 搜索 asm:</p><p><img src="/img/bVbMpdd" alt="image" title="image"></p><ul><li>easyexcel 2.1.6 依赖 cglib 3.1,cglib又依赖 asm 4.2;</li><li>项目的springboot版本是2.0.0.M6,底层会依赖 asm 3.1。</li></ul><h4>2. 为什么会有两个版本的asm包?</h4><p>从Maven 官方网站里搜索 asm 包:</p><p><img src="/img/bVbMo9E" alt="image" title="image"><br>发现有两个 artifactId 都叫 asm(但groupId 不一样),点击第2个 asm,查看详情:</p><p><img src="/img/bVbMo9Q" alt="image" title="image"></p><p>也就是说 gropuId 为 asm 的包,从3.3.1版本后不再维护了,后续版本迁移到 gropuId为 org.ow2.asm 的 asm 包。</p><p>看到这里,结论已经出来:</p><ul><li>asm 包从3.3.1 往后,gropuId 发生了变更(由asm 变更 org.ow2.asm);</li><li>由于项目使用的springboot版本是2.0.0,需要依赖asm3.0,easyexcel 2.1.6 依赖的是asm 4.2;</li><li>导致 Maven 在打包的时候将这两个 asm包( artifactId 一样,但groupId 不一样)都打进去了。</li></ul><h3>四. 问题解决</h3><h4>1. 该使用哪个版本的asm包?</h4><p>到现在为止,<strong>已经在排查过程中也得到了有用的报错信息</strong>:</p><pre><code>class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class</code></pre><blockquote>翻译过来就是: ClassVisitor 定义的是一个 interface,但是现在却作为一个父类(super class)被其它类继承了</blockquote><p>并且也理清了产生冲突的原因:<strong>项目jar包里包含两个asm包</strong>,现在要确定项目使用哪个版本的asm包。</p><p>于是,从本机的 Maven 仓库里 分别找到 asm3.1 和 asm4.2 的包,并在 Idea 里打开<code>ClassVisitor.class</code>:</p><p>asm3.1:</p><p><img src="/img/bVbMo9Y" alt="image" title="image"></p><p>asm4.2:</p><p><img src="/img/bVbMo96" alt="image" title="image"></p><p>可以看到:</p><ul><li>asm3.1的 <code>ClassVisitor.class</code> 是 interface,asm4.2的<code>ClassVisitor.class</code>是 class;</li><li>再结合报错信息,确定项目<strong>应该使用 asm3.1 的包</strong>。</li></ul><h4>2. 怎么解决冲突</h4><p>由于asm 4.2是由 easyexcel 2.1.6 的依赖 cglib 3.1 引入的,因此降级 cglib 的版本,直接在<code>pom.xml</code>里引入低版本的 cglib 即可:</p><pre><code><dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency></code></pre><p>更新依赖,再次使用Maven helper查看:</p><p><img src="/img/bVbMo97" alt="image" title="image"></p><p>可以看到:asm 版本已经降级成功。</p><p>使用 Jenkins 编译、打包、部署至测试环境后,再次测试项目的 excel 下载功能已经可以正常使用,jar包冲突导致的问题已经解决。</p><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=Tx6lG4o6UuEitFRqHc%2B6tg%3D%3D.O8Rk89nFx77fZ2MYgNBdFBxZEQWNoklaC0mip1GFAQypNApAP9CuDUQJdoiNIo6GlEp9X%2BpGk7%2BYdgQL00Al%2BnVPs1Y%2FA9uJDDVOJbUd7ZyzwU9QQ8sJBQuyCjHUbH3IctNSPivliku0zq5bb6re0swxYCF4hKoVQsvDC2mPNLZ5ZriFZhf0TshvKQiki5aeAAqSkPL29cLSZLzu8AOE913i0hZbdhTU8MgH%2FjWeMdfZomvWIEU6rGFKLGssgh3WN8xADMSOK3qAUhLPxKna0ZHzxY8gYLSuqw96oSKMIQcZ%2B97p8sXBbbC7yU%2BYIQru6fRtNrDIFvSWgOykWyjYRtl4egpwjf5MzfpJETE%2Fnrk%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=8s0mgJQnjtzZfOV6fXlNQA%3D%3D.NfsDL8zWZpj%2FekNirXKukr1v0tMFrsS1zj8az6th%2FAOZeTL8C1TQzfhW4o2FytV4QhcaM%2FeCEkZqQnbx3xkHScKJOrJjFsjV%2FeXoqQ9GmLPiJM5geRYFRKQX%2BM2izeD2gAIIs9%2B3qtmFhFVIfLfOMNJ6I%2BIUnNIwyjk8N9k%2FEKNhOExSttJC7%2BHjWbOiu7px2zfRwuhFVoTY2OQmnBdzEVb840UJ%2BViXkqGqUHw8K74caMkyU2Ts1rWgHYgTkI%2F4Ap%2BZcfDLgzscKS%2FBRUKS3kXjbZufpfUswKxpcEzhdK3r6rOolyNo5unEgZm1SqX9VW%2F3p1cDQGZgs049RxLxg0INRszlCmdQ5AIRkBU5kBY%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=8UMTU%2F85E5QBwLoYiU2b%2FA%3D%3D.qwKewP9737CRjrlxcEXwkxD9ELuSjTNtTG%2F7K6EwDZr5RBMZkCMs7nafuzCPgWC18vrraUKAjgXZ6KcZAnY%2BxWWf8qRR8SYyItOIoIc0kIx4vgLInMu7xrlujPdhRgTxHxpDxvv0tUkpDV6RylFB6%2BzuiDz5%2F9R57yAz2qCFyd97gpL%2F32rxiB1fM29e%2Fu8hU1LU0asFVohRUylCS9nAf%2FVVEbrVcYjUlfS4dBHsbTx3Vlatse%2B1lzMzwTIcWoqaXGLTbXFw4uFLdSkkmc%2BnE8tvQcoL0XTpBD0bb%2FZcYHtY5%2FjpLICVeQ%2Fb5P32tXPCGZDliQugOGLnkElGFUJfVT8tbMS%2F2eZH5I91MLxfJzE%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=eCEs2kTbEzxA%2BrPSOhT2dw%3D%3D.Z6tSr14xE26xm9gjQ%2BQo6Zvex73tCqkB0X7NedREePW%2BKAAZyfRPC%2FUugGcI0vAzbPTjMQPqGU%2BpYplRJ8DwnDParDRaeQ8KDWTbEcf8MRviAjopbk3HVHEv5%2FlFlOe1W3QF%2FoRlGmaAMP2851Xcnc6mAjuwmGoygKuV0FehGEOJzhR9G56ICKCoHg%2BVPJTpZXtHRQOw1kcG8epWKXaXJhqJtIJSVDcwPlTN9otVeC19VtAjbVBpS9fiuWZdS8%2BabtzVu%2FVOe8weSt4E9HZfxTjqfE3zz9t3UkfPJRQCPKbQJJhOLEku1P6F7jn%2Fn9bqRG5ovUA9c%2B9j%2FHwcQt%2B8F6af0FAIfdFsuSihmWXdjQc%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=fJal%2BUpPomSTFZRVh28GYw%3D%3D.6P7tBIflFCmzZAZ6PlDJPtGf4rFDtn7PSkujm8sYRNUs4swnlvkyfEW9piAxuRpkxHE%2BYyGGWydMBjJK4gbyDNw%2BRdFvtSwzayFCFLYgSMHh%2BAe7u46OQ9hKN4aTLZUrFSKIM0xqA4kvaQdS2jbPxEULgGw8PitNWmbvCJdRSamRcJl2g40l%2F3vy1D7KYYzqGL9V4kKRealmlEbXhyaKQUTcKpL6tdaoHO8HeSLic%2FKRoRAU7p1wSodv6Snl7e4MmKAJzf7QQUpBVOoWWs%2BXpE7TqRpeiTN2B9q0G74febRZ9ctXFkSxPHo%2ByI2I43nRfJg8tPV93rjxfbn07XuE5WMeAONny32FFH33Umx58dA%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=VPwFLPw4qOlufuhj6LZy4A%3D%3D.5hBtntg%2FsDfsLTg07PYJEhqt4%2FzESETfjtkN8pe6%2BU12oJSFLadh46fGH1z8yJa5D7VNOkV9Z9oE%2FtXXqttGEb3IZzVvqRH%2F8htZW5wwvJbGPbAIAi1jnyewoAe2IIM8%2Fij403i8f%2F0WTNuQj1E91%2FpkNa2ISVXlM3NkrGjNbpSalmawroJDLiMCtD7fypEksi4c7cXCdnEx7aoS92TlJQ%2FmFQPfeCrRAW9bfm4x7CejxFbeyHKVxTbBkRVzZhEzfMHFVSYlcMKh4MauaV%2FuRVUzuAXxHjRYia2ePYEAJ3Wc7c1bLyfl3cEwh%2BC2KwpZ65mQn0oeyCuJrax8gE5bnTqe%2FhQRO%2F%2BTTIpFtrwRxtE%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多原创文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
Maven项目为什么会产生NoClassDefFoundError的jar包冲突?
https://segmentfault.com/a/1190000023827502
2020-08-31T09:59:39+08:00
2020-08-31T09:59:39+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<blockquote>可以访问 <a href="https://link.segmentfault.com/?enc=tmTEZBcFRsfZpynEEbljBA%3D%3D.nfjwdn1TfPJRCTmxugtNn9ruaSqLJLA3Y0tHD4aWcJKkxqCX38ywes3nOPh6BIjDfzr2YUcXKIuDXBH2NKFa7lWGA2a6DVfcuAWqOHLIcWK7rUv%2FM0pqWIdTO5e0eZYbtLf2FzEygdfGV0wWZmTHlf2dwzMaOz0aemt3VfxuNbI9JTWw98JogWS91nLKxmy6" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h3>一. 先看看什么是Maven的传递性依赖</h3><h4>Maven的传递性依赖</h4><p>使用 Maven 工具,带给开发者最直观的好处就是:</p><ul><li>不用再去各个网站下载各种不同的jar包了,也不用考虑它们之间的依赖关系;</li><li>只需把项目依赖的jar包信息配置在pom文件中,它就会帮我们提供好一个jar包和该jar包所依赖的其它jar包。</li></ul><h4>举例解释</h4><p>比如,在项目中引入 easyexcel 后:</p><pre><code><dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.6</version>
</dependency>
</code></pre><p>我们使用 Maven Helper 插件,查询依赖树:<br><img src="/img/bVbL8JO" alt="image" title="image"></p><p>可以看到:</p><ul><li>easyexcel 依赖 cglib,cglib 又依赖 asm;</li><li>因此 cglib 和 asm 这两个包也会被引进来。</li></ul><p>通过查看 External Libraries 列表可以得到验证:<br><img src="/img/bVbL8JW" alt="image" title="image"></p><h3>|二. Maven的最短路径原则和最先声明原则</h3><p>思考一个问题,由于 Maven 的依赖性传递特性,如果有多个jar包同时都依赖一个jar包,且版本不一样的时候,如下图:<br><img src="/img/bVbL8Kh" alt="image" title="image"><br>那么 Maven 该使用jarC 1.1的包还是 jarC 1.2的包呢?</p><p>这时会遵循两个规则,分别是<strong>最短路径原则</strong>和<strong>最先声明原则</strong>。</p><h4>最短路径原则</h4><p>如上图:</p><ul><li>jarA -> jarC(1.1)</li><li>jarB -> jarD -> jarC(1.2)</li></ul><p>Maven 项目最终会使用 1.1 的 C 。</p><h4>最先声明原则</h4><p>那么当最短路径一样的时候该怎么办? <br>这时就需要最先声明原则,且最先声明原则就是在最短路径相同的时候生效,如下图所示:<br><img src="/img/bVbL8Kx" alt="image" title="image"></p><p>由上图可以看出,依赖路径如下:</p><ul><li>jarA -> jarC(1.2)</li><li>jarB -> jarC(1.1)</li></ul><p>现在最短路径是一样的,则就需要看谁先声明的。 <br>在工程的<code>pom.xml</code>里,可以看到是先引入的 jarA,那么根据规则,则<strong>jarC(1.2)</strong>生效,Maven 会使用<strong>jarC(1.2)</strong>来进行编译和打包。</p><h3>三. jar包冲突:NoClassDefFoundError</h3><p>软件开发通常都是会有不断的版本迭代,如果:</p><ul><li>在一个项目中同时会有多个jar包都依赖同一个jar包;</li><li>且都依赖的这个jar进行了版本升级,</li></ul><p>则此时工程就有可能会产生jar包冲突,下面将通过图解举例介绍。</p><h4>1. jarC版本升级丢弃了G.class</h4><p>假如jarC 版本升级,由1.1 版本 升级到 1.2后,<code>G.class</code>由于被修改而不存在了:</p><p><img src="/img/bVbL8KV" alt="image" title="image"></p><h4>2. 编译期间为什么不报错</h4><p>我们知道,Maven编译时会把业务代码编译成<code>class</code>文件。 <br>比如下面这种场景,业务代码<code>DemoController.java</code>类引入了jarA中的<code>A1.class</code>,maven 编译时会将业务代码<code>DemoController.java</code>类编译为<code>DemoController.class</code>:</p><p><img src="/img/bVbL8Le" alt="image" title="image"></p><p>备注:</p><ol><li>DemoController.class没有直接引用jarC中的G.class类,引入的A1.class在jarA中是存在的;</li><li>编译的目的只是把业务源代码编译成.class文件;</li><li>所以在项目在编译的时候不会报错。</li></ol><h4>3. 为什么运行期间报错:NoClassDefFoundError</h4><p>项目部署成功后,当接收到前端发起的一次请求,需要调用selectList接口查询数据时,会因为在项目依赖的jar包里找不到<code>G.class</code>类而报错,一个详细的流程如下图所示:<br><img src="/img/bVbL8LO" alt="image" title="image"></p><p>备注:</p><ol><li>由于要调用jarA中<code>A1.class</code>的<code>handleWrite</code>方法;</li><li>但是jarC 1.2版本已经没有<code>G.class</code>类;</li><li>所以在执行的时候会报错:<code>NoClassDefFoundError</code>。</li></ol><h3>四. jar包冲突:NoSuchMethodError</h3><h4>1. jarC版本升级丢弃了G.class的method1</h4><p>问题来了,jarC 版本升级,由1.1 版本 升级到 1.2后,<code>G.class</code>类的<code>begin</code>方法由于逻辑优化而不存在了,改为<code>begin1</code>:</p><p><img src="/img/bVbL8L2" alt="image" title="image"></p><h4>2. 编译期间为什么不报错</h4><p><img src="/img/bVbL8L7" alt="image" title="image"></p><p>备注:</p><ol><li>DemoController.class没有直接引用jarC中的<code>G.class</code>,而引入的<code>A1.class</code>在jarA中是存在的;</li><li>编译的目的只是把业务源代码编译成<code>class</code>文件;</li><li>所以在项目在编译的时候不会报错。</li></ol><h4>3. 运行期间为什么会报错:NoSuchMethodError</h4><p><img src="/img/bVbL8Mm" alt="image" title="image"><br>备注:</p><ol><li>由于要调用jarA中<code>A1.class</code>的<code>handleWrite</code>方法;</li><li>在执行jarA包里的<code>A1.class</code>的<code>handleWrite</code>方法时需要调用jarC包里的<code>begin</code>方法;</li><li>但是jarC 1.2版本里,<code>G.class</code>的<code>begin</code>方法已经不存在了;</li><li>因此在执行的时候会报错:<code>NoSuchMethodError</code>。</li></ol><h3>五. 总结</h3><ol><li>jar包冲突是java开发中经常会遇见且有的时候要耗时很久才能解决的问题;</li><li>Maven 项目中jar包冲突大部分都是这两种场景,只有在弄清楚jar包冲突产生的原因后,我们才能够在遇见问题的时候行之有效的去排查、解决;</li><li><p>可以借助相关工具去帮助排查jar冲突原因(后边会以实例介绍解决思路):</p><ul><li>如 idea自带的Show Dependencies;</li><li>idea安装插件 Maven Helper,通过查看 Dependency Analyzer;</li><li>通过命令行执行 mvn dependency:tree>temp/tree.txt,可以生成具有jar包依赖关系的文件。</li></ul></li></ol><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=QCB3ASYP84OQ41ULNoO%2Fnw%3D%3D.oaS3QuIVAZvoAixlj1NrDkgL1glu5pAkdBkQk1PT9j4fa%2Fw0niVVRgIVA%2Byhj9EhzNCVii%2FZ7akKQMRakaY4bdnNuxZdKzbRapDfkSX5TZrFveNAWUKpUZWX2FCTymNSuVhn0uhj2f4LG5n70mA%2Bt1oDUWwbFKqxA9LH%2FTA%2Bjkh%2Fk%2FFVFH5IZzmHx%2BqJBd4rSPcEtZlxINBJwuCOgAKvRB1VGaukaIotMwR6rxBlfFSkmRHJg2ffrYv501rDcUKFTDkewMjdppj4JjMP1mV8UmR7qSz7EKSxnGIbjpNdTyv3hDB7FTB0z74O11DIn1%2B%2FU%2FasEqmCPAFl0xE2ftaMsHWw9kAIlwOz%2BggM68cQxTo%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=oB7KWMj8Fos9a1%2B6O3bzMQ%3D%3D.UyBOfOl8qP5AGNH1Kl3cpFI4enDRPzOSHTtWI%2FgCXZisEhX6SVqJ7q2YLt0%2F1iCB2MDW%2F4kDvJVd4l3tdpSCo0Lcff9WJxmhaM%2FK%2FZQkfok%2F4X0iKiTuBK%2F3Q1PdmSiJHUFwHuPRQqJFiRv2t6FUoXPYcvg2kwYGU%2Bh4BK6goW8E3QFEA6bVxA45gi82Gl3vZkv7pNEncEfWk2634WhWSOSCrj55wS7%2BIkBjl81vZzSoMnC94Z3Jjn12w3WplQoMZXUi4hhXqOM%2FaIsyGRmDNJco%2Be0cAE2JKbWyQQMK07UsRloEVJtvQ2AI9lAP6672dmexYANaUtqTG8oyZzc9LCYOU9514OE0HwsgeU90RFA%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=uT7i6BOBs%2BvTFNd7Es%2BZxw%3D%3D.Lr51LUyb%2FCZ0XaNMAb0IxGdm76pWV4TpN3yXRPIBN%2Ff5Mn%2BesA4XT8B0JIACN%2B1O01hZKbNN5yj4D6OKAUhWf5GiGfq6qNierj%2BRHwYNalZaMhdaukNokiUkVK2ruvBTExRQ5P3AtKNiFB4NzSRzfYZH2fPyXBcG1iaow7Z7NSPQMV6sfIdOMVlGhidTOB%2FzunkXCU0jnrBy0IGCKJWhTDbS7nN4JSXlkK4I68AvVsHJiaU5BIGZnHRunCAIJcIbImJbVlRAhYPEkDZdgDr2ic26qj1YTKwi6ucn%2FkkAbfPwcUZSb8TsGrbZBkhlGweU3IZCIl0%2BU%2FmCj3GuuclTMWNYzzpnKwWRFX7KAFDgcrA%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=qGY2cFdE9%2BPaNRgwtAM8IA%3D%3D.M91r2jHfYHrQPu3WNX0VxWWw4dh%2FYMjyekT2GAn4sg%2FE7aZIpoHdabOtQ5jK%2BW%2FOq6vNcu9KTtISLGzHx9hQtl%2BlBRIb1QY6%2BMfR924GUUy14H8jYVHAfbOq%2BRDSALou7o0REj%2BSYbiWJ2v5cplr8xQlTcvIKKUQ7Y3lHz3Wwb%2BAlJO4PNEkjzhKCsSLkdn1cPwaxZZWxmgce%2FOjixfwORTW2xfuOKN850a7hgfwN098ZM8sUV2StQ1FKyNNnM83q7LML3XPLyCqJ6w6OssqJnVdh5T4JZokrIfhYExvx4iQ9atORDDpLfXZdFbUpmOozvwldzq1Q%2BV3KMQiYeNtHeq3fjGrlkygaE%2Bh3rOTyh4%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=aabwUKGFWfNFvn0f8d0jtg%3D%3D.EPbiIqxHVsdly6Zvg5j%2Bb39rGV0THK2NFMZvwuXDjWpzW7K0VIWd8MZH5DuyvskX9xzNf4Ujk1en4fNGzOI0vv10BpqpTYdNdmAio39pL%2B9Lg4bzVp9hkMXG6lOFt9pciUKj%2BfEo3gY5q8U1JYVAA6nCBvOfanW7nw5xzy4cYRijl0%2F2g%2BzNyGE%2Beume8pAlPWG8o5EPF2C4KHN7ybYySlIZRrS9d8o%2FMtGodfyC1HMkA8JYchwGR3dFad6mBYo%2BQn%2F6PxUBrn2L5j0V0jAE33YsRN09CDDDNxhbUAFCBvfYBM5wL5%2BQAajaV4RxDDwXeRpYz9lz82lu8T8c1nEwMP7TEup7QVGHYSBAyvPdU7M%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=Fhf%2BDJ3wVIGZEVFwtoN8WA%3D%3D.v7HuyJ12%2BXADS5Vi5wsGpKByJvSlwXi5tDK5ow3BQL42GxJ3p8SJ9UpQEdG%2Bi4d5ry56Dj17go9yh5P%2F9axPJwcuLlXSqIgBdyf0bqsyt3u5YY7fPL3VZaXkXsRi%2B66Q1WDLV9K0D6SFg%2BiK2urxVSH8waKnIV7NLidlDm%2FnErE7Pz8%2BPIjDaOSkETEa1HAaKTjnfvd8jKgjU%2BGa98hvQWpjordz5O1qDcNmcgJSa7FvCrOy%2F6H7x80nzVgJbY9k5z6Jnfmz1gyeMyZYXMfDT%2F%2FzzmHJbu5z55bmnLveLlhR3EXqMUmkVmQOHK2ujKNTIvK%2BmXI8tSLaNlyukK2E47%2FPCz6ZXi5GD7NXfExAUmk%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>欢迎大家关注微信公众号阅读更多文章</h3><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
移山(数据迁移平台)实时数据同步服务是如何保证消息的顺序性?
https://segmentfault.com/a/1190000023638162
2020-08-14T22:51:41+08:00
2020-08-14T22:51:41+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote><a href="https://link.segmentfault.com/?enc=ae22Q%2FvX8mMk8%2BJZ3Nq1XA%3D%3D.VtdvHNrPkQgAkXUp7OSmA4lLooFFPrQxeOboz49IE0L8%2FgEdWU44UNJUtkjp%2B8nWy1tK2Smbk1k9Pa%2FlaPJ79Ua7qvuxhoEi9t%2BTwZyd1KARXKTv1yPX1BH2sTiR7UT6wOiFQlKr3vpbGDR462Ctr5IzdhhwybiOZD%2FS6QS6X%2FkI66VsSoCxiUV%2FqJkaXLtux3psYxcBgFRnKXQXOyLMRZVupmd%2FjoD%2BxkxMwVKUdnpObSyKEJWjl%2BtFJrMEcuN%2BgdPtHkxrO6jtgwRYW6v4wtJ44IM%2BEbDT79891AgFV532KHMfxGr19NgmwMhATy3%2BHO8aswS7WeYolMUcuLQRgA%3D%3D" rel="nofollow">上一篇</a>介绍了移山(数据迁移平台)实时数据同步的整体架构; <br>本文主要介绍移山(数据迁移平台)实时数据同步是如何保证消息的顺序性。<br>可以访问 <a href="https://link.segmentfault.com/?enc=dcRTTRZw%2FpYHSRk%2BVOPLsw%3D%3D.bF6Ic7c1VByTGUTx%2F2KFeR0rErftjF3SMSWRNUYOcBzpTIVGt%2BDPuRyQmHp5HV9Xr0jSg%2Bi9yrwBc9XoFBK2zXqB73LZZ71PjbodyJlWx1oe3f3rGdAJCUPqdh%2BZ0lDoIZiOozT79%2FrYZcKE5G3yPeE01P9Jk44BTjhBe0g7wB4Kk%2BUDcxhwFm6fpwjw6Kbb" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h3>一. 什么是消息的顺序性?</h3><blockquote><ol><li>消息生产端将消息发送给同一个MQ服务器的同一个分区,并且按顺序发送;</li><li>消费消费端按照消息发送的顺序进行消费。</li></ol></blockquote><h3>二. 为什么要保证消息的顺序性?</h3><p>在某些业务功能场景下需要保证消息的发送和接收顺序是一致的,否则会影响数据的使用。</p><h4>需要保证消息有序的场景</h4><blockquote>移山的实时数据同步使用 <code>canal</code> 组件订阅MySQL数据库的日志,并将其投递至 kafka 中(想了解移山实时同步服务架构设计的可以点<a href="https://link.segmentfault.com/?enc=KByVyIxSrp0j9hHLRG4XCg%3D%3D.9Vhxn4kNlTwWDCzxxQNQK9jXWbAJCuanLBZeRTqW172rMSnkwFQM%2F6wO0bWbC2ZZLcAuo1pvdi6HwnLuj2DRlyBuJWB%2B3yIARJp9pUwdbFg6NNXSt2b7Kdg45k%2By74mPlPfilauDrvstGXIQIdU5gxQeO0%2BEgf%2B1%2BXJSFA5jbuL3hXXUt0q0OqnesAknT2DYq%2F9S38LR92hAf6tc1QI81EsXCS%2FBqimOZZzL1xpyEDVuoBNIRX%2F62zPTt9gHSHTmrKNHnBfLeF%2BQCXOZDjYSISZOEFkrpdfBQ6vhDt1Hky2%2FnIBf%2Fog0B2PTKA6rB%2B10RgcAGWI0tsqtpbBogr53KQ%3D%3D" rel="nofollow">这里</a>); <br>kafka 消费端再根据具体的数据使用场景去处理数据(存入 HBase、MySQL 或直接做实时分析); <br>由于binlog 本身是有序的,因此写入到mq之后也需要保障顺序。</blockquote><ol><li>假如现在移山创建了一个实时同步任务,然后订阅了一个业务数据库的订单表;</li><li>上游业务,向订单表里插入了一个订单,然后对该订单又做了一个更新操作,则 binlog 里会自动写入插入操作和更新操作的数据,这些数据会被 canal server 投递至 kafka broker 里面;</li><li>如果 kafka 消费端先消费到了更新日志,后消费到插入日志,则在往目标表里做操作时就会因为数据缺失导致发生异常。</li></ol><h3>三. 移山实时同步服务是怎么保证消息的顺序性</h3><p>实时同步服务消息处理整体流程如下:</p><p><img src="/img/bVbLlwe" alt="image" title="image"></p><p>我们主要通过以下两个方面去保障保证消息的顺序性。</p><h4>1. 将需要保证顺序的消息发送到同一个partition</h4><h5>1.1 kafka的同一个partition内的消息是有序的</h5><ul><li>kafka 的同一个 partition 用一个write ahead log组织, 是一个有序的队列,所以可以保证FIFO的顺序;</li><li>因此生产者按照一定的顺序发送消息,broker 就会按照这个顺序把消息写入 partition,消费者也会按照相同的顺序去读取消息;</li><li>kafka 的每一个 partition 不会同时被两个消费者实例消费,由此可以保证消息消费的顺序性。</li></ul><h5>1.2 控制同一key分发到同一partition</h5><p>要保证同一个订单的多次修改到达 kafka 里的顺序不能乱,可以在Producer 往 kafka 插入数据时,控制同一个key (可以采用订单主键key-hash算法来实现)发送到同一 partition,这样就能保证同一笔订单都会落到同一个 partition 内。</p><h5>1.3 canal 需要做的配置</h5><p>canal 目前支持的mq有<code>kafka/rocketmq</code>,本质上都是基于本地文件的方式来支持了分区级的顺序消息的能力。我们只需在配置 instance 的时候开启如下配置即可:</p><p><strong>1> canal.properties</strong></p><pre><code># leader节点会等待所有同步中的副本确认之后再确认这条记录是否发送完成
canal.mq.acks = all</code></pre><p>备注:</p><ul><li>这样只要至少有一个同步副本存在,记录就不会丢失。</li></ul><p><strong>2> instance.properties</strong></p><pre><code># 散列模式的分区数
canal.mq.partitionsNum=2
# 散列规则定义 库名.表名: 唯一主键,多个表之间用逗号分隔
canal.mq.partitionHash=test.lyf_canal_test:id</code></pre><p>备注:</p><ul><li>同一条数据的增删改操作 产生的 binlog 数据都会写到同一个分区内;</li><li><p>查看指定topic的指定分区的消息,可以使用如下命令:</p><pre><code>bin/kafka-console-consumer.sh --bootstrap-server serverlist --topic topicname --from-beginning --partition 0</code></pre></li></ul><h4>2. 通过日志时间戳和日志偏移量进行乱序处理</h4><p>将同一个订单数据通过指定key的方式发送到同一个 partition 可以解决大部分情况下的数据乱序问题。</p><h5>2.1 特殊场景</h5><p>对于一个有着先后顺序的消息A、B,正常情况下应该是A先发送完成后再发送B。但是在异常情况下:</p><ul><li>A发送失败了,B发送成功,而A由于重试机制在B发送完成之后重试发送成功了;</li><li>这时对于本身顺序为AB的消息顺序变成了BA。</li></ul><p>移山的实时同步服务会在将订阅到的数据存入HBase之前再加一层乱序处理 。</p><h5>2.2 binlog里的两个重要信息</h5><p>使用 <code>mysqlbinlog</code> 查看 binlog:</p><pre><code>/usr/bin/mysqlbinlog --base64-output=decode-rows -v /var/lib/mysql/mysql-bin.000001</code></pre><p>执行时间和偏移量:</p><p><img src="/img/bVbLlwf" alt="image" title="image"></p><p>备注:</p><ol><li>每条数据都会有执行时间和偏移量这两个重要信息,<strong>下边的校验逻辑核心正是借助了这两个值</strong>;</li><li>执行的sql 语句在 binlog 中是以base64编码格式存储的,如果想查看sql 语句,需要加上:<code>--base64-output=decode-rows -v</code> 参数来解码;</li><li><p>偏移量:</p><ul><li>Position 就代表 binlog 写到这个偏移量的地方,也就是写了这么多字节,即当前 binlog 文件的大小;</li><li>也就是说后写入数据的 Position 肯定比先写入数据的 Position 大,<strong>因此可以根据 Position 大小来判断消息的顺序。</strong></li></ul></li></ol><h4>3.消息乱序处理演示</h4><h5>3.1 在订阅表里插入一条数据,然后再做两次更新操作:</h5><pre><code>MariaDB [test]> insert into lyf_canal_test (name,status,content) values('demo1',1,'demo1 test');
Query OK, 1 row affected (0.00 sec)
MariaDB [test]> update lyf_canal_test set name = 'demo update' where id = 13;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [test]> update lyf_canal_test set name = 'demo update2',content='second update',status=2 where id = 13;
Query OK, 1 row affected (0.00 sec)</code></pre><h5>3.2 产生三条需要保证顺序的消息</h5><p>把<strong>插入,第一次更新,第二次更新</strong>这三次操作产生的 binlog 被 canal server 推送至 kafka 中的消息分别称为:<strong>消息A,消息B,消息C</strong>。</p><ul><li>消息A: <br><img src="/img/bVbLlwg" alt="image" title="image"></li><li>消息B: <br><img src="/img/bVbLlwh" alt="image" title="image"></li><li>消息C: <br><img src="/img/bVbLlwi" alt="image" title="image"></li></ul><h5>3.3 网络原因造成消息乱序</h5><p>假设由于不可知的网络原因:</p><ul><li>kafka broker收到的三条消息分别为:<strong>消息A,消息C,消息B</strong>;</li><li>则 kafka 消费端消费到的这三条消息先后顺序就是:<strong>消息A,消息C,消息B</strong></li><li>这样就造成了消息的乱序,因此<strong>订阅到的数据在存入目标表前必须得加乱序校验处理</strong>。</li></ul><h5>3.4 消息乱序处理逻辑</h5><p>我们利用HBase的特性,将数据主键做为目标表的 rowkey。当 kafka 消费端消费到数据时,乱序处理主要流程(摘自禧云数芯大数据平台技术白皮书)如下:<br><img src="/img/bVbLlwj" alt="image" title="image"></p><p>demo的三条消息处理流程如下: <br>1> 判断消息A 的主键id做为rowkey在hbase的目标表中不存在,则将消息A的数据直接插入HBase: <br><img src="/img/bVbLlwk" alt="image" title="image"></p><p>2> 消息C 的主键id做为rowkey,已经在目标表中存在,则这时需要拿消息C 的执行时间和表中存储的执行时间去判断:</p><ul><li>如果消息C 中的执行时间小于表中存储的执行时间,则证明消息C 是重复消息或乱序的消息,直接丢弃;</li><li>消息C 中的执行时间大于表中存储的执行时间,则直接更新表数据(本demo即符合该种场景): <br><img src="/img/bVbLlwn" alt="image" title="image"></li><li><p>消息C 中的执行时间等于表中存储的执行时间,则这时需要拿消息C 的偏移量和表中存储的偏移量去判断:</p><ul><li>消息C 中的偏移量小于表中存储的偏移量,则证明消息C 是重复消息,直接丢弃;</li><li>消息C 中的偏移量大于等于表中存储的偏移量,则直接更新表数据。</li></ul></li></ul><p>3> 消息B 的主键id做为rowkey,已经在目标表中存在,则这时需要拿消息B 的执行时间和表中存储的执行时间去判断:</p><ul><li>由于消息B中的执行时间小于表中存储的执行时间(即消息C 的执行时间),因此消息B 直接丢弃。</li></ul><h5>3.5 主要代码</h5><p>kafka 消费端将消费到的消息进行格式化处理和组装,并借助 <code>HBase-client API</code> 来完成对 HBase 表的操作。</p><p>1> 使用<code>Put</code>组装单行数据</p><pre><code>/**
* 包名: org.apache.hadoop.hbase.client.Put
* hbaseData 为从binlog订阅到的数据,通过循环,为目标HBase表
* 添加rowkey、列簇、列数据。
* 作用:用来对单个行执行加入操作。
*/
Put put = new Put(Bytes.toBytes(hbaseData.get("id")));
// hbaseData 为从binlog订阅到的数据,通过循环,为目标HBase表添加列簇和列
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes(mapKey), Bytes.toBytes(hbaseData.get(mapKey)));</code></pre><p>2> 使用 <code>checkAndMutate</code>,更新<code>HBase</code>表的数据</p><p>只有服务端对应rowkey的列数据与预期的值符合期望条件(大于、小于、等于)时,才会将put操作提交至服务端。</p><pre><code> // 如果 update_info(列族) execute_time(列) 不存在值就插入数据,如果存在则返回false
boolean res1 = table.checkAndMutate(Bytes.toBytes(hbaseData.get("id")), Bytes.toBytes("update_info")) .qualifier(Bytes.toBytes("execute_time")).ifNotExists().thenPut(put);
// 如果存在,则去比较执行时间
if (!res1) {
// 如果本次传递的执行时间大于HBase中的执行时间,则插入put
boolean res2 =table.checkAndPut(Bytes.toBytes(hbaseData.get("id")), Bytes.toBytes("update_info"),Bytes.toBytes("execute_time"), CompareFilter.CompareOp.GREATER, Bytes.toBytes(hbaseData.get("execute_time")),put);
// 执行时间相等时,则去比较偏移量,本次传递的值大于HBase中的值则插入put
if (!res2) {
boolean res3 = table.checkAndPut(Bytes.toBytes(hbaseData.get("id")),
Bytes.toBytes("update_info"), Bytes.toBytes("execute_position"), CompareFilter.CompareOp.GREATER, Bytes.toBytes(hbaseData.get("execute_position")),put);
}
}</code></pre><h3>四.总结</h3><ol><li>目前移山的实时同步服务,kafka 消费端是使用一个线程去消费数据;</li><li>如果将来有版本升级需求,将消费端改为多个线程去消费数据时,要考虑到多线程消费时有序的消息会被打乱这种情况的解决办法。</li></ol><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=TemIASk7%2Bn0gGJyqoVtjWw%3D%3D.0Bs%2BRl%2FcdyRIrZ9QiR2imRvaPDiaBLpYZEKmPVhJNust3ipQA7IBL7DC18jSQymn3IXrW9vT%2BIJWhffo8hJEBskE2yqoG7%2BBiSvBektJi9mtM8tql67WVsBIlFFjmPZKrtwVtzttoc4H2hgJp7VFTvOpSrU9cgpzzsB8zkFHusgen6W7LEW9j4kHe6JQYJpTVtd9u%2BVuaWX1O1qPLb5xVA9orQz0VcgtsHllIW0fkXELjEF4mIHCLEskQ1CuiAxjmcGP3R%2FsfFb6i7af%2B1lr4WXPRIvFQx1U2pqYIHgwOiA4JQtNzcRa37Tnw6nR8j0G1wDI8EoGgsOCH7Q6AixMSDOh22OeNhm4HYg75TV5QFQ%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=7MBgJq8hyKsbCIzu2gl2pw%3D%3D.yTqrF2m6nqjLuyglY5rxnwvH4dhEKYiuonWqNFbfymo3aSRF5Bn0wrkBSktFzerIxh4NFpWFRkeEBU7weTapOWs5xsFFxrJ1vNlFtn%2BBPK%2FVBs6a881PMOvlwC57HSihnb1woYkz0xPqAVUr%2B8yiQA5T8943QNuN3N7t%2B%2Bi6G07uH7acc%2FYwFtcdA6PlP3F10bvfJtW24yBILy8DewINoWK148VjBIfe1%2BpryiW3%2FyFkYQ1fPTb8HNyfbFZxvvTARbyu5tyB6b6uTEv3cTWdgtADIX5tv2J7foxSEzxOdS3Dc893litRNn7DwAT6rAz9goCwlRsE3qn9JgUazBwcLVe39i693PumZ5IqTQGtbBM%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=IjqrrY8qgKGz7L11gdCGUQ%3D%3D.wPRnTXCkO0qvxmIW0QOYMbOi53Asjmk8J5gvQu0MLKZu5bJhGTEipuirrqkrRc1HqO2FIPZzQ5YVho%2FdHEbwdzpGzIk01V7FxWl9CUZquyB%2Bk4SF%2BwoupF4vx%2FNE0nv7xGRYTWy7IHZPvxMi7GxkcJi2pjNu8kBpl5Q9Q9iIE%2FuYdoRRXqbO8s2q%2FVP1LMiQnBrsZY0W6xAKTXALEDWq%2BVsJBD3ZxSPeZEErx6JnZRDhgrVd947raKwWyOtelQpUc1aOHoPEKiyfFwCifbRZCP8k27PQpatmErENqKYmZEvehjH8Xsrp%2FEL5PUxQWqOPH%2FT7crqFrptJZp5BVsXbjmgFYce62hBDXURxsOzgdME%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=WVkcDXKTmhIJ7E2GJyJOuA%3D%3D.bBQggDEH721fVC7YONocLiEZJGJ0SnMbVuSSLgTIpvFTV3iklzfpnF4UdYjNTU661wV2Vux22TVsjPEryQf7eeHzjVSylF7zI2jgtBLA1w8dw1Hd0aUwV0udQ1YjFeXikgzVfdDT%2FPdt5pAgWzcMXNgHRppn91IQ80XgMcYfTBHTUDKGMFcgBYkS6avVIPVqTyhH1VqsXdfpxZa3Hha4S%2BaUMH7kksSZfRDEC5EhbS5a22d7XpW0BWIdZ35kJ%2Bd5f14YsOLf7QDuG%2FT0J64gb0R7YWEcIwj8WL4yT9nK4YluuodeJ3reTtS6x0VKBeJnS9AA7RsFP6Q1t6SFSeykaTy7eLi%2FFOooGqNuciDH8Ew%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=8ZQ4nAVSPX%2FOlZ863Lzy0Q%3D%3D.GT3d5skB6csZB%2Fy5adTLtgtVl3a50NzPQJVrRIRwrmIE%2Fn32eyRIaKGMkSgNn2BIk%2FPoG3wD%2BrEucpWYYqK%2B3oabMuQthAUHdq89i3mAq12Eg%2BzOi7oDFa4f4WBK68KaBxy%2Bb5KBRZU0OwCgLj6P0coz2Xq5Pp9ywqoqqgd%2F4MH9LqL%2Fv6CvH9grUI5ghz876PnGexfW9wehc09aWzXkTaYpC2TwKD9KN6zkgpvgBifXOR%2FaN7SWaAhn5Ot5XLQoIwSLQ9wonLyYYEDL6QeZonkseITP%2FvPXo4NR5vIUI8bOOv3%2FGTRnORsyMwHUSlJRCouinW37bTfigsE3oqDM8etrd9swCfX7J7j0EeTooEU%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=cOaYoNucRihMOsyFmsWmqg%3D%3D.OATDDtXxZGEluQEzMlnEpxft4ma%2BcX4RMvCE8GosoxS53DTdJlVloVxRPzCyvn526WdRdychzy1YW0l0GpLrnRc6yNRwAsx6ari%2FfJ9QKgSv5FeHmfTIBrXrQZe6GOUOvYAt58AhjJ9dfCCgAv1mKiH3INmLx7AVGyJr9P66YOlWXNqzuVs3%2BrBoRldLDXz7G4ZBlT5pu6uBjgf07K7LvmL7qfJP1gdPhJrAnrk%2FDMyg%2FQr6LNs8VVpZb8Ib7UAdbqnTZaAVlxIpBEf%2BLtC3UT%2BkK0OiBjahe0nwLmwwpzBEA3Ntk2XqHcUX%2FbfixufvlnhfHG9QJC5CXJmMzmL3Pc0cgYHzEhKsADjEh4bz5vY%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
移山(数据迁移平台)实时数据同步服务的架构设计
https://segmentfault.com/a/1190000023638066
2020-08-14T22:37:27+08:00
2020-08-14T22:37:27+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote><ol><li>移山是禧云自研的数据迁移平台,包含异构数据源的迁移、实时数据同步等服务。有兴趣的可以看这里了解 <a href="https://link.segmentfault.com/?enc=7aUMFFChbhWeuhUZgm92zQ%3D%3D.S2te2vclhP8Nq2Lpwf7MG3sHeXq51vOtE7au%2FA2BfRZ0cg97dkJFqjOzDFeKwJZO5xpWs8p5ecnQFuZu8OfN%2Bj445zOHWbtVosJmJRl%2F94PgNxQ9Xxyqo5hA8SXa1MQmrfvAWILYWn5BM1r4PXjgU11DvLmHsJ2Vo1CvooAnXH%2BUgC%2BPXjoAPDhWvuplWiNuxYnoqzJQU6dX%2FWhoZOP%2BQEX85l%2B2U%2F5oubu34wAvhmMnbKlD6B%2FSYjq2VYXfKnP5fgDzs9uwCKJHR0Ozm%2B%2B1lOF7kPALiXrLVc%2FVRCc%2BXMf6cLaG9m021dvoRwd3qolmcx0yCzMGmwGlfDPFL8yVTA%3D%3D" rel="nofollow">在移山中怎么实现异构数据源的迁移</a>;</li><li>本文主要介绍移山实时数据同步服务产生的背景以及整体架构设计。</li><li>可以访问 <a href="https://link.segmentfault.com/?enc=UlikmU68GdUOu9eSO8DTaw%3D%3D.%2ByjMOLLX1FxRcQ%2Bqh9tLEt6gWIjDMfYzc4K36Z9Fruc9hNhvs4llJh5NbwtyIC9tO50h0iLhUVw6D7j6jSgsV0VxkNiE2Xlw1IDRbwLzG8KIirV%2F3qBRh6s5BsjW3uiJTe9jVCDtAnf29pVVU4fUCJ%2BTYMKTik%2F1Q7ycNvtlDi%2BEEktBoScpUOOkAK325ima" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</li></ol></blockquote><h3>一. 移山实时数据同步服务产生背景</h3><ul><li>禧云各个子公司业务系统基本都是以 MySQL 为主;</li><li><p>做为数据支持部门,需要订阅这些业务数据做为数据仓库的数据源,来进行下游的数据分析。比如:</p><ul><li>各种离线数据 T+1 报表展示;</li><li>实时数据大屏展示等。</li></ul></li></ul><p><strong>微信小程序实时数据指标展示</strong></p><p>像这种常见的实时数据指标大屏展示,背后可能就用到实时数据同步服务技术栈。</p><h3>二. 移山实时数据同步服务使用canal中间件</h3><h4>1. 使用场景符合</h4><p>它可以对 MySQL 数据库增量日志解析,提供增量数据订阅和消费,完全符合我们的使用场景。</p><h4>2. 支持将订阅到的数据投递到kafka</h4><p>canal 1.1.1版本之后,server端可以通过简单的配置就能将订阅到的数据投递到MQ中,目前支持的MQ有kafka、RocketMQ,替代老版本中必须通过手动编码投递的方式。</p><p>移山的实时数据同步服务使用的MQ为kafka,以下为主要配置:</p><h5>修改canal.properties中配置</h5><pre><code># 这里写上当前canal server所在机器的ip
canal.ip = 10.200.*.109
# register ip to zookeeper(这里写上当前canal server所在机器的ip)
canal.register.ip = 10.200.*.109
# 指定注册的zk集群地址
canal.zkServers =10.200.*.109:2181,10.200.*.110:2181
# tcp, kafka, RocketMQ(设置serverMode模式,这个配置非常关键,我们设置为kafka)
canal.serverMode = kafka
# 这个demo就是conf目录里的实例
canal.destinations = demo
# HA模式必须使用该xml,需要将相关数据写入zookeeper,保证数据集群共享
canal.instance.global.spring.xml = classpath:spring/default-instance.xml
# 这里设置 kafka集群地址(其它关于mq的配置参数可以根据实际情况设置)
canal.mq.servers = 10.200.*.108:9092,10.200.*.111:9092</code></pre><h5>修改demo.properties中配置</h5><pre><code># canal伪装的MySQL slave的编号,不能与MySQL数据库和其他的slave重复
# canal.instance.MySQL.slaveId=1003
# 按需修改成自己的数据库信息
# position info(需要订阅的MySQL数据库地址)
canal.instance.master.address=10.200.*.109:3306
# 这里配置要订阅的数据库,数据库的用户名和密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.defaultDatabaseName =
# 设置要订阅的topic名称
canal.mq.topic=demo
# 设置订阅散列模式的分区数
canal.mq.partitionsNum=3</code></pre><p><strong>备注</strong></p><ul><li>更多关于mq的配置参数解释,可以访问这里:<a href="https://link.segmentfault.com/?enc=%2F2IZKNngLV%2B6UGe00lxP9g%3D%3D.%2BL%2FBQCdVt8nUo0aqdDrXUHyrKJ2Y%2F%2BkgRc5ZYDiO8iaX3FwtJzKnRUX53aKtpJ0nlmyErSofqeLlcodPXoq8XuIH4UNyDWC4bYs7AUAj%2F8U%3D" rel="nofollow">https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart</a></li><li>多个 canal server 除了 ip 和 MySQL.slaveId 设置不同外,其它都应该保持相同的配置。</li></ul><h4>3.支持带cluster模式的客户端链接,保障服务高可用</h4><ul><li>客户端可以直接指定zookeeper地址、instance name,canal client 会自动从zookeeper中的running节点,获取当前canal server服务的工作节点,然后与其建立链接;</li><li>其它canal server节点则做为Standby状态,如果当前active节点发生故障,可以自动完成failover切换。</li></ul><p>对canal 的高可用(HA机制)想了解更多,可以查看这篇文章。</p><h3>三. 移山实时数据同步流程图</h3><p>实时数据同步服务流程图(摘自《禧云数芯大数据平台技术白皮书》)如下:</p><p><img src="/img/bVbLluQ" alt="image" title="image"></p><p><strong>总结</strong></p><ul><li>canal server 订阅业务系统的 MySQL 数据库产生的 bin log;</li><li>canal server 将订阅到的 bin log 投递至 kafka 指定的topic里;</li><li>kafka 消费端拿到消息,根据实际的数据使用场景,将数据再写入 Hbase 或 MySQL,或直接做实时分析。</li></ul><h3>四. 创建一个实时数据同步任务的主要步骤</h3><p>以创建一个数据订阅类型为 HBase 的数据同步任务为例,主要步骤如下:</p><ol><li>创建kafka的topic;</li><li>进入到canal server的bin目录,拷贝example整个目录,生成一个新的实例目录;</li><li><p>手动修改新实例的配置文件,配置以下主要参数:</p><ul><li>3.1 设置slaveId,不能与已经成功运行的实例设置的slaveId值重复;</li><li>3.2 要订阅的数据库所在的机器地址和端口号;</li><li>3.3 要订阅的数据库名称;</li><li>3.4 要订阅的表;</li><li>3.5 要订阅的数据库用户名、密码;</li><li>3.6 配置向kafka发送消息的topic;</li><li>3.7 配置kafka的partition等;</li></ul></li><li>重启canal server;</li><li>查看实例的启动日志,判断实例是否启动成功。</li></ol><h4>存在的问题</h4><p>由于缺乏 WebUI 的支撑,因此会存在以下问题:</p><ul><li>流程复杂:如上这些一系列的操作都是依靠脚本的方式配置完成,配置过程繁琐,数据开发者很容易在某个环节上发生遗漏、出错;</li><li>不利于任务的统一管理:比如谁开发的任务可能只有写代码的这个人比较熟悉;</li><li>不方便查看任务的运行情况:比如已消费消息数、延迟消息数;</li><li>不利于排查问题:查看任务的执行情况只能登陆 canal server 所在服务器去查看任务所属实例的启动日志,如果遇到错误时,不能够快速及时的排查问题。</li></ul><h4>怎么解决问题</h4><p><strong>为了解决上面提到的这些问题,我们开发了移山的实时数据同步服务。</strong></p><p><strong>后话</strong></p><ul><li>在最新的稳定版:canal 1.1.4版本,迎来最重要的 WebUI 能力;</li><li>instance 可以通过 WebUI 来创建,但是有部分使用者反馈,instance的启动会有不稳定的情况出现,我们期待稳定版本可以快速发布。</li></ul><h3>五. 移山实时数据同步服务整体架构</h3><h4>1. 所需集群环境</h4><h5>zookeeper集群</h5><p>为什么要用zookeeper集群,可以看这篇文章:阿里canal是怎么通过zookeeper实现HA机制的?</p><h5>kafka集群</h5><ul><li>kafka 具有高吞吐量、内置的分区、备份冗余分布式等特点,为大规模消息处理提供了一种很好的解决方案;</li><li>前面已经提到过 canal 中间件通过简单的配置即可支持将订阅到的数据直接投递到 kafka中。</li></ul><h5>canal server集群</h5><p>为保障数据订阅服务的稳定性,我们需要借助 canal 的HA机制,实现故障自动转移,保障服务高可用,因此我们需要部署多个 canal server。</p><h5>hbase集群</h5><ul><li>数据湖在禧云的实践是存储集团各子公司、ISV各种各样原始数据的大型仓库,其中的数据可供存取、处理、分析和传输;</li><li>数据湖的技术解决方案,我们选择的是 Apache HBase。</li></ul><h4>2. 移山实时数据同步架构设计</h4><h5>架构图</h5><p><img src="/img/bVbLlvc" alt="image" title="image"></p><h5>canal server端</h5><ul><li>在canal server 的多个节点上手工创建、运行instance;</li></ul><p>备注:</p><ul><li>我们在配置instance相关参数时,不指定具体的数据库,这样该instance可以订阅到该MySQL节点上的所有数据库,然后在移山创建同步任务时再指定具体要订阅的表。</li></ul><h5>移山</h5><p>前端采用 Vue.js + Element UI,后端使用 SpringBoot 开发:</p><ul><li><p>前端工程</p><ul><li>负责提供创建实时同步任务所需的 WebUI;</li><li>提供丰富的任务运行监控功能。</li></ul></li><li><p>后端工程</p><ul><li>负责前端工程的数据接口,会记录目标表(实时同步任务订阅到的数据最终存储的目的地)的各种元数据信息。</li></ul></li></ul><p>备注:</p><ul><li>如果要创建的同步任务将数据存储至MySQL,则需要提前人工干预创建MySQL表(MySQL数据库由DBA统一管理);</li><li>如果要创建的同步任务将数据存储至Hbase,则在移山创建任务时,由后台自动创建Hbase表。</li></ul><h5>变形金刚</h5><p>处理订阅数据存储的java工程,分为三个可单独部署的模块:</p><ul><li><p>canal client服务</p><ul><li>canal 客户端,从 canal server 拿到数据,并将数据投递至kafka。</li></ul></li><li><p>kafkaToHbase服务</p><ul><li>kafka 消费端,负责将接收的消息进行转化处理,处理后的数据存储至Hbase。</li></ul></li><li><p>kafkaToMySQL服务</p><ul><li>kafka 消费端,负责将接收的消息进行转化处理,处理后的数据存储至MySQL。</li></ul></li></ul><p>备注:</p><ul><li>以上三个服务均支持命令行启动、停止。</li></ul><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=P5QgUjs2DPsTemciUNry%2FA%3D%3D.22F5q7U9XsGCadWd6eIQH0sHeP6Kb6Zh1ZAdT%2BFBCYj69gbRFXEt9ufb7qvuUOGveoNwh3wASUmWaht8OJGPT1sKZMWKpqUvMkvbLm%2Bq3J8ArKD0tEVD1ugspFw3HFc4uuFxb8kyH%2FZk%2BpG5cvdL6dL4Ji9y3ohPrDilSH4RmBbm4UzWl%2FtGZjGlO8eVxs1Qz9UBHDhjXnLjm7cgdhemxAqvF5gpTbbn3bkiy0A%2B3uvjH384t5uNiMVv%2FeDK03m%2Ff%2F8%2FkDhzcaQPxVwtmihiTwpkz9hrPzcTjz5oVEemnAbBVEUFROgPbLBqPdizrTsD7NyzwB%2F6jK4UBhy%2Fpl%2FzDCKq92zClO0oF7DDgDGrOng%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=ERiR6uP1VEzMd4VC177RXg%3D%3D.%2Bgr2EGZ%2FM%2B9IqKmJHD0kWqG2cWP6y%2BGe6AxggSQhAp2ek5Vl42W%2FSrOxCAPREeaaSzD%2B7jKLDjse0EspDXpyChbpRPi0Wua%2FH0481JHZ4PpQdSYdNNX4XK780HSML6BXKTU%2B14tR1XIbYCw8FzTM8sbRoR34VL8JAj16ghPj6IJkBi1mxQ5Sk2LcUPoLSl9Piv1AmAU8UN8ZS0rSOD5q%2BRqkJLzlZj%2BdqJ%2BaHZE3s4qJ7C4j0enkuV2o%2BzWfMGjaIV7kZ9lqaNjJH3O8KAxAKL9E7WYYH2phvgO3yv2jzmw9XO7OyORVFzDt39Dk6ekzHyM%2BZccNQ097K8f6LLPOmsUsWO533n1yanPIgIu32Us%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=W0j2P0u5jQPbE7GwvBuIRw%3D%3D.yDdIE3dAnYqV%2BdYiikdXKn8P6Gjx8hhoiQTOWQLdmDEyd0BuEXSB%2Bhx09f9acA01zj9upX6fOpWzur%2B5Tzo6QAMYhxaxWq9SsSDsVEFeWnKHEMyPotNgsV5S7rLYX%2FyMy4deT0S0nXQx91QIX6A7FsWlYgWVP2DfttbrnwWVnC32aSmqa5RUEG%2B2Hx6ezb3caDlCJoeYbGAwFYCr5zxdKiAzx1uzgIEuQmgOqpKCOeUJDTffMN9sKm%2BTXGs3GMGaEWRxEYIkv18%2BVv9IR3cRdib8UscWTE4UUGUsN3ATITZxk0cw9F00DcbW8xScv9TCQNQw68EMbjxg44IwUBct%2F%2BGe4C7OzVo5mfbqc4X3biY%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=YDpZ%2FQBH7P8KNWuCVR3LSg%3D%3D.IZl1QPRHSwWjz7UJxZgVq0zcTbvgRTflp4sw8lG0B33C%2BrKh7NeCxtEhGOEkj5t51%2FVry7qo8lSCi3oyOjp2s21uAfN9v0dUeUUa03JPJ3PRzTuBttdOimbHQODMeepT8YonPIH3gWMraaXQj%2FoLuVpz%2BoS%2BdZcllbAlfJ3lAt%2Bhm6P%2F2zZeta0g2320cC6ntbWQRP06%2B6tGJmYBc3hbu4f4q2VMO77LY3jfku1pOP7BDspDo0sfxBuV%2FR6xzUpG%2FEsKLxijj17NSM0KKBxKCVOYVX4OJOtWZJrN%2F4e6mUC%2F8aTJ3uVC%2F7GcB4xTCCFc5oSMmXT2923kXz%2B6dPaAbnLHBJVoziT7%2BWIhZsjn6rw%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=7nJ48ZirCatCL1bip7BcVw%3D%3D.B6e8D4HKjofY0gaY4GzE9xim%2FBrL4uPgRru7t3ZiJvrEVpDlzoUnC%2FFo3DPkFWsl0%2F8N9fdXxhpWJ5lqWQSJaUsphLS2Qrb4m73FzBl96agni5AOLeaW514VSkORUabLEFyU80H5NrcmMwpXsCuV3scRSgXLmodSyQ%2F%2FROe6PD4PP7ds46CzXWk8JRIiLVYSrZSq1z7C4qsJ1%2BkeI2BzO8gH%2Fcm1iJ5yxaQ2d0mrkv7IAj51qCy0K%2BAOu0FMaj7IE8okF1BAJssfJhSmGgBsSPrNiZQkfZc0UM9gvLiKB3655vu%2FoaIap%2Fa8HDbTT7N%2FIfDjWfQCNIGPwnqF2SorJBoWXb4ConY%2BS1fdo6aqnj0%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=9uAAuFh%2BVjFOOobksGZ3uw%3D%3D.Lbgd4p2ekQQ5GAOIDGiRVCSoOeOmFBVlARdzLIhU80KDZygnIf7ky1bbBr5IcCmIfgut%2FFVfy4sH6Y%2B7H3LbgFVetN7wbiFBjUMI4e38vrHTpXFgPmvbYjzsiVLTT37xk9i6QjiQxnrbO6Sp0mM9rlfnqstBfMgsoG6vfirkZIxX1q9n5TjqllHJSi2WtKskK8CalrX9qxqiE9sj0UoDaUyecPlVWL47YHrXyaW77gO%2Fhl6AEZdBaZeVdD3pqojDFkAmbYoMXbUSLnqSNVQ9Ti1bEmenrVULaG11ob7adYE6mRzPrbRrUkqjO%2FMEmgGG59th5N3tAEcvexO2sJ5xSwW%2FQ%2Bi6q8NyXDdDzWJ7PbE%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
阿里canal是怎么通过zookeeper实现HA机制的?
https://segmentfault.com/a/1190000023297973
2020-07-20T09:58:04+08:00
2020-07-20T09:58:04+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote>可以访问 <a href="https://link.segmentfault.com/?enc=2MjUHNilRc4cNgKvz3K6tA%3D%3D.n1dMwXKZBw5%2Fo%2F4UrG%2F7nvfccBKkmfhSwihs%2BQNpA4vc5r03sHgg2vLd15Hz2Hj9P13SDop4jDv9jZr%2FrPEi74JZ%2BwHcnvy3chLH9Dq1sbVwvv8yR3LFHGpOh5SsUBQFgQV0hIg1V2xGK%2BrLSK3DREAf5IcCI8bMIKUYhfjIrXO5csn7rcXW2P8Du%2BbgrgEH" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h3>一. 阿里canal工作原理</h3><p>canal 是阿里的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB)。</p><h4>MySQL主备复制原理</h4><p><img src="/img/bVIVV0" alt="mysql主备复制原理.jpeg" title="mysql主备复制原理.jpeg"></p><ol><li>Master 将变更写入binlog日志;</li><li>Slave 的 I/O thread 会去请求 Master 的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中;</li><li>Slave 的 SQL thread 会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL语句,从而使从服务器和主服务器的数据保持一致。</li></ol><h5>更多</h5><p>MySQL 的 Binary Log 介绍</p><ul><li><a href="https://link.segmentfault.com/?enc=PR9yGBLaNTztDUeF9osbig%3D%3D.ot9guO5jtd7Crl30LfJGKU6e0E8gD9ydwE34TDdxWpadMoIWa2%2BjbUS5v7KonyPH6bi5YJiH69L10XnTEPF7Ag%3D%3D" rel="nofollow">http://dev.mysql.com/doc/refman/5.5/en/binary-log.html</a></li><li><a href="https://link.segmentfault.com/?enc=bt1BemuLqVRD5rsTIxj0ug%3D%3D.hCMH7dMtXa86mkdWufYUEfvAjzA0jn8ufwU9%2B28D3ppJxYkD6IWHipRkWDLHFQlwerM%2Bsimm2X30kAbMYFtpMKQBxmGEdnKjqL4YR75%2B2cY%3D" rel="nofollow">http://www.taobaodba.com/html/474\_mysqls-binary-log\_details.html</a></li></ul><h4>canal的工作原理</h4><p><img src="/img/bVTmbI" alt="canal工作原理.jpeg" title="canal工作原理.jpeg"></p><ol><li>canal 模拟 mysql slave 的交互协议,伪装自己为 mysql slave,向 mysql master发送 dump 协议;</li><li>mysql master 收到 dump 请求,开始推送binary log给 slave(也就是canal)</li><li>canal 解析 binary log对象(原始为byte流)。</li></ol><h5>更多</h5><p>关于 canal的详细介绍,可以访问官网:<a href="https://link.segmentfault.com/?enc=xp%2BqkGGQQuIPyEJfv2oyDQ%3D%3D.itt5KJEtv2t8CJ9s%2BGvOWp%2BlOdF4W3vch3EcmpUjZCPExyWg6WK2jMvBXFJtpz%2Fx" rel="nofollow">https://github.com/alibaba/canal</a></p><h3>二. 阿里canal的HA机制</h3><h4>1. 什么是HA机制</h4><blockquote>所谓HA(High Available),即高可用(7*24小时不中断服务)。</blockquote><h4>2. 单点故障</h4><p>实现高可用最关键的策略是消除单点故障。比如Hadoop2.0之前,在HDFS集群中NameNode存在单点故障:</p><ol><li>NameNode机器发生意外,如宕机,集群将无法使用;</li><li>NameNode机器需要升级,包括软件、硬件升级,此时集群也将无法使用。</li></ol><h4>3. Hadoop2.0引入HA机制</h4><p>通过配置Active/Standby两个NameNodes实现在集群中对NameNode的热备来解决上述问题。</p><ul><li>有一台节点是Active模式,也就是工作模式,其它的节点是 Standby(备用模式);</li><li>干活的(Active模式的节点)如果挂了,就从备用模式的节点中选出一台顶上去。</li></ul><p><strong>更多</strong> <br>关于 Hadoop 的HA 机制的详细介绍,可以访问:<a href="https://link.segmentfault.com/?enc=B3WAEsQna4fNI19HMBy0Pg%3D%3D.YMw%2F2RKEE4pxnes4oUDJdkdYnC7YGMzD1NI9uYXkbJT8KJaSKUPFlnhyYJ0Sg9pAK5BaARkRDzOe%2B7lI0sA4IQ%3D%3D" rel="nofollow">https://blog.csdn.net/pengjunlee/article/details/81583052</a></p><h4>4. zookeeper的watcher和EPHEMERAL节点</h4><h5>zookeeper的watcher</h5><p>watcher 机制涉及到客户端与服务器(注意,不止一个机器,一般是集群)的两者数据通信与消息通信:<br><img src="/img/bVbJUWP" alt="zk-watcher.jpg" title="zk-watcher.jpg"></p><h5>更多</h5><p>关于watcher 的详细介绍,可以访问:<a href="https://link.segmentfault.com/?enc=vJabQb0LJr8wfbY6SIxJTA%3D%3D.ZIe%2FgfNplHXj%2FmmKTRdMl5cWK4z6Z97CU5koLrQigQLVAvTARGV0pHt4Xi7vm3TQ" rel="nofollow">https://www.jianshu.com/p/4c071e963f18</a></p><h5>zookeeper的节点类型</h5><p>EPHEMERAL节点是 zookeeper的临时节点,临时节点与session生命周期绑定,<strong>客户端会话失效后临时节点会自动清除</strong>。</p><h5>更多</h5><p>关于 zookeeper EPHEMERAL节点的详细介绍,可以访问: <br><a href="https://link.segmentfault.com/?enc=XmIW9XYz5SEWcMkjP729xQ%3D%3D.yrsNpgKoMcJf2pQZIl%2FGT0BHmedU9bWn3RS1yj08h8X9CqpLz3NbcPljAbkL052gI51vYmgUpSHUkyT7fyK5bA%3D%3D" rel="nofollow">https://blog.csdn.net/randompeople/article/details/70500076</a></p><h4>5. canal的 HA机制</h4><p>整个 HA 机制的控制主要是依赖了zookeeper的上述两个特性:watcher、EPHEMERAL节点。canal的 HA 机制实现分为两部分,canal server 和 canal client分别有对应的实现。<br><img src="/img/bVbJUWV" alt="canal的HA机制.jpg" title="canal的HA机制.jpg"></p><h5>canal server实现流程</h5><ol><li>canal server 要启动某个 canal instance 时都先向 zookeeper 进行一次尝试启动判断 (实现:创建 EPHEMERAL 节点,谁创建成功就允许谁启动);</li><li>创建 zookeeper 节点成功后,对应的 canal server 就启动对应的 canal instance,没有创建成功的 canal instance 就会处于 standby 状态;</li><li>一旦 zookeeper 发现 canal server A 创建的节点消失后,立即通知其他的 canal server 再次进行步骤1的操作,重新选出一个 canal server 启动instance;</li><li>canal client 每次进行connect时,会首先向 zookeeper 询问当前是谁启动了canal instance,然后和其建立链接,一旦链接不可用,会重新尝试connect。</li></ol><p><strong>注意</strong> <br>为了减少对mysql dump的请求,不同server上的instance要求同一时间只能有一个处于running,其他的处于standby状态。</p><h5>canal client实现流程</h5><ul><li>canal client 的方式和 canal server 方式类似,也是利用 zookeeper 的抢占EPHEMERAL 节点的方式进行控制</li><li><strong>为了保证有序性,一份 instance 同一时间只能由一个 canal client 进行get/ack/rollback操作,否则客户端接收无法保证有序</strong>。</li></ul><h3>三. canal的HA集群模式部署</h3><h4>1. canal下载地址</h4><ul><li>下载地址: <a href="https://link.segmentfault.com/?enc=PX5TjFn74F6zpfpkX9c7sA%3D%3D.2N9LnZ0z4UrdqrcN4xrSeKaxn1qSnSe2ZdlIxPhjln4k4eTXIotSaKmwaoVncCsi" rel="nofollow">https://github.com/alibaba/canal/releases</a> <br>我使用的是 1.1.4 稳定版本。</li></ul><h4>2. mysql开启binlog</h4><p>MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下:</p><pre><code>[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复</code></pre><h4>3. mysql授权账号权限</h4><p>授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant:</p><pre><code>CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;</code></pre><p><strong>备注</strong> <br>可以通过在 mysql 终端中执行以下命令判断配置是否生效:</p><pre><code>show variables like 'log_bin';
show variables like 'binlog_format';</code></pre><h4>4. 修改配置</h4><p>canal 服务的部署其实特别简单,解压之后只需修改<code>canal.properties、instance.properties</code>这两个配置两个文件即可。</p><h5>修改canal.properties中配置</h5><pre><code>//指定注册的zk集群地址
canal.zkServers =127.0.0.1:2181,127.0.0.2:2181
//HA模式必须使用该xml,需要将相关数据写入zookeeper,保证数据集群共享
canal.instance.global.spring.xml = classpath:spring/default-instance.xml
// 这个demo就是conf目录里的实例,如果要建别的实例'test'就建个test目录,把example里面的instance.properties文件拷贝到test的实例目录下就好了,然后在这里的配置就是canal.destinations = demo,test
canal.destinations = demo </code></pre><h5>修改demo.properties中配置</h5><pre><code>## mysql serverId , v1.0.26+ will autoGen
# canal伪装的mysql slave的编号,不能与mysql数据库和其他的slave重复
canal.instance.mysql.slaveId=1003
# 按需修改成自己的数据库信息
# position info
canal.instance.master.address=10.200.*.109:3306
# username/password 数据库的用户名和密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.defaultDatabaseName = test</code></pre><h4>5. 详细步骤</h4><p>canal 的HA集群模式部署详细步骤,可以访问:<a href="https://link.segmentfault.com/?enc=gPBI7p3BW0iFYFfr0VvrzA%3D%3D.DcfZGDEEhYAEbOFcJQal6%2FZGSOoPirTkd885vWC%2B3E5%2F2qZzMRsJYBZjM5z8usK02oPzgkM38ujEeA2sPKcwUQ%3D%3D" rel="nofollow">https://blog.csdn.net/XDSXHDYY/article/details/97825508</a></p><h3>四. 通过实例感受zookeeper在HA机制中的作用</h3><p>说了这么多,下面通过一个实例演示来感受一下 zookeeper 到底起了什么作用。 <br>新建一个叫 'demo' 的实例:</p><pre><code>$ cd /data/application/canal.deployer-1.1.4/conf
$ cp -r example demo</code></pre><p>配置文件的修改可以参考<strong>第三部分</strong>贴出的配置。</p><h4>环境</h4><h5>1. 版本</h5><pre><code>canal(1.1.4) + Zookeeper(3.4.6-78--1) </code></pre><h5>2. canal集群</h5><p>canal server部署在两台机器上:</p><pre><code>10.200.*.108 和 10.200.*.109</code></pre><h5>3. zookeeper 集群</h5><p>zookeeper部署在三台机器上:</p><pre><code>10.200.*.109:2181,10.200.*.110:2181,10.200.*.111:2181</code></pre><h4>从 zookeeper 中查看active节点信息</h4><h5>1. 使用 zkClient链接zookeeper server</h5><pre><code>$ ./zkCli.sh -server localhost:2181</code></pre><h5>2. 查看 zookeeper集群中,canal server的 active节点信息</h5><pre><code>[zk: localhost:2181(CONNECTED) 3] get /otter/canal/destinations/demo/running</code></pre><p><img src="/img/bVbJUZA" alt="zk1.png" title="zk1.png"></p><p>由于我还没有启动任何一台 canal server,所以查询的节点不存在。</p><h4>分别启动多台机器上的canal server</h4><p>分别登陆 108 和 109 两台机器,cd 到 canal 所在目录,命令行启动服务:</p><pre><code>cd /data/application/canal.deployer-1.1.4
sh bin/startup.sh</code></pre><h4>现象一:只会有一个canal server的demo(instance名称)处于active状态</h4><h5>1. 继续查看zookeeper集群中,canal server的 active节点信息</h5><p><img src="/img/bVbJU0l" alt="zk2.png" title="zk2.png"></p><p>从图中可以看出:</p><ul><li>当前工作的节点为:10.200.*.109:11111。</li></ul><h5>2. 分别查看canal server的 启动日志</h5><p>通过去 109 和 108 这两台机器上找到 canal server 启动日志,去验证一下上面的结论。</p><ul><li>查看 109 机器的 canal server 启动日志:</li></ul><pre><code>[root@dc23x109-jiqi canal.deployer-1.1.4]# tail -f logs/demo/demo.log</code></pre><ul><li>查看 108 机器的 canal server 启动日志:<br><img src="/img/bVbJU0N" alt="log2.png" title="log2.png"></li></ul><p>从图中可以看出:</p><ul><li>该log目录下面没有 demo目录,也就是说 108 机器上的 canal server 压根没有产生启动日志。</li></ul><h5>3. 结论</h5><p>通过从 zookeeper 中查看节点信息和分别从两台 canal server 所在的机器上查看日志,可以得出如下结论:</p><ul><li>109 和 108 上的 canal server 在接到 <code>sh startup.sh</code> 命令后,都向 zookeeper 尝试创建 EPHEMERAL 节点,109的 canal server 创建成功,因此启动的demo实例就处于active状态;</li><li>108机器上的 canal server 创建 EPHEMERAL 节点失败,因此该 server 的 demo 实例就处于standby状态。</li><li>相同名称的实例在分布在多个机器上的多个 server 里,同一时刻只会有一个实例处于 active 状态,减少对mysql master dump 的请求。</li></ul><h4>现象二:关闭一个canal server后会重新选出一个canal server</h4><h5>1. 手动关闭109机器的canal server</h5><pre><code>cd /data/application/canal.deployer-1.1.4
sh bin/stop.sh</code></pre><h5>2. 查看zookeeper集群中,canal server的 active节点信息</h5><p><img src="/img/bVbJU1l" alt="zk3.png" title="zk3.png"></p><p>从图中可以看出:</p><ul><li>当前可用的 canal server 切换为:10.200.*.109:11111。</li></ul><h5>3. 结论</h5><ol><li>再次验证,canal server 启动时向 zookeeper 创建的节点就是<strong>临时节点</strong>,<strong>它与 session 生命周期绑定,当我手动执行关闭命令,客户端会话会失效,临时节点会自动清除</strong>;</li><li><strong>一旦 zookeeper 发现 canal server 108 机器创建的节点消失后,就会通知其它的 canal server 再次进行向 zookeeper 尝试创建临时节点的操作,就会有新的 active 节点产生</strong>;</li><li>这不就是 HA 机制最核心的作用嘛,一个机器机器发生意外,如宕机,另外一个机器能够立马顶上,保证集群的正常使用,从而保证服务的高可用。</li></ol><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=8r%2B5i%2Baw6tG0QnqpeHo8uA%3D%3D.jYhAznOLMy7WWKPm3F1%2Bl76yAzdvXIXnUuDBRAhQ9Swwm%2FtRdAPmqE3bPRK4pXCXRmvxQNTxDlkL4bh7Of2b7LwrICpea%2FYBA0o%2BwLAIiKpZVd6ThCoR92CiOzjBCWGjwsMNI06Ti%2FJi8Im9J%2FuCFLgSTOfo0pn2MssaZbggu%2FHYVlCvD39sPnR0vzG5CFmXYAk1cPHKX1suTRonBsUDIVJ%2F8aIAp7WcrgfJwRwBELfZ%2FPO0ouCKKl%2Bpj41r8h4TTvlolH61Bjo7ThKYKEDXf6doUehltmxBhZB2z6tCwWdOvcw2qWFBlrxMb8mSdOJm2A5fklgNgsoWFMvWjdISux3B11KiMBNKUnMIyim3GtY%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=gHDXmk%2Bs9x%2BTwBc%2BuAV18Q%3D%3D.ai8n%2FJDROUW9kViy4Ec9cmNrJRrt3QoD179liA1JLhRayLyRxi8c9hxJ8xeOUc0sPdK7MvWYZ%2Fw4aGca%2FRBuZy8GJbljLpR9Hl17LVbh5P3xwz98xfCHpTwMfMqMCc8Gm8uAdqg4KVFeyW6CMEH2%2BN%2FaQJgn1%2FhmOeY%2BkJ9xdNeevg82TbYTc6cT5rHfT7OSsck%2BfmDd7JKjPBXcs6GQ9hyXW5j4MoOj3pDKB7PfMbZ89UGZVSqyIHo2VpSGgWJUKZVVnFotuIy7dEcZ0lf9K0yl%2F51Tgq10pAff8fuZObxxbJAojrLhRrwrnqWjnzeBFkGuiQrHeb51n63KAtBLo0dpnBtVyfia8qfIKVK8Mp8%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=YUUQJus%2FNN1VKV3yq6gOfg%3D%3D.GlAc5YwcCjJzCBU75iFKBAj6bQjCBel2XRKVhFXtrP%2F3YODqnYE69J4IQA%2Bl40atSFzbY4ob%2B%2B%2BFHDKN%2FILkEj0ydgV3K8CVEAfovaRJHjl%2FRYbA04cBPBlHBuL7FoJ7Jzx9PCyo4o5mQL2ZF0pdVdRMUKUfjoLK3oTm7GleZvPQEuDfGpcqhpIHBgyKsEv%2B4AFSGQpfNig4tyYcebsFAR4qxh42rN%2BXqIkbCnbVCXL8omTuNIRPPZH%2FEgqlaSY5cjOR6mEuEyMTeSl5UA2P%2FbuZYrMaB10f7aQ6pdTJieszOgeXyQ8LA1bHD7%2BvIE23OOvoI4E3DQ7nuLSJjks8%2F1VnCgJmIWftL5rgm4kH5oc%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=pK2UhpdVo4qz2EM1O6Jj7g%3D%3D.%2BTLq%2FWOXp%2Bqz5qdW4GfvlA6za3282bby2pf0hIBaF7daeE2m%2BTVB6NNB12UjdHPx1k6XIioORXRhUeo3cB9%2BBWnMsoTSuu80lwgzaf9Z1qSFdJaskalM8M1Eb%2BLXXwXy10X4ok4uKEX8jRXHZCYr4kIPZnenuSRO9fBV4YPs9svyyD%2Bfxht%2FZ%2BNPgSct5OS3BDAcOdU9YS03XgI4aLO5TO%2BXH2Q%2Frcwvsrv2rrrRiiKGmNxt711Jje6pGdV3uJSLFJSKpNQ9bOPXjtpy7aNGwTji6t0ZAN4JBA1o0e7yNqtH%2FElkFp18TpVpagLP3bRvaM1XGrAo9kAFp6A95gWMt%2FQpD%2BY9rSyxbnamx73lKqE%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=sjyuCVC%2BjzDOLbS5YsNe9Q%3D%3D.Oi%2BUDKhJ46mxpLFs6t7gNiC%2BIuqEzzD3CEDa0x2qaKPOOBS9bIDoXSMBAEW7FzPCc2XtDmcYEqm7HiT9dzVCdJF8T%2BJwonyKw6irF8uzk8xGMUprvkMUJGRsNEpxuxVydM%2FVAJjdwAGuRE35u0%2BZz4nWGZLdU%2FqmXGL7c9T7KDsW6lQxB%2BIAPt2ZOwGV100ESabJKHD%2F7NsNnTH9FRaPDG%2B1Qjw50Xci8DQhgz7SZnFXzam1kNoQ0u9Rl8hFwib8rVQ6PyjjDBdWiDtlZ%2F9Dhm75w%2BvoITeEq8BCsX%2BwuAfBTzVshCBpT%2BwWsmPeYPBHsrt0Fah0K3x9284JnqXbYvPAuom3Xa7pPb%2FgoGRhuIg%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=ri%2BDsgjU%2Byk1E0SHuUo%2FoQ%3D%3D.PrvGyqGmXIyRzDZ9jKMfElBeUZs6MrDI16xi0Nobk5zVs2CEy9Cj5dlmV7Hs8x6Xrzy%2FwHO4%2FpH4WOz1k0KX9u8FPCCxWmOkncqNUsD3WG0Xn141peMcEdWi25SO8gx2msbjprVKO2wnTxJdILy%2F0%2FftUua1Yxz7PSY5DnMpf5vt3ICYHA2U06D%2FrJgxja3oqrlOUxRuVx4%2FuqITwY3B2gFTeMU7HnLU7EEecAZrNlGQ%2B6oomQb5strIs8YeUhNuVksKYQfU7rWwuVjpezuxmZMzQXnqaXTPXiwYIbrW9gUQ9LRo%2F1U0BGZGlPSJJI9kRGaGetkpyOpNv8KPSpVoCtjINjmGXCX%2BSH%2Bj36zepPM%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
移山(数据迁移平台)的数据迁移是怎么实现的
https://segmentfault.com/a/1190000023199389
2020-07-13T10:13:31+08:00
2020-07-13T10:13:31+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<blockquote><ol><li>移山是禧云自研的数据迁移平台,通过在移山的配置操作,可以方便的实现第三方数据接入、实时数据同步、异构数据源间迁移。</li><li>本文主要介绍移山数据迁移平台对异构数据源间迁移这块的实现思路和图形化配置实现流程。</li><li>可以访问 <a href="https://link.segmentfault.com/?enc=MMeUnqisxfj%2BOfCd6uQkGw%3D%3D.fJ85GSX3artqSm5wHoC%2BeNEkifvTIEcAmCFXiaRPO9lbgvfcSDDCNO92bo8KIgVHNx8rpQ%2BaBn2P%2F8tLY6RQVgOHoUmXXyayVzRGT0wODi15rPLz8IbNhfExelG7yaTHknmohwdk2RJ1o41%2BLV5rzFLHAsSioIaHR80t9G%2F5SNPsy91IbiHqAWwUhbYG2Bd%2B" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</li></ol></blockquote><h3>| 一. 什么是异构数据源</h3><p>可以理解为:指数据结构、存取方式、形式不一样的多个数据源:</p><ul><li>一个公司在信息化建设中,不同时期、不同背景、面对不同的应用和客户会催生出多个系统;</li><li>每个系统可能会积累大量不同存储方式的数据,从简单的 Excel 文件数据、txt 文本数据到复杂的关系型数据库 MYSQL、Oracle 数据等,它们构成了异构数据源。</li></ul><h3>| 二. 异构数据源迁移</h3><h4>离线数据仓库</h4><ol><li>为支撑集团运营发展和决策分析,禧云起建之初构建了完善的离线数据仓库体系;</li><li>从业务数据源抽取过来的数据存放在数据湖(Hbase)中;</li><li>数据仓库的数据存储为 HDFS,离线计算框架为 Hive,Spark。</li></ol><h4>数据迁移场景</h4><p>在建立数据仓库的过程中,会有大量的 ETL(Extract、Transform、Load)工作,处理数据抽取、数据转换、数据装载的过程中会有需要<strong>多种异构数据源(txt、Hbase、HDFS、Mysql、SqlServer等)迁移</strong>的场景。</p><h5>场景1: 简单的数据转换:字段水平的简单映射</h5><blockquote>数据中的一个字段被转移到目标数据字段中的一个过程。</blockquote><p>比如:</p><ul><li>业务数据库(Mysql)到数据湖(Hbase);</li><li>从数据湖(Hbase)到数据仓库(HDFS);</li><li>数据仓库(HDFS)到目标关系型数据库(Mysql)。</li></ul><p><strong>备注</strong></p><ul><li>复杂的数据转换</li><li>需要做更多的数据分析,包括通用标识符问题、复杂条件过滤、时间类型的转换、去除重复记录等;</li><li>此时可以借助魔盒(开发协作平台)来完成 Spark 计算任务的提交、工作流调度来实现;</li><li>该类数据分析处理不在本文讨论范围,若有兴趣了解可以看<a href="https://link.segmentfault.com/?enc=3XRukaQSWV44kaOZQEewHg%3D%3D.lF8lH0VVwj9EwHebZs7T2dnC2sI0HEVbqBMnLFaMoQtEIKeRPHH%2BLhiBaYIwEuYBtINW32BBf4BdIXRVrHLL4O3BOPIzmSiVdQC2z8Qt9e1IVjGLxpaz8Gxgbkzk90Z%2BdFtwmu1u%2F5vAT5gNQaKh9aRHQPaJ1wlRF%2BNSArQjtp3v0mDfFA%2BdL7a7Z72Tya2WFioQBmt%2Bjglcj02VCxVvm8pFZ5PdY%2BA%2BHCmUsPFemZRhDjY92bujF6TgzL22CCGaS5OBnAb9l1%2B4h8qfwmhb4BNAr653eF0MGt180uLOh7TQICKYOaupHXMD0blS02PI" rel="nofollow">这篇</a>。</li></ul><h5>场景2: 关系型数据库之间的数据迁移</h5><blockquote>比如<strong>数据报表系统 B</strong> 需要使用<strong>业务系统 A</strong> 中的某一张表的数据用来做数据报表展现时,需要保证过多的数据查询、数据聚合处理不会影响该<strong>系统 A</strong> 的正常业务。</blockquote><h3>| 三. 移山数据迁移工具</h3><h4>1. 目的</h4><blockquote>解决数据湖(Hbase)中的数据迁移至数据仓库各层及关系型数据库之间的数据迁移问题。</blockquote><h4>2. 数据迁移工具</h4><p>由于要支持异构数据源的迁移,【移山】采用了在阿里内部被广泛使用的离线数据同步工具 DataX。</p><h5>2.1 DataX简介</h5><p>DataX 是一个异构数据源离线同步工具,致力于实现包括关系型数据库(MySQL、Oracle等)、HDFS、Hive、ODPS、HBase、FTP等各种异构数据源之间稳定高效的数据同步功能。</p><h5>2.2 DataX3.0框架设计</h5><p>DataX本身作为离线数据同步框架,采用Framework + plugin架构构建。将数据源读取和写入抽象成为Reader/Writer插件,纳入到整个同步框架中。<br><img src="/img/bVbJvj8" alt="datax1.jpg" title="datax1.jpg"></p><ul><li>Reader:数据采集模块,负责采集数据源的数据,将数据发送给Framework。</li><li>Writer:数据写入模块,负责不断向Framework取数据,并将数据写入到目的端。</li><li>Framework:Framework用于连接Reader和Writer,作为两者的数据传输通道,并处理缓冲,流控,并发,数据转换等核心技术问题。</li></ul><h5>2.3 DataX3.0插件体系</h5><p>DataX目前已经有了比较全面的插件体系,主流的 RDBMS 数据库、NOSQL、大数据计算系统都已经接入:<br><img src="/img/bVbJvkn" alt="数据迁移能力.png" title="数据迁移能力.png"><br><strong>备注</strong></p><ul><li>详情请点击:DataX数据源参考指南:<a href="https://link.segmentfault.com/?enc=wb49V%2F8q1BrpE47ix5wMcA%3D%3D.ytbh6kJRbnHLMsRQNNOHtDn0acWUpSCtJErXBl5vNOL1pXCjmJvzEdD0qnPIu25B" rel="nofollow">https://github.com/alibaba/DataX</a></li></ul><h5>2.4 移山数据迁移功能对DataX的封装</h5><ul><li>DataX 配置过程比较复杂,并且只支持命令行方式执行;</li><li>为降低使用难度,我们将配置过程进行了图形化处理,采用 <strong>Python 的 Flask 框架进行封装,任务执行支持 HTTP 请求调用</strong>。</li></ul><h3>| 四. 移山数据迁移实现流程</h3><p>针对前面提到的两种场景,我们开发了数据迁移功能,以下为【移山】中<strong>数据迁移</strong>的主要实现流程。</p><h4>技术栈</h4><h5>前端</h5><pre><code>Vue.js + Element UI + vue-socket + codemirror</code></pre><h5>移山服务端</h5><pre><code>SpringBoot</code></pre><h5>DataX 服务端</h5><pre><code>python + Flask + gunicorn + supervisor + Celery</code></pre><h4>1. 创建Reader/Writer插件</h4><h5>1.1 准备插件配置模板</h5><p>拿创建一个 HbaseReader 插件为例,配置样例如下:</p><pre><code>{
"name": "hbase11xreader",
"parameter": {
"hbaseConfig": {
"hbase.rootdir": "hdfs://test/test1",
"hbase.cluster.distributed": "true",
"hbase.zookeeper.quorum": "",
},
"table": "table1",
"encoding": "utf-8",
"mode": "normal",
"column": [{
"name": "info:column1",
"type": "string"
},
{
"name": "info:column2",
"type": "string"
},
{
"name": "info:column3",
"type": "string"
}
],
"range": {
"startRowkey": "",
"endRowkey": "",
"isBinaryRowkey": true
}
}
}</code></pre><p><strong>备注</strong></p><ul><li>更多配置样例请点击这里查看:<a href="https://link.segmentfault.com/?enc=SsKLqh53yQpjtepJtUGNSw%3D%3D.OQsQntP0JASchqPUYBD80h8TK3htoFl1yN7%2FofZKgfuR%2BeLxJHAjo5YmWTat%2Bb0Z" rel="nofollow">https://github.com/alibaba/DataX</a></li></ul><h5>1.2 保存配置</h5><p>移山平台展示插件模板的时候使用了 codemirror 在线代码编辑器,可以友好的展示 JSON 格式的模板数据,效果如下:<br><img src="/img/bVbJvlt" alt="hbase-reader.png" title="hbase-reader.png"><br><strong>总结</strong></p><ul><li>通过借助 codemirror 插件,JSON 模板可以高亮的显示,同时强大的语法检查功能也有助于解决在编辑过程中产生的语法错误,从而降低了 DataX 的Reader/Writer 插件配置难度。</li></ul><h4>2. 创建数据迁移任务</h4><p>在准备好了数据迁移任务所需使用的 Reader/Writer 插件后,就可以通过移山去创建迁移任务了,其实就是通过图形化配置去生成一个完整的 Job 配置文件,为运行任务做准备。</p><h5>2.1 配置任务基本属性</h5><p>该步骤主要配置任务并发数,脏数据限制信息。<br><img src="/img/bVbJvlF" alt="create-task0.png" title="create-task0.png"></p><p><strong>备注</strong></p><ul><li>最大脏记录数:写入数据时可允许的最大脏记录数,超过该阈值时,程序会执行失败;</li><li>脏数据占比:写入数据时可允许的最大脏数据占比,超过该阈值时,程序会执行失败。</li></ul><h5>2.2 使用Reader插件</h5><p><img src="/img/bVbJvlR" alt="create-task1.png" title="create-task1.png"><br><strong>备注</strong></p><ul><li>可以对自动匹配显示的模板内容进行修改,以适应你的需求;</li></ul><h5>2.3 使用Writer插件</h5><p>从下拉列表里选择要使用的 Writer 插件,同样配置内容会自动匹配显示,同样可以对配置内容进行编辑,这里不再截图。</p><h5>2.4 信息确认</h5><p>通过前面步骤的配置,完整的 Job 配置文件已经成型, 在任务保存之前,会将该配置文件完整的展示出来,方便使用者对之前步骤里的设置数据进行检查,格式如下:<br><img src="/img/bVbJvma" alt="job配置文件.png" title="job配置文件.png"></p><h4>3. 执行迁移任务</h4><h5>3.1 前端</h5><p>3.1.1 建立Websocket 服务 <br>点击【执行】按钮,前端会发出执行任务的请求,浏览器与 DataX 服务会通过 Websocket 服务建立连接,方便前端实时拿到 DataX 服务产生的任务执行结果:</p><pre><code>import io from 'vue-socket.io'
Vue.use(io, domain.socketUrl)</code></pre><p>3.1.2 将Job配置数据传给 DataX 服务:</p><pre><code>socket.emit('action', jobConfig);</code></pre><h5>3.2 DataX 服务端</h5><p>3.2.1 接收数据,生成 json 配置文件 <br>从 data 中取出 Job 配置数据,并将该数据写入到一个 .json 文件里:</p><pre><code>job_config = data['job_config']
# 生成临时执行文件
file_name = '/tmp/' + str(job_id) + '.json'
with open(file_name, 'w') as f:
f.write(job_content)
f.close()</code></pre><p>3.2.2 拼接执行命令</p><pre><code>command = DATAX\_ROOT + ' data.py ' + file\_name</code></pre><p>3.2.3 执行任务 <br>利用 subprocess 的 Popen 用法来执行命令:</p><pre><code>child_process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
shell=True)</code></pre><p>3.2.4 通过 stdout 获取执行信息</p><pre><code>while child_process.poll() is None:
line = child_process.stdout.readline()
line = line.strip() + '\n'</code></pre><p>3.2.5 将执行结果传送给 Websocket 客户端<br>通过<code>socket.emit('action',line)</code>方法,将日志传送给 Websocket 客户端(浏览器):</p><pre><code>if line:
emit('execute_info', line)
if child_process.returncode == 0:
emit('execute_info', '执行成功!')
else:
emit('execute_info', '执行失败!')</code></pre><h4>4. 移山查看任务的执行情况</h4><h5>4.1 接收执行信息</h5><p>前端利用 socket.on 方法接收任务执行日志:</p><pre><code>this.$socket.on('execute_info', (info) => {
// TODO 拿到执行信息,并展示
})</code></pre><h5>4.2 展示执行结果</h5><p><img src="/img/bVbJvng" alt="execute-info.png" title="execute-info.png"></p><h5>4.3 监控迁移任务</h5><p>在监控管理界面,可以对当天所有的迁移任务进行监控:<br><img src="/img/bVbJvnq" alt="迁移任务监控.png" title="迁移任务监控.png"></p><h3>五. 总结</h3><ul><li>通过使用移山的数据迁移功能,数据开发人员在移山中可以无感知的使用 DataX 这个 ETL 工具,取代通过手动开发、命令行执行的传统方式;</li><li>通过图形化配置,就能够快速的创建、执行一个数据迁移任务;</li><li>通过监控管理功能,可以方便的监控任务的执行情况;</li><li>大大的提高了数据开发人员的开发效率。</li></ul><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=%2BB33Sk%2Fxmd4Tqz6Rm8AJtA%3D%3D.4RQ58bBkq7A%2BYNtaMMj6bp%2FT22%2FUjckfdqFQ5yys1KD73A3pnFGA8yU5qViGRiT04QpWoXfJ79%2F2OUuSrwEG9e9PusGitrgqincx%2BWFI%2B6Sp0YUw9Kr2bX0qxlOmCEizSrCHXzMwDAg%2B9gM2YJ0h%2Fjyt51fpzZqWZjYTTMkfkfDICBIFQwgeF4xFr9fOL4ROdQUcFGNUpmGfPrDgMSS9O53orUqp07EvbjZJ58dAO7bIOjoRM3kEx1HeSeNgCrqjYt45O3vWL1fLfdt4QAgNZ8IvRFpLM5GsYWbEeOZgj3UuwXjzb%2F3ngBaaVO5c7GLWwzHW6PBPnrX0XDkpRgqCAvxtEJWtu9ouUt5HPRcWDe4%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=Kq5UjTv64JKZf38ydKipjA%3D%3D.ihRo%2FT3biz6rruOIKdqNCCYQIITkk9HgKlrjUFWtLVMuNxIijknBdpHGoDaeubHGMIZQJGdCSQtO6BxG2AxR5h0vUxpnKIVMqE%2Fveg1omH4Om5W1XUr9qt8tDWUyO8S4YQ%2F%2F2rxXmBpoxbB8%2BiytBxHeELyWsf389B4mnB6xDz1spSGRchFF30oMlelC0xmbvvt63t%2BZhkG6ujAMONsJpFW%2FMRLTzmPSy0E7MkrQT8ZzeRUgIyYjJJUYA1MJ1juPOlrMllLDoSnBNcl5tXK3Zc4fIVIxVKLzqy3gkV7NMO6zNKnfDQL58N2Zd%2BnBV7H0dFUOvurYJS37WXBq%2BjxcxbjNxCvOk4ewWz0JtvFRYzA%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=AkWZrN6MwBGRUIQzTp80tg%3D%3D.J7a5Zxuk%2Bu8TRubyNgZFr92GEhorr%2ByR5Z4Em35qfB7OASzRbjqywQEWsV4XtUQWJMunwl8kEpaYLI2RAyoxe12toJsT7opjV4DqX7j0qR6GcRER0YGo8Kz0N3vVo538I323z6IkWS76s24F3%2BzAD4Nh3xjEDuLG%2BxRXe33V1pj%2FV86CKYk%2Fp4cwaFFvbsdzwGlfXOtl0l80e8EHFSO2BT82qPIrf2OXnhTNrKAbjpzF1Of1MIfzIVM2RNKi4aiLbmZyKtjR%2F989W0FPBsrM8j8WYbWJZ6FXbCg1gbDo8OKeACWLwcIEHubcUPQizp4xXW2Mjpxk%2Bm6uYW%2FwysGNQ3Bc3EuSw6vQilCqxSTZuWA%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=q9vyJU7mBEvL7m0Qb6cbFQ%3D%3D.EvXqvcYGezP8Vp1gGHaGUVWuzytJcQR%2BWMxjodC2MqENMi4WBMOX4n%2F7o79DErqwVd%2F9LJNW17lVylb%2F346qfIVwjp3vmIdSEjsVkhSdDTmI%2Ftd1Y5sWLZjI9hACn6aLxjx1ObcgQmIQIRIn5SELPuIb1iX2tG%2FwA3sZovSFRsOu1D3sKAaQ0daFDuTM6U9TwdvmJHWD%2B%2FsI6UeEDklLMmks9sMUPfgt9iiguH3LzWTy3tCS%2BQPM%2BXGO3lLwNIbxV1WEBLmh8C%2FcFQ1hnv5KuinXf3UNeNqVWwm8HrK8EzLb2CONLby3flqKToXB5He9heV3ePF8JUTgaXTbE%2B4YWkDru%2Bij7nf3aFHKgFuFOzY%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=v98YFDg7KHW7R21HYQitoA%3D%3D.UnAI5R8BRO2Flme5HrEbagpnKz0ScoDY%2FNqDMZakYFbtVFqGkeAP6mkf5%2FdaleqJHIP9tOMwIsFbRrgkCCGrhV0ayhCEHrM9qbsv8IO7%2F1pbKCH8rGXbCgixg31RSr9sIqqjZOm00%2FZqO9AzliOtBCrr%2Fv5yCTTrY%2BXkQDYyouFm3fZXgC4KlUB2jsENO%2FGUPlwKwN2RTuO2t%2BO5caEdfyYF1dKQXJnulyjKcdP52olp3TpcAb3rf8rxctHFLyzhzZvwyRZWe3ZJbyP4crzIdi%2FtJSoZ4450UzNtMACtzD1P4Id%2BI8VygvU6NZZsEulzJFNBAoDhAL2nztF9cadvodDgvj61wnz3m7M8ifvk7kI%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=ACdHmLJdEh4bnsxouXpcyA%3D%3D.AcG7dcUARt0NfF9tnGDHKdjYOlOISAzwHPAs850COXVBNflAhsWK0jWGVDTBOTs673HfsE512zYdBVOTk9pJqFQWeYooC%2FzCDeKT9xZucxgMSN5HvtnAu1IqBy9F75N5a61kfP5yYQLm7Bsnsw9xeLyk5gsu7UIPpUFNWDQfKzIlwZ%2FPYVTxu5BPcJMJH6VyRI0puIq7tycD0S80Gb4N59avV96WqO1HBbHmTOIrEB5Dw9hS92tKtInM0JjHqzfIwwEcuNmgu6lHbIcvD15ZKOm4b7wWvmOv4AxqzOelHr7Resx5sAwGvwol4Tt4V0bDfTmM1cdL%2Fwri3RPeW0obS8cXBSr8bp%2BfY0WMRhDzt0U%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
魔盒(大数据协作平台)是如何实现离线计算任务的工作流调度
https://segmentfault.com/a/1190000023073753
2020-07-02T11:29:38+08:00
2020-07-02T11:29:38+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote><ol><li>魔盒是禧云自研的大数据开发协作平台,<a href="https://link.segmentfault.com/?enc=dmDjD%2BqryCOst%2F5dzgPdYg%3D%3D.MmvAlP6jpzYuTjo1eSVN4P2j%2FgUmAvmfHKfy3jdkQkbUtphc7JJpw2tpodDr1GFD%2BG4siWrpGVfSSt298z9iBWVQPaTY7xoKZm2g4rC8teQ9A9sn48bQ3WOxNybQ4E5FbXfKUwhyL%2FJz%2FIGUUgxbP1ogI2tv5bHhYkX%2BiNihEevl30rC2Tx7w9Xfw598Pj9IrXVfcqHuPluWMD%2Ff3IGiI7ayZmezDNPYVFv7iwtFNgPs%2BTHiKWbUFV8Xu5ERdLu43LTg4I3yKBOib%2FHpcVNXZyxjBbyEg25Ne9AvJkWZ6Ws%3D" rel="nofollow">上一篇</a> 介绍了魔盒在离线任务打包过程中怎么提高RabbitMQ消费速度;</li><li>数据开发人员通过魔盒不仅可以很方便的进行离线任务的打包、测试、上线,还可以方便的设置离线任务的串行、并行工作流调度;</li><li>本文以<strong>创建一个需要依赖多个并行job的工作流</strong>为例,来介绍魔盒集成 Azkaban实现<strong>离线任务工作流调度</strong>的思路和流程。</li></ol><p>4.可以访问 <a href="https://link.segmentfault.com/?enc=%2B6y2pvw4qjHZRiogm7Ndgw%3D%3D.2U2Z4buawcKjUi%2FEj%2FgPnY6BTFDGYUQh89TsN%2F3RDREeZj0RsTmJy%2BzhzQibiBbcd6yphaC7Euy8goMaqb1CeHW7%2FgCVzYT4iIGb6DJlpnJhNUhQEZW4tUTJyz0T%2BVjf5r4ElPaM%2B%2FZiRu%2FESJkPqeuEy1nu7cg2FztP4%2BsYF8eJsRRWCReMCmTgow6Cq7hm" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</p></blockquote><h3>一. 离线计算</h3><h4>魔盒管理离线任务</h4><ul><li>禧云离线计算支持 Hive,Spark 等计算框架;</li><li>数据开发人员用 Spark 编写完分析代码后,通过使用魔盒可以打包、测试、上线(详细可以看<a href="https://link.segmentfault.com/?enc=HPsUppRRcmqJfjzM6Znsgw%3D%3D.y%2BtsFcgJ3ATyBnFv4nNsukZW%2FNoZVjgAKUebn3WNu%2B4DIEI2yRTXn8OunnmqG05tSt3GEfwzfTUXYuzDr8%2BxLKy2Q4S9%2BW%2BEuOFjSFxrNMqUldJk5X6PMbO8NbAFcg85titfaTNCRgCAQNu4ctZr2QUoNvi%2FZDDb63bOrvqOltXjTIaphGx7NTwlT%2FTOysaVYWitL3ibEZ8lgaSBFZfoTYfhKJj6YBtdcTzprQyaBU2YdpuLlcY7b4stF0DUug95%2FlpRgSogZobgpWvO7xExaT4kkdRUKAnOKtsWIR%2F910OYP1AoRdmcR7WLCrFa4yMvYxGY5Ms6PiOSyluawcTwxg%3D%3D" rel="nofollow">这篇</a>文章)。</li></ul><h3>二. 任务调度</h3><h4>为什么需要工作流调度器</h4><ul><li>一个完整的数据分析系统通常都是由大量任务单元组成: shell 脚本程序、java 程序、mapreduce 程序、hive 脚本等;</li><li>各任务单元之间存在时间先后及前后依赖关系,禧云每天运行着上百个离线分析任务,不同优先级的任务调度时机也不同;</li><li>为了很好地组织起这样的复杂执行计划,需要一个工作流调度系统来调度执行。</li></ul><h4>crontab+shell</h4><p>为了解决上述问题,我们早期使用的是 crontab+shell 的方式来执行,但是这种方式的弊端如下:</p><ul><li>任务之间的依赖关系完全依靠脚本来控制;</li><li>在任务比较多的情况下,管理和维护起来比较麻烦;</li><li>出现问题也难以排查。</li></ul><h4>Azkaban</h4><p>Azkaban是由 Linkedin 开源的一个批量工作流任务调度器,优势如下:</p><ul><li>可以在一个工作流内以一个特定的顺序运行一组工作和流程;</li><li>可以通过一种 KV 文件格式来建立任务之间的依赖关系;</li><li>并提供一个易于使用的 web 用户界面维护,通过它可以跟踪你的工作流。</li></ul><h3>三.魔盒实现工作流调度</h3><p>魔盒的离线计算部分集成了 Azkaban,通过 ajax 调用接口的方式与 Azkaban 进行交互,用户不用登陆 Azkaban 的 web UI,直接通过魔盒就可以完成:</p><ul><li>工作流的创建;</li><li>工作流的删除;</li><li>执行工作流;</li><li>取消执行工作流;</li><li>查看工作流执行记录、执行状态、执行时长等。</li></ul><p>下面我会介绍一下在魔盒中怎么去创建一个<strong>多个Job并行</strong>的工作流,要创建的工作流<strong>任务依赖关系图</strong>如下所示:<br><img src="/img/bVbIYyT" alt="azkaban-2.png" title="azkaban-2.png"></p><p><strong>备注</strong></p><ul><li>Azkaban 流程名称以最后一个没有依赖的 job 定义的。</li></ul><h4>1. 创建Spark任务</h4><p>工作流会依赖一个或多个任务,因此在创建工作流之前,需要准备好任务:</p><ul><li>在魔盒中,通过指定任务处理类、设置执行任务所需的参数来创建 Spark 任务;</li><li>任务创建成功后,通过魔盒的项目构建、测试无误后,会将运行任务所需要的 jar 包自动上传至 HDFS 中。</li></ul><p><img src="/img/bVbIYy1" alt="创建离线任务.png" title="创建离线任务.png"></p><p>执行参数会以 JSON 字符串的格式存入表中去,大概格式如下:</p><pre><code>{
"type":"spark",
"conf.spark.yarn.am.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.history.fs.logDirectory":"hdfs://dconline/spark/eventlog",
"conf.spark.driver.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.eventLog.enabled":"true",
"master":"yarn",
"conf.spark.dynamicAllocation.executorIdleTimeout":"60",
"deploy-mode":"cluster",
"queue":"develop",
// 创建spark任务时表单里填写的任务处理类
"class":"...Main",
// 默认为空,会在创建工作流时赋值
"name":"",
// 默认为空,会在创建工作流时赋值
"execution-jar":"",
// 默认为空,会在创建工作流时赋值
"dependencies":"",
}</code></pre><h5>主要执行参数解释</h5><ul><li>name:spark 任务提交到 Yarn 平台上时的 application 名字;</li><li>execution-jar: 为执行该任务时依赖的 jar 包;</li><li>dependencies:在这里设置依赖关系(比如:<code>task_a,task_b</code>,则表明该工作流依赖<code>task_a</code>和<code>task_b</code>);</li></ul><p><strong>这三个参数默认值为空,在创建工作流的时候,会根据配置的工作流依赖关系,动态更新赋值。</strong></p><p><strong>备注</strong></p><ul><li>上图中,我创建了一个 spark 任务:<strong>离线任务测试001</strong>(离线任务测试001为魔盒中记录的任务名字,对应到 Azkaban 中的 job 名称为为:<code>spark_task_10046</code>);</li><li>照此步骤,还需要创建二个 spark 任务,分别对应job: <code>spark_task_10006</code> 和 <code>spark_task_10008</code>,这里不再截图展示。</li></ul><h4>2. 身份验证</h4><p>所有 Azkaban API 调用都需要进行身份验证,其实就是模拟一个用户登录的过程。因此在与 Azkaban 进行交互之前,首先需要进行身份验证。</p><h5>请求参数</h5><table><thead><tr><th>Parameter</th><th>Description</th></tr></thead><tbody><tr><td>action=login</td><td>The fixed parameter indicating the login action.</td></tr><tr><td>username</td><td>The Azkaban username.</td></tr><tr><td>password</td><td>The corresponding password.</td></tr></tbody></table><h5>主要代码</h5><pre><code>/**
* 登录Azkaban,并返回sessionId
* @公众号 全栈在路上
*
* @return string
* @throws Exception
*/
@Override
public String login() throws Exception {
SSLUtil.turnOffSslChecking();
HttpHeaders hs = new HttpHeaders();
hs.add("Content-Type", CONTENT_TYPE);
hs.add("X-Requested-With", X_REQUESTED_WITH);
LinkedMultiValueMap<String, String> linkedMultiValueMap = new LinkedMultiValueMap<String, String>();
linkedMultiValueMap.add("action", "login");
linkedMultiValueMap.add("username", username);
linkedMultiValueMap.add("password", password);
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(linkedMultiValueMap, hs);
RestTemplate client = new RestTemplate();
String result = client.postForObject(AzkabanUrl, httpEntity, String.class);
log.info("--------Azkaban返回登录信息:" + result);
return new Gson().fromJson(result, JsonObject.class).get("session.id").getAsString();
}</code></pre><p><strong>备注</strong></p><ul><li>通过执行身份验证,会为用户提供一个会话(会在 Response 里返回一个 session.id);</li><li>在会话到期(默认是24 hours)之前,可以执行任何 API 请求;</li><li>当然,如果你有注销、改变机器、改变浏览器这些动作或者 Azkaban 服务重新启动等,会话就会到期。</li></ul><h4>3. 创建工作流</h4><p>创建工作流时可以选择所依赖的任务单元,既可以设置串行,也可以设置并行。</p><p><strong>下面来展示怎么在魔盒中创建前面需求中的工作流(依赖多个并行job)。</strong></p><h5>3.1 创建工作流(多job并行)</h5><p>选择任务:<br><img src="/img/bVbIYzg" alt="create-flow1.png" title="create-flow1.png"></p><p>选择依赖任务:<br><img src="/img/bVbIYzl" alt="create-flow2.png" title="create-flow2.png"></p><p>点击【添加】按钮:<br><img src="/img/bVbIYzz" alt="创建并行工作流1.png" title="创建并行工作流1.png"></p><p><strong>备注</strong></p><ul><li>魔盒使用 <a href="https://link.segmentfault.com/?enc=WNIhz%2FiASj3SDdAeKiEDUQ%3D%3D.Va1baUBStb4N3hXEQO179H01NvL5Sacy0eF3zHpHusg%3D" rel="nofollow">vis.js</a> 来配置和展示流程拓扑图(本篇不做为介绍的重点)。</li></ul><h5>3.2 前端处理:工作流传参</h5><p>前端会将创建的工作流(包含的任务单元依赖数据)组装为一个叫<code>dependList</code>的 JSON 字符串,传递给服务端:</p><pre><code>[
{
"id":"task_100046",
"depId":"task_100008"
},
{
"id":"task_100046",
"depId":"task_100006"
}
]</code></pre><p><img src="/img/bVbIYzU" alt="保存工作流.png" title="保存工作流.png"></p><p><strong>备注</strong></p><ul><li>如果子级也有需要依赖的任务,则该数据结构会是一个典型的树状结构(本 demo 展示的依赖关系只有两级)。</li></ul><h5>3.3 服务端处理:得到依赖关系</h5><p>服务端接收到本次前端提交的工作流数据(<code>dependList</code>)后,会对数据进行处理,得到一个存储工作流任务单元依赖关系的 <code>dependencies</code>:</p><pre><code>[
{
"task_100046": [
"spark_task_10008",
"spark_task_10006"
]
}
]</code></pre><p><strong>备注</strong></p><ul><li>如果子级也有需要依赖的任务,则该数组会存在多个元素,数据格式类似(本 demo 展示的依赖关系只有两级);</li><li>处理树状结构的工作流依赖数据时需要借助多个递归方法得到所有依赖关系。</li></ul><h5>3.4 服务端处理:更新执行参数</h5><p>循环存储依赖关系的 <code>dependencies</code>,主要逻辑如下:</p><ol><li>通过截取任务名称 spark_task_10008 得到任务id: 10008;</li><li>由任务id从表中取出创建spark任务时保存的 JSON 格式的执行参数 <code>config_params</code>;</li><li>更新<code>config_params</code>,为<code>config_params</code>里部分参数(name、execution-jar、dependencies)赋值。</li></ol><p><strong>离线任务测试001</strong>最终的执行参数<code>config_params</code>为:</p><pre><code>{
"type":"spark",
"conf.spark.yarn.am.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.history.fs.logDirectory":"hdfs://******/eventlog",
"conf.spark.driver.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.eventLog.enabled":"true",
"master":"yarn",
"conf.spark.dynamicAllocation.executorIdleTimeout":"60",
"deploy-mode":"cluster",
"queue":"develop",
// 创建spark任务时表单里填写的任务处理类
"class":"...Main",
// 该spark任务提交至yarn平台中的application名字
"DataCube-SparkTask[100046]",
// 该spark任务的jar包在HDFS中的存储路径
"execution-jar":"hdfs://******/20190801/prod-***_feature_***_20190724165218.jar",
// 该工作流的依赖任务
"dependencies":"spark_task_100008,spark_task_100006",
}</code></pre><p><strong>lyf创建任务1</strong>的执行参数<code>config_params</code>为:</p><pre><code>{
"type":"spark",
"conf.spark.yarn.am.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.history.fs.logDirectory":"hdfs://******/eventlog",
"conf.spark.driver.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.eventLog.enabled":"true",
"master":"yarn",
"conf.spark.dynamicAllocation.executorIdleTimeout":"60",
"deploy-mode":"cluster",
"queue":"develop",
// 创建spark任务时表单里填写的任务处理类
"class":"...Main",
// 该spark任务提交至yarn平台中的application名字
"DataCube-SparkTask[100006]",
// 该spark任务的jar包在HDFS中的存储路径
"execution-jar":"`hdfs://******/20190801/prod-***_feature_***_20190724165218.jar`",
// 该工作流的依赖任务
"dependencies":"",
}</code></pre><p><strong>lyf创建任务2</strong>的执行参数<code>config_params</code>为:</p><pre><code>{
"type":"spark",
"conf.spark.yarn.am.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.history.fs.logDirectory":"hdfs://******/eventlog",
"conf.spark.driver.extraJavaOptions":"-Dhdp.version=3.1.0.0-78",
"conf.spark.eventLog.enabled":"true",
"master":"yarn",
"conf.spark.dynamicAllocation.executorIdleTimeout":"60",
"deploy-mode":"cluster",
"queue":"develop",
// 创建spark任务时表单里填写的任务处理类
"class":"...Main",
// 该spark任务提交至yarn平台中的application名字
"DataCube-SparkTask[100008]",
// 该spark任务的jar包在HDFS中的存储路径
"execution-jar":"`hdfs://******/20190801/prod-***_feature_***_20190724165218.jar`",
// 该工作流的依赖任务
"dependencies":"",
}</code></pre><h5>3.5 准备job资源文件所需要的数据</h5><p>更新完每个 spark 任务需要使用的执行参数后,开始组装 jobLists,用来存放 job 资源文件所需要的数据,格式如下:</p><pre><code>[
{
"newId": "spark_task_100046",
"config": 该任务更新后的config_params变量值,
},
{
"newId": "spark_task_100008",
"config": 该任务更新后的config_params变量值,
},
{
"newId": "spark_task_100006",
"config": 该任务更新后的config_params变量值,
},
]</code></pre><h5>3.6 创建Azkaban项目</h5><p><strong>请求参数</strong></p><table><thead><tr><th>Parameter</th><th>Description</th></tr></thead><tbody><tr><td>session.id</td><td>The user session id.</td></tr><tr><td>action=create</td><td>The fixed parameter indicating the create project action.</td></tr><tr><td>name</td><td>The project name to be uploaded.</td></tr><tr><td>description</td><td>The description for the project. This field cannot be empty.</td></tr></tbody></table><p><strong>主要代码</strong></p><pre><code>/**
* 创建项目
* @公众号 全栈在路上
*
* @param projectName 项目名字
* @param description 项目描述
* @throws Exception
*/
@Override
public void createProject(String projectName, String description) throws Exception {
SSLUtil.turnOffSslChecking();
HttpHeaders hs = new HttpHeaders();
hs.add("Content-Type", CONTENT\_TYPE);
hs.add("X-Requested-With", X\_REQUESTED\_WITH);
LinkedMultiValueMap<String, String\> linkedMultiValueMap = new LinkedMultiValueMap<String, String\>();
linkedMultiValueMap.add("session.id", login());
linkedMultiValueMap.add("action", "create");
linkedMultiValueMap.add("name", projectName);
linkedMultiValueMap.add("description", description);
HttpEntity<MultiValueMap<String, String\>> httpEntity = new HttpEntity<>(linkedMultiValueMap, hs);
String result = restTemplate.postForObject(azkabanUrl + "/manager", httpEntity, String.class);
log.info("--------Azkaban返回创建Project信息:" + result);
// 创建成功和已存在,都表示创建成功
JsonObject jsonObject = new Gson().fromJson(result, JsonObject.class);
String status = jsonObject.get("status").getAsString();
if (!AZK\_SUCCESS.equals(status)) {
String message = jsonObject.get("message").getAsString();
if (!"Project already exists.".equals(message)) {
throw new Exception("创建Azkaban Project失败");
}
}
}</code></pre><p><strong>备注</strong></p><ul><li>该方法中如果 projectName 已经存在也表示创建成功。</li></ul><p><strong>效果展示</strong></p><p><img src="/img/bVbIYCc" alt="createProject.png" title="createProject.png"></p><h5>3.7 生成压缩包</h5><p>循环 jobLists,生成该工作流需要的一个或多个 .job 文件,并将文件打包,主要逻辑如下:</p><ol><li>生成以 newId 为名字,以 .job 为后缀的文件;</li><li>将 config 的值写入文件;</li><li>将当前目录下的所有文件压缩为一个由不会重复的随机数命名的 .zip 文件。</li></ol><p><strong>主要代码</strong></p><pre><code>/**
* 循环jobList里的执行参数,写入到文件(newId.job)中,然后将文件写入到压缩包中去
* @公众号 全栈在路上
*
* @param jobLists
* @return
* @author liuyongfei
* @date 2019/04/03
*/
Map<String, Object> zipJobFile(List<Map<String, String>> jobLists) {
int randomNumber = (int) Math.round(Math.random() * (9999 - 1000) + 1000);
String zipName = "jobList" + randomNumber + ".zip";
// 定义压缩文件
String zipFilePath = CommonUtils.handleMultiDirectory("data/zip") + "/" + zipName;
// 创建输出流
FileOutputStream fos = null;
try {
fos = new FileOutputStream(zipFilePath);
ZipOutputStream zipOut = new ZipOutputStream(fos);
for (Map<String, String\> map : jobLists) {
try {
// 取出configParams内容,写入到newId.job文件中去
File file = new File(map.get("newId") + ".job");
if (!file.exists()) {
file.createNewFile();
}
// 获取执行参数
String configParams = map.get("config");
// 将执行参数写入到file中去
FileWriter fw = new FileWriter(file.getAbsoluteFile());
BufferedWriter bw = new BufferedWriter(fw);
bw.write(configParams);
bw.close();
// 将文件写入到压缩包中去
FileInputStream fis = new FileInputStream(file);
ZipEntry zipEntry = new ZipEntry(file.getName());
zipOut.putNextEntry(zipEntry);
byte\[\] bytes = new byte\[1024\];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
fis.close();
file.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
zipOut.close();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Map<String, Object> zipFileMap = new HashMap<>();
zipFileMap.put("zipFilePath", zipFilePath);
zipFileMap.put("zipFile", new File(zipFilePath));
return zipFileMap;
}
/**
* 创建目录
* 支持多级目录创建
* @公众号 全栈在路上
*
* @date 2019/04/03
* @return String
*/
public static String handleMultiDirectory(String multiDirectory) {
File savePath = null;
try {
savePath = new File(getJarRootPath(), multiDirectory);
//判断上传文件的保存目录是否存在
if (!savePath.exists() && !savePath.isDirectory()) {
log.info(savePath + "目录不存在,需要创建");
//创建目录
boolean created = savePath.mkdirs();
if (!created) {
log.error("路径: '" + savePath.getAbsolutePath() + "'创建失败");
throw new RuntimeException("路径: '" + savePath.getAbsolutePath() + "'创建失败");
}
}
log.info("文件存储路径为: {}", savePath.getAbsolutePath());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return savePath.getAbsolutePath();
}</code></pre><p><strong>生成的job文件和压缩包</strong><br><img src="/img/bVbIYCl" alt="unzip.png" title="unzip.png"><br><img src="/img/bVbIYCr" alt="10046.job.png" title="10046.job.png"></p><p>至此,包含工作流任务依赖关系的 zip 压缩包准备完成。</p><h4>4. 上传压缩包至Azkaban</h4><h5>请求参数</h5><table><thead><tr><th>Parameter</th><th>Description</th></tr></thead><tbody><tr><td>session.id</td><td>The user session id.</td></tr><tr><td>ajax=upload</td><td>The fixed parameter to the upload action.</td></tr><tr><td>project</td><td>The project name to be uploaded.</td></tr><tr><td>file</td><td>The project zip file. The type should be set as application/zip or application/x-zip-compressed.</td></tr></tbody></table><h5>主要代码</h5><pre><code>/**
* 上传文件至Azkaban
* @公众号 全栈在路上
*
* @param projectName
* @param file
* @return
* @throws Exception
*/
@Override
public String uploadZip(String projectName, File file) throws Exception {
SSLUtil.turnOffSslChecking();
FileSystemResource resource = new FileSystemResource(file);
LinkedMultiValueMap<String, Object> linkedMultiValueMap = new LinkedMultiValueMap<String, Object>();
linkedMultiValueMap.add("session.id", login());
linkedMultiValueMap.add("ajax", "upload");
linkedMultiValueMap.add("project", projectName);
linkedMultiValueMap.add("file", resource);
String result = restTemplate.postForObject(AzkabanUrl + "/manager", linkedMultiValueMap, String.class);
if (result.length() < 10) {
throw new BusinessException("上传文件包到Azkaban失败,请检查工作流是否正确!");
}
log.info("--------Azkaban返回上传文件信息:" + result);
String projectId = new Gson().fromJson(result, JsonObject.class).get("projectId").getAsString();
if (StringUtils.isEmpty(projectId)) {
throw new Exception("上传文件至Azkaban失败");
}
return projectId;
}</code></pre><p><strong>备注</strong></p><ul><li>上传zip压缩包成功后,会返回在 Azkaban 中创建成功的项目id。</li></ul><h4>5. 查看创建的工作流</h4><h5>5.1 在魔盒查看</h5><p>在工作流列表里可以看到刚刚创建的工作流,点击工作流可以进入详情页:<br><img src="/img/bVbIYDN" alt="工作流详情.png" title="工作流详情.png"></p><p><strong>备注</strong></p><ol><li>在详情页为该工作流设置和清除 cron 调度规则;</li><li>在详情页可以执行工作流,查看工作流执行记录、执行状态;</li><li>返回工作流列表,可以对已经创建的工作流进行删除(同时会删除Azkaban中的数据)。</li></ol><p><strong>我们在魔盒中集成了Azkaban web UI 中提供的常见主要功能,如果有非常用操作则可以去 Azkaban web UI 里去执行。</strong></p><h5>5.2 在Azkaban的 web UI 查看</h5><p>zip 压缩包上传成功之后,我们可以到 Azkaban 的 web UI 里去查看已经创建的工作流。 <br>在 Projects 栏里可以看到刚刚通过魔盒创建成功的工作流:<br><img src="/img/bVbIYD1" alt="azkaban_index.png" title="azkaban_index.png"></p><p><img src="/img/bVbIYD3" alt="azkaban-1.png" title="azkaban-1.png"></p><h3>四. 总结</h3><h4>魔盒集成 Azkaban 的好处</h4><ul><li>数据开发人员在魔盒这个大数据协作开发平台中,可以很方便的完成<code>spark</code>任务的打包和上线;</li><li>数据开发人员可以很方便的完成<strong>串行、并行等复杂工作流设置</strong>,使禧云的离线计算任务管理更加有序可依;</li><li>数据开发人员不用在魔盒和Azkaban web UI 两个平台之间<strong>频繁切换</strong>,在任务比较多的情况下,管理起来也比较方便,提高了数据开发人员的效率;</li><li>配合魔盒灵活的、完善的异常监控报警机制,<strong>数据质量保障的稳定性得到很大的提高</strong>,从而可以更好的发挥大数据平台支撑体系的价值。</li></ul><h4>更多</h4><p>通过调用 <a href="https://link.segmentfault.com/?enc=3q%2FHM8nsKzfjj3ULyt03iQ%3D%3D.B3ADis6O0sZaetYEk3%2B2YTYYdXHWmNtK3%2BBa4oNOROWuiHRh1chy%2FN9jiKd8OHiIbUt1pvKWOiRKOOlIpUx34Q%3D%3D" rel="nofollow">Azkaban 的 api</a> ,结合 Azkaban 元数据库数据查询,我们在魔盒中还可以完成对工作流的以下操作:</p><ul><li>删除工作流;</li><li>设置 cron 定时任务;</li><li>执行工作流、取消执行工作流;</li><li>查看看该工作流执行日志;</li><li>获取该工作流的依赖任务及任务信息;</li><li>工作流执行情况的监控报警。</li></ul><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=y9SlQ1gGpF6Fsx5tLm8CzQ%3D%3D.bYGi3Ouk%2BYkzFzzJ%2BC0Xcs7vgbwyef84tlf%2FcDIKgcGQcqO77%2FFCH9a5MBB6gjM2Tc68uCqOiAHMYk6ksvYFnVmWjZKRGDkrJktE8Qg0745zQcTZ38A%2F5xnVqxaQKa%2Bt%2BWI1W9sSG2IBusG%2BiE4DbUgwWZBCzNh5n8d6dTMhPyEFKpDbF%2F5q3Lhmww2DJpQ2YfkJyHZfJlHGUNuFTkNmWaj4aUuc%2FxdpBDSBVy1XgD7VJN4V0V%2FoXJBaYz6vLMyDTTrOuRaZcpE7Lw4vbTTUP6bskrTkE%2FEuJQ5MBaKcl5smkhOqLECEcW9qDJnZid3LSpqUEbHppujbfxSjMkhVRmt7AxTUOSd8JwulyLNK7Uk%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=23L%2FeDVgxN%2FhfPDGQtGQ4w%3D%3D.DWpaZ33KUARDzGp2dl%2FNOJZW4%2FvPE4HigTuV9dj2R112qPSQVqZ5a0DSpeqMRsDZL8%2F42aZQLBPNVDDZpHw0JGVc30O07E3xl6ySkhJqthKcdvFqeUFPzuQNpKCa1fbdC3ZjuXAM%2B0jqxufMUPqHf%2Fe7q%2FWsnh4GIRRuovKnIZ1x983d%2BHSnPOcX3Yr8ZBaYzEtCK0B5Ku1v5M%2FwWcGQtJBigw4yvC5daGDYPUs%2BpvBqbg4EkP3yyOqmkWN%2BMJQDCwmQAthlW3ZCgYT0aWFj8LzuO4dk2OrZbVo1bFXwOiGgWWZjsiQ7LBGs%2BEOz%2Fqoy%2BW4j3DugsU05eN81YVJIjqVGf65%2FTspokB5%2BIAG8fTI%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=LpXqnkfGZ1DnmcD7AqkNRA%3D%3D.p5uPk%2FdIbSRWa%2BYrqv0i5hFfm0F0zWMKSBnzbovm1BbctWue2lN7471o0AxoRWOnXiI0vX78rKCFZc%2FuGKVcCFekOfcQF52myhAiFA0fm32VUVVSQ61FJxxMz%2BYcvm0PGWtZ2hpDe9eKGW04IEsM%2Bj4Xl1Hl%2FikbsqrFhXGbuj7EJesSeVBYEe2st%2B9bYmRTk7sDtMieAo%2BDlrJ6Kbs4wmjjR9YNz6157Zt42kwXdzGqRdsR3I2VSoCEIHjOXHJpFe1DZliI8jLD76yAWfCcX06HHKkODbyzMvSeRllYFolzyniDAoh0D%2FJXfT1JeRFQWMZIInKB5EH90DOkqx1v0uhGbD36jwRnMKwsB3mvIr0%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=lyCU%2BYgSx0up8ylYSyV1YA%3D%3D.ZxDE7FHjyWLd4XOkAgBjnv%2BHUq9jPabCRcb52pTB56KvRW4EyDVddIuk%2BZusEPDDsMtFv3H8gnOzw71l6NVbSFURbkExXOP2hCCos0a5j9Lw%2BIHI9PiFHXaIkqQ58UejHvE3tklMRYC1RaF2PKUpW711IOk%2BowETgi9EW8aGdF0wUVTKyKVVu3Bf1McVdzt3%2BM9Rm5eOHZd7fHG2Y5bzrrd1%2FciafDHyV1YJ5mtJIliU2KJRtx3iwNSsCY7ubhki7JE%2FkPEsC%2BKcrvQHI3JY68SwOLeATNJ53EqSkNxiXgMeR%2FnKN8mUZdiySqNJBT8VWjy16W%2BaaKjwyoraHZ8fMkouizqoNbBo%2B06iBgl8G4I%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=oZ0s3nTaW%2F8eIIG3cVUEzg%3D%3D.NDdgWQ%2Bk91uMf8MJWil1Jk67xJLZxErIKZKNBkBQVbBTOsJw7EjJVTAdsYlyjzdcRI3l0Z2Egas21VRujRN0GfnKgMIPjUXkJwQcR6dNy%2Flspju4ttqmDk7j%2B2aEr%2FQQTnW%2FzfYyuGOxA%2B%2FVw5bA3b5%2BV5Gu%2FLY3M0r%2FO%2BU4mHsl0JhM8mzmlcew7WapXQe1U9n4cfmAKk9%2B47by5fSxH3ZXADgVMgsHIRW4Ci6mu1sxbjewSnpW%2F7P1CnnWmenbnGKOP5R1my%2B%2FVwCGnXpFwEmMwqUmZKNMFuj1KPhyadJHfqO2V6wtuwQ6UbG0ZJJmHO1Kz32tJS6eB5ErpoVhB5Wk4Ln6dUgqbDGbB6SnMhI%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=vWgltZzoA1inA7%2FLdyA3tw%3D%3D.jfQrPDYLR4N1%2FRYqd0S66AAPGPkZD1PiUiYmEHW9d%2BE1KEw7UQftJkp8jdjS%2BZIxXAjPYDSLUvm7CBYmyQrXEo52mS9sFGK6PC%2FmjGeliB7eRdAJuaUPKFvSqbzPjxuk8BTW3IImulUxz1k0shHb7jsgHn8oy5DA1ORgc7H53V2CmrFHaWh9EIynv%2BsTP29nx3hxJ9H54vZJo%2BnDkwfHlTv%2FkYEA9HhEPnAUTg2RPLFDYCeYgeaDmTPY0qjKKSUbfjdgFkOnbkZPuN0UygeHMSYMmvBOPuEGHvku467Mk%2BuhaYf3b3DOtzV0v65USfWnqIuTBNwcflAVLFxCDzEo59BzO%2F9MMfxqra5LfG7ZIOg%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
来看一看,如何在魔盒(大数据开发协作平台)中提高RabbitMQ消费速度
https://segmentfault.com/a/1190000022936743
2020-06-15T15:37:30+08:00
2020-06-15T15:37:30+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote>1.本篇主要介绍实际的生产项目中,在消费者集群资源有限的前提下,通过哪些优化手段可以去提高 RabbitMQ 消费端的消费速度。 <br>2.为了帮助大家能够更清晰的认识问题,文中特意将优化前和优化后的耗时进行了一个总结对比,文末提供有 demo 下载地址。<br>3.可以访问 <a href="https://link.segmentfault.com/?enc=0KDCpzJDHCXetYjqHreWYA%3D%3D.WDt0D23nC5sCY2MgoyT7nGQKYVBZFmsFBYK9SUeEg6wNYMT2KKcYFgv95VipAtGP7AEes3VvoL%2FqgtSDkUTtji2wdYdMNhRywf96hCHeOBQnPPnVy6bMFB%2BOVVUCKRyz7sC6%2FF4l229yPb7qqVd271iBhdp%2B%2Fa%2FpZpJCIuTYTFMGMyQz%2BBtICmE8%2Brgufa7C" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h3>一. 魔盒简介</h3><blockquote><ol><li>魔盒是禧云数芯大数据开发平台中的一个开发协作平台;</li><li>数据开发人员通过魔盒可以很方便的完成离线任务和实时任务的打包、测试、发布上线;</li><li>支持离线任务的串行、并行工作流设置;</li><li>提供完善的任务运行监控报警体系。</li></ol></blockquote><h3>二. 魔盒离线任务打包流程</h3><h4>流程图</h4><p><img src="/img/bVbInXz" alt="魔盒打包流程.png" title="魔盒打包流程.png"></p><h4>流程梳理</h4><ol><li>数据开发人员在线下收到需求代码开发完毕之后,可以通过魔盒在测试环境创建 Spark 任务;</li><li>数据开发人员通过点击页面的项目构建按钮,发起一个对该 Spark 任务进行打包的请求;</li></ol><p><img src="/img/bVbInXE" alt="packaging1.png" title="packaging1.png"></p><ol><li>服务端收到打包请求后,会更新该条数据的状态为待构建;</li><li>定时任务每隔 1 秒从数据库中扫描一次,发现有待构建的任务,则就将任务放入队列中;</li><li>消息的消费者收到消息(即将打包的数据),开始对服务器上的项目进行打包(<strong>打包过程可能需要1-5分钟左右</strong>),通过 WebSocket 服务将打包日志返回给前端;</li></ol><p><img src="/img/bVbInYh" alt="packaging3.png" title="packaging3.png"></p><ol><li>前端(WebSocket 客户端)收到打包日志后,在界面窗口内实时展示,直到打包完成。</li></ol><p><img src="/img/bVbInYr" alt="packaging2.png" title="packaging2.png"></p><h3>三. 使用RabbitMQ的场景</h3><ol><li>服务端的定时任务只要从数据库中查找到有待构建的任务,就将该任务放入队列中,不用考虑该消息什么时候被消费掉,在这里充当的就是RabbitMQ 生产端的角色;</li><li>RabbitMQ 消费端监听该队列,收到消息后,执行业务逻辑,通过后台执行 mvn 命令进行打包;</li><li>为了保障打包流程应用的健壮性,我们将消息的生产端和消息的消费端部署为两个服务。</li></ol><h3>四. 存在的问题</h3><ol><li>由于 mvn 打包的过程比较耗时(<strong>打包过程可能需要耗时1-5分钟左右</strong>),所以消费端消费消息会比较缓慢,会有较多 unack 状态的消息,导致产生消息积压;</li><li>当前端发起过多的打包请求时,由于队列中有较多的消息等待消费,因此吞吐量大大降低,反映出来的现象就是<strong>你的打包请求可能要等较长的时间后才能被服务端消费掉,因此在界面上要等待较长的时间才能看到打包日志的输出</strong>。</li></ol><h3>五. 优化思路</h3><h4>1. 开启消费者多线程</h4><p>在RabbitMQ中,我们可以创建多个消费者来消费同一个队列,从而提升消费速度。<br><img src="/img/bVbInYY" alt="多个消费者.png" title="多个消费者.png"></p><h5>添加容器工厂配置</h5><pre><code>/**
* RabbitMQ配置
* 开启消费者多线程,同时消费消息
* @author lyf
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-06-02 06:10
*/
@Slf4j
@Configuration
public class RabbitConsumerConfig {
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
// 设置线程数
factory.setConcurrentConsumers(10);
// 设置最大线程数
factory.setMaxConcurrentConsumers(10);
configurer.configure(factory,connectionFactory);
return factory;
}
}</code></pre><h5>消费端使用该容器工厂</h5><pre><code>@RabbitListener(queues = {QueueConstants.QUEUE_NAME}, containerFactory = "customContainerFactory")</code></pre><h5>查看设置效果</h5><p>重启服务后,打开 RabbitMQ 管理界面,找到本次发送消息使用的队列并点击,会跳转到队列详情页里,在 Consumers 这一栏里会显示消费者的一些信息:<br><img src="/img/bVbIn1M" alt="Consumers1.png" title="Consumers1.png"></p><p>从图中可以看出,现在一共有 10 个消费者,证明我们刚刚的配置是生效的。</p><h4>2. 消费端限流机制</h4><h5>轮询分发</h5><p>在 RabbitMQ 中,默认的消息分发机制是 轮询分发。多个消费者会从队列里依次轮序去消费消息:</p><blockquote>比如当前总共有 10 条消息,2个消费者, RabbitMQ Server 不会考虑当前哪个消费者是空闲状态还是繁忙状态,而是会一次性分配给 2个消费者每个消费者 5 条消息,平均每个消费者会获得相同数量的消息。</blockquote><p>下面通过一个简单的 demo 去感受一下这种消息分发机制会有什么弊端。</p><ol><li>生产端</li></ol><pre><code>/**
* 消息生产端
*
* @author lyf
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-06-11 06:30
*/
@Slf4j
@Data
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public String send() {
for (int i = 1; i < 11; i++) {
String message = "NO. " + i;
String msgId = UUID.randomUUID().toString();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
rabbitTemplate.convertAndSend("", QueueConstants.QUEUE_NAME, message, new CorrelationData(msgId));
log.info("生产端发送消息:[{}] 成功。", message);
}
return "发送消息成功";
}
}</code></pre><ol><li>消费端A</li></ol><pre><code>/**
* 消息消费端A
*
* @author Liuyongfei
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-06-11 13:30
*/
@Slf4j
@Data
@Component
public class Consumer1Controller {
@RabbitListener(queues = {QueueConstants.QUEUE\_NAME})
public void work(Message message, Channel channel) {
// 获取消息
String info = (String) message.getPayload();
log.info("消费者A获取到消息: {}", info);
try {
// 获取header
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
TimeUnit.MILLISECONDS.sleep(200);
channel.basicAck(tag, false);
} catch (InterruptedException | IOException e) {
log.error("获取消息发生异常: " + e.getMessage());
}
}
}</code></pre><ol><li>消费端B</li></ol><pre><code>/**
* 消息消费端B
*
* @author Liuyongfei
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-06-11 13:50
*/
@Slf4j
@Data
@Component
public class Consumer2Controller {
@RabbitListener(queues = {QueueConstants.QUEUE_NAME})
public void work(Message message, Channel channel) {
// 获取消息
String info = (String) message.getPayload();
log.info("消费者B获取到消息: {}", info);
try {
// 获取header
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY\_TAG);
TimeUnit.MILLISECONDS.sleep(200);
channel.basicAck(tag, false);
} catch (InterruptedException | IOException e) {
log.error("获取消息发生异常: " + e.getMessage());
}
}
}</code></pre><ol><li>查看消费情况</li></ol><p><img src="/img/bVbIn2v" alt="consumer-console.png" title="consumer-console.png"><br>我们从 idea console 控制台里可以看到两个消费者的消费情况,Queue 中的消息会被平摊给多个消费者。</p><ol><li><p>弊端</p><ul><li>如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况;</li><li>具体到我们的实际项目中,<strong>有的 Spark 任务在打包的时候可能需要依赖的 jar 包比较少,有的可能需要依赖的 jar 包比较多,还有打包时网络的影响,因此每个消费端处理具体的打包动作时的耗时是不一样的</strong>,所以使用轮询分发而不是按照消费者的实际能力去分发这种机制肯定会大大降低消费端的吞吐量。</li></ul></li></ol><h5>公平分发</h5><p>基于轮询分发机制会遇到的各种问题,那么怎样才能做到按照消费者的能力去公平的消费消息呢? <br>我们使用<strong>消费端限流和 ack 确认机制</strong>,来修改 RabbitMQ 的默认消息分发机制:<br><img src="/img/bVbIn26" alt="消息公平分发.jpeg" title="消息公平分发.jpeg"></p><p>确保每个消费者在同一个时间点最多只处理一个 Message,换句话说,在接收到该消费者的 ack 之前,RabbitMQ Server不会将新的 Message 分发给该消费者。</p><ul><li>application.properties<br>在配置文件<strong>application.properties</strong>里添加以下两行配置:</li></ul><pre><code># 开启 ACK(消费者接收到消息时手动确认)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 设置当前信道最大预获取消息量为1
spring.rabbitmq.listener.simple.prefetch=1</code></pre><p>重启服务,从 RabbitMQ 的管理界面查看:<br><img src="/img/bVbIn4p" alt="公平分发1.png" title="公平分发1.png"></p><ul><li>查看消费情况</li></ul><p>我们将上面的消费者B的 sleep 时间调大为 1000 毫秒,用来模拟消费者B处理比较耗时的任务。然后重启服务,再次查看消费情况:<br> <img src="/img/bVbIn4t" alt="consumer-console2.png" title="consumer-console2.png"><br>从 idea console 控制台可以看出,经过设置后 RabbitMQ 已经按消费者的实际能力去分配消息了。</p><h3>六. 打包任务</h3><p>我们假设现在魔盒的数据库中有 5 条需要打包的数据,下面我们就使用这 5 条数据来作为后边的测试数据:<br><img src="/img/bVbIn4R" alt="将要打包的数据1.png" title="将要打包的数据1.png"></p><p>备注:</p><ol><li>以下测试均在本机完成,打包耗时会受到本地机器配置和网络环境的影响;</li><li><p>后边测试中的耗时时间包含以下几个流程:</p><ul><li>先从 git 仓库拉取所使用的分支代码;</li><li>对当前项目代码进行打包;</li><li>打包完成后自动将 jar 包上传至 HDFS 中去。</li></ul></li></ol><h3>七. 优化前</h3><h4>1. 打包数据放入队列</h4><p><img src="/img/bVbIo2U" alt="打包console1.png" title="打包console1.png"></p><p>从图中我们可以看出这 5 条数据在 15:06 分的时候按照 version_id 由小到大的顺序被放入到 RabbitMQ 的队列中去:<br><img src="/img/bVbIo23" alt="queues1.png" title="queues1.png"></p><p>消费端使用的是 1 个消费者:<br><img src="/img/bVbIo25" alt="consumers2.png" title="consumers2.png"></p><h4>2. 消费时间</h4><p>通过查询打包日志,可以找到最后一条消息的消费情况:</p><p><img src="/img/bVbIo29" alt="打包2.png" title="打包2.png"><br><img src="/img/bVbIo3b" alt="打包3.png" title="打包3.png"></p><p>可以看到:</p><ul><li>最后一条消息在 15:10 开始消费;</li><li>于 15:13 打包完毕;</li><li>从 15:06 到 15:13 最后一个任务打包完毕,总共持续大约 7分钟。</li></ul><h3>八. 优化后</h3><p>在消费端使用的是 10 个消费者,且设置当前信道最大预获取消息量(prefetch count)为1:</p><p><img src="/img/bVbIo3e" alt="优化后prefetch count.png" title="优化后prefetch count.png"></p><h4>1. 打包数据放入队列</h4><p><img src="/img/bVbIo3g" alt="优化后消息入列.png" title="优化后消息入列.png"><br>从图中可以看到消息在 18:17 放入到队列中去。</p><h4>2. 消费时间</h4><p>通过查询打包日志,我们可以看到启动了多个线程同时消费消息:</p><pre><code>2020-06-12 18:17:50,931 INFO LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=388......
......
2020-06-12 18:17:50,932 INFO LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=390, ......
......
2020-06-12 18:17:50,932 INFO LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=386, ......
......
2020-06-12 18:17:50,932 INFO LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=387, ......
......
2020-06-12 18:17:50,932 INFO LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=389, ......</code></pre><p>可以看到最后一条消息的消费情况:<br><img src="/img/bVbIo3m" alt="优化后打包时间.png" title="优化后打包时间.png"></p><p>可以看到:</p><ul><li>启动了多个线程,一共10个消费者;</li><li>从 18:17:50 到18:21 最后一个任务打包完毕,总共持续大约 3分钟左右;</li><li>从优化前的 7 分钟 到 优化后的 3 分钟,耗时大大缩短,消费端的吞吐量得到了很大的提高。</li></ul><h3>九. 代码下载地址</h3><ul><li><a href="https://link.segmentfault.com/?enc=I8jTy1%2F6%2F0lUW7UE7HJrqQ%3D%3D.yIu2pHtskvf%2F0C9UQcQK13uhaDXd3o6z1UmuH%2BBDd9GDhPhYOvkBgloG3GDksNM3" rel="nofollow">https://github.com/liuyongfei1/blog-demo</a></li><li>克隆到本地后,请将代码切换至<code>feature/rabbitmq-polling-dispatch</code>分支。</li></ul><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=h2HAGv0TqDUthCcVbhhkew%3D%3D.BESaGgyrEdyVW%2BYYpy3Ffb%2FC%2FPnvq28iSes7geoBxeoasREv266426%2BBa1PtCClq1BfWZrN%2F3SPUArewJX%2BS9H4fq786lJuuUeesPUm5p20Jvu%2BLKyih2k8TNFsBTzH%2BeXwoR%2Bg5b03FNLMrZsLhX9xh11GKmwqlDNzfX23oIzj1yjo7s6Watwb8MOzXjyWo%2FdYOHdm%2BmMFm6O7NkjEcIxIUXLremWIegYKQtQ8ddFNcz9gae6z8TJojNgQCj6i2ARXlQqW%2FFEyz3CLjiFEY7tWFx3nEaIYYDQGOtWA%2Fd7KaMPBw%2BjTk2VLckY0RfJndilcCTpSFwKRvkvvpSrnuJXCWRSpqtm6kwrzAakaSeEM%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=KGFnN8Rl48z6cI39EWqeew%3D%3D.xBPgSY8N2hEK%2FwLLC9tlgDUmJ%2BZpqR%2F%2B3rTrs0TVu2I%2F8L%2BogpheCkCyQYhuPulTOgK6cvni1TZYYISdpwoJ1sSXhoZW7cxm1HM79VJilBNCLQRFQMYHStkJcCx1bRhBXeykJ3dzNQjdPt6EP%2FmjD9hphTKJuN4VRAU2VH%2BkTQXBHa7x8AJAkDWqLglgldwKznBKtB7d0QXtFIzw9Su0PiRkaKFkW2slEQmGREuMwUF7Dpq3XJKonJRFH1CqXqjx3dXlN9F1po3vY1dxd%2Bk2HU9Dh5R4KaiwHuoVFpvu%2F5JcoTfJ1xwoMjmq3SvjWvSOrJMQkQgKZIFxOO3DicChbymFoL5jML0CiESrIKvr7lw%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=bMquL2P3t%2FxgbcxPly087A%3D%3D.iB0OXT7Rcie1KTAd7mG20SkF%2Fxgqcgn6N5qApwb2FL2koQFWuCopCJm6RtfBFNExyUKln%2F5fWAWD%2FhUDq1dxtvGv8fVnrOso6W3PyRqa%2FPLjlpXytkfsN1QAJdH0CwYa2ZImYyZcgAvgOVZ7Q6dw%2FmfGUIlRIFuE6KF4HGq9EjXC%2FSXqxckh678vcC5kb4vHv4%2BHGSHjrgLWDmoQKzh5Igud%2BBsF0KSU3hV69Ov9ptLbC5nC05r0mvHS9e0LkhnW15lSHPoNWlSd2asW1kltonBF2ZWTieUocMzdY5IHrwwzF3wHVko23AV76U0%2Ff%2B9hRlxPhGRHsJJUydyWusEiO4lV3oYAUQ781gopj5icSso%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=aM6G3DAIWBob%2BwVGYj5ffw%3D%3D.3fB3EG34gGlee5tdCnMKNolPrKbaU8meWc3GdsYkLI9HE%2Bo4EOtL9amVDh1OSMFbYkucYeiU7JqTBAlkXh4JcWRqxtEimQwlOzshqWtygGaL3zX8YsmjTbVINCPFftZ%2FOtrCh3v2F%2Bu9%2FSNYmRgx3aYp7dn9q7KpXeEuH4Y39zBeOX%2BaG23%2FROFWiabxKc9rQZ6WBWQwfmRuZHmn1pNoSnqa4MkE7byoqqxzBNy0tJjBVo0ssdEuzPzF4PIydKTaoQSJEhwHkVuraUONuIyD%2FOCuj1HCkQjespmh5aqYZKPXUax0sst7yM8HwaB%2FBrXNOY%2FSELWR8voapwObw6bw1KxzQmDMBc5wQEw2TkT9JWY%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=xt%2BIzh4pkt9eQmKtbI46lw%3D%3D.fNeAYnbF8AAnRQ0W4A%2BGljRXsznOqiIUojrtIBTeL%2FjSUNxIwefvj5qrP43cDAS50%2BrmgZvfeuZBhbR2yMftpk7xmXzNuhchgcy3QZPG2OUdYMZifb3sVMFztfvnv5wx8BOnlj3nKcFGjPNel37mYdb1CG3oroMKliiGNjBrhBPiocuTMFbGceh43EN8PtYSgAe2fMgo44wKUdVH17bCPNbCzYiageS1181HwvZrM5P6btV85fZRFrFCRxqEbOa%2FbvMGdit3%2BcO9tXY%2Bf%2FYZnXStJLN81nLbzVayKQE9z2Ex37KdwcpTW6xJ2QSoFjmf9nQYfScHzXBuEMHADz%2FF4qPsWfwc5l3TgkbDn9q%2BfZI%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=ODc95AMOvKfOiCuLF0TAsQ%3D%3D.8RWxtS5dU6PQBoVajCqR55Cx22nSf5ks1DGF88Bkl%2BLHc0mvXZU%2FkLZVfnJPbJ9bDaB8SkN4PMT6SltPl5HwUUAFIZ%2FOWCJ1MdPITH8Djk%2FEQ%2FB5H2i4lpQ95E2n%2BKwCDHT61xQk0TMGRs1gEmXrEdcdGmcUPuTTHvfYwEXhjtSVpBgy2YPI7l7i7K0Nefw7vwCcAeoqHfWynqKFkrchoI3L7pD6ptVmNASA%2FjBcR97S9%2FK9wv3F%2FFqREC1Ps8pKFTsUVfEWIKKyKbRoAiokosSUnRx%2B8hgQza8%2Bf7j6yRIl7TBB2%2BRw56BjRFjGWRwbPqLL1izCdR6fgNFvgCHeoKDP3xN0AhrklBURP8F5Wow%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
RabbitMQ系列之单机模式安装
https://segmentfault.com/a/1190000022827535
2020-06-03T16:51:17+08:00
2020-06-03T16:51:17+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote>关于 RabbitMQ 的安装,官方<a href="https://link.segmentfault.com/?enc=zvVjQ6n%2FBjPEmk9kapGOHg%3D%3D.FDdUg6X3ng%2BUc2VgeSJbtRLQff6OikBqGqFh%2BL2iq5WFBLPgjq0l%2BrR6PW%2Btsi4o" rel="nofollow">文档</a>里对不同的操作系统的安装都介绍的很详细。 <br>本文主要记录一下本人使用 macOS 安装时遇到的问题以及解决办法,方便以后查阅。</blockquote>
<h3>操作系统版本</h3>
<p>macOS Sierra,版本 10.12.6。</p>
<h3>安装</h3>
<p>使用 macOS 安装 RabbitMQ 非常方便,推荐使用 Homebrew 包管理器。</p>
<h4>1. 先安装 Homebrew</h4>
<p>如果本机没有安装 Homebrew(可以通过在终端输入 brew 指令来判断),请执行以下命令:</p>
<pre><code>/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"</code></pre>
<p>回车即可。</p>
<h5>安装失败</h5>
<p>由于需要下载国外的源,有可能会报 443的错误:</p>
<pre><code>curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused</code></pre>
<h5>解决办法一</h5>
<p>如果有fq工具打开fq工具即可暴力解决。</p>
<h5>解决办法二</h5>
<ol>
<li>查看网址对应的 IP 地址 <br> 打开<a href="https://link.segmentfault.com/?enc=9LG00SUX4NMvuLint8STRg%3D%3D.nCEg6xIPDQTppYjiMImP89XV90z3BHmdsN7Y9fZ%2FAhY%3D" rel="nofollow">https://www.ipaddress.com/</a>,查询<code>raw.githubusercontent.com</code>对应的 IP 地址:<br><img src="/img/bVbHNWB" alt="ipaddress.png" title="ipaddress.png">
</li>
<li>
<p>修改系统的<code>host</code>文件</p>
<pre><code>sudo vim /etc/hosts</code></pre>
<p>打开 hosts 文件,将刚刚查询到的 IP 地址添加进去:</p>
<pre><code> 199.232.68.133 raw.githubusercontent.com</code></pre>
<p>保存后,重新执行上面贴出的安装 Homebrew 命令。</p>
</li>
</ol>
<h4>2. 安装RabbitMQ</h4>
<pre><code>#自动升级homebrew
brew update
#安装RabbitMQ server
brew install rabbitmq</code></pre>
<p>备注:</p>
<p><strong>RabbitMQ 的运行需要依赖 Erlang 环境,使用此安装命令会自动帮我们安装 Erlang 环境及其依赖,这就是使用 brew 的方便之处。</strong></p>
<h5>安装失败</h5>
<p>由于 brew 默认的官方更新源都是存放在 GitHub 上的,因此速度太慢经常会导致下载 Erlang 或其它依赖时经常下载失败:</p>
<pre><code>Error: Failed to download resource "erlang"
Download failed: https://homebrew.bintray.com/bottles/.....</code></pre>
<h5>解决办法</h5>
<p>最好的解决办法是更换成国内的镜像源。 <br>Homebrew 的更新源由三部分组成:本体(brew.git)、核心(homebrew-core.git)以及二进制预编译包(homebrew-bottles),我这里使用的是中国科大的镜像。</p>
<p>1.替换brew.git</p>
<pre><code>$ cd "$(brew --repo)"
$ git remote set-url origin https://mirrors.ustc.edu.cn/brew.git</code></pre>
<p>2.替换homebrew-core.git</p>
<pre><code>$ cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
$ git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git</code></pre>
<p>3.替换homebrew-cask</p>
<pre><code>$ cd "$(brew --repo)"/Library/Taps/homebrew/homebrew-cask
$ git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-cask.git</code></pre>
<p>4.替换Homebrew Bottles源</p>
<pre><code>$ echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >> ~/.bash_profile
$ source ~/.bash_profile</code></pre>
<h4>3. 安装成功</h4>
<p>安装完成后可以用<code>brew info rabbitmq</code>检查信息:</p>
<pre><code>$ brew info rabbitmq
rabbitmq: stable 3.8.3
Messaging broker
https://www.rabbitmq.com
/usr/local/Cellar/rabbitmq/3.8.3 (281 files, 20.4MB)
Built from source on 2020-05-02 at 19:04:15
From: https://mirrors.ustc.edu.cn/homebrew-core.git/Formula/rabbitmq.rb
==> Dependencies
Required: erlang ✔
==> Caveats
Management Plugin enabled by default at http://localhost:15672
Bash completion has been installed to:
/usr/local/etc/bash_completion.d
To have launchd start rabbitmq now and restart at login:
brew services start rabbitmq
Or, if you don't want/need a background service you can just run:
rabbitmq-server
==> Analytics
install: 9,648 (30 days), 33,180 (90 days), 137,570 (365 days)
install-on-request: 9,471 (30 days), 32,348 (90 days), 132,710 (365 days)
build-error: 0 (30 days)</code></pre>
<h5>访问 rabbitmq 管理界面</h5>
<p>浏览器访问<a href="https://link.segmentfault.com/?enc=taO1%2Fa0TjDSYDG7Ws%2Blv5g%3D%3D.YYUe%2F4ucAn35325tgFeKZ8whsRJkpGzGUxAZngModOc%3D" rel="nofollow">http://localhost:15672</a>,用户名和密码都是 guest:<br><img src="/img/bVbHNW6" alt="localhost.png" title="localhost.png"></p>
<h3>关注微信公众号</h3>
<p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
使用IDEA启动项目遇见ClassNotFoundException的正确解决姿势
https://segmentfault.com/a/1190000022821021
2020-06-03T09:25:17+08:00
2020-06-03T09:25:17+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<h3>一. 错误现象</h3><p>本地开发 SpringBoot 项目的时候,在集成 MyBatis 查询数据库的时候,使用 IDEA 启动项目的时候,有时候会遇见如下的报错:</p><pre><code>Caused by: java.sql.SQLException: com.mysql.jdbc.Driver at com.alibaba.druid.util.JdbcUtils.createDriver ...... Caused by: java.lang.ClassNotFoundException: com.mysql.jdbc.Driver at java.net.URLClassLoader.findClass(URLClassLoader.java:382) .......</code></pre><h3>二. 正确的解决姿势</h3><h4>1. 是否添加了mysql 驱动</h4><p>去 pom.xml 里查看是否添加了mysql 驱动。如果之前没有添加,需要引入 mysql 的 jar 驱动:</p><pre><code><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency></code></pre><p>重新编译后运行,如果还是报这个错,看第 2 步。</p><h4>2. 执行 mvn install</h4><p>执行 IDEA 里的 mvn install 命令,下载可能缺失的 jar 包。 如果重新编译后运行还是不行,看第 3 步。</p><h4>3. 检查配置文件是否有错</h4><p>.yml 和 .properties 的配置文件对格式都有严格要求,确认一下自己的数据库连接配置是否有格式错误。 <br><strong>比如:</strong></p><ul><li>行首和行位是否有空格之类的。</li></ul><h4>4. 找到原因</h4><p>笔者按照前面的 3 个步骤反复检查确认,依然还是报这个错误,折腾了半天。会不会 IDEA 哪个地方的设置有问题。</p><h5>命令行执行jar包</h5><p>为了验证猜想,于是进到 jar 包所在的工程目录下面, 在命令行下直接使用java -jar 的方式启动:</p><pre><code>`java -jar projectName.jar`</code></pre><p>却发现服务能够正常启动起来,没有报java.lang.ClassNotFoundException这个错误。 由于我的项目是 SpringBoot 聚合工程,于是就猜测会不会是这个 模块下的依赖没有被 IDEA 读到呢?</p><h5>验证猜想</h5><p>通过 File -> Project Structure -> Project Settings -> Modules 打开 弹窗,选中该模块,在右侧找到 Dependencies 选项卡并打开,在下面会出现该模块依赖的 jar 包列表:<br><img src="/img/bVbHUUE" alt="dependencies1.png" title="dependencies1.png"><br><strong>仔细找了一会儿,发现竟然没有找到第 1 步添加的 mysql 的 jar 包。</strong> <br>找到了问题的原因,下面就列出三种解决办法。</p><h4>5. 添加Module的 Dependencies</h4><h5>方式一</h5><ul><li>打开添加 Dependencies 的弹窗</li><li>添加 mysql jar包</li></ul><p>点 <code>+</code>号,然后选择 <code>Library</code>:<br><img src="/img/bVbHUUU" alt="dependencies3.png" title="dependencies3.png"><br>点 <code>Add Selected</code> 保存。</p><h5>方式二</h5><ul><li>选中模块后右键</li></ul><p><img src="/img/bVbHUVT" alt="maven1.png" title="maven1.png"><br>在弹出的菜单中选择<code>Reimport</code>,则会重新从 pom.xml 里解析并下载依赖。</p><h5>方式三</h5><p>如果你在在开发过程中新建了一个模块,然后删除了该模块,后边又新建了一个相同名字的模块,则依赖也是引不进来的。 这是因为 IDEA 默认已经删除的模块将不再使用,解决办法:</p><ol><li>打开 项目目录 .idea/misc.xml 文件:</li></ol><p><img src="/img/bVbHUWd" alt="misc.png" title="misc.png"><br>删除掉圈红的该行(对应你之前删掉的模块名)</p><ol><li>重新新建同名的模块,依赖就可以正常导入。</li></ol><h3>三. 总结</h3><ol><li>遇到这种情况,先确认代码级别是否有什么遗漏或者配置文件是否格式有错误,注意不要忘记执行 clean 、install、package ;</li><li><p>可以在命令行执行 Jar 包:</p><ul><li>如果仍然报同样的错误,则还是在代码级别上有错误,需要仔细的排查;</li><li>如果不报错,那就可以确定是 IDEA 在某处的设置有问题,比如 Jar 包的版本默认选择不对、mvn选择的版本过低、module 的依赖没有引进来等。</li></ul></li></ol><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=8P9u85CzcK71752odWfYYg%3D%3D.8gYso9diGFAQ6Hp8NBbGiFNNa%2FbDaug5Q2Lb1rRgbBeMFpTv9Zm9C4Oqy7ug%2FQXxVxx72mHsuF%2F6ZF1Le2QbnTtpyDyYKSE89mifEsc4klZ%2FAt3ry15BZu5%2FMwpHGgbQVLGCfxxjst%2B1gaT7NTC2%2BdgQqNj%2BlVindaa%2BeHBnpCP7%2FqEjOeeD1dpTdcD1p%2FDGPE%2BdzNmoT9jqCq%2F0G8W7cLmKFKPtG814bytB2DVYLVEl4%2BC7vsscgwpHpyQYnpjgLrTMZbxdiUVyVHfXjte9Xe3Dddk4AWggrLSIpfxJAdErMwJY2QR%2BqSsS83gTLWRPJhG4sRFjzDIwTYry3g4EVAtUlajSEOBR5AF0ScO9jR8%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=qJM7%2F1dtC2C6LODe4cGxtQ%3D%3D.ZpiJcbNQxRaqOvKYIZmh8EvXEThWbp4fFcOyidljAn8FKbNa3N4UaaAxYzpr61mHAy2vHXuYh6sCWVedXPqaUTYX8rrEkrZD4r1B%2FwD5latwZPV9VFNBXTlx%2FQ34oj2XjcKWUBC84fnYVAVF28O5F1HELrABGT1IF2wXI2PK4B3KSUx2OX%2FGyAf9RIpssw%2BLgJIN7M5zjQoX691s5UgcwPszNkMh7bat25SZm9aXJbbHP9F5Q1%2FYO1DHLCxagvRTiSpNNm7sLNYXqKKPFMmI7QscIlyGaqSyK8gUDF%2Bx8qD3hgHth3hp1IrmsU2Fy7h3qmj0Y3JMXzWrlTRjV2RAEiHlMYCg%2FZAF0ZwF6eGah%2Bs%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=rxPGcmHIwLr9%2B4nYTKNj0Q%3D%3D.7IswoVmQG%2BnO72qwhnn62XG71xj3pQ7c4ZNGEW8h4xKDvTkoO5h1wlc8M5aJcEcPjsVybD0o6f8LwT6ub3TnfCfX9Fb1MZ8gDqeLLVH9NsNO2xWU2IkYG9aVFOFXIOEFs1JBSPhvYiLdtXmcV2xbMpAIlo0Z6iPZDBEP2fB5KG9MnKfrKSHL0DEs2TLdZXVvyyHBJ7vrlmeDJ1BRHn254q%2B1u8Mw7XX8Baf5iSdnyl4Avem0Yfu%2F8AaGQINCNTwqqWFbHUYoLtwJ%2BXeV1AdTA2%2F3dUUd9q8NTqtCXGBNYgesV%2FNGBvhXUlZWS%2B2IHqf0wfbGdWYvqqXwmohAQMGx86lZvnO7MCnPi2GdH2Y8cps%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=INzY%2Bx6xH%2BnlOGFdTG%2BHZg%3D%3D.jZxyJpLVR2xB26LtE0BBqAiJFW6ClpT7qNqleC0QK84UmmkwofHIh0vOOY40UEdwIYqKLwmoaZxA%2BhUSNrYyZlQp056YxIs%2BHHEfIybP0%2BYTLqjegKirYPR5JZsSOClo9rGlJPSUYk%2FBw5x95h%2FrYV7291QYO6xOXh2TTBREZMjRDW3k%2B8UyLhtWHVgQs3Kj%2BZJoZ%2FchSnCpePHzgolqd%2BX0D2SYeRgYSDNwijfcF%2By4bOxrhzsU2yGP5XTWvasVwZrdj2SYxOV6gp0JE0xOLwTsgoEbbxjeH9mQdNLowi4zHNliobdEUbtG7lzBIF7Y4GEmZPa8J9aLKAzFm%2BxCg2kTBD9BcDAtX0rk%2BfecgCA%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=ohcofPfMkJhQGankQv%2FVIQ%3D%3D.jkp9MJQHWuFWaIR1COsSergjfy0HVpbAbscQtUjmd6%2BVluvtOGMfEjDTS2wfrIYDm9xKeh0iE6MjC5HB1F7Nl31c8ClDjThBmh8UojNeetR7oe3C1DslarhQM%2F%2BcNNHMssokqIVWlXjsXfeSQBWpCv%2BOQ65JwWRur5YWYomcd2Y3svXrlOaUYUoFnRIXr68T0yZFXhTR3fSxz55QdxcEUnBDE95PaNwlQs%2Fan3ac5pAn0c18ZCjlWwP13qPNNCvxN6EwYTQVwbdlIaqqIZ7rBazHaTnGsAUOlMKb%2BrqVxXYq9UHwgqfyawI2r0ljui2p%2FhDuqE3xVDn1NMGyO%2BP7jQbZLNnR1lhvPrbBCjODY4w%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=mm1O8AgWSSJTdtZLScb4BA%3D%3D.MS8Nkv7yeQJKHVnQLn2o49nkqmAUOA8uGW4iFRXXb4omyViQvG3GQdXIZDSSK0SWxNIKMQGFVkPzBtPkqZz5yMCQhKHOdmlKLyzqeUIwTW1y5J3OiLctnR4R1N4ggT32Js7gDdZT5cxdjgnp6cp3bbogLLk%2Bn%2BGUbp8UNex9syBM1lSAmeOY9dLKsOsoPETB%2BYEUaaLFcazfBarShYVG58ns3nifAPovtzowhulc7UCSLI%2Bph92cmvOUzz1IOw7nN2VRNKU7PEX9xLQtkg%2BUzrbu%2FxE0aJA21NxAc7J3V%2FC7muSYSdNM2XPYpyknWPxhKReayQ9l9bfLnrvAvTsIs%2FxT4D%2FH7Lbuj9doYmRgw%2BQ%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
MQ介绍
https://segmentfault.com/a/1190000022779612
2020-05-29T10:11:25+08:00
2020-05-29T10:11:25+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<h3>什么是MQ</h3><blockquote>MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。<br>可以访问 <a href="https://link.segmentfault.com/?enc=bdOD8gCb9DG%2BAlFs0LQdDQ%3D%3D.Ei5IgpGYBW%2BVvhAJuFyJjy39Q%2BFrbh4jMoFy%2FMPiuI4HonP4XzvC26IJjFtHZXhnR4X7hVuuGsiFiyZwzpwQrnbJcdd0pu%2FXS9Zcl8xsomDoBKkMJhkHScm%2BPkbSm%2FchXCXeYh7IdoScmzWiXQ0UNMWX5pUEZmFNF9ctT17AGwhNFDwwugFnijF6yNr0XHWF" rel="nofollow">这里</a> 查看更多关于RabbitMQ的原创文章。</blockquote><p><img src="/img/bVbHJ8R" alt="MQ.png" title="MQ.png"></p><h3>为什么使用MQ</h3><p>其应用场景主要包含以下三个方面</p><h4>应用解耦</h4><p>系统的耦合性越高,系统的容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户体验。<br><img src="/img/bVbHJ9j" alt="应用解耦.png" title="应用解耦.png"></p><p>使用消息队列解耦合。比如物流系统发生故障,需要几分钟才能修复好,在这段时间内,物流系统要处理的数据被缓存到消息队列中,用户的下单操作正常完成。当物流系统恢复后,补充处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。<br><img src="/img/bVbHJ9Y" alt="应用解耦2.png" title="应用解耦2.png"></p><h4>流量削峰</h4><p>比如一个秒杀系统,当秒杀开始时,整个系统的流量会突然间增大,这个时候,就有可能把数据库压垮。<br><img src="/img/bVbHJ9Z" alt="流量削峰.png" title="流量削峰.png"></p><p>有了消息队列后,可以将大量请求缓存起来,分流到很长一段时间处理,这样可以大大提高系统的稳定性和用户体验。</p><p><img src="/img/bVbHKab" alt="流量削峰2.png" title="流量削峰2.png"></p><ul><li>一般情况,为了保证系统的稳定性,如果系统负载超过阈值,就会阻止用户请求,这会影响用户体验。</li><li>而如果使用消息队列将请求缓存起来,等待系统处理完毕后通知用户下单完毕,这样总比不能下单体验要好。</li><li>业务系统正常时段的<code>QPS</code>如果是 1000,流量最高峰时是10000,为了应对流浪高峰而去配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰。</li></ul><h4>数据分发</h4><h5>未使用MQ</h5><p>我们先来看这样的一个场景:<br><img src="/img/bVbHKaK" alt="数据分发1.png" title="数据分发1.png"></p><p><code>A</code>系统需要频繁的更改代码,这显然不是一个好的设计,使用<code>MQ</code>可以解决这个问题。</p><h5>使用MQ</h5><p><img src="/img/bVbHKaQ" alt="数据分发2.png" title="数据分发2.png"></p><p>通过消息队列可以让数据在多个系统之间进行流通,数据的生产方不用关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。</p><h3>MQ的缺点</h3><h4>系统的可用性降低</h4><p>系统引入的外部依赖越多,系统的稳定性越差,一旦MQ宕机,就会对业务造成影响。</p><h4>系统的复杂性提高</h4><p><code>MQ</code>的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过<code>MQ</code>进行异步调用:</p><ul><li>如何保证消息没有被重复消费?</li><li>怎么处理消息丢失情况?</li><li>怎么保证消息传递的时效性?</li></ul><h4>一致性问题</h4><p><code>A</code>系统处理完业务,通过<code>MQ</code>给<code>B,C,D</code>三个系统发送消息,如果<code>B</code>系统、<code>C</code>系统处理成功,<code>D</code>系统处理失败:</p><ul><li>如何保证消息数据处理的一致性?</li></ul><h3>各种<code>MQ</code>产品的比较</h3><p>常见的<code>MQ</code>产品包括<code>Kafka</code>,<code>ActiveMQ</code>,<code>RabbitMQ</code>,<code>RocketMQ</code>。<br>| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | <br>| :-------- | :--------:| :------: |:------: |------:| <br>| 开发语言 | java | erlang | java | scala | <br>| 单机吞吐量 | 万级 | 万级 | 10万级 | 10万级 | <br>| 时效性 | ms级 | us级 | ms级 | ms级以内 | <br>| 可用性 | 高(主从架构) | 高(主从架构) | 非常高(分布式架构) | 非常高(分布式架构)| <br>| 功能特性 | 成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好 | 基于erlang开发,所以并发能力强;性能极其好,延时性很低;管理界面较丰富 | MQ功能比较完善,扩展性佳 | 支持主要的MQ功能;像一些消息查询,消息回调等功能没有提供;毕竟是为大数据准备的,在大数据领域应用广 |</p><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=9pi8mzm0UTJrSuUjBgPIWw%3D%3D.K1Sv5ZTqZ1zqUXu9Ng9mQWqs9Nhauh%2B0TEkW6Gk8MA3CWZpZihOso%2BZc4zXrBLqCLEyGRwvw0NTgYs1LDBgTjtFohzFbeUdjmnTuwHWzU7hWcAZNuehTd2rIv%2BmjVdGpeYS%2FnTn68hg3pRkg%2FiQ95BtkJnpHfAQRwbgvBnOieQv7UXUv9xG8kNqa3%2BFDBXQS%2BqyYd0okZCwTUlXVOZbEr3xpCbfBUnBchQNXSXVnLVmI5wOwoHrP6Jzt4qZh6ueXF8GBm01MW2liMkJ2PLiopBQOSOZPifAifaZnSzs%2FlFdnWbVD4RAdhUMx8bAMDeqhHe%2FHWqYK8nNiJRAwRgS5pNL75FpKtoaVlqCLjLw7%2Bw8%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=jnjOhVtsuFVao3xi6%2BHoDA%3D%3D.tFG05QQ%2FArEvSHqK27%2FrjT%2FKIDgzYI0CSruUeVVJdy0j27us590wiH8dAo06Ya8hNozM7n%2BU2y98ixlquYH9hlZUO48%2F0%2FnyP6NQ7NE9lPkBulV%2FsNB42R8B7qkT1tlBjr09%2FLarUzmvEE53IBndpgqzaHDENZBUkvk0UpFsthbyQRj7Fhcebd3Bte8qaZ1fqGJ6eI0b7GaJ1W06IMEz%2BKVEh245J00Y7yGyRdabrVBaV%2BH1Xa61pqWcEZ70kPgcIZEP0AGbxH9G67hcsu1Afii5jz5vL8I3gge93OfeDKoE6104uezqwOm81KGpN%2FnVmBpuJuVBWHj9bAQsvjEn4z%2BKUOb4Jng7mMjyVQVAliI%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=qtLPcyLK6z2BLU1VOteSyg%3D%3D.axp4U%2FsNtRvOHKmOycJ8dg1BVsnKzKLPQo%2FSEpSECgxMu3ePUPIewi9%2Bmv%2FO%2B8ANsjwBWm9OuzxExIjP9xJ0PUpUhvQo1RHppjo%2BIj45eL3M%2BmASpp3Ue1AcN%2B66R8QIGm0N6q2%2BJT%2BvGPCcAiHU31%2FsdOlJPwd0Dmao4aTviZm0ebTqOkl%2BnDWN0hpI1A%2BeT%2BBS6fsiEQF4UpMMR2xl8YTHhFF8UrWQGPyYprSETfPx%2FRlqn8bTd8sKFbVN%2BpLAizhfXweKcvuBXUVKTPDHh4q9qLQKhUZom%2F99DMQ684tY2Y%2BO%2B2kMq3Pq3Vo%2BihIaiyJds4xBzJmikXCUW516bpF60fTAClOmkaB9IH5gv8s%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=XBOSaPoVi9deHz9xLWydIw%3D%3D.heoRqArmsAjkYO%2F66MMPzayow5RD%2Fe9qBUj9Y0ZsWUV1wUbOSUZZEewY1pGf9G5Poo7NxUb8S2i1zjfUH%2B9XWhNlSPtcW6HqLMEmclE7BbUYMO00S0xk%2BDXZyrBdv77sqqRiMxT0ey9lJlMn67gKfJqvDbJx94O%2BhUtlohEKelozGyvzMB8UiGHlL7Bom6I3Y1%2FjCY6hXQqs34PAD7XJ6XVKP8bMinhMDBNg3Hra8JuXCaWX1wjIT1xUH29sxwvoWJ7RYQ3ha%2BeauPHL184esfVvNeHWP3CLQDlDI3DHgyunZHtg5hj%2BPnssO4AGn27eSw7jsK4iwBcZBwAN3KI8Ptc1%2B%2BDSYRIALjhixPrS7YI%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=uu%2B5DHTgbjfQw%2FeQMGpRxw%3D%3D.yBbCaKBkAU7nCdjv0HV68hUcmSJ7DQOdMftzvT%2F3Q8hv5Btm1fmRgtihEfFc12Yqo%2FpZaE0JASG3en6cVPVNxW4raQ6QcVSgh51Deo9dfo5fRiRXv4O9y4lVNmSUiFNurM8LPbdFr17AFsEyeVsuIAJyJKFRCS0UVrtqQ3Vu8HCFUDg5ZE7Sz5l%2BiX1JDNWvFo7SyHZFvFCopjY0zDzYXAhh4oDNt%2BhKBL1LXsQxrKX%2FlM7rywWVqs%2BD2%2F5d7XigFHWR%2B88cazhUvAsg26k6%2Bo5qwppVqCVucToG3yhUkTEZvoiLFpybDAuWiKuX1IOf4wGSFz9dRBSK491qWTm7q5da1rJY4%2BXf60HDNgbmFao%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=6T9QR9p5CAHDSecQFA9Hfg%3D%3D.ZmfctVq%2BaXIPXrc%2BSfhafWrNWuDX1Cgg0NnGSghmX%2FG339QCtSiEL5HnNsFnfJRdhf%2F9H%2Bj%2FYDPQccu%2F4wU2z4UUpvuyXfVmgzDf2A%2FT5MTCKClt8tjfW90KzKYLnuHzEKry16%2BYXFzztbixw3JPMCy3abns%2Fl1%2ByVGtwCf1OEIvvU8r14qi539YuQb7j9Dbn9Bua6Su%2FnpfneTKpFytVhWbWzclF8K4iDLHOmf4gZPdAcfzYYvj1Z7igAfbPc9VPx%2F5fodzZx%2FaqqfuVh1cN%2BV19tDu7OY9YcahxKeivr6hcHa0MIRR6oDKAKtoYKKaAtemidMiMoB7lshJkEsER4QqbCJohsrgiDqN1K5bm4s%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
RabbitMQ系列之怎么确保消息不丢失
https://segmentfault.com/a/1190000022779238
2020-05-29T09:43:45+08:00
2020-05-29T09:43:45+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote><p>1.<a href="https://link.segmentfault.com/?enc=2o4INO%2FosYuuQhPS%2FbNJfg%3D%3D.rLtWKgJ4vjj1oY9iPS6qAn5IGpUa9ATTekY%2F%2BVIZDtqYMRUj0ez2VgdAl6hqqy1r2kIASM2lk3A5TFqjP1RGIM0Jh2LgBcTOgo0EDIsTun6l%2FigDPDVMZtCaLx%2Fwcr%2F7zMBYRRo%2B7r1O3NywwBLt0yJusE%2BtmhvF%2BJFyAAM3m3q1mD8S6hqEPeNHJwjvQLBAXjkTEyrzK4zXA3H8TC3lnDbOO3xgSMU3UTXjD4L%2Fk8QHjve5mqlAKEO%2FGg7OLb5LC3KOHLgSSDYZG4rsiEnwWpceH%2FeVEMEyxxWk7%2FRgytg%3D" rel="nofollow">上一篇</a>介绍了在 SpringBoot 中怎么使用 RabbitMQ 来实现 RPC 功能,分享了可能踩到的坑及解决办法; <br>2.本篇主要介绍消息可能会存在丢失的场景及解决思路,基本上涵盖了可能会遇到的所有的场景。</p><ol><li>可以访问 <a href="https://link.segmentfault.com/?enc=AYL5O0G2LRUxmZFk1Rgvbg%3D%3D.IH5uHbd95S3D6TfH5x3HfDOs5GGNVcJbWclvxFnKnRHCQM9ik8GDZzmHrRY1GxiuK09b0E9HP9WVKZedP1ABHh17eONSEjBFHpbKZ9zLKcA8uz04bjthJK52ifFeXR4hQFCos27VWpmKvbCMggpbgf834xO2BjRxhgYGX6l8lBN22DWk0dEt3X6y6NZ8AeHQ" rel="nofollow">这里</a> 查看更多关于RabbitMQ的原创文章。</li></ol></blockquote><h3>一. 通过设置持久化</h3><p>持久化可以提高 RabbitMQ 的可靠性,以防止在异常情况(比如:重启、关机、宕机等)下的数据丢失。 RabbitMQ 持久化分为三部分:交换机的持久化、队列的持久化、消息的持久化。</p><h4>1. 什么是交换机持久化</h4><blockquote>交换机持久化是指将交换机的属性数据存储在磁盘上,当 MQ 的服务器发生意外或关闭之后,在重启 RabbitMQ 时不需要重新手动或执行代码去创建交换机了,交换机会自动被创建,相当于一直存在。</blockquote><h4>2. 怎么将交换机持久化</h4><p>在创建交换机的时候将<code>durable</code>参数设置为<code>true</code>即可。 比如,我声明一个类型为 direct 的交换机:</p><pre><code>/**
* 设置交换机,类型为 direct
* @return DirectExchange
*/
@Bean
DirectExchange myExchange() {
return new DirectExchange(QueueConstants.QUEUE\_EXCHANGE\_NAME, true, false);
}</code></pre><h5>说明</h5><ul><li>通过将<code>durable</code>参数设置为<code>true</code>,则交换机的元数据会被存储在磁盘上,对于一个长期使用的交换机来说,建议将其设置为持久化。</li></ul><h4>3. 队列持久化</h4><blockquote>如果不将队列设置为持久化,那么在 RabbitMQ 服务重启之后,相关队列的元数据会丢失,数据也会丢失。队列都没有了,消息也找不到地方存储了。</blockquote><h4>4. 怎么将队列持久化</h4><p>同样,在创建队列的时候将<code>durable</code>参数设置为<code>true</code>即可。</p><pre><code>/**
* 创建队列
*/
@Bean
public Queue myQueue() {
return new Queue(QueueConstants.RPC_QUEUE1);
}</code></pre><h5>说明</h5><ul><li>durable 参数默认为 false,只针对当前连接有效,当 RabbitMQ 服务重启后数据会丢失;</li><li>队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失;</li><li>如果要确保消息不会丢失,就需要设置消息的持久化。</li></ul><h4>5. 消息持久化</h4><p><strong>RabbitMQ 的消息是依附于队列存在的,所以要想消息持久化,那么前提是队列也必须设置持久化。</strong></p><h4>6. 怎么将消息持久化</h4><p>在创建消息的时候,添加一个持久化消息的属性(将<code>delivery_mode</code>设置为 2)。</p><h5>SpringBoot中怎么设置消息持久化</h5><p>在 SpringBoot 中使用 rabbitTemplate 发送的消息默认就是持久化的,因为默认已经设置为 <code>delivery_mode = 2</code>,下面我们通过查看源码来验证一下。</p><h5>源码分析</h5><p><strong>1> sendAndReceive</strong><br>生产者发送消息的时候会使用 rabbitTemplate 的 sendAndReceive 接口来发送消息:</p><pre><code>@Nullable
public Message sendAndReceive(String exchange, String routingKey, Message message) throws AmqpException {
return this.sendAndReceive(exchange, routingKey, message, (CorrelationData)null);
}</code></pre><p><strong>2> Message</strong> <br>第三个参数 Message 有一个 MessageProperties 属性</p><p>打开 Message.class:</p><pre><code>public class Message implements Serializable {
private static final long serialVersionUID = -7177590352110605597L;
private static final String ENCODING = Charset.defaultCharset().name();
private static final Set<String> whiteListPatterns = new LinkedHashSet(Arrays.asList("java.util.*", "java.lang.*"));
private final MessageProperties messageProperties;
private final byte[] body;</code></pre><p><strong>3> MessageProperties</strong><br>打开 MessageProperties.class :</p><pre><code>static {
DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT;
DEFAULT_PRIORITY = 0;
}</code></pre><p>MessageDeliveryMode.class:</p><pre><code>public enum MessageDeliveryMode {
NON_PERSISTENT,
PERSISTENT;
private MessageDeliveryMode() {
}
public static int toInt(MessageDeliveryMode mode) {
switch(mode) {
case NON_PERSISTENT:
return 1;
case PERSISTENT:
return 2;
default:
return -1;
}
}</code></pre><h5>结论</h5><p>通过源码查看在 SpringBoot 中使用 rabbimqTemplate 发送的消息默认就是持久化的消息。</p><h4>7. 总结</h4><ol><li>设置了队列和消息的持久化,当 RabbitMQ 服务重启之后,消息依旧会存在;</li><li>仅设置队列持久化,重启之后消息会丢失;</li><li>仅设置消息持久化,重启之后队列会消失,因此消息也就丢失了,所以只设置消息持久化而不设置队列持久化是没有意义的;</li><li>将所有的消息都设置为持久化(写入磁盘的速度比写入内存的速度慢的多),可能会影响 RabbitMQ 的性能,对于可靠性不是那么高的消息可以不采用持久化来提高 RabbitMQ 的吞吐量。</li></ol><h3>二. 生产者开启发送确认</h3><h4>场景</h4><blockquote>1.不知道生产者发送的消息究竟是否已经到达 RabbitMQ Server; > 2.不知道生产者发送的消息是否已经成功的分配到队列中去。</blockquote><h4>解决办法</h4><p>开启消息发送确认,通过 ConfirmCallback 接口 和 ReturnCallback 接口 来保障。</p><p><strong>备注</strong> <br>具体的操作方式可以参考 <a href="https://segmentfault.com/a/1190000022712785">RabbitMQ系列之消息确认机制</a> 这篇文章。</p><h3>三. 消费者开启消息确认(ACK)</h3><h4>场景</h4><blockquote>消费者收到消息还没来得及处理服务就宕机了。</blockquote><h4>解决办法</h4><p>消费端开启消息确认(ACK),将消息设置为手动确认:</p><pre><code># 开启 ACK(消费者接收到消息时手动确认)
spring.rabbitmq.listener.simple.acknowledge-mode=manual</code></pre><p>这样虽然服务宕机,但是在重启之后,消费者仍然会消费到该条数据。 <br><strong>备注</strong> <br>具体的操作方式可以参考 <a href="https://segmentfault.com/a/1190000022712785">RabbitMQ 系列之消息确认机制</a> 这篇文章。</p><h3>四. 使用 RabbitMQ 的镜集群像模式进行部署 #### 场景</h3><blockquote>持久化的消息成功存入 RabbitMQ 之后,如果在存入磁盘的这个过程中 RabbitMQ 服务节点宕机、异常重启等,消息还没来得及存入磁盘。</blockquote><h4>解决办法</h4><p>可以使用 RabbitMQ 的镜集群像模式进行部署,如果主节点在这个特殊的时间段内挂掉了,会自动切换到从节点,这样就保证了高可用性,除非整个集群都挂掉。</p><p><strong>备注</strong> <br>了解更多<strong>关于 RabbitMQ 镜像集群模式</strong>可以参考 RabbitMQ系列之部署模式 这篇文章。</p><h3>五. 消息补偿机制</h3><h4>场景</h4><blockquote>比如设置为持久化的消息,在保存到磁盘的过程中,当前队列节点挂了,存储节点的磁盘也挂了。</blockquote><h4>解决办法</h4><ol><li>由于系统功能复杂,加上网络不确定性太多,所以消息补偿机制需要建立在系统记录了详细的日志,比如消息发送日志,消息接收日志,存入数据库日志等的前提下;</li><li>通过手动触发或者定时扫描,从这些日志中提取出符合消息补偿要求的数据,进行消息补偿。</li></ol><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=1LXGYehzo6u56l4ARC6kUA%3D%3D.T3YsVPIHQ5CRoJP6xidyB9stQxljFe6r5tdyE%2FnjmVPSReZvJ3BtDmrZYv%2FeB0%2BeFT5Xr59WoAJ0J%2BUIe4i6Vgjj5SloCZ3BTF5NDpP1MGu6hLppnxrgLD6ylFmm4wQT85nO0dIdVKM4R2phEIFdDMbMtE7PQ9BA4yRvP7a6yIdjsP5oeUOQI4fh2zDZwtOrNAuL4EbHGAYJF3kQFVn3iqVmNbNrsF%2B6WRTPmNPVTe4sLI6HdywK4AdS%2FT%2FnQvBsw9DhUjEo0FdQHTo0iuZ1vFh23ODjbsSJ8x9tZ8uXm1LojdfXWhGOxHImg0u1lk1J1FAEkwrWmwdTPdQDntU%2FvBf676o7D%2BeMUk2VIGA12kA%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=L2G1qfp%2BGl1x90DrTQvNFg%3D%3D.tmfPDh6Av%2BlEhe0FoCDjYYdWdmm8fLWEmAiIEH3kgipqSjkPNwZ6hp8qNws7DHoxE0UlaZSYxANzDRvMt%2B3FVSOtb2ByKvD0bVtyGP47F7E0yxzvNCfeVzKF6Flo9gXx7NxvsBn0w%2FcQ07rEYYl0oGn8UeCFn%2FhYB0SAlcW7woVyhzkBtU%2BwOflFwczzX6EKclg%2FtALkwc953bsk6dQ62DH2a5DMznHsUrbH0xUWnWWdAdv%2FCOBSamj48KoF%2BHhcJxW9ni1ETM55bM3penhnbX17QvSh06jrR7A%2Bws0O%2FOzae5cWMfsAjszLmF8KkZ9CrrZKhQw1mGNmK1rhj4SURA2sTmXkKU7UdS5ckDUr2U4%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=xvBvBdvS0cbyM%2Blg%2BTrL8g%3D%3D.AedtMIkLNtxKBBp6gNz2BEB8IcrmCvoLB3pSXR2aJmA8jYcor05RU%2FskD1G0RTmtJc711qZDs6sKbeXnskMlgAKIkx5X56rypLI8QG5BVFX6cJYXyzMIM8Y4lq92pErSVWOPgPefpmuEkIJw6UFOZZ9K4j2h%2FnskXTI%2BazOzx2v8yiDWLG1xAQhF4LKWh4uRnOdL%2FOR%2Bv%2BLzUgweybe0UW6Y9H7GlHSW%2B6cgpWs4EtyYKIBDQYfGYiPEgX837M%2Bdss6z0%2Bdc1uoEF70P7RrXubQYk6UBLfn1eXsOCB5ZlTHs2ROblCpNNtmlYxECToPAeIG5vrxG5bkQcxZEXrRxocLUoU8aGJxexbNOKxk7jCs%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=4XSa1WfZOBEnnYQ7Asp0ZQ%3D%3D.TI9kOG6QggTe60CsZCBljkprMcqy1FyxmMIV36O8dsOBKQo3KZj20Z%2BGDF051kSwLagDdl%2FYLQDzY3Yjq6NZExYv2IRo6t%2B00skevw1erA6i3EVtlZFa%2FrGSHjJZqHSrre9%2BO7S4MZU3SCnEwO187qWq938CTviC2nKiFINcIRfLQWA1H4IhWs9Je3%2BCyKd%2F9YF5R5mS6bJE7yWHuvxc22qHvSvsANe7DxWnv4Tu4E2EWwWuJV6MTmK4k4lsFFa2vPG%2FXOIw1p9SNbgXubo4e5%2FIHWEpf67yph0vg%2Bat4z6p02g%2FAkmzd7pz0vPHzvrbM%2BxLCH1xYorXX%2FUa681lUrp53tWyOvzaHKooYl1gHv0%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=SyRRoyE5oL%2BF3FTewCKkLA%3D%3D.ItqBYAd%2FzUlBPwKc%2F5QL%2FB%2Bt4h37IRzkMpUOZaWmUsAyUDot7RqJMOG%2BinyIVDoQxu529r1h2zbxfQAhPS%2FelS6BuL2k0%2BxPXp3B62Qi9ShWC8gmu%2FjSdDKV4Ws43f6OZaiJXR27Yx3hxBY6db1PCPRFJBKFp4r%2FK7FajjzYPDBEiXh0n115dR2pBd15AW4xf60rTErQl5zRXalMbBWAlPUC%2BLACEeF0qqiRlpPUpPxoa6Gxg9zFQW%2Bjel9m7oDp44UYD6OSHT1QBZsaR%2FTeelO38bUjC2zocmfubQ8B5amgOLg8XoxWkdAWWCZ2DSUHOP4D3jIVTODoSqB16LMEP6GmXkMsaeykq0sBJJ8V6ps%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=czrACbNXt2BnX2FkHOt35A%3D%3D.5g5vLss4lxWrBxDBG6wnAE%2F3IU4XpRHf3G1LsSPVNYX2PwI9%2FihPGxvgQ1%2BFdLxdimxVLLV0JyiLEd41Od0i53aC%2Bexc20s8zSG0lMFOfxkIcd5oPjA37JZqeQbR3Z6TKqPv4WoKTA%2BKsuqqNTI7pZnW2fTu6LO0zShQOIeG%2Fyuyta7UsOb65ZxQEoDNCVm9ySrIx0d7J0RwbZ9VPqgzvLcAFQNl%2FiHIV3XcSzPfyU0Du5Z2bddngn9vXVsEmzdVGn5Y%2B8FBrZL7H9YTyU%2F2mPL8cbelSvKPYRMyktSrkDaQvA8IxcpZWaQ1rKEMO7OJ3TS1n6kqjp39a12fSzxl2tyuBfdg9mtgarfdKhcil0U%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>关注微信公众号</h3><p>欢迎大家关注我的微信公众号阅读更多文章:<br><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
RabbitMQ系列之RPC实现
https://segmentfault.com/a/1190000022771164
2020-05-28T13:25:21+08:00
2020-05-28T13:25:21+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
4
<blockquote>1.<a href="https://link.segmentfault.com/?enc=CAA3qciAdDGRdk6uUUJNFQ%3D%3D.Ox0y3qnyZ2lllQ9fljuZe4al25FasOOHri8zKdWRBzJyaQ1aGn0nnoqC5qdZMRQPijMvrae4az%2F0alNa0mX1%2FbU57NmyT4Msb1Qs0CqwqyQ5sCJ04kl8FRe2lRkyN8VCjgJRuPNDF13WBNTIj%2Fx7AhF%2B8zUb8Iq4y%2BTF3vKRW6cQ9BnKybkxDsQYwC7pgaSSPOyAM5YvnhtnO6TQEhnU3TU3M4PDubMufXYkb3wswCyb%2FyrXkDkoaiXBIA%2B5hxGZIVYxB0x4cR4wGlYs2G4ptuWx7lWuxN%2FohbMZ6IKyeBddxbp7NFAqO7TMJbmrhhp%2FdoSj%2FZGj4ggLimWeW7ZV%2FA%3D%3D" rel="nofollow">前一篇</a>介绍了 RabbitMQ 中的消息确认机制;<br>2.本篇主要介绍一下使用 SpringBoot + RabbitMQ 怎么实现 RPC,且详细记录了可能遇到的坑及解决办法;<br>3.在文末提供完整实例代码下载地址。</blockquote><h3>一. 什么是RPC</h3><blockquote>(RPC)Remote Procedure Call Protocol 远程过程调用协议。通俗一点解释就是<strong>允许一台计算机程序远程调用另外一台计算机的子程序,而不用去关心底层网络通信</strong>。</blockquote><h3>二. 使用RPC场景</h3><blockquote>在一个大型的公司,系统往往是由大大小小的服务构成,不同的团队维护不同的代码,且部署在不同的机器上; <br>但是在做开发时候往往需要调用其他团队开发的方法,由于这些服务部署在不同的机器上,想要调用就需要网络通信,而且效率优势将是需要考虑的非常重要的一块; <br>这个时候 RPC 的优势就比较明显了(RPC 主要是基于 TCP/IP 协议的,HTTP 服务主要是基于HTTP协议,在传输层协议 TCP 之上的)。</blockquote><h3>三. RabbitMQ实现RPC的流程</h3><h4>1. 流程</h4><p><img src="/img/bVbHHV5" alt="rabbitmq-rpc.jpeg" title="rabbitmq-rpc.jpeg"></p><p>在 RabbitMQ 中实现 RPC 的流程很简单:</p><ol><li>生产者(也称 RPC 客户端)发送一条带有标签(消息ID(correlation\_id)+ 回调队列名称)的消息到发送队列;</li><li>消费者(也称 RPC 服务端)从发送队列获取消息并处理业务,解析标签的信息将业务结果发送到指定的回调队列;</li><li>生产者(也称 RPC 客户端)从回调队列中根据标签的信息(检查correlationId 属性,如果与request中匹配)获取发送消息的返回结果。</li></ol><h4>2. 实现RPC的好处</h4><ol><li>MQ 实现的 RPC 服务端高可用,只需要简单地启动多个 RPC 服务即可,不需要额外的服务注册发现以及负载均衡;</li><li>如果原有的 MQ 的普通消息需要知道执行结果,可以很方便地切换到 RPC 模式;</li><li>RabbitMQ RPC 的工作方式非常擅长处理异步回调式的任务。</li></ol><h3>四. SpringBoot中使用RabbitMQ的RPC功能</h3><h4>环境介绍</h4><p>macOS Sierra + SpringBoot2.1.8.RELEASE + RabbitMQ 3.8.3 + Erlang 22.3.3</p><h4>1. 客户端</h4><h5>1.1 application.properties</h5><pre><code>server.port=10420 spring.rabbitmq.host=127.0.0.1 spring.rabbitmq.username=guest spring.rabbitmq.password=guest \# 开启发送确认 spring.rabbitmq.publisher-confirms=true \# 开启发送失败退回(消息有没有找到合适的队列) spring.rabbitmq.publisher-returns=true</code></pre><h5>1.2 rabbitmqConfig配置类</h5><pre><code>/**
* RPC客户端
*
* @author lyf
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-05-25 17:20
*/
@Slf4j
@Configuration
public class RabbitConfig {
/**
* 设置同步RPC队列
*/
@Bean
public Queue syncRPCQueue() {
return new Queue(QueueConstants.RPC_QUEUE1);
}
/**
* 设置返回队列
*/
@Bean
public Queue replyQueue() {
return new Queue(QueueConstants.RPC_QUEUE2);
}
/**
* 设置交换机
*/
@Bean
public TopicExchange exchange() {
return new TopicExchange(QueueConstants.RPC_EXCHANGE);
}
/**
* 请求队列和交换器绑定
*/
@Bean
public Binding tmpBinding() {
return BindingBuilder.bind(syncRPCQueue()).to(exchange()).with(QueueConstants.RPC_QUEUE1);
}
/**
* 返回队列和交换器绑定
*/
@Bean
public Binding replyBinding() {
return BindingBuilder.bind(replyQueue()).to(exchange()).with(QueueConstants.RPC_QUEUE2);
}
/**
* 使用 RabbitTemplate发送和接收消息
* 并设置回调队列地址
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
// 设置回调队列地址
template.setReplyAddress(QueueConstants.RPC_QUEUE2);
// 设置请求超时时间为6s
template.setReplyTimeout(60000);
return template;
}
/**
* 给返回队列设置监听器
*/
@Bean
public SimpleMessageListenerContainer replyContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(QueueConstants.RPC_QUEUE2);
container.setMessageListener(rabbitTemplate(connectionFactory));
return container;
}
}</code></pre><p>备注:</p><ul><li>这里的队列监听器必不可少,否则客户端是无法收到服务端回应的消息。</li></ul><h5>1.3 客户端</h5><pre><code>/**
* RPC客户端
*
* @author lyf
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-05-25 19:30
*/
@Slf4j
@RestController
public class RPCClient {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage")
public String send(String message) {
// 封装Message,直接发送message对象
Message newMessage = convertMessage(message);
log.info("客户端发送的消息:" + newMessage.toString());
// 备注:使用sendAndReceive 这个方法发送消息时,消息的correlationId会变成系统动编制的 1,2,3 这种格式,因此通过手动set的方式没有用
Message result = rabbitTemplate.sendAndReceive(QueueConstants.RPC_EXCHANGE, QueueConstants.RPC_QUEUE1,
newMessage);
String response = "";
if (result != null) {
// 获取已发送的消息的唯一消息id
String correlationId = newMessage.getMessageProperties().getCorrelationId();
// 提取RPC回应内容的header
HashMap<String, Object> headers = (HashMap<String, Object>) result.getMessageProperties().getHeaders();
// 获取RPC回应消息的消息id(备注:rabbitmq的配置参数里面必须开启spring.rabbitmq.publisher-confirms=true,否则headers里没有该项)
String msgId = (String) headers.get("spring_returned_message_correlation");
// 客户端从回调队列获取消息,匹配与发送消息correlationId相同的消息为应答结果
if (msgId.equals(correlationId)) {
// 提取RPC回应内容body
response = new String(result.getBody());
log.info("收到RPCServer返回的消息为:" + response);
}
}
return response;
}
/**
* 将发送消息封装成Message
*
* @param message
* @return org.springframework.amqp.core.Message
* @Author Liuyongfei
* @Date 下午1:23 2020/5/27
**/
public Message convertMessage(String message) {
MessageProperties mp = new MessageProperties();
byte[] src = message.getBytes(Charset.forName("UTF-8"));
// 注意:由于在发送消息的时候,系统会自动生成消息唯一id,因此在这里手动设置的方式是无效的
// CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
// mp.setCorrelationId("123456");
mp.setContentType("application/json");
mp.setContentEncoding("UTF-8");
mp.setContentLength((long) message.length());
return new Message(src, mp);
}
}</code></pre><hr><h5>你可能会遇到的坑1</h5><ul><li>使用 sendAndReceive 这个方法发送消息时,消息的 correlationId 会变成系统自动生成的 1,2,3 这种格式,因此通过手动 set 的方式没有用。</li></ul><h5>你可能会遇到的坑2</h5><ul><li>因此为了拿到当前已发送消息的 correlationId,只能在<strong>消息发送之后</strong>(<em>注意这里必须在消息发送之后再获取</em>)通过 getMessageProperties().getCorrelationId() 的方式来获取到;</li></ul><h4>2. 服务端</h4><h5>2.1 application.properties</h5><pre><code>server.port=10420
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 开启发送确认
spring.rabbitmq.publisher-confirms=true
# 开启发送失败退回(消息有没有找到合适的队列)
spring.rabbitmq.publisher-returns=true
</code></pre><h5>2.2 rabbitmqConfig配置类</h5><p>代码同 RPC 客户端的 rabbitmqConfig 配置类。</p><h5>2.3 服务端</h5><pre><code>/**
* RPC服务端
*
* @author lyf
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-05-25 22:00
*/
@Slf4j
@Component
public class RPCServer {
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitListener(queues = QueueConstants.RPC_QUEUE1)
public void process(Message msg) {
log.info("Server收到发送的消息为: " + msg.toString());
int millis = (int) (Math.random() * 2 * 1000);
// 模拟处理业务逻辑
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 数据处理,返回Message
String msgBody = new String(msg.getBody());
String newMessage = msgBody + ",sleep " + millis + " ms。";
Message response = convertMessage(newMessage, msg.getMessageProperties().getCorrelationId());
CorrelationData correlationData = new CorrelationData(msg.getMessageProperties().getCorrelationId());
rabbitTemplate.sendAndReceive(QueueConstants.RPC_EXCHANGE, QueueConstants.RPC_QUEUE2, response, correlationData);
}
@RabbitListener(queues = QueueConstants.RPC_QUEUE2)
public void receiveTopic2(Message msg) {
System.out.println("...队列2:" + msg.toString());
}
/**
* 封装消息
*
* @param s 消息
* @param id 消息id
* @return org.springframework.amqp.core.Message
* @Author Liuyongfei
* @Date 下午1:25 2020/5/27
**/
public Message convertMessage(String s, String id) {
MessageProperties mp = new MessageProperties();
byte[] src = s.getBytes(Charset.forName("UTF-8"));
mp.setContentType("application/json");
mp.setContentEncoding("UTF-8");
mp.setCorrelationId(id);
return new Message(src, mp);
}
}</code></pre><h4>3. 客户端向服务端发送消息</h4><p>启动 RPC 客户端服务,使用postman 请求发送消息接口,发送一个 <code>hello</code>字符串:<br><img src="/img/bVbHHXg" alt="rpcclient-postman.png" title="rpcclient-postman.png"></p><h4>4. 服务端收到客户端的消息</h4><p>启动 RPC 服务端服务,通过打断点,查看收到的消息格式:<br><img src="/img/bVbHHXj" alt="rpcserver-msg1.png" title="rpcserver-msg1.png"></p><p>从图中我们可以看出:</p><ul><li>生产者(RPC客户端)发出的这条消息包含了标签ID和回调队列名称,符合了 RPC 实现流程的第一步要求。</li></ul><h4>5. 服务端向指定的的回调队列发送消息</h4><p>在服务端,处理相关的业务逻辑后,需要将消息通过指定的回调队列发送给客户端。 同样是通过借助 sendAndReceive 来发送消息:</p><pre><code> // 数据处理,返回Message
String msgBody = new String(msg.getBody());
String newMessage = msgBody + ",sleep " + millis + " ms。";
Message response = convertMessage(newMessage, msg.getMessageProperties().getCorrelationId());
CorrelationData correlationData = new CorrelationData(msg.getMessageProperties().getCorrelationId());
rabbitTemplate.sendAndReceive(QueueConstants.RPC_EXCHANGE, QueueConstants.RPC_QUEUE2, response, correlationData);</code></pre><h5>你可能会遇到的坑3</h5><ul><li>一定要注意这里使用的队列为回调队列(<code>RPC_QUEUE2</code>);</li></ul><h5>你可能会遇到的坑4</h5><ul><li>这里在发送消息的时候一定要使用第四个参数 <code>correlationData</code>,否则客户端有可能收不到数据;</li><li>由于客户端在收到消息后要取 correlationId 与之前发出的消息的 correlationId 进行匹配,因此这里在发送消息的时候一定要使用第四个参数 <code>correlationData</code>;</li></ul><h4>6. 客户端收到服务端回应的消息</h4><p>由于客户端已经设置了回调队列监听器,因此可以监听到 RPC 服务端返回的消息:<br><img src="/img/bVbHHYr" alt="rabbitmq-return2.png" title="rabbitmq-return2.png"></p><h5>6.1 客户端根据correlationId来匹配消息</h5><p>RPC 客户端从回调队列中根据标签的信息(检查 correlationId 属性,如果与发送的消息 correlationId 匹配)获取发送消息的返回结果,主要代码如下:</p><pre><code>// 获取已发送的消息的唯一消息id
String correlationId = newMessage.getMessageProperties().getCorrelationId();
// 提取RPC回应内容的header
HashMap<String, Object> headers = (HashMap<String, Object>) result.getMessageProperties().getHeaders();
// 获取RPC回应消息的消息id(备注:rabbitmq的配置参数里面必须开启spring.rabbitmq.publisher-confirms=true,否则headers里没有该项)
String msgId = (String) headers.get("spring_returned_message_correlation");
// 客户端从回调队列获取消息,匹配与发送消息correlationId相同的消息为应答结果
if (msgId.equals(correlationId)) {
// 提取RPC回应内容body
response = new String(result.getBody());
log.info("收到RPCServer返回的消息为:" + response);
}</code></pre><p>备注:</p><ul><li>回调队列监听器详见 rabbitmqConfig 配置类。</li></ul><h5>你可能会遇到的坑5</h5><ul><li>在 RPC 服务端返回的消息 headers 里找不到 spring_returned_message_correlation 属性:</li></ul><p><img src="/img/bVbHHYE" alt="rabbitmq-reutrn.png" title="rabbitmq-reutrn.png"></p><p>那么去确认一下在 \`application.properties\`里是否开启了发送确认:</p><pre><code># 开启发送确认
spring.rabbitmq.publisher-confirms=true
# 开启发送失败退回(消息有没有找到合适的队列) spring.rabbitmq.publisher-returns=true</code></pre><h3>demo下载地址</h3><ul><li><a href="https://link.segmentfault.com/?enc=ExFcYvrasTh92%2BEWijisQQ%3D%3D.F4oL5T29awenfedDdNYIRw%2Bf%2F6p%2FOSqCuSKVmcmp91w1gdxPgOdIKE8HDYp6iu4Q" rel="nofollow">https://github.com/liuyongfei...</a></li><li>在本篇实例中,我将消息生产端和消费端部署为两个单独的服务,大家克隆完毕后请切换到 <code>feature/rabbitmq-rpc</code> 分支进行启动测试。</li></ul><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=%2FeABrWkddWvZs7fMV1ZHuA%3D%3D.dg%2FNbckmRwnLOv0I6ZmTVQ4nHv8X2LBgVgaOjuR1%2Fgrg0NPKcAJECHgi%2FYK21zD%2BCAFWp7fKKlEE4PuRb2vEfMyGgzR03g9uRUZV3xUGutcCOi2mMDOaSYa0kr5jammrNw0LcK%2BTr3FSF1qj8dLfpT2%2Fm8C91S47ZocEXSzeH9WPl2EBlgYHKNu3v6QO5bPbtVvvXd2he%2F6vtL28gXgvX2YwN1%2FOyQxo8wyX1VuupaPheLTW6BBpGyCJ6FWd3BMGGTvvjyx5Pbo0nsisFWzcqpaTRZ1hUs9lBgK5d3xFr0qVyIAKbsSj%2B6DNlxaQtXNNXBoWoHsvz0XxcFTGNbqc4v%2FnN4ycrW1QX6%2BnRaTTV1o%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=TiBLfk%2BUwVjTODJ5t9FdSQ%3D%3D.t2KA8IxTTL5dVQ1WEtxqyAYbyDRaJ2fXTcbjLf0LbalNN5fjm7BCrjf%2FfDgQTvTk2rvsxjABOu9vQFyx11cgsN3YlmjkOYc%2FtdX0yFUhwtZe1Iwu8ZsowoGnROUrvn1HdUDKWh%2BksCJfvQaFu25kXx1KNxy8QeNnPpXPUCjmIYNm0wdhIcq6xFv%2BOrFpcGbWDvqQVphi3H%2BeqnGojjkUaL7WYyM7kwkN5pkhOszulFlue4RH5O76MUod24G1pqFjpPYfp9SQjWRKjAcAseT5AyVIVE2e9bfkP9E7DD%2BgeO5HCPuy3Rmdct8MCt9Lj1Zv5eD%2FYJCaeWvWw%2FIBdD4PZQ%2Fej8dlfW%2BXy1J1v9CRSC4%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=5qHInH6M3QWO9XLapTQF0A%3D%3D.AqCPGS8EH8TTwO6zcXH4XPndqQ%2FHuwZg9IybvDk8jHdcI6Y1pgWYcSxmnVcmswYimFCaoTWvz%2BNdbpbTm1mUqDip3TGwB3htPRO08JveSWGbAlPbWAWEVFmcxOH2Rz1k%2BCPPdwHciAr0aAKEzvmkxSMZ5dpjgGiAmzi2Ct3Kko05t%2BEiIu1tVDM9Aiaiwla3UEx0qgUJSotKU5unTSVuIPp%2F10u4DHAmA510lRLWVfeBNvZXd3bbPggrtdwefOW5Y2tdVNMaDE6Mu%2Fr9cI783YMPBUzz6%2FP9N5O5Cm%2FORxAIOE6av8vcpLG4A37%2BHEHrpXJkvP998hrntedFkTaVNt9fHS28LaoW3i%2F4eHDH6ro%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=nEza1ocuDMQuXgpB%2FaOR%2Bw%3D%3D.sctGTpAnAJ6Yz0GzcQBNUL3Egck%2F9rViywnADmp3PoDy8G%2BunqlOkDc7zRpKnbFHJizeXBMp3laJsz5DYGf%2FUYvWEr8NrFC4QByrY%2F2hZqQ9WKYMwDnMv3hv1l3VN0ofUnRDupNHXcftVOMjC0JKfXAPxUflEBcUo%2F%2FAMAxoERQnfX4MNfz2dZwseJSrSDWnjL2tov%2F%2B%2BOpvvnKz934csfWdMyPJPmhMUJ49UjviKPogmjSeM0hwXBP5gy%2FAbzOrsDjbvEpKAIlwwtJf%2FezpmnNKotgAjeVutvu4Nb8g7QrD6jTTbsKI69Wae8PyW%2Fnj9hW2JI5uMynLw0uttTuBHMaj9sTxGlqJNbLeUEjKDTk%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=SO0Vw%2FsNLaf7Ft53aVdeuQ%3D%3D.qpyXwGKt30lZHHzrAxdBLKX18Me7jScaxJoA0h3iXuEQF3beIQWsBF6bf6gnSkM%2B8KkxgHWP4Au9O3g%2Bjl2TIv%2BS%2BUWCD83mG8dzkuCpPfsh8Sm%2FGjiy1i3GBmDN2bNX8Xji68F9O9iSiYHv%2B0p7lR%2BDIsrGrtrWXImSt68OO%2BDW2pHBQHfzTn3%2Fy%2BXLg9Sg16kMekSiSW8Lq1d8qJ6ZVeAfFZfIurdHXRDcXcBZZI0%2BRxr6E%2FCSAjvCaImeC8tG2pfrOeIBnds%2FJWnaGS1zvsklxu0uX9jC%2Fyschplcy1QFhK2KI4HEGBjwzp9FBqb4B8HmJIT2f3XghVc%2FqHBcCJFCt48nzaCYX98Y1q1qT8U%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=BMnpHw2Vaj%2BO0CI%2Bfdv%2Fvg%3D%3D.UH7%2BQv6Y35X93gEwVO4kNTXTpV9xqGnmU%2BI8RHcC%2BYulXuWLvNz3CIJ5cxguEXZnm4BAHlV%2B%2BOGoyQeqgl18IVBnyBzLGpEfEpU0y2hnZ64R%2FbPSazzq41%2Fq4Tq8mZrFEyCnkxQ0zkPV30%2BJAnnz4JiW2CUtwkwN9NLF%2B32choGQzfGUce%2BuA9zqe%2BNAFaCoqnMiR4W9nHFGbczSS9zvOhfULIV0qvZYzNwVQ53VwktJUHcqkuPISwthhbQ1Kkr7bjQ0CkU1x9XkVq8mOL2vo3o6pRetYDIZA3c%2BELp4hBzgJMa7XCN42Yg8FoykAqSO8EtUGv0kLPbTUwLdqkw2Rbd249OuS89wKlc%2FPcBy0CE%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>欢迎大家关注微信公众号</h3><p>阅读更多关于<strong>消息队列</strong>的原创的文章:<img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
RabbitMQ系列之消息确认机制
https://segmentfault.com/a/1190000022712785
2020-05-22T09:43:18+08:00
2020-05-22T09:43:18+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
6
<ol><li><a href="https://link.segmentfault.com/?enc=thTi2SD21gTQPpj9M8fe9g%3D%3D.eQ9UGGVCoKQP6LqNdH7wtcLDUaTs%2FPadVqaKqSrd7QgCc6aO3Q%2FajrJGzHw%2BmFB25B4Sldv4tobZXQZfKNi0jZ41WpaFRGEoinI0ERUzYBHysbZG%2Fo9pAVIfkMBm8uMN6EhHb4cuBJmBFx3A511GCl5PwX3v4kaLJY30qxFfwQV49O2eaDENM97BkbCyJIPTaQKGqSyG%2BpJbvUBAZxj6kXC0hFIDemhcRjU7CHu8QH45NYmrzLUoSfjaZq18oSWhRUTb3sZOgWvmurNMECdWGYXE2jDivdsFN63oviKg3NU%3D" rel="nofollow">上一篇</a>介绍了 RabbitMQ 中的一些基本概念,并通过 SpringBoot 工程整合 RabbitMQ,做了一个小 demo;</li><li><strong>那么 RabbitMQ 是怎么知道消息到底有没有被消费者消费,生产者是怎么知道自己发送的消息时真的已经发送到 RabbitMQ 中了呢?</strong></li><li>本篇通过实例演示去介绍一下 RabbitMQ 的消息确认机制,阅读完本篇内容上面的这些疑问就迎刃而解,而且也有助于后边我们理解 <strong>RabbitMQ 的消息为什么会出现重复消费</strong>的问题。</li><li>文中实例会在文末提供下载地址。</li><li>可以访问 <a href="https://link.segmentfault.com/?enc=RFw5x3tRwXXGu0DXF6xtAA%3D%3D.5M9aQf%2FzV6OLc3Q5DLFbUbPejHh2cI3INY4sHdf7ve5RMQ6Y8xtnyG7cpFH9p%2F%2Bp9UwUWqFPcP80UckvlIQ%2Bn4z7uWJA1sUsESqcTzVT1rLUGqS14UppWSYno9wCgouoGdVdzPvuDhyVaKfzGIeV5qSjbhSp4p%2FAi9VrbY63u6yL5r9DnvC50ps3NXPph41p" rel="nofollow">这里</a> 查看更多关于RabbitMQ的原创文章。</li></ol><h3>一. 为什么要有消息确认</h3><ol><li>由于网络可能以不可预知的方式出现故障,且检测故障可能需要耗费一些时间;</li><li>因此不能保证发送的消息能够到达对等方或由它成功地处理。</li></ol><h3>二. 消息确认流程</h3><p>RabbitMQ 的消息确认机制如下:<br><img src="/img/bVbHsHM" alt="RabbitMQ消息确认.png" title="RabbitMQ消息确认.png"></p><p>从图中我们可以看出:</p><ul><li>生产者发送消息到 RabbitMQ Server 后,RabbitMQ Server 需要对生产者进行消息 Confirm 确认;</li><li>消费者消费消息后需要对 RabbitMQ Server 进行消息 ACK 确认。</li></ul><p>这两个机制都是收到 TCP 协议的启发,它们对于数据安全至关重要。 <br>下面就分别从生产者、消费者两个方面结合实例来认识消息确认机制。</p><p>备注:</p><ol><li>在 RabbitMQ 中 有两种事务机制来确保消息的安全送达,分别是事务机制和确认机制;</li><li>事务机制需要每个消息或一组消息发布、提交的通道设置为事务性的,因此会非常耗费性能,降低了 Rabbitmq 的消息吞吐量;</li><li>因此我们在实际生产中通常采用确认机制,下面的实例演示就采用<strong>确认机制</strong>来进行编码。</li></ol><h3>三. 生产者确认</h3><h4>1. 消息投递和消息确认链路</h4><p>我们先来看一下RabbitMQ 消息投递和接收的一个完整链路如下:<br><img src="/img/bVbHsHP" alt="RabbitMQ消息推送到接收.png" title="RabbitMQ消息推送到接收.png"></p><h4>2. 消息投递可靠性保证</h4><p>消息投递的链路用文字表示:<br><code>producer->rabbitmq broker cluster->exchange->queue->consumer</code></p><p>由于:</p><ol><li>生产者向 RabbitMQ Server 发出的消息可能会在发送途中丢失或者需要经过一定的延迟后才能成功发送到 RabbitMQ Server;</li><li>因此,需要 RabbitMQ 告诉生产者,生产者才能知道自己发布的消息是否已经送达。</li></ol><p>在编码时我们可以用两个选项用来控制消息投递的可靠性:</p><ul><li>消息从 producer 到 RabbitMQ broker cluster 成功,则会返回一个 <code>confirmCallback</code>;</li><li>消息从 exchange 到 queue 投递失败,则会返回一个 <code>returnCallback</code></li></ul><p>我们可以利用这两个 callback 接口来控制消息的一致性和处理一部分的异常情况。</p><h4>3. 开启 confirm 和 return 确认</h4><pre><code>server.port=10420
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 开启发送确认
spring.rabbitmq.publisher-confirms=true
# 开启发送失败退回(消息有没有找到合适的队列)
spring.rabbitmq.publisher-returns=true</code></pre><h4>4. 使用 callback 接口来确保消息投递状态</h4><p>在 RabbitConfig 配置类里,定义 RabbitTemplate Bean,使用 callback 接口:</p><pre><code>/**
* RabbitMQ配置
*
* @author lyf
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @date 2020-05-17 17:20
**/
@Slf4j
@Configuration
public class RabbitConfig {
@Autowired
CachingConnectionFactory cachingConnectionFactory;
@Bean
RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
// 消息只要被 rabbitmq broker 接收到就会执行 confirmCallback
// 如果是 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback
// 被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
String msgId = data.getId();
if (ack) {
log.info(msgId + ": 消息发送成功");
} else {
log.info(msgId + ": 消息发送失败");
}
});
// confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。
// 在有些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式
// 这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info(MessageFormat.format("消息发送失败,ReturnCallback:{0},{1},{2},{3},{4},{5}", message, replyCode,
replyText, exchange, routingKey));
// TODO 做消息发送失败时的处理逻辑
});
return rabbitTemplate;
}
/**
* 声明队列
* 参数说明:
* durable 是否持久化,默认是false(持久化队列则数据会被存储在磁盘上,当消息代理重启时数据不会丢失;暂存队列只对当前连接有效)
* exclusive 默认是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
* autoDelete 默认是false,是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
* 一般设置一下队列的持久化就好,其余两个就是默认false
*
* @return Queue
**/
@Bean
Queue myQueue() {
return new Queue(QueueConstants.QUEUE\_NAME, true);
}
// 设置交换机,类型为 direct
@Bean
DirectExchange myExchange() {
return new DirectExchange(QueueConstants.QUEUE\_EXCHANGE\_NAME, true, false);
}
// 绑定:将交换机和队列绑定,并设置路由匹配键
@Bean
Binding queueBinding() {
return BindingBuilder.bind(myQueue()).to(myExchange()).with(QueueConstants.QUEUE\_ROUTING\_KEY\_NAME);
}</code></pre><h4>5. 消息生产端</h4><p>在 ProducerController 里,主要干了以下几件事:</p><ul><li>提供了一个 Rest接口<code>sendDirectMessage</code>,通过请求该接口,可以实现生产者发送消息的功能;</li><li>在该接口内部使用了 <code>CorrelationData</code>,该对象内部只有一个 id 属性,用来表示消息的唯一性;</li><li>使用 rabbitTemplate.convertAndSend 像 RabbitMQ 发送消息(这里使用的rabbitTemplate 就是在 RabbitConfig 里被重写的 RabbitTemplate)。</li></ul><pre><code>/**
* 消息生产端
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @author lyf
* @date 2020-05-17 18:30
**/
@RestController
public class ProducerController {
/\*\*
\* RabbitTemplate提供了发送/接收消息的方法
\*/
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 生产消息
*
* @Author Liuyongfei
* @Date 上午12:12 2020/5/20
* @param test
* @param test2
* @return java.lang.String
**/
@GetMapping("/sendDirectMessage")
public String sendDirectMessage(String test,Integer test2) {
// 生成消息的唯一id
String msgId = UUID.randomUUID().toString();
String messageData = "hello,this is rabbitmq demo message";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 定义要发送的消息对象
Map<String,Object\> messageObj = new HashMap<>();
messageObj.put("msgId",msgId);
messageObj.put("messageData",messageData);
messageObj.put("createTime",createTime);
rabbitTemplate.convertAndSend(QueueConstants.QUEUE\_EXCHANGE\_NAME,QueueConstants.QUEUE\_ROUTING\_KEY\_NAME,
messageObj,new CorrelationData(msgId));
return "message send ok";
}
}</code></pre><h4>6. 生产消息</h4><ol><li>保存代码,在 RabbitConfig 里的 <code>setConfirmCallback</code>方法内部打上断点;</li><li>重启服务后,使用 PostMan 请求生产消息接口:<a href="https://link.segmentfault.com/?enc=lbiptr%2F0OoUUPw8RgGalug%3D%3D.lm50FssFsgGz3Tr%2FQ%2F7VjGIeGAl9BiiXZflcO92BHiBX4c8mMKp7hDF57qdh1luv" rel="nofollow">http://你的域名</a>:10420/sendDirectMessage,生产消息,并将消息发送给 RabbitMQ:<br><img src="/img/bVbHhNr" alt="postman.png" title="postman.png"></li><li>然后打开 RabbitMQ 管理界面,找到对应的队列,会发现:<br> <img src="/img/bVbHhNA" alt="rabbitmq-web1.png" title="rabbitmq-web1.png"><br> 关于 Read,Total 的状态代表什么意思,可以翻看<a href="https://segmentfault.com/a/1190000022670544?_ea=45669504">上一篇</a>文章。</li><li>在 IDEA 里,服务在启动后直接停在断点处:<br> <img src="/img/bVbHsJO" alt="confirmBack.png" title="confirmBack.png"><br> <strong>也就说明我们生产的消息已经成功的到达了 RabbitMQ Server 里。</strong></li><li>继续执行断点调试的绿色箭头,发现 setReturnCallback 方法里的断点没有执行到,<strong>也就说明了我们生产的消息已经被交换机顺利的投递到队列里去了</strong>。</li></ol><h4>总结</h4><p>至此,生产者消息确认结束,且通过运行的实例,我们能够得出结论:本次生产的消息已经正确无误的投递到了队列中去。</p><h3>四. 消费者确认</h3><p>消费者确认指的就是 RabbitMQ 需要确认消息到底有没有被收到,来确定要不要将该条消息从队列中删除掉。这就需要消费者来告诉 RabbitMQ,有以下两种方式:</p><h4>1. 自动应答</h4><p>消费者在消费消息的时候,如果设定应答模式为自动,则消费者收到消息后,消息就会立即被 RabbitMQ 从 队列中删除掉。 <br>因此,在实际开发者,我们基本上是将消费应答模式设置为手动确认更为妥当一些。</p><h4>2. 手动应答</h4><p>消费者在收到消息后:</p><ul><li>可以在既定的正常情况下进行确认(告诉 RabbitMQ,我已经消费过该消息了,你可以删除该条数据了);</li><li>可以在既定的异常情况下不进行确认(RabbitMQ 会继续保留该条数据),这样下一次可以继续消费该条数据。</li></ul><h4>3. 开启手动应答</h4><pre><code>server.port=10421
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 开启 ACK(消费者接收到消息时手动确认)
spring.rabbitmq.listener.simple.acknowledge-mode=manual</code></pre><h4>4. 消息消费者</h4><p>ConsumerController 里主要干了以下几件事儿:</p><ol><li>使用 <code>@RabbitListener</code> 来监听队列;</li><li>从消息头里拿到消息的唯一表示 <code>deliveryTag</code>;</li><li>使用 <code>channel.basicAck</code> 来确认消息已经消费;</li><li>如果有异常,使用 <code>channel.basicNack</code> 把消费失败的消息重新放入到队列中去。</li></ol><pre><code>/**
* 消息消费端
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @author Liuyongfei
* @date 2020-05-21 18:00
**/
@Component
public class ConsumerController {
@RabbitListener(queues = {QueueConstants.QUEUE\_NAME})
public void handler(Message message, Channel channel) throws IOException {
System.out.println("收到消息:" + message.toString());
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY\_TAG);
try {
// 手动确认消息已消费
channel.basicAck(tag,false);
} catch (IOException e) {
// 把消费失败的消息重新放入到队列
channel.basicNack(tag, false, true);
e.printStackTrace();
}
}
}</code></pre><h4>5. 消费消息</h4><ol><li>重启消费端服务,停在断点处:<p><img src="/img/bVbHsJ5" alt="consumer1.png" title="consumer1.png"></p></li><li>查看 RabbitMQ 管理界面会发现 队列的 Ready 和 Total 仍然是 1,说明我们的手动应答设置生效:</li><li>点击 Debug 的绿色箭头继续像下执行,查看 RabbitMQ 管理界面:<p><img src="/img/bVbHsKH" alt="rabbitmq-web3.png" title="rabbitmq-web3.png"></p></li><li>几秒后再次查看 RabbitMQ 管理界面:<br> <img src="/img/bVbHsLo" alt="rabbitmq-web4.png" title="rabbitmq-web4.png"><br> 会发现:Ready 变为0,Unacked 为 0,Total 为 0。 说明该条数据已经被成功消费。</li></ol><h4>总结</h4><p>至此,消费者消息确认结束。大家可以在 ConsumerController 里添加一些测试代码来触发异常,体验一下 <code>channel.basicNack</code> 的作用。这里我就不再一一测试。</p><h3>五. demo下载地址</h3><ul><li><a href="https://link.segmentfault.com/?enc=kF4Le8zWTEO4n%2BZL9Lv0jw%3D%3D.TQ7sQyqVwIU41BpVmu765iZjBVWRdI57hXirc4vlylganRi%2BTCqS%2FNniosF9zG2Q" rel="nofollow">https://github.com/liuyongfei1/blog-demo</a></li><li>在本篇实例中,我将消息生产端和消费端部署为两个单独的服务,大家克隆完毕后请切换到 feature/rabbitmq-confirm 分支进行启动测试。</li></ul><h3>更多文章 </h3><p>欢迎访问更多关于<strong>消息中间件</strong>的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=ZASWGspLDPMs9nTIXMELfw%3D%3D.912GwMTIrqERjmSBlEgCf5N%2FoDB9srHK%2BkPOfKdVdKP8Nmuu3FMNAurDAno6rge9sWxKGkYyDlhhCTnF14of9Rc%2Bp5f7G%2BQQKsjHC7r9pDau77NAGJfICjyhASyHHc5uGKBiksTHfZSDVEkL3i45KGIW2oySFAOsXxUfI3F7HBFbUVAC3468Wc4N5DGHEpV2u3%2Bu45XR8UX6QkljZd4XglQ1wR17mbQwpe8szqedWCFljTJLEBYTgNQfR2ahZqjC1t3eOqqeYSiP6mr1J3B9AJ1NJ4TYH4Fx1w8LyidLe4q7kOGEttwr8zfMdrDlhyGjsKW4EbFYew6nX6hXVFI85nliRiFsu9JJLayz06kyXwo%3D" rel="nofollow">RabbitMQ系列之消息确认机制</a></li><li><a href="https://link.segmentfault.com/?enc=KkFnhrpop%2F8qJMvfxVhRyg%3D%3D.qppvmI2iXUxYD5ouOIEk8AxF0T3xkm5MVnmPkOfFwv8y6MhWDHT%2BgjGKBJip2%2F4J9TdCY2EKNftXkg95wxK0Ml4o%2BJ3lKjM%2Fzx%2Bfl31Hu1H1ts5E2NY5i7CsS%2BsZBubblMwbKcz6xOp5Ok2S6D7TZrVlNHtsOiv1oGv8ZBVj2QVlElNexDDNt%2BD%2Bwq29xDmbY71ZGeVp53GjdgRYMgCcVevRL8xf0aNT0eq6YeDPWz8%2B1Ws8UtrCgGx2a1gPDm%2Fk6EiW0Qwm%2Frp%2B9x5U9R3kz93jjhpG5yEAX9sHjL1pX35bJy9B0g2UKN6t%2BJfd0UAEvWdlQJzhDcW3TaVVT004BXYDR0iv9EsRADBhqosO4aw%3D" rel="nofollow">RabbitMQ系列之RPC实现</a></li><li><a href="https://link.segmentfault.com/?enc=z4T4X1bZTst4JV4c8y3Arw%3D%3D.w7h1GR%2FzrqClI5Fwewr01TALS1KJVEoV2G%2FDfLu1pnR1TUsLLTZORdTcmMOGo9RkoH6fAZUAO42N2pMgeHmMxJyNsgtCx3hbfv8nbnJ4F%2FPZ%2FIfg0rBM3g0wXoLY70Hs4zYtEwuNvdtZlhpnXavWDpKUU2rDcVN%2B5kdkgCpe3F4l3E%2BDB%2FnsKQZhyIQvqF%2FibOf6CrF%2FiS2q%2B0XBVTsmW%2FlrVj%2B8n792PDiHz6cdAZATyWYMvSEbQS6K70mVH%2FtM2nPhFxVsAJfVllqSsBi%2BH%2FZEp11%2BXkgA5MPh3PpgrN6HD891Ch3n6yoGYxfuySQPp%2B9rr6gh%2ByNbOWsFY%2FQkZ6%2FJgR1n7SXSIVRgfW2TJKw%3D" rel="nofollow">RabbitMQ系列之怎么确保消息不丢失</a></li><li><a href="https://link.segmentfault.com/?enc=x%2F3LiqG%2B4pDaMaDv1bTqGA%3D%3D.WxtCc3gAUDjPVXrWsmtrwTq3GaKSDoCkV8J5yeHTGUnXE8QWWBVOUPfqeXKjEUw%2FjqKowbHTH1lGa3dJbkfAKU260N0OeoWCW1aUb0zFF9Qiu6ozNw392ZUyEMvhwUUOUtsFNy2cAO26xPaqRtV2FkDc6iAeNXHltNifeko29o1Jsv58bOsdGK6Zu5vWdjVnoWXtMZkGvfxLWkG%2F6YgkeXzjQijJjZUNF7gcaaSaIg9Ztf8qwzEo%2BgaN4gkIeZ1XdYktp%2BsceO5LnXg%2BEdqqJO3XhndKOO49HvEK7%2F31WWdgahlLnivFZ%2F83jytaUA1W62bcEG57TPFsXnR%2BPpbftL31tBPYq6ryEiGS13fZa20%3D" rel="nofollow">RabbitMQ系列之基本概念和基本用法</a></li><li><a href="https://link.segmentfault.com/?enc=jGaKkaMkNPooYtk38w5f6g%3D%3D.wUW4Dyjg0KXgmHXi%2FbUXOwEF1470CUbvDpbfaIiRzfMfeR0TD2PSIwX2zf9CA%2BcMR0HCG7Ml0xTRi3fqMMGbDKxaaA9nqoy9f%2BxdM6%2BZkOij0mUswo73jIDBkxjuvy%2FmXYnVOTaCDo%2BxXYHcPQkF6E8YhLSXGi15MFRiLN6gReDT2yAPw5r0%2BvxixqrIRb8eobsJ6XWBfhFFwe1lAF6TFUMDUxc2ELCMT1I2C7sPNskmNysjWc3heWcXaCtPUJleXdIbEiqCZ2nEXRSQIJf9IjxvcYONYf0jCbbnHDTRZHdbFN7XD5%2F3raz9E24%2BG86DR%2F%2BAq580xyZwl8IsIco1h1NiyIvdsG9wycmGbGZobWM%3D" rel="nofollow">RabbitMQ系列之部署模式</a></li><li><a href="https://link.segmentfault.com/?enc=mi689zBNZQVgUIfmy224Ow%3D%3D.gCPQEBSnhsuVp1%2FkDe1BCWXJcKUEPmzJ9sXK%2FzFG1%2FbRJEIrtMkEJExMwOpqS71t3JNlJfVM5%2FnALDP7SBot3qv31LqSeoyyCGet6Wn1YETm7jg70WNsqs5lV7Tn1GQyXm%2FRFTjLJmiZA0TL0HpvbVJbV%2Fil75VGZ6P8RuW7SrVr5%2B8G3K6TZcUpKM%2F2IS9jiMFzYF1IXzKKha72Ku7hDqhaPNb5D2n%2Fk2dsPyrpjes%2FMtdMqmZUs%2BrlF61xFRGzaFdQDSRM3YQxwhNPIVmi55HRgpCFEsOhQdiuGGZle53qMgYXsTvEcjtv168ie39Q%2FM2EuEd3DPlt3xWLG4D04fYcT%2BWrbFLpRSb8K8K278I%3D" rel="nofollow">RabbitMQ系列之单机模式安装</a></li></ul><h3>欢迎大家关注微信公众号</h3><p>阅读更对关于 <strong>消息队列</strong> 的原创文章:<br> <img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
RabbitMQ系列之基本概念和基本用法
https://segmentfault.com/a/1190000022670544
2020-05-18T09:34:05+08:00
2020-05-18T09:34:05+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
2
<blockquote><ol><li>本文主要整理一下与 RabbitMQ 相关的一些基本概念,了解这些概念,是使用好 RabbitMQ 的基础;</li><li>并使用直连型交换机做了一个demo,通过实例去更好的理解这些概念。文末提供有 demo 下载地址。</li><li>可以访问 <a href="https://link.segmentfault.com/?enc=VphclhAFwU5g9DrQtaPRjw%3D%3D.ZCgrS5YID%2F5hQJotFDfPUri4K5EAWke8fuxQudn0%2B0qR%2B5Vby9jh6h7DPLwZ1%2Fe0uH7pD%2B6ynWdEWVb6T8FtynBwlKGgIEcmlLe0z1BfNoFHCvKQkvwiAOVcoL1C7M%2Bhx4RHH4UbA%2BGqiGC024JzovMnl%2BJq9U7pJ5%2FwADr1zGxCLbwFEY92%2BMXV71eXXSXL" rel="nofollow">这里</a> 查看更多关于RabbitMQ的原创文章。</li></ol></blockquote><h3>AMQP</h3><ul><li>即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计;</li><li>AMQP 的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。</li><li>RabbitMQ 是一个开源的 AMQP 实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、PHP等。</li></ul><p>备注:想对 AMQP 有深入了解可以点<a href="https://link.segmentfault.com/?enc=k9pBXB9Dn8SX50D%2Fcvc1ZQ%3D%3D.dzKVwP%2BNTcy1T5zTDBo2OSvvE5nrqvK7cLv%2Buez%2B8Vw03mV%2F4j8MfCw0kPcd4hRIsoe4Aaf78K3FzXRvDEdR1Q%3D%3D" rel="nofollow">这里</a>查看。</p><h3>RabbitMQ 的一些基本概念</h3><p>通过下面这张图片,我们可以对 RabbitMQ 的模型有一个大概的了解:<br><img src="/img/bVbHhM8" alt="rabbitMQ模型.png" title="rabbitMQ模型.png"></p><h4>基本概念</h4><p>需要了解的基本概念主要如下:</p><ul><li>Producer:消息生产者。</li><li>Consumer:消息消费者。</li><li>Connection(连接):Producer 和 Consumer 通过 TCP 连接到 RabbitMQ Server。</li><li>Channel(信道):基于 Connection 创建,数据流动都是在 Channel 中进行。</li><li>Broker(消息代理):实际上就是消息服务器实体。</li><li>Vhost(虚拟主机) : 虚拟主机,一个消息代理(Broker)里可以开设多个虚拟主机(Vhost),用作不同用户的权限分离。</li><li>Exchange(交换机) : 用来发送消息的 AMQP 实体,它指定消息按什么路由规则,路由到哪个队列。</li><li>Queue(消息队列) :是 RabbitMQ 的内部对象,用于存储消息。每个消息都会被投入到一个或多个队列。且多个消费者可以订阅同一个 Queue(这时 Queue 中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理)。</li><li>Binding(绑定) : 它的作用就是把交换机(Exchange)和队列(Queue)按照路由规则绑定起来。</li><li>Routing Key(路由键) :消息发送给 Exchange(交换机)时,消息将拥有一个路由键(默认为空), Exchange(交换机)根据这个路由键将消息发送到匹配的队列中。</li><li>Binding Key(绑定键):指定当前 Exchange(交换机)下,什么样的 Routing Key(路由键)会被下派到当前绑定的 Queue 中。</li></ul><h4>交换机的三种类型</h4><ul><li>Direct:完全匹配,消息路由到那些 Routing Key 与 Binding Key 完全匹配的 Queue 中。比如 Routing Key 为 <code>test1</code>,则只会转发转发 test1,不会转发 test2。</li><li><p>Topic:模式匹配,Exchange 会把消息发送到一个或者多个满足通配符规则的 routing-key 的 Queue。</p><ul><li><code>*</code>号表示匹配一个 word(比如:满足 <code>a.*.c</code>的 routing-key 有 <code>a.test1.c</code>);</li><li><code>#</code>号匹配多个 word 和路径,路径之间通过 . 隔开(比如:满足 <code>a.#.c</code>的 routing-key 有 <code>a.test1.test2.c</code>);</li></ul></li><li>Fanout:忽略匹配,把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中。</li></ul><h3>通过实例去深入理解</h3><p>一下子看到这么多概念,可能会有点发懵。下面我们通过一个很简单的 demo 去深入理解一下这些概念。<br><img src="/img/bVbHhNh" alt="panda.jpeg" title="panda.jpeg"></p><h4>SpringBoot 工程添加 RabbitMQ</h4><h5>pom文件添加依赖</h5><p>创建一个 SpringBoot 项目,在 <code>pom.xml</code>文件里添加依赖:</p><pre><code> <!--mq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency></code></pre><h5>设置 RabbitMQ 配置参数</h5><pre><code># rabbitmq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest</code></pre><h4>创建 RabbitMQ 配置类</h4><p>添加一个 RabbitMQ 的配置类 RabbitConfig:</p><pre><code>/**
* RabbitMQ配置
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @author lyf
* @date 2020-05-17 17:20
*/
Configuration
public class RabbitConfig {
/**
* 声明队列
* 参数说明:
* durable 是否持久化,默认是false(持久化队列则数据会被存储在磁盘上,当消息代理重启时数据不会丢失;暂存队列只对当前连接有效)
* exclusive 默认是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
* autoDelete 默认是false,是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
* 一般设置一下队列的持久化就好,其余两个就是默认false
* @return Queue
*/
@Bean
Queue myQueue() {
return new Queue(QueueConstants.QUEUE_NAME, true);
}
/**
* 设置交换机,类型为 direct
* @return DirectExchange
*/
@Bean
DirectExchange myExchange() {
return new DirectExchange(QueueConstants.QUEUE_EXCHANGE_NAME, true, false);
}
/**
* 绑定:将交换机和队列绑定,并设置路由匹配键
* @return Binding
*/
@Bean
Binding queueBinding() {
return BindingBuilder.bind(myQueue()).to(myExchange()).with(QueueConstants.QUEUE_ROUTING_KEY_NAME);
}
}</code></pre><h4>消息生产端</h4><pre><code>/**
* 消息生产端
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @author lyf
* @date 2020-05-17 18:30
*/
@RestController
public class ProducerController {
/**
* RabbitTemplate提供了发送/接收消息的方法
*/
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 发送消息(交换机类型为 Direct)
* @return
*/
@GetMapping("/sendDirectMessage")
public String sendDirectMessage() {
// 生成消息的唯一id
String msgId = UUID.randomUUID().toString();
String messageData = "hello,this is rabbitmq demo message";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 定义要发送的消息对象
Map<String,Object> messageObj = new HashMap<>();
messageObj.put("msgId",msgId);
messageObj.put("messageData",messageData);
messageObj.put("createTime",createTime);
rabbitTemplate.convertAndSend(QueueConstants.QUEUE_EXCHANGE_NAME,QueueConstants.QUEUE_ROUTING_KEY_NAME, messageObj);
return "message send ok";
}
}</code></pre><h4>生产消息</h4><p>代码保存后,启动服务,使用 Postman 请求生产消息接口:<br><img src="/img/bVbHhNr" alt="postman.png" title="postman.png"></p><h4>查看 RabbitMQ 管理界面</h4><p>Postman 请求成功,我们打开 RabbitMQ 的管理界面:<br><img src="/img/bVbHhNA" alt="rabbitmq-web1.png" title="rabbitmq-web1.png"></p><p>可以看到有一个名为 <code>demo1_queue</code> 的队列,说明我们测试的消息已经推送到RabbitMq 服务器上面了:</p><ul><li><p>Total 为 1</p><ul><li>因为使用Postman就请求了接口一次,因此生产端总共就生产了 1 条消息。</li></ul></li><li><p>Ready 为 1</p><ul><li>因为我们还没有启动消费端,所以没有被消费的消息数量为 1。</li></ul></li></ul><h4>消息消费端</h4><pre><code>/**
* 消息消费端
* @公众号 全栈在路上
* @GitHub https://github.com/liuyongfei1
* @author lyf
* @date 2020-05-17 18:00
*/
@Component
@RabbitListener(queues = {QueueConstants.QUEUE_NAME})
public class ConsumerController {
@RabbitHandler
public void handler(Map message) throws IOException {
System.out.println("收到消息:" + message.toString());
}
}</code></pre><p>保存代码,重启服务,在 Idea 的终端窗口,可以看到之前生产的那条消息已经被被消费了:<br><img src="/img/bVbHhNJ" alt="consumer-console.png" title="consumer-console.png"><br>由于当前队列就 1 条消息 且已经被成功消费掉了,再次访问 RabbitMQ 管理界面,会发现 Ready 和 Total 已经更新为 0。</p><h3>demo下载地址</h3><ol><li>文中 demo 代码下载请点击<a href="https://link.segmentfault.com/?enc=9QyCx8xaNRA9R88ts460hA%3D%3D.enToCp7yM6FlKo3B2n8Bp61fWz2oOxA2lV2Fou7D62zstGoHw3ArgkJf%2Fea6H4%2ByUUT0NFfwvX8oilJnjq2Wog%3D%3D" rel="nofollow">这里</a>。</li><li>欢迎大家关注扫描二维码或 添加微信公众号:全栈在路上</li><li><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></li></ol>
webpack系列之plugin及简单的使用
https://segmentfault.com/a/1190000014562068
2018-04-24T14:15:47+08:00
2018-04-24T14:15:47+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
3
<blockquote>欢迎访问 <a href="https://link.segmentfault.com/?enc=AOp06hfw6qRZCzro1RgRSA%3D%3D.LhoAMsVzaMOcVD%2FJSxZ5oGK9G75CncuAm2id7TMBuW729H0XucwIgaaUQXXzy4S2cxO7G%2FmmeTwFVrOQ8B0VR8PL29cEOBk%2FqXN6BfcN8AzuClybnSIyZiVj8M7Du8AU4hvwfpzvUevxw5QgfGm6lErk6wTcmWJqCBZQwh08ID3IY%2BoJUohoOIxXNeZ7sCPU" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h3>一.plugin有什么用</h3><blockquote><code>plugin</code>是<code>webpack</code>核心功能,通过<code>plugin(插件)webpack</code>可以实现<code>loader</code>所不能完成的复杂功能,使用<code>plugin</code>丰富的自定义<code>API</code>,可以控制<code>webpack</code>编译流程的每个环节,<strong>实现对<code>webpack</code>的自定义功能扩展。</strong></blockquote><h4>举例</h4><p>我们实际项目中就使用了<code>HtmlWebpackPlugin</code>插件,它帮助我们做了下面几件事儿:</p><ol><li>在工程打包成功后会自动生成一个<code>html</code>模板文件</li><li>同时所依赖的<code>CSS/JS</code>也都会被自动引入到这个<code>html</code>模板文件中</li><li>设置生成<code>hash</code>添加在引入文件地址的末尾,类似于我们常用的时间戳,来解决可能会遇到的缓存问题。</li></ol><p>项目打包后生成的模板文件如下:</p><pre><code class="htmlbars"><!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>移山</title>
<link rel=icon href=/static/assets/favicon.ico type=image/x-icon>
<link href=/static/css/app.37f937e3e08602bbb89778796e294cf1.css rel=stylesheet>
</head>
<body>
<div id=app>
</div>
<script type=text/javascript src=/static/js/manifest.2ae2e69a05c33dfc65f8.js></script>
<script type=text/javascript src=/static/js/vendor.d903c30c8b95cb48653b.js></script>
<script type=text/javascript src=/static/js/app.0c675ae0a3c300e0af57.js></script>
</body>
</html></code></pre><h3>二.什么是plugin</h3><blockquote><code>plugin</code>是一个具有 <code>apply</code>方法的 <code>js</code>对象。 <code>apply</code>方法会被 <code>webpack</code>的 <code>compiler</code>(编译器)对象调用,并且 <code>compiler</code> 对象可在整个 <code>compilation</code>(编译)生命周期内访问。</blockquote><p>一个<code>plugin</code>看起来大概是这个样子:</p><pre><code class="javascript">function CustomPlugin(options){
// options是配置文件,你可以在这里进行一些与options相关的工作
}
// 每个plugin都必须定义一个apply方法,webpack会自动调用这个方法
CustomPlugin.prototype.apply = function(compiler){
......
});
}
module.exports = CustomPlugin;</code></pre><p>有兴趣对自定义插件感兴趣,想了解的更多的,可以看<a href="https://link.segmentfault.com/?enc=84dQLPbanJp0pXLmRTUrdw%3D%3D.KUNUtKZn5qIcl0c%2BK8PgiOpnWoYVE4YMVI3iX64eOfT8AP7hE5b0b1%2B1NH5ZywCr3m0FzZAEYPoJco2Wd3P8cQ%3D%3D" rel="nofollow">这里</a>。</p><h3>三.使用plugin</h3><p>在 <code>webpack</code> 配置文件(<code>webpack.config.js</code>)中,向 <code>plugins</code> 属性传入 <code>new</code> 实例即可。比如:</p><pre><code class="javascript">const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
module: {
loaders: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader'
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin(), //访问内置的插件
new HtmlWebpackPlugin({template: './src/index.html'}) //访问第三方插件
]
};</code></pre><h4>注意</h4><ul><li><code>webpack</code>中的插件分为内置插件和第三方插件</li><li>内置插件不需要额外安装依赖,如上面的例子中:<a href="https://link.segmentfault.com/?enc=m36ihK3%2F%2B%2Bmy1yb%2F4BZ62w%3D%3D.CuohKTJxRYVQpxO%2BsMxjrSfqqgW4bwxAqiAyDjBvmMpxM8p7w2O7cnx9prMI5SrqundB9b4bfOAzgPEizVenAA%3D%3D" rel="nofollow">UglifyJsPlugin插件</a></li><li>如果是第三方插件,如上面的例子中<a href="https://link.segmentfault.com/?enc=72i0xj3Wt3FpmYb7S990MA%3D%3D.EgSFjXFQGpoWcVo0QYs%2BaRjBxPsTasaTDGpKK92Km%2BjiZgn%2B4aQyb5mUKtZMKwkH4NoX27rNyjNh%2BQeOF%2BOLEQ%3D%3D" rel="nofollow">HtmlWebpackPlugin插件</a>,则使用之前需要进行安装:</li></ul><pre><code class="bash">npm install html-webpack-plugin --save-dev</code></pre><h3>四.案例</h3><p>在对<code>plugin</code>有了一个基本认识后,来做一个小案例:</p><blockquote>“我想对所有的文件打包后添加一个版权声明”</blockquote><h4>目录结构</h4><p><code>webpackPluginDemo</code>的目录结构如下:<br>├── app<br>├── package-lock.json<br>├── package.json<br>├── src<br>│ └── index.js<br>└── webpack.config.js</p><h5>1. 安装<code>webpack</code></h5><p>在<code>webpackPluginDemo</code>根目录下安装<code>webpack:</code></p><pre><code class="bash">npm install --save-dev webpack</code></pre><h5>2.入口文件<code>index.js</code></h5><pre><code class="javascript">document.write('webpack系列之plugin的基本使用!');</code></pre><h5>3.<code>webpack</code>配置文件<code>webpack.config.js</code></h5><pre><code class="javascript">const webpack = require('webpack')
module.exports = {
entry: __dirname + "/src/index.js", //入口文件
output: {
path: __dirname + "/app", //打包后的文件存放的地方
filename: "bundle.js" //打包后输出文件的文件名
},
plugins: [
new webpack.BannerPlugin('版权所有,翻版必究')
],
}</code></pre><p><strong>注意</strong>:<code>BannerPlugin</code>为内置插件,如果是其它的外置插件,则需在使用前要先安装。</p><h5>4.执行打包命令</h5><pre><code class="bash">➜ webpackPluginDemo webpack
Hash: 16453f43abe665633286
Version: webpack 2.4.1
Time: 70ms
Asset Size Chunks Chunk Names
bundle.js 2.86 kB 0 [emitted] main
[0] ./src/index.js 210 bytes {0} [built]</code></pre><h5>5.查看结果</h5><p>打包成功,可以看到<code>app</code>目录下面已经生成了<code>bundle.js</code>,打开<code>bundle.js</code>会发现版权信息已经加上了:<br><img src="/img/bV9gqy" alt="图片描述" title="图片描述"></p><h3>五.常用插件</h3><h4>常用插件</h4><ol><li><a href="https://link.segmentfault.com/?enc=uth6B3r0v3jaNLvIfhYiPA%3D%3D.Z2f%2FcgL6O1R5M0uQt4FMAMdxQ7bGjgGM9szTjGDMVRwyQBZkkIDW7oIbr1nfqPIkvv9WBHU1wzCgiMJaqErv9Q%3D%3D" rel="nofollow">BannerPlugin</a>:对所有的文件打包后添加一个版权声明</li><li><a href="https://link.segmentfault.com/?enc=8KiNLN1IEV5DqTJJMa3FJA%3D%3D.3Bq8%2F%2Bq8T2WEiVSsR8swuqUefFhnuJi2lDpGNcyItiAOSpiWI9x%2BGBaCV26x0FkBdcs4pSkjrYNzXrAv3Cdi4g%3D%3D" rel="nofollow">uglifyjs-webpack-plugin</a>: 对JS进行压缩混淆</li><li><a href="https://link.segmentfault.com/?enc=1CbZ%2Bx6VrHFLquoyn0FfoA%3D%3D.5%2Bb%2F60224eB2c2X%2B6Akb06JE27kFnTFdOJdQd%2BhPTdWZlFgztCcZJZqP74oh96RI3dJ8OxadpL%2FphM%2BLaXDeSA%3D%3D" rel="nofollow">HtmlWebpackPlugin</a>:可以根据模板自动生成html代码,并自动引用css和js文件</li><li><a href="https://link.segmentfault.com/?enc=dqXqCSxqFnsIvQk3VhQWjw%3D%3D.zva9pGLG4ABp%2B1KjbbFJjpV0EMSOhrFM%2BVkHAyKzzDuQ68MFCRdiQj4C8aeqmGjiNSazGPqemEjngWuwYrR61U9bK75VKCqFAGBjVtasiV0%3D" rel="nofollow">Hot Module Replacement</a>:在每次修改代码保存后,浏览器会自动刷新,实时预览修改后的效果</li><li><a href="https://link.segmentfault.com/?enc=GH%2BTrdzsm7eAZGurcgRnXg%3D%3D.rDAuECh48NinTg%2BJDhDNJ3V4Iy1bqRvR6BT8Zs%2FrnBQ7OtxRaqqKKWLz8iMqCff9ASd19qJ4FvjMNfQG%2Bmmp%2FA%3D%3D" rel="nofollow">copy-webpack-plugin</a>:通过Webpack来拷贝文件</li><li><a href="https://link.segmentfault.com/?enc=onQtcVeIi3enQMrF20Hnfw%3D%3D.YeG6bPFdlJ1FWifdCd8CRoxa3zt5jMhXyFFwNx4NBVwGWgR16kHzxLOFfkF%2F8Ulsqxyx8c%2FomvkLS8z8uIiRmA%3D%3D" rel="nofollow">extract-text-webpack-plugin</a>:将js文件和css文件分别单独打包,不混在一个文件中</li><li><a href="https://link.segmentfault.com/?enc=CnmpocqW1KKwLeSYNU6Djg%3D%3D.zrWMxZS21prZAdaH%2BK1VMUgPrU4lP3nlmdVZ4%2FXVUbNAso9Q%2FbCoqbHBHWrwjWq7Xl7rFuxNPsWGJM3CfjU2%2FQ%3D%3D" rel="nofollow">DefinePlugin</a> 编译时配置全局变量,这对开发模式和发布模式的构建允许不同的变量时非常有用</li><li><a href="https://link.segmentfault.com/?enc=ia2qf0u9%2Bs9KJ8cYc8Palg%3D%3D.m8yRzbRD12a7kwT%2Bf7ZD9kWCxBbJ%2BV0kofVPLfMcfwKLOCmIpV39CbkkErfkdZ%2F8OKqJ4sqPwbrWDypmmMEawL%2FDz6VuSd3xOWeQy2lV2dY%3D" rel="nofollow">optimize-css-assets-webpack-plugin</a> 不同组件中重复的css可以快速去重</li><li>更多可点击这里<a href="https://link.segmentfault.com/?enc=qmPkl7udakY8R8X5ZV1oXg%3D%3D.yOjOFy48zruhedbXf9KXRIl5cVUzwNqkg63ZJaBZw4YSx6XJsY9s0mD4%2FgV81K1oLRbWWjn6%2F6y7o3Tp0Z9Yqg%3D%3D" rel="nofollow">查看</a>。</li></ol><h2>六. 更多文章</h2><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=Y613MmhiAVrCQVvXngMUzg%3D%3D.87CQ%2Bc8ful2mecShqoH9D5gWM53EaDGhp8ex7KD60nIvxQptzW%2Bwg43yFrmSAWvFCgUoc4wauFmGRMPL8obMYY93i1mIOWTpOIuZtw6GpPSMY6yQJnsUtQ11J8fGwVgV4HqAhai8MjXKvu%2F8Is1SyOsG65JlaQHHQHoE7CmXbetj2imzwyEIISpC9q08e0qgcAr4flkbXAKoiutGIdayGtP0r69SsFdhz2zOj1cLwosNlXNatxy16tNbo1fZ7r5nCZUMDSVz8PuGMMSVw7ci%2BF2pnbMl%2BDgus5ZsEN9lsybZFhDk2vJ%2BQ5%2BHr%2FOw28mkw7n4xgBdx9S8tS%2FZrfOTeA%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=Jk724S72%2FiIjMaQKJzNLFw%3D%3D.%2BU30TVzxwJ6wG8K9sPvlqJOkqaiiUtFdcpxNwqqe%2Bvi0AwSz0H%2FH3D0f4wOrCBQXOBcbVdb10nzdf3CW3ilAXet9mFGX%2FY5vUivqN8N738dg%2FGuoCXUFyxWtWE4TxVGz5OCvceBoxvGXCtMWWCDXwSofXwHLrPizGWBxhiz%2BXkXVdhQVwj%2FnDWYIj%2FdxoKrLNuTvMPaGB7pqFgkcXd2UCTPGbHHA0y%2BYMSNUTijZI72%2Fah0096UApYmEdcy%2FAd5o8sEStyUACAjKKHcTUfujZxE%2FmapRN19q3lqkVtuVQG7DOdkSkOZ%2BJiEGsOzQTPjV9KLTFQsiv54isb84ezEzLw%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=eTxea5ykTyKHxTbpXo2aMw%3D%3D.jpuhmKI42zomGQXSMh5r6B%2BT46ZXDcjORKbeVE3nlYEaSMC0q088B%2Ftc7wwkBcYY%2FGBJMPs1Ppf5xtvKs5KCMKh3qUosvqCuKaZXmyQFKqzMTHRbhMo47PRIILHQd3HgXqQLlgXV2szGYGErta8SBpt%2FiYWVe%2BheMTCLQLbXr8JrE2CVOgeN6GnxNoSAaYPygW5pq%2FTWEViragpCUj%2FWP7J55FOwEXM2t%2B7tjWqQpdH8yFbgemW11oHZosb169aG%2F4nZBlIm5r8EcKKFwJti6PkhV1vtERWlJMOkW0DPjud113ig%2F%2FKt0fCdpKiJwdbXV9iLiS7SK9VGJo4DKUaUIg%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=JBP7wGUnDBdtv0wa86Barg%3D%3D.jYp8GwPPKPCFMm3aVMFR2uyJiudB6BizEJtDo%2FkTNYZl1bnucOV7JPKZD5XQQxHuOEB2GvB4ez605PO2DKuPd%2B7KMM%2BXHPN4G4xjJJNDMgwKD2tbw1a%2FPeoDmC4ma0Aj8e6AX%2BKQj%2Bbe5%2FG4dEp5WZIjMeCMPmBTh2evPchCl6%2FuYLZJYdCwrWjn4KU9dyoLwS3mwK%2BKQcLQ5pJM9hxXXqsFD%2F5p0S7ZYYdsd4wTyKurDci431NE207yiHKsqa3mSW5epLXjuRAtmCTSU7HL6VVUmpqLtgXbqpkhKhx7Qf5MkV2jxwv1jpeaUmzQtg4cM1IvSRXaqeqZFLVpa9FvqA%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><h3>欢迎大家关注微信公众号</h3><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
webpack系列之loader的基本使用
https://segmentfault.com/a/1190000014408973
2018-04-16T14:22:09+08:00
2018-04-16T14:22:09+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
16
<blockquote>可以访问 <a href="https://link.segmentfault.com/?enc=TxvF6CdUuUVOt1yuTIWRfQ%3D%3D.bqiyCOgvGsdurS9QoaFPE785MQ8EJOrj2FnEC0AISAQavvaV2jzL3OU4N%2B2cRPWpzXRI%2B%2F%2BzQThlkSG4%2Fp0Vf13gxm8I6m9syl6GkvQAyHdypIIawAnlsoJGwrCCN5moEbIalWYxxVl6dl1P4Z5N%2BRDhe7VCjr0qKOamxuw0dCkrmJyd4FtfLCwFolszm77i" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h2>webpack系列之loader及简单的使用</h2><h3>一. loader有什么用</h3><blockquote><code>webpack</code>本身只能打包<code>Javascript</code>文件,对于其他资源例如 <code>css</code>,图片,或者其他的语法集比如<code>jsx</code>,是没有办法加载的。 这就需要对应的<code>loader</code>将资源转化,加载进来。</blockquote><p><strong>比如</strong><br>你的工程中,样式文件都使用了<code>less</code>语法,是不能被浏览器识别的,这时候我们就需要使用对应的<code>loader</code>,来把<code>less</code>语法转换成浏览器可以识别的<code>css</code>语法。</p><p>例如一个简单的<code>less</code>文件:<br><strong>转换前:</strong></p><pre><code class="less">.demo {
width: 200px;
height: 100px;
margin: auto;
border: 1px solid;
p {
font-weight:bold;
padding-left: 30px;
}
}</code></pre><p><strong>转换后:</strong></p><pre><code class="css">.demo {
width: 200px;
height: 100px;
margin: auto;
border: 1px solid;
}
.demo p {
font-weight: bold;
padding-left: 30px;
}</code></pre><p>后面的案例也是拿这个<code>less</code>文件来做演示的。</p><h3>二. loader是什么</h3><p>先来看一下官方对<code>loader</code>的一个解释:</p><blockquote>A loader is a node module exporting a function</blockquote><p>翻译过来:<code>loader</code>就是一个<code>export</code>出来的<code>function</code>。</p><hr><p>既然是 <code>node module</code>,所以如果你自己要自定义一个<code>loader</code>,完全可以这么写:</p><pre><code class="javascript">module.exports = function (source) {
// todo
}</code></pre><p><strong>解释</strong></p><ul><li>其中<code>source</code>参数是这个<code>loader</code> 要处理的源文件的字符串</li><li>返回经过<strong>"翻译"</strong>后的<code>webpack</code> 能够处理的有效模块</li></ul><p>如果你所写的 <code>loader</code> 需要依赖其他模块的话,那么同样以 <code>module</code> 的写法,将依赖放在文件的顶部引进来即可:</p><pre><code class="javascript">var fs = require("fs")
module.exports = function (source) {
// todo
}</code></pre><p>如果你希望将处理后的结果(不止一个)返回给下一个 <code>loader</code>,那么就需要调用 <code>webpack</code> 所提供的 <code>API</code>。</p><p>由于本篇我们只讲<code>loader</code>的基本使用,故这里不再深入讲解,有兴趣的可以<a href="https://link.segmentfault.com/?enc=MSsOafI8RrWke3Jj0V%2BRYA%3D%3D.N0gdEwClEoZg9PJQQF6MU7X3K7UGM1pTI7Z6yh%2Bd%2BVxHL%2Brmroi70ClPgCD9EEme" rel="nofollow">点击</a>这里学习。</p><h3>三. 使用loader</h3><p>在看了前面的介绍后,接下来给大家介绍一下怎么使用<code>loader</code>。</p><h4>使用loader的方式</h4><p>有三种使用方式,如下:</p><ul><li><a href="https://link.segmentfault.com/?enc=oVRvoEn3lsFGRsmTTpfiJg%3D%3D.59u2irg41%2FqiH2QXnp1IWdbZiorKZFSAbjTI36%2BDUn0TniFsXzVhLBWKbF5fe4hrkpNcR%2BMc0G3aiDOwmhgg4w%3D%3D" rel="nofollow">配置</a><strong>(推荐)</strong>:在 <code>webpack.config.js</code> 文件中指定 <code>loader</code>。</li><li><a href="https://link.segmentfault.com/?enc=Tq5LhqmZR8uYNZ%2F2Xl2T%2Bw%3D%3D.5U4ZK9iQslLAlpwmYuE%2BX6In6l63gAFHW9D0y%2FtuneFyp8JhBbYm6TlHK6zjGGiTRv9GIRSQBONZJByazMvCrA%3D%3D" rel="nofollow">内联</a>:在每个 <code>import</code> 语句中显式指定 <code>loader</code>。</li><li><a href="https://link.segmentfault.com/?enc=2j9unPYiSBVyg5dJWI0hIg%3D%3D.jbYFIrurdHChSqRK3FFl%2FzTFNbL57iXBmvYr%2F8VJ5%2BLZvuuFCcgri6s6TfgfCr1fmTDzIsco5L4whj6B9WHCEw%3D%3D" rel="nofollow">CLI</a>:在 <code>shell</code> 命令中指定它们。</li></ul><hr><p><strong>以上三种方式,我们在开发过程中推荐使用第一种方式:</strong></p><p>比如你想使用<code>webpack</code>来打包样式文件,则可以在<code>webpack.config.js</code>里添加如下代码:</p><pre><code class="javascript"> module: {
rules: [
{
test: /\.css$/, // 正则匹配所有.css后缀的样式文件
use: ['style-loader', 'css-loader'] // 使用这两个loader来加载样式文件
}
]
}
</code></pre><p><code>module.rules</code> 允许你在 <code>webpack</code> 配置中指定多个 <code>loader</code>。 这是展示 <code>loader</code> 的一种简明方式,并且有助于使代码变得简洁。</p><p><strong>上述rules的作用:</strong><br><code>webpack</code>在打包过程中,凡是遇到后缀为<code>css</code>的文件,就会使用<code>style-loader</code>和<code>css-loader</code>去加载这个文件。</p><h3>四.案例</h3><p>在对<code>loader</code>有了一个大概的认识后,来做一个小案例,需求如下:</p><blockquote>将上一篇(<code>webpack</code>系列之基本概念和使用)的<code>demo</code>输出文字居中并用黑框圈起来</blockquote><h4>目录结构</h4><p>代码目录结构如下:<br>├── node_modules<br>├── app<br>│ ├── bundle.js<br>│ └── index.html<br>├── package-lock.json<br>├── package.json<br>├── src<br>│ ├── index.js<br>│ └── main.less<br>└── webpack.config.js</p><h4>1. 安装loader</h4><p>我们必须使用 <code>loader</code> 告诉 <code>webpack</code> 加载 <code>less</code> 文件,为此,需要首先安装相对应的 <code>loader</code>:</p><pre><code class="bash">npm install --save-dev less
npm install --save-dev less-loader
npm install --save-dev css-loader
npm install --save-dev style-loader</code></pre><p>这些<code>loader</code>的作用如下:</p><ul><li>安装<code>less-loader</code>后可以在<code>js</code>中使用<code>require</code>的方式来加载<code>less</code>文件了;</li><li>安装<code>css-loader</code>后可以在<code>js</code>中加载<code>css</code>文件;</li><li>安装<code>style-loader</code>的目的是为了让加载的<code>css</code>作为<code>style</code>标签内容插入到<code>html</code>中。</li></ul><h4>2. 配置loader</h4><p><strong>webpack.config.js代码如下:</strong></p><pre><code class="javascript">module.exports = {
devtool: 'eval-source-map',
entry: __dirname + "/src/index.js", //入口文件
output: {
path: __dirname + "/app", //打包后的文件存放的地方
filename: "bundle.js" //打包后输出文件的文件名
},
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader','css-loader', 'less-loader']
}
]
}
}</code></pre><h4>3.新建样式文件</h4><p><code>main.less</code>代码如下:</p><pre><code class="less">.demo {
width: 200px;
height: 100px;
margin: auto;
border: 1px solid;
p {
font-weight:bold;
padding-left: 30px;
}
}
</code></pre><h4>4. 修改入口文件</h4><p>在入口文件<code>index.js</code>里引入我们的样式文件</p><pre><code class="javascript">require ('./main.less');
var element = document.createElement('div');
element.className = 'demo';
var p = document.createElement('p');
p.innerText = 'webpack系列之loader的基本使用!';
element.appendChild(p);
document.body.appendChild(element);
</code></pre><h4>5.打包</h4><p>在项目根目录(<code>webpack-demo</code>)下执行打包命令:</p><pre><code class="bash">➜ webpack-demo webpack</code></pre><p>打包成功,会输出如下:</p><pre><code class="bash">Hash: 1bb51c6a348686a223db
Version: webpack 3.10.0
Time: 1077ms
Asset Size Chunks Chunk Names
bundle.js 53.8 kB 0 [emitted] main
[0] ./src/index.js 273 bytes {0} [built]
[2] ./src/main.less 1.19 kB {0} [built]
[2] ./node_modules/css-loader!./node_modules/less-loader/dist/cjs.js!./src/main.less 304 bytes {0} [built]</code></pre><h4>6. 查看结果</h4><p>在浏览器里刷新<code>index.html</code>:<br>你会发现<strong>输出的文字被一个黑框给圈了起来,并且加粗显示</strong>,这就表明我们的样式文件已经生效了,而且从截图当中也可以看见样式文件也插入到了html中。<br><img src="/img/bV8CAh" alt="图片描述" title="图片描述"></p><h3>五.常用loader</h3><h4>样式</h4><ol><li><a href="https://link.segmentfault.com/?enc=lnuvGoLdx1HhashfIkMnqg%3D%3D.zA9VR%2Btigse%2FA%2FeyQDTpxFoUQWL%2FgQht2qHBy24JQJtaIoneoxKmZrlc5h4m0H7t" rel="nofollow">css-loader</a> : 解析<code>css</code>文件中代码</li><li><a href="https://link.segmentfault.com/?enc=H8zPfJ4G7HJZws7MwFXhAQ%3D%3D.sQ8LLMTEy9iH72u4fMPWXoN0lBVAx7FE73WgmNoZ4jXDMoAmWfjRLTO0Waof6gyG" rel="nofollow">style-loader</a> : 将<code>css</code>模块作为样式导出到<code>DOM</code>中</li><li><a href="https://link.segmentfault.com/?enc=Tpq2wONtSNvjsonhxt%2BRDg%3D%3D.zn8fNeJa03D%2BLjU4RYHLau7hb3gvyISbXayb1Tk7QlNFTY%2Bi%2BFsQRjC6W%2FQ06UC7" rel="nofollow">less-loader</a> : 加载和转义<code>less</code>文件</li><li><a href="https://link.segmentfault.com/?enc=Wtumrn3xKAKe4Xb7gY8vOQ%3D%3D.SnaR%2FyoHEmLbxqbXodTCbwxjY%2Bb4ftwRzj9gZtomPkLhzAD2K%2FXym%2FFnJll%2FyIXz" rel="nofollow">sass-loader</a> : 加载和转义<code>sass/scss</code>文件</li></ol><h4>脚本转换编译</h4><ol><li><a href="https://link.segmentfault.com/?enc=CDouFUIHegu6k%2FKSKBOsZQ%3D%3D.6Ws5zaFSdQu0mdZe694tvnWJ8jKDaTHLxaoSQZzdfEjPXQmH%2BETikhrYZdAs8Ehv" rel="nofollow">script-loader</a> : 在全局上下文中执行一次<code>javascript</code>文件,不需要解析</li><li><a href="https://link.segmentfault.com/?enc=i14gXxu7hQwxjg24y2S1%2FQ%3D%3D.20q4WNhI58EbND6j0DOgZ3H5AfBVjH0qX8mIxzsf2%2FZVuuBzEGuZ22Wp95JFubUp" rel="nofollow">babel-loader</a> : 加载<code>ES6</code> 代码后使用<code>Babel</code>转义为<code>ES5</code>后浏览器才能解析</li></ol><h4>Files文件</h4><ol><li><a href="https://link.segmentfault.com/?enc=egP2McjE0b0hwLxmhraUug%3D%3D.W%2BQLDl30qHVZj2UID0G8KPViLuKWrpfBCQ%2FjUI%2FEy1maFp08kOuUF%2FA5ANNcVykO" rel="nofollow">url-loader</a> : 多数用于加载图片资源,超过文件大小显示则返回<code>data URL</code></li><li><a href="https://link.segmentfault.com/?enc=742pACO2gFaECQ2e1t80Lw%3D%3D.ytHg433ls9lKhv6QYkpePJTIVjm%2Ff5j5zLUKWYngN1hak29r02ooPkuSA5RcjJpK" rel="nofollow">raw-loader</a> : 加载文件原始内容<code>(utf-8</code>格式)</li></ol><h4>加载框架</h4><ol><li><a href="https://link.segmentfault.com/?enc=VfayJH%2FSeuaZszndtteVXA%3D%3D.Lm3YTUF%2Bt%2Fn8%2FtCNj3M7AnxgVKn5TG%2BPV6PvjwbiSCK6LmcSogXBVdgtAJTe8n7J" rel="nofollow">vue-loader</a> : 加载和转义<code>vue</code>组件</li><li><a href="https://link.segmentfault.com/?enc=DoPAVVL6WFQxh5mOXcL4oQ%3D%3D.gPAlN1UK4dRuXhbDbQ1do%2FhjfFAFv9jZxl5Nntv%2BlstjR0HI2yy6MkEgxEC6CrH%2B" rel="nofollow">react-hot-loader</a> : 动态刷新和转义<code>react</code>组件中修改的部分</li></ol><h3>六. 总结</h3><p>本篇向大家介绍了loader有什么用,什么是loader,以及怎么使用loader这些基础知识,如果有兴趣想了解得更深入一些,可以看看<a href="https://link.segmentfault.com/?enc=x0p2xgSW3b%2BOeWDbbtXm%2FA%3D%3D.gGutT%2Fc1iZMAcx9NLqykXfqAR1nLZOLSVeKzUGg7x82Xz2woR2DhUNkGm4VzIc77%2BGhshjEznR4ecPHZylczYA%3D%3D" rel="nofollow">怎么编写一个loader</a>。<br>下一篇会给大家介绍:<strong><a href="https://segmentfault.com/a/1190000014562068">webpack系列之Plugin及简单的使用</a></strong></p><h3>欢迎大家关注微信公众号阅读更多文章</h3><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=hsIbboSTvhAE1YSyuaVXHQ%3D%3D.Xq0WbKnhLLy7M1odw5bfcmgW4OSraD5hHnHQoCL6vVIFFMJoNRwftfvxw%2BSZG6nzcWH89Qt641slTZE%2F45yH%2Bi8MSnsFaGWu8y1DyOFBgoYl6w8PZJ33w6Z6CGrab7bAdEBDFWjMX86IIePfRYlSHr7sUynS3xWG4gjlGo9ykFImVafqxz4dEOeb%2BgSDJYbuL20lsssPjIecVbCoa1ZlBs0gBG9kr9h0bnbwbTnC5sGAlA40W%2FoQqPJuBlE%2BKUrjTTDWfYesg6%2F0rZzSV3I3Mdw568j%2F4DeQZF2rDz9c%2FNrc2kTBh8aJFlF7rf2FOo1U8zFOzqUJPAbuqhy1239sog%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=F07U1tp5nwlen4Vpdzy4Lw%3D%3D.GygqXdOpcTmaxAD5DExw%2BhTGepx2SyfJS9W5RZs0jmrQ3zuvLkesd1a%2BcGVcSRq9HgdEKtHDwZB3%2BVR%2BG%2Fw78xqY5UM6GhancK2%2FihwtJr%2FRSB0XaFstvGJol1AX9deOSGIZRxXAX%2BGnFaW0wKxWjmdIZPOkkup9qIBXc2weDkrsyPPW%2BsRl1mewbqWLCYY%2BzEykpbA3TMO3XoRM6dHcqVNezHfzT99DNZ5uQMiVgNev66j5g4IihZKQ5C4E0s%2F2Z%2BKy9eDdgLkHfWHFDIF2JXlJ0ljRZWm1vsYngCWtAbZwbXAybAHtHtTWYooJq8ZAtJnOyjLsmgPPTT2euj%2B1LQ%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=Q2XUsMuJbKAncCPIcPybZA%3D%3D.Hp6qp5Co3SUlIQ5Aslz7h4XVL5qbI%2BlQWQYkSTUopo87pwf1DtSIEnmBKHS2xBpnfTXa6P9cIxXdRX1kyvA%2FkxqKTn%2Fk7fDiVTfj52PwQF2S7%2B721cwj834PzdY8gdhNEX%2FI8x0lVzMmJSjNrksMEG7R26eAPfbnTGo4D7xgDSViBUKBex5Qxj6o5UWdGizIKZW4Td%2BtwU5i9lns1klbU%2BS%2F9XwHorjNTwNno9sI3L13kGdZFxaRyn5Yq58Cd35jlcURNBNMv3h9KC9%2B%2F%2BSp0VJBHx3Mxaa7F33Hi7siptvIBp4E10%2FfVcN%2FDp74UW9ETxGqOC%2FE0PV8BAd43cdAuA%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=HCl3F%2FL31Fjqi6bPtHZ4Og%3D%3D.X%2FiTGjD0u6WAoHjb%2FyYz3QElXwMMCViNjd%2FCVnkghhUBJXbpGPu2SxP0bJkQvaWw9ukA%2BtJIFcr%2BnLTPixrZpj3ZnkWdQvJkIqQdfLPgElySgtAXQt2X%2F%2F6rHEBFblP4YyR6wsRNR6y%2FhLp3whJ6NLs7%2BKXTaIlNK5YBol8nhF0cWwykoFwQq1dhiYcNvlnrd%2ByHlvWftmZ6B5pxlK7YlVedREeqqFpZniKr6L0zAj%2BL5PjlZgyBXDbAF7%2F6%2FVCn1pqmrOwEmuR%2BKcZq8HtivlaHamkGOLQg1%2BJFHQUiwwmuVY2pcQDEVJngkFp54BcBuAw2ZJjM3wCprLnhpWUZhQ%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
webpack系列之基本概念和使用
https://segmentfault.com/a/1190000013761990
2018-03-15T17:49:40+08:00
2018-03-15T17:49:40+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
8
<blockquote>可以访问 <a href="https://link.segmentfault.com/?enc=ZORG9E11nVZf8c4LW%2B%2FRyQ%3D%3D.xdaJpE0NKPr%2Bd1zldj24iRBiZlgEGCpjkqpxOBPFnNv5M6ygluqos3MOV%2Bfi%2FeXYvpteowEj2Pmih3Zm1dIpz0d5kClz%2F4KESfnVaJc5SHV0zXr1rFXCmFgE%2F17mhwKKn9ShsXNESdRXmoj86h9QYG21Gy5%2F6MnFEcVxmtana54ke%2FUWK5JSREHR93RxhrWw" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong> 的原创文章。</blockquote><h2>前言</h2><p>最近在做<code>vue</code>项目,自然而然就接触到了<code>webpack</code>这个打包工具,借此把一些总结分享出来。<br><code>webpack</code>知识点比较多,打算做为一个系列分篇讲解,今天先分享第一篇:<code>webpack</code>系列之基本概念和使用。</p><h2>一. 什么是webpack</h2><p><code>webpack</code>可以看做是模块打包工具:它将各种静态资源(比如:<code>JavaScript</code> 文件,图片文件,样式文件等)视为模块,它能够对这些模块进行解析优化和转换等操作,最后将它们打包在一起,打包后的文件可用于在浏览器中使用。</p><p>下面看一个图能够很清晰的展现这个打包流程:<br><img src="/img/bV5Uhs" alt="图片描述" title="图片描述"></p><h2>二. 为什么使用webpack</h2><p>先举一个大家都很熟悉的例子:</p><blockquote>一台计算机内部需要很多很多根线来连接单元器件,假如每条线不按规则摆放,将要占用极大的空间,且不好管理。幸好人们发明了集成电路,才有了现代计算机的板卡。<br><img src="/img/bV5Uut" alt="图片描述" title="图片描述"></blockquote><p>大家看,通过这个板卡,所有的元器件,电线都按规则摆放,规整有序,很大的节省了空间,而且也很方便管理。</p><p>你可以把<code>webpack</code>看做是这个板卡,项目中的各种各样的<code>JavaScript</code>程序和依赖包可以看成是计算机内部需要的很多根线和单元器件,<code>webpack</code>通过<code>loader</code>将这些<code>JavaScript</code>程序和依赖包都转换成<code>JavaScript</code> 模块,就好比我们将单元器件和线按照一定的规则摆放,放在固定的位置方便管理,这样通过<code>webpack</code>我们就把一个项目中的复杂程序细化为了各种具有依赖关系的模块,从而使我们的项目管理起来更加方便。</p><h3><code>webpack</code>优势</h3><p><code>webpack</code>能替代部分 <code>grunt/gulp</code> 的工作,比如打包、压缩混淆、图片转<code>base64</code>等,而且还具有以下几点优势:</p><ul><li><code>webpack</code> 是以 <code>commonJS</code> 的形式来书写脚本的,但对 <code>AMD/CMD</code> 的支持也很全面,方便旧项目进行代码迁移</li><li>能被模块化的不仅仅是 <code>JS</code> 了</li><li>扩展性强,具有强大的插件(<code>Plugin</code>)接口,使用起来比较灵活,特别是支持热插拔的功能很实用</li><li>可以将代码切割成不同的块(<code>chunk</code>),每个块包含一个或多个模块,块可以按需被异步加载,降低了初始化时间</li><li>......</li></ul><hr><h2>三. 基本概念</h2><p>在正式讲解怎么使用<code>webpack</code>之前,你需要先理解四个核心概念:</p><ul><li>入口(entry)</li><li>输出(output)</li><li>loader</li><li>插件(plugins)</li></ul><p>本篇做为一个开门篇,先讲解前两个核心概念:入口(entry)和输出(output)。</p><h3>3.1 入口(entry)</h3><p>入口起点(entry point)指示 <code>webpack</code> 应该使用哪个模块做为入口文件,来作为构建其内部依赖图的开始。进去入口起点后,<code>webpack</code> 会找出有哪些模块和库是入口起点(直接和间接)依赖的,每个依赖项随即被处理,最后输出到称之为 <code>bundles</code> 的文件中。</p><h3>3.2 出口(output)</h3><p><code>output</code> 属性告诉 <code>webpack</code> 在哪里输出它所创建的 <code>bundles</code>,以及如何命名这些文件,这些都可以在<code>webpack</code>的配置文件中指定,后面的案例会给大家介绍怎么去配置。</p><h2>四.案例</h2><p>在讲了<code>webpack</code>是什么,为什么使用<code>webpack</code>,以及两个核心概念后,我们来做一个小案例来真实感受一下。</p><h3>4.1 基本安装</h3><p>我们创建一个目录,初始化<code>npm</code>,并且在本地使用<code>npm</code>安装<code>webpack</code></p><pre><code class="bash">mkdir webpack-demo && cd webpack-demo
npm init
npm install --save-dev webpack</code></pre><h3>4.2 目录结构</h3><pre><code class="bash">├── node_modules
├── dist
│ └── index.html
├── package-lock.json
├── package.json
└── src
└── index.js</code></pre><h3>4.3 修改入口文件</h3><ul><li>打开<code>src\index.js</code>,添加如下代码:</li></ul><pre><code class="bash"> var element = document.createElement('div');
element.innerHTML = 'webpack demo!';
document.body.appendChild(element);</code></pre><h3>4.4 执行打包命令</h3><pre><code class="bash">➜ webpack-demo webpack src/index.js dist/bundle.js
Hash: 2432d7e2ecc1d3cb0c5b
Version: webpack 3.10.0
Time: 63ms
Asset Size Chunks Chunk Names
bundle.js 2.65 kB 0 [emitted] main
[0] ./src/index.js 179 bytes {0} [built]</code></pre><p>打开<code>dist</code>目录,你会发现打包后的文件<code>bundle.js</code>已经生成。</p><h3>4.5 引入<code>bundle.js</code></h3><ul><li>打开<code>dist\index.html</code>添加如下代码:</li></ul><pre><code class="htmlbars"><html>
<head>
<title>webpack练习</title>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html></code></pre><h3>4.6 效果</h3><p>直接浏览器打开 <code>index.html</code>:<br><img src="/img/bV5UhG" alt="图片描述" title="图片描述"></p><h3>4.7 使用配置文件</h3><p>可能大家看到打包命令后会有疑问:</p><blockquote>这个打包命令有点长,这样岂不是很容易出错?</blockquote><p>的确,<code>webpack</code>有许多比较高级的功能都可以通过命令行模式去实现,但是这样很不方便,且容易出错,更好的办法就是定义个配置文件,我们可以把所有的与打包相关的信息放在里面。 这比在终端(<code>terminal</code>)中输入大量命令要高效的多。</p><p><strong>那该怎么做呢?</strong></p><p>我们新建一个<code>webpack</code>配置文件:<code>webpack.config.js</code>:</p><pre><code class="bash">module.exports = {
devtool: 'eval-source-map',
entry: __dirname + "/src/index.js", //入口文件
output: {
path: __dirname + "/dist", //打包后的文件存放的地方
filename: "bundle.js" //打包后输出文件的文件名
}
}</code></pre><p>命令行执行只需要:<code>webpack</code>即可实现打包:</p><pre><code class="bash">➜ webpack-demo webpack
Hash: 37ae154d97c486e04d87
Version: webpack 3.10.0
Time: 73ms
Asset Size Chunks Chunk Names
bundle.js 3.4 kB 0 [emitted] main
[0] ./src/index.js 179 bytes {0} [built]</code></pre><p>这条命令会自动引用<code>webpack.config.js</code>文件中的配置选项进行打包,再次访问<code>index.html</code>,会发现第一种打包方式后输出同样的结果,但是简化了命令,也降低了因命令行过长而导致的错误。</p><p><strong>注意:</strong></p><blockquote><code>webpack.config.js</code>是<code>webpack</code>默认的配置文件名,如果我们的配置文件不叫这个名字时,我们需要借助一个 <code>--config</code> 参数来实现打包(<code>--config</code> 参数来指定去找哪个配置文件):</blockquote><pre><code class="bash">webpack --config `webpack.filename.js`</code></pre><hr><h2>五.总结</h2><p>第一篇先讲解一下<code>webpack</code>的基本概念,以及通过一个小案例让大家感受一下<code>webpack</code>的简单使用,下一篇会深入一些,讲解<code>webpack</code>中的另一个重要概念:<a href="https://segmentfault.com/a/1190000014408973">Loader</a>。</p><h2>六.欢迎大家关注微信公众号阅读更多文章</h2><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=E%2BONhRyOU8Fl9R%2Box0%2FRsg%3D%3D.NwpOWy1bfQlsQOBq9sVcVf2kw44gz7wV44%2BO6CWUJCnTpT6h%2FiVShD4JWurNRSYi9Wa3VWL273%2F3xxQAE5ixErpNPx4CnNA8FemM2w9RQ4KFErbuX2jewUGtO1VNWLiRbgWwsBRBmJNH8QvKUAG9VpaLxKSb7SJjdJrYasrfWstRp%2FfmIHlUjen4%2FPu28t7dtZfRVAdxjzVzKE%2FtXg7PxB45l8fCq9%2F3ZvfJAhjBtE9Avg%2BVxDcBTi%2FF0aJxU57xkrVmIiE9%2F6PM6AzrAr6aQSATcTdGYJj4f228K3xM8fk8Nv3YfdrrP3Bo4yLJu%2By1x8GLEbf82WA2z33MycmafQ%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=65mMtI9FdvAVXs6XZGUt1g%3D%3D.PE4VOZ3cRJhmuX3L6H%2FAS%2FqZWb%2FNKmL8jO25qTSdxbJNP7UiUKG6ybZOLk4CJu0OMQVvOVVkrrE7z16CM52ER7zMRshqJEz%2B%2BgbeHqLzcVDSZqpSOFNLTg5Im%2FNjN7Scd8PrwTV4pvxffl1cE9zvAAIZGBgt%2FRZau9UE6%2Bs9ubNEEudc%2BPd2qvjCUIRjJSKhoRUXRCmNYHcQZytMVSZ%2FMTmrGR5XKXovQN6Qgj0KLx%2Fgk%2FW3C6D0gVDA8%2FbFRJnrmfCp5pY7MYLTDYaL%2FfImeF8Zkf4oP46eXboZwI8DDXFNILso%2BxjsaNVGm1jTgTlX46is206jB4tmuj5FJ97Hlg%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=cxC%2BR7r3P5J%2BVbrDvpfJGw%3D%3D.8O6oKs93UsmOgpWaGVnO2HCMOhw5iYiwZLbQkQ1z2fuJPzIYaZj%2Bx5dkXYc87Au8kixDN41NKh6LTD%2BIcwRWtb0Fo3VKNuVpGBcWGvTTRlhYsIk1v%2BHy%2FuYkk3xqMfMlipJifYHA14Fk5EtNFUmgwaGxncCs6uALS3Zvjb132V8g93ZL792YsALB7aHvQuJ1%2BAsDOCW%2BSbIvI8Rf317L2CGJgb%2Bj3DRvu4K2jL1PWbqMiK5tdXMWRJgJS5xLcTtaHqMWOeUIE%2BmpDOK7O8V5GHI3fbOn2uHBTlB%2B0XNoIhiL%2B4tYwPX9d43RSQ8v2GQ6dbBecLlGVYjS%2BDMazJbjFQ%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=ZjUqkjrcyWmeTUz%2BOk1Quw%3D%3D.3MkqLx5U1tSlx46%2B%2BJT5VxrQQN%2BbHamewzWi6ndJo20ZonMJ4bCIhMEU1ahxVzyBTDDS3m1tSCXn1F9d7U9z6DQRkI9W%2B3aoZMKO%2BZcXLAMnf0GuzC0W7v2lKU0stCe0qJ03%2B0y3HyNhRRRTCMjsYT%2BDOXU%2BV2JRg9FKH%2FLbVL8VM4USNkR5Kti7WrA1DOKUEC1ltu1VWmZoVnhKxfTYcGFrzmTnFPv6oiOCCHSlCjyjCDcxio64vP82FiBzmnJBJJYIOJa0fLi2NlaY1nojAdjBTVWcZG%2BAmkuDx9Ia7%2F35jnauEEU8ybSE%2BcHk0cmIh228jHQEeoqUFdgB6blxJA%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
React之渲染元素
https://segmentfault.com/a/1190000009492871
2017-05-20T21:52:36+08:00
2017-05-20T21:52:36+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote>欢迎访问 <a href="https://link.segmentfault.com/?enc=a17Eu51rAAz1u5zJ1%2FIYGw%3D%3D.cKUkk%2F7gYellgGaNE5m5sYWgsoHk6T0NOP6enDjNHsblEjAahieIoGhMLPKmW8HE%2FPCeH8u9aaWpB0cXx3vKd1KoAL7XpyndvJQY5i%2Fkqm5TibvO4pvd4sJEvZMhGfEo2zcaaeZrAzBILNlL3PdhADPsAtjZDJTjk3wCaOSK6gzoPPaTNSi%2F66z18ZouFFtb" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h2>开场白</h2><blockquote><p>接着上一节JSX的讲解后:我们大概清楚了以下几个事儿:</p><blockquote><ol><li>知道JSX是个什么东东</li><li>为什么React要推荐使用JSX</li><li>以及JSX的一些基本语法。</li></ol></blockquote><p>本篇文章谈一下React是怎么渲染元素的。</p></blockquote><h3>元素</h3><p>元素是React应用中的最小部件,正是由一个或多个元素构建出来了组件。<br>一个元素用于描述你将在屏幕上看到的内容,比如:</p><pre><code class="javascript">const element = <h1>Hello, world</h1>;</code></pre><h3>渲染元素到DOM</h3><h4>根DOM节点</h4><p>假设我们的HTML文件中有这样的一个<code><div></code>:</p><pre><code class="htmlbars"><div id="root"></div></code></pre><p>我们称这是<strong>一个根DOM节点,该节点内的所有内容都是有React DOM管理</strong></p><h4>注意</h4><ol><li>一个用React构建的应用程序通常只有一个根DOM节点。</li><li>但是如果把这些应用程序整合到现有的app当中去,那么该app中就可能会包含多个相互独立的<strong>根DOM节点</strong>。</li></ol><h3>更新已渲染的元素</h3><p>React元素是不可变的,一旦你创建了一个元素,就不能再修改其子元素或任何属性。<br>更新UI的唯一方法是创建一个新的元素,并将其传入到<code>ReactDOM.render()</code>方法。<br>来思考下时钟的例子,完整代码如下:</p><pre><code class="htmlbars"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>rendering-elements</title>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../build/browser.min.js"></script>
<script src="../build/jquery.min.js"></script>
<script type="text/babel">
function tick() {
const element = (
<div>
<h1>Hello,world!</h1>
<h2>It is {new Date().toLocaleTimeString()}</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
)
}
setInterval(tick,1000);
</script>
</head>
<body>
<div id="root"></div>
</body>
</html></code></pre><p>以上代码,每隔1秒,就会通过<code>setInterval()</code>回调<code>ReactDOM.render()</code>方法来重新渲染元素。</p><h4>注意:</h4><blockquote>实际上,大多数 React 应用只会调用 ReactDOM.render() 一次。在接下来的章节中,我们将学习如何将这些代码封装到有状态的组件中。</blockquote><h3>React 只更新必需要更新的部分</h3><p>React DOM 会将元素及其子元素与之前版本逐一对比, 并只对有必要更新的 DOM 进行更新, 以达到 DOM 所需的状态。<br>我们对 上一个例子 进行检查来验证这一点:<br><img src="/img/bVNZGU" alt="图片描述" title="图片描述"></p><p>从上图中我们可以看出,即使我们我们每隔 1 秒都重建了整个元素, 但实际上 React DOM 只更新了修改过的文本节点。<br>本文做为自己加强记忆之篇,均参考自:<br><a href="https://link.segmentfault.com/?enc=wW3Vp9%2BP6r44KPl0BP75mg%3D%3D.oKuP9XrtfkuLTqtAJYYNJBCS55fieHPcuYDLRpw8jAkS7HK%2BoFtVab4W36rLrykr%2Fd1H%2Ff98gYwTafjTv5gKwQ%3D%3D" rel="nofollow">http://www.css88.com/react/do...</a>,在此列出,大家共同学习。</p><h3>更多文章</h3><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=KZ0gdLihe6wY095%2F5W3t1Q%3D%3D.rVuCKJF4rJIShGGIlmlEJdM6NZwNyDSbXZJCCZ9HIODQ9rdKKQA5UcH7B8m8kItwbwWr6Q%2BzDOIoIYX9%2FWR%2FqAlfBSrxPwyTlmixJtovq2SrQyJG%2B7opB45HEQFxkOXEsN%2BBtnopiRiCDBeY5jYsndmPU1zZoNBBouHApp8OtGSJpvkaXwfbzsecoCII%2Fqo1JNC5WAQRuAvBZCVfQs0fFvRYWAVv3eQl5wnLpjBmSIi6zT2iPkyA0XoHlAysrB10Tuxsdjts9yRJCwlbVhhb4K29YH5vDNUFL7RbgXJHq%2BpMVtOOM2h0WSgaWFfwvDLjxr3zyJQXvzGokDx24jLOTA%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=b0jtLVC%2F36AWcKecWsu3tw%3D%3D.ysm4%2F6XxzJxD%2B9GtEF%2BDrT%2BLCDD7meAA9K9R0i0I3uFJ%2FYXzvEaXWxvyhkF9iiNFhJ5H7gSugVdzjDLzC9Z9NNoSbd4b2H9wFqlE5CAuOCorO9nb89kJvhSLBvc3cPTM77QkbYTmpoOw5U%2FoDPTcYt10OqOxIgqQ8B%2BI8F9LGzvy9aT6IZTl7sFHZutay9qL2mqVsu6OUORYoRGgfVHg319EZijRF5WoMoyxmeX3PpsvVt4x8f8TWdqjOmaPFgClXxarcDn5j4ykB5KtoZuaoJ6DYeT5ij%2B6KhJyj2j5POXkVlUqKiPdmBfQ%2FHYpJil4EfPJcrFQeD6YoaZwdP2IGA%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=jvVE2vZBcDGkZupE7KXTTQ%3D%3D.PmpiEG9kGbeuihts8OTV1GkjZDDkA%2Boa9lMqo%2F58eUA%2FeLbgIM9kLa%2BroJIBJe5dnYabE8G149ZHhL47HiBxRWbsnozXzbTMEYHFuALrH3E9Y3p%2BmeBpIuhVlAYVi5c%2BEzy8K0RlD1hmVLoXBDgZDTP39sCV93RRarC1HB07Jd5ybN0DucUt3sysH%2BV4%2B4n65ei28Aa%2BoVJFYyDs3MFLxj7wk6vl8fv%2F%2FsVpHxAJxMAB2j9vmHfX14GE6qkhgF9WVqKSSCKMRYUKX%2BNnLU5%2FlRXuVKb%2FNfcDtpK%2BzcYXRwu52hu4vmxzfSIwK62i7YLOeZEYxu2UDDSwQrAt7%2FGkpw%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=7YXSq9eBiyc11M7kS72ZoA%3D%3D.YCbh2r0aHCPQYLOhhMVSLydE%2FYd%2BGEXaLv9jmNiI%2FkdV%2BFi%2FM0UJU7e8njbQ7IvIaBrRISKkgZYa%2Bh8QPxQ3vdkRoNRgfVRL2f4ux005snWX945D1Y5qYGHFfsKEjRy9RtfNbQ5z0DoB3nvujwl4Z4ijkTobthH3dSknkS3HkIG3XmwFqUdhPneMhxgEF1%2F6b7e1zwsU5ZEPGEMKyGfRoSL3yIx%2F9dcaq%2BaG%2F0x7wzS%2FhxSCICzgFPamzJ4sMDXNjHAY3E7mhWVbFm8i7kX8Q9xupZV8gOIN9vu5anlYP71riggcZ3aBjTRzK3HHXKa4%2BlO88N8P9ZrZAcmFeppEkw%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><h3>欢迎大家关注微信公众号</h3><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
说说对JSX的认识
https://segmentfault.com/a/1190000009317820
2017-05-06T22:36:00+08:00
2017-05-06T22:36:00+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<blockquote>欢迎访问 <a href="https://link.segmentfault.com/?enc=uVl6RgB%2Ft0nWIuybmO62zA%3D%3D.qL16NYVfr5kVvFN8XIlBRwnqVo%2FvpTDs1SGotWCVRQ%2BMhvyZpf%2BuXn9ziTy3fMOvMnd7V3ZGwDMCWbcQPxP2y9CizGT1JRGJCPunK%2FUNjzFbUM%2BDwxrbnNai9PsFGOE0RQbkNwPO0L43or1ZVTE3Gm2Aro34%2FNWB6JVudcrwFSQvPbS%2BRzy0GffvQiOcHa8z" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h2>引子</h2><blockquote>最近几个月做的一个项目,使用了react技术体系,自然而然的用到了JSX。下面就总结一下自己对JSX的认识。</blockquote><h2>什么是JSX</h2><blockquote><ol><li>即JavaScript XML,一种在React组建内部构建标签的类XML语法。(增强React程序组件的可读性)</li><li>JSX可以看作JavaScript的拓展,看起来有点像XML。使用React,可以进行JSX语法到JavaScript的转换。</li></ol></blockquote><p>下面我们来看一下一个简单的例子。<br>考虑一下这个变量的声明:</p><pre><code class="jsx">const element = <h1>Hello, world!</h1>;</code></pre><p>这个标签语法既不是字符串也不是HTML,这就是<strong>JSX</strong>。它是JavaScript的一种扩展语法。</p><h2>JSX小例子</h2><p>我们先从<a href="https://link.segmentfault.com/?enc=UWtZtmNGpiVv9rxgRFj%2Brw%3D%3D.%2FhXeN2L34JVBkGYtyRCTZ71Ff7td2BObuX%2FYDWcT%2FZJ6gGBGRXen%2FkjgnqpGDnojmrSwGnDfThgeYE90lXL2kg%3D%3D" rel="nofollow">官网</a>的一个最简单的例子说起,为了让大家能够直接在本地运行,我贴出了完整的代码如下:</p><pre><code class="htmlbars"><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
const element = <h1>Hello, world!</h1>;
ReactDOM.render(
element,
document.getElementById('example')
);
</script>
</body>
</html></code></pre><p>大家可以直接粘贴上面代码,保存在本地的一个test.html文件里,双击打开后,在浏览器里输出:</p><p>Hello, world!</p><p>我们看到</p><pre><code class="jsx">const element = <h1>Hello, world!</h1>;</code></pre><p>element变量的声明就是用了JSX语法,HTML语言直接写在JavaScript语言之中,不加任何引号。</p><p><strong>注意:</strong></p><ol><li><code>script</code> 标签的 <code>type</code> 属性为 <code>text/babel</code>,这是React 独有的 JSX 语法,跟 JavaScript 不兼容。凡是在页面中直接使用 JSX 的地方,都要加上 <code>type="text/babel"</code>。</li><li>一共用了三个库: react.js 、react-dom.js 和 browser.min.js ,它们必须首先加载。其中,react.js 是 React 的核心库,react-dom.js 是提供与 DOM 相关的功能, browser.min.js的作用是将 JSX 语法转为 JavaScript 语法。</li></ol><h2>js构造dom</h2><p>比如要创建一个dom超链接:</p><pre><code class="htmlbars"><a class="link" href="https://github.com/facebook/react">React<a></code></pre><p>我们在原生DOM中,用js构造dom的方式是这样的:</p><pre><code class="javascript">var a = document.createElement('a')
a.setAttribute('class', 'link')
a.setAttribute('href', 'https://github.com/facebook/react')
a.appendChild(document.createTextNode('React'))</code></pre><p>这个代码应该是大家比较熟悉的。当你在写代码的时候会不会感觉很繁琐呢,我们可以封装一下:</p><pre><code class="javascript">//第一个参数为node名
//第二个参数为一个对象,dom属性与事件都以键值对的形式书写
//第三个到第n个为子node,它们将按参数顺序出现,
//在这个例子中只有一个子元素,而且也是文本元素,所以可以直接书写,否则还得React.createElement一下
var a = React.createElement('a', {
className: 'link',
href: 'https://github.com/facebook/react'
}, 'React')</code></pre><p>看完这个代码,是不是感觉一下子要简洁的多。<br>现在有个编译工具,可以让你用html语法来写React.createElement,部署上线前编译回来。你愿意吗?<br><strong>不管你的答案是什么,但这就是jsx的一半真相。</strong></p><h3>来看个直接的对比</h3><p>前面已经回答过,在使用React的时候,可以不使用JSX,大概这样写:</p><pre><code class="javascript">var child1 = React.createElement('li', null, 'First Text Content');
var child2 = React.createElement('li', null, 'Second Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child1, child2);</code></pre><p>使用这样的机制,我们完全可以用JavaScript构建完整的界面DOM树,正如我们可以用JavaScript创建真实DOM。但这样的代码可读性并不好,于是React发明了<code>JSX</code>,<strong>利用我们熟悉的HTML语法来创建虚拟DOM:</strong></p><pre><code class="javascript">var root =(
<ul className="my-list">
<li>First Text Content</li>
<li>Second Text Content</li>
</ul>
);</code></pre><p><strong>总结</strong></p><ol><li>这两段代码是完全等价的,后者将XML语法直接加入到JavaScript代码中,让你能够高效的通过代码而不是模板来定义界面。</li><li>之后JSX通过翻译器转换到纯JavaScript再由浏览器执行。</li></ol><p><strong>注意</strong></p><blockquote>在实际开发中,JSX在产品打包阶段都已经编译成纯JavaScript,JSX的语法不会带来任何性能影响。<br>另外,由于JSX只是一种语法,因此JavaScript的关键字class, for等也不能出现在XML中,而要如例子中所示,使用className, htmlFor代替,这和原生DOM在JavaScript中的创建也是一致的。</blockquote><p>相信大家在看完了上面的这些举例后,心中的疑问自然而然就迎刃而解了。</p><p><strong>因此,JSX本身并不是什么高深的技术,可以说只是一个比较高级但很直观的语法糖。它非常有用,却不是一个必需品,没有JSX的React也可以正常工作:只要你乐意用JavaScript代码去创建这些虚拟DOM元素。</strong></p><h2>为什么使用JSX</h2><h3>抛出疑问</h3><p>看了上面的这些简单的demo,大家肯定会抛出这样的疑问:</p><ol><li>为什么React官方推荐使用JSX呢?</li><li>等等。。。</li></ol><p><strong>使用React,不一定非要使用JSX语法,可以使用原生的JS进行开发。</strong><br>但是React作者强烈建议我们使用JSX,因为:</p><ol><li>JSX在定义类似HTML这种树形结构时,十分的简单明了。</li><li>简明的代码结构更利于开发和维护。</li><li>XML有着开闭标签,在构建复杂的树形结构时,比函数调用和对象字面量更易读。</li></ol><p><strong>可能说这些你会感觉比较模糊,下面来举几个看得见的例子。</strong></p><p>前端界面的最基本功能在于展现数据,为此大多数框架都使用了模板引擎,</p><h3>在AngularJS中:</h3><pre><code class="javascript"><div ng-if="person != null">
Welcome back, <b>{{person.firstName}} {{person.lastName}}</b>!
</div>
<div ng-if="person == null">
Please log in.
</div></code></pre><h3>在EmberJS中:</h3><pre><code class="javascript">{{#if person}}
Welcome back, <b>{{person.firstName}} {{person.lastName}}</b>!
{{else}}
Please log in.
{{/if}}</code></pre><h2>总结</h2><ol><li>模板可以直观的定义UI来展现Model中的数据,你不必手动的去拼出一个很长的HTML字符串,几乎每种框架都有自己的模板引擎。</li><li>传统MVC框架强调界面展示逻辑和业务逻辑的分离,因此为了应对复杂的展示逻辑需求,这些模板引擎几乎都不可避免的需要发展成一门独立的语言。</li><li>如上面代码所示,每个框架都有自己的模板语言语法。而这无疑增加了框架的门槛和复杂度。</li></ol><p><strong>使用JSX</strong><br>正因为如此,React直接放弃了模板而发明了JSX。看上去很像模板语言,但其本质是通过代码来构建界面,<strong>这使得我们不再需要掌握一门新的语言就可以直观的去定义用户界面:掌握了JavaScript就已经掌握了JSX。</strong></p><p>这里不妨再引用之前文章举过的例子,在展示一个列表时,模板语言通常提供名为Repeat的语法,例如在Angular中:</p><pre><code class="javascript"><ul class="unstyled">
<li ng-repeat="todo in todoList.todos">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul></code></pre><p>而使用JSX,则代码如下:</p><pre><code class="javascript">var lis = this.todoList.todos.map(function (todo) {
return (
<li>
<input type="checkbox" checked={todo.done}>
<span className={'done-' + todo.done}>{todo.text}</span>
</li>
);
});
var ul = (
<ul class="unstyled">
{lis}
</ul>
);</code></pre><p>可以看到,JSX完美利用了JavaScript自带的语法和特性,我们只要记住HTML只是代码创建DOM的一种语法形式,就很容易理解JSX。<br>而这种使用代码构建界面的方式,完全消除了业务逻辑和界面元素之间的隔阂,让代码更加直观和易于维护。</p><h2>JSX的语法</h2><p>你可以用 花括号 把任意的 JavaScript 表达式 嵌入到 JSX 中。<br>例如,2 + 2, user.firstName, 和 formatName(user),这些都是可用的表达式。</p><pre><code class="javascript">function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
ReactDOM.render(
element,
document.getElementById('root')
);</code></pre><h3>JSX 也是一个表达式</h3><p>编译之后,JSX 表达式就变成了常规的 JavaScript 对象。</p><p>这意味着你可以在 if 语句或者是 for 循环中使用 JSX,用它给变量赋值,当做参数接收,或者作为函数的返回值。</p><pre><code class="javascript">function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}</code></pre><h3>用 JSX 指定属性值</h3><p>您可以使用双引号来指定字符串字面量作为属性值:</p><pre><code class="javascript">const element = <div tabIndex="0"></div>;</code></pre><p>您也可以用花括号嵌入一个 JavaScript 表达式作为属性值:</p><pre><code class="javascript">const element = <img src={user.avatarUrl}></img>;</code></pre><p><strong>注意</strong></p><blockquote>在属性中嵌入 JavaScript 表达式时,不要使用引号来包裹大括号。否则,JSX 将该属性视为字符串字面量而不是表达式。<br>对于字符串值你应该使用引号,对于表达式你应该使用大括号,但两者不能同时用于同一属性。</blockquote><h3>用 JSX 指定子元素</h3><p>如果是空标签,您应该像 XML 一样,使用 />立即闭合它:</p><pre><code class="javascript">const element = <img src={user.avatarUrl} />;</code></pre><p>JSX 标签可能包含子元素:</p><pre><code class="javascript">const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);</code></pre><h3>JSX 防止注入攻击</h3><p>在JSX中嵌入用户输入是安全的:</p><pre><code class="javascript">const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;</code></pre><p>默认情况下, 在渲染之前, React DOM 会格式化(escapes) JSX中的所有值。<br>从而保证用户无法注入任何应用之外的代码。<br>在被渲染之前,所有的数据都被转义成为了字符串处理。 以避免 XSS(跨站脚本) 攻击。</p><h3>JSX 表示对象</h3><p>Babel 将JSX编译成 React.createElement() 调用。</p><p>下面的两个例子是是完全相同的:</p><pre><code class="javascript">const element = (
<h1 className="greeting">
Hello, world!
</h1>
);</code></pre><pre><code class="javascript">const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);</code></pre><p>React.createElement() 会执行一些检查来帮助你编写没有bug的代码,但基本上它会创建一个如下所示的对象:</p><pre><code class="javascript">// 注意: 这是简化的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
};</code></pre><h2>结尾</h2><p>关于JSX的介绍大概讲到这里,看完这篇文章后,希望大家能能够了解什么是JSX,React为什么推荐使用JSX等问题。<br>在下一节中来探索如何将 React 元素渲染到 DOM 上。<br>欢迎大家访问我的<a href="https://link.segmentfault.com/?enc=J7HnaqrGdjIIbwxDpi7JgQ%3D%3D.fjJ2xXSoqXYCYcQgRSs1pl9Ye2434aKRTmti2QaFFw0%3D" rel="nofollow">blog</a>,有更精彩的文章吆!<br><strong>参考链接</strong></p><ol><li><a href="https://link.segmentfault.com/?enc=OFvNWy%2ByRHi1BOqsgZUlxw%3D%3D.Sf%2B2bSDgMxg96DbD6PxQEaXfWmIJTwyOxzlrt0Hd6UCzcVOLWMGND35T7OnNflIyJVib7xeuhtFcjwZJZS6KUQ%3D%3D" rel="nofollow">https://facebook.github.io/re...</a></li><li><a href="https://link.segmentfault.com/?enc=mMAhWKWLY6T0zwXAXbGL%2FQ%3D%3D.zhmMZX2ICRW0UL55UqUiFtyVOOg%2FFUTj%2BDYTmcgRRxyh6GPqu7UKNtrs8RoX58Pe1xLUI%2B6%2FDK0d0D05JplVlA%3D%3D" rel="nofollow">http://www.css88.com/react/do...</a></li><li><a href="https://link.segmentfault.com/?enc=p23aaiZos%2BZ29N07ApZUVg%3D%3D.dhFYoUHvu9SIoG5UIJwrNf42eJwVOh11ODE5c%2BG%2Fm7Gmh%2B0fGR1HDzZ2nNwn5HHK%2BPDiKUPmby2PE6kaZcQwSg%3D%3D" rel="nofollow">http://www.infoq.com/cn/artic...</a></li></ol><h2>更多文章</h2><hr><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=BwR60hkzyY%2FtQdr0%2BfxUMQ%3D%3D.HNWyVyrq4CQ8Lmfk94UDh7iIhsor1xXK0%2BzPuASq9zey6Y%2FpsgpGcewwzvtMpX0G3YOdej9%2B%2Frr16JqDzEm7Ep8N6BPpzSQ9dRT6QTuDZZi62W%2B2uWpUN%2BW0GUnfxZZ6KrCXXouGegbFEAJ4rZ4nqS3SsIZjfXlVnjRc1osK6YKmUxKv%2B2HgJBNk2b6nwD5zAnfMECbm5WvZYkyxGi8lPb0f987TQ951mqDHJyAcQjMFhESigDX2bWsF6oYToMVfOSdoRdTAgBAnrj4se5CZYRwKQ2gfLshT1zbUhH7tGggoR0z59wp%2F0%2F2gWBvurH6sndAdOjKcpCEKoyFUdRLsWg%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=sjYu29kgZKjGuxp29HeP6A%3D%3D.E3JirGUYSKOXfc6O8xU18%2BImnSmpZTe5MwnD8VmNNzUzgEvdrHR%2B8F3TVJ6USFGfjF%2BddzGHDctck7jwvtL%2BnDYmwzGAE4kIwcnhlfqu2IdQSC%2FdQtUFn%2FbOxR%2F0sWR3eZ49F5Zc1rOGIbuddF6pFFUMqdGyEzVkxdGrQe7Mt%2Fo9eukMvzpE1ZegfzfYSupwoVR7eXciAsDWiDdbgQBUmUosu0F77RJsKvCzI1A15FNB2OLR00MI0D80GByzCIgxrwrj1D4jPovK6wtXGHMX6UYZaN6lRqxmioxFDdl%2BNu5mjZDk5sKw4UE05j%2FVf9%2BtYhamOQ0g59%2FFU7agW45g3Q%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=rXoZGHda9nRhL%2Bmtc1wCaQ%3D%3D.ogTcpKzpoPDfoZ0n%2BR3MyTBGM2eOXcZOA7eqKS6Gtum9y8yFQDSE3cRUidN9bkBE%2FBf%2FLntjja5eCZoRJdNk5bbU3IIV6BznbA9joxftSabfqF4iDPa3%2FtDQXJ3wi3aa%2BVXm4%2F9cm2DsWuFWYWOvw6LDIWkEtCnb2iRvHQ8XXnMhokolE15ko7cZOY3eJeFXVzi7AOy0uLzH0YzchS3Qbz9hw1WDMeaHWkqXw4r6ULfcnmuNu0qHJQQh0CVtK3UDIZAU7bjSj6lL%2Bj3hRuNbJ8px%2F7DypGk%2F3c5UNW3rlpJv5liJtadWJ526z4dSvkxdli9Epoy4suEJ5Sl6maq3%2Bw%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=B85jzT7fEriJI9JLntkjbw%3D%3D.YDcMhn%2BBNZYBiPLhzI6tiR%2FcqJ7yfzBTa986C0%2FC%2Fcil3%2Fxfu6b9QcvOujJcBNBnQ%2BoR32eiLi9wHyL4N%2FuLv4DHQ14%2FRKClRQV7fDey3BVBiJ%2FcT3Ipz2QHG%2B25FW%2Bk6eSARGuOM3AUoK1ExTApFvFcvcuAmUmDFPtxlvBYHSDecf40gtmpHIUHeVpeuPsTQniMnmOLS%2BFxkkLksXuc4RAdotOmta6iufNSGye95UkvRE0wGQcRtijLn%2FbfTi5ZbtbqUaFd2AsHKKnqIv%2F05XehqWrYze2Y9aPlGqHLUEdy5jXw5I5uFgQujoh21KmrF%2FQEuER%2FPySTfdeFG7vz5g%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><h2>欢迎大家关注微信公众号</h2><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
手把手教你开发nodejs微博网站-连接数据库
https://segmentfault.com/a/1190000008265319
2017-02-06T16:44:45+08:00
2017-02-06T16:44:45+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
0
<blockquote>欢迎访问 <a href="https://link.segmentfault.com/?enc=kws%2FEdC4bS6mfZaYjDmTrQ%3D%3D.wGQpuwobWaAStyNSEsbSgswKMrIxXdKOdds0FBYcHOFXIGq3l4Q95393x7fsBpXpEWT%2BmlXQVEmLX0x%2BTdgY3J04%2FbdVK%2F6bHHEscKew1I5LcMjszpxjFdoIAfyUvPEwFqgFccKsjsEGbB4wRWfj4tGqGKCRMOyc3ZiSnl%2Fx2ATRQfa4mR7UIWTgFx75C%2Bm2" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h4>引子</h4><blockquote>博客肯定是以用户为中心,包括用户的注册,用户的登录,用户发表留言,对留言进行评价等等功能,所以,自然离不开数据库。</blockquote><h4>MongoDB</h4><h5>MongoDB简介</h5><ol><li>MongoDB是一个开源的NoSql数据库,相比mysql那样的关系型数据库,它更为轻巧,灵活,非常适合在数据规模很大,事务性不强的场合下使用</li><li>MongoDB将数据存储为一个文档,数据结构由键值对(key=>value)组成。字段值可以包含其他文档,数组及文档数组</li><li>相对于mysql这类需要把对象属性转换成SQL语句才能保存下来 ,MongoDB这些可以直接保存JS对象成数据库的文档,来看一个MongoDB文档的示例:</li></ol><pre><code class="bash"> {
"_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ),
name : 'phping',
age : 28,
hobby : ['movies','music','nba']
}</code></pre><p>可以看到数据格式为json,因此与javascript的亲和性很强,我们的项目也是使用MongoDB。</p><h5>MongoD概念解析</h5><p>在<code>mongodb</code>中基本的概念是文档、集合、数据库。下表将帮助您更容易理解Mongo中的一些概念:</p><table><thead><tr><th align="left">SQL术语/概念</th><th align="right">MongoDB术语/概念</th><th align="center">解释/说明</th></tr></thead><tbody><tr><td align="left">database</td><td align="right">database</td><td align="center">数据库</td></tr><tr><td align="left">table</td><td align="right">collection</td><td align="center">数据库表/集合</td></tr><tr><td align="left">row</td><td align="right">document</td><td align="center">数据库记录集合/文档</td></tr><tr><td align="left">column</td><td align="right">field</td><td align="center">数据字段/域</td></tr><tr><td align="left">index</td><td align="right">index</td><td align="center">索引</td></tr><tr><td align="left">table joins</td><td align="right"> </td><td align="center">表连接,mongodb不支持</td></tr><tr><td align="left">primary key</td><td align="right">primary key</td><td align="center">主键,MongoDB自动将_id字段设置为主键</td></tr></tbody></table><p>通过下图实例我们也能更好的了解Mongo中的一些概念:<br><img src="/img/bVIQE8" alt="图片描述" title="图片描述"></p><h5>MongoDB安装</h5><p>大家可以针对自己的系统,参考下面的链接向导来进行安装,步骤都是非常的详细。这里不再赘述。</p><ul><li>Windows 用户向导:<a href="https://link.segmentfault.com/?enc=UweIphy72DvPs5vg4RrYJg%3D%3D.grLajHUrIUKCeYHfZpHPMtVOHPyGKj%2BwHyKW72%2B6O1XML%2Bop%2F6NzUbSAfRs8iO0EFCz41uKfmPGRlR5bFw0k3KCa7H2LXPpRYIsEc%2F3Fy14%3D" rel="nofollow">https://docs.mongodb.com/manu...</a></li><li>Linux 用户向导:<a href="https://link.segmentfault.com/?enc=B9K8SwsWA5%2Fij1XdCzgyhw%3D%3D.XIZMSJtphBTk6khVtkVT0PtiVBdgoaRse49hRAnc5R85UZL8RFUkg3neCqI1QXitf5TO6aCqB5gB8bZ8w6wDpCl8DRxkSrrn9xXLTTclGDQ%3D" rel="nofollow">https://docs.mongodb.com/manu...</a></li><li>Mac 用户向导:<a href="https://link.segmentfault.com/?enc=TpBGhXS%2FLGCLrn8ZT%2BzQNQ%3D%3D.Y%2FaOAAiS8NNVlDfHU7kMc5fz0pcelJpiPPJpkcGyS2%2Fht5G0vMl8nutMc2Iys7WwuW1lr9SkBWPcoBNwve7AI8NudQu2V2EMOu%2FIre265VU%3D" rel="nofollow">https://docs.mongodb.com/manu...</a></li></ul><h5>Robomongo</h5><p>我使用的MongoDB 可视化管理工具是Robomongo,当然还有其它的可以使用,如:MongoChef等。</p><ul><li>Robomongo</li></ul><p>Robomongo 是一个基于 Shell 的跨平台开源 MongoDB 管理工具。嵌入了 JavaScript 引擎和 MongoDB mogo 。只要你会使用 mongo shell ,你就会使用 Robomongo。提供语法高亮、自动完成、差别视图等。</p><p><a href="https://link.segmentfault.com/?enc=dM7%2Bd2JWObM2Ul19WoGcVg%3D%3D.R3dejW00vNrhGh%2FocIyicFYKUNvNKtYlHY41F7u960s%3D" rel="nofollow">点击这里下载</a></p><ul><li>创建连接</li></ul><p>下载并安装成功后点击左上角的 Create 来创建一个连接,给该连接起个名字如: localhost,使用默认地址(localhost)和端口(27017)即可,点击 Save 保存,如下图:</p><p><img src="/img/bVIQlD" alt="图片描述" title="图片描述"></p><ul><li>添加数据</li></ul><p>前面已经简单的介绍了mongodb中的一些概念,让我们使用Robomongo这个图形化管理工具来添加数据:</p><ul><li>点插入文档:</li></ul><p><img src="/img/bVIQFb" alt="图片描述" title="图片描述"></p><ul><li>会打开一个空白面板,让我们手动写入要添加的数据:</li></ul><p><img src="/img/bVIQFg" alt="图片描述" title="图片描述"></p><ul><li>写一个简单的测试数据如下:</li></ul><p><img src="/img/bVIQFh" alt="图片描述" title="图片描述"><br> 其中左下角的按钮可以用来验证我们写入的数据格式,数据格式无误后点击右下角的保存按钮即可。</p><ul><li>查看新增加的文档:<br> <img src="/img/bVIQFj" alt="图片描述" title="图片描述"></li><li>这时,在你右边区域就可以看到我们刚才新增加的数据了:<br><img src="/img/bVIQFn" alt="图片描述" title="图片描述"></li></ul><h4>使用Mongolass连接数据库</h4><p>好了,MongoDB安装成功了,我们来使用<a href="https://link.segmentfault.com/?enc=3jXX4SusSq8E2AiVYQ5A0Q%3D%3D.Owp2eimRoIs4b57QahjoeIclKynOdhdiTa8l0Pb6uaV4dDSsQzkQBUUffmHvRwqK" rel="nofollow">Mongolass</a>连接数据库。</p><h5>安装Mongolass</h5><pre><code class="bash">$ npm install mongoose</code></pre><h5>连接MongoDB</h5><p>首先,我们需要定义一个连接。如果您的应用程序只使用一个数据库,您应该使用<code>mongoose.connect</code>;<br>如果您需要创建额外的连接,使用<code>mongoose.createConnection</code>。</p><p>这两种方式连接都需要<code>mongodb:// URI</code>,或者主机名字,数据库名字,端口号,配置项等。</p><pre><code class="bash">var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/my_database');</code></pre><h5>定义一个模型</h5><pre><code class="bash">var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/my_database');
var Cat = mongoose.model('Cat', { name: String });</code></pre><h5>访问一个模型</h5><pre><code class="bash">var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/my_database');
var Cat = mongoose.model('Cat', { name: String });
var kitty = new Cat({ name: 'Zildjian' });
kitty.save(function (err) {
if (err) {
console.log(err);
} else {
console.log('meow');
}
});</code></pre><p>更多使用可以查看<a href="https://link.segmentfault.com/?enc=9m31e5FC7JYAzhRsVQzYRg%3D%3D.ffZvFl2dgJuFuV4s4rOzOoh5Jjmj1OO2poIXTdNNgpk%3D" rel="nofollow">官网文档</a>.</p><h4>总结</h4><p>本篇主要讲解了我在开发本项目时使用的什么数据库,怎么连接数据库,<strong>下节干货就来了:用户注册功能开发</strong>,敬请期待!<br>工程代码可从github上下载:<a href="https://link.segmentfault.com/?enc=wrhp%2F8KxTtNRmJWd7W32AA%3D%3D.c2KQJdl%2FtXLlEh1u4brU00Xrysx8A50xcvGo86x6KejM%2BX3Lnq%2BPWDH5VgWNqaU4" rel="nofollow">https://github.com/liuyongfei...</a></p><h3>欢迎大家关注微信公众号阅读更多文章</h3><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=RNkg4t%2BF0BVfkp4%2FbpEpLQ%3D%3D.UOlfxyPI%2BId2l8wX68MZmpR%2BJWw6DYnYwHZ%2BSur%2BwwNl3wy2czWBxYSDhY7C2hkvavFmnD6Qdb7fsHHDB13y4d6HCuTvDzs5wjHqoqRfnBS12knqrpfXG9P%2BHb83TpYKAYDnqf44KPGqGpLjl9BF3BFU554e0auYZklMhy6Ak8ydnLXqTyVpWKvromPNljhXH7lJaCT1huIhZnBy2RvFOHJIniSY891w2MR9bUW5xdqxhfqpqBX5SdzzqDJdBTBdFXCZYrw7GbFFTWXF2A2uf7Fh%2BR0EqzjNjxglp0lNQs%2FjAkSiH1LfAcdqhNgbAfzNaW1%2FdoyrecMsequDbvGcBg%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=XAjYlwomzQeQVGUOnzXUlw%3D%3D.QQdEVaYoLuZ%2Fph7N2L6nGxmAi8lvp%2FvCjp5STsSR%2Ff0V1oLsKieDecls1MnRMtcgfYWju4RgGBzNr5EEsyTUt%2F9E%2Fw9SRjq1hxrZpxJz26H7esOuUOs6ZU9pHD8vXEyqITMSt7ZSClzM3EJZz%2BWuwpFwjcBa1guzfNHmSs1dUj90lQoYrZl0wBh5mrMwVnEFCQJuTT7SAxKcVe73%2Ffh1rwxP9QhxrtMaU0K9jTG7B4GL51cmIwIyaGcfn8JFiR2fRjYioUotKSUQvngYF%2FYjd6IpRD02CzGcCU1jAgrLvsn7%2BqQLPBOpZRd%2FZS5Lz9m1Ajt3uz9BzHSHWFi3VDefEw%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=DalDix0eTauj2vlMoG8udA%3D%3D.WdJV5NVyFoBzhP1iJ%2BCvrlxtVASh%2BUw7QYFaEvs2%2BAAIx1IKB8yw47H9J2TmB%2FNAnbnxCGtxbKwi6D8KC8dkCQRTk%2F7w55YuDs4QhJkT1jAWkjMvw%2BAyI6Ub3OHqeuCsgOH96tjJQymSfKg1VKT3HZPz0LpjAnrK6gPtahoQesAG3viQSI11tL9MR2qRK2bldXZmfPmp5gG0rVJ3dLE1HZKR%2BK4Arl9VHqQ5jcTZYCxq84yis2Ho5OS9ocYAkpoyYPS2JdqG4ZtiPcN8mTQBcHBt%2BgiI%2FVpbWAUR%2BcqPgdm66yh8FEmwRpsQMMQ3v0Bq5QClMmEAKjkM5YYBG5gv6g%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=1Fq3OsOg3kvxpcL54Fow5g%3D%3D.dhG4%2F1Ly547rWsa4SBtIVuR%2F0mTEMPpu8wrD2fAtq383WLxkAXPeiLpShW%2BeHKUwryMsVDn3qQxLDGaatDTf1unA631qKhQpDNxrvxde3fYEX86HZqICYio4SiA8UyGLg90lY7StFaGJrr0kzfnISN4d6RZb4cGeEroP0JtEvMAoSpy1HXomY25xxkOt41VNNdoO%2FO7%2F4FKW0LySsO4wPPuxSHwIbDX9UfEe5lTq790xd2HQJ58FZoaUtV6YnGkKuQQhRcxy5VSOoHFxW9ieUxQucIM7h%2BFrIuun63JEEv2k%2F5ZsnCBsDUWNMvVUxWyKAgtGUUdAi2a7PivbghEVRQ%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
手把手教你开发nodejs微博网站-开站篇
https://segmentfault.com/a/1190000007981838
2017-01-01T16:21:06+08:00
2017-01-01T16:21:06+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
2
<blockquote>欢迎访问 <a href="https://link.segmentfault.com/?enc=plSN3rzGHQaZTNfDn%2Fw5Aw%3D%3D.wMcpLZponYYjexXz0SSVyEQqCYkd1a8Zi6f%2FwaqRsfQvpTdR7s2PCeeDzIzRKFPFQ35K%2FdYOrq1KBu%2B4Qr7KG71f68Uj8sSxJH51NkydYui1Voqwwyywn4yoq1c8SiOXnQtUlWUVhcaI3iOARqYc4nXQcUF7FvhVZG%2Ft%2FkoyPgBTtnSKoiP3PQM3kofTVLfn" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h4>引子</h4><blockquote>本项目参考Nodejs开发指南一书的第5章,受限于书中的nodejs和express版本太低,相当一部分代码在新版本的nodejs和express下都是无法使用,因此自己通过nodejs社区和参考网上的一些问题解决方式,最终完成了一个社交微博网站的雏形。</blockquote><h4>一.项目功能</h4><p>微博网站以用户为中心,包括:</p><ol><li>用户注册</li><li>用户登录</li><li>用户发表信息</li><li>显示用户发表的信息</li><li>权限控制</li><li>其它功能</li></ol><p>由于要保存用户信息,这里要牵涉到数据库连接,我使用的是<code>Mongodb</code>。<br>本人也是<code>nodejs</code>的初学者,暂时先完成了着一些基本的功能。<br>还有其它的功能点,比如:对微博的评论,转发等功能会在后面的学习中一步一步完善。</p><h4>二.开发环境</h4><p>nodejs社区的版本更新比较频繁。列一下我的开发环境:</p><ol><li>操作系统: OSX Yosemite 10.10.5</li><li>nodejs : v5.1.0</li><li>express : 4.14.0</li><li>MongoDB : 3.2.7</li></ol><p>没有安装express的可以点我的<a href="https://link.segmentfault.com/?enc=kcqwKWqenDJB%2FHcuPZbhiw%3D%3D.FJa8EppVwk8lJhqZ%2FetWLxJj1YqzaX6ZjDItc2fsVZGW5DlmWGwDPovYZysneh9bWK0gmuTsNOYMuAaMb5dPxA%3D%3D" rel="nofollow">另一篇文章</a>,有详细的介绍。</p><h4>三.项目截图:</h4><h5>用户注册</h5><p><img src="/img/bVHEBc" alt="clipboard.png" title="clipboard.png"></p><h5>用户登录</h5><p><img src="/img/bVHEBd" alt="clipboard.png" title="clipboard.png"></p><h5>用户发表信息</h5><p><img src="/img/bVHEBe" alt="clipboard.png" title="clipboard.png"></p><h5>显示用户发表的信息</h5><p><img src="/img/bVHEBg" alt="clipboard.png" title="clipboard.png"></p><h4>四.项目下载地址</h4><p>托管在github上:<a href="https://link.segmentfault.com/?enc=IxfXpCr%2F75G0cysN5vdRVQ%3D%3D.8PDNFL9p3D%2Ba3QuWHPq15%2FQkymwhgnu7XDV9wz%2FWEJRS09OZTXbphtyJbi%2BpKfF9" rel="nofollow">点这里</a><br>大概情况就是这样,下一节开始详细讲解 <strong>用户注册模块</strong>,敬请大家关注。<br>今天是2016年的最后一天,祝大家新年快乐,也希望我能把写博客的习惯带到2017年,给保留下去。<br>我会在后面分篇陆续讲解每一个模块的实现,大家有什么情况可以给我留言。</p><h4>关注微信公众号阅读更多文章</h4><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=mKIVl4YJnlYwU1eKlOxdwQ%3D%3D.ujIz51jIFpIpU6JnTM8j2g1hS0hQBb71RVi%2FeKy47n%2BE0fBQhLtBb1NCBwK%2Fw7JlZ4TZJdqQHwgLNwmDXjpOdmMERhaw9mmf9EMCY2k6uQ6DVHGyI7lZ18iWQe%2Fzt5uaMaj1v0WzR06EGj9Ece6JfyUjfMlr%2FGJABX%2BmFTHsD3bXUEkU2ni4HSZpY5kZJBdkfpdV%2Bwn6EW84uAIC1%2Bg6SGNR0WVxGfR0r4GJgmuo1cxkp5B14G9yDk6hew3Ik2dGYLMKohQ4kN%2FxKz4V7b1lu7egtKEkOayPmRHZH8ZIbdD1WCFYgBwgslWISDz1QEFThsBRrD3K%2B7vyHyn4zUy1Sg%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=PGrKtGRAb1IcAE5mRjbeXQ%3D%3D.ZuOWB%2F1lulZWmAfx30rtmU53mhtp6Y%2FBQrizX2bhDJ0K9HLoGxpXrBd0HxpTbceymlAFovUMacxi4zrOf%2FxGYQ1X3AUHDWu1knGUM6pyRoGVrXtQ6FCOtp51I%2BoZ8zrvlqnaXwYd7rKzUa08pSd0uK3CZAtj1c8WOJnPuH8pFupXXRLq%2FUdbP1VaS2RwldVsnNd57QU02erDihI6g8s5TlniW9vjN4bynJywnj2nL8u80%2BBUFEeAibbNRibrLnFczqvstIKWWxUoRbLHCyAcvZ0bNf5FgDQE5nigMer5iCk6NCYQF%2FizemKug1xs3PRonLnNzQU3iSrExwm5MFrcWw%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=j0R1HBnEb76S3%2B%2FrjF7k7g%3D%3D.OKGNjFhNCZr8D61hh2I5SB88x75qmLhRq%2FwjCjThpNL4fP3pcZ9qBWb99gw6qVvwl2aZxQInPGp3k9C3zUXRlBFaDze2vVRzc2cHeaOsNmsk6JgvnINRhOmWnyWNoVg4O5PmVvTY7PgTDh5QJmXlEQ2NtlwkHR3HEyMQBh0J36jam6rtW%2FQFuHWA8W9NdLTcgMvIwYZAXdO0D5uvV%2BYqRWUmaz6w2V1UpIKZjHC4fBPgJuxARR73taxR4agUttSmOGuZpjONqBXvL2g%2BMNENiom1N5K0ltp%2FSoyLbfJElxjaZLtx2TJnu1D%2BHcmgPu%2BJ4bkv3SEQP1WSMs%2BmxR70xA%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=XKE6X9d4uUQkgDwY1SCX5w%3D%3D.R%2FEjmGsYCxiWPuDWXF5jgl4rtdG8xSPTfc2qPWb9LYdnSMJ29YXxcYH4ePbzr%2BmqDHP4faRmAxnXOXcrxtpJvK%2FHV6AI%2F23AVfs4GCjsJAnoUTuW3vSIFjRpIUNHULiTxeaMZHsN4Rwvr0h3wnWNFrtw%2BAqjZOz5cPVaze7FrZbmyojkZvn3H%2FU8KFSMqRp7lE2Vh7VvrHJidiGmtRuTmmbgFymgqSgix1t%2FxEsA3SSMnYiF3m55%2BiGa9mMegGXiYflFTkbNvopmsjGXpscYqCPVtbEuLLy31QvojrWfGpllKcAlZ7NhblYlvVDj05obO2nPLiDb2rZ9GWljxKBu1g%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
手把手教你实现echarts3的折线图下钻drilldown功能系列篇二
https://segmentfault.com/a/1190000006978935
2016-09-22T18:54:54+08:00
2016-09-22T18:54:54+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<h2>开场白</h2><blockquote>好了,<a href="https://link.segmentfault.com/?enc=uBV3YGPusI%2BFChiJYrqZqw%3D%3D.hYy%2FuN%2Bll6MxR0y9mHdd5vKxNPLUSkip6FpshCs%2FxKS4Gbr2XokMZCcYQDKUwWeD1Ik7P1O2lcqHvZ398o6WYa%2BK%2FJ4Yfkmzg1CghKtzZ1o%3D" rel="nofollow">上一篇</a>介绍了关于echarts下钻(drilldown)的一些信息,通过上一篇文章我们知道echarts折线图,柱状图没有支持下钻(drilldown)功能的api,那就需要我们自己动手,丰衣足食了。<br>这一篇我开始进行实质性的代码演示。你可以按照我的步骤一步一步来做,有什么疑问可以留言我。</blockquote><p>欢迎大家访关注我的微信公众号:全栈在路上<br>也可以访问我的<a href="https://link.segmentfault.com/?enc=llnwid9QhadFzqJ5H5uvLw%3D%3D.2EdvzGakz34T%2BP8fAFxmvuyNAnhAUtWu4ftdXeRw%2F7A%3D" rel="nofollow">github blog</a>查看更多文章</p><h2>一.效果贴图</h2><p>为了避免枯燥无味,我先不贴代码,写贴上我的demo图,这里还会拿上一篇的那个demo图为例。</p><h3>1.下钻(drilldown)前效果</h3><p><img src="/img/bVDrHu" alt="图片描述" title="图片描述"></p><p>从图可以看出:<code>这是展示2016年1月一直到2016年9月份的数据的折线图。</code></p><p><strong>下钻(drilldown)开始:比如我点击2016年9月份(201609)的这个点,则应该显示一个属于2016年9月份下的每一天的一个折线图。</strong></p><h3>2.下钻(drilldown)后效果:</h3><p><img src="/img/bVDrHv" alt="图片描述" title="图片描述"></p><p>从图可以看出:<code>这是展示的从2016年9月份0901号开始直到0930号这30天的一个折线图。</code><br><strong>完全符合我们的要求,对吧。</strong></p><h3>3.返回父级所在折线图</h3><blockquote>这里我提供了一个返回按钮,点击返回按钮后,会重新返回到父级的折线图:<br><img src="/img/bVDrHw" alt="图片描述" title="图片描述"></blockquote><h3>4.总结</h3><ul><li>由这3张图我们能够看出一个标准的折线图下钻(drilldown)功能就出来了。</li><li>那么,实现起来复杂了,其实也很简单。因为我们有万能的 setOption 函数。</li></ul><p><strong>不废话了,下面开始贴出详细的代码,准备好了吗?</strong></p><p><strong>注意:如果有对echarts的最基础的使用还不太了解的话,建议去 <a href="https://link.segmentfault.com/?enc=f1CN%2FQkjlyYqexngCUhOdA%3D%3D.jjjssa65hnFuZ%2BVlTyKalTpGtfC6XjMs%2FMAvSQ3t17uUSQPyNsqX0H2vGfE6Z87f" rel="nofollow">官网</a> 看看api和教程之类的,我这里就不再对基础的只是进行赘述了。</strong></p><h2>二.折线图界面line-drill-down.html</h2><pre><code class="html"><!DOCTYPE html>
<html style="height: 100%">
<head>
<meta charset="utf-8">
</head>
<body style="height: 100%; margin: 0">
<div style="margin-left:40%;margin-top:2%">
<button id='return-button' value=''>返回</button>
</div>
<div id="container" style="height: 50%;width: 50%"></div>
<script type="text/javascript" src="http://echarts.baidu.com/gallery/vendors/echarts/echarts-all-3.js"></script>
<script type="text/javascript" src="../jquery.js"></script>
<script type="text/javascript" src="./drillDown.js"></script>
<script type="text/javascript">
var dom = document.getElementById("container");
var myChart = echarts.init(dom);
var option = drillDown.getOption(); //获取配置
drillDown.initChart(myChart,option); // 初始化加载折线图,并显示出来
// 点击返回按钮,会重新回到一.1的折线图
$('#return-button').on('click',function(){
var myChart = echarts.init(dom);
var option = drillDown.getOption();
drillDown.initChart(myChart,option);
});
</script>
</body>
</html></code></pre><h3>代码解释:</h3><ul><li>10行:在绘图前我们需要为 ECharts 准备一个具备高宽的 DOM 容器;</li><li>13行:加载了一个drillDown.js文件,详细代码见下面的第三步;</li><li>61,62行:基于准备好的dom,初始化echarts实例;</li><li>63行:指定图表的配置项;</li><li>64行:使用封装好的initChart方法为图表填充数据,并使用63行的配置项和64行的数据来显示图表.</li></ul><p>这里都是echarts的基础知识,详细的可以点击<a href="https://link.segmentfault.com/?enc=MkcfUZuYjhuOEPO%2Fxw6l5g%3D%3D.F1Rk%2B28MUdW7NR9uChm5FjHfr%2FkCDPV%2F1OlX9CoVgHWLuZbElD0RkEtlFGB0Xui0HzmGReA6oE4e%2B2GGBkzEFIB%2BR2ENHXZTOfg2zDBu9fCa5hwz1GQ8AY6VR3aAMfXN" rel="nofollow">这里</a>进行充电。</p><h2>三.drillDown.js代码</h2><p>在这个js文件里我封装了几个方法:</p><ul><li>getOption: 获取当前echart对象的配置数组,我就不再详细讲解了。</li><li><p>initChart: 初始化折线图,这个方法做了两件事:</p><ul><li>显示图表;</li><li>为图表添加点击事件,也就是点击 返回按钮时触发的事件,详细逻辑看代码。</li></ul></li></ul><p>看代码:</p><pre><code class="javascript">var drillDown = {
getOption : function () {
var option = null;
option = {
title: {
text: '折线图下钻(drilldown)示例',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}'
},
legend: {
left: 'left',
data: ['月数据']
},
xAxis: {
type: 'category',
name: 'x',
splitLine: {show: false},
data: ['201601', '201602', '201603', '201604', '201605', '201606', '201607', '201608', '201609']
},
......
};</code></pre><p>由于代码篇幅过长,影响阅读性,我这里不详细贴出,大家可以从<a href="https://link.segmentfault.com/?enc=YzzBNRanPIj%2F%2BCXwjVvg0A%3D%3D.vBuUH%2BcwrbbQth7ks%2FEtm1jZLBWTfbdjYKwX5Uek8eYXcyVi65MdPfS7m1H9gQhyvAGySuqPi05Fza3ftbIzgw%3D%3D" rel="nofollow">这里</a>下载。</p><h2>四.后台数据接口代码data.php</h2><p><em>这里我写了伪代码,大家看一下应该就能够明白了:</em></p><pre><code class="php"><?php
// 这里的接口代码伪代码大概如下:
function getInterfaceData($month) {
// 1.写查询语句,这里只是写伪代码,自己做防sql注入
$sql = "select ... from table where month = $month";
// 2.连接数据库,查询结果为$data
// 3.对查询的结果进行组装,返回json格式的数组
$chartResult = [];
// 4.取出x轴的值
$chartResult['xAxis'] = $data['report_date'];
// 5.为第一个series赋值,我们这里的demo只有一条折线
$yAxisArr['yAxis'][0] = $data['data0'];
// 如果有多条折线的情况下,可以这样写
// $yAxisArr['yAxis'][5] = $data['data1'];
// $yAxisArr['yAxis'][6] = $data['data2'];
// 6.为y轴赋值
$chartResult['yAxis'] = $yAxisArr;
// 7.返回json格式的数据
exit(json_encode($chartResult));
}
?></code></pre><h3>说明:</h3><p>`data.php每一步我都有详细的注释,该方法主要是返回一个json格式的字符串,来供ajax回调使用。<br>客户端再拿到这个json字符串后,再进行拆分,分别给图表的x轴和y轴赋值即可。`</p><h2>五.代码下载</h2><p><img src="/img/bVIXnD" alt="图片描述" title="图片描述"></p><p><strong>demo下载点击</strong> <a href="https://link.segmentfault.com/?enc=oSJpsFGnKRWzveyKRZzz9Q%3D%3D.kROH%2BIINurRSQMiIA%2BOxlkg9uJdhTRmX9z4paYNMi%2BG%2BOqjdyTLfD3Y%2F1m4LLx%2FAXmzmxIbCTbXPs4AcDLCmoA%3D%3D" rel="nofollow">这里</a>。</p><h2>六.总结</h2><ul><li>好了,如果在第二部分中直接使用我模拟的测试数据的话,可以先不理会data.php的代码。<br>直接打开<code>line-drill-down.html</code>运行即可测试;</li><li>跑通后,需要与后台接口打通时,可以看一下<code>data.php</code>的一个思路,根据自己的业务写逻辑,然后再做测试即可;</li><li>大家在测试的过程中有什么问题,可以跟我留言,我会在第一时间回复;</li><li>码字不易,转载请注明出处。</li></ul><p>最近结合hexo和github pages又搭建了一个<a href="https://link.segmentfault.com/?enc=0RC11g%2Bc2%2BKDmMF0vI%2Fp%2BA%3D%3D.LXG%2FqBxSmtFFHqNOG8L90oQM13s%2FTfTPvGdDKuznH8Q%3D" rel="nofollow">新的博客</a>,我会慢慢的将<a href="https://link.segmentfault.com/?enc=VoZ6HXrhZXkjp%2Fy4YnA%2F7g%3D%3D.BEvHpOUhiFCN1fwr9EO2vszZ7OXdkXO4y10bCaPXvI4%3D" rel="nofollow">sae博客</a>的文章逐渐迁移过去,欢迎大家访问。</p><p>欢迎大家访问我的新系列文章,主要是讲用最新版的express怎么去开发一个简单的blog.<br>目前已经更新两篇:<a href="https://link.segmentfault.com/?enc=yQ0YWVdBGKidK3nKuJXkVQ%3D%3D.qjTXkXCqctx88zuzaYNuLzzJVI9e3LEbLX16iN%2FhXwUbsMlIwbe6Rz%2FIiSssWBMjomMeUFQxISL2c4LBP%2FfMHw%3D%3D" rel="nofollow">手把手教你开发nodejs微博网站-开站篇</a><br> <a href="https://link.segmentfault.com/?enc=518TNS0SiqVf%2F500unf3vQ%3D%3D.xCjOzJ6LquW%2FngR9QeqrLOTHx%2FCT5wgxSZqduHOK4AV9tUtrBI3Ytof36z2IqWLRyaT%2F%2BKCp8XnEBBlfzJNaLw%3D%3D" rel="nofollow">手把手教你开发nodejs微博网站-首页篇</a><br>最近在学习nodejs,欢迎大家在看过后踊跃拍砖。</p><h3>关注微信公众号阅读更多文章</h3><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=9m%2Bwf51a%2FEOu8vco3Qxf1w%3D%3D.U8HkDL3Eg4vpGK1rlNofj31L%2FI8e8R3iar8JaaOK158hUcq7j4IoAt%2B9jh%2BJpX3apnZxHu4cyQxPlrK9TVKi0l2WJYGN1U%2FDpQAqz6dsSyQw39%2BtaiVfiETF0NoAF0ZyBhBV6yVmD4O%2FAGF4TnvrROysFcreLjGMALjp3VwbwJuChX01k3MmMgjEftmygRLMDY1TOqgytHq%2FEyM%2BAp6pBYpqCkm7qIeFpdf3jJzSfJpohwu5BiYylSsqa44m3piN8rhClrzyify0K4Jysp71lXUhMQzSTZmqawic0YCt%2BJtbKd%2Fl5sot6T5wysyQmCPQtG%2Bsa4bg94EAdsseWi0djw%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=BLHVNUAxeGOdtfZoDMYNFw%3D%3D.ZuOVsgBOZMGDDA4W%2FCZOLzRj%2BivZApWw9u6w79UtxBEbQUWYpcqMaUs5IhT9QWJM4KMHOpPD5pPPJsn1nxXuuljF4a5mi5Ux4q9Sg5Vtvc9deoPVZBK7nMVOOXb9c0O6vwYTrHKmkpgQmqWy0%2FBW%2FfQOZ%2F9t7XJPrTkzXVseL%2FOCnUEiXXVmRRA1Da0qSPnnQETPIDFBN7EaBAxdxuCKhDBS%2BwFQ6daREYtKZT9neegyzR%2Fm%2BifrfgxkP4lKBFk3XeZg2%2FR6iJi%2F6g%2F%2FdWYu%2BRlj%2BcRflpEvSG%2Fw886INZxNWELzEtzhRMwj7R0sx45GCzkAEmQFg7zMRYRG5aGyrA%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=bNcFLWL7otOWWu81Ni11fQ%3D%3D.KZ1Nf6daKFJueO92Q8XcSKMLRdMcmn3ZrEQcBYN4dum%2FbhF4WyLw9ghpxFX0NHjt7wyQLEfRncCUuk%2FEfh8z0MVAdWMrnoEm%2BsiE%2FzgDyp1TiGPRkpM2EtChBSZi%2FbDZOx4jiVBolwrXmixCUgcimgKCKdzUEun7qP5WO2GYf1niNz5btwakLa5xKP6RMfEsdDQCSDdMFiwOQHcebrON1RtAVSheEEirUr2zbSpP3pQ0ctLTwYgHSBeAQBPdOQzoqe%2F2W4vppQJYsZOLOiwfkvr75JoWAK4YKhJ63HgZ62k29gR%2Bf7QrHHXa94C4%2FLybGX%2Bbcp0ra6yonohiUMpTpw%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=dxX%2B4IOhgz0VYtW1k0S1sg%3D%3D.%2BZWTFZ5jZgGKacS3HuKoHjweqF1Mjyh6wmyQwWXa7QN6X%2B86ObZyK0uqq4Nr4XpTTdOuCFJfYzvcEfdYKTJhfgaGLgpS%2FDlq72cpc0XqA8WY4gI3tXQ1sfiE2f6wt4jDyFpQGfPD4bIGqtys6oAlRr0FMaGkEzcyPIlIJiDZT9V%2BvhHLhvJ3DPt2Za6voNfAmX5qXh%2BrKtsJpYoU8NumCbR71MCvMH97lycsU%2FhDLLVih6Cv3W6QfDnITaoXjyiIGsE%2BaaWrpTjvbqnK9soy%2BKMw23Fuvofcvvr37jEgo0RqWrcjpBYl9xRw6ngAQ5uGtLRQT20Qt%2F2R2k327ysT4g%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
手把手教你实现echarts3的折线图下钻drilldown功能系列篇一
https://segmentfault.com/a/1190000006967546
2016-09-21T19:06:03+08:00
2016-09-21T19:06:03+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
2
<blockquote>欢迎访问 <a href="https://link.segmentfault.com/?enc=IdDKrMWK3OSVQdh5wC24Bg%3D%3D.DzAme3war3GdN6wXyCcG%2FeIpRaqsgnCrGIIJiveDYVkuELeK6tuOc5LqdUZsOuXOoOe4Q%2BazfLDh60k6JaQmRBQEj2AHsmFeo1bvDSeHIj92c98MQhD9urMNAMurhWbF80ZRo3gugj3Iz8OnnRO5NR%2FwLPiYwZxzezUocI%2BnOaVr%2Fyhg0an%2Fzk7bx8KOgI0U" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。</blockquote><h2>一.抛出需求:折线图-下钻功能</h2><blockquote>最近接到领导的一个需求:说是想在原来的月份数据的折线图上,点击某个月份后能显示一个该月份对应的每一天的一个折线图。</blockquote><p>欢迎大家访问我的微信公众号: 全栈在路上 <br>来查看更多文章<br>折线图图一如下:</p><p><img src="/img/bVDoIz" alt="图片描述" title="图片描述"></p><p>领导的意图是:<strong>比如,点击201609月份这个点后,能够出现一个新的折线图,显示的是201609月份的每一天的数据。</strong></p><p>需求看到这里,相信以前接触或使用过highcharts的童鞋就知道了,这是一个下钻的功能。</p><p><strong>我的项目现在使用的是echarts3,以前是用过highcharts</strong></p><h2>二.highcharts的下钻功能</h2><h3>1.看图表</h3><p>我们来看<a href="https://link.segmentfault.com/?enc=Kjva%2FztzJXrC%2B7BYoNjWBA%3D%3D.aiHmBh0%2BJoxV3X6%2FXHdDd2UCshRnpCJmUWLp%2F9sK7hkcQHIS%2FXgYFtMx3Q0mpYmtc564dek52nDp8CE56AMaig%3D%3D" rel="nofollow">highcharts官网</a>的一个例子,显示的是各个主流浏览器的一个数值比重,我这里截图如图二所示:</p><p><img src="/img/bVDoIM" alt="图片描述" title="图片描述"></p><p>然后点击随意的一个柱子,会出现一个新的图表,如下图图三:(我点击的是第一个柱子:IE浏览器):</p><p><img src="/img/bVDoIO" alt="图片描述" title="图片描述"></p><p>从图三可以看出:</p><ul><li>显示的是 IE浏览器的各个版本使用率的占比.</li><li>点击圈红的后退按钮,可以返回上一级的图表.</li></ul><h3>2.看API</h3><p>看一下API对钻取的支持,如下图四:</p><p><img src="/img/bVDoIP" alt="图片描述" title="图片描述"></p><h3>3.疑问</h3><p>既然highcharts对下钻功能支持那么好,<code>那么echarts在这一块做的怎么样呢?</code></p><h2>三.echarts的下钻功能-drilldown</h2><h3>1.看图表</h3><p>翻了半天,只找到一个矩形树图的例子,类似有那么点下钻的意思,感觉很鸡肋。想了解的点这里。</p><h3>2.看API</h3><p>只找到了series的type=treemap类型,如图五:</p><p><img src="/img/bVDoIU" alt="图片描述" title="图片描述"></p><p><code>感觉好鸡肋呀,折线图,柱状图的这些都不支持下钻。。。</code></p><h3>3.echarts项目issue区域</h3><p>去github的echarts项目<a href="https://link.segmentfault.com/?enc=gqxrDEjikEXoB7AchaHxiQ%3D%3D.t5n3hmqzgf8p%2BSoYb6KVi8zX6YTSo5FoV%2BmewOjm0jxnLaMjOvOyUfcFvqXwLGfT" rel="nofollow">issue区域</a>看了看,发现已经有了好几条关于钻取的问题,如图六:</p><p><img src="/img/bVDoI1" alt="图片描述" title="图片描述"></p><p>然后点最下面的一条件进去看了看,发现是echarts的作者林峰的回复,哈哈,终于看见大神了。如下:<br><img src="/img/bVDo86" alt="图片描述" title="图片描述"></p><h4>4.翻译一下</h4><p>好吧,翻译一下:<code>大概意思就是告诉我们echarts不会提供什么钻取的接口,大家要想使用钻取功能,请使用万能的setOption吧。。。</code></p><h2>四.总结</h2><p>看到这里你大概会想说:</p><ul><li>BB了那么久,怎么还不入正题呢?</li><li>echarts的折线图究竟怎么来实现下钻(drilldown)功能呢?</li></ul><h2>五.未完待续</h2><ul><li>不要着急,本篇主要讲了一下遇见需求后分析问题和解决问题的一些思路。</li><li><a href="https://link.segmentfault.com/?enc=jDMbVOOfJrAeE1Cp1WFn1w%3D%3D.nxS7UekDI4zUNBBvJUzumhjSxVSjbG2q8eTEuJAEj2Ga%2Fa%2Bv3LYAml%2BPc0Zdlv2bOJTJV41kUDXw32z1xkpjThcqX9H2W2064P6nHagF19Q%3D" rel="nofollow">下篇</a> 我会从代码的角度详细介绍怎么一步一步来实现,敬请关注!</li><li>代码可从github上下载: <a href="https://link.segmentfault.com/?enc=J9%2B2R5Q%2F3WWXdnCWGrIYcg%3D%3D.OMm%2BcANKS8pcWQ9OVUrVygH7IfRafehgyGxaOsho%2BMJbbtBxuemR1uwQzZmEVyJzw2JLhEPAwHz81GUBP3vbdg%3D%3D" rel="nofollow">https://github.com/phping1/ec...</a></li><li><code>码字不易,转载请注明出处!</code></li></ul><p><strong>最近结合hexo和github pages又搭建了一个<a href="https://link.segmentfault.com/?enc=V6aGmS8nhUQFfJsVW14cpA%3D%3D.52AIjo7lSD87am77IcpjXJ%2BDSlgWhI2%2BCQhAH1DSG1U%3D" rel="nofollow">新的博客</a>,我会慢慢的将<a href="https://link.segmentfault.com/?enc=I4mbEMkypFoW9pOKz2BGXw%3D%3D.bGttVP9BduTi7WyFIwYzRtmEtb1EPSEkb6W6PeaueCQ%3D" rel="nofollow">sae博客</a>的文章逐渐迁移过去,欢迎大家访问。</strong><br>欢迎大家访问我的新系列文章,主要是讲用最新版的express怎么去开发一个简单的blog.<br>目前已经更新两篇:<a href="https://link.segmentfault.com/?enc=36aDYoXrUW6ND%2F74Hzm%2B8A%3D%3D.AB3IZD8ypKzDI0k%2FDrdM7%2BZLFnhwxvzPn88wp6YLhpzLq6KPbixNndbKCXoFme%2FW%2FivITWspmoPLogmfBB0hbQ%3D%3D" rel="nofollow">手把手教你开发nodejs微博网站-开站篇</a><br>,<a href="https://link.segmentfault.com/?enc=Xg1HorGuIOyW95rK3c5ezg%3D%3D.RP5NKt2Q9BH3UK5BCQaU%2F01TFgRp2pjmaEDDHkflRP2kLaN5GjWWfzoJrrGT3tdISkQEDzSUSxuDnqTIHIEGYA%3D%3D" rel="nofollow">手把手教你开发nodejs微博网站-首页篇</a>。<br>最近在学习nodejs,欢迎大家在看过后踊跃拍砖。</p><h2>六.欢迎大家关注微信公众号</h2><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=CHOBMnGwZXuBFnUh238W8g%3D%3D.d1JOTo%2FnXZGJRMxQm%2BT2ySzzE37U%2BLk07xdC2%2Bh8PalS5xAF%2FeUlNTkwfh8%2B5ecRxURTGLuAAoIrQxIPiOOJYFTs7uAZVEKnAXvt5u4iqCTmNXQWA7qYRjBKAUw9%2BsIJZ0nyZ1qi7A6NIgv6TntuBW1TJhLpDDjvruyQMmmv85x6Ok%2BanNv%2FtyVuSyxRnkLaj6HZ%2F2xRQYeEzN2nVzVCPMuW%2B5Hk7d0pcINNLoBj05V3NbcXRttvAVa80q05ogkJAmauIiAOGg78MMpwQct5L7CuI1LQg7hgG%2FlZes5EEEfNl6IiTQoS8zN8%2BnrK7SXqbUo6TSoUjDfaKNANSmnHVg%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=YEGc7hsbHlWsXbCm2cl3OA%3D%3D.pJtLpDsNVl0ZIukhpb7qycYcYYgwEvtEFJMdTx1MDpm53RiDPAZjAV5YpnnjBR6wL1MmjfaE5apDoB32t8lbtsbqTDlniM9CRLrzktU3VKjPEJGshHO9WUvKl7pXrUN7H8yWN9jZ341UIGMqkatIgnYhOdb5qwuPa20aesTr0axsjN6tZUBEas6Sej1gOOVlYPi7pcU6J3A%2FpeKJL%2B5ZvDrEN4kUd0LKOVXskwsdfm76wpnRaFUkWehFV%2FamJsFgSd7ru5ZxJNRr%2FM0s%2FRkkzzVliGkepqXCCJ%2F3DG0mvNkH4bOrXN%2BLr0YGUowG5mueh8wHIbTN7P94dWQRt1mOOw%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=cCOkpK4%2FY1mU6pcJbfzpjQ%3D%3D.ZpOrBGEODfFq1JQVzHU%2BO7wCFi2vJD6j8oQp37fZnD1u4xDVZc3lRJVNY9toupNXob%2B42LC1AKHNOPtFIEzWhy1jOD6vEx4EXQmKH1vHvEHcHN%2B%2Fx82yNbiV2AwXUwj9ZYWzu8hH7uPPHOkOhUZ6Yu%2FPdKQs2Oe8AJ4A4WIsNuUvKnby1xpqxNx1sOMOSK1UjW172e1%2FTYQCz1gHNvTfLsSprGjb22V%2Bp4tLj1TVjAx7ZRw3QIAPeaPMMx9J9VKlMbTgqO0XYDjKAsjNBMgcXmJvYGlmnSkB5QG5ZGhQcYgkQpn4QeD48Tt6fa2PtMRzTLiIzXG5kH7sIbAq38Buzg%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=uxHulq8DDJNfglr83K9Prw%3D%3D.2KsAOjG2S%2BmYAaLjbhcofeYsCbo3cHAb1UgUbVRMYLEzqcwhEOgapFydiQz6RkG3huRo1he4iNK7EKzIRh3nZhyLHDyoEsZQ5NPDVacRvqKXuYLmsxWR%2BDkzRyJbhGRdTzrR9UBoMRqHIY5v0cXmgZKmGnMZdAQ6%2Bq4cxAEE%2BcfMuERbGwEohbakKkMRf0z0QKkjqRjdUMPDfIkcmWXsmDVyIQkCgovm4kyNljQtTIKFkNLZKaumZjlF2VSRnBJ0cjhX%2BGQ6pxOqKAGBcGcrmLoebQNMQ5yJiWJfdSgpu%2FUZYoUt0d8surB9u873cvHs83Wxep2ziVxXfg%2FlXoUEWw%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>
echarts3的折线图怎么分段显示不同的颜色
https://segmentfault.com/a/1190000005648860
2016-06-05T15:55:22+08:00
2016-06-05T15:55:22+08:00
liuyongfei
https://segmentfault.com/u/liuyongfei
1
<blockquote>欢迎访问 <a href="https://link.segmentfault.com/?enc=qpoI913uzEDSXZ7IokH87A%3D%3D.PkDN4sDKPb4MATuLAe6hmzu2vW70Y3LUQAO6P%2Fo9vsTE5072o5WH2U%2BAlVC2GTqO%2BbwwUENyk4MNgnVo8trlElF%2FZs5pOEIPqUaMc2E3t10PgxGIc9iQqdKHFgNneYvNZ2ZOFES3VUELcZkdSogEg0f511QW6rjLMc4T4ixcn8e5AzAKUfIkz24S0bgz3B8Z" rel="nofollow">这里</a> 查看更多关于<strong>大数据平台建设</strong>的原创文章。<br>一.场景</blockquote><hr><p>在使用echarts3做图表的时候,可能会遇到一些特殊的需求:</p><p>星期一到星期四这几个点的折线显示一个颜色,周五到周日这几个点的折线显示另外一个颜色,来起到强调区别的作用。</p><h2>二.效果图</h2><p>先看一下效果图,你会有一个更清晰的认识:</p><p><img src="/img/bVxRGl" alt="图片描述" title="图片描述"></p><p>从图中大家可以看到,整个折线图分了2段颜色:周一到周四的折线是红色,周五到周日的折线是黑色。 这样一来,是不是就有个很明显的强调区别的作用啦。</p><p><img src="/img/bVxRGs" alt="clipboard.png" title="clipboard.png"></p><p>demo下载点击<a href="https://link.segmentfault.com/?enc=JUYaO87ftwkWVi%2Bwm64Y1Q%3D%3D.2RY75oAnknrygiiyEtbFELDy2iKbzvPpQfdS0Pyd10YpOQk%2FNP7MfO4uXszvcyyRY6SdSf%2Bvn5huMbin012UhA%3D%3D" rel="nofollow">这里</a>。</p><p><strong>那么,怎么去实现这个效果呢?别着急,一步一步来,往下看。</strong></p><h2>三.echarts3的api支持吗</h2><p>看到这样的需求,第一反应就是去api里看看有支持的函数没。。。 在api找到半天,果然不支持。领导非要这样做,echarts3的api里又不支持,那么怎么办? <strong>答案:换思路</strong></p><h2>四.思路</h2><p>1.折线图的数据点在哪里被赋值的?</p><p>我们知道在echarts中图表是通过series来实现的:</p><p><img src="/img/bVxRGv" alt="clipboard.png" title="clipboard.png"></p><p><strong>其中圈红的第一个就是图形类型为折线图时用到的配置,折线图的这些数据点都是通过里面的这个data数组来生成的。</strong></p><p>2.拆分为多个series</p><p>将一个完整的折线分成两段折线,不同的折线显示不同的颜色即可。</p><p>我们知道周一到周日总共是7个点,series的data数据为:</p><pre><code>series: [
{
name: '指数',
type: 'line',
data: [4, 8, 16, 32, 64, 128, 256]
}
]</code></pre><p>如果拆分成两段折线的话,就得用两个series,两个series就得有两个数据集(data数组).</p><p>其中第一个series的数据集为:</p><pre><code>4, 8, 16, 32, 64</code></pre><p>第二个series的数据集为:</p><pre><code>128,256</code></pre><p>但是在echarts中,图形的每一个点都要有与x轴和y轴对应的,否则,画出来的图形是与数据对应不上的。</p><p>所以我们需要对上面的两个数组进行一下改造。</p><p>3.普及一小技巧</p><p>在echarts中,若是不想让某个点展示,则这个点对应的data数值可以用'-'来表示。 反正这个知识点没有在echarts3的api里提到,应该在echarts2中继承下来的知识点吧。</p><p>4.转化数据集</p><p>知道上面这个小技巧后,我们就可以把这两个数据集写成下面这种格式了: series[0].data:</p><pre><code>[4, 8, 16, 32, 64,'-','-']</code></pre><p>series[1].data:</p><pre><code>['-','-','-','-','-',128,256]</code></pre><h2>五.摞代码</h2><p>既然思路都有了,那么我们开始试试吧。</p><p>1.option的配置和主要代码如下:</p><pre><code>// blog: phping.sinaapp.com
var dom = document.getElementById("container");
var myChart = echarts.init(dom);
option = null;
option = {
title: {
text: 'echarts3的折线图分段显示不同的颜色',
left: 'center',
link: 'http://phping.sinaapp.com'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}'
},
legend: {
left: 'left',
data: ['指数']
},
xAxis: {
type: 'category',
name: 'x',
splitLine: {show: false},
data: ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
yAxis: {
type: 'log',
name: 'y'
},
series: [
{
name: '指数',
type: 'line',
data:[]
},
{
name: '指数',
type: 'line',
data:[]
}
]
};
if (option && typeof option === "object") {
var startTime = +new Date();
option.series[0].data = [4, 8, 16, 32, 64,'-','-'];
option.series[1].data = ['-','-','-','-',64,128,256];
myChart.setOption(option, true);
}</code></pre><p>2.走一个试试:</p><p><img src="/img/bVxRGD" alt="clipboard.png" title="clipboard.png"></p><p><strong>两条线是分出来了,但是中间是有个断点。如果你觉得这样影响需求的话,则直接在series[1].data里把这个点补出来即可。</strong></p><p>2.1原来的格式:</p><pre><code>option.series[0].data = [4, 8, 16, 32, 64,'-','-'];
option.series[1].data = ['-','-','-','-','-',128,256];</code></pre><p>2.2修改为现在的格式:<br>篇幅所限,我这里没有详细列出来.<br>详细请访问我的blog: <a href="https://link.segmentfault.com/?enc=nZAy0OoZFQhQH%2FJxzkZa0Q%3D%3D.bPtau2NQW8ML3eHc3Ki5ZdmKtuv9%2FVHmF%2BqLg90WX6hO5TJcC5u41FXHIo0He1yNwLsLtpOz65cByP%2BS%2BcKRGbH3Iu%2ButgQje%2BYiDI88aBA%3D" rel="nofollow">echarts3的折线图怎么分段显示不同的颜色</a></p><pre><code>---
---</code></pre><p>再次刷新,是不是两条断线连上了呢。效果就跟文首的demo是一样的了。</p><h2>六.总结</h2><p>1.遇见此类需求时,先看看api里提供了类似的方法没有,有的话,就不用费大头筋儿了;<br>2.没有的话,就得转变思路了,将一个折线分成多个折线。<br>3.巧妙利用四.3中的小知识点来绘制空点<br>4.实际开发中可能要比这个demo要复杂一些,但是基本思路都是一样的。<br>demo下载点击<a href="https://link.segmentfault.com/?enc=5TKiCshWS86meRDkavT4Kw%3D%3D.ORMl6ceB7BE%2BFjdoyhkLjsZAfS97OwhluNq64cSOb0dbXRNT%2BddIbNONJIwEU4MsFo4wpo7uJTmHjwt1qjuXG4j60USQpZmZcYQETztgN%2BE%3D" rel="nofollow">这里</a>。欢迎大家关注我的微信公众号或访问我的<a href="https://link.segmentfault.com/?enc=m6d0%2B5nrVX2K7Jl7uYY0iw%3D%3D.imcNwvO0%2BrJcopKcq7kUUqo0sUGQVNrQs3%2Fj7j30C64%3D" rel="nofollow">blog</a>,有更精彩的文章吆!<br>码字不易,转载请注明出处。</p><h2>七.欢迎大家关注微信公众号</h2><p>欢迎访问更多关于webpack系列的原创文章:</p><ul><li><a href="https://link.segmentfault.com/?enc=ZuD%2FIzcq2c2CNtuMzoGSDg%3D%3D.82sH65%2BOuzuxQ1RqrT3uacYir6Z3zc1SYUWgUEdbDiNc1yediQjf9BJdt%2FSyyG%2BurQgns%2FKV4%2BH475OgwiF0RcCwOL1RqmSv6rC%2ByLZ%2BpRDWXdn2S2iZgn6%2Bnj4n2rhqEXuHJ06%2F7zXpQKKRdvFVzQpdmViZNinb%2FpMAUZUwD4qT4daiMsLyEOn9fXS%2Blzhbc3F8oRcXg7fghak2dAf%2BlSjlzZICjBHqAqfSIH%2FglXhnwDWEhaphmj2TRwCK0a0lpZ2tEIDf5YN%2B7VJnhSUJ6ERQh0OCZojkuXqhWFEZmJ1gvGVijishm6aNJnG4J5EmFwvJzxW9qotDiX0EIZjijg%3D%3D" rel="nofollow">webpack系列之基本概念和使用</a></li><li><a href="https://link.segmentfault.com/?enc=sXPgUCsQZsJAn3%2BIJbYdPw%3D%3D.TLpllcxNaRTltbi0fg%2Bec7dq%2BshJZJlZ%2BS5BAEV505k7t%2BBLay3ex3qtHHNKBDUaa1FLuUk5X2XmVaUGAmgA5cH13cVBBqX5uyZWAWDnOpRHx5j0EG5i3tIo5ZzWyaEJFgecijypJmjRnKeO%2FhqUbLLVuMkp5mUGvrAMmVjrN1gz2uAlw9wm9zX5%2FS6ZnHo50Sezr%2FClibaBj9EA7%2B0opQNIx32JtP5GZBCCUrWPeDJ3mcU4FoUAyNhfiXOYhY8yFMoSUOajxkZZ3skatAhB%2FX9Fo27SlN2Ci%2Bsb9L3FI0zkLQw8d41OlO76Y7fwwhYKGONblLIcn2Mp%2FlS%2BC4Exow%3D%3D" rel="nofollow">webpack系列之loader及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=Gj1xXd68jVC%2B52geN6Cz2Q%3D%3D.Fi%2BsZ%2FE25P%2Fm3o2EpEwyffmy7xRxpUnKIOunosVFc9FkHubrL0M%2Bn%2FtrkrxfVk%2Fe8M%2BVjhO226kVrW2t0V4Chz3xdA0L8JkaP9vVkLlYqevhwe4MLuI4WWzUfWrixmwlAnAkYVYi1mLPnUAi4gMotOnhExGRh0k34G3b1eO%2Fb1d2F9inl8SVglLizxSdVxp77My4lXeLGyYgJ1mJ1EcskCuy%2FqxJRWlKi8jfvrmJHrJYdas4fcMVqUovm0Jvu%2B866bsLPvrlM%2BK92wmoIjLW2396vE%2F2NQ8h59kc9oknhBi92Rfd%2BGW9yuJvN9z%2BJSx1JBQl5%2FpnY3fAt1ZB5Lz6MQ%3D%3D" rel="nofollow">webpack系列之plugin及简单的使用</a></li><li><a href="https://link.segmentfault.com/?enc=k7hzJ7xcapoP23PiKqAzQA%3D%3D.7UM1d3T1JA5DzuDZlPJsR43vNrljRoByLmYUf%2BzjTkdgPM2VXn2nk8nubaTAsBaTedJu1EN4YuKXBvpwcA5QTB0XPiAHPZsXhrzM14TCyQB4QjJCY8l%2Be4%2BqZNKZ8ILR7hfHOfynOoCH2mln2yX7vFuk3mCmuVnJKLC0vRnmarMctLnwB8AXp%2Ft%2Buyxuxv%2FqkziR9C61rLay%2Fu3OX8vYBaelA8vFQdjiBgy7sVcwA%2FRw4JjY%2B%2B5GixVzRWAASq0o7FPbThUVoayB0IenR9hMSJ6klWq1Q4FWhiVY4DBgDT9BJojka5qaL3xL%2B6%2BYU4f9IQD%2BWOrQlJtmvYChBp7QNg%3D%3D" rel="nofollow">webpack项目如何正确打包引入的自定义字体</a></li></ul><p><img src="/img/bVbHhQv" alt="微信公众号二维码.jpg" title="微信公众号二维码.jpg"></p>