[{"content":"","date":"2026-08-15","externalUrl":null,"permalink":"/tags/ai-agent/","section":"标签","summary":"","title":"AI Agent","type":"tags"},{"content":"","date":"2026-08-15","externalUrl":null,"permalink":"/tags/deepseek-harness/","section":"标签","summary":"","title":"DeepSeek Harness","type":"tags"},{"content":"📌 本文为实战复盘原创文章，发布于本站：haifeiWu.github.io\n建站那篇里我说\u0026quot;从想搭博客到博客跑起来，隔了整整一个夏天\u0026quot;；这次不一样，两天，44 个会话，2324 步执行——只隔了一个周末。\n缘起：agent 的工作日志长什么样 # 用 DeepSeek Harness（下文简称 dsh）干了两天活，攒了一堆会话。写代码前有个习惯：先搞清楚数据在哪。翻了一下发现，dsh 把每个会话都落在了本地：\n~/.dsh/sessions/\u0026lt;工作区路径编码\u0026gt;/\u0026lt;会话ID\u0026gt;/session.jsonl.zstd 每个会话是一个 zstd 压缩的 JSONL 事件流，从创建到结束，每一步都有记录：用户消息、助手消息、turn/step 的开始结束、每次工具调用、审批策略变更，甚至自动生成的会话标题。解压之后长这样：\n{\u0026#34;type\u0026#34;:\u0026#34;turn/start\u0026#34;,\u0026#34;seq\u0026#34;:11,\u0026#34;time\u0026#34;:1786778276273,\u0026#34;data\u0026#34;:{\u0026#34;turn\u0026#34;:1}} {\u0026#34;type\u0026#34;:\u0026#34;step/start\u0026#34;,\u0026#34;seq\u0026#34;:14,\u0026#34;time\u0026#34;:1786778276365,\u0026#34;data\u0026#34;:{\u0026#34;turn\u0026#34;:1,\u0026#34;step\u0026#34;:1}} {\u0026#34;type\u0026#34;:\u0026#34;tool/call\u0026#34;,\u0026#34;seq\u0026#34;:...} 这哪里是日志，这就是 agent 的工作日报。于是写了个小脚本把全部会话扫了一遍，数字如下：\n指标 数值 会话总数 44（其中 40 个非空） 对话轮数 141 执行步数 2324 用户消息 265 条 助手消息 1019 条 时间跨度 2026-08-14 09:26 ~ 08-15 15:30 44 个会话分布在 12 个工作区目录下，最大的单会话 418 步、28 条用户消息。下面按主线拆开看。\n主线一：从 pi 到 dsh，飞书桥接的诞生 # 一切要从一个调研会话说起（13 轮、60 步）：当时项目的架构是 lark-cli + pi，想用 dsh 顶替 pi，于是先让 dsh 自己评估\u0026quot;DeepSeek Harness 替换 pi 的技术方案与可行性\u0026quot;。结论很务实：不硬替换，直接基于 dsh + lark-cli 新建一个项目 feishu-dsh-bridge。\n随后就是本次复盘里最大的一仗：一个 418 步、28 条用户消息的会话，按照设计文档把桥接完整实现出来。核心是一个 NDJSON RPC（stdin/stdout） 协议，对齐了 prompt / cancel / ping / resume / get_last_assistant_text 这些原语，让飞书侧的消息能驱动 dsh 干活。\n这仗打得并不顺利，踩了两个印象深刻的坑：\n坑 1：发一条飞书消息，回了两条 # \u0026ldquo;发一条飞书消息，但是会回复两条，其中一条执行失败。\u0026ldquo;排查半天，真相是本地和 Docker 各跑了一个 bridge 实例，消息被两个实例同时消费，一个成功一个失败。解法简单粗暴：停掉本地的，只保留 Docker 的。双实例是最难排查的 bug 之一，因为它看起来像逻辑问题，其实是拓扑问题。\n坑 2：容器只读，挂载点建不出来 # 容器根文件系统是 read_only: true，/app 整树又被项目根目录只读 bind，运行期想新建挂载点直接报 read-only file system。最后方案是把宿主机目录以只读卷的形式挂进容器：\n- ${HOME}/work:/app/host-work:ro 桥接上线后还有个收尾会话：README 全面去 pi 化、仓库打上 dsh 标签，从此这个项目只属于 dsh。\n主线二：给 dsh 写插件，一写就是五个 # dsh 的扩展方式是 cordis 插件：声明 name / inject / Config / apply，可以注入 tools / agents / sessions / systemPrompt。这两天围绕它写了五个插件，正好覆盖了\u0026quot;一个 Agent 平台插件生态\u0026quot;的各种形态：\ncomputer use：让 dsh 拥有\u0026quot;手\u0026rdquo; # 在 dsh 主仓库的会话里从零写的桌面控制插件（173 步）：鼠标点击/拖拽/滚动、键盘输入、截图、屏幕 OCR 一整套工具。做完直接装进 web profile 实机验证，还跑了一个 e2e 冒烟会话——让插件打开 example.com 并汇报页面内容，一次通过。\ndsh-ocr：让 dsh 拥有\u0026quot;眼\u0026rdquo; # 这是个完整的\u0026quot;调研 → 验证 → 开发\u0026quot;三部曲（225 步）。先调研\u0026quot;为 dsh 开发一个可以看图的插件\u0026quot;的实现方案，结论是两条路：\nmacOS Vision 做 OCR：毫秒级，返回文本+像素坐标，任何模型都能消费，不花 token； Ollama 本地 VLM 看图：默认 qwen2.5vl:3b，中文描述，离线可用。 为了验证\u0026quot;给 Mac 装识图小模型可行么\u0026quot;，专门装 Ollama 跑了一轮基准测试，确认这台电脑带得动。最终交付 ocr_image + view_image 两个工具，12 个 commit，TDD 全绿。这轮的感悟是：能本地跑就本地跑，能用系统能力就不用大模型——OCR 这种高频操作，用系统 API 一毫秒搞定，何必去烧 API 额度。\nDeepSeek API 用量监控：让 dsh 学会\u0026quot;记账\u0026quot; # 这个会话最久（10 轮、347 步），需求也最\u0026quot;产品化\u0026quot;：实时显示 DeepSeek API 的 token 消耗、金额花费和账户剩余金额。中途的迭代很能说明 agent 协作的节奏：\n先做出来 → 要求\u0026quot;显示位置像上下文组件，集成进一个按钮\u0026quot;； 再要求\u0026quot;展示今天的消费、按模型拆分、最近 7 天趋势\u0026quot;； UI 优化：参考上下文组件，能用图标就不写字，v4 flash / v4 pro 做成简称； 最后一轮返工：\u0026ldquo;最近 7 天的金额算得不对，你是怎么计算的？\u0026quot;——账目核对到 ¥61.85，一分不能差。 飞书互通 + 配置迁移：打通边界 # 飞书互通插件（70 步）：把 dsh 的会话结果和待审批消息通过 feishu-dsh-bridge 推到飞书，同时能接收飞书消息回来执行——让 dsh 变成了一个可以远程遥控的 agent。配置迁移插件则是一键把 claude / pi / codex / workbuddy / qcoder 的配置搬到 dsh（这个会话 8 月 15 日刚开，还是子代理在推进）。\n另外还有个没打完的仗：给 dsh 配置 GitHub MCP（92 步），最后卡在 loader 的插件实例语法上，会话结束时的原话是\u0026quot;查 loader 源码里正确的『新增插件实例』语法\u0026rdquo;——给 agent 配 MCP 也需要查源码，这很真实。\n主线三：跨会话通信，完整跑了一遍\u0026quot;开发流水线\u0026quot; # 要说这两天最有价值的一件事，是一个 27 轮、185 步的调研会话，第一句话是：\n\u0026ldquo;如果想要实现在 dsh 中的跨会话通信，我该怎么做？\u0026rdquo;\n从这个问题长出了一个独立仓库 dsh-courier。架构并不复杂，但很扎实：\n模块 职责 Mailbox 信箱 JSONL 持久化的消息队列，重启不丢 RoleRegistry 角色注册表 角色到会话的持久化映射 Courier 投递服务 唤醒、排队、补投、FIFO 工具三件套 courier_send / courier_register / courier_list 角色协议 prompts 按角色注入系统提示词 真正有意思的不是架构，是过程。这次走了自研的 dev-pipeline 流水线：8 个任务，每个任务一个\u0026quot;实现子代理\u0026quot;配一个\u0026quot;评审子代理\u0026quot;，评审发现问题就派\u0026quot;修复子代理\u0026quot;，最后整支终审再修一轮。44 个会话里 20+ 个是这条流水线的子代理，每个子代理都有独立的会话记录——这就是为什么会话数这么多。\n流水线的效果是实打实的：终审阶段真的揪出了两个隐藏 bug——给自己发消息（self-send）没有拦截、角色重绑后旧的逆向映射没清理。修复会话先写回归测试复现 RED，再修到 GREEN，最后 40/40 全绿、10 个 commit 收尾。子代理的 review 不是走过场，它是真能抓到问题的那种。\n主线四：生活类的自动化 # 除了写代码，dsh 还被用来干了不少\u0026quot;生活杂活\u0026quot;：\n考勤打卡（133 步）：先让 dsh 查飞书文档总结考勤规则，再把打卡绑定到对应的命令行，最后更新打卡时间窗口（上班 08:5009:30、下班 18:0021:00，闭区间含边界）。期间它还试图帮我办进京证……连进京证 API 都接上了。 持仓分析文档：把\u0026quot;纳指分批止盈 1/3~1/2\u0026quot;这种拍脑袋表述，修正成\u0026quot;按目标仓位再平衡\u0026quot;，止盈资金去向、溢价率执行条件写得明明白白——让 agent 帮忙复核投资结论，它给的逻辑比原文档严谨。 观察：两天下来的一些总结 # 会话即日志。~/.dsh/sessions 下的 JSONL 事件流记录了 agent 的每一步，复盘成本几乎为零。给团队写周报？先解压会话文件。 插件的核心是注入。dsh 插件能注入 tools、agents、sessions、systemPrompt，能力边界一下子就打开了——从\u0026quot;对话\u0026quot;变成\u0026quot;可编程的平台\u0026quot;。 本地优先。OCR 用系统 Vision、看图用 3B 小模型，能省 token 的地方绝不动大模型；API 用量监控插件上线后，每一分钱都看得见。 流水线化开发可行。实现 + 评审 + 修复的子代理循环，TDD 红线先行，质量有保障，规模可复制。8 个任务并行推进，两天交付一个完整的 npm 插件。 信任是逐步建立的。8 月 15 日中午，我把 dsh 的审批策略从 ask 改成了 never——会话日志里清晰地记录着这一刻。从\u0026quot;每步都要问\u0026quot;到\u0026quot;放手去干\u0026quot;，是 agent 从玩具变成工具的标志。 结语 # 两天，44 个会话，2324 步。回头看，dsh 给我的不是\u0026quot;自动补全代码\u0026quot;，而是一个可以无限并行的开发团队 + 生活助理：桥接、插件、流水线、打卡、记账，全都发生在同一天。\n如果你也想看看自己的 agent 都干了什么，其实很简单：\nunzstd ~/.dsh/sessions/*/*/session.jsonl.zstd | jq -r \u0026#39;.type\u0026#39; | sort | uniq -c 下一篇文章，准备把 dsh-courier 的架构和子代理流水线拆开细讲，先挖个坑。\n","date":"2026-08-15","externalUrl":null,"permalink":"/posts/deepseek-harness-shi-zhan-fu-pan/","section":"文章","summary":"用脚本把 dsh 本地全部 44 个会话翻了个底朝天：141 轮对话、2324 步执行，复盘了 pi 到 dsh 的飞书桥接迁移、五个自研插件、一次完整的子代理开发流水线，以及那些值得记下来的坑。","title":"DeepSeek Harness 实战复盘：44 个会话里的桥接、插件与自动化","type":"posts"},{"content":"","date":"2026-08-15","externalUrl":null,"permalink":"/tags/%E5%A4%8D%E7%9B%98/","section":"标签","summary":"","title":"复盘","type":"tags"},{"content":"","date":"2026-08-15","externalUrl":null,"permalink":"/categories/%E6%80%BB%E7%BB%93/","section":"分类","summary":"","title":"总结","type":"categories"},{"content":"","date":"2026-08-15","externalUrl":null,"permalink":"/tags/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91/","section":"标签","summary":"","title":"插件开发","type":"tags"},{"content":"","date":"2026-08-15","externalUrl":null,"permalink":"/posts/","section":"文章","summary":"","title":"文章","type":"posts"},{"content":"","date":"2026-08-12","externalUrl":null,"permalink":"/tags/github-pages/","section":"标签","summary":"","title":"GitHub Pages","type":"tags"},{"content":"","date":"2026-08-12","externalUrl":null,"permalink":"/tags/hugo/","section":"标签","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"2026-08-12","externalUrl":null,"permalink":"/tags/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/","section":"标签","summary":"","title":"个人网站","type":"tags"},{"content":"📌 本文为建站复盘原创文章，发布于本站：haifeiWu.github.io\n说来惭愧，从 \u0026ldquo;想搭一个个人博客\u0026rdquo; 到 \u0026ldquo;博客真的跑起来了\u0026rdquo;，中间隔了整整一个 2026 年的夏天。\n缘起：为什么会有这个站 # 程序员嘛，总得有个自己的地盘。\nGitHub 主页的 README 用了一年多，摆着技术栈徽章、统计卡片、精选项目，倒也算体面。但 README 终究是 README——它装不下几十篇文章，也撑不起一个\u0026quot;作品集 + 博客\u0026quot;的门面。恰逢这段时间在掘金和代码星冰乐写了不少东西，散落在各个平台，总觉得缺一个自己说了算的地方。\n于是决定：搭一个 GitHub Pages 上的个人站点，把文章全部搬过来，以后首发都在这。\n技术选型：为什么是 Hugo + Blowfish # 选型时其实没纠结太久：\nHugo：Go 写的静态站点生成器，构建快到离谱（87 篇文章本地构建也就一两秒），而且咱本身就是写 Go 的，亲切。 Blowfish：Hugo 生态里非常成熟的主题，内置了 Projects（项目展示）、i18n（多语言）、明暗主题、SEO 全套，基本开箱即用。 定了之后就一个目标：中英双语，作品集 + 博客混合，全部文章带过来。\n内容迁移：87 篇文章是怎么搬过来的 # 这是整个建站过程中最耗时、也最\u0026quot;有意思\u0026quot;的部分。我一共迁移了两拨内容。\n第一波：掘金 64 篇 # 掘金的文章列表 API 是公开的，翻页拉元数据很顺利。但文章详情 API 需要登录态，直接调不通。绕路方案：直接抓文章页面，从页面的 Nuxt 数据里把 web_html_content 抠出来。\n新文章好办，字段就在明面上；老文章（2019 年那批）web_html_content 是 null，内容被压缩在 window.__NUXT__= 这个表达式里。咋办？用 node 把表达式 eval 出来，再一层层剥到 article_info.content。\n再就是限流。掘金对高频抓取不太友好，重试 + 延迟 + 断点续跑，折腾了一晚上，总算 64 篇全须全尾地进了 content/posts/。\n第二波：代码星冰乐 23 篇 # 代码星冰乐是我和朋友合写的老博客，我是其中一个作者。这波迁移有几个记忆点：\n双作者筛选：站点上 91 篇文章，作者标签是 haifeiWu 的有 64 篇，其中有 41 篇和掘金迁移过来的重复，最终新增 23 篇。 Hexo 的代码块：老博客是 Hexo，代码渲染成\u0026quot;行号表格\u0026quot;结构（\u0026lt;table\u0026gt;\u0026lt;td class=\u0026quot;gutter\u0026quot;\u0026gt;…\u0026lt;td class=\u0026quot;code\u0026quot;\u0026gt;…），直接转 markdown 会碎成一地 HTML。写了个预处理，先把表格还原成 \u0026lt;pre\u0026gt;\u0026lt;code\u0026gt;，再交给 pandoc 转。 图床没了：最扎心的一环。老博客的图片挂在 img.hchstudio.cn（DNS 已删）和七牛云老空间（已失效），试了 Wayback Machine，被限流到怀疑人生。最后忍痛把图片去掉，只保留图注文字——文字、代码、表格都在，图丢了，也算是给\u0026quot;云端的东西不归你管\u0026quot;交了一笔学费。 踩坑记：印象深刻的几个坑 # 建站全程踩坑无数，挑几个有代表性的记录一下，给后来人排雷。\n坑 1：主题的 CSS 是预编译的，自定义类会被\u0026quot;裁剪\u0026quot; # Blowfish 的 CSS 是主题发布时预编译好的，Tailwind 扫描不到我站点 layouts/ 里写的类。结果就是：我写的\u0026quot;请我喝杯咖啡\u0026quot;弹窗，点击后在屏幕外渲染——类没了，样式全塌。排查了半天，最后在 assets/css/custom.css 里手动补齐缺失的 Tailwind 工具类，问题解决。\n教训：改主题之前，先搞清楚它的构建产物是不是\u0026quot;成品\u0026quot;。\n坑 2：Pages 的 Source 设成了 Deploy from a branch，Hugo 站点变 404 # 这是最惊险的一个。第一次部署后站点正常，第二次 push 之后整个站点 404。查了半天：Pages 的构建方式被设成了 Jekyll（Deploy from a branch），GitHub 直接拿 Hugo 源码去跑 Jekyll，构建出来的自然是空壳。在仓库 Settings → Pages 里把 Source 改回 GitHub Actions，站点立刻恢复。\n教训：GitHub Actions 部署的站点，Source 必须是 GitHub Actions。\n坑 3：菜单配置文件会\u0026quot;隐式定义语言\u0026quot; # exampleSite 残留了 7 种语言的 languages.*.toml 和 menus.*.toml，结果站点悄悄构建出了 8 种语言（连德语都有）。删掉 menus.de.toml 这类文件后，语言才只剩 en / zh-cn。\n坑 4：文章归属默认语言，中文站一篇都没有 # Hugo 多语言站点里，不带语言后缀的 index.md 默认归属 en。于是 /posts/ 一直是空的——明明 87 篇文章都是中文内容。\n解决方案：给每篇文章生成一个 index.zh-cn.md stub（只带 front matter + translationKey），再用一个 include-post 短代码，在中文页里渲染英文页的内容。文章内容只存一份，两个语言站共用。\n坑 5：掘金图床有 Referer 防盗链 # 文章图片从掘金 CDN 加载时，带了 github.io 的 Referer 会被 403。解法是一行 meta：\u0026lt;meta name=\u0026quot;referrer\u0026quot; content=\u0026quot;no-referrer\u0026quot;\u0026gt;，图片立刻恢复。\n坑 6：主题作者留下的\u0026quot;彩蛋\u0026quot; # Blowfish 的 exampleSite 配置里有不少作者自己的东西：Buy Me a Coffee 组件（收款人还是主题作者）、Firebase 演示项目（阅读数永远 loading 的来源）、默认的 blowfish banner（社交分享图）。全部替换成了自己的：微信收款码弹窗、自制的 OG 图。\nSEO / GEO：让搜索引擎和 AI 都能读懂 # 内容搬完了，还做了一轮可见性优化：\n结构化数据：Person / Article / BreadcrumbList JSON-LD，作者、日期、关键词齐全 OG 分享图：自制的 1200×630 品牌图，微信/社交分享不再显示主题作者横幅 llms.txt：给 AI 引擎（ChatGPT / Perplexity / 文心）的站点摘要，站里放一份，AI 就能更好地\u0026quot;读懂\u0026quot;我 hreflang + sitemap：多语言声明 + 自动生成的 sitemap，Google / Bing / 百度验证都已就位 一点总结 # 回头看看，这趟建站之旅最大的收获不是站点本身，而是把\u0026quot;发布内容\u0026quot;这件事重新掌握在了自己手里。\n几个比较深的体会：\n内容永远比平台重要。掘金的文章说删就删（老文章连 API 都不给你），云存储的图说没就没。自己的站点 + 自己的仓库，东西才是自己的。 读源码胜过读文档。主题的坑、Pages 的坑，最后都是靠翻模板源码、看构建产物才定位到的。 自动化是安全感。push 即部署，git push origin main 之后站点自动上线，再也不用记\u0026quot;部署流程\u0026quot;。 最后，站点在 haifeiWu.github.io，欢迎来逛。如果你也在考虑搭自己的博客，希望这篇复盘能帮你少踩几个坑——毕竟，我已经替你踩过了。\n","date":"2026-08-12","externalUrl":null,"permalink":"/posts/cong-0-dao-1-yong-hugo-blowfish-da-jian-ge-ren-bo-ke-jian-zhan-fu-pan/","section":"文章","summary":"从 GitHub 主页 README 到个人博客与作品集，记录了 Hugo + Blowfish 建站、掘金与旧博客 87 篇文章迁移、GitHub Actions 自动部署的全过程，以及那些让人印象深刻的坑。","title":"从 0 到 1：用 Hugo + Blowfish 搭建个人博客（建站复盘）","type":"posts"},{"content":"","date":"2026-08-12","externalUrl":null,"permalink":"/tags/%E5%8D%9A%E5%AE%A2/","section":"标签","summary":"","title":"博客","type":"tags"},{"content":"📌 本文原发布于掘金社区：Hadoop MapReduce：一个十年回顾与未来趋势（论文综述）\n摘要： 在过去的十年中，Hadoop MapReduce 成为了处理大规模数据集的主流框架。它通过简化复杂数据处理任务，并提供高度可扩展、容错性强且成本效益高的计算解决方案而获得成功。本文对 Hadoop MapReduce 自诞生以来在学术界和工业界中的发展进行了全面回顾，并探讨了其架构演变、优化策略、应用场景以及未来发展趋势。\n关键词： Hadoop，MapReduce，大数据，分布式计算，未来趋势\n引言 # 在当今数据驱动的时代，大规模数据处理不仅是一项技术挑战，也是商业和科研领域取得突破的关键。过去，传统的数据库系统和计算框架难以应对海量、多样化且持续增长的数据集——这种现象我们通常称为“大数据”。就在这个背景下，MapReduce 模型应运而生，并迅速革新了我们处理巨量信息集的方式。\n最初由 Google 提出并实现，在 2004 年首次公开发表后，MapReduce 模型通过其简洁而强大的设计理念吸引了广泛关注。它将复杂的计算任务分解为两个主要阶段：Map（映射）阶段负责处理输入数据并生成中间键值对；紧随其后的 Reduce（归约）阶段则负责合并这些中间结果并输出最终值。此模式之所以具有革命性，在于它使得大规模并行计算变得简单易行，并能够扩展到成千上万台机器上执行。\n然而，Google MapReduce 论文发布后不久，Hadoop 便作为一个开源实现诞生了。由 Apache 软件基金会支持和维护，Hadoop 让 MapReduce 框架及其配套生态系统如 HDFS（Hadoop Distributed File System）得到更广泛的采用与发展。作为一种可靠、可伸缩且成本效益高的解决方案，Hadoop 已经成为许多组织进行大规模数据处理不可或缺的工具。\n本文旨在深入探讨自 MapReduce 概念问世以来所经历过的显著变迁，并分析 Hadoop 如何发挥至关重要的作用。文章将审视这些技术如何塑造当前行业实践，并预测未来可能出现哪些转变。通过案例分析、性能评估和市场趋势预测等内容，本研究将提供一个全面视角来理解 MapReduce 及相关技术如何推动了当今社会信息处理方式的进步，并对未来产生深远影响。\nHadoop MapReduce 概念与原理 # 基础架构 # HDFS（Hadoop Distributed File System）和 MapReduce 是 Apache Hadoop 框架的两个核心组件，它们共同解决了存储和处理大规模数据集的问题。下面将详细描述这两个组件及其协同工作方式。\nHDFS: Hadoop Distributed File System # 设计初衷： 在大数据时代，传统的文件系统无法有效地存储和管理海量数据。HDFS 应运而生，目标是提供一种可靠、高吞吐量的分布式文件存储系统，能够跨越成百上千台服务器存储超大规模数据集，并且具备良好的容错性。\n主要特点：\n分布式存储： HDFS 将大文件分割为固定大小的块（block），默认情况下每个块大小为 128MB 或 256MB，并且跨多台机器进行分布式存放。 容错性： 每个块会有多个副本存在于不同节点上，默认是三份。当某节点出现故障时可以从其他节点获取副本。 高吞吐量访问： 系统设计优化了批处理操作，保证了在读写巨型文件时能够获得高吞吐率。 硬件成本低廉： 由于其设计用于普通商用硬件上，因此降低了建立和维护大型计算集群的成本。 MapReduce # 设计初衷： MapReduce 框架旨在简化并行计算过程。它允许开发者编写并行算法来处理被切片后分散在整个集群中的数据，而不必关注底层实现细节如任务调度、容错以及节点间通信等。\n主要特点：\n抽象化并行计算： 开发者通过实现map和reduce这两种类型的函数来表达复杂逻辑，而无需直接管理并发或者通信问题。 可扩展性： MapReduce 可以水平扩展到数以千计的机器上，适应不断增长的数据处理需求。 容错与自动恢复： 当某些任务执行失败或机器出故障时，系统会自动重试失败任务直至成功完成。 协同工作方式： # 当配合使用时，HDFS 负责提供一个稳定可靠的环境来持久保存用户数据，而 MapReduce 则利用这些数据执行并行计算任务。\n用户程序首先将输入文件上传到 HDFS 中，文件被切割成多个块并跨集群进行复制与保存。 当运行 MapReduce 作业时，map 阶段启动，并对每一个输入块执行映射操作（map tasks），生成键值对（key-value pairs）作为中间结果。 这些中间结果再经过排序（shuffle）与合并（reduce tasks），最后由 reduce 阶段汇总整理并输出最终结果。 输出结果再次写回到 HDFS 之中供进一步使用。 通过这样密切配合，HDFS 与 MapReduce 共同解决了海量信息存储与快速、有效地对这些信息进行批量处理的问题。\n核心组件 # 在 Hadoop 框架中，HDFS 和 MapReduce 组件由几个关键元素组成，它们承担不同的职责以确保系统的整体运行效率和稳定性。\nHDFS 中的基本元素 # NameNode:\nNameNode 是 HDFS 的主服务器，并且是一个高可用性系统。 它存储文件系统的元数据，比如目录树、文件属性（如权限、修改日期等）以及每个文件块列表和块所在的 DataNode 等。 它不存储实际数据或数据集本身，而只是管理对文件系统的访问操作。 DataNode:\nDataNodes 通常有很多个，分布在不同机器上，负责存储实际用户数据块。 它们根据 NameNode 指示来创建、删除和复制数据块。 每个 DataNode 周期性地向 NameNode 发送心跳信号和 Blockreports，表明它正在运行并报告其上所有块的状态。 MapReduce 中早期版本（1.x）基本元素 # JobTracker:\nJobTracker 位于主节点上，在早期版本的 MapReduce 作业调度器中起着核心作用。 它负责接收应用程序提交的作业，并将工作安排到各个 TaskTracker 节点上执行。JobTracker 监控所有 TaskTrackers， 重新执行失败任务，并提供信息给用户或其他应用程序。 TaskTracker:\nTaskTrackers 运行在从属节点上， 负责执行由 JobTracker 分配下来的任务。每一个 TaskTracker 都能够并发执行多个任务。 TaskTrackers 也会定时向 JobTracker 发送心跳信号表明他们还活着，并带有其当前任务的进度信息或完成情况。 这种架构虽然简单易懂但存在一些问题：\n扩展性受限：当集群规模增长时，单点故障风险增加且性能瓶颈更为显著。 资源利用效率低：固定划分资源给 map 与 reduce 两类任务导致无法灵活应对各种工作负载。 YARN 引入后改进 # 为了克服以上缺点，Hadoop 2.x 引入了 YARN（Yet Another Resource Negotiator），大幅改善了资源管理：\nResourceManager (RM): RM 取代了 JobTracker 成为新版 Hadoop 集群的资源管理与调度者。它包含两部分：\nScheduler: 只负责按需将资源（如 CPU 内存）分配给各个运行的应用程序但不保证容错。 ApplicationManager: 负责接收作业提交并协调第一阶段即 ApplicationMaster 的启动。 ApplicationMaster (AM):\n每当一个应用被提交至 YARN 时，便会启动与该应用对应的专属 AM 实例。AM 需要向 RM 申请自己所需的资源，并与之沟通获取足够的计算节点（NodeManager）来进行具体计算。\nNodeManager (NM): NM 替代了 Tasktracker，变成从属节点的代理角色。它们管理单机计算资源使用情况并监控容器（Container），给予必要支持使得 ApplicationMaster 可以利用这些容器执行特定计算任务。\n通过 YARN，Hadoop 现在可以更好地支持多租户（multi-tenancy），更有效地使用集群资源，并支持除 MapReduce 之外更多类型的框架（比如 Spark 等）同时运行在同一环境下。\n工作机制 # MapReduce 是一种编程模型，用于大规模数据集（大量数据）的并行运算。其主要分为三个步骤：Map、Shuffle 和 Reduce。下面我将用文字描述这三个步骤的执行流程。\nMap 阶段：\n输入切片（Input Split）：原始数据被分割成多个小块，这些小块称为“输入切片”。每一个输入切片会被分配给一个 Map 任务。 键值对生成（Key-Value Pairs Generation）：每个 Map 任务处理它所获得的输入切片，并且对该切片内的数据进行处理，通常是解析成键值对（key-value pairs）。例如，在处理文本时，可以将单词映射成数量 1。 Shuffle 阶段：\n分组与排序：来自所有 Mapper 的输出根据键（key）进行排序和分组。所有相同键的值都会聚集在一起形成一个列表。 Shuffle 过程确保了相同键的所有值都传递到同一个 Reducer 上去进行后续处理。 Reduce 阶段：\n聚合操作：Reducer 接收到各自关联键（key）下所有值（value）列表后，执行聚合操作。例如，在单词计数场景中，Reducer 会计算每个单词出现次数之和。 由于目前无法直接展示图表，请想象以下流程：\n数据集被拆分为多个小块（input splits），然后这些小块并行地通过不同的 Mapper 函数； 每个 Mapper 读取它们各自的输入，并产生一系列中间键值对（key-value pairs）； 这些中间结果经过系统自动排序（shuffling），以确保相同 key 下所有 value 能够汇总到一起； Reducer 按照 key 接收到 value 集合，并对这些 values 执行 reduce 操作； 最终结果输出并存储。 整体来说，MapReduce 允许开发者通过简单地编写 Map 函数和 Reduce 函数就可以在大量机器上并行处理海量数据。Shuffle 阶段作为桥梁，在 Mapper 产生输出和 Reducer 准备接受这些输出之间起着至关重要的作用——确保正确分配与排序以便于高效归约（reduce）。\n发展历程与里程碑事件 # 自 Google 在 2006 年发布了其 MapReduce 编程模型的白皮书以来，这一模型对大数据处理产生了深远的影响，并催生了多种技术和工具。以下是从 2006 年至今，MapReduce 及相关技术领域中一些重要更新点和行业影响事件的时间线：\n2006 年\nGoogle 发布 MapReduce 白皮书，详细描述了这个用于大规模数据处理的编程模型。 2008 年\nApache Hadoop 成为 Apache 顶级项目。Hadoop 是一个开源框架，它实现了 MapReduce 模型，并能够在普通硬件上运行。 2009-2010 年\nHadoop 开始获得企业界的广泛关注和应用。 Cloudera 成立，作为第一家商业化 Hadoop 解决方案的公司。 2011 年\nApache Hadoop 1.0.0 发布。这是 Hadoop 第一个稳定版本的发布。 2012 年\nApache Hadoop 2.0 引入 YARN（Yet Another Resource Negotiator），为不同类型的计算提供资源管理和调度。这标志着从单一用途（批量处理）到多用途平台（包括实时处理）的转变。 2013 - 2014 年\n出现各种优化 Hadoop 生态系统，或替代 MapReduce 作为主要计算引擎的尝试，如 Apache Tez、Apache Spark 等。 2014 - 2015 年\nApache Spark 开始流行起来，并被认为是比传统 MapReduce 更高效、更易于使用的大数据处理框架。Spark 可以在内存中进行迭代计算，而不仅仅局限于磁盘 IO 操作。 后续几年\n大数据技术持续发展与创新：例如 Apache Flink、Amazon Redshift 等服务相继问世并获得市场青睐。 至于“最新版本”的信息可能会随时间变化而更新，请参考各自项目或软件当前官方网站提供最新消息及版本说明文档获取最准确信息。\n需要注意，在过去十几年里，传统 MapReduce 虽然在形式上基于磁盘 I/O、强调批量处理，具备强大的批量处理能力，但延迟较高、编程复杂，因此在很多实际应用场景中，它已经被更灵活、更快速、更易于使用且支持流式计算等复杂分析任务的分布式计算框架所取代或补充。\n技术优化与创新 # 性能优化策略 # 在现代计算环境中，针对输入/输出（I/O）效率、网络传输速度和计算性能的优化是确保系统整体性能的关键。以下综述将详细探讨这些领域中的优化技术，包括压缩技巧和调度算法等。\nI/O 效率优化 # 数据本地化（Data Locality） : 数据本地化策略旨在最小化数据移动，通过在数据存储位置执行计算任务来降低网络 I/O 开销。MapReduce 框架就是一个经典例子，它尽可能将处理逻辑移至靠近数据源头的节点上。 压缩技术（Compression Techniques） : 压缩可以显著减少需要读写的数据量。例如，Snappy 提供了快速压缩和解压功能而不牺牲太多压缩比；GZIP 虽然解压较慢但提供更高的压缩比；LZ4 则平衡了速度与压缩比。 批处理（Batching） : 批处理是将多个小型 I/O 操作合并为较大批次以减少调用次数从而降低总体开销的过程。 高效文件格式（Efficient File Formats） : 列式存储格式如 Apache Parquet 或 ORC 可针对分析查询进行优化，并且通常与字段级别的压缩结合使用以进一步提升效率。 预取（Prefetching）与内存缓存（Caching） : 预取指将即将被访问的数据预先加载到内存中；而内存层面上对热点数据进行有效管理同样至关重要。例如，在分布式系统中广泛使用 LRU（最近最少使用）策略作为一种普遍采纳的页面替换算法来实现此目标。 网络传输速度 # 带宽管理（Bandwidth Management）与 QoS（Quality of Service） : 通过监控和分配网络资源来保证关键任务有足够带宽，并可能实施 QoS 策略以确保重要应用程序性能不受影响。 流量整形（Traffic Shaping）与调度（Scheduling）策略：这些技术可以帮助避免或缓解网络拥塞。例如 TCP 拥塞控制机制就是一个典型例子，它通过动态调整窗口大小来适应网络状态变化。 序列化/反序列化开销：序列化过程会增加 CPU 负载及额外网络负荷。因此选择如 Protocol Buffers、Apache Thrift 或 Avro 等高效序列化框架可以降低这方面的开销。 远程直接内存访问（RDMA）：RDMA 支持从服务器直接读写其他服务器内存而无需 CPU 介入，可显著降低延迟并节省 CPU 资源，对于某些高性能计算场景非常有益。 计算性能 # 并行处理（Parallel Processing）\u0026amp; 多线程（Multithreading）: 在多核心架构下利用并行编程模型如 OpenMP，MPI 或者基于事件驱动模型如 Node.js 进行编码可以有效利用硬件资源提升运行速率。 2 . 向量运算（Vector Operations）\u0026amp; GPU 加速（GPU Acceleration）: SIMD（Single Instruction Multiple Data）指令集使得单个指令操作多组数据成为可能；而 GPU 则专门设计用于大规模并行处理图形和数学运算。\n3 . 异步 I/O（Asynchronous I/O）\u0026amp; 非阻塞 I/O（Non-blocking I/O）: 异步 I/O 允许程序在等待 I/O 完成时继续执行其他任务，从而提高资源利用率和吞吐量。\n4 . 任务调度算法（Scheduling Algorithms）: 在多任务环境下，如何合理分配 CPU 时间片对性能至关重要。常见的调度算法包括先来先服务（FCFS）、短作业优先（SJF）、轮转（Round Robin）等。\n5 . 数据结构与算法优化：根据具体需求选择合适的数据结构和算法对于性能有着直接影响。例如，在内存受限情况下使用空间效率高的数据结构或者改进计算复杂度较高的算法。\n6 . 编译器优化（Compiler Optimizations）: 现代编译器提供了多种优化策略，例如循环展开（loop unrolling），公共子表达式消除（common subexpression elimination），死代码删除（dead code elimination）等。\n7 . 资源隔离与限制（Resource Isolation and Limitation）: 在虚拟化或容器技术中通过 cgroups，namespaces 等机制为不同应用程序划分资源可以有效避免单个应用消耗过多资源影响整体系统稳定性。\n资源管理改进（YARN） # YARN（Yet Another Resource Negotiator）是 Hadoop 2.0 引入的资源管理层，用于替代原来的 MapReduce 作为计算框架。它允许多种计算框架如 MapReduce、Spark 等共享同一资源池，并有效提升系统利用率。下面将从几个关键方面来剖析 YARN 是如何实现这些功能的：\n1. 资源抽象和统一管理 # 在 YARN 中，所有的计算资源（如 CPU、内存、磁盘 I/O 等）都被抽象成一个统一的资源池，不同的应用程序可以根据自己的需求从中申请资源。\nResourceManager（RM）：负责整个集群资源的管理和分配。 NodeManager（NM）：运行在集群中每个节点上，负责监控其节点上容器（Container）的使用情况并向 ResourceManager 报告。 2. 应用程序多样性与兼容性 # YARN 设计之初就考虑了对不同类型计算框架的支持。它通过定义 ApplicationMaster 通用接口来实现这一点。\nApplicationMaster（AM）：每个应用程序都有自己专属的 AM，它负责协调 ResourceManager 为当前应用程序分配所需资源，并与 NodeManager 通信以启动和停止容器。 3. 资源隔离 # 通过引入“容器”概念，YARN 能够在物理服务器上创建隔离环境以运行各种应用程序进程。\nContainer：封装了特定数量的 CPU 核心、内存等计算资源信息，并可以执行指定任务。 4. 动态资源分配 # 相比 Hadoop 早期版本静态地划分 Map 和 Reduce 任务所需资源，在 YARN 中采取更灵活的动态方式进行资源分配：\n应用可根据需要而非固定比例申请更多或更少的 Resource Container。 ResourceManager 会基于当前系统负载情况和策略进行合理调度。 5. 可扩展性与高效利用率 # 由于 ResourceManager 对整个集群有全局视角，因此能够更加高效地进行全局优化以提升整体利用率：\n支持按优先级排队及基于策略调度（例如公平调度器 FairScheduler 或容量调度器 CapacityScheduler），确保不同队列或用户公平获取到系统资源。 6. 容错机制与稳定性 # 当某节点出现故障时：\nNodeManager 会周期性向 ResourceManager 报告健康状况。如果失联，则该节点上运行的 Container 将被转移至其他节点。 通过以上机制，YARN 成功地将数据处理工作从单一模式（MapReduce）转变为可以支持多种数据处理模式（MapReduce，Spark，Tez 等），从而大幅提高了集群利用率及灵活性。同时也使得 Hadoop 成为一个真正意义上可伸缩且适合企业级部署使用的大数据平台。\n应用领域案例分析 # MapReduce 是一种编程模型，用于大规模数据集（大于 1TB）的并行运算。它通过将任务分解为许多小块，让这些小块可在任何数量的计算机上并行处理，从而简化了分布式计算的复杂性。我们来看几个典型案例：\n搜索引擎 # 索引构建：Google 使用 MapReduce 来生成和处理其搜索引擎中使用的网页索引。Map 阶段用于处理原始网页数据，并提取关键词及其上下文信息；Reduce 阶段则负责合并这些信息，并构建一个倒排索引（inverted index），使得搜索查询能够快速定位包含特定关键词的文档。 PageRank 计算：PageRank 是 Google 用来衡量网页重要性的算法之一。它可以通过 MapReduce 实现，其中每个页面被视作一个节点，Map 阶段负责输出每个节点及其出链页面；而在 Reduce 阶段，则聚合所有指向同一个页面的入链信息以计算该页面的 PageRank 值。 社交网络分析 # 社交图谱挖掘：社交网络如 Facebook 和 Twitter 会利用 MapReduce 对复杂网络结构进行图谱挖掘、社区发现等操作。例如，在推荐好友功能中，可以通过 Map 步骤统计共同好友数量，并在 Reduce 步骤中对推荐列表进行排序。 情感分析：对用户生成内容进行情感分析以了解公众对某一话题或产品的看法也常常依赖于 MapReduce 模型。此时，原始用户评论数据会被映射成关键词及其情感倾向，在归约过程中统计各种情绪的表达频率。 金融风控 # 信用评分：金融机构可能使用 MapReduce 来处理大量客户数据以评估信用风险。在此过程中，客户历史记录作为输入数据，在映射过程中提取信用相关参数，在归约过程中汇总不同来源和时间点收集到的信贷历史。 欺诈检测：银行和支付公司利用海量交易记录进行欺诈检测时也会采取类似方法。例如，在映射阶段标记异常或可疑活动，并在归约阶段聚合相关事务以确认是否存在欺诈模式。 典型案例说明 # 以上所述案例展示了如何将复杂问题拆解为可以并行执行且更容易管理与理解的子任务：\n在搜索引擎领域内部署 MapReduce 可以有效地处理海量 Web 文档。 社交网络平台利用 MapReduce 来理解庞大且不断变化的用户生成内容。 金融服务公司依靠 MapReduce 处理跨越多年、涉及数百万客户账户和事务记录等庞大数据集。 由此可见，无论是面对结构化还是非结构化数据集，“切片”问题然后“归约”结果这样简单直观却强大有力的策略使得 MapReduce 成为云时代最强大且灵活应对各种场景需求不可或缺的工具之一。\n面临挑战与问题评估 # Hadoop MapReduce 是一个强大的分布式数据处理工具，但随着技术和业务需求的发展，它也面临了一些挑战。以下是一些主要挑战及其可能的解决方案：\n处理小文件问题 # 挑战：Hadoop 最适合处理大量数据。对于小文件，每个文件都是一个单独的输入分片（split），这会导致大量的元数据存储在 NameNode 上，并可能成为瓶颈。\n解决方案：\n合并小文件：在作业执行前使用 Hadoop 归档工具（如Har或SequenceFile）将多个小文件打包成更少、更大的文件。 自定义 InputFormat：创建一个自定义 InputFormat，将多个小文件作为单个分片来读取。 实时数据处理不足 # 挑战：MapReduce 设计用于批量处理，并不适用于需要低延迟响应的实时数据处理。\n解决方案和研究方向：\nApache Storm/Spark Streaming: 这些框架专为实时流式计算设计，可以与 Hadoop 集群配合使用。 Lambda 架构：结合使用批量处理和流式计算系统以满足实时和准实时应用程序的需求。 资源管理不够灵活 # 挑战：在 MapReduce 中，资源管理（CPU、内存）相对静态，不够灵活。\n解决方案和研究方向： # Apache YARN（Yet Another Resource Negotiator） : 替代传统 MapReduce 中资源管理器角色，提供更灵活高效的资源调度机制。 编程模型复杂性 # 挑战：对于非开发人员来说，编写 MapReduce 程序可能比较复杂且容易出错。\n解决方案和研究方向\n高层次抽象库如 Apache Pig 和 Apache Hive: 这些工具提供了 SQL-like 语言（HiveQL）或脚本（Pig Latin）来简化 MapReduce 编程任务。 数据局部性优化有限 # 当节点上没有输入数据副本可用时，数据必须从其他节点传输到正在运行任务的节点，这增加了延迟并降低了吞吐量。\n解决办法\n优化 HDFS 本身：改进 HDFS 的副本放置策略以增强局部性。 更智能地调度任务：根据可用性安排任务执行位置，减少跨网络移动。 通过这些建议与改进措施，可以克服现有问题并使得 Hadoop MapReduce 更好地适应当前快速变化且要求高效率、低延迟处理能力的环境。然而，在某些情景下还是可能需要考虑替代技术或新兴平台以满足特定需求。\n替代技术出现背景及对比 # Apache Spark 是在 Hadoop MapReduce 存在一定局限性的背景下出现的。Hadoop MapReduce 虽然在处理大规模数据集时非常有效，但它主要针对批量处理设计，在以下几个方面存在不足：\n速度：MapReduce 作业通常写入和读取磁盘，这意味着高延迟。对于需要快速迭代或交互式分析的任务来说，这种设计降低了效率。 实时处理：由于其批量处理本质，MapReduce 并不擅长实时数据分析。 复杂性：编写 MapReduce 程序相对复杂，并且调试起来也比较困难。 资源利用率：MapReduce 的资源调度不够灵活。 Apache Spark 应运而生，解决了上述很多问题：\n基于内存计算：Spark 使用了基于内存（RAM）的计算技术（RDDs - 弹性分布式数据集），可以比磁盘 I/O 操作快上百倍。 易用性：提供了 Scala、Java、Python 和 R 等高级语言的 API 接口，并支持 SQL 查询、流式计算、机器学习和图形处理等丰富功能。 通用框架：Spark 可以进行批量数据处理、交互式查询、实时流分析、机器学习和图形数据库等各种不同类型的数据工作负载。 优化资源管理：结合使用 Apache Mesos 或 YARN 使得资源管理更加灵活和高效。 性能方面 # Spark 在内存中执行大多数操作以减少磁盘 I/O 操作次数，显著提升了速度。尤其是在需要多次访问同一数据集进行操作（例如机器学习算法）时表现更佳。 对于只需单次遍历数据的简单任务而言，Hadoop MapReduce 可能与 Spark 表现相近，因为硬盘读写并非主要瓶颈。 易用性方面 # Spark 提供简洁易懂的 API 和支持多种语言编程接口，大幅降低开发者门槛。 同时它还支持即席查询（ad-hoc query）与交互式分析。 实时处理 # Spark Streaming 扩展了核心 API 来支持实时流数据处理，能够做到近乎实时地进行复杂运算。 综合来看，Apache Spark 在很多应用场景下都展现出比 Hadoop MapReduce 更优异的性能。然而，在某些特定情况下 —— 如当可用内存有限或者仅需对大型文件系统进行一次遍历 —— 原始 Hadoop MapReduce 可能仍然是一个可行甚至更好的选择。此外，Spark 的全面功能可能会导致其成为一个较重型系统；因此，在小型或资源受限环境中部署可能会面临挑战。\n未来发展方向预测 # MapReduce 作为一种编程模型，已经在大数据处理方面确立了其重要性。然而，随着云计算和机器学习的快速发展以及 Apache Spark 等新兴技术的出现，MapReduce 的应用场景和发展路径也在不断演变。\n云计算集成 # 即服务模式：Hadoop 和 MapReduce 可能会更多地以服务形式存在（例如 Amazon EMR），让用户无需管理底层基础设施就能运行 MapReduce 工作负载。 容器化与微服务：MapReduce 可能会进一步与 Kubernetes 等容器管理工具集成，来提高资源利用效率、简化部署流程，并支持微服务架构。 自动化与优化：通过集成到云平台中，MapReduce 的调度和优化可能会更加智能，利用机器学习对工作负载进行预测并相应地调整资源。 机器学习库支持 # 专门库的开发：虽然 Spark MLlib 等已经很受欢迎，但针对 MapReduce 的专门机器学习库可能会继续开发和优化。 深度学习集成：将深度学习框架如 TensorFlow 或 PyTorch 集成到 MapReduce 中可以扩展其在 AI 和 ML 应用中的适用性。 性能改进 # 内存计算：虽然 Spark 在内存计算方面表现出色，但未来可能出现新技术使得 MapReduce 能够更有效地处理内存操作。 实时数据处理：对于需要实时分析的场景，Hadoop/MapReduce 可以通过改进其生态系统（如 Apache Flink）来强化这方面能力。 数据湖与多模型数据库 # 随着数据湖概念（如 Delta Lake）和多模型数据库（如 Apache Cassandra）的崛起，在这些环境下运行 Hadoop/MapReduce 任务将变得更加常见。为了适应这样的环境：\n将需要提高它们对各种文件格式（包括非结构化数据）和存储系统的兼容性； 提供更好的元数据管理功能以及查询优化； 开源社区活跃度 # 尽管商业公司可能减少对 Hadoop 和 MapReduce 的投资，开源社区仍有潜力推动它们向前演进。社区可以帮助解决当前问题并引入新特性来保持相关性。\n总体上看，尽管现有趋势表明 Spark 等框架正在取代传统 Hadoop/MapReduce 解决方案，Hadoop/MapReduce 很可能不会完全消失。相反，它们将继续演变并找到自己独特且合适的市场定位 —— 特别是在那些已经投入使用且依赖于此类处理方式进行批量分析的工作流程中。\n结论 # MapReduce 在过去十年中对大数据和分布式计算领域做出了巨大的贡献，为处理海量数据集提供了一个可靠的编程模型。随着技术进步和新需求的不断涌现，我们可以总结以下几点：\n成就 # 扩展性：MapReduce 能够处理 PB 级别的数据，在节点增加时能保持良好的扩展性。 容错性：它可以在廉价硬件上运行，并且具有很强的容错能力。 简化编程模型：MapReduce 为开发者提供了一个相对简单的编程接口，使得并行计算更加易于理解和实施。 挑战 # 处理速度：面对需要快速迭代或实时分析的场景时，MapReduce 的批处理模型显得较慢。 资源利用率：由于其设计初衷是优化磁盘 I/O 操作而非内存操作，它在资源利用率方面可能不如新兴技术高效。 复杂性与灵活性：对于复杂任务流或需要多阶段管道作业（pipeline jobs）来说，编写和维护 MapReduce 程序可能会比较困难。 未来演化 # Hadoop 生态系统正在通过以下方式适应未来：\n更紧密地集成云服务：以支持动态伸缩、存储与计算分离等现代云特征； 支持多种数据处理范式：例如 Hadoop 生态系统中其他工具（如 Apache Hive，Apache Pig）已经提供了比纯粹使用 MapReduce 更高级别且易用的抽象； 配合现代硬件趋势：改进底层框架以充分利用 SSD、大内存等新硬件； 拥抱机器学习与 AI：通过整合 Spark MLlib、TensorFlow 等工具使得 Hadoop 可以高效地执行机器学习任务。 尽管面临诸如 Spark 等竞争者带来的挑战，Hadoop 生态系统仍然展现出适应变化、满足企业与社区需求并继续发展壮大的能力。因此我们可以期待它将在一定程度上保持其在大数据领域的核心位置，并根据市场需求不断进化。\n参考文献 # Dean, J., \u0026amp; Ghemawat, S. (2004). MapReduce: Simplified data processing on large clusters. In Proceedings of the 6th Conference on Symposium on Opearting Systems Design \u0026amp; Implementation - Volume 6 (OSDI’04). USENIX Association. Borthakur, D. (2007). The Hadoop distributed file system: Architecture and design. Hadoop Project Website, 11(2007), 21. Vavilapalli, V.K., Murthy, A.C., Douglas, C., Agarwal, S., Konar, M., Evans, R., Graves T., Lowe J., Shah H., Seth S., Saha B., Curino C,. O\u0026rsquo;Malley O,. Radia S,. Reed B,. Baldeschwieler E,. (2013). Apache Hadoop YARN: Yet Another Resource Negotiator. In Proceedings of the 4th annual Symposium on Cloud Computing (SOCC \u0026lsquo;13). Association for Computing Machinery. Zaharia M,, Chowdhury M,, Franklin MJ,, Shenker S,, Stoica I.. (2010) Spark: Cluster computing with working sets.. HotCloud'10 Proceedings of the 2nd USENIX conference on Hot topics in cloud computing.. Raghupathi W,, Raghupathi V.. (2018) A Survey of Big Data Analytics in Healthcare and Government.. Health Information Science and Systems,. ","date":"2024-06-18","externalUrl":null,"permalink":"/posts/hadoop-mapreduce-yigeshinianhuiguyuweilaiqushi-lunwenzongshu/","section":"文章","summary":"在过去的十年中，Hadoop MapReduce 成为了处理大规模数据集的主流框架。它通过简化复杂数据处理任务，并提供了高度可扩展、容错性强且成本效益高的计算解决方案而获得成功。","title":"Hadoop MapReduce：一个十年回顾与未来趋势（论文综述）","type":"posts"},{"content":"","date":"2024-06-18","externalUrl":null,"permalink":"/categories/%E5%90%8E%E7%AB%AF/","section":"分类","summary":"","title":"后端","type":"categories"},{"content":"","date":"2024-06-18","externalUrl":null,"permalink":"/tags/%E5%90%8E%E7%AB%AF/","section":"标签","summary":"","title":"后端","type":"tags"},{"content":"","date":"2024-06-18","externalUrl":null,"permalink":"/tags/%E5%BC%80%E6%BA%90/","section":"标签","summary":"","title":"开源","type":"tags"},{"content":"","date":"2024-06-18","externalUrl":null,"permalink":"/tags/%E6%9E%B6%E6%9E%84/","section":"标签","summary":"","title":"架构","type":"tags"},{"content":"📌 本文原发布于掘金社区：基于 WebSocket 的长连接服务的设计与实践\n业务场景 # 在常见的 pk 游戏的场景下，pk 双方需要实时感知双方的做题状态，因此长连接在这种场景下的应用极为常见，本文将基于此来讨论下长连接服务的设计与实现。\n实现方案 # 轮询 # 轮询是一种客户端定期询问服务器是否有可用消息的技术。根据轮询频率，轮询的成本可能很高。大多数轮询是无效的，QPS 会特别高，导致服务器资源的浪费。\n长轮询 # 在长轮询中，客户端保持连接打开，直到实际有新消息可用或连接超时。一旦客户端收到新消息，它会立即向服务器发送另一个请求，重新开始这一过程。长轮询有一些缺点：\n发送者和接收者不能连接到同一个聊天服务器。基于 HTTP 的服务器通常是无状态的。如果在分布式的场景下，接收消息的服务器可能与接收消息的客户端没有长轮询连接。 服务器没有很好的方法来判断客户端是否断开连接。 这是低效的。跟轮询的方案类似，如果用户不多，长轮询仍会在超时后进行周期性连接。消耗服务器资源。 长连接 # WebSocket 连接由客户端发起，它是一个全双工的长连接。它以 HTTP 连接开始，并且可以通过一次定义明确的握手“升级”到 WebSocket 连接。通过这种持久连接，服务器可以向客户端发送更新。\nWebSocket 可以实现实时通信，但是存在的问题是它是长连接，需要连接到具体的实例上，如果是客户端 a 与客户端 b 之间相互通信的话，就需要保证这两个连接在同一个实例上，因此就需要我们维护一套服务发现的机制实现不同长连接之间的通信。\n分布式长连接 # 维持大量的长连接对单台服务器的压力也挺大的，这里也就要求该服务需要可以扩容，也就是分布式地扩展。分布式对于可存储的公共资源有一套完整的解决方案，但对于 WebSocket 来说，操作对象就是每一个连接，它是维持在每一个程序中的。每一个连接不能存储起来共享、不能在不同的程序之间共享。所以我能想到的方案是让不同程序之间进行通讯。那么，怎样知道某个连接在哪个应用呢？下面两种方案我们来一一讨论下\nIP 直连 # 通过 ip + port 去判断。每个连接都保存在本应用，然后用对称加密算法加密服务器 IP 和端口，得到的值作为 client id。对指定 client id 进行操作时，只需要解密这个 key，就能得到相应的 IP 和端口。判断是否为本机，不是本机的话则通过 RPC 通讯告知相应的程序。长连接的连接数据不可迁移，程序挂掉了相应的连接也就挂了，在该程序上的连接也就断开了，这时重连的话会找到另一个可用的程序。\n时序图 # 单发消息\n客户端发送连接请求，连接请求通过 Nginx 负载均衡找到一台 ws 服务器； ws 服务器响应连接请求，通过对称加密服务器 IP 和端口号，得到的值作为 client id，并返回。 客户端拿到 client id 之后，交给业务系统； 业务系统拿到 client id 之后，通过 HTTP 发送相关消息，经过 Nginx 负载分配到一台 ws 服务器； 这台 ws 服务器拿到 client id 和消息，解密出对应的服务器 IP 和端口； 拿到 IP 地址和端口，通过 HTTP/RPC 协议给指定 ws 程序发送信息； 该 ws 程序接收到 client id 和信息，给指定的连接发送信息； 客户端收到信息。 群发消息\n前 3 个步骤跟单发的一样； 业务系统拿到 client id 之后，通过 HTTP 给指定分组发送消息，经过 Nginx 负载分配到一台 ws 服务器； 这台 ws 服务器拿到分组 ID 和消息，去 Redis 获取服务器列表，然后发送 HTTP/RPC 广播； 所有收到广播的 ws 长连接服务，找到本机所有该分组的连接，给所有这些连接发送消息； 客户端收到信息。 MQ 分布式方案 # ws-server：长连接接入层，可以通过服务发现组件实现长连接服务的无限横向扩展； nacos：这里作为服务发现组件来实现连接服务的横向扩展； push-server：无状态服务 push 层，理论上可以无限横向扩展； MQ：这里我们可以使用 Kafka 或者 RocketMQ，来实现消息的异步发送； push-consumer：push 消息发送层，负责 push 消息的投递。 时序图 # 单发消息\n客户端发送连接请求，连接请求通过 Nginx 负载均衡找到一台 ws 服务器； ws 服务器响应连接请求，将用户的 deviceid 或者 uid 与建立长连接的 ws 长连接服务器的 ip+port 存储到 Redis 或者 nacos 中，并将连接升级成 ws 长连接后返回。 业务系统就可以通过调用 push-server 的接口发送消息到指定的 deviceid 或者 uid，push-server 收到消息后将消息打包并发送给 mq，并返回发送成功； push-consumer 作为一个常驻任务，不停地消费 mq 中的消息，收到消息后，通过 deviceid 或者 uid 查询用户的长连接所在的服务，通过 HTTP 或者 rpc 调用将消息发送到指定的 ws 长连接服务； 该 ws 程序接收到信息，给指定的连接发送信息； 客户端收到信息。 群发消息\n前 2 个步骤跟单发的一样； 业务系统就可以通过调用 push-server 的接口发送消息到指定的房间 roomid，push-server 收到消息后，通过查询 Redis 获取到房间中的 uid 或者 deviceid，将消息打包并发送给 mq，并返回发送成功； push-consumer 作为一个常驻任务，不停地消费 mq 中的消息，收到消息后，通过 deviceid 或者 uid 查询用户的长连接所在的服务，通过 HTTP 或者 rpc 调用将消息发送到指定的 ws 长连接服务； 该 ws 程序接收到信息，给指定的连接发送信息； 房间中的所有客户端收到信息。 方案对比 # 轮询 VS 长连接 # 轮询 长轮询 长连接 分布式长连接 实现复杂度 低 低 高 较高 资源利用率 低 低 高 高 可扩展性 低 低 低 高 可复用性 低 低 高 较高 IP 直连 VS MQ 异步 # IP 直连 MQ 异步 实现复杂度 低 高 可复用性 业务定制 模块各司其职，通用性高 可扩展性 高 较高 ","date":"2024-06-18","externalUrl":null,"permalink":"/posts/jiyu-websocket-dezhanglianjiefuwudeshejiyushijian/","section":"文章","summary":"业务场景 在常见的 pk 游戏的场景下，pk 双方需要实时感知双方的做题状态，因此长连接在这种场景下的应用极为常见，本文将基于此来讨论下长连接服务的设计与实现。","title":"基于 WebSocket 的长连接服务的设计与实践","type":"posts"},{"content":"","date":"2024-06-18","externalUrl":null,"permalink":"/tags/%E7%A8%8B%E5%BA%8F%E5%91%98/","section":"标签","summary":"","title":"程序员","type":"tags"},{"content":"📌 本文原发布于掘金社区：千万级消息推送系统的设计与实践\n推送平台支持千万级的通知/消息推送，透传消息，快速触达用户，能够有效提升用户留存率、活跃度。推送平台提供了全链路的移动推送能力，只需接入推送平台的 API 就可以立即将推送消息送达到用户的移动设备。\n同时，推送平台还提供了操作便捷的网页端管理后台，方便业务方的老师，进行应用授权，业务管理，推送消息管理，数据大盘查看，方便接入时的调试。\n基本概念 # 什么是移动推送？ # 推送是指服务器定向将信息实时送达手机的服务，其中涉及到 “服务器” 和 “手机”，服务器为消息的发送者，手机为消息的接收者。“定向”和“实时”则说明推送可以指定接收者，并且需要实时送达。\n推送消息分类 # 通知消息 当通知消息到达时，操作系统将负责接收消息，手机的通知栏（状态栏）上会显示一条通知信息。通知主要用于提示用户，应用于新闻内容、促销活动、产品信息、版本更新提醒、订单状态提醒等多种场景。其在华为、小米、vivo、oppo、iphone 五种机型上能够在开机联网的情况下实时到达，不要求 APP 保活。\n透传消息 与通知消息不同，透传消息通常不会展示到通知栏上，通过长连接直接送达到应用，应用实时收到信息后解析消息内容，进而根据消息内容处理业务逻辑，透传消息主要用于应用内部业务逻辑，需要 APP 在线才可以接收到消息，通常需要 APP 保活。\n应用内消息就是通过透传消息将推送内容发送到客户端，当客户端收到消息后会解析消息内容，从而进行消息的自定义展示，同时也可以控制消息在指定页面的展示。\n推送基本原理 # 推送通常有三种实现方式：PULL、PUSH、SMS；\nPULL 方式：\n客户端使用 PULL（拉）的方式，客户端和服务器端不需要维持 TCP 长连接，客户端以一定的时间间隔不断查询服务端是否有新的消息，通常采用 HTTP 方式，但需要考虑轮询频率，频率太低可能导致消息延迟，如果太快，则会大量消耗网络带宽和电池。所以大多数推送服务都不会使用轮询方式。\nPUSH 方式：\n服务器使用 PUSH（推送）的方式，对于客户端来说是一种被动的方式，而主动权在服务端，客户端和服务器之间需要维持一个 TCP/IP 长连接，当有消息时，服务端会向注册到推送服务器的客户端推送消息。这种推送方式的优势是实时性强，客户端实现简单。当然也会有不足，如大量的客户端与服务端保持长连接会消耗服务器的资源，在未推送消息时，需要以一定的间隔发送心跳消息来维持连接，通常这种连接主要消耗的是内存资源。例如，200 万用户可能会消耗数 10GB 的内存。因此搭建这种推送机制时要使用性能好的服务器。这个方案可以解决由轮询带来的性能问题，但是会提高手机耗电。\nSMS 方式：\n在 Android 平台上，可以通过拦截 SMS 消息并且解析消息内容来了解服务器的意图，并获取其显示内容进行处理，这种方式可以归为 PUSH 方式，成本相对比较高，需要向移动公司缴纳相应的费用，基本上很少使用。\n目前主流的推送服务都是采用 PUSH 的方式，比如 iOS 平台的 APNS，Android 平台的 GCM，国内第三方推送平台个推，极光，友盟以及各大手机厂商自建的系统级的推送通道。\n功能介绍 # 提供多种推送功能，能够帮助移动业务精准，快速触达 C 端用户。\n多种推送形式和方式 # 提供了多种推送方式，可以满足不同的业务需求。\n从展示形式方面来说，我们提供了：\n通知栏消息。 应用内的透传消息。 点击跳转消息。 从推送方式来说，我们提供了：\n按用户推送。（支持一个用户多个设备） 按设备推送。（支持对一个用户的单个设备） 按模版推送。 按指定条件进行推送。 定时推送。 推送整体流程 # 客户端 sdk 经过网关的安全验证后连接到接入层服务（push-api）。 将从厂商处获得的 token 及 App 用户等信息绑定到服务端。此时会根据不同 App 的用户进行分库分表存储。 业务方调用推送平台提供的统一 API 发起推送请求。 将推送请求写入队列 Kafka。 消费服务根据业务方发送的 clientId 或者 userId 查找符合条件的设备。 将找到的设备推送给第三方推送平台。 第三方推送系统将消息推送给客户端。 厂商回调接入层服务触达接口，以完成触达率统计。 客户端 SDK 回调接入层点击接口，以完成点击率统计。 服务端设计 # 并创建 token，根据各家厂商的时效缓存应用 token。 提供统一的接口给业务方使用，屏蔽各家厂商底层推送细节。 由于厂商批量推送接口一次最多只能推送 1000 个用户，当出现全量推送的任务时，推送接口会按厂商将任务拆分成多个子任务。 再次消费子任务直接给厂商进行推送。 这时专门的推送模块消费到的子任务就是已经拼装好的各厂商的结构了。这里只负责推送并记录推送结果。供统计使用。 当端上收到推送消息后，再回调服务端的接口。 消息生命周期 # 业务侧 # 通道侧 # 规划与展望 # 不同 App 在厂商的活跃用户不一样，被限制发送的速率也不一样。需要增加主动退避的速率限制功能。 对接用户画像。由于移动推送中业务方用户的数据来源仅靠客户端 SDK 绑定，信息量较少。为业务方的运营老师更方便地在管理后台上圈选用户群体，我们正在与大数据同学不断迭代用户的画像标签。这样可以在移动推送的管理后台以更多的维度去圈选用户。 数据漏斗目前数据维度还不够丰富，计划添加用户，设备，第三方 token 维度。推送数据方面增加漏斗层级。更高效地监控查看推送数据。 将推送数据结合机器学习的模型，用以提高召回率。 自建长连接能力，提高用户的转化率。 接入微信的模版消息，丰富对 c 端通知能力的支持。 推送内容审核。 ","date":"2024-06-17","externalUrl":null,"permalink":"/posts/qianwanjixiaoxituisongxitongdeshejiyushijian/","section":"文章","summary":"推送平台支持千万级的通知/消息推送，透传消息，快速触达用户，能够有效提升用户留存率、活跃度。推送平台提供了全链路的移动推送能力，只需接入推送平台的 API 就可以立即将推送消息送达到用户的移动设备。","title":"千万级消息推送系统的设计与实践","type":"posts"},{"content":"","date":"2024-06-17","externalUrl":null,"permalink":"/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/","section":"标签","summary":"","title":"微服务","type":"tags"},{"content":"📌 本文原发布于掘金社区：golang 项目中的全链路追踪（tracing）\n是什么 # 链路追踪（Tracing）是一种技术，用于监视和记录计算机程序或系统的运行情况。在分布式系统中，追踪可以帮助开发者和运维人员理解多个组件是如何协同工作的，以及在处理请求时它们之间的交互情况。\n想象一下，你在一个大型购物中心里跟踪一个购物者的行为。这个购物者从进入购物中心开始，先后访问了不同的商店，比如服装店、电子产品店和食品店。在这个过程中，你可能想知道购物者在每个商店花了多长时间，他们是否遇到了任何问题，以及他们最终购买了什么商品。\n同样地，在计算机系统中，一个请求可能需要经过多个服务或组件来得到处理。追踪系统就像是一个高级的摄像头，记录下请求在系统中的每一个步骤，包括它访问了哪些服务、每个服务处理请求所花费的时间、以及在处理过程中是否有任何异常或错误发生。\n通过分析这些追踪数据，开发者可以发现系统的性能瓶颈，比如某个服务响应慢，或者某个环节出现了错误。这样，他们就可以针对性地优化系统，提高效率和稳定性。\n为什么 # 在分布式系统中使用追踪（tracing）的原因主要包括以下几点：\n复杂性管理：分布式系统通常由多个组件和服务构成，这些组件可能分布在不同的地理位置和服务器上。系统的复杂性随着组件数量的增加而呈指数级增长。追踪可以帮助理解和管理这种复杂性，通过记录和分析请求在系统中的流动路径，开发者可以清晰地看到每个组件的行为和性能。 性能优化：追踪提供了关于系统性能的详细信息，包括请求的处理时间、瓶颈位置和资源使用情况。这些数据对于识别性能瓶颈、优化资源分配和提高系统效率至关重要。 故障诊断：当系统出现问题时，追踪可以帮助快速定位问题的根源。通过分析请求的追踪数据，可以发现是哪个环节出现了问题，以及问题发生的原因，从而快速解决问题并恢复服务。 系统可见性：追踪增加了系统的可见性，使得开发者和运维人员能够理解系统的内部工作机制。这种可见性对于维护系统的健康状态和预测潜在问题非常重要。 总之，分布式追踪是理解和管理现代复杂分布式系统的关键工具。它不仅帮助开发者和运维团队保持系统的稳定和高效运行，还为业务增长和创新提供了支持。\n怎么做 # OpenTelemetry # OpenTelemetry 为我们在系统中更轻量化地接入 tracing 提供了标准协议，让我们在系统中接入 tracing 更简单，基本可以实现自动或者半自动的 tracing 埋点。\n如上图所示，tracing 实现链路追踪是通过 Trace 的“父子关系”来构造的，而这个关系主要由组成 Trace 的 Span 而来（Trace Span 的概念最早来自于 Google 的 Dapper ）。\nTrace 记录了整个请求的生命周期，本身由一组 Span 组成，Span 代表其中的一条调用链 Span 具有“父子关系”，这个父子关系由 SpanID 和 ParentSpanID 组成\n当调用传播到下一层时，原来的 SpanID 就变成了 ParentSpanID，随后会生成一个新的 SpanID。 Span 的传播可能会跨进程、跨主机，因此需要有一个传递 TraceID、SpanID 的途径，这个途径叫做 Trace Propagation，Trace Propagation 需要保证上下游的服务都能够支持一样的协议才行，否则传播到下一层时，因为服务无法识别，Trace 会断掉。 由于系统中可能具有多个服务，还有队列、数据库、ServiceMesh 等中间件，因此 Trace Propagation 需要遵循某个国际化标准，这个标准需要尽可能的通用。 最早出现的国际化标准是 OpenTracing，随后还有 Google 发起的 OpenCensus 项目。 而目前 OpenTracing 项目和 OpenCensus 项目已经合并成为 OpenTelemetry，OpenTelemetry 已经成为 Trace 领域的唯一国际化标准。 而 OpenTelemetry 标准带来的好处不仅仅是解决各个系统之间的 Trace 互通问题，还有统一的 SDK、自动化埋点方案、数据采集、Traces/Metrics/Logs 互通等等好处。感兴趣的同学可以移步：OpenTelemetry 介绍。\nSLS # 基于阿里云的 sls 提供的基于标准 OpenTelemetry 协议的 tracing 采集方案。\n如上图所示：sls 支持的采集方式有多种，日志服务支持如下接入方案。\n使用 OpenTelemetry、Jaeger（目前仅支持 https、grpc 方式）、Zipkin、OpenCensus 等直接将 Trace 数据接入到日志服务。 使用 OpenTelemetry Collector 转发 OpenTelemetry、Jaeger（全协议支持）、Zipkin、OpenCensus、AWS X-Ray、SignalFX（Splunk）等平台上的 Trace 数据到日志服务。 使用 Logtail 转发 SkyWalking 的 Trace 数据到日志服务。 使用自定义协议将 Trace 数据接入到日志服务，并通过日志服务加工功能将 Trace 数据格式转换为 OpenTelemetry 格式。 这次在 push 服务中实现的 tracing 的接入方案主要是基于 OpenTelemetry 统一协议的采集方案，直接使用阿里云提供的 sdk 即可，无需部署其他依赖组件。\n初始化 # 配置信息： [data.tracing] traceExporterEndpoint = \u0026#34;\u0026#34; metricExporterEndpoint = \u0026#34;\u0026#34; slsProject = \u0026#34;\u0026#34; slsInstance = \u0026#34;\u0026#34; accessKeyId = \u0026#34;\u0026#34; accessKeySecret = \u0026#34;\u0026#34; 上面的配置信息是测环境的，配置完成后可以本地调试，同时可以在下面的 sls 看板上看到数据\nprovider 初始化代码块：\nvar ( MetricPushInterval = 20 Namespace string = \u0026#34;xxx\u0026#34; // MetricName is the name of the compiled software. MetricName = \u0026#34;xxx\u0026#34; // Name is the name of the compiled software. Name = \u0026#34;xxx\u0026#34; // Version is the version of the compiled software. Version string = \u0026#34;v0.1.0\u0026#34; ) func initTracerConfig(tracingConfig *conf.Tracing) *provider.Config { // Namespace, Name, Version 必须有值 slsConfig, err := provider.NewConfig(provider.WithServiceName(Name), provider.WithServiceVersion(Version), provider.WithServiceNamespace(Namespace), provider.WithTraceExporterEndpoint(tracingConfig.TraceExporterEndpoint), provider.WithMetricExporterEndpoint(tracingConfig.MetricExporterEndpoint), provider.WithSLSConfig(tracingConfig.SlsProject, tracingConfig.SlsInstance, tracingConfig.AccessKeyId, tracingConfig.AccessKeySecret)) // 如果初始化失败则 panic，可以替换为其他错误处理方式 if err != nil { panic(err) } return slsConfig } main.go 中接入： func main() { flag.Parse() c := config.New(config.WithPath(confPath)) if err := c.Load(); err != nil { panic(err) } if err := c.Scan(\u0026amp;conf.Cfg); err != nil { panic(err) } // ... // 初始化 tracing slsConfig := initTracerConfig(\u0026amp;conf.Cfg.Data.Tracing) if err := provider.Start(slsConfig); err != nil { panic(err) } defer provider.Shutdown(slsConfig) // ... } gin 接入 tracing # gin 提供了官方支持的采集方案，侵入性很低，基本上一行代码可以搞定\n引入采集 tracing 的组件：\ngo get \u0026#34;go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin\u0026#34; 在 gin 中使用中间件即可：\nfunc main() { // ...... 省略 r := gin.Default() r.Use(otelgin.Middleware(serviceName)) // ...... 省略 } Redis 接入 tracing # go-redis 组件提供了官方支持的 OpenTelemetry 协议的采集方案，侵入性很低，一行代码搞定。\n引入 redisotel 组件：\ngo get \u0026#34;github.com/go-redis/redis/extra/redisotel/v8\u0026#34; 在初始化 redis-cli 的地方添加 hook 代码即可\nfunc NewRedisClient(conf *conf.Data) *redis.Client { // ... 省略 // 添加下面这行采集 tracing 的代码 client.AddHook(redisotel.NewTracingHook()) // ... 省略 return client } MySQL 接入 tracing # gorm 官方也提供了支持 OpenTelemetry 协议的采集方案，侵入性很低，一行代码搞定。\n引入 gromotel 组件：\ngo get \u0026#34;gorm.io/plugin/opentelemetry/tracing\u0026#34; 在初始化 db-cli 的地方添加如下代码即可：\nfunc NewDB(conf *conf.Data, logger log.Logger, zapLogger *zap.Logger) *gorm.DB { // ... 省略 if err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics())); err != nil { panic(err) } return db } 已接入系统展示 # push-service 服务概览 ：\n接口概览：\ntrace 详情展示：\n未来规划 # 基于 sls 的 trace 能力，将 sls 的 tracing，metric，log 串联起来，同时也可以接入相关报警，实现更细粒度的监控报警方案。\n相关文档与 lib # github.com/go-gorm/ope…\nupstash.com/blog/go-red…\ngithub.com/redis/go-re…\nhelp.aliyun.com/zh/sls/user…\nflow.visionhope.cn/posts/opent…\n相关 Tracing 实践 # xie.infoq.cn/article/8f4…\ndeveloper.aliyun.com/article/785…\ndeveloper.aliyun.com/article/783…\n","date":"2024-03-30","externalUrl":null,"permalink":"/posts/golang-xiangmuzhongdequanlianluzhuizong-tracing/","section":"文章","summary":"链路追踪（Tracing）是一种技术，用于监视和记录计算机程序或系统的运行情况。在分布式系统中，追踪可以帮助开发者和运维人员理解多个组件是如何协同工作的，以及在处理请求时它们之间的交互情况。","title":"golang 项目中的全链路追踪（tracing）","type":"posts"},{"content":"","date":"2023-12-20","externalUrl":null,"permalink":"/tags/mysql/","section":"标签","summary":"","title":"MySQL","type":"tags"},{"content":"📌 本文原发布于掘金社区：MySQL 的存储结构-页\n先从几个问题开始 # InnoDB 引擎下 varchar 类型的最大长度？ 什么情况下我们需要水平分表？为什么？ MySQL 存储结构简介 # MySQL InnoDB 存储引擎逻辑存储结构\n1、表空间（table space） # 表空间（Tablespace）是一个逻辑容器，表空间存储的对象是段，在一个表空间中可以有一个或多个段，但是一个段只能属于一个表空间。数据库由一个或多个表空间组成，表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。\n在 InnoDB 中存在两种表空间的类型：共享表空间和独立表空间。如果是共享表空间就意味着多张表共用一个表空间。如果是独立表空间，就意味着每张表有一个独立的表空间，也就是数据和索引信息都会保存在自己的表空间中。独立的表空间可以在不同的数据库之间进行迁移。\nInnoDB 把数据保存在表空间内，表空间可以看作是 InnoDB 存储引擎逻辑结构的最高层。本质上是一个由一个或多个磁盘文件组成的虚拟文件系统。InnoDB 用表空间并不只是存储表和索引，还保存了回滚段、双写缓冲区等。\n2、段（segment） # 范围查询，其实是对 B+ 树叶子节点中的记录进行顺序扫描，而如果不区分叶子节点和非叶子节点，统统把节点代表的页面放到申请到的区中的话，进行范围扫描的效果就大打折扣了(这里解释一下：因为 B+树从上到下查询，如果叶子和非叶子节点混在同一个区中，会让一个区中存储的页目录数据大大减少，造成多个区随机 IO 发生)。所以对 B+ 树的叶子节点和非叶子节点进行了区别对待，也就是说叶子节点有自己独有的区，非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段（ segment ），存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成 2 个段，一个叶子节点段，一个非叶子节点段。\n默认情况下一个使用 InnoDB 存储引擎的表只有一个聚簇索引，一个索引会生成 2 个段，而段是以区为单位申请存储空间的，一个区默认占用 1M 存储空间，所以默认情况下一个只存了几条记录的小表也需要 2M 的存储空间。\n3、区（extent） # 在 InnoDB 存储引擎中，一个区会分配 64 个连续的页。因为 InnoDB 中的页大小默认是 16KB，所以一个区的大小是 64*16KB=1MB。在任何情况下每个区大小都为 1MB，为了保证页的连续性，InnoDB 存储引擎每次从磁盘一次申请 4-5 个区。默认情况下，InnoDB 存储引擎的页大小为 16KB，即一个区中有 64 个连续的页。\n为什么要划分多个区呢？ # 本质上还是为了性能！\n如果我们表中数据量很少的话，比如说你的表中只有几十条、几百条数据的话，的确用不到 区 的概念，因为简单的几个页就能把对应的数据存储起来，但是你架不住表里的记录越来越多呀。\n从理论上说，不引入区的概念只使用 页 的概念对存储引擎的运行并没啥影响，但是我们来考虑一下下边这个场景：\n我们每向表中插入一条记录，本质上就是向该表的聚簇索引以及所有二级索引代表的 B+ 树的节点中插入数据。而 B+ 树的每一层中的页都会形成一个双向链表，如果是以 页 为单位来分配存储空间的话，双向链表相邻的两个页之间的物理位置可能离得非常远。我们介绍 B+ 树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录，然后沿着双向链表一直扫描就可以了，而如果链表中相邻的两个页物理位置离得非常远，就是所谓的随机 I/O。再一次强调，磁盘的速度和内存的速度差了好几个数量级，随机 I/O 是非常慢的，所以我们应该尽量让链表中相邻的页的物理位置也相邻，这样进行范围查询的时候才可以使用所谓的顺序 I/O。\n所以才引入了区（ extent ）的概念，一个区就是在物理位置上连续的 64 个页。在表中数据量大的时候，为某个索引分配空间的时候就不再以页为单位分配了，而是以区为单位分配，甚至在表中的数据非常多的时候，可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费（数据不足填充满整个区），但是从性能角度看，可以消除很多的随机 I/O，功大于过嘛！\n4、页（Page） # 页是 InnoDB 存储引擎磁盘管理（数据读取）的最小单位，每个页默认 16KB；\nInnoDB 存储引擎中，常见的页类型有：\n数据页（B-tree Node)\nundo 页（undo Log Page）\n系统页（System Page）\n事务数据页（Transaction System Page）\n插入缓冲位图页（Insert Buffer Bitmap）\n插入缓冲空闲列表页（Insert Buffer Free List）\n未压缩的二进制大对象页（Uncompressed BLOB Page）\n压缩的二进制大对象页（compressed BLOB Page）\nInnoDB 页定义 # 真正处理数据的过程是发生在内存中的，所以需要把磁盘中的数据加载到内存中，如果是处理写入或修改请求的话，还需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢，和内存读写差了几个数量级，所以当我们想从表中获取某些记录时，InnoDB 存储引擎需要一条一条地把记录从磁盘上读出来么？不，那样会慢死，InnoDB 采取的方式是：将数据划分为若干个页，以页作为磁盘和内存之间交互的基本单位，InnoDB 中页的大小一般为 16 KB。也就是在一般情况下，一次最少从磁盘中读取 16KB 的内容到内存中，一次最少把内存中的 16KB 内容刷新到磁盘中。\n页的结构 # 从图中可以看出，一个 InnoDB 数据页的存储空间大致被划分成了 7 个部分，有的部分占用的字节数是确定的，有的部分占用的字节数是不确定的。\n各部分的作用：\n记录在页中的存储 # 在页的 7 个组成部分中，我们自己存储的记录会按照我们指定的行格式存储到 User Records 部分。但是在一开始生成页的时候，其实并没有 User Records 这个部分，每当我们插入一条记录，都会从 Free Space 部分，也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分，当 Free Space 部分的空间全部被 User Records 部分替代掉之后，也就意味着这个页使用完了，如果还有新的记录插入的话，就需要去申请新的页了，这个过程的图示如下：\n行记录格式（Compact 类型） # 行记录在 User_Records 中的存在形式 # 通过 next_record 属性，可以让页中的多条记录形成一个链表，链表是有序的，按照主键顺序排序。\n行记录的删除 # 删除第 2 条记录前后主要发生了这些变化：\n第 2 条记录并没有从存储空间中移除，而是把该条记录的 delete_mask 值设置为 1。\n第 2 条记录的 next_record 值变为了 0，意味着该记录没有下一条记录了。\n第 1 条记录的 next_record 指向了第 3 条记录。\n最大记录的 n_owned 值从 5 变成了 4。\n所以，不论我们怎么对页中的记录做增删改操作，InnoDB 始终会维护一条记录的单链表，链表中的各个节点是按照主键值由小到大的顺序连接起来的。\n另外，当插入的主键和被删除的主键相同时，删除的行记录存储空间是可以被复用的。\n页目录 # 页目录就是 page 的目录，就像书的目录，作用是加快查找。\n记录在页中按照主键值由小到大顺序串联成一个单链表，那如果我们想根据主键值查找页中的某条记录该咋办呢？比如说这样的查询语句：\nSELECT * FROM test WHERE id = 100; 从 Infimum 记录（最小记录）开始，沿着链表一直往后找就可以了。\n但效率低了些\u0026hellip;\n更好的办法是：页目录\n页目录的生成 # 它的生成过程是这样的：\n将所有正常的记录（包括最大和最小记录，不包括标记为已删除的记录）划分为几个组。\n每个组的最后一条记录（也就是组内最大的那条记录）的头信息中的 n_owned 属性表示该记录拥有多少条记录，也就是该组内共有几条记录。\n将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方，这个地方就是所谓的 Page Directory，也就是 页目录。页面目录中的这些地址偏移量被称为 槽（英文名：Slot ），所以这个页面目录就是由 槽组成的。\n举例 # 分组规则：\n对于最小记录所在的分组只能有 1 条记录，最大记录所在的分组拥有的记录条数只能在 18 条之间，剩下的分组中记录的条数范围只能是 48 条之间。\n每个槽中存储的是：当前分组中最大记录的地址偏移量，偏移量从 page 的 0 字节开始计算\n查找步骤 # 通过二分法确定该记录所在的槽，并找到该槽中主键值最小的那条记录。\n通过记录的 next_record 属性遍历该槽所在的组中的各个记录。\nPage Header（页面头部） # 专门针对数据页的。\nFile Header（文件头） # Page Header 是专门针对 数据页 记录的各种状态信息，比方说页里头有多少个记录了呀，有多少个 槽了呀。我们现在描述的 File Header 针对各种类型的页都通用，也就是说不同类型的页都会以 File Header 作为第一个组成部分，占用固定的 38 个字节\nFIL_PAGE_SPACE_OR_CHKSUM\n这个代表当前页面的校验和（checksum）。啥是个校验和？就是对于一个很长很长的字节串来说，我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串，这个比较短的值就称为 校验和。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和，如果校验和都不一样，两个长字节串肯定是不同的，所以省去了直接比较两个较长的字节串的时间损耗。\nFIL_PAGE_OFFSET\n每一个 页 都有一个单独的页号，就跟你的身份证号码一样，InnoDB 通过页号可以唯一定位一个 页。\nFIL_PAGE_PREV 和 FIL_PAGE_NEXT\nInnoDB 可能无法一次性为这么多数据分配一个非常大的存储空间，如果分散到多个不连续的页中存储的话，需要把这些页关联起来，FIL_PAGE_PREV 和 FIL_PAGE_NEXT 就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了，而无需这些页在物理上真正连着。需要注意的是，并不是所有类型的页都有上一个和下一个页的属性，在数据页（也就是类型为 FIL_PAGE_INDEX 的页）是有这两个属性的，所以所有的数据页其实是一个双链表（很重要）\nInnoDB 的索引结构 # MySQL 中的索引有多种，在 InnoDB 中，默认情况下所有主键都会自动创建一个 B+ 树索引，所以我们仅讨论 B+ 树索引。\n索引的结构示意图：\n对我们的启发 # 我们设计系统时也可以参考分层缓存的理念，提升系统的性能。比如：数据记录不多，且频繁读取其中的行，可以一次查出所有（或部分） 将相关数据放置在相邻的物理位置上，以利用空间局部性原则。比如：在选择 Redis 数据结构时，相关数据可以考虑使用 hash 来替代 string ","date":"2023-12-20","externalUrl":null,"permalink":"/posts/mysql-decunchujiegou-ye/","section":"文章","summary":"先从几个问题开始 innodb 引擎下 varchar 类型的最大长度？什么情况下我们需要水平分表？为什么？","title":"MySQL 的存储结构-页","type":"posts"},{"content":"","date":"2023-12-20","externalUrl":null,"permalink":"/tags/%E9%9D%A2%E8%AF%95/","section":"标签","summary":"","title":"面试","type":"tags"},{"content":"","date":"2023-12-19","externalUrl":null,"permalink":"/tags/redis/","section":"标签","summary":"","title":"Redis","type":"tags"},{"content":"","date":"2023-12-19","externalUrl":null,"permalink":"/tags/%E5%88%86%E5%B8%83%E5%BC%8F/","section":"标签","summary":"","title":"分布式","type":"tags"},{"content":"📌 本文原发布于掘金社区：如何使用 Redis 实现分布式锁\n锁是我们在设计和实现大多数系统时绕不过的话题。一旦有竞争条件出现，在没有保护的前提下，可能会出现不可预知的问题。\n而现代系统大多为分布式系统，这就引入了分布式锁，要求具备在分布于各处的服务上保护资源的能力。\n而实现分布式锁，目前大多有以下三种方式：\n使用数据库实现。 使用 Redis 等缓存系统实现。 使用 ZooKeeper 等分布式协调系统实现。 其中 Redis 简便灵活、高可用、分布式，且支持持久化。本文即介绍基于 Redis 实现分布式锁。\nSETNX 语义 # 使用 Redis 实现分布式锁，根本原理是 SETNX 指令。其语义如下：\nSETNX key value 命令执行时，如果 key 不存在，则设置 key 值为 value（同set）；如果 key 已经存在，则不执行赋值操作。并使用不同的返回值来标识执行结果。命令描述文档\n还可以通过 SET 命令的 NX 选项使用：\nSET key value [expiration EX seconds|PX milliseconds] [NX|XX] NX - 仅在 key 不存在时执行赋值操作。命令描述文档 而如下文所述，通过 SET 的 NX 选项使用时，可同时使用其它选项，如通过 EX/PX 设置超时时间，是更好的方式。\nSETNX 实现分布式锁 # 下面我们对比下几种具体实现方式。\n方案 1：SETNX + delete # 伪代码如下：\nsetnx lock_a random_value // do sth delete lock_a 此实现方式的问题在于：一旦服务获取锁后，因某种原因挂掉，则锁一直无法自动释放。从而导致死锁。\n方案 2：SETNX + SETEX # 伪代码如下：\nsetnx lock_a random_value setex lock_a 10 random_value // 10s超时 // do sth delete lock_a 按需设置超时时间。此方案解决了方案 1 死锁的问题，但同时引入了新的死锁问题：如果 setnx 之后，setex 之前服务挂掉，会陷入死锁。根本原因在于 setnx/setex 分为两个步骤，并非原子操作。\n方案 3：SET NX PX # 伪代码如下：\nSET lock_a random_value NX PX 10000 // 10s超时 // do sth delete lock_a 此方案通过 set 的 NX/PX 选项，将加锁、设置超时两个步骤合并为一个原子操作，从而解决方案 1、2 的问题。(PX 与 EX 选项的语义相同，差异仅在单位。) 此方案目前大多数 sdk、Redis 部署方案都支持，因此是推荐使用的方式。但此方案也有如下问题：\n如果锁被错误地释放（如超时），或被错误地抢占，或因 Redis 问题等导致锁丢失，无法很快地感知到。\n方案 4：SET key randomvalue NX PX # 方案 4 在 3 的基础上，增加对 value 的检查，只解除自己加的锁。类似于 CAS，不过是 compare-and-delete。此方案 Redis 原生命令不支持，为保证原子性，需要通过 lua 脚本实现：\n伪代码如下：\nSET lock_a random_value NX PX 10000 // do sth eval \u0026#34;if redis.call(\u0026#39;get\u0026#39;,KEYS[1]) == ARGV[1] then return redis.call(\u0026#39;del\u0026#39;,KEYS[1]) else return 0 end\u0026#34; 1 lock_a random_value 此方案更严谨：即使因为某些异常导致锁被错误地抢占，也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放，从而进行特殊处理。\n注意事项 # 超时时间 # 从上述描述可看出，超时时间是一个比较重要的变量：\n超时时间不能太短，否则在任务执行完成前就自动释放了锁，导致资源暴露在锁保护之外。超时时间不能太长，否则会导致意外死锁后长时间的等待。除非人为介入处理。因此建议根据任务内容合理衡量超时时间，将其设置为任务内容的几倍即可。如果实在无法确定而又要求比较严格，可以通过定期 setex/expire 更新超时时间来实现。\n重试 # 如果拿不到锁，建议根据任务性质、业务形式进行轮询等待。等待次数需要参考任务执行时间。\n与 Redis 事务的比较 # setnx 使用起来更为灵活。multi/exec 的事务实现形式更为复杂。且部分 Redis 集群方案(如 codis)，不支持 multi/exec 事务。\n","date":"2023-12-19","externalUrl":null,"permalink":"/posts/ruheshiyong-redis-shixianfenbushisuo/","section":"文章","summary":"锁是我们在设计和实现大多数系统时绕不过的话题。一旦有竞争条件出现，在没有保护的操作的前提下，可能会出现不可预知的问题。","title":"如何使用 Redis 实现分布式锁","type":"posts"},{"content":"","date":"2023-08-08","externalUrl":null,"permalink":"/tags/%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/","section":"标签","summary":"","title":"代码规范","type":"tags"},{"content":"","date":"2023-08-08","externalUrl":null,"permalink":"/tags/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/","section":"标签","summary":"","title":"单元测试","type":"tags"},{"content":"📌 本文原发布于掘金社区：单元测试之 gomock\n简介 # gomock 的由来和概述 # gomock 是 Uber 公司于 2015 年开源的一个 mock 框架。它是基于 Go 语言中的反射机制实现的，可以自动生成接口的 mock 对象。传统的 mock 测试需要手动编写 mock 对象的代码，而 gomock 可以根据接口描述自动生成 mock 代码，大大简化了 mock 对象的创建过程。开发人员只需提供接口描述，gomock 会生成对应的 mock struct，并在测试中直接使用。gomock 的关键优势包括：\n自动生成 mock 代码：不需要手写 mock 对象代码，提高开发效率 类型安全：基于接口自动生成 mock，类型检查友好 灵活的匹配：支持自定义 matcher，可以进行灵活的方法参数匹配 丰富的 action：支持设置返回值、返回错误等 action 便于调试：生成的 mock 代码简洁易读，便于调试 总体来说，gomock 是一个轻量级的 mock 框架，通过自动生成可以减少重复代码的编写，使测试代码更简洁清晰。它的类型安全、灵活的 matcher 和 action 也使 mock 测试更加便捷和可靠。gomock 已经被许多 Go 语言项目采用，是 Go 语言生态里一个流行和实用的 mock 测试框架。\n为什么需要使用 gomock # 效率更高，它可以自动生成 mock 代码，无需开发者手动编写； 更安全可靠，基于接口的 mock 对象类型安全，可以充分利用 Go 的接口优势； 更灵活可控，可以方便地设置 mock 方法的各种行为，提高测试场景的覆盖率； 总之，gomock 使编写单元测试更高效、更安全和更灵活，是 Go 测试不可或缺的重要框架。它良好的自动化、类型安全和行为可控性，可以大大提升 Go 测试的效率和质量。\ngomock 的基本用法 # 安装 # go install go.uber.org/mock/mockgen@latest 定义接口 # 创建名为 biz.go 的文件，定义下面的接口\ntype INacosRepo interface { GetConfig(ctx context.Context, dataId string) interface{} } type IAdRepo interface { ESSearchAds(ctx context.Context, grade string, version, endpoint, userStatus int, preview bool, previewUserId int, previewDeviceId string) ([]model.Ad, error) ESCheckAdPreview(ctx context.Context, previewUserId int, previewDeviceId string) (bool, error) DBExpireAds(ctx context.Context) error ESExpireAds(ctx context.Context) error DBAdPreviewExpire(ctx context.Context) error ESAdPreviewExpire(ctx context.Context) error } 生成 mock 对象 # mockgen -source=user_pk.go -destination user_pk_mock.go -package biz 通过上面的命令生成名为 biz_mock.go 的 mock 文件，代码如下所示\n// Code generated by MockGen. DO NOT EDIT. // Source: biz.go // Package biz is a generated GoMock package. package biz import ( context \u0026#34;context\u0026#34; model \u0026#34;oralArithmetic/pkg/model\u0026#34; reflect \u0026#34;reflect\u0026#34; gomock \u0026#34;go.uber.org/mock/gomock\u0026#34; ) // MockINacosRepo is a mock of INacosRepo interface. type MockINacosRepo struct { ctrl *gomock.Controller recorder *MockINacosRepoMockRecorder } // MockINacosRepoMockRecorder is the mock recorder for MockINacosRepo. type MockINacosRepoMockRecorder struct { mock *MockINacosRepo } // NewMockINacosRepo creates a new mock instance. func NewMockINacosRepo(ctrl *gomock.Controller) *MockINacosRepo { mock := \u0026amp;MockINacosRepo{ctrl: ctrl} mock.recorder = \u0026amp;MockINacosRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockINacosRepo) EXPECT() *MockINacosRepoMockRecorder { return m.recorder } // GetConfig mocks base method. func (m *MockINacosRepo) GetConfig(ctx context.Context, dataId string) interface{} { m.ctrl.T.Helper() ret := m.ctrl.Call(m, \u0026#34;GetConfig\u0026#34;, ctx, dataId) ret0, _ := ret[0].(interface{}) return ret0 } // GetConfig indicates an expected call of GetConfig. func (mr *MockINacosRepoMockRecorder) GetConfig(ctx, dataId interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \u0026#34;GetConfig\u0026#34;, reflect.TypeOf((*MockINacosRepo)(nil).GetConfig), ctx, dataId) } // MockIAdRepo is a mock of IAdRepo interface. type MockIAdRepo struct { ctrl *gomock.Controller recorder *MockIAdRepoMockRecorder } // MockIAdRepoMockRecorder is the mock recorder for MockIAdRepo. type MockIAdRepoMockRecorder struct { mock *MockIAdRepo } // NewMockIAdRepo creates a new mock instance. func NewMockIAdRepo(ctrl *gomock.Controller) *MockIAdRepo { mock := \u0026amp;MockIAdRepo{ctrl: ctrl} mock.recorder = \u0026amp;MockIAdRepoMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockIAdRepo) EXPECT() *MockIAdRepoMockRecorder { return m.recorder } // DBAdPreviewExpire mocks base method. func (m *MockIAdRepo) DBAdPreviewExpire(ctx context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, \u0026#34;DBAdPreviewExpire\u0026#34;, ctx) ret0, _ := ret[0].(error) return ret0 } // DBAdPreviewExpire indicates an expected call of DBAdPreviewExpire. func (mr *MockIAdRepoMockRecorder) DBAdPreviewExpire(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \u0026#34;DBAdPreviewExpire\u0026#34;, reflect.TypeOf((*MockIAdRepo)(nil).DBAdPreviewExpire), ctx) } // DBExpireAds mocks base method. func (m *MockIAdRepo) DBExpireAds(ctx context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, \u0026#34;DBExpireAds\u0026#34;, ctx) ret0, _ := ret[0].(error) return ret0 } // DBExpireAds indicates an expected call of DBExpireAds. func (mr *MockIAdRepoMockRecorder) DBExpireAds(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \u0026#34;DBExpireAds\u0026#34;, reflect.TypeOf((*MockIAdRepo)(nil).DBExpireAds), ctx) } // ESAdPreviewExpire mocks base method. func (m *MockIAdRepo) ESAdPreviewExpire(ctx context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, \u0026#34;ESAdPreviewExpire\u0026#34;, ctx) ret0, _ := ret[0].(error) return ret0 } // ESAdPreviewExpire indicates an expected call of ESAdPreviewExpire. func (mr *MockIAdRepoMockRecorder) ESAdPreviewExpire(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \u0026#34;ESAdPreviewExpire\u0026#34;, reflect.TypeOf((*MockIAdRepo)(nil).ESAdPreviewExpire), ctx) } // ESCheckAdPreview mocks base method. func (m *MockIAdRepo) ESCheckAdPreview(ctx context.Context, previewUserId int, previewDeviceId string) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, \u0026#34;ESCheckAdPreview\u0026#34;, ctx, previewUserId, previewDeviceId) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // ESCheckAdPreview indicates an expected call of ESCheckAdPreview. func (mr *MockIAdRepoMockRecorder) ESCheckAdPreview(ctx, previewUserId, previewDeviceId interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \u0026#34;ESCheckAdPreview\u0026#34;, reflect.TypeOf((*MockIAdRepo)(nil).ESCheckAdPreview), ctx, previewUserId, previewDeviceId) } // ESExpireAds mocks base method. func (m *MockIAdRepo) ESExpireAds(ctx context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, \u0026#34;ESExpireAds\u0026#34;, ctx) ret0, _ := ret[0].(error) return ret0 } // ESExpireAds indicates an expected call of ESExpireAds. func (mr *MockIAdRepoMockRecorder) ESExpireAds(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \u0026#34;ESExpireAds\u0026#34;, reflect.TypeOf((*MockIAdRepo)(nil).ESExpireAds), ctx) } // ESSearchAds mocks base method. func (m *MockIAdRepo) ESSearchAds(ctx context.Context, grade string, version, endpoint, userStatus int, preview bool, previewUserId int, previewDeviceId string) ([]model.Ad, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, \u0026#34;ESSearchAds\u0026#34;, ctx, grade, version, endpoint, userStatus, preview, previewUserId, previewDeviceId) ret0, _ := ret[0].([]model.Ad) ret1, _ := ret[1].(error) return ret0, ret1 } // ESSearchAds indicates an expected call of ESSearchAds. func (mr *MockIAdRepoMockRecorder) ESSearchAds(ctx, grade, version, endpoint, userStatus, preview, previewUserId, previewDeviceId interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \u0026#34;ESSearchAds\u0026#34;, reflect.TypeOf((*MockIAdRepo)(nil).ESSearchAds), ctx, grade, version, endpoint, userStatus, preview, previewUserId, previewDeviceId) } 使用 mock 对象编写测试 # func (s *_Suite) TestOralPkBiz_PkRecommend() { ctx := context.Background() type args struct { ctx context.Context userId int req request.PkRecommendReq } tests := []struct { name string args args wantResp response.PkRecommendResp wantErr bool }{ // TODO: Add test cases. { name: \u0026#34;test_session_over\u0026#34;, args: args{ ctx: ctx, userId: 11108, req: request.PkRecommendReq{ GradeId: \u0026#34;2\u0026#34;, VersionId: \u0026#34;5\u0026#34;, TermId: \u0026#34;1\u0026#34;, }, }, wantResp: response.PkRecommendResp{}, wantErr: false, }, } pkInfo := preparePkConfig(ctx) pkRobotInfo := preparePkRobotConfig(ctx) s.RepoMock.INacosRepo.EXPECT().GetConfig(ctx, constant.DataIdPkConfig).Return(pkInfo).AnyTimes() s.RepoMock.INacosRepo.EXPECT().GetConfig(ctx, constant.DataIdPkRobotCfgConfig).Return(pkRobotInfo).AnyTimes() s.RepoMock.ICacheRepo.EXPECT().Set(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes() s.RepoMock.IKnowledgeSearchRepo.EXPECT().SearchPkKnowledge(ctx, gomock.Any()).Return(\u0026amp;elastic.SearchResult{ Hits: \u0026amp;elastic.SearchHits{ Hits: []*elastic.SearchHit{ {Source: []byte(`{\u0026#34;knowledgeId\u0026#34;:\u0026#34;2ebf9c53589d43ce966ac52429f52337\u0026#34;,\u0026#34;knowledgeIdForMerge\u0026#34;:[\u0026#34;2ebf9c53589d43ce966ac52429f52337\u0026#34;],\u0026#34;knowledgeName\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;example\u0026#34;:\u0026#34;\u0026#34;}`)}, }, }, }, nil).AnyTimes() s.RepoMock.IOralPkRepo.EXPECT().GetUserPkScoreById(ctx, gomock.Any()).Return(\u0026amp;model.UserPk{ PkSeasonId: \u0026#34;\u0026#34;, UserId: 0, PkGrade: 2, PkSeasonPoints: 100, }, nil).AnyTimes() s.RepoMock.IOralPkRepo.EXPECT().GetPkQuestionByKnowledgeIDs(ctx, gomock.Any()).Return([]*model.OralArithmeticQuestion{ { QuestionId: \u0026#34;2a44e4fb7aee4dbcbfd1f7a6f20a65d2\u0026#34;, }, }, nil).AnyTimes() s.RepoMock.IOralPkRepo.EXPECT().GetQuestionListByIDs(ctx, gomock.Any()).Return([]*model.OralArithmeticQuestion{ { Id: 608177, KnowledgeId: \u0026#34;2ebf9c53589d43ce966ac52429f52337\u0026#34;, QuestionId: \u0026#34;2a44e4fb7aee4dbcbfd1f7a6f20a65d2\u0026#34;, ContentOperated: \u0026#34;5-3=\\square\u0026#34;, AnswerOperated: \u0026#34;2\u0026#34;, QuestionType: 1, QuestionStatus: 2, QuestionStatusName: \u0026#34;\u0026#34;, Difficulty: 1, ParseType: 0, }, }, nil).AnyTimes() for _, tt := range tests { s.T().Run(tt.name, func(t *testing.T) { gotResp, err := s.OralPkBiz.PkRecommend(tt.args.ctx, tt.args.userId, tt.args.req) if (err != nil) != tt.wantErr { s.T().Errorf(\u0026#34;PkRecommend() error = %v, wantErr %v\u0026#34;, err, tt.wantErr) return } s.T().Logf(\u0026#34;PkRecommend() gotResp = %v\u0026#34;, gotResp) }) } } gomock 的高级用法 # 提升测试覆盖率 # 增加参数化测试可以通过设置不同的参数来增加测试场景： func TestAdd(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() adder := NewMockAdder(mockCtrl) adder.EXPECT().Add(1, 2).Return(3) adder.EXPECT().Add(3, 4).Return(7) // ... } 测试错误场景返回错误来测试错误处理： adder.EXPECT().Add(4, 5).Return(0, errors.New(\u0026#34;Error\u0026#34;)) 模拟异常使用 gomock.Panic（）来模拟 panic: adder.EXPECT().Add(4, 5).Do(gomock.Panic()) 设置方法副作用可以通过 Do（）来产生副作用： var sharedValue int adder.EXPECT().Add(1, 2).Do(func() { sharedValue = 10 }) 并发测试使用 Go 并发功能并发调用： var wg sync.WaitGroup for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go func() { adder.Add(1, 2) wg.Done() }() } wg.Wait() 模拟不同场景 # 模拟不同的返回值可以通过 Return 方法来设置 mock 方法的不同返回值，模拟不同的返回场景： mockObj.EXPECT().Get(gomock.Eq(1)).Return(10) mockObj.EXPECT().Get(gomock.Eq(2)).Return(20) 模拟返回错误使用 Return 方法返回 error 对象可以模拟返回错误的场景： mockObj.EXPECT().Get(gomock.Eq(3)).Return(0, errors.New(\u0026#34;not found\u0026#34;)) 模拟网络延迟使用 Do 方法来添加自定义行为，可以添加 sleep 来模拟网络延迟： mockObj.EXPECT().Get(gomock.Eq(4)).Do(func() { time.Sleep(1 * time.Second) return 30 }) 模拟 RPC 错误可以返回自定义的 RPC 错误： mockObj.EXPECT().Get(gomock.Eq(5)).Return(nil, rpctypes.Errorf(codes.NotFound, \u0026#34;not found\u0026#34;)) 模拟并发问题在 Do 方法中使用共享的状态变量和锁来模拟并发场景： var mutex sync.Mutex var count int mockObj.EXPECT().Get(gomock.Eq(6)).Do(func() { mutex.Lock() count++ mutex.Unlock() return count }) 自定义匹配器 # 实现 Matcher 接口 // 实现 Matcher 接口 type myMatcher struct {} func (m *myMatcher) Matches(x interface{}) bool { // 匹配逻辑 } func (m *myMatcher) String() string { return \u0026#34;myMatcher\u0026#34; } 定义匹配器函数 func MyMatch(param int) gomock.Matcher { return \u0026amp;myMatcher{wanted: param} } 在 EXPECT 中使用自定义匹配器 mockObj.EXPECT().Foo(MyMatch(5)) 此时 Foo 方法调用时会使用 MyMatch 匹配器进行参数匹配。自定义匹配器的常见场景：\n正则表达式匹配； 对象字段匹配； 复杂逻辑匹配：自定义匹配器可以让我们摆脱仅通过 Equals 等有限的匹配方式，实现更加灵活和宽松的匹配逻辑。 编写易用的 wrapper # 定义 interface 编写测试要用的 interface，包含待 mock 的所有方法。 实现 wrapper 实现一个结构体，内部含有 gomock 生成的 mock 对象。 封装方法在 wrapper 的方法中直接调用内部 mock 对象的方法。 提供便捷方法根据需求在 wrapper 中提供一些便捷方法，如默认返回值、可选参数等。示例： // 1. 接口定义 type Adder interface { Add(int, int) int } // 2. 实现 wrapper type AdderMock struct { MockAdder *MockAdder } // 3. 封装方法 func (a *AdderMock) Add(x, y int) int { return a.MockAdder.Add(x, y) } // 4. 提供便捷方法 func (a *AdderMock) AddWithDefault() int { return a.Add(1, 2) } // 测试中使用 wrapper mock := NewAdderMock(ctrl) mock.AddWithDefault() 这样可以简化 gomock 的使用代码，提高测试用例的复用性和可读性。\ngomock 的实现原理 # 生成 mock 对象的过程 # 解析接口 gomock 会使用 reflect 包解析需要 mock 的接口，获取接口中的方法名、参数和返回值等信息。 生成代码根据接口信息，gomock 使用 text/template 自动生成接口方法的 mock 实现代码，包括函数体、匹配器、调用记录等。 构建对象使用 go generate 工具自动编译生成的 mock 代码，得到 mock 对象的实现。 调节行为测试时通过调用 mock 对象的 EXPECT（）等方法设置返回值、行为等，调节 mock 行为。 记录调用 mock 对象记录每次方法调用，assert 是否与预设的行为一致。 gomock 通过自动化代码生成和运行时行为控制，可以快速灵活地构造 mock 对象，减轻编写 mock 对象的工作量。其核心是通过解析接口定义自动生成针对接口的 mock 实现代码。这样可以确保 mock 对象与目标接口一致，类型安全。\nmatchers 的工作原理 # 实现 Matcher 接口每个 matcher 都实现了 Matcher 接口，该接口定义了 Matches 和 String 两个方法。 Matches 方法判断匹配当调用 Expect（）配置 mock 行为时，会用 matcher 的 Matches 方法来判断参数是否匹配。 String 方法生成代码 matcher 的 String 方法返回其类型名称，gomock 使用该名称生成对应 mock 方法的代码。 生成匹配表达式针对每个 Expect（）调用和 matcher，gomock 会生成对应的匹配表达式代码。 运行时评估表达式当 mock 方法被调用时，会执行生成的匹配表达式，以判断参数是否满足预期。这样通过匹配器接口和生成的匹配代码，可以实现灵活 parameter matching，如 Eq（）、Any（）等。 常用的匹配器有：\nEq（）等于 Any（）任意参数 Nil（）nil 参数 Not（）排除指定参数 我们也可以定制 matcher 来实现自定义匹配逻辑。\n执行 mock 方法的流程 # 测试代码中调用 mock 对象的方法。\n根据方法参数，在预设的期望（EXPECT call）中查找匹配的调用记录。\n如果找到匹配的调用记录，则执行该调用记录配置的行为。\n如果是返回值，直接返回预设的返回值。\n如果是函数，则执行函数实现的自定义逻辑。\n如果没有匹配的调用记录，则测试失败。\n调用结束后，在当前的 Controller 中记录这次调用。\n用户可以通过 Controller 查看方法调用次数、参数等信息来进行断言。\n如果配置了调用后重置期望（After call），则重置对该方法的期望配置。\n所以执行流程主要分为：\n查找匹配的预设期望 执行已配置的行为 记录调用 重置期望。这使得我们可以方便地设置 mock 对象的 RETURN、DO 等行为，增强测试的灵活性。 gomock 的优缺点 # 优点：自动生成 mock，提高测试效率等 缺点：依赖反射，debug 困难等 总结 # gomock 的用途 # 单元测试 - 生成 mock 对象来隔离被测系统，进行更聚焦的测试。 集成测试 - 通过 mock 依赖来编写关键路径的集成测试。 模拟依赖 - 在本地开发时 mock 真实后端服务，使开发环境独立。 验证交互 - 使用 gomock.Controller 来记录并验证 mock 对象的交互行为。 测试不确定性 - 配置 mock 对象来模拟错误、延迟等情况。 提高覆盖率 - mock 对象可以灵活设置返回值，提高代码覆盖率。 模拟容错 - 配置各种错误情况来测试容错能力。 增强测试可控性 - 使用 gomock 可以显著提高测试用例的可控性和稳定性。 总之，gomock 适用于各类测试场景，是 Go 语言单元测试不可或缺的重要组件，可以帮助编写更优秀和可靠的测试。\n参考文档 # xiaoming.net.cn/2021/06/29/…\nmojotv.cn/2018/12/26/…\ngithub.com/uber-go/moc…\n","date":"2023-08-08","externalUrl":null,"permalink":"/posts/danyuanceshizhigomock/","section":"文章","summary":"简介 gomock 的由来和概述 gomock 是 Uber 公司开源的一个 mock 框架，于 2015 年开源。它是基于 Go 语言中的反射机制实现的，可以自动生成接口的 mock 对象。","title":"单元测试之 gomock","type":"posts"},{"content":"📌 本文原发布于掘金社区：MapReduce 论文阅读\n概述 # 这篇论文主要介绍了 MapReduce 编程模型和相关实现，用于处理和生成大型数据集。它隐藏了并行化、容错、本地性优化和负载平衡等细节，使得即使没有并行和分布式系统经验的程序员也很容易使用。此外，该模型可以轻松地表达许多现实世界的任务，并且已经成功地应用于 Google 的生产 Web 搜索服务、排序、数据挖掘、机器学习等多个系统中。最后，该论文还介绍了 MapReduce 实现的可扩展性，可以在由数千台机器组成的大型集群上运行。\n什么是 MapReduce # MapReduce 是一种编程模型和相关实现，用于处理和生成大型数据集。用户指定一个 map 函数，该函数处理一个键/值对以生成一组中间键/值对，并指定一个 reduce 函数，该函数合并与同一中间键关联的所有中间值。许多现实世界的任务都可以用这个模型来表达。下面我们给出一个基于 MapReduce 实现单词统计的伪代码：\n假设我们有一个包含多个文档的数据集，我们想要计算每个单词在所有文档中出现的次数。我们可以使用 MapReduce 编程模型来实现这个任务。首先，我们需要定义 map 函数和 reduce 函数：\n// Map函数：将每个文档解析为单词，并将每个单词映射到一个中间键/值对 function map(document): for each word in document: emitIntermediate(word, 1) // Reduce函数：将所有具有相同单词的中间值合并在一起，并生成一个输出键/值对 function reduce(word, counts): total = 0 for each count in counts: total += count emit(word, total) 然后，我们需要在 MapReduce 框架中调用这些函数：\n// MapReduce任务： function wordCount(documents): // Step 1: 划分输入数据并启动程序副本 splits = splitInput(documents) startWorkers(splits) // Step 2: 执行map任务并生成中间结果 intermediate = [] for each split in splits: results = runMap(split) intermediate.append(results) // Step 3: 对中间结果进行排序和分区，并执行reduce任务 sortedIntermediate = sortAndPartition(intermediate) output = [] for each partition in sortedIntermediate: result = runReduce(partition) output.append(result) // Step 4: 返回最终输出结果 return output 在这个示例中，我们首先划分输入数据并启动程序副本。然后，我们执行 map 任务并生成一组中间结果。接下来，我们对这些中间结果进行排序和分区，并执行 reduce 任务。最后，我们返回最终输出结果。\nMapReduce 的架构 # MapReduce 的架构是基于 Master/Worker 模型的分布式系统。在这个架构中，有一个由 Master 节点和多个 Worker 节点组成的集群。Master 节点负责协调整个 MapReduce 任务的执行，包括划分输入数据、调度 map 和 reduce 任务、处理 Worker 节点故障等。Worker 节点负责执行具体的 map 和 reduce 任务，并将中间结果传递给 Master 节点进行进一步处理。当用户程序调用 MapReduce 函数时，MapReduce 库首先将输入文件划分为 M 个大小相等的片段，并启动多个程序副本在集群中运行。其中一个程序副本被指定为 Master 节点，其余副本被指定为 Worker 节点。Master 节点负责将 map 和 reduce 任务分配给空闲的 Worker 节点，并监控它们的执行情况。每个 Worker 节点都会执行一些 map 或 reduce 任务，并将中间结果写入本地磁盘上的文件中。当所有 map 任务完成后，MapReduce 框架会对所有中间结果进行排序和分区，并将相同键值对应的中间结果发送到同一个 reduce 任务所在的 Worker 节点上进行合并处理。每个 reduce 任务都会读取自己所需的所有中间结果，并按照用户定义的 reduce 函数进行合并处理。最终输出由所有 reduce 任务生成的键/值对组成。除了基本架构之外，MapReduce 还提供了一些优化技术，如本地性优化、数据压缩、内存管理等，以提高任务执行效率和可靠性。例如，在本地性优化中，MapReduce 框架会尽可能地将 map 任务分配到与其输入数据所在位置相同或相邻的 Worker 节点上执行，以减少网络传输开销。\nMapReduce 做了哪些优化 # MapReduce 框架提供了多种优化技术，以提高任务执行效率和可靠性。以下是一些常见的优化技术：\n本地性优化：MapReduce 框架会尽可能地将 map 任务分配到与其输入数据所在位置相同或相邻的 Worker 节点上执行，以减少网络传输开销。这可以通过使用 Hadoop Rack Awareness 机制来实现。\n数据压缩：MapReduce 框架可以对中间结果和输出结果进行压缩，以减少磁盘空间和网络传输开销。这可以通过使用 Gzip、Bzip2 等压缩算法来实现。\n内存管理：MapReduce 框架可以通过调整 Java 虚拟机的堆大小、使用内存映射文件等方式来管理内存，以提高任务执行效率。\n负载平衡：MapReduce 框架会动态地调整 map 和 reduce 任务的分配，以确保所有 Worker 节点都能够充分利用其计算资源，并避免出现瓶颈。\n容错处理：MapReduce 框架会定期写入主数据结构的检查点来处理机器故障。如果 Master 节点失败，可以从最后一个检查点状态开始启动新的副本。此外，在 reduce 任务中也会使用备份机制来保证容错性。\n预取技术：MapReduce 框架可以在 map 任务执行之前预取输入数据块到 Worker 节点上的本地磁盘中，以减少网络传输开销和 I/O 延迟。\n组合技术：MapReduce 框架可以将多个 reduce 任务合并为一个单独的任务，并将中间结果直接传递给下一个 reduce 任务进行进一步处理，以减少磁盘 I/O 和网络传输开销。\nMapReduce 的容错 # MapReduce 框架是具有容错性的，可以处理各种类型的故障，包括 Worker 节点故障、Master 节点故障、网络故障等。以下是 MapReduce 处理失败的一些方法：\nWorker 节点故障：如果一个 Worker 节点失败，MapReduce 框架会将其任务重新分配给其他可用的 Worker 节点，并在必要时从备份中恢复数据。\nMaster 节点故障：如果 Master 节点失败，MapReduce 框架会从最后一个检查点状态开始启动新的副本，并继续执行未完成的任务。\n网络故障：如果网络出现问题导致某些 Worker 节点无法与 Master 节点通信，MapReduce 框架会将这些 Worker 节点标记为不可用，并将它们的任务重新分配给其他可用的 Worker 节点。\n任务超时：如果某个 map 或 reduce 任务超时或无响应，MapReduce 框架会将其标记为失败，并将其重新分配给其他可用的 Worker 节点。\n容错处理：MapReduce 框架会定期写入主数据结构的检查点来处理机器故障。如果 Master 或 Worker 节点失败，可以从最后一个检查点状态开始启动新的副本。此外，在 reduce 任务中也会使用备份机制来保证容错性。总之，通过这些方法和技术，MapReduce 框架可以有效地处理各种类型的失败，并保证整个任务能够顺利完成。\n跳过失败的记录 # 在 MapReduce 中，如果 Worker 节点执行某些失败的记录，MapReduce 框架会采取以下措施来跳过这些记录并继续执行任务：\n检测错误：MapReduce 框架会检测哪些记录导致了 Worker 节点的崩溃，并将这些记录标记为失败。\n跳过失败记录：在后续的任务执行中，MapReduce 框架会跳过这些已经标记为失败的记录，并将其从处理流程中删除。\n继续执行：通过跳过失败记录，MapReduce 框架可以使任务继续向前推进，并最终完成整个任务。总之，在 MapReduce 中，通过检测和跳过失败记录，可以有效地处理各种类型的故障，并保证整个任务能够顺利完成。\nMapReduce 设计的实现 # 这篇论文进行了多个实验来评估 MapReduce 的性能和可扩展性，包括：\nWord Count：在这个实验中，作者使用 MapReduce 框架对一个大型文本文件进行单词计数。实验结果表明，MapReduce 可以有效地处理大规模数据集，并且具有良好的可扩展性。\nDistributed Grep：在这个实验中，作者使用 MapReduce 框架对一个大型文本文件进行分布式搜索。实验结果表明，MapReduce 可以轻松地处理各种类型的搜索任务，并且具有良好的容错性。\nURL Access Frequency：在这个实验中，作者使用 MapReduce 框架对 Google 的 Web 服务器日志进行分析，以确定每个 URL 的访问频率。实验结果表明，MapReduce 可以有效地处理大规模数据集，并且具有良好的可扩展性和容错性。\nInverted Indexing：在这个实验中，作者使用 MapReduce 框架对一个大型文本文件进行倒排索引构建。实验结果表明，MapReduce 可以轻松地处理各种类型的索引构建任务，并且具有良好的可扩展性和容错性。总之，在这些实验中，作者证明了 MapReduce 框架是一种强大而灵活的编程模型和实现方法，在处理和生成大型数据集方面具有很高的效率和可靠性。\n总结 # 这篇论文主要介绍了 MapReduce 编程模型和相关实现，用于处理和生成大型数据集。以下是该论文的主要要点：\nMapReduce 是一种编程模型，用户可以通过定义 map 和 reduce 函数来处理和生成大型数据集。\nMapReduce 框架提供了一个 Master/Worker 模型的分布式系统架构，其中 Master 节点负责协调整个 MapReduce 任务的执行，而 Worker 节点负责执行具体的 map 和 reduce 任务。\nMapReduce 框架隐藏了并行化、容错、本地性优化和负载平衡等细节，使得即使没有并行和分布式系统经验的程序员也很容易使用。\nMapReduce 框架可以轻松地表达许多现实世界的任务，并且已经成功地应用于 Google 的生产 Web 搜索服务、排序、数据挖掘、机器学习等多个系统中。\nMapReduce 框架提供了多种优化技术，以提高任务执行效率和可靠性。这些技术包括本地性优化、数据压缩、内存管理、负载平衡、容错处理等。\nMapReduce 框架具有良好的可扩展性，可以在由数千台机器组成的大型集群上运行，并且可以有效地处理各种类型的故障。总之，该论文介绍了一种简单而强大的编程模型和实现方法，为处理和生成大型数据集提供了一种有效而易于使用的方法。\n","date":"2023-03-08","externalUrl":null,"permalink":"/posts/mapreducelunwenyuedu/","section":"文章","summary":"概述 这篇论文主要介绍了 MapReduce 编程模型和相关实现，用于处理和生成大型数据集。它隐藏了并行化、容错、本地性优化和负载平衡等细节，使得即使对于没有并行和分布式系统经验的程序员也很容易使用。","title":"MapReduce 论文阅读","type":"posts"},{"content":"","date":"2023-03-08","externalUrl":null,"permalink":"/tags/%E7%AE%97%E6%B3%95/","section":"标签","summary":"","title":"算法","type":"tags"},{"content":"📌 本文原发布于掘金社区：GFS 论文阅读\n概述 # 这篇论文主要讲述了 Google 文件系统（GFS）的设计和实现，介绍了 GFS 的目标、架构、组件和数据流，以及租约、快照、恢复等关键技术；讨论了 GFS 高可靠性、高可用性和高性能等特性，并给出了一些实验结果作为佐证；最后还介绍了 GFS 在 Google 内部的使用情况和一些使用案例。\nGFS 整体架构 # GFS 是一个分布式文件系统，由一个主控节点（Master）和多个数据节点（Chunkserver）组成。Master 负责管理文件系统的元数据，如文件名、目录结构和访问控制等。Chunkserver 负责存储和管理实际的数据块，并响应客户端的读写请求。客户端通过与 Master 交互来获取文件位置信息，并直接与 Chunkserver 通信来读取或写入数据。\nMaster：负责管理文件系统的元数据，如文件名、目录结构和访问控制等。Master 还负责维护数据块的副本数量和位置信息，并处理客户端的读写请求。 Chunkserver：负责存储和管理实际的数据块，并响应客户端的读写请求。每个 Chunkserver 通常存储多个数据块，并维护它们之间的一致性。 Client：与 Master 交互来获取文件位置信息，并直接与 Chunkserver 通信来读取或写入数据。Client 还负责缓存最近使用过的数据块，以提高访问速度。 Master # Master 是 GFS 中的主控节点，负责管理文件系统的元数据，如文件名、目录结构和访问控制等。Master 还负责监控 Chunkserver 的状态，并在发生故障时进行数据恢复。当客户端请求读取或写入数据时，它会向 Master 查询文件位置信息，并直接与相应的 Chunkserver 通信来读取或写入数据。Master 会维护一个映射表，将文件名映射到对应的 Chunkserver 和数据块上。当客户端请求创建新文件时，Master 会为该文件分配一个唯一的 64 位 ID，并将其映射到一个或多个 Chunkserver 上。为了保证系统的高可用性和可靠性，GFS 采用了多种技术。例如，在进行写操作时，客户端会将数据块发送给主副本所在的 Chunkserver，并要求主副本将数据块复制到其他副本所在的 Chunkserver 上。同时，在进行原子追加操作时，GFS 需要保证所有副本都按照相同顺序执行追加操作。为此，Master 需要协调不同副本之间的操作顺序，并确保所有副本最终都包含相同的数据块序列。另外，在进行故障恢复时，Master 还需要监控所有 Chunkserver 的状态，并在某个 Chunkserver 失效时将其标记为失效状态，并将其上存储的所有数据块复制到其他正常运行的 Chunkserver 上。\nChunkserver # Chunkserver 是 GFS 中的数据节点，负责存储和管理实际的数据块，并响应客户端的读写请求。每个 Chunkserver 通常会存储多个数据块，并且每个数据块通常会有多个副本存储在不同的 Chunkserver 上，以保证数据的可靠性和一致性。当客户端请求写入数据时，它会将数据块发送给主副本所在的 Chunkserver，并要求主副本将数据块复制到其他副本所在的 Chunkserver 上。当客户端请求读取数据时，它会向 Master 查询文件位置信息，并直接从相应的 Chunkserver 读取数据块。为了保证数据的可靠性和一致性，GFS 采用了多副本机制和校验和技术。每个数据块通常会有多个副本存储在不同的 Chunkserver 上，当某个副本损坏或失效时，可以从其他副本中恢复。同时，在读取或写入数据时，GFS 还会使用校验和技术来检测是否存在损坏或错误。另外，在进行原子追加操作时，GFS 还需要保证所有副本都按照相同顺序执行追加操作。因此，在进行原子追加操作时，GFS 会将新写入的数据块追加到已有数据块的末尾，并保证所有副本都按照相同顺序执行追加操作。\nClient # Client 是 GFS 中的客户端，负责与 Master 和 Chunkserver 通信，并实现文件系统 API。每个应用程序都需要链接 GFS 客户端代码，以便能够访问 GFS 文件系统。当客户端请求读取或写入数据时，它会向 Master 查询文件位置信息，并直接与相应的 Chunkserver 通信来读取或写入数据。在进行写操作时，客户端会将数据块发送给主副本所在的 Chunkserver，并要求主副本将数据块复制到其他副本所在的 Chunkserver 上。在进行原子追加操作时，客户端只需要指定要追加的数据块，GFS 会将其追加到文件末尾，并保证所有副本都按照相同顺序执行追加操作。为了提高系统的性能和可用性，GFS 还支持客户端缓存机制。当客户端读取数据时，它可以将最近使用过的数据块缓存在本地。这样，在下一次访问相同数据块时，就可以直接从缓存中读取，而不必再次向 Chunkserver 发送请求。另外，在进行故障恢复时，Client 还需要监控所有 Chunkserver 的状态，并在某个 Chunkserver 失效时重新连接到其他正常运行的 Chunkserver 上。\n租约 # 租约是 GFS 中的一种机制，用于管理 Chunkserver 对数据块的访问权限。每个 Chunkserver 在访问某个数据块时，需要向 Master 请求获取该数据块的租约。租约有一个初始超时时间，通常为 60秒。如果 Chunkserver 正在修改数据块，则可以向 Master 请求并获得无限期延长租约的权限。在进行写操作时，客户端会将数据块发送给主副本所在的 Chunkserver，并要求主副本将数据块复制到其他副本所在的 Chunkserver 上。此时，主副本需要向 Master 请求获取该数据块的租约，并将其分配给所有副本所在的 Chunkserver。如果某个副本失效或无法响应，则 Master 会收回该副本上的租约，并将其重新分配给其他正常运行的 Chunkserver。另外，在进行文件重命名等操作时，Master 还可能会尝试收回某些 Chunkserver 上的租约，并将其重新分配给其他正常运行的 Chunkserver。\nGFS 的数据读写 # GFS 的数据流是从客户端到 Chunkserver，或者从一个 Chunkserver 到另一个 Chunkserver。客户端通过与 Master 交互来获取文件位置信息，并直接与 Chunkserver 通信来读取或写入数据。当客户端请求写入数据时，它会将数据块发送给主副本所在的 Chunkserver，并要求主副本将数据块复制到其他副本所在的 Chunkserver 上。当客户端请求读取数据时，它会向 Master 查询文件位置信息，并直接从相应的 Chunkserver 读取数据块。如果某个 Chunkserver 失效，Master 会将该 Chunkserver 上存储的所有数据块复制到其他正常运行的 Chunkserver 上，以保证数据不丢失。\n客户端向 Master 查询文件位置信息，Master 返回该文件的元数据，包括文件名、目录结构和数据块位置等。\n客户端根据元数据获取数据块所在的 Chunkserver 列表，并选择其中一个 Chunkserver 作为主副本。\n客户端向主副本发送写请求，并将数据块发送给主副本。主副本将数据块复制到其他副本所在的 Chunkserver 上，并返回写操作结果给客户端。\n在进行原子追加操作时，客户端只需要指定要追加的数据块，GFS 会将其追加到文件末尾，并保证所有副本都按照相同顺序执行追加操作。客户端会收到一个偏移量，表示新写入数据块在文件中的位置。\n在进行读操作时，客户端向主副本或其他副本所在的 Chunkserver 发送读请求，并从相应的 Chunkserver 读取数据块。如果某个副本失效或无法响应，则客户端会尝试从其他正常运行的副本中读取数据块。\n为了提高系统性能和可用性，GFS 支持客户端缓存机制。当客户端读取数据时，它可以将最近使用过的数据块缓存在本地。这样，在下一次访问相同数据块时，就可以直接从缓存中读取，而不必再次向 Chunkserver 发送请求。\n在进行故障恢复时，Master 会监控所有 Chunkserver 的状态，并在某个 Chunkserver 失效时重新连接到其他正常运行的 Chunkserver 上。如果某个副本失效，则 Master 会将其标记为失效状态，并将其上存储的所有数据块复制到其他正常运行的 Chunkserver 上。\n命名空间与锁的实现机制 # 在 GFS 中，Master 负责管理文件系统的元数据，如文件名、目录结构和访问控制等。为了保证多个操作之间的正确性和一致性，GFS 使用了命名空间管理和锁机制。GFS 将整个文件系统划分为多个命名空间，并为每个命名空间分配一个唯一的 ID。Master 维护一个映射表，将文件名映射到对应的命名空间 ID 上。当客户端请求读取或写入数据时，它会向 Master 查询文件位置信息，并直接与相应的 Chunkserver 通信来读取或写入数据。为了保证多个操作之间的正确性和一致性，GFS 使用了锁机制。在进行某些操作时（如创建新文件、删除文件、重命名文件等），客户端需要获取相应命名空间上的锁。如果某个客户端已经获取了该锁，则其他客户端需要等待该客户端释放锁后才能获取该锁并执行相应操作。为了避免死锁和提高系统性能，GFS 使用了两种类型的锁：全局锁和局部锁。全局锁用于保护整个命名空间树结构，并且只有一个全局锁可用。而局部锁则用于保护单个目录或单个文件，并且可以有多个局部锁可用。在进行某些操作时（如创建新目录、删除目录等），客户端需要获取相应目录上的局部锁。如果某个客户端已经获取了该局部锁，则其他客户端需要等待该客户端释放该局部锁后才能获取该局部锁并执行相应操作。\nGC # 在 GFS 中，GC（Garbage Collection）是一种用于回收不再使用的数据块的机制。当某个文件被删除或重命名时，GFS 会将其对应的数据块标记为“垃圾数据块”，并在后续的 GC 过程中将其回收。GFS 会定期执行 GC 操作，并扫描整个文件系统以查找所有未被使用的数据块。如果某个数据块已经被标记为“垃圾数据块”，则 GFS 会将其从所有 Chunkserver 上删除，并释放相应的存储空间。为了避免误删和提高系统性能，GFS 采用了多种技术来优化 GC 操作。例如，在进行 GC 操作时，GFS 会先将所有“垃圾数据块”标记为“待删除状态”，并等待一段时间以确保这些数据块确实不再被使用。如果在等待期间某个客户端又访问了这些数据块，则 GFS 会取消对该数据块的删除操作，并重新标记为“正常状态”。另外，在进行 GC 操作时，GFS 还需要考虑多副本机制和租约机制。在删除某个副本上的“垃圾数据块”时，GFS 需要先检查该副本上是否还有其他正在使用该数据块的租约。如果存在租约，则不能立即删除该副本上的“垃圾数据块”，而需要等待租约过期或者其他副本上复制完成后再进行删除。\nGFS 快照 # GFS 支持快照功能，可以在不影响正在进行的写操作的情况下，对文件系统进行快照。快照是文件系统状态的一份静态副本，可以用于数据备份、恢复和测试等目的。GFS 使用了一种称为“写入时复制”的技术来实现快照功能。当客户端请求创建快照时，Master 会为每个数据块创建一个新的副本，并将这些副本标记为只读状态。这样，客户端后续的写操作会被重定向到新创建的副本上，而不会影响原始数据块。\n如何进行数据恢复 # GFS 使用了多种技术来保证数据的可靠性和一致性，并在发生故障时进行数据恢复。GFS 通过以下方式进行数据恢复：\n定期进行心跳检测：Master 会定期向所有 Chunkserver 发送心跳信号，以检测它们是否正常运行。如果某个 Chunkserver 没有响应，则 Master 会将其标记为失效状态，并将其上存储的所有数据块复制到其他正常运行的 Chunkserver 上。\n使用校验和检测数据损坏：GFS 使用校验和技术来检测数据块是否损坏。每个数据块都有一个对应的校验和，当读取数据块时，GFS 会计算校验和并与存储在元数据中的校验和进行比较。如果两者不匹配，则说明该数据块已经损坏，需要从其他副本中恢复。\n多副本机制：GFS 使用多副本机制来保证数据的可靠性。每个数据块通常有多个副本存储在不同的 Chunkserver 上，当某个副本损坏或失效时，可以从其他副本中恢复。\n快速恢复机制：当某个 Chunkserver 失效时，Master 会立即将其标记为失效状态，并将其上存储的所有数据块复制到其他正常运行的 Chunkserver 上。这样可以快速地恢复失效节点上的所有数据。\n松散的一致性模型 # GFS 采用了一种称为“松散一致性模型”的技术来保证数据的一致性。在这种模型下，不同副本之间的数据可能会存在短暂的不一致状态，但最终会达到一致状态。这种模型相对于严格一致性模型来说，可以提高系统的性能和可用性。在 GFS 中，客户端进行写操作时，会将数据块发送给主副本所在的 Chunkserver，并要求主副本将数据块复制到其他副本所在的 Chunkserver 上。当客户端请求读取数据时，它会向 Master 查询文件位置信息，并直接从相应的 Chunkserver 读取数据块。在这个过程中，如果某个副本没有及时更新，则可能导致不同副本之间存在短暂的不一致状态。为了解决这个问题，GFS 使用了原子追加操作来保证数据的一致性。在进行追加操作时，GFS 会将新写入的数据块追加到已有数据块的末尾，并保证所有副本都按照相同顺序执行追加操作。这样可以保证所有副本最终都包含相同的数据块序列，从而达到一致状态。\n原子追加 # 原子追加操作是 GFS 中的一种特殊的写操作，用于保证多个客户端同时向同一个文件追加数据时的一致性。在传统的写操作中，客户端需要指定数据写入的偏移量，这可能导致多个客户端同时向同一个文件的相同位置写入数据，从而导致数据不一致。而在原子追加操作中，客户端只需要指定要追加的数据块，GFS 会将其追加到文件末尾，并保证所有副本都按照相同顺序执行追加操作。在进行原子追加操作时，GFS 会将新写入的数据块追加到已有数据块的末尾，并保证所有副本都按照相同顺序执行追加操作。这样可以保证所有副本最终都包含相同的数据块序列，从而达到一致状态。另外，在进行原子追加操作时，GFS 还会返回一个偏移量给客户端，表示新写入数据块在文件中的位置。原子追加操作可以有效地避免多个客户端同时向同一个文件写入数据时可能出现的冲突和不一致问题。\nGFS 的高可用性和高性能 # 高可用的实现 # 数据副本：GFS 将每个数据块复制到多个 Chunkserver 上，以实现数据冗余和容错。当某个 Chunkserver 失效时，GFS 会自动将存储在该 Chunkserver 上的数据块复制到其他正常运行的 Chunkserver 上，并重新分配相应的副本。这样可以保证即使某个 Chunkserver 失效，系统仍然可以继续正常运行。\nChunkserver 失效检测：为了及时发现 Chunkserver 失效并进行处理，GFS 会定期向各个 Chunkserver 发送心跳包，并检查其响应情况。如果某个 Chunkserver 长时间未响应，则 GFS 会将其标记为失效状态，并将其上存储的所有数据块复制到其他正常运行的 Chunkserver 上。\n副本重分布：当某个 Chunkserver 失效或新增 Chunkserver 时，GFS 会自动将存储在该 Chunkserver 上的数据块复制到其他正常运行的 Chunkserver 上，并重新分配相应的副本。为了避免过多地影响系统性能和可用性，GFS 会限制每个 Chunkserver 每秒钟可以复制的数据块数量，并根据网络带宽和负载情况动态调整复制速度。\nMaster 备份：在 GFS 中有一个 Master 节点负责管理整个文件系统的元数据，并协调各个 Chunkserver 之间的操作。为了保证 Master 节点的高可用性，GFS 采用了 Master 备份机制。在 GFS 中有一个或多个备份 Master 节点，它们会定期从主 Master 节点同步元数据，并在主 Master 节点失效时接管其工作。\n锁机制：在进行某些操作时（如创建新文件、删除文件、重命名文件等），客户端需要获取相应命名空间上的锁。如果某个客户端已经获取了该锁，则其他客户端需要等待该客户端释放锁后才能获取该锁并执行相应操作。这样可以保证多个操作之间的正确性和一致性。\n高性能的实现 # 数据切块：GFS 将大文件切分成多个数据块，并将这些数据块分散存储在多个 Chunkserver 上。这样每个 Chunkserver 只需要处理部分数据，从而提高系统的并行度和吞吐量。\n数据本地化：GFS 会尽可能地将客户端访问的数据块存储在距离客户端最近的 Chunkserver 上，以减少网络传输延迟和带宽消耗。同时，GFS 还会根据 Chunkserver 的负载情况和网络带宽等因素动态调整数据块的存储位置，以保证系统性能和可用性。\n副本重分布：当某个 Chunkserver 失效或新增 Chunkserver 时，GFS 会自动将存储在该 Chunkserver 上的数据块复制到其他正常运行的 Chunkserver 上，并重新分配相应的副本。为了避免过多地影响系统性能和可用性，GFS 会限制每个 Chunkserver 每秒钟可以复制的数据块数量，并根据网络带宽和负载情况动态调整复制速度。\n快速恢复：当某个 Chunkserver 失效时，GFS 会自动将存储在该 Chunkserver 上的数据块复制到其他正常运行的 Chunkserver 上，并重新分配相应的副本。为了尽快恢复失效节点上存储的数据，GFS 采用了一种快速恢复机制，即在新节点加入集群后立即向其传输一些热点数据块。\n并发控制：为了保证多个操作之间的正确性和一致性，在进行某些操作时（如创建新文件、删除文件、重命名文件等），客户端需要获取相应命名空间上的锁。如果某个客户端已经获取了该锁，则其他客户端需要等待该客户端释放锁后才能获取该锁并执行相应操作。这样可以保证多个操作之间的正确性和一致性。\n总结 # 这篇论文主要介绍了 Google File System（GFS）的设计和实现。GFS 是一个分布式文件系统，由一个主控节点（Master）和多个数据节点（Chunkserver）组成。Master 负责管理文件系统的元数据，如文件名、目录结构和访问控制等。Chunkserver 负责存储和管理实际的数据块，并响应客户端的读写请求。客户端通过与 Master 交互来获取文件位置信息，并直接与 Chunkserver 通信来读取或写入数据。GFS 具有高可靠性、高可用性和高性能的特点。它使用多副本机制和校验和技术来保证数据的可靠性，使用了多种技术来保证系统的高可用性，例如定期进行心跳检测、使用快速恢复机制等。同时，GFS 通过并行化、负载均衡和缓存等技术来提高系统的性能。另外，论文还介绍了 GFS 的一些特色功能，如快照功能、松散一致性模型、原子追加操作等。总之，GFS 是一个适用于大规模数据处理工作负载的分布式文件系统，在 Google 内部得到了广泛应用，并且对其他分布式文件系统的设计也产生了重要影响。\nGFS 的开源实现 # Hadoop branch 0.1\ngithub.com/apache/hado…\n","date":"2023-03-08","externalUrl":null,"permalink":"/posts/gfslunwenyuedu/","section":"文章","summary":"概述 这篇论文主要讲述了 Google 文件系统（GFS）的设计和实现。它介绍了 GFS 的目标、架构、组件、数据流和一些关键技术，如租约、快照和恢复等。","title":"GFS 论文阅读","type":"posts"},{"content":"📌 本文原发布于掘金社区：配送特征平台\n一、业务背景 # 现实世界中，一个运单的完成过程是从用户下单开始到用户收餐骑手离客为止，需经历一系列室内室外的场景——用户下单，系统派单，骑手室外骑行到目的地然后下车步行到商家，等待取餐驻留室内，商家出餐，骑手取餐离店后步行上车，室外骑行到用户目的地，下车步行上楼送餐，驻留室内等待用户收餐，最后用户收餐骑手离客。那么特征平台就是将上述过程中涉及到骑手的基础数据沉淀下来，进行数据挖掘并提供特征数据，来辅助算法平台更好地支撑骑手派单、配送费计算以及倾斜等场景。\n二、整体架构与服务拆分 # 整体架构 # 特征平台的整体架构需要兼顾下面的基本规范。\n流程标准化：从数据输入，加工计算到数据输出做了一个流程的标准化。 数据分层，将共性沉淀下来。 特征兜底，降低风险。 基于定制的基本规范，服务的整体架构如上图所示，共分为 7 层：\n数据源层：主要有线上的业务表、运单表、骑手表等，以及离线 Hive 表 数据层：对数据进行清洗和转换，最后形成实时特征与离线特征的宽表。 计算层：通过标准化的 SQL 对宽表数据进行计算。 存储层：存储计算层输出的特征数据。 服务层：提供统一的 RPC 特征读取服务，将存储层数据统一输出给应用层应用。 应用层：主要有 ETA 时间预估服务、分单引擎服务、骑手管控服务以及供需服务。 管理系统：负责特征元数据从创建到销毁的整个生命周期的管理，以及特征的计算口径、数据源、存储格式和特征默认值的管理。 监控报警：包括对特征生产链路服务依赖的监控，以及对特征数据质量的可视化，通过人工巡检的方式发现数据问题。 服务拆分 # 特征读取服务：抽象出统一的特征读取 RPC 接口。 实时特征写入服务：提供基于 RPC，HTTP，MQ 的实时特征数据写入。 离线特征写入服务：依赖大数据平台，对离线数据进行在线化操作。 特征管理平台：特征元数据管理。三、特征抽象 数据源模型（feature_source）：抽象特征生产源，来定义特征生产数据源的元数据模型。如下 { \u0026#34;product_id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;feature_ids\u0026#34;: \u0026#34;\u0026#34; } 特征使用业务方模型(feature_business)：抽象使用特征的业务方，来定义业务方的元数据模型。如下\n{ \u0026#34;business_id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;feature_ids\u0026#34;: \u0026#34;\u0026#34; } 特征元数据(feature)：通过抽象特征的共性，来定义特征的元数据模型。如下\n{ \u0026#34;feature_id\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;feature_name_space\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;object_id\u0026#34;:\u0026#34;\u0026#34;, // 特征实体ID（骑手，店铺，用户等维度），骑手的特征就是骑手id \u0026#34;feature_val\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;expire_time\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;update_time\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;source\u0026#34;: \u0026#34;\u0026#34; } 特征数据源，特征业务方以及特征三者之间的关系：\nfeature_source vs feature (1:n)\nfeature_business vs feature (1:n)\n四、实时特征 # 介绍实时特征从数据源到存储的整个流程。\n上图是整个实时特征从数据源到存储的整个流程，整个架构可以分成 4 层，\n特征管理平台：负责特征从创建到销毁的整个生命周期的管理，以及特征管理相关的其他业务抽象的管理，包括特征实时生产任务，特征离线生产任务管理等。 任务调度平台：使用开源的 airflow 分布式调度平台，通过抽象特征生产任务的共性，定制 DAG 任务，实现实时特征的秒级或者分钟级的生产，将生产的特征数据写入到 MQ。 flink 数据同步任务：通过 flink 任务实时同步线上业务数据到索引宽表与明细宽表。 consumer 服务：提供 RPC，MQ，HTTP 的特征写入，将特征数据按照特征管理平台定制的统一数据模型写入到存储中。 五、离线特征 # 介绍离线特征从数据源到存储的整个流程。\n上图是整个离线特征从数据源到存储的整个流程，整个架构可以分成 4 层，\n任务调度平台：这里的任务调度平台是指大数据的离线任务调度平台，负责离线任务调度的整个生命周期。 特征管理平台：负责特征从创建到销毁的整个生命周期的管理，以及特征管理相关的其他业务抽象的管理，包括特征实时生产任务，特征离线生产任务管理等。 特征抽取任务：抽象出统一的特征抽取任务，依赖特征管理平台的配置信息，生成标准的 Hive SQL，并将抽取结果数据写入到特征共享表（特征宽表）。 特征聚合任务：将特征宽表的数据根据特征管理平台的配置，生成特定业务方的特征数据。 特征同步任务：将聚合任务生成的结果数据，通过离线数据在线化的工具加载到线上存储。 六、特征算子的思考 # 举个例子：有一个骑手历史完单量的特征 a（T-1），以及另一个骑手当天完单量的实时特征 b。现在我们需要一个骑手完单量的特征 c，c = a + b。对于这种情况我们可以复用 a，b 两个特征来计算特征 c。对于上述情况，就可以设计通用的算子来进行特征的复用，只要简单地提供一些配置项，就可以在不写代码的情况下完成计算和存储流程。比如上面的骑手完单量特征的表达式算子，可以支持下面这些形式的“计算”：\n七、相关资料 # 人工智能在线特征系统中的生产调度\n人工智能在线特征系统中的数据存取技术\n美团外卖特征平台的建设与实践\n美团配送实时特征平台建设实践\n一套实时特征系统的迭代过程\n","date":"2021-06-21","externalUrl":null,"permalink":"/posts/peisongtezhengpingtai/","section":"文章","summary":"现实世界中，一个运单的完成过程是从用户下单开始到用户收餐骑手离客为止，需经历一系列室内室外的场景——用户下单，系统派单，骑手室外骑行到目的地然后下车步行到商家，等待取餐驻留室内，商家出餐，骑手取餐离店","title":"配送特征平台","type":"posts"},{"content":"","date":"2021-05-12","externalUrl":null,"permalink":"/tags/kafka/","section":"标签","summary":"","title":"Kafka","type":"tags"},{"content":"📌 本文原发布于掘金社区：Kafka 的 Replica 机制\nKafka 是一个分布式的发布-订阅消息系统。它最初是在 LinkedIn 开发的，2011年7月成为一个 Apache 项目。今天，Kafka 被 LinkedIn、Twitter 和 Square 用于日志聚合、队列、实时监控和事件处理等应用程序。下面我们来讨论一下 Kafka 的 replication 设计。\nreplication 的目的是为了保证服务的高可用，即使有些节点失败，Producer 可以继续发布消息，Consumer 可以继续接收消息。\n保证数据一致性的方式 # 有两种典型的方式来保证数据的强一致。这两种方式都要求指定一个 leader，所有的写都是发送给 leader。leader 负责接收所有的写请求，并以相同的顺序将这些写传播给其他 follower。\n多数复制 # 基于多数提交的方式。leader 要等到大多数 follower 接收到数据之后才认为数据是可提交的。在 leader 失败的情况下，通过多数 follower 的协调选出新的 leader。这种方式的算法有 raft、paxos 等算法，比如 ZooKeeper、Google Spanner、etcd 等。这种方式在有 2n + 1 个节点的情况下，最多可以容忍 n 个节点失败。\n主从复制 # 基于主从复制的方式。需要等 leader 和 follower 都写入成功才算消息接收成功，在有 n 个节点的情况下，最多可以容忍 n-1 个节点失败。\nKafka 使用的是主从复制的方式来实现集群之间的日志复制。原因如下：\n基于主从复制的方式可以在相同数量的副本中容忍更多故障。也就是说，它可以容忍带有 n + 1 个副本的 n 个故障，而基于多数复制的方式通常只能容忍带有 2n +1 个副本的 n 个故障。例如，如果只有 2 个副本，则基于多数复制的方式不能容忍任何故障。Kafka 的日志复制主要考虑的是同一个数据中心机器之间的数据复制，相对来说延迟并不会成为日志复制的瓶颈。\n几个概念 # 在 Kafka 中，消息流是由 topic 定义的，topic 被划分为一个或多个 partition。而复制发生在 partition 级别，每个 partition 都有一个或多个副本（replica）。\n在 Kafka 集群中，将副本均匀地分配到不同的 broker 上，每个副本都在磁盘上维护一个日志。发布的消息按顺序附加到日志中，每条消息都通过日志中的单调递增 offset 来标识。offset 是分区中的逻辑概念。给定一个 offset，可以在每个分区副本中标识相同的消息。当 consumer 订阅某个主题时，它会跟踪每个分区中用于消费的偏移量，并使用它向 broker 发出读取请求。\n如上图所示，当 producer 将消息发布到 topic 的某个 partition 时，该消息首先被转发到该 partition 的 leader 副本，并追加到其日志中。follower 的副本不断地从 leader 那里获取新的信息。一旦有足够多的副本接收到消息，leader 就提交消息。有个问题就是 leader 如何决定到什么程度才算足够。leader 不能总是等待所有副本的写操作完成。这样为了保证数据一致性而降低我们服务的可用性是不可行的，这是因为任何跟随者副本都可能失败，而领导者不能无限地等待。那么如何解决这个问题呢？Kafka 为了解决这种情况下出现的问题，提出了一种新的解决方案 ISR，下面是 ISR 的详细介绍。\nKafka 的 ISR 模型 # 为了解决上面提出的问题，Kafka 采用了一种折中的方案，引入了 ISR 的概念。ISR 是 in-sync replicas 的简写。ISR 的副本保持和 leader 的同步，当然 leader 本身也在 ISR 中。初始状态所有的副本都处于 ISR 中，当一条消息发送给 leader 的时候，leader 会等待 ISR 中所有的副本告诉它已经接收了这个消息，如果一个副本失败了，那么它会被移出 ISR。下一条消息来的时候，leader 就会将消息发送给当前 ISR 中的节点了。\n同时，leader 还维护着 HW(high watermark)，这是一个分区的最后一条消息的 offset。leader 会持续地将 HW 发送给 follower，broker 可以将它写入到磁盘中以便将来恢复。\n当一个失败的副本重启的时候，它首先恢复磁盘中记录的 HW，然后将它的消息同步到 HW 这个 offset。这是因为 HW 之后的消息不保证已经 commit。这时它变成了一个 follower，从 HW 开始，从 Leader 中同步数据，一旦追上 leader，它就可以重新加入到 ISR 中。\nKafka 使用 ZooKeeper 实现 leader 选举。如果 leader 失败，controller 会从 ISR 选出一个新的 leader。leader 选举的时候可能会有数据丢失，但是已经提交的消息保证不会丢失。\n数据一致性与服务可用性的权衡 # 为了保证数据的一致性，Kafka 提出了 ISR，在同步日志到 follower 的时候为了提高服务的可用性，follower 在将 leader 同步的日志写入内存后，就将日志写入成功的标志返回给 leader。然后这些操作都是可以通过 Kafka 的配置来实现的。\n参考文档 # kafka.apache.org/documentati… colobu.com/2017/11/02/… engineering.linkedin.com/kafka/intra… cwiki.apache.org/confluence/… ","date":"2021-05-12","externalUrl":null,"permalink":"/posts/kafkadereplicajizhi/","section":"文章","summary":"Kafka 是一个分布式的发布-订阅消息系统。它最初是在 LinkedIn 开发的，2011年7月成为一个 Apache 项目。","title":"Kafka 的 Replica 机制","type":"posts"},{"content":"","date":"2021-01-14","externalUrl":null,"permalink":"/categories/java/","section":"分类","summary":"","title":"Java","type":"categories"},{"content":"📌 本文原发布于代码星冰乐：Kafka 的日志复制机制\nKafka 是一个分布式的发布-订阅消息系统。它最初是在 LinkedIn 开发的，2011年7月成为一个 Apache 项目。今天，Kafka 被 LinkedIn、Twitter 和 Square 用于日志聚合、队列、实时监控和事件处理等应用程序。在下面的文章中，我们将讨论 Kafka 的 replication 设计。\\\nreplication 的目的是提供服务的高可用，即使有些节点失败了，Producer 可以继续发布消息，Consumer 可以继续接收消息。\n保证数据一致性的方式 # 有两种典型的方式来保证数据的强一致。这两种方式都要求指定一个 leader，所有的写都是发送给 leader。leader 负责接收所有的写请求，并以相同的顺序将这些写传播给其他 follower。\n多数复制 # 基于多数提交的方式。leader 要等到大多数 follower 接收到数据之后才认为数据是可提交的。在 leader 失败的情况下，通过多数 follower 的协调选出新的 leader。这种方式的算法有 raft、paxos 等，比如 ZooKeeper、Google Spanner、etcd 等。这种方式在有 2n + 1 个节点的情况下，最多可以容忍 n 个节点失败。\n主从复制 # 基于主从复制的方式。需要等 leader 和 follower 都写入成功才算消息接收成功，在有 n 个节点的情况下，最多可以容忍 n-1 个节点失败。\nKafka 使用主从复制的方式来实现集群之间的日志复制。原因如下：\n基于主从复制的方式可以在相同数量的副本中容忍更多故障。也就是说，它可以容忍带有 n + 1 个副本的 n 个故障，而基于多数复制的方式通常只能容忍带有 2n +1 个副本的 n 个故障。例如，如果只有 2 个副本，则基于多数复制的方式不能容忍任何故障。\nKafka 的日志复制主要考虑的是同一个数据中心的机器之间的数据复制，相对来说延迟并不会成为日志复制的瓶颈。\n几个概念 # 在 Kafka 中，消息流是由 topic 定义的，topic 被划分为一个或多个 partition。而复制发生在 partition 级别，每个 partition 都有一个或多个副本。\n📷 图注：topic 的逻辑关系 📷 图注：topic 的屋里存储关系\n在 Kafka 集群中，将副本均匀地分配到不同的 broker 上。每个副本都在磁盘上维护一个日志。发布的消息按顺序附加到日志中，每条消息都通过日志中的单调递增 offset 来标识。\noffset 是分区中的逻辑概念。给定一个 offset，可以在每个分区副本中标识相同的消息。当 consumer 订阅某个主题时，它会跟踪每个分区中用于消费的偏移量，并使用它向 broker 发出读取请求。\n如上图所示当 producer 将消息发布到 topic 的某个 partition 时，该消息首先被转发到该 partition 的 leader 副本，并追加到其日志中。follower 的副本不断地从 leader 那里获取新的信息。一旦有足够多的副本接收到消息，leader 就提交消息。\n有个问题是 leader 如何决定到什么程度是足够的。leader 不能总是等待所有副本的写操作完成。这样为了保证数据一致性而降低我们服务的可用性是不可行的，这是因为任何跟随者副本可以失败，而领导者不能无限地等待。\nKafka 的 ISR 模型 # 为了解决上面提出的问题，Kafka 采用了一种折中的方案，引入了 ISR 的概念。ISR 是 in-sync replicas 的简写。ISR 的副本保持和 leader 的同步，当然 leader 本身也在 ISR 中。初始状态所有的副本都处于 ISR 中，当一个消息发送给 leader 的时候，leader 会等待 ISR 中所有的副本告诉它已经接收了这个消息，如果一个副本失败了，那么它会被移出 ISR。下一条消息来的时候，leader 就会将消息发送给当前 ISR 中的节点了。\n同时，leader 还维护着 HW(high watermark),这是一个分区的最后一条消息的 offset。HW 会被持续地发送给 follower，broker 可以将它写入到磁盘中以便将来恢复。\n当一个失败的副本重启的时候，它首先恢复磁盘中记录的 HW，然后将它的消息同步到 HW 这个 offset。这是因为 HW 之后的消息不保证已经 commit。这时它变成了一个 follower，从 HW 开始，从 Leader 中同步数据，一旦追上 leader，它就可以再加入到 ISR 中。\nKafka 使用 ZooKeeper 实现 leader 选举。如果 leader 失败，controller 会从 ISR 选出一个新的 leader。leader 选举的时候可能会有数据丢失，但是 committed 的消息保证不会丢失。\n故障恢复，leader 重新选举的表述~\n数据一致性与服务可用性的权衡 # 为了保证数据的一致性，Kafka 提出了 ISR，在同步日志到 follower 的时候为了提高服务的可用性，follower 在将 leader 同步的日志写入内存后就返回给 leader 日志写入成功的标志。然后这些操作都是可以通过 Kafka 的配置来实现的。\n参考文档 # https://kafka.apache.org/documentation/#replication https://colobu.com/2017/11/02/kafka-replication/ https://engineering.linkedin.com/kafka/intra-cluster-replication-apache-kafka https://cwiki.apache.org/confluence/display/KAFKA/kafka+Detailed+Replication+Design+V3 ","date":"2021-01-14","externalUrl":null,"permalink":"/posts/kafka-de-ri-zhi-fu-zhi-ji-zhi/","section":"文章","summary":"Kafka 是一个分布式的发布 订阅消息系统。它最初是在 LinkedIn 开发的，2011年7月成为一个 Apache 项目。今天，Kafka 被 LinkedIn、Twitter 和 Square 用于日志聚合、队列、实时监控和事件处","title":"Kafka 的日志复制机制","type":"posts"},{"content":"","date":"2021-01-14","externalUrl":null,"permalink":"/tags/%E6%BA%90%E7%A0%81/","section":"标签","summary":"","title":"源码","type":"tags"},{"content":"","date":"2021-01-13","externalUrl":null,"permalink":"/tags/zookeeper/","section":"标签","summary":"","title":"ZooKeeper","type":"tags"},{"content":"📌 本文原发布于掘金社区：ZooKeeper 与分布式锁\n在上篇文章中讨论了基于 Redis 的单机分布式锁与集群分布式锁的方案，在数据一致性要求不是很高的情况下，Redis 实现的分布式锁可以满足我们的要求。最近拜读了 ZooKeeper 的论文之后，对于 ZooKeeper 实现的分布式锁，也是有必要了解一下的。\nZooKeeper 是什么？ # ZooKeeper 的论文是这样描述的：\nZooKeeper 是一种用于协调分布式应用程序的服务。由于 ZooKeeper 是关键基础结构的一部分，因此 ZooKeeper 旨在提供一个简单而高性能的内核，以供客户端构建更复杂的协调原语。它在复制的集中式服务中合并了来自组消息传递，共享寄存器和分布式锁定服务的元素。ZooKeeper 公开的接口具有共享寄存器的免等待方面，它具有事件驱动机制，类似于分布式文件系统的缓存失效，以提供简单而强大的协调服务。\nZooKeeper 实现的简单的分布式锁 # 使用 ZooKeeper 实现分布式锁的最简单的方案是在 ZooKeeper 中创建一个临时状态（EPHEMERAL）的锁节点，为了获取锁，客户端尝试使用 EPHEMERAL 标志创建指定的 znode（如 lock ）。如果创建成功，则客户端将持有该锁。否则，客户端可以读取设置了监视标志的 znode，以便在当前领导者死亡时得到通知。客户端死亡或显式删除 znode 时会释放该锁。其他等待锁的客户端一旦观察到 znode 被删除，就会再次尝试获取锁。\n对 ZooKeeper 分布式锁的优化 # 对于上面的方案，细心的同学可能会发现，如果同时有 n 个客户端去获取锁资源，就只能有一个客户端获取锁成功，那么就会有 n-1 个客户端设置对锁节点的监听，在 n 比较大的情况下，客户端获取锁的时间会被延长，甚至是无限延长，在某些情况下是不可接受的。\n对于上面的这种极端情况，ZooKeeper 官方给出了比较好的解决方案，就是将竞争的客户端排队，具体实现的伪代码如下所示：\nLock 1 n = create(l + “/lock-”, EPHEMERAL|SEQUENTIAL) 2 C = getChildren(l, false) 3 if n is lowest znode in C, exit 4 p = znode in C ordered just before n 5 if exists(p, true) wait for watch event 6 goto 2 Unlock 1 delete(n) 在上面的代码中，Lock 的第 1 行中使用 SEQUENTIAL 标志，可确保客户端获取锁的尝试相对于所有其他尝试排序。如果客户端的 znode 在第 3 行的序列号最低，则客户端将持有该锁。否则，客户端将等待删除持有锁或将在该客户端的 znode 之前获得锁的 znode。通过仅查看客户端 znode 之前的 znode，我们仅在释放锁或放弃锁请求时才唤醒一个进程，从而避免了羊群效应。客户端监视的 znode 消失后，客户端必须检查它现在是否持有该锁。（先前的锁定请求可能已被放弃，并且具有较低序号的 znode 仍在等待或持有锁。）\n总而言之，这个锁的实现方案具有以下优点：\n1.删除一个 znode 只会导致一个客户端唤醒，因为每个 znode 都被另一个客户端监视，因此我们没有羊群效应。 2.没有轮询或超时。 3.由于我们实现锁定的方式，因此通过浏览 ZooKeeper 数据可以看到锁定争用、中断以及调试锁定问题的数量。 // Lock attempts to acquire the lock. It will wait to return until the lock // is acquired or an error occurs. If this instance already has the lock // then ErrDeadlock is returned. func (l *Lock) Lock() error { if l.lockPath != \u0026#34;\u0026#34; { return ErrDeadlock } prefix := fmt.Sprintf(\u0026#34;%s/lock-\u0026#34;, l.path) path := \u0026#34;\u0026#34; var err error for i := 0; i \u0026lt; 3; i++ { path, err = l.c.CreateProtectedEphemeralSequential(prefix, []byte{}, l.acl) if err == ErrNoNode { // Create parent node. parts := strings.Split(l.path, \u0026#34;/\u0026#34;) pth := \u0026#34;\u0026#34; for _, p := range parts[1:] { var exists bool pth += \u0026#34;/\u0026#34; + p exists, _, err = l.c.Exists(pth) if err != nil { return err } if exists == true { continue } _, err = l.c.Create(pth, []byte{}, 0, l.acl) if err != nil \u0026amp;\u0026amp; err != ErrNodeExists { return err } } } else if err == nil { break } else { return err } } if err != nil { return err } seq, err := parseSeq(path) if err != nil { return err } for { children, _, err := l.c.Children(l.path) if err != nil { return err } lowestSeq := seq prevSeq := -1 prevSeqPath := \u0026#34;\u0026#34; for _, p := range children { s, err := parseSeq(p) if err != nil { return err } if s \u0026lt; lowestSeq { lowestSeq = s } if s \u0026lt; seq \u0026amp;\u0026amp; s \u0026gt; prevSeq { prevSeq = s prevSeqPath = p } } if seq == lowestSeq { // Acquired the lock break } // Wait on the node next in line for the lock _, _, ch, err := l.c.GetW(l.path + \u0026#34;/\u0026#34; + prevSeqPath) if err != nil \u0026amp;\u0026amp; err != ErrNoNode { return err } else if err != nil \u0026amp;\u0026amp; err == ErrNoNode { // try again continue } ev := \u0026lt;-ch if ev.Err != nil { return ev.Err } } l.seq = seq l.lockPath = path return nil } 小结 # 就这样吧，先水一篇文章~\n参考 # Note on fencing and distributed locks\n基于 ZooKeeper 的分布式锁原理及实现\n","date":"2021-01-13","externalUrl":null,"permalink":"/posts/zookeeper-yufenbushisuo/","section":"文章","summary":"在上篇文章中讨论了基于 Redis 的单机分布式锁与集群分布式锁的方案，在数据一致性要求不是很高的情况下，Redis 实现的分布式锁可以满足我们的要求。最近在拜读了 ZooKeeper 的论文之后，对于 ZooKeeper 实现的分布式锁，也是有必要了解一下的。使用 Zook…","title":"ZooKeeper 与分布式锁","type":"posts"},{"content":"📌 本文原发布于代码星冰乐：从 20 到 21\n这一年过得不容易，记个流水账吧~\\\n一场突如其来的瘟疫 # 一场突如其来的瘟疫，开启了 2020年，刚开始的时候口罩都买不起了快，一时是京城口罩贵~\n因为疫情宅家的小半年，倒是学着自己做了不少道菜。\n两个月考下来的驾照 # 北京疫情缓解之后，就被对象催着报了考驾照的班，我这个人做什么事的时候比较拖，多亏了她~\n学驾照的时候，科目二一共八个课时，硬是换了八个教练，奈何我天赋异禀，科目二科目三都是一把过。\n两次远行，见识了不一样的风景 # 一次成都，一次上海。\n人生第一次坐飞机，在飞机爬坡的时候心跳真的超级快，感觉要跳出来一样。\n两次莫名其妙的车祸 # 第一场车祸，十一的时候仗着自己刚学的驾照，租了一辆车出去浪，一个不小心在变道的时候把别人的车给刮了，万幸的是租车的时候买了保险，除了交警的罚单，倒是没有什么其他损失。在这里学到好多，就是做事不能太过于循规蹈矩。再一个就是与人交流，不要太自以为是以及轻易地轻信于他人。\n第二场车祸，这次是打的滴滴，我是乘客，万幸的是人没出事，我当时直接下车就离开了，不知道滴滴师傅后来怎么样了。\n数十趟北京天津之间的往来 # 年初的时候就开始打算天津落户的事情，在落户的事差不多稳了的时候，就开始看天津的房子了，鉴于最近两年还会在北京工作就没有看市区的二手房，主要看天津环城四区的新房，应该说主要是看北辰跟西青的新房。\n这期间还有个插曲就是之前过于着急没看清楚房子的信息，就稀里糊涂交了定金，后来又后悔退定金的问题，细节就不多说了。总的来说天津的房地产监管还是比较严格的，让我对这个城市有了一定的好感~\n这期间北京天津之间的往返没少跑，不过离得是真的近~\n一套上百万的房子 # 在看了大半年的房子之后，在综合多方面考虑之后，定下了在天津这座城市的第一套房，也是我人生中的第一套房，一套上百万的房子，从此沦为银行的佃农。\n交首付的时候，当五十多万的存款从我银行卡上一笔划走的时候，我的心里竟然没有一丝丝的波澜，镇定得像这钱从来都不属于我一样。\n在这里要感谢借钱给我，助我凑够首付的每一个人，谢谢你们~\n2020年其他的东西 # 从业三年多，2020年从一月到十二月是我第一个完整的一年，一直呆在一个公司工作，为自己的坚持鼓掌。\n关于理财，2020年跟着 A 股的大牛市，稍稍赚了一点，没有大富大贵的命，小赚不亏就好。\n总结一下吧 # 这一年是我有限人生中过得极其快速的一年，眨眼之间，真的是眨眼之间~\n2021年会发生什么 # 2021年会发生什么，我也不知道，至于 2021年自己能做成什么事，先挖个坑，到明年的这个时候再来补吧……\n不过，最明显的就是 2021年突然意识到自己开始要奔三了，岁月真的是把杀猪刀~\n","date":"2021-01-02","externalUrl":null,"permalink":"/posts/cong-20-dao-21/","section":"文章","summary":"这一年过得不容易，记个流水账吧~  一场突如其来的瘟疫 一场突入起来的瘟疫，开启了 2020年，刚开始的时候口罩都买不起了快，一时是京城口罩贵~ 因为疫情宅家的小半年，倒是学着自己做了不少道菜。两个月考下来的驾照 之后北京疫情缓解了的时候","title":"从 20 到 21","type":"posts"},{"content":"","date":"2021-01-02","externalUrl":null,"permalink":"/tags/%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93/","section":"标签","summary":"","title":"年度总结","type":"tags"},{"content":"","date":"2020-07-01","externalUrl":null,"permalink":"/categories/go/","section":"分类","summary":"","title":"Go","type":"categories"},{"content":"📌 本文原发布于代码星冰乐：go 并发编程\n基本的同步原语 # Go 语言在 sync 包中提供了用于同步的一些基本原语，包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once。\nMutex # 数据结构\nGo 语言的 sync.Mutex 由两个字段 state 和 sema 组成。其中 state 表示当前互斥锁的状态，而 sema 是用于控制锁状态的信号量。\ntype Mutex struct { state int32 sema uint32 } 上述两个字段加起来只占 8 字节空间的结构体表示了 Go 语言中的互斥锁。\n实现原理\n互斥锁的状态比较复杂，如下图所示，最低三位分别表示 mutexLocked、mutexWoken 和 mutexStarving，剩下的位置用来表示当前有多少个 Goroutine 等待互斥锁的释放。\n在默认情况下，互斥锁的所有状态位都是 0，int32 中的不同位分别表示了不同的状态：\nmutexLocked — 表示互斥锁的锁定状态； mutexWoken — 表示从正常模式被从唤醒； mutexStarving — 当前的互斥锁进入饥饿状态； waitersCount — 当前互斥锁上等待的 Goroutine 个数； 正常模式和饥饿模式\nsync.Mutex 有两种模式 — 正常模式和饥饿模式。我们需要在这里先了解正常模式和饥饿模式都是什么，它们有什么样的关系。\n在正常模式下，锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时，大概率会获取不到锁，为了减少这种情况的出现，一旦 Goroutine 超过 1ms 没有获取到锁，它就会将当前互斥锁切换到饥饿模式，防止部分 Goroutine 被『饿死』。\n饥饿模式是在 Go 语言 1.9 版本引入的优化，目的是保证互斥锁的公平性。\n在饥饿模式中，互斥锁会被直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态，它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms，那么当前的互斥锁就会被切换回正常模式。\n相比于饥饿模式，正常模式下的互斥锁能够提供更好的性能，饥饿模式能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。\n锁的使用\\\nvar lock = sync.Mutex{} func test_lock() { lock.Lock() defer lock.Unlock() // do something } 注意事项\n1，Unlock 未加锁或者已解锁的 Mutex 会 panic\n2，Mutex 不会比较当前请求的 goroutine 是否已经持有这个锁，所以可以一个 goroutine Lock ,另一个 goroutine Unlock, 但是慎用，避免死锁\n3，Mutex 是非重入锁。如果想重入，使用扩展的同步原语。\n注：这里解释下重入锁与非重入锁\n重入锁：顾名思义，就是指当前线程在获取锁成功后可以反复进入的锁。\n不可重入锁：就是指在获取锁成功后需要释放当前锁之后才能再次获取锁。\nRWMutex\n读写互斥锁 sync.RWMutex 是细粒度的互斥锁，它不限制资源的并发读，但是读写、写写操作无法并行执行。适合写少读多的状态，对并发的读很适合。\n读 写 读 Y N 写 N N 一般常见的服务场景中，对资源的读多写少，因为大多数的读请求之间不会相互影响，所以我们可以对读写资源操作进行分离，提高服务的性能。\n数据结构\\\ntype RWMutex struct { w Mutex writerSem uint32 readerSem uint32 readerCount int32 readerWait int32 } 在上面的代码中的五个字段的含义分别是：\nw — 复用互斥锁提供的能力； writerSem 和 readerSem — 分别用于写等待读和读等待写： readerCount 存储了当前正在执行的读操作的数量； readerWait 表示当写操作被阻塞时等待的读操作个数； 写锁 # 获取写锁\\\nfunc (rw *RWMutex) Lock() { rw.w.Lock() // 阻塞后续的读操作 r := atomic.AddInt32(\u0026amp;rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders if r != 0 \u0026amp;\u0026amp; atomic.AddInt32(\u0026amp;rw.readerWait, r) != 0 { runtime_SemacquireMutex(\u0026amp;rw.writerSem, false, 0) } } 释放写锁\\\nfunc (rw *RWMutex) Unlock() { // 调用 atomic.AddInt32 函数将变回正数，释放读锁； r := atomic.AddInt32(\u0026amp;rw.readerCount, rwmutexMaxReaders) if r \u0026gt;= rwmutexMaxReaders { throw(\u0026quot;sync: Unlock of unlocked RWMutex\u0026quot;) } // 通过 for 循环触发所有由于获取读锁而陷入等待的 Goroutine： for i := 0; i \u0026lt; int(r); i++ { runtime_Semrelease(\u0026amp;rw.readerSem, false, 0) } rw.w.Unlock() } 读锁 # 获取读锁\\\nfunc (rw *RWMutex) RLock() { // 如果该方法返回负数 — 其他 Goroutine 获得了写锁，当前 Goroutine 就会调用 sync.runtime_SemacquireMutex 陷入休眠等待锁的释放；否则则成功获取读锁 if atomic.AddInt32(\u0026amp;rw.readerCount, 1) \u0026lt; 0 { runtime_SemacquireMutex(\u0026amp;rw.readerSem, false, 0) } } 释放读锁\\\nfunc (rw *RWMutex) RUnlock() { // 如果返回值大于等于零 — 读锁直接解锁成功； // 如果返回值小于零 — 有一个正在执行的写操作，在这时会调用sync.RWMutex.rUnlockSlow 方法； if r := atomic.AddInt32(\u0026amp;rw.readerCount, -1); r \u0026lt; 0 { rw.rUnlockSlow(r) } } func (rw *RWMutex) rUnlockSlow(r int32) { if r+1 == 0 || r+1 == -rwmutexMaxReaders { throw(\u0026quot;sync: RUnlock of unlocked RWMutex\u0026quot;) } if atomic.AddInt32(\u0026amp;rw.readerWait, -1) == 0 { runtime_Semrelease(\u0026amp;rw.writerSem, false, 1) } } WaitGroup # 如下图所示，WaitGroup 可以将原本顺序执行的代码在多个 Goroutine 中并发执行，加快程序处理的速度。\nsync.WaitGroup 必须在 sync.WaitGroup.Wait 方法返回之后才能被重新使用；\nsync.WaitGroup.Done 只是对 sync.WaitGroup.Add 方法的简单封装，我们可以向 sync.WaitGroup.Add 方法传入任意负数（需要保证计数器非负）快速将计数器归零以唤醒其他等待的 Goroutine；\n可以同时有多个 Goroutine 等待当前 sync.WaitGroup 计数器的归零，这些 Goroutine 会被同时唤醒；\nwaitGroup 的例子：\\\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf(\u0026quot;Worker %d starting\\n\u0026quot;, id) time.Sleep(time.Second) fmt.Printf(\u0026quot;Worker %d done\\n\u0026quot;, id) } func main() { var wg sync.WaitGroup for i := 1; i \u0026lt;= 5; i++ { wg.Add(1) go worker(i, \u0026amp;wg) } wg.Wait() } ``` ### sync.Once Go 语言标准库中 sync.Once 可以保证在 Go 程序运行期间的某段代码只会执行一次。 结构体 ``` go type Once struct { done uint32 m Mutex } sync.Once.Do 是 sync.Once 结构体对外唯一暴露的方法，该方法会接收一个入参为空的函数：\n如果传入的函数已经执行过，就会直接返回；\n如果传入的函数没有执行过，就会调用 sync.Once.doSlow 执行传入的函数：\\\nfunc (o *Once) Do(f func()) { if atomic.LoadUint32(\u0026amp;o.done) == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(\u0026amp;o.done, 1) f() } } sync.Once 执行逻辑：\n为当前 Goroutine 获取互斥锁； 执行传入的无入参函数； 运行延迟函数调用，将成员变量 done 更新成 1； 通过成员变量 done 确保函数不会执行第二次。 关于 sync.Once 使用的例子：\\\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; ) func main() { o := \u0026amp;sync.Once{} for i := 0; i \u0026lt; 10; i++ { o.Do(func() { fmt.Println(\u0026quot;only once\u0026quot;) }) } } Channel # Go 语言中最常见的、也是经常被人提及的设计模式就是：不要通过共享内存的方式进行通信，而是应该通过通信的方式共享内存。\n在很多主流的编程语言中，多个线程传递数据的方式一般都是共享内存的方式来实现的，为了解决线程冲突的问题，我们需要限制同一时间能够读写这些变量的线程数量，这与 Go 语言鼓励的方式并不相同。\n通过共享内存的方式实现多线程之间的数据传递：\ngo 中使用 channel 实现 goroutine 之间的数据共享：\\\nChannel 类型 # 无缓冲区的 channel 有缓冲区的 channel 下图是示意图：\\\n非缓冲通道特性：\n向此类通道发送元素值的操作会被阻塞，直到至少有一个针对该通道的接收操作开始进行为止。 从此类通道接收元素值的操作会被阻塞，直到至少有一个针对该通道的发送操作开始进行为止。 针对非缓冲通道的接收操作会在与之相应的发送操作完成之前完成。\n总结来说就是：如果接收者没有准备好，则发送者会阻塞，反之亦然。 package main import \u0026quot;fmt\u0026quot; func main() { var c = make(chan int) var a string go func() { a = \u0026quot;hello world\u0026quot; \u0026lt;-c }() c \u0026lt;- 0 fmt.Println(a) } select 多路复用 # select 语句提供了一种处理多通道的方法。跟 switch 语句很像，但是每个分支都是一个通道：\n所有通道都会被监听 select 会阻塞直到某个通道读取到内容 如果多个通道都可以处理，则会以伪随机的方式处理 如果有默认分支，并且没有通道就绪，则会立即执行 结合 goroutine、channel、select 的一个简单示例，将 6 个数字 1~6 发送到一个容量为 3 的管道中，两个 goroutine 每秒接收一次数字后打印信息：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; ) func main() { ch := make(chan int, 3) var wg sync.WaitGroup // start 2 goroutines for i := 0; i \u0026lt; 2; i++ { wg.Add(1) go func(id int) { tick := time.Tick(1 * time.Second) for { select { case \u0026lt;-tick: { i, ok := \u0026lt;-ch if !ok { wg.Done() return } fmt.Println(\u0026quot;goroutine\u0026quot;, id, \u0026quot;recv\u0026quot;, i) } } } }(i) } // sender for i := 0; i \u0026lt; 6; i++ { ch \u0026lt;- i fmt.Println(\u0026quot;send\u0026quot;, i) } close(ch) wg.Wait() fmt.Println(\u0026quot;main goroutine end\u0026quot;) } 参考文献 # https://colobu.com/2018/12/18/dive-into-sync-mutex/\nhttps://iswade.github.io/articles/go_concurrency/\nhttps://segmentfault.com/a/1190000016466500\nhttps://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/\n","date":"2020-07-01","externalUrl":null,"permalink":"/posts/go--bing-fa-bian-cheng/","section":"文章","summary":"基本的同步原语 Go 语言在 sync 包中提供了用于同步的一些基本原语，包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once。Mutex 数据结构 Go 语言的 sync.Mute","title":"go 并发编程","type":"posts"},{"content":"","date":"2020-07-01","externalUrl":null,"permalink":"/tags/%E5%B9%B6%E5%8F%91/","section":"标签","summary":"","title":"并发","type":"tags"},{"content":"","date":"2020-07-01","externalUrl":null,"permalink":"/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/","section":"标签","summary":"","title":"性能优化","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：【译】了解 Linux CPU 负载-您何时应该担心？\n您可能已经熟悉 Linux 平均负载。平均负载是 uptime 和 top 命令显示的三个数字-它们看起来像这样：\nload average: 0.09, 0.05, 0.01 大多数人都对负载平均值的含义有所了解：三个数字代表了较长时间段内的平均值（一分钟，五分钟和十五分钟的平均值），而较低的数字更好。较高的数字表示问题或机器过载。但是，阈值是多少？什么算“好”和“坏”负载平均值？什么时候应该关注负载平均值，什么时候应该修复它？\n首先，简要了解负载平均值的含义。我们将从最简单的情况开始：一台带有一个单核处理器的机器。\\\n📷 图注：cpu-load\nThe traffic analogy # 单核 CPU 就像一条流量通道。想象您是一名桥梁操作员…有时您的桥梁太忙了，有汽车排成一行。您想让人们知道桥上的交通如何。一个像样的指标是在特定时间等待多少辆汽车。如果没有汽车在等，驶入的驾驶员知道他们可以马上驶过。如果汽车排起了队，则驾驶员知道他们要耽误时间。\n那么，桥梁操作员，您将使用哪种编号系统？怎么样：\n0.00 表示桥上根本没有流量。实际上，介于 0.00 和 1.00 之间表示没有备份，到达的汽车将继续行驶。\n1.00 表示桥刚好处于满负荷状态。 一切都还不错，但是如果流量增加一点，事情就会变慢。\n超过 1.00 表示有备份。 多少？那么，2.00 意味着总共有两个车道的汽车-桥上一个车道，一个车道在等待。3.00 表示总共有 3 个车道的汽车-桥上 1 个车道，2 个车道在等待。等等。\n这基本上是 CPU 负载。“汽车”是指使用 CPU 时间（“过桥”）或排队使用 CPU 的进程。Unix 将其称为运行队列长度：当前正在运行的进程数与正在等待（排队）的进程数之和。\n就像桥梁操作员一样，您希望您的汽车/进程永远不会等待。因此，理想情况下，您的 CPU 负载应保持在 1.00 以下。就像桥梁操作员一样，如果您暂时获得高于 1.00 的峰值，您仍然可以…但是当您始终高于 1.00时，您就需要担心。\nSo you’re saying the ideal load is 1.00? # 好吧，不完全是。负载为 1.00 的问题是您没有余量。实际上，许多系统管理员会在 0.70 处画一条线：\n“需要研究”的经验法则：0.70 如果平均负载保持在 0.70 以上，那么应该在情况变得更糟之前进行调查。\n“立即解决”的经验法则是：1.00。如果平均负载保持在 1.00 以上，请查找问题并立即解决。否则，您将在半夜醒来，这将不会很有趣。\n“ Arrgh，这是 WTF 3AM？” 经验法则：5.0。如果平均负载高于 5.00，则可能会遇到严重的麻烦，盒子要么挂着要么减速，这将（莫名其妙地）发生在最坏的时间，例如深夜或当您正在开会时。不要让它到达那里。\nWhat about Multi-processors? My load says 3.00, but things are running fine! # 有一个四处理器系统？3.00 负载仍然很健康。\n在多处理器系统上，负载是相对于可用处理器核心数量而言的。在单核系统上，“ 100％利用率”标记是 1.00，在双核上是 2.00，在四核上是 4.00，依此类推。\n如果再回到桥梁类比，“ 1.00”实际上意味着“一个车道的通行价值”。在单车道的桥上，这意味着它已被填满。在两车道的桥上，负载为 1.00 表示其容量为 50％-只有一个车道已满，因此还有一整条车道可以填满。\n与 CPU 相同：1.00 的负载意味着单核机器上的 CPU 利用率为 100％。在双核计算机上，负载为 2.00 就是 100％CPU 使用率。\nMulticore vs. multiprocessor # 既然说到这个话题，让我们谈谈多核与多处理器。出于性能目的，具有单个双核处理器的计算机是否基本上等同于具有两个具有一个内核的处理器的计算机？是的，大致如此。关于缓存的数量，处理器之间的进程切换频率等，这里有很多微妙之处。尽管有这些细微之处，但为了确定 CPU 负载值，内核总数是重要的，无论这些内核分布在多少个物理处理器上。\n这引出了两个新的经验法则：\n-“核数=最大负载”经验法则：在多核系统上，您的负载不应超过可用核数。\n-“核心就是核心”经验法则：核心在 CPU 上的分布方式无关紧要。两个四核==四个双核==八个单核。这些都是八个核心。\nBringing It Home # 让我们看一下 uptime 的平均负载输出：\\\n~ $ uptime 23:05 up 14 days, 6:08, 7 users, load averages: 0.65 0.42 0.36 这是在双核 CPU 上，因此我们有很大的余量。在负载达到并保持在 1.7 左右之前，我甚至不会考虑它。\n现在，那三个数字呢？0.65 是最近一分钟的平均值，0.42 是最近五分钟的平均值，而 0.36 是最近 15分钟的平均值。这使我们想到了一个问题：\n我应该观察哪个平均值？1、5 或 15分钟？\n对于我们已经讨论过的数字（1.00 =立即修复，依此类推），您应该查看 5 或 15分钟的平均值。坦白说，如果您的负载平均值在一分钟内达到 1.0 以上的峰值，您还是可以的。这是 15分钟平均值超过 1.0 并保持在该水平上的时间。（显然，据我们了解，将这些数字调整为系统具有的处理器核心数量）。\n因此，核的数量对于解释平均负载很重要……我如何知道我的系统有多少个核？\ncat / proc / cpuinfo 可获取系统中每个处理器的信息。注意：在 OSX 上不可用，但可以求助 Google。要获得一个计数，请通过 grep 和单词计数运行它：grep’模型名称’/ proc / cpuinfo | wc -l\n参考文档 # 原文链接\nWikipedia - A good, brief explanation of Load Average\nLinux Journal - very well-written article\n","date":"2020-06-24","externalUrl":null,"permalink":"/posts/yi---liao-jie-linux-cpu-fu-zai---nin-he-shi-ying-gai-dan-xin/","section":"文章","summary":"您可能已经熟悉 Linux 平均负载。平均负载是 uptime 和 top 命令显示的三个数字 它们看起来像这样：load average: 0.09, 0.05, 0.01 大多数人都对负载平均值的含义有所了解：三个数字代表了较长时间段内","title":"【译】了解 Linux CPU 负载-您何时应该担心？","type":"posts"},{"content":"","date":"2020-06-24","externalUrl":null,"permalink":"/categories/linux/","section":"分类","summary":"","title":"Linux","type":"categories"},{"content":"","date":"2020-06-24","externalUrl":null,"permalink":"/tags/linux/","section":"标签","summary":"","title":"Linux","type":"tags"},{"content":"","date":"2020-06-24","externalUrl":null,"permalink":"/tags/%E8%BF%90%E7%BB%B4/","section":"标签","summary":"","title":"运维","type":"tags"},{"content":"📌 本文原发布于掘金社区：基于 Redis 的分布式锁到底安全吗？\n单机 Redis 实现的分布式锁 # 1，单机实现分布式锁的脚本（官方推荐实现）\nSET lock_key random_value NX PX 10000 // do sth eval \u0026#34;if redis.call(\u0026#34;get\u0026#34;,KEYS[1]) == ARGV[1] then return redis.call(\u0026#34;del\u0026#34;,KEYS[1]) else return 0 end\u0026#34; 2，注意事项（对释放锁的控制，以及锁超时的控制）：random_value 要保证唯一，可以用 trace_id 来保证！\n3，存在的问题：单机 Redis 只是依赖单台 Redis，当依赖的 Redis 挂掉之后会造成比较大的问题！\n4，那么部署 Redis 的主从可以保证吗？主要原因是 Redis 主节点与从节点之间的数据同步是异步的。\n分布式 Redis 实现的分布式锁 # Redlock 算法 # Redlock 算法是基于 N 个完全独立的 Redis 节点（通常情况下 N 可以设置成 5）。\n1，获取当前时间（毫秒数）。\n2，按顺序依次向 N 个 Redis 节点执行获取锁的操作。这个获取操作跟基于单 Redis 节点的获取锁的过程相同。为了保证在某个 Redis 节点不可用的时候算法能够继续运行，这个获取锁的操作还有一个超时时间(time out)，它要远小于锁的有效时间（几十毫秒量级）。客户端在向某个 Redis 节点获取锁失败（比如该 Redis 节点不可用，或者该 Redis 节点上的锁已经被其它客户端持有）以后，应该立即尝试下一个 Redis 节点。\n3，计算整个获取锁的过程总共消耗了多长时间，计算方法是用当前时间减去第 1 步记录的时间。如果客户端从大多数 Redis 节点（\u0026gt;= N/2+1）成功获取到了锁，并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time)，那么这时客户端才认为最终获取锁成功；否则，认为最终获取锁失败。\n4，如果最终获取锁成功了，那么这个锁的有效时间应该重新计算，它等于最初的锁的有效时间减去第 3 步计算出来的获取锁消耗的时间。\n5，如果最终获取锁失败了（可能由于获取到锁的 Redis 节点个数少于 N/2+1，或者整个获取锁的过程消耗的时间超过了锁的最初有效时间），那么客户端应该立即向所有 Redis 节点发起释放锁的操作（以保证所有 Redis 节点上已获取的锁都能被释放掉）。\nRedlock 算法实现的前提是假设不同的机器具有相同的时钟，或者误差很小，可以忽略不计。(这也是 DDIA 作者喷的一点)\n存在的问题：1，关于 Redis 的持久化的问题：假设一共有 5 个 Redis 节点：A, B, C, D, E。设想发生了如下的事件序列：\n1.1 客户端 1 成功锁住了 A, B, C，获取锁成功（但 D 和 E 没有锁住）。\n1.2 节点 C 崩溃重启了，但客户端 1 在 C 上加的锁没有持久化下来，丢失了。\n1.3 节点 C 重启后，客户端 2 锁住了 C, D, E，获取锁成功。\\\nRedis 给出的解决方案：延迟重启，即当 Redis 节点挂掉之后不要立刻重启，而要等待一个锁的过期时间之后再重启。\n2，如果获取锁消耗的时间过多以至于无法完成后续的操作，如何释放锁？个人认为需要业务方自己拿捏一个业务操作需要消耗的时长，\n3，Redis 作者在设计 Redlock 的时候，是充分考虑了网络延迟和程序停顿所带来的影响的。但是，对于客户端和资源服务器之间的延迟（即发生在算法第 3 步之后的延迟），他承认所有的分布式锁的实现，包括 Redlock，是没有什么好办法来应对的。\nDDIA 作者对于 Redlock 的观点 # 在 Martin 的这篇文章中，他把锁的用途分为两种：\n为了效率(efficiency)，协调各个客户端避免做重复的工作。即使锁偶尔失效了，只是可能把某些操作多做一遍而已，不会产生其它的不良后果。比如重复发送了一封同样的 email。 为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生，因为一旦发生，就可能意味着数据不一致(inconsistency)，数据丢失，文件损坏，或者其它严重的问题。 1，带有自动过期功能的分布式锁，必须提供某种 fencing 机制来保证对共享资源的真正的互斥保护。Redlock 提供不了这样一种机制。\n上图展示的是：由于 GC 停顿造成的共享资源被多个客户端访问的问题。原因是：客户端因 GC 停顿造成锁的等待超时，从而被释放，然而客户端并不知道，认为自己还是处于持有锁的状态。\n上图展示的是：通过使用 fencing tokens 来解决锁失效未释放的问题。Redis 指出上图的漏洞，如果客户端 1 和客户端 2 都发生了 GC pause，两个 fencing token 都延迟了，它们几乎同时到达了资源服务器，但保持了顺序，那么资源服务器是不是就检查不出问题了？这时对于资源的访问是不是就发生冲突了？\n2，Redlock 构建在一个不够安全的系统模型之上。它对于系统的计时假设(timing assumption)有比较强的要求，而这些要求在现实的系统中是无法保证的。Redlock 算法对机器时钟有强依赖，Martin 认为算法的实现不应该对时序做任何假设：进程可能会暂停任意时长，数据包可能会在网络中被任意延迟，时钟可能会任意出错，即便如此，该算法仍可以正确执行并给出正确结果。\n关于时钟的不可靠性：Redis 作者认为 Redlock 对时钟的要求，并不需要完全精确，它只需要时钟差不多精确就可以了。\n3，在 Redlock 第三步完成之后的网络延迟，也可能造成 Redlock 算法的失效，但是这个问题并不是 Redlock 算法独有的\nMartin 得出了如下的结论：\n如果是为了效率(efficiency)而使用分布式锁，允许锁的偶尔失效，那么使用单 Redis 节点的锁方案就足够了，简单而且效率高。Redlock 则是个过重的实现(heavy weight)。 如果是为了正确性(correctness)在很严肃的场合使用分布式锁，那么不要使用 Redlock。它不是一个建立在异步模型上的足够强的算法，它对于系统模型的假设中包含很多危险的成分(对于 timing)。而且，它没有一个机制能够提供 fencing token。那应该使用什么技术呢？Martin 认为，应该考虑类似 ZooKeeper 的方案，或者支持事务的数据库。 最后的讨论 # 在了解了 Redlock 算法的逻辑及其可能存在的问题之后，我们可以针对自己的业务场景进行选择~\n参考 # How to do distributed locking\nIs Redlock safe?\nDistributed locks with Redis\n关注我们 # ","date":"2020-02-11","externalUrl":null,"permalink":"/posts/jiyu-redis-defenbushisuodaodianquanma/","section":"文章","summary":"4，那么部署 Redis 的主从可以保证吗？主要原因是 Redis 主节点与从节点之间的数据同步是异步的。Redlock 算法是基于 N 个完全独立的 Redis 节点（通常情况下 N 可以设置成 5）。1，获取当前时间（毫秒数）。2，按顺序依次向 N 个 Redis 节…","title":"基于 Redis 的分布式锁到底安全吗？","type":"posts"},{"content":"📌 本文原发布于代码星冰乐：【译】Raft 学生指南\n在过去的几个月中，我一直担任 MIT 的 6.824 分布式系统课程的助教。传统上，该课程有许多基于 Paxos 共识算法的实验，但是今年，我们决定转向 Raft。Raft 的设计更易于理解，我们希望这种改变可以使学生的学习更轻松。\n这篇文章，以及随附的《Raft 教师指南》一文，记录了我们使用 Raft 的旅程，并希望对 Raft 协议的教学者和试图更好地了解 Raft 内部原理的学生有所帮助。如果您想对 Paxos 与 Raft 进行比较，或者需要对 Raft 进行更多的教学分析，请阅读《Raft 教师指南》。这篇文章的底部包含 6.824 学生常见的Q\u0026amp;A。如果您遇到的问题未在本文的主要内容中列出，请查看Q\u0026amp;A。这篇文章很长，但它提出的所有观点都是许多 6.824 学生遇到的实际问题，这是值得一读的。\n背景 # 在我们深入研究 Raft 之前，有些背景可能会有用。6.824 曾经有一组用 Go 编写的基于 Paxos 的实验；选择 Go 是因为它语法简单易于学习，而且非常适合编写并发的分布式应用程序。在四个实验过程中，学生将构建一个容错的分布式 KV 存储系统。第一个实验让他们建立了一个基于共识的日志库，第二个实验在此基础上添加了一个键值存储，第三个实验通过多个容错的分片主节点处理配置更改，在多个容错集群之间分割键空间。我们还有第四个实验，学生必须在磁盘完好无损的情况下处理机器的故障和恢复。该实验可作为学生的默认最终项目使用。\n今年，我们决定使用 Raft 重写所有这些实验。前三个实验保持不变，但是第四个实验被删除了，因为 Raft 已经内置了持久化和故障恢复功能。本文将主要讨论我们在第一个实验中的经验，因为它与 Raft 最直接相关，尽管我还将介绍如何在 Raft 之上构建应用程序。\nRaft 是什么呢？对于那些刚开始了解它的人，我们最好用 Raft 官网上的文字来描述：\nRaft 是一种共识算法，旨在使其易于理解。在容错和性能方面与 Paxos 相当。区别在于它被分解为相对独立的子问题，并且彻底地解决了实际系统所需的所有主要问题。我们希望 Raft 将使共识能够为更广泛的受众所接受，并且希望这个更广泛的受众能够开发出更高质量的基于共识的系统。\n像这样的可视化演示很好地概述了协议的主要组成部分，并且更直观地描述了 Raft 的各个阶段。如果您还没有阅读过 Raft 论文，那么在继续本文之前，您应该先阅读该文档，因为我假设您对 Raft 相当熟悉。\n与所有分布式共识协议一样，细节之处非常重要。在没有故障的稳定状态下，Raft 的行为很容易理解，并且可以直观地解释。例如，从可视化中很容易看出，假设没有失败，最终将选举一名 leader，并且最终发送给 leader 的所有操作将由 followers 按正确的顺序执行。但是，当引入延迟的消息，网络分区和故障服务器时，每一个 if，but 和 and 都变得至关重要。特别是，我们反复看到许多错误都源于阅读本文时的误解或疏忽。这个问题并非 Raft 独有，而是所有提供正确性的复杂分布式系统中都存在的问题。\nRaft 协议的实现 # Raft 的权威指南在 Raft 论文的 Figure 2 中。该图指定了 Raft 服务器之间交换的每个 RPC 的行为，给出了服务器必须维护的各种不变量，并指定了何时应执行某些操作。在本文的其余部分中，我们将大量讨论 Figure 2。如下文描述的一样。\nFigure 2 定义了每个服务器在任何状态下对于每个传入的 RPC 应该做什么，以及何时应该发生某些其他事情（例如，何时可以安全地在日志中应用条目）。刚开始，您可能会倾向于将 Figure 2 视为非正式指南。您只需阅读一次，然后开始编写大致按照其要求来实现的代码。这样做，您将快速让大多数正常的 Raft 操作运行起来。然后问题开始浮现了。\n实际上，Figure 2 是非常精确的，并且应将其所作的每个陈述都视为必须，而不应视为应该。例如，每当您收到 AppendEntries 或 RequestVote RPC 时，您都可以合理地重置对等方的选举计时器，因为这两者都表明其他一些服务器要么认为自己是 leader，要么正试图成为 leader。这意味着我们不应该干涉。但是，如果您仔细阅读 Figure 2，如 Raft 论文所示：\n如果在没有收到当前 leader 的 AppendEntries RPC 或未向候选人授予投票的情况下经过选举超时，请转换为候选人。\n事实证明，区别非常重要，因为在某些情况下，前一种实现可能导致活性大大降低。\nRaft 协议实现的细节 # 为了使讨论更加具体，让我们考虑一个例子，这个例子曾难倒不少上 6.828 课程的学生。Raft 论文在许多地方提到了心跳 RPC。具体来说，leader 每个心跳间隔至少会向所有对等方发送一次 AppendEntries RPC，以防止他们开始新的选举。如果领导者没有新条目要发送到特定对等方，则 AppendEntries RPC 不包含任何条目，并被视为心跳。\n我们的许多学生都认为心跳在某种程度上是“特殊的”。当对等方收到心跳时，应将其与非心跳 AppendEntries RPC 区别对待。特别是，许多人在接收到心跳信号后便会简单地重置其选举计时器，然后返回成功，而无需执行 Figure 2 中指定的任何检查。这非常危险。通过接受 RPC，followers 可以隐式告诉 leader，他们的日志与领导者的日志相匹配，并且包括 AppendEntries 参数中的 prevLogIndex。收到答复后，领导者可能错误地认为某些条目已被复制到大多数服务器，然后开始提交。\n许多人遇到的另一个问题（通常是在解决上述问题后立即发生）是，一旦收到心跳，他们就会在 prevLogIndex 之后截断 followers 的日志，然后追加 AppendEntries 参数中包含的所有条目。这也是不正确的。我们可以再次转到 Figure 2：\n如果现有条目与新条目（索引相同但任期不同）冲突，则删除现有条目及其后的所有条目。\n这个“如果”在这里至关重要。如果 followers 具有 leader 发送的所有条目，则 followers 务必不要截断其日志。领导者发送的条目之后的任何条目都必须保留。这是因为我们可能会从领导者那里收到过时的 AppendEntries RPC，而截断日志意味着“收回”我们可能已经告诉领导者我们在日志中的条目。\n调试 Raft 协议 # 不可避免地，您的 Raft 实现的第一次迭代会出现错误。第二个也是如此。第三。第四。总的来说，每个错误都比前一个错误少，并且根据经验，大多数错误是由于不忠实地遵循 Figure 2 而导致的。\n在调试 Raft 时，通常有四个主要的 bug 来源：死锁，错误或不完整的 RPC 处理程序，未遵循规则以及任期混乱。死锁也是一个普遍的问题，但是通常可以通过记录所有锁和解锁并找出获取后未释放的锁来调试死锁。让我们依次考虑以下每个方面：\n未释放锁 # 当系统发生死锁时，系统中的每个节点都在做某事，但是总的来说，您的节点都处于没有进展的状态。在 Raft 中，这很容易发生，特别是如果您不认真地遵循 Figure 2。一种死锁情况特别经常出现。没有选举任何 leader，或者一旦选举了 leader，其他节点开始选举，迫使最近当选的 leader 立即退位。\n出现这种情况的原因有很多，但我们已经看到许多学生犯了一些错误：\n确保您按照 Figure 2 所述正确地重置了选举计时器。具体来说，您仅应在以下情况下重新启动选举计时器：a）从当前 leader 那里获得了 AppendEntries RPC（如果 AppendEntries 参数中的任期已过时，则不应重置计时器）；b）您正在开始选举；c）您给另一位 follower 投票。\n在不可靠的网络中，后一种情况尤为重要，在这种网络中，followers 可能拥有不同的日志。在这种情况下，您通常只会获得少数服务器的投票，而大多数服务器都愿意投票。如果您在有人要求您投票给他们时重置选举计时器，则日志过时的服务器和日志较长的服务器一样有可能前进。\n实际上，由于只有很少的服务器具有足够的最新日志，因此这些服务器很难获得足够的静默期来进行选举。如果您遵循 Figure 2 中的规则，则具有最新日志的服务器将不会因过时的服务器选举而中断，因此更有可能完成选举并成为 leader。\n请按照 Figure 2 的指示来决定何时开始选举。特别要注意的是，如果您是候选人（即您当前正在进行选举），但是选举计时器触发了，则应该重新进行选举。这对于避免由于 RPC 延迟或丢失而导致的系统停顿非常重要。\n在处理传入的 RPC 之前，请确保遵循“服务器规则”中的第二条规则。第二条规则指出：\n如果 RPC 请求或响应包含条件 T \u0026gt; currentTerm：设置 currentTerm = T，则转换为 followers\n例如，如果您已经对当前任期进行了投票，而传入的 RequestVote RPC 具有较高的任期，那么您应该首先卸任并采用其任期（从而重置 votedFor），然后处理 RPC，这将导致您同意投票！\n不正确的 RPC 处理程序 # 即使 Figure 2 清楚地说明了每个 RPC 处理程序应该做什么，但仍然有些微妙之处容易遗漏。这是我们反复看到的少数几个问题，您在实施时应格外注意：\n如果某个步骤说“答复错误”，则意味着您应立即答复，而不要执行任何后续步骤。 如果您获得带有指向日志末尾的 prevLogIndex 的 AppendEntries RPC，则应像该条目存在但任期不匹配一样处理它（即，回复 false）。 注意 #2，即使领导者未发送任何条目，也应执行 AppendEntries RPC 处理程序。 AppendEntries 的最后一步（＃5）中的最小值是必需的，并且需要使用最后一个新条目的索引进行计算。仅具有当 lastApplied 和 commitIndex 停在日志末尾时应用日志内容的功能还不够。这是因为在领导者发送给您的条目之后，您的日志中可能有与领导者的日志不同的条目（所有条目都与您的日志中的条目匹配）。由于＃3 要求您仅在条目冲突时才截断日志，因此不会删除这些条目，并且如果 LeaderCommit 超出了领导者发送给您的条目，则您可能会应用错误的条目。 严格按照第 5.4 节中的描述实施“最新日志”检查很重要。没有作弊，只是检查长度！ 不遵守规则 # 尽管 Raft 论文非常明确地说明了如何实现每个 RPC 处理程序，但它也留下了许多规则和不变量的实现，未作明确规定。它们在 Figure 2 右侧的“服务器规则”块中列出。尽管其中一些是不言自明的，但也有一些需要非常仔细地设计应用程序，以使其不违反 The Rules：\n在执行过程中的任何时候执行 go if commitIndex\u0026gt; lastApplied，则应应用特定的日志条目。立即执行操作（例如，在 AppendEntries RPC 处理程序中）不是至关重要的，但是确保此应用操作仅由一个实体完成很重要。具体来说，您将需要一个专用的“应用例程”，或者锁定这些应用例程，以便其他一些例程也不会检测到需要应用条目并尝试应用。 确保您定期或在更新 commitIndex 之后（即在 matchIndex 更新之后）检查 commitIndex\u0026gt; lastApplied。例如，如果在将 AppendEntries 发送给对等方的同时检查 commitIndex，则可能必须等到下一个条目追加到日志之后才能应用刚刚发送并得到确认的条目。 如果领导者发出一个 AppendEntries RPC 并被拒绝，但不是由于日志不一致（只有在我们的任期过去时才可能发生），那么您应该立即下台，而不要更新 nextIndex。如果这样做，并且立即连任，则可能与 nextIndex 的重置产生竞争。 领导者不允许将 commitIndex 更新到上一个任期（或就此而言，将来的任期）中的某个位置。因此，按照规则所说，您特别需要检查 log \\[N\\].term == currentTerm。这是因为 Raft 领导者如果不是从其当前任期开始，就无法确定该条目是否确实已提交（将来也不会更改）。Raft 论文的 Figure 8 对此进行了说明。 造成混淆的一个常见原因是 nextIndex 和 matchIndex 之间的差异。特别是，您可能会注意到 matchIndex = nextIndex-1，而根本没有实现 matchIndex。这不安全。通常将 nextIndex 和 matchIndex 同时更新为相似的值（具体来说，nextIndex = matchIndex + 1），但两者的作用却截然不同。nextIndex 是对领导者与给定跟随者共享的前缀的猜测。它通常是相当乐观的（我们共享一切），并且仅在负面响应时才向后移动。例如，当刚刚选择一个领导者时，将 nextIndex 设置为日志末尾的索引。在某种程度上，nextIndex 用于提高性能–您只需要将这些内容发送给该对等方即可。\nmatchIndex 用于安全。这是对领导者与给定跟随者共享日志的前缀的保守度量。matchIndex 永远不能设置为太高的值，因为这可能导致 commitIndex 移得太远。这就是为什么将 matchIndex 初始化为-1（即我们没有前缀），并且仅在跟随者肯定地确认 AppendEntries RPC 时才进行更新的原因。\n任期不一致 # 任期混淆是指服务器被来自旧任期的 RPC 混淆。通常，在接收 RPC 时这不是问题，因为 Figure 2 中的规则明确说明了您看到旧任期时应采取的措施。但是，Figure 2 通常不会讨论收到旧的 RPC 答复时应采取的措施。根据经验，到目前为止，最简单的方法是首先在答复中记录该任期（该任期可能高于您当前的任期），然后将当前任期与您在原始 RPC 中发送的任期进行比较。如果两者不同，请丢弃答复并返回。仅当两个任期相同时，您才可以继续处理答复。您可以使用一些聪明的协议推理在此处进行进一步的优化，但是这种方法似乎很好用。不这样做会让你走上一条充满眼泪和绝望的漫长曲折之路。\n一个相关但不完全相同的问题是，假设在您发送 RPC 的时间与收到回复之间的状态没有发生变化。一个很好的例子是在收到对 RPC 的响应时设置 matchIndex = nextIndex-1 或 matchIndex = len（log）。这是不安全的，因为自发送 RPC 以来，这两个值都可能已更新。相反，正确的做法是将 matchIndex 更新为您最初在 RPC 中发送的参数的 prevLogIndex + len（entries []）。\n先把优化放在一边 # Raft 包括几个有趣的可选功能。在 6.824 中，我们要求学生实施其中两个：日志压缩（第 7 节）和加速日志回溯（第 8 页左上角）。前者对于避免日志无限制增长是必要的，而后者对于使过时的 follower 快速更新很有用。\n这些功能不是 Raft 最核心的一部分，因此在本文中没有像主要共识协议那样受到足够的重视。日志压缩已经相当全面地介绍了（在论文图 13 中），但是省略了一些设计细节，如果您随便阅读它可能会错过：\n对应用程序状态进行快照时，需要确保应用程序状态与 Raft 日志中某个已知索引处的状态相对应。这意味着应用程序需要把该快照对应的索引告知 Raft，或者 Raft 需要延迟应用其他日志条目，直到快照完成为止。\n本文不讨论服务器崩溃时的恢复协议，而这一问题在涉及快照时会重新出现。特别是，如果 Raft 状态和快照分别提交，则服务器可能在持久化快照和持久化更新后的 Raft 状态之间崩溃。这是一个问题，因为论文的图 13 中的步骤 7 指示必须删除快照覆盖的 Raft 日志。\n如果在服务器恢复时读取了更新的快照，但读取了过时的日志，则可能最终应用了快照中已包含的一些日志条目。发生这种情况是因为 commitIndex 和 lastApplied 未持久保存，因此 Raft 不知道这些日志条目已被应用。解决此问题的方法是在 Raft 中引入一个持久状态，该状态记录 Raft 持久日志中第一个条目所对应的“真实”索引。然后可以将其与加载的快照的 lastIncludedIndex 进行比较，以确定要丢弃日志开头的哪些条目。\n加速日志回溯优化的规格非常少，可能是因为作者认为对于大多数部署而言，它不是必需的。从文本中不清楚领导者应如何使用从跟随者发送回的冲突索引和任期来确定要使用的 nextIndex。我们认为作者可能希望您遵循的协议是：\nIf a follower does not have prevLogIndex in its log, it should return with conflictIndex = len(log) and conflictTerm = None. If a follower does have prevLogIndex in its log, but the term does not match, it should return conflictTerm = log \\[prevLogIndex\\].Term, and then search its log for the first index whose entry has term equal to conflictTerm. Upon receiving a conflict response, the leader should first search its log for conflictTerm. If it finds an entry in its log with that term, it should set nextIndex to be the one beyond the index of the last entry in that term in its log. If it does not find an entry with that term, it should set nextIndex = conflictIndex. 一个半途而废的解决方案是只使用冲突索引（并忽略冲突 term），这简化了实现，但是领导者有时最终会向跟随者发送比严格追平最新日志所需更多的日志条目。\n参考 # Students’ Guide to Raft\n","date":"2020-02-02","externalUrl":null,"permalink":"/posts/yi--raft--xue-sheng-zhi-nan/","section":"文章","summary":"在过去的几个月中，我一直担任 MIT 的 6.824 分布式系统课程的助教。传统上，该班级有许多基于 Paxos 共识算法的实验，但是今年，我们决定转向 Raft。Raft 的设计更易于理解，我们希望这种改变可以使学生的学习更轻松。这篇文","title":"【译】Raft 学生指南","type":"posts"},{"content":"","date":"2020-02-02","externalUrl":null,"permalink":"/categories/raft/","section":"分类","summary":"","title":"Raft","type":"categories"},{"content":"","date":"2020-02-02","externalUrl":null,"permalink":"/tags/raft/","section":"标签","summary":"","title":"Raft","type":"tags"},{"content":"","date":"2020-02-02","externalUrl":null,"permalink":"/tags/%E4%B8%80%E8%87%B4%E6%80%A7/","section":"标签","summary":"","title":"一致性","type":"tags"},{"content":"","date":"2020-01-19","externalUrl":null,"permalink":"/categories/threadpoolexecutor/","section":"分类","summary":"","title":"ThreadPoolExecutor","type":"categories"},{"content":"","date":"2020-01-19","externalUrl":null,"permalink":"/tags/threadpoolexecutor/","section":"标签","summary":"","title":"ThreadPoolExecutor","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：ThreadPoolExecutor 的简单梳理\n还是楼主惯用的论述三连问，先问是什么，再问为什么，最后祭出终极大杀器 just do it ……\nwhat ? # 那么什么是线程池呢？总的来说，线程池是一种线程使用模式。线程的频繁创建与调度会带来调度开销，进而影响缓存局部性和整体性能。而线程池维护着多个线程，等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用，还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络 sockets 等的数量。\nwhy ? # 上面说的是整个线程池的总体概念，当然 Java 中的线程池也是基于相同的理念设计的。在 Java 中，线程池可以提高线程复用，又可以固定最大线程使用量，防止无限制地创建线程。当程序提交一个任务而需要一个线程时，会去线程池中查找是否有空闲的线程，若有，则直接使用线程池中的线程工作，若没有，会去判断当前已创建的线程数量是否超过最大线程数量，如未超过，则创建新线程，如已超过，则排队等待或者直接抛出异常。如下图所示\\\n总的来说，线程池就是把资源池化，预先准备好，以避免不必要的额外开销。\nhow ? # Java 提供了一套 Executor 框架，根据常用的场景对 ThreadPoolExecutor 类做了简单的封装，当然这样做的话难免有些束手束脚，所以大部分情况下都是根据自己的业务需求直接调用 ThreadPoolExecutor 实现自己的线程池。\\\n这个框架中包括了 ScheduledThreadPoolExecutor 和 ThreadPoolExecutor 两个核心线程池。前者是用来定时执行任务，后者是用来执行被提交的任务。因为这两个线程池的原理是一样的，都是调用底层的 ThreadPoolExecutor，下面我们就重点看看 ThreadPoolExecutor 类是如何实现线程池的。\\\npublic ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 int maximumPoolSize,//线程池的最大线程数 long keepAliveTime,//当线程数大于核心线程数时，多余的空闲线程存活的最长时间 TimeUnit unit,//时间单位 BlockingQueue\u0026lt;Runnable\u0026gt; workQueue,//任务队列，用来储存等待执行任务的队列 ThreadFactory threadFactory,//线程工厂，用来创建线程，一般默认即可 RejectedExecutionHandler handler) //拒绝策略，当提交的任务过多而不能及时处理时，我们可以定制策略来处理任务 通过上面代码，我们发现线程池有两个线程数的设置，一个为核心线程数，一个为最大线程数。在创建完线程池之后，默认情况下，线程池中并没有任何线程，等到有任务来才创建线程去执行任务。\n当创建的线程数等于 corePoolSize 时，提交的任务会被加入到设置的阻塞队列中。当队列满了，会创建线程执行任务，直到线程池中的数量等于 maximumPoolSize。当线程数量已经等于 maximumPoolSize 时，新提交的任务无法加入到等待队列，也无法创建非核心线程直接执行，当我们又没有为线程池设置具体的拒绝策略时，线程池就会抛出 RejectedExecutionException 异常，即线程池拒绝接受当前提交的任务。\n当线程池中创建的线程数量超过设置的 corePoolSize，在某些线程处理完任务后，如果等待 keepAliveTime 时间后仍然没有新的任务分配给它，那么这个线程将会被回收。线程池回收线程时，会对所谓的“核心线程”和“非核心线程”一视同仁，直到线程池中线程的数量等于设置的 corePoolSize 参数，回收过程才会停止。从下面代码中可以看出\\\npublic void setCorePoolSize(int corePoolSize) { if (corePoolSize \u0026lt; 0 || maximumPoolSize \u0026lt; corePoolSize) throw new IllegalArgumentException(); int delta = corePoolSize - this.corePoolSize; this.corePoolSize = corePoolSize; if (workerCountOf(ctl.get()) \u0026gt; corePoolSize) interruptIdleWorkers(); else if (delta \u0026gt; 0) { int k = Math.min(delta, workQueue.size()); while (k-- \u0026gt; 0 \u0026amp;\u0026amp; addWorker(null, true)) { if (workQueue.isEmpty()) break; } } } 小结 # 到目前为止，楼主只是围绕 ThreadPoolExecutor 的构造方法，简单阐述了下 Java 中的线程池实现的基本逻辑，想要更深入地理解，也可以尝试在阅读完 JDK 的线程池实现源码之后造个轮子。\n","date":"2020-01-19","externalUrl":null,"permalink":"/posts/threadpoolexecutor--de-jian-dan-shu-li/","section":"文章","summary":"还是楼主惯用的论述三连问，先问是什么，再问为什么，最后祭除终极大杀器 just do it ……what ? 那么什么是线程池呢？总的来说，线程池是一种线程使用模式。线程的频繁创建于调度会带来调度开销，进而影响缓存局部性和整体性能。而线程","title":"ThreadPoolExecutor 的简单梳理","type":"posts"},{"content":"📌 本文原发布于掘金社区：MapReduce 实现的简单实现\n最近有幸拜读 Google 的分布式三大论文，本着好记性不如烂笔头的原则，谈谈楼主对分布式系统开发的一点小小的心得~\n相信用过 Hadoop 的同学在等待结果输出的时候会出现类似于这样的 INFO : 2020-01-17 11:44:14,132 Stage-11 map = 0%, reduce = 0% 的日志，它展示了 MapReduce 的执行过程，下面我们就 MapReduce 进行展开，阐述 MapReduce 的执行原理，并根据 Google 的论文实现了 mini 版的 MapReduce。\n什么是 MapReduce # MapReduce is a programming model and an associated implementation for processing and generating large data sets. Users specify a map function that processes a key/value pair to generate a set of intermediate key/value pairs, and a reduce function that merges all intermediate values associated with the same intermediate key.\n就像 Google 的 MapReduce 论文中所说的，MapReduce 是一个编程模型，也是处理和生成超大数据集的算法模型及其相关实现。用户首先创建一个 Map 函数处理一个基于 key/value pair 的数据集合，输出一个基于 key/value pair 的中间数据集合，然后再创建一个 Reduce 函数用来合并所有具有相同中间 key 值的中间 value 值。\nMapReduce 的例子 # Map 与 Reduce 的原理 # MapReduce 编程模型的原理是：利用一个输入 key/value pair 集合来产生一个输出的 key/value pair 集合。MapReduce 库的用户用两个函数表达这个计算：Map 和 Reduce。\nMap : 用户自定义的 Map 函数接受一个输入的 key/value pair 值，然后产生一个中间 key/value pair 值的集合。MapReduce 库把所有具有相同中间 key 值的中间 value 值集合在一起后传递给 Reduce 函数。\nReduce : 用户自定义的 Reduce 函数接受一个中间 key 的值和相关的一个 value 值的集合。Reduce 函数合并这些 value 值，形成一个较小的 value 值的集合。一般来说，每次 Reduce 函数调用只产生 0 或 1 个输出 value 值。通常我们通过一个迭代器把中间 value 值提供给 Reduce 函数，这样我们就可以处理无法全部放入内存中的大量 value 值的集合。\nMap 与 Reduce 的应用例子 # 计算文档中每个单词出现的次数：Map 函数处理文档，对文档中的单词进行拆分，然后输出(单词，1)。Reduce 函数把相同单词的 value 值都累加起来，产生(单词，记录总数)结果。 倒排索引：Map 函数分析每个文档输出一个(词，文档号)的列表，Reduce 函数的输入是一个给定词的所有（词，文档号），排序所有的文档号，输出(词，list（文档号）)。所有的输出集合形成一个简单的倒排索引，它以一种简单的算法跟踪词在文档中的位置。 MapReduce 执行过程图解 # 上图中展示了我们的 MapReduce 实现中的全部执行流程。当用户调用 MapReduce 函数时，将发生下面的一系列动作（下面的序号和上图中的序号一一对应）：\n用户程序首先调用 MapReduce 库，将输入文件分成 M 个数据片段，每个数据片段的大小一般从 16MB 到 64MB (可以通过可选的参数来控制每个数据片段的大小)。然后用户程序在机群中创建大量的程序副本。 这些程序副本中有一个特殊的程序 master。副本中其它的程序都是 worker 程序，由 master 分配任务。M 个 Map 任务和 R 个 Reduce 任务将被分配，master 将一个 Map 任务或 Reduce 任务分配给一个空闲的 worker。 被分配了 Map 任务的 worker 程序读取相关的输入数据片段，从输入的数据片段中解析出 key/value pair，然后把 key/value pair 传递给用户自定义的 Map 函数，由 Map 函数生成并输出中间 key/value pair，并缓存在内存中。 缓存中的key/value pair 通过分区函数分成 R 个区域，之后周期性地写入到本地磁盘上。缓存的 key/value pair 在本地磁盘上的存储位置将被回传给 master，由 master 负责把这些存储位置再传送给 Reduce worker。 当 Reduce worker 程序接收到 master 程序发来的数据存储位置信息后，使用 RPC 从 Map worker 所在主机的磁盘上读取这些缓存数据。当 Reduce worker 读取了所有的中间数据后，对 key 进行排序后使得具有相同 key 值的数据聚合在一起。由于许多不同的 key 值会映射到相同的 Reduce 任务上，因此必须进行排序。如果中间数据太大无法在内存中完成排序，那么就要在外部进行排序。 Reduce worker 程序遍历排序后的中间数据，对于每一个唯一的中间 key 值，Reduce worker 程序将这个 key 值和它相关的中间 value 值的集合传递给用户自定义的 Reduce 函数。Reduce 函数的输出被追加到所属分区的输出文件。 当所有的 Map 和 Reduce 任务都完成之后，master 唤醒用户程序。在这个时候，用户程序里对 MapReduce 的调用才返回。 在成功完成任务之后，MapReduce 的输出存放在 R 个输出文件中（对应每个 Reduce 任务产生一个输出文件，文件名由用户指定）。一般情况下，用户不需要将这 R 个输出文件合并成一个文件，我们经常把这些文件作为另外一个 MapReduce 的输入，或者在另外一个可以处理多个分割文件的分布式应用中使用。\nMapReduce 程序的实现 # MapReduce 的核心就是实现其 Map 与 Reduce 的逻辑代码，现在楼主将按照上面描述的 Map 与 Reduce 的执行过程完成对 Map 与 Reduce 的实现。\n实现 Map # 1，下面的 doMap 函数管理一项 map 任务：它读取输入文件（inFile），为该文件的内容调用用户定义的 map 函数（mapF），然后将 mapF 的输出分成 nReduce 个中间文件。\n2，每个 reduce 任务对应一个中间文件。文件名包括 map 任务编号和 reduce 任务编号。使用由 reduceName 函数生成的文件名作为 reduce 任务的中间文件。对每个 key 调用 ihash（）并对 nReduce 取模，来选择对应的 reduce 任务。\n3，mapF 是应用程序提供的 map 函数。第一个参数应该是输入文件名。第二个参数应该是整个输入文件的内容。mapF（）返回包含用于 reduce 的键/值对的切片。\n4，下面程序中使用 JSON 格式将 mapF 处理好的数据写入文件中，为了数据处理方便，下面程序中处理好的每条数据都采用换行符进行分割。\nfunc reduceName(jobName string, mapTask int, reduceTask int) string { return \u0026#34;mrtmp.\u0026#34; + jobName + \u0026#34;-\u0026#34; + strconv.Itoa(mapTask) + \u0026#34;-\u0026#34; + strconv.Itoa(reduceTask) } func doMap( jobName string, // MapReduce 的任务名称 mapTask int, // 当前执行的 mapTask inFile string, // 输入的的文件 nReduce int, // reduceTask 的数量 mapF func(filename string, contents string) []KeyValue, // 用户自定义的 map 函数 ) { f, err := os.Open(inFile) if err != nil { debug(\u0026#34;open file err %v\u0026#34;, err) } defer f.Close() dat, err := ioutil.ReadAll(f) if err != nil { debug(\u0026#34;open map file err %v\u0026#34;, err) } res := mapF(inFile, string(dat)) for _, kv := range res { hash := ihash(kv.Key) r := hash % nReduce // mrtmp.xxx-0-0 fd, err := os.OpenFile(reduceName(jobName, mapTask, r), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { debug(\u0026#34;open mrtmp.xxx file err %v\u0026#34;, err) continue } enc := json.NewEncoder(fd) if err := enc.Encode(\u0026amp;kv); err != nil { debug(\u0026#34;encode json err %v\u0026#34;, err) continue } fd.Close() } } func ihash(s string) int { h := fnv.New32a() h.Write([]byte(s)) return int(h.Sum32() \u0026amp; 0x7fffffff) } 实现 Reduce # doReduce 管理一个 reduce 任务：它读取任务的中间文件，按 key 对中间文件中的数据对进行排序，为每个 key 调用用户定义的 reduceF 函数，并将 reduceF 的输出写入磁盘。\nfunc reduceName(jobName string, mapTask int, reduceTask int) string { return \u0026#34;mrtmp.\u0026#34; + jobName + \u0026#34;-\u0026#34; + strconv.Itoa(mapTask) + \u0026#34;-\u0026#34; + strconv.Itoa(reduceTask) } func doReduce( jobName string, // MapReduce 的任务名称 reduceTask int, // 当前运行的 reduce 任务的任务号 outFile string, // 结果输出的文件路径 nMap int, // map 任务的个数 reduceF func(key string, values []string) string, // 用户的自定义 reduce 函数 ) { kvMap := make(map[string][]string) for i := 0; i \u0026lt; nMap; i++ { func() { inFileName := reduceName(jobName, i, reduceTask) inFile, err := os.Open(inFileName) if err != nil { panic(\u0026#34;can\u0026#39;t open file:\u0026#34; + inFileName) } defer inFile.Close() // Read and Decoder the file var kv KeyValue for decoder := json.NewDecoder(inFile); decoder.Decode(\u0026amp;kv) != io.EOF; { kvMap[kv.Key] = append(kvMap[kv.Key], kv.Value) } }() } var keys []string // sort by key for k := range kvMap { keys = append(keys, k) } sort.Strings(keys) // reduce outfd, err := os.Create(outFile) if err != nil { panic(\u0026#34;can\u0026#39;t create file:\u0026#34; + outFile) } defer outfd.Close() enc := json.NewEncoder(outfd) for _, k := range keys { reducedValue := reduceF(k, kvMap[k]) enc.Encode(KeyValue{Key: k, Value: reducedValue}) } } 对 doMap 与 doReduce 的封装 # 下面的函数是对 doMap 与 doReduce 进行顺序调用，将 MapReduce 任务的结果输出到结果文件中。\nfunc Sequential(jobName string, files []string, nreduce int, mapF func(string, string) []KeyValue, reduceF func(string, []string) string, ) (mr *Master) { mr = newMaster(\u0026#34;master\u0026#34;) go mr.run(jobName, files, nreduce, func(phase jobPhase) { switch phase { case mapPhase: for i, f := range mr.files { doMap(mr.jobName, i, f, mr.nReduce, mapF) } case reducePhase: for i := 0; i \u0026lt; mr.nReduce; i++ { doReduce(mr.jobName, i, mergeName(mr.jobName, i), len(mr.files), reduceF) } } }, func() { mr.stats = []int{len(files) + nreduce} }) return } 使用 MapReduce 实现词频统计和倒排索引 # 在上面我们提到了 MapReduce 在实际应用中的例子，下面我们将对这两个例子做一下简单的实现。\n实现词频统计 # 为了实现词频统计这一功能，我们使用 MapReduce 框架的思路就是实现自定义的 map 与 reduce 函数：1，map：读取文档，将文档中的单词逐个提取出来，生成（单词，1）这样的键值对，然后把数据落盘，写入到中间文件中。\n2，reduce：读取中间文件，按照键值对进行排序，将 key 相同的数据聚合到一起，统计每个单词出现的次数，然后将结果写入到文件中。\npackage main import ( \u0026#34;6.824/src/mapreduce\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;unicode\u0026#34; ) func mapF(filename string, contents string) (res []mapreduce.KeyValue) { // Your code here (Part II). f := func(c rune) bool { return !unicode.IsLetter(c) } words := strings.FieldsFunc(contents, f) for _, w := range words { kv := mapreduce.KeyValue{Key: w, Value: \u0026#34;1\u0026#34;} res = append(res, kv) } return res } func reduceF(key string, values []string) string { // Your code here (Part II). sum := 0 for _, e := range values { data, err := strconv.Atoi(e) if err != nil { fmt.Printf(\u0026#34;Reduce err %s%v\\n\u0026#34;, key, err) continue } sum += data } return strconv.Itoa(sum) } func main() { if len(os.Args) \u0026lt; 4 { fmt.Printf(\u0026#34;%s: see usage comments in file\\n\u0026#34;, os.Args[0]) } else { var mr *mapreduce.Master mr = mapreduce.Sequential(\u0026#34;wcseq\u0026#34;, os.Args[3:], 3, mapF, reduceF) mr.Wait() } } 实现倒排索引 # 同样，在理解了倒排索引的基础上设计我们自己的 map 与 reduce 方法，1，map：读取文档，将文档中的单词作为 key，单词所在的文档作为 value，写入到中间文件中。\n2，reduce：读取中间文件，按照键值对进行排序，将 key 相同的数据聚合到一起，将单词出现的文件名拼接在一起，写入到结果文件中。\npackage main import ( \u0026#34;bytes\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;unicode\u0026#34; ) import \u0026#34;fmt\u0026#34; import \u0026#34;6.824/src/mapreduce\u0026#34; func mapF(document string, value string) (res []mapreduce.KeyValue) { // Your code here (Part V). words := strings.FieldsFunc(value, func(c rune) bool { return !unicode.IsLetter(c) }) for _, w := range words { res = append(res, mapreduce.KeyValue{Key: w, Value: document}) } return res } func reduceF(key string, values []string) string { // Your code here (Part V). sum := 0 var buffer bytes.Buffer if key == \u0026#34;www\u0026#34; { fmt.Println(values) } isExist := make(map[string]string) for _, e := range values { if _, ok := isExist[e]; !ok { buffer.WriteString(e) buffer.WriteString(\u0026#34;,\u0026#34;) sum += 1 isExist[e] = e } } iiRes := strconv.Itoa(sum) + \u0026#34; \u0026#34; + strings.TrimRight(buffer.String(), \u0026#34;,\u0026#34;) return iiRes } func main() { if len(os.Args) \u0026lt; 4 { fmt.Printf(\u0026#34;%s: see usage comments in file\\n\u0026#34;, os.Args[0]) } else { var mr *mapreduce.Master mr = mapreduce.Sequential(\u0026#34;iiseq\u0026#34;, os.Args[3:], 3, mapF, reduceF) mr.Wait() } } 小结 # 本文参考 Google 的论文，实现了一个单机版的 MapReduce 框架，并实现了两个简单的 MapReduce 实例，文中的代码可以在楼主的 GitHub 下载查看。\n参考 # MapReduce: Simplified Data Processing on Large Clusters\n","date":"2020-01-18","externalUrl":null,"permalink":"/posts/mapreduce-shixiandejiandanshixian/","section":"文章","summary":"相信用过 Hadoop 的同学在等待结果输出的时候会出现类似于这样的 INFO : 2020-01-17 11:44:14,132 Stage-11 map = 0%, reduce = 0% 的日志，它展示了 MapReduce 的执行过程，下面我们也将就 MapReduce…","title":"MapReduce 的简单实现","type":"posts"},{"content":"","date":"2020-01-18","externalUrl":null,"permalink":"/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/","section":"标签","summary":"","title":"大数据","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：使用 Map 实现策略模式\n上篇文章在谈到优化代码的时候，有一部分涉及到了使用策略模式优化我们的代码，本篇文章将围绕策略模式谈谈自己的思考~\nWhat? # 总的来说，设计模式是对软件设计中普遍存在并且反复出现的各种问题所提出的通用解决方案，是一系列编码经验的集合。\n那么什么是策略模式呢？它定义一系列算法，将各个算法封装起来，并且使它们可以互相替换。策略模式使得算法能够独立于使用它的用户而改变。如下图所示\\\n📷 图注：策略模式时区图\nWhy ? # 完成一项任务，往往可以有多种不同的方式，每一种方式称为一个策略，我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。 在软件开发中也常常遇到类似的情况，实现某一个功能有多个途径，此时可以使用一种设计模式使得系统可以灵活地选择解决途径，也能够方便地增加新的解决途径。 在软件系统中，有许多算法可以实现某一功能，如查找、排序等，一种常用的方法是硬编码(Hard Coding)在一个类中，如需要提供多种查找算法，可以将这些算法写到一个类中，在该类中提供多个方法，每一个方法对应一个具体的查找算法；当然也可以将这些查找算法封装在一个统一的方法中，通过 if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码，如果需要增加一种新的查找算法，需要修改封装算法类的源代码；更换查找算法，也需要修改客户端调用代码。在这个算法类中封装了大量查找算法，该类代码会比较复杂，维护较为困难。 除了提供专门的查找算法类之外，还可以在客户端程序中直接包含算法代码，这种做法更不可取，将导致客户端程序庞大而且难以维护，当存在大量可供选择的算法时问题将变得更加严重。 为了解决这些问题，可以定义一些独立的类来封装不同的算法，每一个类封装一个具体的算法，在这里，每一个封装算法的类我们都可以称之为策略(Strategy)，为了保证这些策略的一致。 How ? # 在软件编码中，实现策略模式需要我们定义各种策略类，但是在 go 中我们可以使用 map 来避免这一缺点，直接定义需要实现的策略方法即可。\n一般情况下我们都是采用 if/else 来做策略选择，然而当业务逻辑越来越复杂的时候，代码就会变得很臃肿，难以维护，比如这样的\nif condition : // doSomething else if condition: // doOtherthing else if // doAnOtherThing else // doAlotOfthing golang 实现的”策略模式“ # 策略模式的精髓是封装一组算法实现以供使用时的调度，golang 里面有一个很重要的语法糖就是 func() 方法变量，因此，在 golang 中实现类似策略模式的做法，不需要依赖于对象即可实现，比如\\\npackage main import ( \u0026quot;fmt\u0026quot; ) var Strategy map[string]func(v ...interface{}) func init(){ Strategy := make(map[string]func(v ...interface{})) Strategy[\u0026quot;add\u0026quot;] = func(a int,b int) { fmt.Println(\u0026quot;a + b\u0026quot;) } Strategy[\u0026quot;reduce\u0026quot;] = func(a int,b int) { fmt.Println(\u0026quot;a - b\u0026quot;) } Strategy[\u0026quot;multiply\u0026quot;] = func(a int,b int) { fmt.Println(\u0026quot;a*b\u0026quot;) } Strategy[\u0026quot;divide\u0026quot;] = func(a int,b int) { fmt.Println(\u0026quot;a/b\u0026quot;) } } func main(){ Strategy[\u0026quot;insert\u0026quot;]() } 小结 # 使用 map 来实现策略模式的优点 # 策略模式的核心是封装一组算法实现特别是相似的算法实现，所以我们可以通过 map 来进行 KV 的约束，key 是客户端传进来的对应策略，用具体的算法实现 fun() 作为 value，这样无论是算法的封装还是调度都从业务场景中解耦了。\n使用 map 来实现策略模式的缺点 # 当然，缺点就是如果需要扩展策略，就要去增加一个 Entry\u0026lt;K,V\u0026gt;,没有传统的实现方式中直接扩展一个实现了策略接口的对象那么方便，这两个还得看具体的项目取舍，一句老话，没有好坏，只有合适不合适，好的软件实现都是考虑到各种情况的折中。\n","date":"2020-01-16","externalUrl":null,"permalink":"/posts/shi-yong--map--shi-xian-ce-lve-mo-shi/","section":"文章","summary":"上篇文章在谈到优化代码的时候，有一部分涉及到了使用策略模式优化我们的代码，本篇文章将围绕策略模式谈谈自己的思考~ What? 总的来说，设计模式是对软件设计中普遍存在并且反复出现的各种问题，所提出的通用解决方案，是一系列编码经验的集合。那","title":"使用 Map 实现策略模式","type":"posts"},{"content":"","date":"2020-01-16","externalUrl":null,"permalink":"/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/","section":"标签","summary":"","title":"设计模式","type":"tags"},{"content":"📌 本文原发布于掘金社区：使用 Go 优化我们的接口\n标题起的是有点大，不过还好本篇文章主要也是使用 Go 来优化 HTTP 服务的，也算打个擦边球吧~\n背景 # 特征数据暴增，导致获取一个城市下所有的特征的接口延时高，下面是监控上看到的接口响应耗时，最慢的时候接口响应时间能达到 5s 多。\n缓存优化方案 # 代码优化思路：\n1，使用缓存。\n1.1 为什么使用内存，而不是 Redis？\n分析业务需求，当前需要存储起来的数据是 ObjectId，ObjectId 是一个长度为 14 左右的字符串，我们假设平均下来 ObjectId 是长度为 16 的字符串，这样算下来，每个 ObjectId 占用的内存大小是 2 个字节，当前业务需要存储的 ObjectId 大概是 30万条，这样算下来当前业务需要存储的 ObjectId 占用的内存在 0.5M，完全可以在内存中进行操作。相比于使用 Redis 来说没有网络开销，效率更高。\n1.2 缓存初始化：当服务启动时，本地缓存初始化为空。\n1.3 关于缓存版本的概念。\n缓存版本是：离线特征生产任务更新后，将数据版本更新到 DB 中。\n下面三种方案都是基于内存存储 ObjectId 数据，在内存更新的时候策略有所不同。\n方案一 # 2.1 缓存更新\n使用主动更新缓存的方式，创建定时任务，每间隔 1分钟查一次 DB 的数据版本，若更新则更新缓存中的数据。\n2.2 缺点\n单独启动一个缓存更新线程，代码不好维护，也会有定时任务线程挂掉的情况，不易发现。还有就是需要提前把相关参数配置到代码中或者引入配置中心，维护成本较高。\n方案二 # 3.1 缓存更新\n采用被动触发的缓存更新策略，由接口调用触发。请求进来后检测当前缓存中的数据的版本与 DB 中的数据版本是否一致，若版本更新，则重新读取当前请求对应城市的所有数据到缓存中，并将更新后的数据返回给调用方。\n3.2 缺点\n由于被动触发是同步更新缓存的，容易造成接口调用时如果正好遇上版本更新，需要更新数据到内存中，从而出现偶现的毛刺。\n3.3 业务执行时序图 方案三（最终采用的方案） # 4.1，缓存更新\n采用被动更新缓存的策略，由接口调用方触发。若当前缓存中有数据则直接返回缓存中的数据，然后检测当前缓存中的数据的版本与 DB 中的数据版本是否一致，若版本更新，则重新读取当前请求对应城市的所有 feature 数据到缓存中，反之结束缓存更新逻辑。\n4.2 业务执行时序图 并发优化方案 # 使用 Goroutine 来优化我们的串行逻辑 # Go 语言最大的特色就是从语言层面支持并发（Goroutine），Goroutine 是 Go 中最基本的执行单元。事实上每一个 Go 程序至少有一个 Goroutine：主 Goroutine。当程序启动时，它会被自动创建。\n为了更好地理解 Goroutine，现讲一下线程和协程的概念：\n线程（Thread）：有时被称为轻量级进程(Lightweight Process，LWP），是程序执行流的最小单元。一个标准的线程由线程 ID，当前指令指针(PC），寄存器集合和堆栈组成。另外，线程是进程中的一个实体，是被系统独立调度和分派的基本单位，线程自己不拥有系统资源，只拥有一点儿在运行中必不可少的资源，但它可与同属一个进程的其它线程共享进程所拥有的全部资源。\n线程拥有自己独立的栈和共享的堆，共享堆，不共享栈，线程的切换一般也由操作系统调度。\n协程（coroutine）：又称微线程。与子例程（或者称为函数）一样，协程（coroutine）也是一种程序组件。相对子例程而言，协程更为一般和灵活，但在实践中使用没有子例程那样广泛。\n和线程类似，共享堆，不共享栈，协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费，兼顾了多线程的优点，简化了高并发程序的复杂度。\ngolang 中的 map 是线程不安全的 # 很显然，我们可以用锁机制解决 Map 的并发读写问题。我们将上面的 map 结构改成如下：\n// M type M struct { Map map[string]string lock sync.RWMutex // 加锁 } // Set ... func (m *M) Set(key, value string) { m.lock.Lock() defer m.lock.Unlock() m.Map[key] = value } // Get ... func (m *M) Get(key string) string { return m.Map[key] } 在上面的代码中，我们引入了锁机制，从而保证了 map 在多个 goroutine 中的安全。\n使用策略模式优化我们的逻辑 # 这块主要是因为代码中存在太多的 if/else，故采用策略模式来优化我们的代码结构。这里先放上一篇网上找到的文章，之后有时间再单独出一篇相关文章吧。优化后的代码相较于之前代码量少了 50%，更加清晰与便于维护。下面是优化的代码上线后的效果，请求耗时都在 100ms 以下：\n小结 # 上面整体介绍了下我们的接口耗时较长时的一般处理方案，当然具体问题还得具体分析，所以当出现接口反应慢的情况时，我们应该具体分析接口反应慢的原因，方可对症下药！\n","date":"2019-12-30","externalUrl":null,"permalink":"/posts/shiyong-go-youhuawomendejiekou/","section":"文章","summary":"特征数据暴增，导致获取一个城市下所有的特征的接口延时高，下面是监控上看到的接口响应耗时，最慢的时候接口响应时间能达到 5s 多。1，使用缓存。分析业务需求，当前需要存储起来的数据是 ObjectId，ObjectId 是一个长度为 14 左右的字符串，我们假设平均下来 Object…","title":"使用 Go 优化我们的接口","type":"posts"},{"content":"","date":"2019-12-30","externalUrl":null,"permalink":"/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/","section":"标签","summary":"","title":"服务器","type":"tags"},{"content":"","date":"2019-11-27","externalUrl":null,"permalink":"/tags/tcp/ip/","section":"标签","summary":"","title":"TCP/IP","type":"tags"},{"content":"📌 本文原发布于掘金社区：我理解的 TCP 连接\n我理解的 TCP 连接-原文链接\n我理解的 TCP 连接-原文链接\n我理解的 TCP 连接-原文链接\\\n总述 # TCP 是面向连接的协议。运输连接是用来传输 TCP 报文的。TCP 运输连接的建立和释放是每一次面向连接通信中必不可少的过程。因此，运输连接有三个阶段，即：连接建立，数据传输和连接释放。\n在 TCP 连接建立过程中要解决以下三个问题。\n（1）要使一方明确知道对方的存在。（2）要允许双方协商一些参数（如最大窗口值等）。（3）能够对运输实体资源进行分配。\nTCP 的连接建立（三次握手） # 如上图所示，上图画出了 TCP 的连接过程。假定主机 A 运行的是 TCP 客户程序，而 B 运行的是 TCP 服务器程序。最初两端的 TCP 进程都处于 CLOSE 状态。图中在主机下面的方框中分别是 TCP 进程所处于的状态。请注意，A 主动打开连接，而 B 被动打开连接。\nB 的 TCP 服务器进程先创建传输控制块 TCB，准备接受客户进程的连接请求。然后服务器进程处于 LISTEN 状态，等待客户的连接请求。如有，即作出响应。\nA 的 TCP 客户进程也是首先创建传输控制块 TCB，然后向 B 发出连接请求报文段。其首部的同步位 SYN = 1，同时选择一个初始序号 seq = x。TCP 规定，SYN 报文段，不能携带数据，但要消耗掉一个序号，这时 TCP 客户程序进入 SYN-SEND（同步已发送）状态。\nB 接收到连接请求报文段后，如同意连接，则向 A 发送确认。在确认报文段中应把 SYN 位和 ACK 位都设置为 1，确认号是 ack = x + 1，同时也为自己选择一个初始序号 seq = y。请注意，这个报文段也不能携带任何数据，但同样要消耗掉一个序号。这时，TCP 服务器程序进入 SYN-RCVD（同步收到）状态。\nTCP 客户进程收到 B 的确认后，还要向 B 确认。确认报文段的 ACK 置 1，确认号 ack = y + 1，而自己的序号 seq = x + 1。TCP 的标准规定，ACK 报文段可以携带数据。但如果不携带数据则不消耗序号，在这种情况下，下一个数据报文段的序号仍然是 seq = x + 1。这时 TCP 连接建立完成，A 进入 ESTABLISHED（已建立连接）状态。当 B 收到 A 的确认后，也进入 ESTABLISHED 状态。\nTCP 连接的释放（四次挥手） # 数据传输结束后，通信双方都可以释放连接。现在 A 和 B 都处于 ESTABLISHED 状态。A 的应用进程先向其 TCP 发出连接释放报文段，并不再发送数据，主动关闭 TCP 连接。A 把连接释放报文段首部的终止控制位 FIN 设置为 1，其序号 seq = u，它等于前面传送过的数据的最后一个字节序号加 1。这时 A 进入 FIN_WAIT_1（终止等待 1）状态，等待 B 的确认。请注意，TCP 规定，FIN 报文段即使不携带数据，它也消耗掉一个序号。\nB 收到连接释放的报文段后立即发出确认，确认号 ack = u + 1，而这个报文段自己的序号是 v，等于 B 前面已传送过的数据的最后一个字节加 1。然后 B 进入 CLOSE_WAIT（关闭等待）状态。TCP 服务器进程这时通知高层的应用进程，因而从 A 到 B 这个方向的连接就释放了，这时的 TCP 连接处于半关闭（half-close）状态，即 A 已经没有数据要发送了，但是 B 若发送数据，A 仍要接收。也就是说，从 B 到 A 这个方向的连接并未关闭，这个状态可能会持续一段时间。\nA 收到来自 B 的确认后，就进入 FIN_WAIT_2（终止等待 2）状态，等待 B 发出连接释放报文段。\n若 B 已经没有要向 A 发送的数据，其应用进程就通知 TCP 释放连接。这时 B 发出连接释放报文段使 FIN = 1。现假定 B 的序号为 w（在半关闭状态 B 可能又发送了一些数据）。B 还必须重复上次已经发过的确认号 ack = u + 1。这时 B 就进入 LAST_ACK（最后确认）状态，等待 A 的确认。\nA 在收到 B 的连接释放报文段后，必须对此发出确认。在确认报文段中把 ACK 设置为 1，确认号 ack = w + 1，而自己的序号是 seq = u + 1（根据 TCP 的标准，前面发送过的 FIN 报文要消耗一个序号）。然后进入到 TIME_WAIT（时间等待）状态。请注意，现在 TCP 连接还没有释放掉。必须经过时间等待计时器设置的时间 2MSL 后，A 才进入到 CLOSE 状态。时间 MSL 叫做最长报文段寿命，RFC 793 建议设置为 2 分钟。但这完全是从工程上来考虑，对于现在的网络，MSL = 2分钟可能太长了一些。因此 TCP 允许不同的实现可以根据实际情况使用更小的 MSL 值。因此，从 A 进入到 TIME_WAIT 状态后，要经过 4 分钟才能进入到 CLOSE 状态，才能建立下一个连接。当 A 撤销相应的传输控制块 TCB 后，就结束这次的 TCP 连接。\n两个小问题 # 在三次握手的过程中，为什么 A 还要发送一次确认呢？ 这主要是为了防止已失效的连接请求报文突然又传到了 B，因而产生错误。\n为什么 A 在 TIME_WAIT 状态必须等待 2MSL 的时间呢？\n第一，为了保证 A 发送的最后一个 ACK 报文段能够到达 B。\n第二，防止刚提到的 “已失效的连接请求报文段” 出现在本连接中。A 在发送完最后一个 ACK 报文段后，再经过时间 2MSL，就可以使本连接持续时间内所产生的所有报文段从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。\n参考链接 # 计算机网络第 6 版\n关注我们 # ","date":"2019-11-27","externalUrl":null,"permalink":"/posts/wolijiede-tcp-lianjie/","section":"文章","summary":"TCP 是面向连接的协议。运输连接是用来传输 TCP 报文的。TCP 运输连接的建立和释放是每一次面向连接通信中必不可少的过程。因此，运输连接有三个阶段，即：连接建立，数据传输和连接释放。在 TCP 连接建立过程中要解决一下三个问题。（1）要使一方明确知道对方的存在。（2）要…","title":"我理解的 TCP 连接","type":"posts"},{"content":"📌 本文原发布于掘金社区：Kafka Consumer 的 Rebalance 机制\nKafka Consumer 的 Rebalance 机制-原文链接\nKafka Consumer 的 Rebalance 机制-原文链接\nKafka Consumer 的 Rebalance 机制-原文链接\\\n上周参加了 Kafka Meetup 北京站的技术分享，本文简单介绍下 Kafka Consumer 的 Rebalance 机制以及其新版本中的优化策略~\nKafka 之前版本的 Consumer Groups # Consumer Group # 如上图所示，Consumer 使用 Consumer Group 名称标记自己，并且发布到主题的每条记录都会传递到每个订阅消费者组中的一个 Consumer 实例。Consumer 实例可以在单独的进程中或在单独的机器上。\n如果所有 Consumer 实例都属于同一个 Consumer Group，那么这些 Consumer 实例将以负载均衡的方式来消费 Kafka。\n如果所有 Consumer 实例具有不同的 Consumer Group，则每条记录将被广播到所有 Consumer 进程。\nGroup Coordinator # Group Coordinator 是一个服务，每个 Broker在启动的时候都会启动该服务。Group Coordinator 的作用是存储 Group 的相关 Meta 信息，并将对应 Partition 的 Offset 信息记录到 Kafka 内置Topic(__consumer_offsets) 中。Kafka 在 0.9 之前是基于 Zookeeper 来存储 Partition 的 Offset 信息 (consumers/{group}/offsets/{topic}/{partition})，因为 Zookeeper 并不适用于频繁的写操作，所以在 0.9 之后通过内置 Topic 的方式来记录对应 Partition 的 Offset。如下图所示：\n在 Kafka 0.8.2 之前是这样的\n之后是这样的：\n每个 Group 都会选择一个 Coordinator 来记录自己组内各 Partition 的 Offset 信息，选择的规则如下：\n计算 Group 对应在 __consumer_offsets 上的 Partition 根据对应的 Partition 寻找该 Partition 的 leader 所对应的 Broker，该 Broker 上的 Group Coordinator 就是该 Group 的 Coordinator Partition 计算规则：\npartition-Id(__consumer_offsets) = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount) 其中 groupMetadataTopicPartitionCount 对应 offsets.topic.num.partitions 参数值，默认值是 50 个分区\nConsumer Rebalance Protocol # 发生 rebalance 的时机 # 组成员个数发生变化。例如有新的 consumer 实例加入该消费组或者离开组。 订阅的 Topic 个数发生变化。 订阅 Topic 的分区数发生变化。 消费者进程挂掉的情况 # session 过期 heartbeat 过期 Rebalance 发生时，Group 下所有 Consumer 实例都会共同参与，Kafka 能够保证尽量达到最公平的分配。但是 Rebalance 过程对 Consumer Group 会造成比较严重的影响。在 Rebalance 的过程中 Consumer Group 下的所有消费者实例都会停止工作，等待 Rebalance 过程完成。\n消费者的 Rebalance 协议 # Rebalance 发生后的执行过程 # 1，有新的 Consumer 加入 Consumer Group\n2，从 Consumer Group 选出 leader\n3，leader 进行分区的分配\nIssues # Known Issue #1: Stop-the-world Rebalance\n如上图所示：之前版本的 Kafka 在发生 Rebalance 的时候会释放 Consumer Group 的所有资源，造成比较长的 Stop-the-world\nKnown Issue #2: Back-and-forth Rebalance\n如上图所示：在发生 Rebalance 的时候发生了不必要的资源释放与重新分配。\n当前的 Rebalance 与 改进后的 ReBalance 对比 # 渐进式 Rebalance 协议 # 如上图所示，新的渐进式 Rebalance 协议，在 Rebalance 的时候不需要当前所有的 Consumer 释放所拥有的资源，而是当需要触发 Rebalance 的时候对当前资源进行登记，然后进行渐进式的 Rebalance。这样做产生的优化效果如下\n相较之前进行了更多次数的 Rebalance，但是每次 Rebalance 对资源的消耗都是比较廉价的 发生迁移的分区相较之前更少了 Consumer 在 Rebalance 期间可以继续运行 参考文章 # Incremental Cooperative Rebalancing in Apache Kafka: Why Stop the World When You Can Change It? KIP-429: Kafka Consumer Incremental Rebalance Protocol Incremental Cooperative Rebalancing: Support and Policies 关注我们 # ","date":"2019-11-19","externalUrl":null,"permalink":"/posts/kafka-consumer-de-rebalance-jizhi/","section":"文章","summary":"如上图所示，Consumer 使用 Consumer Group 名称标记自己，并且发布到主题的每条记录都会传递到每个订阅消费者组中的一个 Consumer 实例。Consumer 实例可以在单独的进程中或在单独的机器上。如果所有 Consumer 实例都属于同一个 Con…","title":"Kafka Consumer 的 Rebalance 机制","type":"posts"},{"content":"📌 本文原发布于掘金社区：双重检查锁定与单例\n双重检查锁定与单例-原文链接\n双重检查锁定与单例-原文链接\n双重检查锁定与单例-原文链接\\\n对于单例模式，相信大多数人都可以写出好几种实现方法，懒汉，饿汉等等，然而小小单例真要写好，写的完全正确也并非易事。\n双重检查锁的单例 # 下面是我们经常使用的一种单例的实现，也就是双重检查锁的实现方案。\npublic class Singleton { private static Singleton instance; private Singleton() { } public Singleton getInstance() { if (null == instance) { synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); // error } } } return uniqueSingleton; } } 让我们来看一下这个代码是如何工作的：首先当一个线程发出请求后，会先检查 instance 是否为 null，如果不是则直接返回其内容，这样避免了进入 synchronized 块所需要花费的资源。其次，如果两个线程同时进入了第一个 if 判断，那么他们也必须按照顺序执行 synchronized 块中的代码，第一个进入代码块的线程会创建一个新的 Singleton 实例，而后续的线程则因为无法通过 if 判断，而不会创建多余的实例。\n但还有一个问题，在有些情况下，通过这种方式拿到的 Singleton 对象，可能是错误的。\n回顾我们 new 对象的 3 个步骤\n1，分配内存空间\n2，初始化对象\n3，将对象指向刚分配的内存空间\n但 jvm 在指令优化时，会出现步骤 2 和 3 对调的情况，比如线程 1 在经过两层为 null 判断后，进入 new 的动作，在还没有初始化对象时，就返回了地址值，线程 2 在进行第一个为 null 判断时，因为对象已经不为空，那么就直接返回了对象。然而当线程 2 打算使用 Singleton 实例，却发现它没有被初始化，于是错误发生了。\n解决方案 # 对于上面的问题，有两种解决方案\n1，使用 volatile 关键词主要可以保证代码的执行顺序不受 jvm 重排序的影响。\npublic class Singleton { private volatile static Singleton instance; private Singleton() { } public Singleton getInstance() { if (null == instance) { synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); // error } } } return instance; } } 2，通过内部类实现多线程环境中的单例模式。\npublic class Singleton { private Singleton() { } private static class SingletonContainer { private static Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonContainer.instance; } } 关注我们 # ","date":"2019-11-13","externalUrl":null,"permalink":"/posts/shuangchongjianchasuodingyudanli/","section":"文章","summary":"对于单例模式，相信大多数人都可以写出好几种实现方法，懒汉，饿汉等等，然而小小单例真要写好，写的完全正确也并非易事。下面是我们经常使用的一种单例的实现，也就是双重检查所的实现方案。让我们来看一下这个代码是如何工作的：首先当一个线程发出请求后，会先检查 instance 是否为 nu…","title":"双重检查锁定与单例","type":"posts"},{"content":"","date":"2019-11-12","externalUrl":null,"permalink":"/tags/lua/","section":"标签","summary":"","title":"Lua","type":"tags"},{"content":"📌 本文原发布于掘金社区：实时数据并发写入 Redis 优化\n实时数据并发写入 Redis 优化-原文链接\\\n对于并发请求如何在不强制加锁的情况下快速更新呢？这里有热腾腾的线上实践……\n背景 # 当前架构的逻辑是将并发请求数据写入队列中，然后起一个单独的异步线程对数据进行串行处理。这种方式的好处就是不用考虑并发的问题，当然其弊端也是显而易见的~\n乐观锁实现数据的并发更新 # 根据当前业务的数据更新在秒级、key 的碰撞率较低的情况。笔者打算采用 CAS 乐观锁方案：使用 Lua 脚本实现 Redis 对数据的原子更新，即便是在并发的情况下其性能也会上一个级别。下面是 CAS 乐观锁实现数据并发更新的流程图：\n根据上面的流程图设计出了 Lua 脚本：\nlocal keys,values=KEYS,ARGV local version = redis.call(\u0026#39;get\u0026#39;,keys[1]) if values[1] == \u0026#39;\u0026#39; and version == false then redis.call(\u0026#39;SET\u0026#39;,keys[1],\u0026#39;1\u0026#39;) redis.call(\u0026#39;SET\u0026#39;,keys[2],values[2]) return 1 end if version == values[1] then redis.call(\u0026#39;SET\u0026#39;,keys[2],values[2]) redis.call(\u0026#39;INCR\u0026#39;,keys[1]) return 1 else return 0 end 可能存在问题及其解决方案 # 1，在并发冲突概率大的高竞争环境下，如果 CAS 一直失败，会一直重试，CPU 开销较大。针对这个问题的一个思路是引入退出机制，如重试次数超过一定阈值后失败退出：\nfunc main() { for i := 0; i \u0026lt; 10; i++ { isRetry := execLuaScript() if !isRetry { break } } } func execLuaScript() bool { ctx := context.Background() r := client.GetRedisKVClient(ctx) defer r.Close() luaScript := ` local keys,values=KEYS,ARGV local version = redis.call(\u0026#39;get\u0026#39;,keys[1]) if values[1] == \u0026#39;\u0026#39; and version == false then redis.call(\u0026#39;SET\u0026#39;,keys[1],\u0026#39;1\u0026#39;) redis.call(\u0026#39;SET\u0026#39;,keys[2],values[2]) return 1 end if version == values[1] then redis.call(\u0026#39;SET\u0026#39;,keys[2],values[2]) redis.call(\u0026#39;INCR\u0026#39;,keys[1]) return 1 else return 0 end` casVersion, err := r.Get(\u0026#34;test_version\u0026#34;) kvs := make([]redis.KeyAndValue, 0) kvs = append(kvs, redis.KeyAndValue{\u0026#34;test_version\u0026#34;, casVersion.String()}) kvs = append(kvs, redis.KeyAndValue{\u0026#34;test\u0026#34;, \u0026#34;123123123\u0026#34;}) mv, err := r.Eval(luaScript, kvs...) if err != nil { log.Errorf(\u0026#34;%v\u0026#34;, err) } val, _ := mv.Int64() log.Debugf(\u0026#34;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; lua 脚本运行结果 ：%d\u0026#34;, val) if val == 1 { // lua 脚本执行成功，无需重试 return false } else if val == 0 { return true } } 2，Lua 脚本执行时只能在同一台机器上生效，因此在 Redis 集群中就要求相关联的 key 分配到相同机器。这里很多同学可能会问为什么，其实很简单，Redis 是单线程的，倘若 Lua 脚本操作的 key 在不同机器上执行，也就无法保证其执行的原子性了。\n解决方法还是从分片技术的原理上找：数据分片，就是一个 hash 的过程：对 key 做 md5，sha1 等 hash 算法，根据 hash 值分配到不同的机器上。\n为了实现将 key 分到相同机器，就需要相同的 hash 值，即相同的 key（改变 hash 算法也行，但比较复杂）。但 key 相同是不现实的，因为 key 都有不同的用途。但是我们让 key 的一部分相同，对我们业务实现来说是可以实现的。那么能不能拿 key 的一部分来计算 hash 呢？答案是肯定的。\n这就是 Hash Tag。允许用 key 的部分字符串来计算 hash。当一个 key 包含 {} 的时候，就不对整个 key 做 hash，而仅对 {} 中的字符串做 hash。假设 hash 算法为 sha1。对 user:{user1}:ids 和 user:{user1}:tweets，其 hash 值都等同于 sha1(user1)。\n小结 # 对于上面的优化过程，目前代码重构开发工作已经完成，但是还未正式上线，等上线之后再来补一下优化之后性能的提升情况~\n关注我们 # ","date":"2019-11-12","externalUrl":null,"permalink":"/posts/shishishujubingfaxieru-redis-youhua/","section":"文章","summary":"当前架构的逻辑是将并发请求数据写入队列中，然后起一个单独的异步线程对数据进行串行处理。这种方式的好处就是不用考虑并发的问题，当然其弊端也是显而易见的~ 根据当前业务的数据更新在秒级，key 的碰撞率较低的情况。笔者打算采用使用 CAS 乐观锁方案：使用 Lua 脚本实现 Red…","title":"实时数据并发写入 Redis 优化","type":"posts"},{"content":"📌 本文原发布于掘金社区：我理解的零拷贝\n我理解的零拷贝-原文链接\\\n最近做的业务涉及到的 I/O 操作比较多，对于 Linux 上的 I/O 操作的优化 Zero Copy 早有耳闻，今天打算由上而下（从应用层到底层，当然并不会涉及到内核的细节）地研究一下这个问题。\n什么是零拷贝 # 为了更好地描述 zero copy，本文将以网络服务器的一个简单过程为例展开讨论，该过程通过网络将存储在服务端的文件中的数据提供给客户端。整个过程主要是网络 I/O 操作，数据至少被复制了 4 次，并且发生了多次用户态/内核态上下文切换。如下图所示，经过了下面四个步骤：\n步骤一：操作系统执行 read 系统调用，读取磁盘中的文件内容并将其存储到内核地址空间缓冲区中。\n第二步：将数据从内核缓冲区复制到用户缓冲区，read 系统调用返回。调用的返回导致了从内核模式返回到用户模式的上下文切换，现在，数据存储在用户地址空间缓冲区中，它可以再次开始向下移动。\n第三步：write 系统调用导致从用户模式到内核模式的上下文切换，执行第三次复制，将数据再次放入内核地址空间缓冲区中。但是这一次，数据被放入一个不同的缓冲区，这个缓冲区是与套接字相关联的。\n第四步：写系统调用返回，产生第四次上下文切换。并将数据写入网络 I/O 中，网络传输中的服务端的操作逻辑到此结束。\n从上图中我们知道，整个网络传输过程中数据被复制了多达 4 次，也进行了多次从用户态到内核态的切换。那么有没有可能减少数据的复制次数，提高网络 I/O 的效率呢？答案是肯定的。\n那么到底什么是零拷贝呢？就是将数据直接从内核态的缓冲区中拷贝到 Socket 的缓冲区中，没有经过用户态的缓冲区，之所以被叫做零拷贝是相对于用户态来说的。如下图所示：\n总的来说，从操作系统的角度来看是零拷贝，因为数据不是在内核缓冲区之间复制的。当使用零拷贝时，除了避免复制之外，还有其他性能优势，例如更少的上下文切换、更少的 CPU 数据缓存污染和没有 CPU 校验和计算。\n零拷贝的 Java 实现 # NIO 中的 FileChannel 拥有 transferTo 和 transferFrom 两个方法，可直接把 FileChannel 中的数据拷贝到另外一个 Channel，或直接把另外一个 Channel 中的数据拷贝到 FileChannel。该接口常被用于网络/文件数据的高效传输和大文件拷贝。在操作系统支持的情况下，通过该方法传输数据并不需要将源数据从内核态拷贝到用户态，再从用户态拷贝到目标通道的内核态，同时也避免了两次用户态和内核态间的上下文切换，也即使用了“零拷贝”。\n/** * disk-nic零拷贝 */ class ZeroCopyServer { ServerSocketChannel listener = null; public static void main(String[] args) { ZerocopyServer dns = new ZerocopyServer(); dns.mySetup(); dns.readData(); } protected void mySetup() { InetSocketAddress listenAddr = new InetSocketAddress(9026); try { listener = ServerSocketChannel.open(); ServerSocket ss = listener.socket(); ss.setReuseAddress(true); ss.bind(listenAddr); System.out.println(\u0026#34;监听的端口:\u0026#34; + listenAddr.toString()); } catch (IOException e) { System.out.println(\u0026#34;端口绑定失败 : \u0026#34; + listenAddr.toString() + \u0026#34; 端口可能已经被使用,出错原因: \u0026#34; + e.getMessage()); e.printStackTrace(); } } private void readData() { ByteBuffer dst = ByteBuffer.allocate(4096); try { while (true) { SocketChannel conn = listener.accept(); System.out.println(\u0026#34;创建的连接: \u0026#34; + conn); conn.configureBlocking(true); int nread = 0; while (nread != -1) { try { nread = conn.read(dst); } catch (IOException e) { e.printStackTrace(); nread = -1; } dst.rewind(); } } } catch (IOException e) { e.printStackTrace(); } } } 说点题外话 # 对于 I/O 操作的优化也可以参考零拷贝的思路来对我们的系统进行优化，最近了解到 Kafka 之所以能够承载高吞吐量跟它强依赖底层操作系统的 page cache 有很大关系，所以在使用 Kafka 时，并不是 jvm 的内存越大越好，跟零拷贝减少数据在内核态与用户态之间的拷贝、上下文切换有异曲同工之妙，对 Kafka 还不甚了解不敢多说了……\nKafka 官网看到的 # 为了弥补这种性能差异，现代操作系统越来越注重使用内存对磁盘进行 cache。现代操作系统主动将所有空闲内存用作 disk caching，代价是在内存回收时性能会有所降低。所有对磁盘的读写操作都会通过这个统一的 cache。如果不使用直接 I/O，该功能不能轻易关闭。因此即使进程维护了 in-process cache，该数据也可能会被复制到操作系统的 pagecache 中，事实上所有内容都被存储了两份。\n此外，Kafka 建立在 JVM 之上，任何了解 Java 内存使用的人都知道两点：\n对象的内存开销非常高，通常是所存储的数据的两倍(甚至更多)。 随着堆中数据的增加，Java 的垃圾回收变得越来越复杂和缓慢。 受这些因素影响，相比于维护 in-memory cache 或者其他结构，使用文件系统和 pagecache 显得更有优势\u0026ndash;我们可以通过自动访问所有空闲内存将可用缓存的容量至少翻倍，并且通过存储紧凑的字节结构而不是独立的对象，有望将缓存容量再翻一番。这样使得 32GB 的机器缓存容量可以达到 28-30GB,并且不会产生额外的 GC 负担。此外，即使服务重新启动，缓存依旧可用，而 in-process cache 则需要在内存中重建(重建一个 10GB 的缓存可能需要 10分钟)，否则进程就要从 cold cache 的状态开始(这意味着进程最初的性能表现十分糟糕)。这同时也极大地简化了代码，因为所有保持 cache 和文件系统之间一致性的逻辑现在都被放到了 OS 中，这样做比一次性的进程内缓存更准确、更高效。如果你的磁盘使用更倾向于顺序读取，那么 read-ahead 可以有效地使用每次从磁盘中读取到的有用数据预先填充 cache。\n这里给出了一个非常简单的设计：相比于维护尽可能多的 in-memory cache，并且在空间不足的时候匆忙将数据 flush 到文件系统，我们把这个过程倒过来。所有数据一开始就被写入到文件系统的持久化日志中，而不用在 cache 空间不足的时候 flush 到磁盘。实际上，这表明数据被转移到了内核的 pagecache 中。\n关于文中多次出现的用户态，内核态 # 如上图所示，从宏观上来看，操作系统的体系架构分为用户态和内核态。内核从本质上看是一种软件——控制计算机的硬件资源，并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间，应用程序的执行必须依托于内核提供的资源，包括 CPU 资源、存储资源、I/O 资源等。为了使上层应用能够访问到这些资源，内核必须为上层应用提供访问的接口：即系统调用。\n参考链接 # 维基百科-零拷贝 Linux 零拷贝原理\n关注我们 # ","date":"2019-11-09","externalUrl":null,"permalink":"/posts/wolijiedelingkaobei/","section":"文章","summary":"最近做的业务涉及到的 I/O 操作比较多，对于 Linux 上的 I/O 操作的优化 Zero Copy 早有耳闻，今天打算由上而下（从应用层到底层，当然并不会涉及到内核的细节）的研究一下这个问题。为了更好的描述 zero copy，本文将以网络服务器的简单过程所涉及的内容展开…","title":"我理解的零拷贝","type":"posts"},{"content":"","date":"2019-11-09","externalUrl":null,"permalink":"/categories/%E9%98%85%E8%AF%BB/","section":"分类","summary":"","title":"阅读","type":"categories"},{"content":"📌 本文原发布于掘金社区：Redis 与 Lua 使用中的小问题\n问题 # 在 Redis 里执行 get 或 hget 不存在的 key 或 field 时返回值在终端显示的是 (nil)，类似于下面这样\n127.0.0.1:6379\u0026gt; get test_version (nil) 如果在 Lua 脚本中判断获取到的值是否为空值时，就会产生比较令人迷惑的问题，以为判断空值的话就用 nil 就可以了，然鹅事实却并不是这样的，如下所示：\n127.0.0.1:6379\u0026gt; get test_version (nil) 127.0.0.1:6379\u0026gt; EVAL \u0026#34;local a = redis.call(\u0026#39;get\u0026#39;,KEYS[1]) print(a) if a == \u0026#39;nil\u0026#39; then return 1 else return 0 end\u0026#34; 1 test_version test_version (integer) 0 我们来看下执行 Lua 脚本返回结果的数据类型是什么\n127.0.0.1:6379\u0026gt; get test_version (nil) 127.0.0.1:6379\u0026gt; EVAL \u0026#34;local a = redis.call(\u0026#39;get\u0026#39;,KEYS[1]) return type(a)\u0026#34; 1 test_version test_version \u0026#34;boolean\u0026#34; 从上面的脚本可以看出，当 Redis 返回的结果为 (nil) 时，其真实的数据类型为 boolean，因此我们直接判断 nil 是有问题的。\nRedis 官方文档 # 通过翻阅官方文档，找到下面所示的一段话，\nRedis to Lua conversion table.\nRedis integer reply -\u0026gt; Lua number Redis bulk reply -\u0026gt; Lua string Redis multi bulk reply -\u0026gt; Lua table (may have other Redis data types nested) Redis status reply -\u0026gt; Lua table with a single ok field containing the status Redis error reply -\u0026gt; Lua table with a single err field containing the error Redis Nil bulk reply and Nil multi bulk reply -\u0026gt; Lua false boolean type Lua to Redis conversion table.\nLua number -\u0026gt; Redis integer reply (the number is converted into an integer) Lua string -\u0026gt; Redis bulk reply Lua table (array) -\u0026gt; Redis multi bulk reply (truncated to the first nil inside the Lua array if any) Lua table with a single ok field -\u0026gt; Redis status reply Lua table with a single err field -\u0026gt; Redis error reply Lua boolean false -\u0026gt; Redis Nil bulk reply. 解决方案 # 通过官方文档，我们知道判断 Lua 脚本返回空值时，应该直接判断 true/false，修改判断脚本如下所示\n127.0.0.1:6379\u0026gt; get test_version (nil) 127.0.0.1:6379\u0026gt; EVAL \u0026#34;local a = redis.call(\u0026#39;get\u0026#39;,KEYS[1]) if a == false then return \u0026#39;empty\u0026#39; else return \u0026#39;not empty\u0026#39; end\u0026#34; 1 test_version test_version \u0026#34;empty\u0026#34; 关注我们 # ","date":"2019-11-08","externalUrl":null,"permalink":"/posts/redis-yu-lua-shiyongzhongdexiaowenti/","section":"文章","summary":"通过上面的脚本可以看到，当 Redis 返回的结果为 (nil) 时候，其真实的数据类型为 boolean，因此我们直接判断 nil 是有问题的。Redis to Lua conversion table. Lua to Redis conversion table. Lua…","title":"Redis 与 Lua 使用中的小问题","type":"posts"},{"content":"","date":"2019-09-01","externalUrl":null,"permalink":"/tags/leetcode/","section":"标签","summary":"","title":"LeetCode","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：二分查找算法细节详解\n思路 # 我相信对很多读者朋友来说，编写二分查找的算法代码属于玄学编程，虽然看起来很简单，就是会出错，要么会漏个等号，要么少加个 1。\n不要气馁，因为二分查找其实并不简单。看看 Knuth 大佬（发明 KMP 算法的那位）怎么说的：\nAlthough the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky... 这句话可以这样理解：思路很简单，细节是魔鬼。\n本文以问答的形式，探究几个最常用的二分查找场景：寻找一个数、寻找左侧边界、寻找右侧边界。第一个场景是最简单的算法形式，解决这道题，后两个场景就是本题。\n而且，我们就是要深入细节，比如不等号是否应该带等号，mid 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因，保证你能灵活准确地写出正确的二分查找算法。\n零、二分查找框架 # int binarySearch(int[] nums, int target) { int left = 0, right = ...; while(...) { int mid = (right + left) / 2; if (nums[mid] == target) { ... } else if (nums[mid] \u0026lt; target) { left = ... } else if (nums[mid] \u0026gt; target) { right = ... } } return ...; } 分析二分查找的一个技巧是：不要出现 else，而是把所有情况用 else if 写清楚，这样可以清楚地展现所有细节。本文都会使用 else if，旨在讲清楚，读者理解后可自行简化。\n其中 …标记的部分，就是可能出现细节问题的地方，当你见到一个二分查找的代码时，首先注意这几个地方。后文用实例分析这些地方能有什么样的变化。\n另外声明一下，计算 mid 时需要技巧防止溢出，即 mid=left+(right-left)/2。本文暂时忽略这个问题。\n一、寻找一个数（基本的二分搜索） # 这个场景是最简单的，可能也是大家最熟悉的，即搜索一个数，如果存在，返回其索引，否则返回 -1。\nint binarySearch(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 注意 while(left \u0026lt;= right) { int mid = (right + left) / 2; if(nums[mid] == target) return mid; else if (nums[mid] \u0026lt; target) left = mid + 1; // 注意 else if (nums[mid] \u0026gt; target) right = mid - 1; // 注意 } return -1; } 为什么 while 循环的条件中是 \u0026lt;=，而不是 \u0026lt;？ 答：因为初始化 right 的赋值是 nums.length-1，即最后一个元素的索引，而不是 nums.length。\n这二者可能出现在不同功能的二分查找中，区别是：前者相当于两端都闭区间 [left, right]，后者相当于左闭右开区间 [left, right)，因为索引大小为 nums.length 是越界的。\n我们这个算法中使用的是前者 [left, right] 两端都闭的区间。这个区间其实就是每次搜索的区间，我们不妨称为「搜索区间」。\n什么时候应该停止搜索呢？当然，找到了目标值的时候可以终止：\nif(nums[mid] == target) return mid; 但如果没找到，就需要 while 循环终止，然后返回 -1。那 while 循环什么时候应该终止？搜索区间为空的时候应该终止，意味着你没得找了，就等于没找到嘛。\nwhile(left \u0026lt;= right) 的终止条件是 left == right + 1，写成区间的形式就是 [right + 1, right]，或者带个具体的数字进去 [3, 2]，可见这时候搜索区间为空，因为没有数字既大于等于 3 又小于等于\n2的吧。所以这时候 while 循环终止是正确的，直接返回 -1 即可。\nwhile(left \u0026lt; right) 的终止条件是 left == right，写成区间的形式就是 [left, right]，或者带个具体的数字进去 [2, 2]，这时候搜索区间非空，还有一个数 2，但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了，索引\n2 没有被搜索，如果这时候直接返回 -1 就是错误的。\n当然，如果你非要用 while(left \u0026lt; right)也可以，我们已经知道了出错的原因，就打个补丁好了：\n//... while(left \u0026lt; right) { // ... } return nums[left] == target ? left : -1; 为什么 left = mid + 1，right = mid - 1？我看有的代码是 right = mid 或者 left = mid，没有这些加加减减，到底怎么回事，怎么判断？ 答：这也是二分查找的一个难点，不过只要你能理解前面的内容，就能够很容易判断。\n刚才明确了「搜索区间」这个概念，而且本算法的搜索区间是两端都闭的，即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时，如何确定下一步的搜索区间呢？\n当然是 [left, mid - 1] 或者 [mid + 1, right] 对不对？因为 mid 已经搜索过，应该从搜索区间中去除。\n此算法有什么缺陷？ 答：至此，你应该已经掌握了该算法的所有细节，以及这样处理的原因。但是，这个算法存在局限性。\n比如说给你有序数组 nums = [1,2,2,2,3]，target = 2，此算法返回的索引是 2，没错。但是如果我想得到 target 的左侧边界，即索引 1，或者我想得到 target 的右侧边界，即索引 3，这样的话此算法是无法处理的。\n这样的需求很常见。你也许会说，找到一个 target，然后向左或向右线性搜索不行吗？可以，但是不好，因为这样难以保证二分查找对数级的复杂度了。\n我们后续就来讨论这两种二分查找的算法。\n二、寻找左侧边界的二分搜索 # 直接看代码，其中的标记是需要注意的细节：\nint left_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0; int right = nums.length; // 注意 while (left \u0026lt; right) { // 注意 int mid = (left + right) / 2; if (nums[mid] == target) { right = mid; } else if (nums[mid] \u0026lt; target) { left = mid + 1; } else if (nums[mid] \u0026gt; target) { right = mid; // 注意 } } return left; } 为什么 while(left \u0026lt; right) 而不是 \u0026lt;= ? 答：用相同的方法分析，因为 right = nums.length 而不是 nums.length - 1。因此每次循环的「搜索区间」是 [left, right) 左闭右开。\nwhile(left \u0026lt; right) 终止的条件是 left == right，此时搜索区间 [left, left) 为空，所以可以正确终止。\n为什么没有返回 -1 的操作？如果 nums 中不存在 target 这个值，怎么办？ 答：因为要一步一步来，先理解一下这个「左侧边界」有什么特殊含义：\\\n📷 图注：binary-search\n对于这个数组，算法会返回 1。这个 1 的含义可以这样解读：nums 中小于 2 的元素有 1 个。\n比如对于有序数组 nums = \\[2,3,5,7\\], target = 1，算法会返回 0，含义是：nums 中小于1 的元素有0个。\n再比如说 nums 不变，target = 8，算法会返回 4，含义是：nums 中小于 8 的元素有4 个。\n综上可以看出，函数的返回值（即 left 变量的值）取值区间是闭区间 [0, nums.length]，所以我们简单添加两行代码就能在正确的时候 return -1：\\\nwhile (left \u0026lt; right) { //... } // target 比所有数都大 if (left == nums.length) return -1; // 类似之前算法的处理方式 return nums[left] == target ? left : -1; 为什么 left = mid + 1，right = mid？和之前的算法不一样？ 答：这个很好解释，因为我们的「搜索区间」是 [left, right) 左闭右开，所以当 nums[mid] 被检测之后，下一步的搜索区间应该去掉 mid 分割成两个区间，即 [left, mid) 或 [mid + 1, right)。\n为什么该算法能够搜索左侧边界？ 答：关键在于对于 nums[mid] == target 这种情况的处理：\nif (nums[mid] == target) right = mid; 可见，找到 target 时不要立即返回，而是缩小「搜索区间」的上界 right，在区间 [left, mid)中继续搜索，即不断向左收缩，达到锁定左侧边界的目的。\n为什么返回 left 而不是 right？ 答：都是一样的，因为 while 终止的条件是 left == right。\n三、寻找右侧边界的二分查找 # 寻找右侧边界和寻找左侧边界的代码差不多，只有两处不同，已标注：\nint right_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0, right = nums.length; while (left \u0026lt; right) { int mid = (left + right) / 2; if (nums[mid] == target) { left = mid + 1; // 注意 } else if (nums[mid] \u0026lt; target) { left = mid + 1; } else if (nums[mid] \u0026gt; target) { right = mid; } } return left - 1; // 注意 } 为什么这个算法能够找到右侧边界？ 答：类似地，关键点还是这里：\nif (nums[mid] == target) { left = mid + 1; 当 nums[mid] == target 时，不要立即返回，而是增大「搜索区间」的下界 left，使得区间不断向右收缩，达到锁定右侧边界的目的。\n为什么最后返回 left - 1 而不像左侧边界的函数，返回 left？而且我觉得这里既然是搜索右侧边界，应该返回 right 才对。 答：首先，while 循环的终止条件是 left == right，所以 left 和 right 是一样的，你非要体现右侧的特点，返回 right - 1 好了。\n至于为什么要减一，这是搜索右侧边界的一个特殊点，关键在这个条件判断：\nif (nums[mid] == target) { left = mid + 1; // 这样想: mid = left - 1 因为我们对 left 的更新必须是 left = mid + 1，就是说 while 循环结束时，nums[left] 一定不等于 target 了，而 nums[left-1] 可能是 target。\n至于为什么 left 的更新必须是 left = mid + 1，同左侧边界搜索，就不再赘述。\n为什么没有返回 −1\n−1 的操作？如果 nums 中不存在 target 这个值，怎么办？ 答：类似之前的左侧边界搜索，因为 while 的终止条件是 left == right，就是说 left 的取值范围是 [0, nums.length]，所以可以添加两行代码，正确地返回 −1\nwhile (left \u0026lt; right) { // ... } if (left == 0) return -1; return nums[left-1] == target ? (left-1) : -1; 四、最后总结 # 来梳理一下这些细节差异的因果逻辑：\n第一个，最基本的二分查找算法：\n因为我们初始化 right = nums.length - 1 所以决定了我们的「搜索区间」是 [left, right] 所以决定了 while (left \u0026lt;= right) 同时也决定了 left = mid+1 和 right = mid-1 因为我们只需找到一个 target 的索引即可 所以当 nums[mid] == target 时可以立即返回 第二个，寻找左侧边界的二分查找：\\\n因为我们初始化 right = nums.length 所以决定了我们的「搜索区间」是 [left, right) 所以决定了 while (left \u0026lt; right) 同时也决定了 left = mid + 1 和 right = mid 因为我们需找到 target 的最左侧索引 所以当 nums[mid] == target 时不要立即返回 而要收紧右侧边界以锁定左侧边界 第三个，寻找右侧边界的二分查找：\\\n因为我们初始化 right = nums.length 所以决定了我们的「搜索区间」是 [left, right) 所以决定了 while (left \u0026lt; right) 同时也决定了 left = mid + 1 和 right = mid 因为我们需找到 target 的最右侧索引 所以当 nums[mid] == target 时不要立即返回 而要收紧左侧边界以锁定右侧边界 又因为收紧左侧边界时必须 left = mid + 1 所以最后无论返回 left 还是 right，必须减一 如果以上内容你都能理解，那么恭喜你，二分查找算法的细节不过如此。 通过本文，你学会了：\n分析二分查找代码时，不要出现 else，全部展开成 else if 方便理解。\n注意「搜索区间」和 while 的终止条件，如果存在漏掉的元素，记得在最后检查。\n如需要搜索左右边界，只要在 nums[mid] == target 时做修改即可。搜索右侧时需要减一。\n以后就算遇到其他的二分查找变形，运用这几点技巧，也能保证你写出正确的代码。\n出处 # 出处\n","date":"2019-09-01","externalUrl":null,"permalink":"/posts/er-fen-cha-zhao-suan-fa-xi-jie-xiang-jie/","section":"文章","summary":"思路 我相信对很多读者朋友来说，编写二分查找的算法代码属于玄学编程，虽然看起来很简单，就是会出错，要么会漏个等号，要么少加个 1。不要气馁，因为二分查找其实并不简单。看看 Knuth 大佬（发明 KMP 算法的那位）怎么说的：Altho","title":"二分查找算法细节详解","type":"posts"},{"content":"📌 本文原发布于掘金社区：lang3 的 split 方法误用\nlang3 的 split 方法误用-原文链接\napache 的 lang3 是我们开发中常用到的三方工具包，然而对这个包不甚了解的话，会产生莫名其妙的 bug，在这里做下记录。\n误用示例 # public class TestDemo { @Test public void test() throws IOException { String sendMsg = \u0026#34;{\u0026#34;expiredTime\u0026#34;:\u0026#34;20190726135831\u0026#34;,\u0026#34;drives\u0026#34;:\u0026#34;androidgetui\u0026#34;,\u0026#34;msgBody\u0026#34;:\u0026#34;{\\\\\u0026#34;serialNumber\\\\\u0026#34;:\\\\\u0026#34;wow22019072611349502\\\\\u0026#34;,\\\\\u0026#34;push_key\\\\\u0026#34;:\\\\\u0026#34;appactive#549110277\\\\\u0026#34;,\\\\\u0026#34;title\\\\\u0026#34;:\\\\\u0026#34;\\\\xe6\\\\x9c\\\\x89\\\\xe4\\\\xba\\\\xba@\\\\xe4\\\\xbd\\\\xa0\\\\\u0026#34;,\\\\\u0026#34;message\\\\\u0026#34;:\\\\\u0026#34;\\\\xe4\\\\xbb\\\\x8a\\\\xe5\\\\xa4\\\\xa9\\\\xe5\\\\x87\\\\xa0\\\\xe7\\\\x82\\\\xb9\\\\xe5\\\\x87\\\\xba\\\\xe5\\\\x8f\\\\x91\\\\xef\\\\xbc\\\\x9f\\\\\u0026#34;,\\\\\u0026#34;link\\\\\u0026#34;:\\\\\u0026#34;chelaile://homeTab/home?select=3\\\\\u0026#34;,\\\\\u0026#34;open_type\\\\\u0026#34;:0,\\\\\u0026#34;expireDays\\\\\u0026#34;:\\\\\u0026#34;30\\\\\u0026#34;,\\\\\u0026#34;type\\\\\u0026#34;:14}\u0026#34;,\u0026#34;clients\u0026#34;:[\u0026#34;13065ffa4e25c4a7c68\u0026#34;]}CHELAILE_PUSH{\u0026#34;cityId\u0026#34;:\u0026#34;007\u0026#34;,\u0026#34;gpsTime\u0026#34;:\u0026#34;2019-07-24 21:33:06\u0026#34;,\u0026#34;lat\u0026#34;:\u0026#34;30.605916\u0026#34;,\u0026#34;lng\u0026#34;:\u0026#34;103.980439\u0026#34;,\u0026#34;s\u0026#34;:\u0026#34;android\u0026#34;,\u0026#34;sourceUdid\u0026#34;:\u0026#34;a4419b93-fb0e-43c7-98fa-5b7c18255660\u0026#34;,\u0026#34;token\u0026#34;:\u0026#34;13065ffa4e25c4a7c68\u0026#34;,\u0026#34;tokenType\u0026#34;:\u0026#34;3\u0026#34;,\u0026#34;udid\u0026#34;:\u0026#34;UDID2TOKEN#a4419b93-fb0e-43c7-98fa-5b7c18255660\u0026#34;,\u0026#34;userCreateTime\u0026#34;:\u0026#34;2018-04-20 08:13:32\u0026#34;,\u0026#34;userLastActiveTime\u0026#34;:\u0026#34;2019-07-24 21:33:06\u0026#34;,\u0026#34;vc\u0026#34;:\u0026#34;150\u0026#34;}\u0026#34;; String[] dataArr = StringUtils.split(sendMsg,\u0026#34;CHELAILE_PUSH\u0026#34;); Assert.assertEquals(dataArr.length,2); } } 分析原因 # 通过分析字符串的拆分结果，发现该方法并不是将分隔符去截取字符串，而是将分隔符的每一个字符都当成分隔符去截取字符串，当我们的分隔符是一个字符的时候一般不会出现上面示例中出现的问题，如果分隔符是多个字符的时候这个问题就显现出来了。\n查看 StringUtils 源码 # /** * * \u0026lt;pre\u0026gt; * StringUtils.split(null, *) = null * StringUtils.split(\u0026#34;\u0026#34;, *) = [] * StringUtils.split(\u0026#34;abc def\u0026#34;, null) = [\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;] * StringUtils.split(\u0026#34;abc def\u0026#34;, \u0026#34; \u0026#34;) = [\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;] * StringUtils.split(\u0026#34;abc def\u0026#34;, \u0026#34; \u0026#34;) = [\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;] * StringUtils.split(\u0026#34;ab:cd:ef\u0026#34;, \u0026#34;:\u0026#34;) = [\u0026#34;ab\u0026#34;, \u0026#34;cd\u0026#34;, \u0026#34;ef\u0026#34;] * \u0026lt;/pre\u0026gt; * * @param str 要解析的字符串，可能为空 * @param separatorChars 用做分割字符的字符们（注意是字符串们哦！），当 separatorChars 传入的值为空的时候则用空格来做分隔符 */ public static String[] split(final String str, final String separatorChars) { return splitWorker(str, separatorChars, -1, false); } /** * Performs the logic for the {@code split} and * {@code splitPreserveAllTokens} methods that return a maximum array * length. * * @param str the String to parse, may be {@code null} * @param separatorChars the separate character * @param max the maximum number of elements to include in the * array. A zero or negative value implies no limit. * @param preserveAllTokens if {@code true}, adjacent separators are * treated as empty token separators; if {@code false}, adjacent * separators are treated as one separator. * @return an array of parsed Strings, {@code null} if null String input */ private static String[] splitWorker(final String str, final String separatorChars, final int max, final boolean preserveAllTokens) { // Performance tuned for 2.0 (JDK1.4) // Direct code is quicker than StringTokenizer. // Also, StringTokenizer uses isSpace() not isWhitespace() if (str == null) { return null; } final int len = str.length(); if (len == 0) { return ArrayUtils.EMPTY_STRING_ARRAY; } final List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); int sizePlus1 = 1; int i = 0, start = 0; boolean match = false; boolean lastMatch = false; if (separatorChars == null) { // 用空格作为分隔符切割字符串 while (i \u0026lt; len) { if (Character.isWhitespace(str.charAt(i))) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } list.add(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } else if (separatorChars.length() == 1) { // 分隔符的字符数为 1 的时候，切割字符串的逻辑 final char sep = separatorChars.charAt(0); while (i \u0026lt; len) { if (str.charAt(i) == sep) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } list.add(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } else { // 当分隔符的字符数为多个的时候，分割字符串的逻辑 // 示例：分隔字符串 abc，分割字符串的分隔符可以是 a,ab,abc while (i \u0026lt; len) { if (separatorChars.indexOf(str.charAt(i)) \u0026gt;= 0) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } list.add(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } if (match || preserveAllTokens \u0026amp;\u0026amp; lastMatch) { list.add(str.substring(start, i)); } return list.toArray(new String[list.size()]); } 小结 # 平时只知道调用 API，在使用三方包的时候，没有认真查看 API 文档，对于三方包的方法，使用上处于想当然的状态，这里应该做好反省。\n","date":"2019-08-07","externalUrl":null,"permalink":"/posts/lang3-de-split-fangfawuyong/","section":"文章","summary":"apache 的 lang3 是我们开发常用到的三方工具包，然而对这个包不甚了解的话，会产生莫名其秒的 bug，在这里做下记录。通过分析字符串的拆分结果，发现该方法并不是将分隔符去截取字符串，而是将分隔符的每一个字符都当成分隔符去截取字符串，当我们的分隔符是一个字符的时候一…","title":"lang3 的 split 方法误用","type":"posts"},{"content":"📌 本文原发布于掘金社区：实现自己的 RPC 框架（二）\n前段时间自己搞了个 RPC 的轮子，不过相对来说比较简单，最近在原来的基础上加以改造，使用 ZooKeeper 实现了 provider 自动寻址以及消费者的简单负载均衡，对之前的文章感兴趣的请转 造个轮子\u0026mdash;RPC 动手实现。\nRPC 模型 # 在原来使用 TCP 直连的基础上实现基于 ZooKeeper 的服务的注册与发现，改造后的依赖关系是这样的。\n怎么用 # 话不多说，我们来看下如何发布和引用服务。服务端我们将服务的 IP 和端口号等基础信息注册到 ZooKeeper 上。\n/** * @author wuhaifei 2019-08-02 */ public class ZookeeperServerMainTest { public static void main(String[] args) { ServerConfig serverConfig = new ServerConfig(); serverConfig.setSerializer(AbstractSerializer.SerializeEnum.HESSIAN.serializer) .setHost(\u0026#34;172.16.30.114\u0026#34;) .setPort(5201) .setRef(HelloServiceImpl.class.getName()) .setRegister(true) .setInterfaceId(HelloService.class.getName()); RegistryConfig registryConfig = new RegistryConfig().setAddress(\u0026#34;127.0.0.1:2181\u0026#34;) .setSubscribe(true) .setRegister(true) .setProtocol(RpcConstants.ZOOKEEPER); ServerProxy serverProxy = new ServerProxy(new NettyServerAbstract()) .setServerConfig(serverConfig) .setRegistryConfig(registryConfig); try { serverProxy.export(); while (true){ } } catch (Exception e) { e.printStackTrace(); } } } 通过 ZooKeeper 引用注册在其上的服务。\n/** * @author wuhaifei 2019-08-02 */ public class ZookeeperClientMainTest { public static void main(String[] args) { ClientConfig clientConfig = new ClientConfig(); clientConfig.setProtocol(RpcConstants.ZOOKEEPER) .setTimeoutMillis(100000) .setSerializer(AbstractSerializer.SerializeEnum.HESSIAN.serializer); RegistryConfig registryConfig = new RegistryConfig() .setAddress(\u0026#34;127.0.0.1:2181\u0026#34;) .setProtocol(RpcConstants.ZOOKEEPER) .setRegister(true) .setSubscribe(true); ClientProxy\u0026lt;HelloService\u0026gt; clientProxy = new ClientProxy(clientConfig, new NettyClientAbstract(), HelloService.class) .setRegistryConfig(registryConfig); for (int i = 0; i \u0026lt; 10; i++) { HelloService helloService = clientProxy.refer(); System.out.println(helloService.sayHi()); } } } 运行结果就不一一贴出了，感兴趣的小伙伴可以查看楼主传到 github 上的源码这是一个 rpc 的轮子。\n服务的发布与订阅 # 楼主在原来代码的基础上添加了 ZooKeeper 的注册逻辑，原来代码的相关介绍请转 造个轮子\u0026mdash;RPC 动手实现。\n服务的发布 # /** * 发布服务 */ public void export() { try { Object serviceBean = Class.forName((String) serverConfig.getRef()).newInstance(); RpcInvokerHandler.serviceMap.put(serverConfig.getInterfaceId(), serviceBean); this.childServer.start(this.getServerConfig()); if (serverConfig.isRegister()) { // 将服务注册到zookeeper register(); } } catch (Exception e) { // 取消服务注册 unregister(); if (e instanceof ChildRpcRuntimeException) { throw (ChildRpcRuntimeException) e; } else { throw new ChildRpcRuntimeException(\u0026#34;Build provider proxy error!\u0026#34;, e); } } exported = true; } /** * 注册服务 */ protected void register() { if (serverConfig.isRegister()) { Registry registry = RegistryFactory.getRegistry(this.getRegistryConfig()); registry.init(); registry.start(); try { registry.register(this.serverConfig); } catch (ChildRpcRuntimeException e) { throw e; } catch (Throwable e) { String appName = serverConfig.getInterfaceId(); LOGGER.info(appName, \u0026#34;Catch exception when register to registry: \u0026#34; + registryConfig.getId(), e); } } } 服务的订阅 # /** * 服务的引用. */ public T refer() { try { if (config.isSubscribe()) { subscribe(); } childClient.init(this.clientConfig); return invoke(); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 订阅zk的服务列表. */ private void subscribe() { Registry registry = RegistryFactory.getRegistry(this.getRegistryConfig()); registry.init(); registry.start(); this.clientConfig = (ClientConfig) config; List\u0026lt;String\u0026gt; providerList = registry.subscribe(this.clientConfig); if (null == providerList) { throw new ChildRpcRuntimeException(\u0026#34;无可用服务供订阅！\u0026#34;); } // 使用随机算法，随机选择一个provider int index = ThreadLocalRandom.current().nextInt(providerList.size()); String providerInfo = providerList.get(index); String[] providerArr = providerInfo.split(\u0026#34;:\u0026#34;); clientConfig = (ClientConfig) this.config; clientConfig.setHost(providerArr[0]); clientConfig.setPort(Integer.parseInt(providerArr[1])); } 上面代码比较简单，就是在原来直连的基础上添加 zk 的操作，在发布服务的时候将 provider 的 IP 和端口号等基础信息注册到 zk 上，在引用服务的时候使用随机算法从 zk 上选取可用的 provider 信息，然后进行 invoke 调用。\n小结 # RPC（Remote procedure call）底层逻辑相对来说比较简单，楼主在实现的过程中参考了其他 RPC 框架的部分代码，受益匪浅~\n","date":"2019-08-06","externalUrl":null,"permalink":"/posts/shixianzijide-rpc-kuangjia-er/","section":"文章","summary":"前段时间自己搞了个 RPC 的轮子，不过相对来说比较简单，最近在原来的基础上加以改造，使用 ZooKeeper 实现了 provider 自动寻址以及消费者的简单负载均衡，对之前的感兴趣的请转 造个轮子—RPC 动手实现。在原来使用 TCP 直连的基础上实现基于 Zooke…","title":"实现自己的 RPC 框架（二）","type":"posts"},{"content":"📌 本文原发布于代码星冰乐：[译] 为什么 String 在 Java 中是不可变的\nString 在 Java 中是不可变的。不可变类只是一个无法修改其实例的类。创建实例时，将初始化实例中的所有信息，并且无法修改信息。不可变类有许多优点。本文总结了为什么 String 设计为不可变的。这篇文章从内存，同步和数据结构的角度说明了不变性概念。\n1. 字符串池 # 字符串池（String intern pool）是方法区中的特殊存储区域。当创建字符串并且池中已存在该字符串时，将返回现有字符串的引用，而不是创建新对象。\n以下代码将在堆中仅创建一个字符串对象。\nString string1 = \u0026quot;abcd\u0026quot;; String string2 = \u0026quot;abcd\u0026quot;; 如下图所示：\n📷 图注：字符串池\n如果字符串是可变的，则使用一个引用更改字符串将导致其他引用出错。\n2. 缓存的哈希码 # 字符串的哈希码经常在 Java 中使用。例如，在 HashMap 或 HashSet 中。不可变性保证哈希码总是相同的，这样它就可以被缓存起来而不用担心变化。这意味着，每次使用时都不需要计算哈希码。这更有效率。\n在 String 类中，它具有如下代码：\nprivate int hash;//this is used to cache hash code. 3. 其他对象中的字符串 # 具体来说，请参考以下程序：\nHashSet\u0026lt;String\u0026gt; set = new HashSet\u0026lt;String\u0026gt;(); set.add(new String(\u0026quot;a\u0026quot;)); set.add(new String(\u0026quot;b\u0026quot;)); set.add(new String(\u0026quot;c\u0026quot;)); for(String a: set) a.value = \u0026quot;a\u0026quot;; 在此示例中，如果 String 是可变的，则可以更改其值，这将违反 set 的设计（set 包含非重复元素）。当然，上面的示例仅用于演示目的，并且实际字符串类中没有值字段。\n4. 安全 # String 被广泛用作许多 Java 类的参数，例如 网络连接，打开文件等。如果字符串不是不可变的，连接或文件将被更改，这可能会导致严重的安全威胁。该方法认为它连接到一台机器，但事实并非如此。可变字符串也可能在 Reflection 中引起安全问题，因为参数是字符串。\n如下例子：\nboolean connect(string s){ if (!isSecure(s)) { throw new SecurityException(); } //here will cause problem, if s is changed before this by using other references. causeProblem(s); } 5. 不可变保证了线程安全 # 由于无法更改不可变对象，因此可以在多个线程之间自由共享它们。这消除了同步的要求。\n综上所述，出于效率和安全原因，String 被设计为不可变的，这也是在一般情况下优选不可变类的原因。\n","date":"2019-07-08","externalUrl":null,"permalink":"/posts/yi---wei-shen-me-string-zai-java-zhong-shi-bu-ke-bian-de/","section":"文章","summary":"String 在 Java 中是不可变的。不可变类只是一个无法修改其实例的类。创建实例时，将初始化实例中的所有信息，并且无法修改信息。不可变类有许多优点。本文总结了为什么 String 设计为不可变的。这篇文章从内存，同步和数据结","title":"[译] 为什么 String 在 Java 中是不可变的","type":"posts"},{"content":"","date":"2019-07-08","externalUrl":null,"permalink":"/tags/jvm/","section":"标签","summary":"","title":"JVM","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：LRU 算法\nWhat？ # LRU 是 Least Recently Used 的缩写，即最近最少使用，是一种常用的页面置换算法，选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段，用来记录一个页面自上次被访问以来所经历的时间 t，当须淘汰一个页面时，选择现有页面中其 t 值最大的，即最近最久未使用的页面予以淘汰。-百科\n上面是对操作系统中 LRU 算法的阐述，本文说的 LRU 主要是指该算法在业务层缓存算法中的应用，总而言之，基本的实现逻辑是一样的。\nHow？ # 算法思想：\n1，新数据插入到链表头部。 2，每当缓存命中（即缓存数据被访问），则将数据移到链表头部。 3，当链表满的时候，将链表尾部的数据丢弃。 算法实现 # class Node { public Node(String key,String value) { this.key = key; this.value = value; } public Node pre; public Node next; public String key; public String value; } public class LRUCache { private Node head; private Node end; // 缓存上限 private int limit; private HashMap map; public LRUCache(int limit) { this.limit = limit; map = new HashMap(); } public String get(String key) { Node node = map.get(key); if(node == null) { return null; } // 调整node到尾部 refreshNode(node); return node.value; } public void put(String key, String value) { Node node = map.get(key); if(node == null) { // key不存在直接插入 while(map.size() \u0026gt;= limit) { // 去除链表内的节点 String oldKey = removeNode(head); // 去除map中的缓存 map.remove(oldKey); } node = new Node(key, value); // 链表中加入节点 addNode(node); // map中加入节点 map.put(key, node); } else { // 更新节点并调整到尾部 node.value = value; refreshNode(node); } } private void refreshNode(Node node) { // 如果访问的是尾节点，无须移动节点 if(node == end) { return; } // 把节点移动到尾部，相当于做一次删除插入操作 removeNode(node); addNode(node); } private String removeNode(Node node) { // 尾节点 if(node == end) { end = end.pre; } else if(node == head) { // 头结点 head = head.next; } else { // 中间节点 node.pre.next = node.next; node.next.pre = node.pre; } return node.key; } private void addNode(Node node) { if(end != null) { end.next = node; node.pre = end; node.next = null; } end = node; if(head == null) { head = node; } } } JDK 中的算法实现 # 通常不需要我们自己专门实现此算法的数据结构，使用 JDK 内置的 LinkedHashMap 稍加改造即可。\\\npublic class LRUCache\u0026lt;K, V\u0026gt; extends LinkedHashMap\u0026lt;K, V\u0026gt; { private final int MAX_CACHE_SIZE; public LRUCache2(int cacheSize) { super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); MAX_CACHE_SIZE = cacheSize; } @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() \u0026gt; MAX_CACHE_SIZE; } } mybatis 中的 LRU 算法实现 # /** * 最近最少使用缓存 * 基于 LinkedHashMap 覆盖其 removeEldestEntry 方法实现。 */ public class LruCache implements Cache { private final Cache delegate; //额外用了一个map才做lru，但是委托的Cache里面其实也是一个map，这样等于用2倍的内存实现lru功能 private Map\u0026lt;Object, Object\u0026gt; keyMap; private Object eldestKey; public LruCache(Cache delegate) { this.delegate = delegate; setSize(1024); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } public void setSize(final int size) { keyMap = new LinkedHashMap\u0026lt;Object, Object\u0026gt;(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; // 核心就是覆盖 LinkedHashMap.removeEldestEntry方法, // 返回true或false告诉 LinkedHashMap要不要删除此最老键值 // LinkedHashMap内部其实就是每次访问或者插入一个元素都会把元素放到链表末尾， // 这样不经常访问的键值肯定就在链表开头啦 @Override protected boolean removeEldestEntry(Map.Entry\u0026lt;Object, Object\u0026gt; eldest) { boolean tooBig = size() \u0026gt; size; if (tooBig) { // 这里没辙了，把eldestKey存入实例变量 eldestKey = eldest.getKey(); } return tooBig; } }; } @Override public void putObject(Object key, Object value) { delegate.putObject(key, value); // 增加新纪录后，判断是否要将最老元素移除 cycleKeyList(key); } @Override public Object getObject(Object key) { // get的时候调用一下LinkedHashMap.get，让经常访问的值移动到链表末尾 keyMap.get(key); //touch return delegate.getObject(key); } @Override public Object removeObject(Object key) { return delegate.removeObject(key); } @Override public void clear() { delegate.clear(); keyMap.clear(); } @Override public ReadWriteLock getReadWriteLock() { return null; } private void cycleKeyList(Object key) { keyMap.put(key, key); // keyMap是linkedhashmap，最老的记录已经被移除了，然后这里我们还需要移除被委托的那个cache的记录 if (eldestKey != null) { delegate.removeObject(eldestKey); eldestKey = null; } } } ","date":"2019-06-29","externalUrl":null,"permalink":"/posts/lru--suan-fa/","section":"文章","summary":"What？LRU 是 Least Recently Used 的缩写，即最近最少使用，是一种常用的页面置换算法，选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段，用来记录一个页面自上次被访问以来所经历的时间 t，当须淘汰","title":"LRU 算法","type":"posts"},{"content":"","date":"2019-06-29","externalUrl":null,"permalink":"/tags/%E7%BC%93%E5%AD%98/","section":"标签","summary":"","title":"缓存","type":"tags"},{"content":"","date":"2019-03-15","externalUrl":null,"permalink":"/tags/nacos/","section":"标签","summary":"","title":"Nacos","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：Nacos 配置中心的调研\n为了进一步减少不必要的重复工作，最近在把之前的项目重构成 SpringBoot 项目之后，源于 N 台机器配置的管理甚是麻烦，所以便有了将项目的配置进行统一管理的需求。\\\n是什么？ # Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集，帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷、更轻松地构建、交付和管理微服务平台。\nNacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。本文主要调研的是 Nacos 的配置中心的功能。\n如何安装 Nacos # Nacos 通过下载发行包或者编译源码包来获取。\n从 Github 上下载源码方式 # git clone https://github.com/alibaba/nacos.git cd nacos/ mvn -Prelease-nacos clean install -U ls -al distribution/target/ // change the $version to your actual path cd distribution/target/nacos-server-$version/nacos/bin ` 下载编译后压缩包方式 # 您可以从 最新稳定版本 下载 nacos-server-$version.zip 包。\nunzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz cd nacos/bin 启动 Nacos # Linux/Unix/Mac\n启动命令(standalone 代表着单机模式运行，非集群模式):\\\nsh startup.sh -m standalone Windows\n启动命令：\\\ncmd startup.cmd 或者双击 startup.cmd 运行文件。\nSpringboot 下使用 Nacos 配置中心 # 新建 springboot 的 web 项目，可以通过 SpringBoot start 来创建。并添加 Nacos 的配置中心依赖。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;nacos-config-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.2.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 创建 Controller 并添加 Nacos 的配置注解。\n/** * @author wuhf * @Date 2019/3/14 19:28 **/ @NacosPropertySource(dataId = \u0026quot;goocity-test\u0026quot;, groupId = \u0026quot;goocity-test\u0026quot;,autoRefreshed = true) @RestController public class HelloController { @NacosValue(value = \u0026quot;${bus.pay.udids:null}\u0026quot;, autoRefreshed = true) private String busPayUdid; @GetMapping(\u0026quot;/hello\u0026quot;) public String test() { System.out.println(busPayUdid); return busPayUdid; } } 小结 # 本文总的来说写得比较流水账，主要记录一下使用 Nacos 作为配置中心时的过程，共勉……\n","date":"2019-03-15","externalUrl":null,"permalink":"/posts/nacos--pei-zhi-zhong-xin-de-diao-yan/","section":"文章","summary":"进一步减少不必要的重复工作，最近打算在把之前的项目重构成 SpringBoot 项目之后，源于 N 台机器配置的管理甚是麻烦，所以便有了进一步将项目的配置进行统一的管理的需求。是什么？Nacos 致力于帮助您发现、配置和管理微服务。N","title":"Nacos 配置中心的调研","type":"posts"},{"content":"","date":"2019-03-15","externalUrl":null,"permalink":"/tags/%E9%85%8D%E7%BD%AE%E4%B8%AD%E5%BF%83/","section":"标签","summary":"","title":"配置中心","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：LeetCode 每日一题（day 1）\n题目 # 题目描述：\n给定一个按非递减顺序排序的整数数组 A，返回由每个数字的平方组成的新数组，要求也按非递减顺序排序。\n示例 1：\n输入：[-4,-1,0,3,10] 输出：[0,1,9,16,100] 示例 2：\n输入：[-7,-3,2,3,11] 输出：[4,9,9,49,121] 提示：\n1\u0026lt;= A.length \u0026lt;= 10000 -10000 \u0026lt;= A \\[i\\]\u0026lt;= 10000 A 已按非递减顺序排序。 解决方案 # 方法一：排序 # 思路与算法\n创建一个新的数组，它的每个元素是给定数组对应位置元素的平方，然后排序这个数组。\npublic int[] sortedSquares(int[] A) { int[] B = new int[A.length]; for (int i=0; i \u0026lt; A.length; i++) { B[i] = (A[i]) * (A[i]); } Arrays.sort(B); return B; } 复杂度分析\n时间复杂度：O(N \\log N)O(NlogN)，其中 NN 是数组 A 的长度。\n空间复杂度：O(N)O(N)。\n方法二：双指针 # 思路\n因为数组 A 已经排好序了，所以可以说数组中的负数已经按照平方值降序排好了，数组中的非负数已经按照平方值升序排好了。\n举一个例子，若给定数组为 \\[-3, -2, -1, 4, 5, 6\\]，数组中负数部分 \\[-3, -2, -1\\] 的平方为 \\[9, 4, 1\\]，数组中非负部分 \\[4, 5, 6\\] 的平方为 \\[16, 25, 36\\]。我们的策略就是从前向后遍历数组中的非负数部分，并且反向遍历数组中的负数部分。\n算法\n我们可以使用两个指针分别读取数组的非负部分与负数部分 —— 指针 i 反向读取负数部分，指针 j 正向读取非负数部分。\n那么，现在我们就在使用两个指针分别读取两个递增的数组了（按元素的平方排序）。接下来，我们可以使用双指针的技巧合并这两个数组。\npublic int[] sortedSquares(int[] A) { int N = A.length; int j = 0; while (j \u0026lt; N \u0026amp;\u0026amp; A[j] \u0026lt; 0) j++; int i = j-1; int[] ans = new int[N]; int t = 0; while (i \u0026gt;= 0 \u0026amp;\u0026amp; j \u0026lt; N) { if (A[i] * A[i] \u0026lt; A[j] * A[j]) { ans[t++] = A[i] * A[i]; i--; } else { ans[t++] = A[j] * A[j]; j++; } } while (i \u0026gt;= 0) { ans[t++] = A[i] * A[i]; i--; } while (j \u0026lt; N) { ans[t++] = A[j] * A[j]; j++; } return ans; } ","date":"2019-02-13","externalUrl":null,"permalink":"/posts/leetcode--mei-ri-yi-ti--day-1/","section":"文章","summary":"题目 题目描述：给定一个按非递减顺序排序的整数数组 A，返回每个数字的平方组成的新数组，要求也按非递减顺序排序。示例 1：输入：4, 1,0,3,10 输出：0,1,9,16,100 示例 2：输入：7, 3,2,3,11 输","title":"LeetCode 每日一题（day 1）","type":"posts"},{"content":"📌 本文原发布于代码星冰乐：ArrayBlockingQueue 阻塞队列\n一直都在写业务代码，对于 jdk 底层的代码难免有些疏忽，所以决定把一些比较重要的源码过一遍……\\\n是什么？ # ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。此队列按照先进先出（FIFO）的原则对元素进行排序。默认情况下不保证访问者公平地访问队列，所谓公平访问队列是指阻塞的所有生产者线程或消费者线程，当队列可用时，可以按照阻塞的先后顺序访问队列，即先阻塞的生产者线程，可以先往队列里插入元素，先阻塞的消费者线程，可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。\n主要源码实现 # ArrayBlockingQueue 基于数组，代码相对简单，下面是主要的代码实现。\n主要常量 # /** 队列元素数组 */ final Object[] items; /** 队列头部元素的索引位置 */ int takeIndex; /** 队列尾部元素的索引位置 */ int putIndex; /** 记录当前队列的元素个数 */ int count; /* * 控制队列的锁 */ /** Main lock guarding all access */ final ReentrantLock lock; /** Condition for waiting takes */ private final Condition notEmpty; /** Condition for waiting puts */ private final Condition notFull; 主要方法 # /** * 入队，当锁阻塞线程时，要释放锁。 */ private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); } /** * 将当前元素出队，并释放锁 */ private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; final Object[] items = this.items; @SuppressWarnings(\u0026quot;unchecked\u0026quot;) E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); notFull.signal(); return x; } /** * 移除当前索引所指定的元素 */ void removeAt(final int removeIndex) { // assert lock.getHoldCount() == 1; // assert items[removeIndex] != null; // assert removeIndex \u0026gt;= 0 \u0026amp;\u0026amp; removeIndex \u0026lt; items.length; final Object[] items = this.items; // 若当前移除的元素是队尾元素则直接移除 if (removeIndex == takeIndex) { // removing front item; just advance items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); } else { // 否则通过迭代器来移除元素，防止触发 ConcurrentModifyException 的异常 // an \u0026quot;interior\u0026quot; remove // slide over all others up through putIndex. final int putIndex = this.putIndex; for (int i = removeIndex;;) { int next = i + 1; if (next == items.length) next = 0; if (next != putIndex) { items[i] = items[next]; i = next; } else { items[i] = null; this.putIndex = i; break; } } count--; if (itrs != null) itrs.removedAt(removeIndex); } // 释放锁 notFull.signal(); } /** * 加锁的入队方法。 */ public void put(E e) throws InterruptedException { // 判空 checkNotNull(e); // 获取可重入锁 final ReentrantLock lock = this.lock; // 加锁，但是该锁可被中断 lock.lockInterruptibly(); try { while (count == items.length) // 若队列满，则等待 notFull.await(); // 入队 enqueue(e); } finally { // 释放锁 lock.unlock(); } } /** * 出队列 */ public E take() throws InterruptedException { final ReentrantLock lock = this.lock; // 加锁，但是该锁可被中断 lock.lockInterruptibly(); try { while (count == 0) // 当队列为空时，等待 notEmpty.await(); return dequeue(); } finally { // 释放锁 lock.unlock(); } } 基于 ArrayBlockingQueue 实现的生产者-消费者模型 # 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯，而通过阻塞队列来通讯，所以生产者生产完数据之后不用等待消费者处理，直接扔给阻塞队列，消费者不找生产者要数据，而是直接从阻塞队列里取，阻塞队列就相当于一个缓冲区，平衡了生产者和消费者的处理能力。\npublic static void main(String[] args) { final ArrayBlockingQueue\u0026lt;String\u0026gt; container = new ArrayBlockingQueue\u0026lt;\u0026gt;(10); final int[] producerCount = {0}; new Thread(() -\u0026gt; { while (true) { try { System.out.println(\u0026quot;我生产了一个 : \u0026quot; + producerCount[0]++); container.put(producerCount[0] + \u0026quot;\u0026quot;); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(() -\u0026gt; { while (true) { try { System.out.println(\u0026quot;我消费了一个 : \u0026quot; + container.take()); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } 小结 # 精诚所至，金石为开……\n","date":"2019-01-27","externalUrl":null,"permalink":"/posts/arrayblockingqueue--zu-se-dui-lie/","section":"文章","summary":"一直都在写业务代码，对于 jdk 底层的代码难免有些疏忽，所以决定把一些比较重要的源码过一遍……是什么？ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。此队列按照先进先出（FIFO）的原则对元素进行排序。默认情况","title":"ArrayBlockingQueue 阻塞队列","type":"posts"},{"content":"","date":"2019-01-27","externalUrl":null,"permalink":"/tags/%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97/","section":"标签","summary":"","title":"阻塞队列","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：Pika 性能优化\n最近在迁移线上 Redis 到 Pika 的过程中，因为业务需要，需要对项目中原有的对 pika 读取操作的代码进行优化，最后结果就是读取百万级的数据由原来的 30 分钟降低到 10 分钟左右。\\\nPika 是什么 # Pika 是应 DBA 需求，由基础架构组开发的大容量、高性能、持久化、支持多数据结构的类 Redis 存储系统，目前已经开源，最新版本为 Pika 2.2。它所使用的 nemo 引擎本质上是对 Rocksdb 的改造和封装，使其支持多数据结构的存储，并在 nemo 引擎之上封装 Redis 接口，使其完全支持 Redis 协议。Pika 兼容 string、hash、list、zset、set 等多数据结构，使用磁盘而非内存存储数据解决了 Redis 由于存储数据量巨大而导致内存不够用的容量瓶颈。这段话摘自官网，感兴趣的小伙伴请移步 pika\n为什么要进行优化 # 因为慢啊……，主要原因是单线程的 Redis 是基于内存的，即便是单线程性能也是相当强悍的。Pika 的话是基于磁盘的，相对来说就有点先天不足了，所以多线程访问才能真正发挥它的性能。\n具体操作伪代码 # 业务场景：用户信息以 Hash 的形式存放在 Pika 中，需要根据 Hash 的 Key 获取用户信息，并将用户信息写入文件，数据量在百万级。\n方案一 # 将数据分批，然后将分好批的数据分配给创建的线程，执行获取用户信息，并写入文件的逻辑。下面只是大概的业务逻辑，\\\npublic class Test { public void businessService() { ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0;i \u0026lt; 100; i++) { executor.submit(new Task()); } executor.shutdown(); try { // 等待所有线程 boolean loop; do { loop = !executor.awaitTermination(2, TimeUnit.SECONDS); } while(loop); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\u0026quot;mina thread is over\u0026quot;); } /** * 要执行的业务逻辑 */ class Task implements Runnable { @Override public void run() { // getDataFromPika(); // writeDataToFile(); } } } 方案二 # 将数据读取到阻塞队列中（ArrayBlockingQueue），然后多线程从 ArrayBlockingQueue 中读取数据，写入文件中。线程结束的逻辑就是在队列中读取到特定字符，结束线程。下面是大概的业务逻辑。\\\npublic class Test { private ArrayBlockingQueue\u0026lt;String\u0026gt; queue = new ArrayBlockingQueue\u0026lt;\u0026gt;(1000); public void businessService() { // 读取数据，放入到队列中 while (true) { queue.put(\u0026quot;data\u0026quot;); } ExecutorService executor = Executors.newFixedThreadPool(10); // 创建线程 for (int i = 0;i \u0026lt; 100; i++) { executor.submit(new Task()); } for (int i = 0;i \u0026lt; 100; i++) { queue.put(\u0026quot;STOP\u0026quot;); } executor.shutdown(); } /** * 要执行的业务逻辑 */ class Task implements Runnable { @Override public void run() { while (true) { // getDataFromPika(); // writeDataToFile(); if (queue.take() == \u0026quot;STOP\u0026quot;) { break; } } } } } 小结 # 在优化代码时踩了不少坑，原因还是在于自己不够细心，程序员敲代码这活还是比较细的，稍不留意，被自己坑半天。\n","date":"2019-01-09","externalUrl":null,"permalink":"/posts/pika--xing-neng-you-hua/","section":"文章","summary":"最近在迁移线上 Redis 到 Pika 的过程中，因为业务需要，需要对项目中原有对 pika 读取操作的代码进行优化，最后结果就是读取百万级的数据由原来的 30 降低到 10分钟左右。Pika 是什么 Pika 是 DBA 需求，基础架构组开发","title":"Pika 性能优化","type":"posts"},{"content":"","date":"2019-01-09","externalUrl":null,"permalink":"/tags/%E5%AD%98%E5%82%A8/","section":"标签","summary":"","title":"存储","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：年中总结\n本来想 18年没什么要总结的，但是想想过去的日子总是值得纪念的，那就写点东西吧……\\\n这一年 # 上班的第二个年头，也是自食其力的第二年头，这一年过得还算充实，年初并没有做一些好的计划，所以整个 2018，做的好些事情都是有些随心所欲，甚至有些任性。\n上半年 # 2018年1月到 6月，这半年真的除了换了份工作之外，并没做一些特别值得纪念的事情，现在想想，好些东西已经完全想不起来了。不过翻翻自己的博客，在技术上也算是有点收获的，起码坚持了每月至少一篇博客，哪怕在自己最忙的时候也没有断。\n感谢那个努力奋斗的自己。\n下半年 # 2018年7月到 12月的今天，这半年印象最深的就是完成了两件自认为还是比较有意义的事情。\n一件就是，控制体重，从七月到九月基本上每天都坚持跑步，成功甩掉了十几斤的脂肪。\n另一件就是自己认为很重要的很牛逼的事情，因为有些事情，当下不做的话，之后再去做就真的没有机会，或者没有勇气了。从九月份开始备战考研，是的，考研。这个决定是整个 2018年最随心所欲的一次，也是决定之后一直都没放弃的一次。万事开头难，在你决定的那一刻，一切就没那么难了。在这个期间到底如何如何的苦，不想一一叙述了，毕竟有些事情适合放在心里，说出来就没意思了。知道结果就好了，我坚持下来了并且完成了每一场考试。\n感谢那个没有放弃的自己。\n下一年 # 第三个年头，希望能够在技术上有所突破，能够再减几斤肉，能够再读更多书，走更多路……\n无论如何未来可期 # 在这里借用普希金的诗来作为结束……\n假如生活欺骗了你，不要悲伤，不要心急！\n忧郁的日子里须要镇静：相信吧，快乐的日子将会来临！\n心儿永远向往着未来；现在却常是忧郁。\n一切都是瞬息，一切都将会过去；\n而那过去了的，就会成为亲切的怀恋。\n","date":"2018-12-26","externalUrl":null,"permalink":"/posts/nian-zhong-zong-jie/","section":"文章","summary":"本来想 18年没什么要总结的，但是想想过去的日子总是值得纪念的，那就写点东西吧……这一年 上班的第二个年头，也是自食其力的第二年头，这一年过得还算充实，年初并没有做一些好的计划，所以整个 2018，做的好些事情都是有些随心所欲，甚至有些任性","title":"年中总结","type":"posts"},{"content":"","date":"2018-12-03","externalUrl":null,"permalink":"/tags/docker/","section":"标签","summary":"","title":"Docker","type":"tags"},{"content":"📌 本文原发布于掘金社区：Docker 快速上手指南\nDocker 听其大名已久，但总是疏于操练，今天准备好好搞一下。\nDocker `Docker` 是什么？ # Docker 属于 Linux 容器的一种封装，提供简单易用的容器使用接口。 它是目前最流行的 Linux 容器解决方案。\nDocker 将应用程序与该程序的依赖打包在一个文件里面。运行这个文件，就会生成一个虚拟容器。程序在这个虚拟容器里运行，就好像在真实的物理机上运行一样。有了 Docker，就不用担心环境问题。\n总体来说，Docker 的接口相当简单，用户可以方便地创建和使用容器，把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改，就像管理普通的代码一样。\n`Docker` 怎么用？ # 本文安装 Docker 是基于 CentOS7 ，其他系统的小伙伴请直接转到官网的 quick start.\n卸载旧版本的 `Docker` # 较旧版本的 Docker 被称为 docker 或 docker-engine 。如果已安装这些，请卸载它们以及相关的依赖项。\nsudo yum remove docker \\ docker-client \\ docker-client-latest \\ docker-common \\ docker-latest \\ docker-latest-logrotate \\ docker-logrotate \\ docker-selinux \\ docker-engine-selinux \\ docker-engine 安装 `Docker CE` # 您可以根据需要以不同方式安装 Docker CE ：\n大多数用户设置 Docker 的存储库并从中进行安装，以便于安装和升级。这也是 Docker 官方推荐的方法。\n有些用户下载 RPM 软件包并手动安装，升级也完全手动管理。这在无法访问互联网的气隙系统上安装 Docker时非常有用。\n在测试和开发环境中，一些用户选择使用自动便捷脚本来安装 Docker。\n本文采用的是第一种方法：\n在新主机上首次安装 Docker CE之前，需要设置 Docker 存储库。之后，您可以 从存储库安装和更新 Docker 。\n设置 `REPOSITORY` # 1，安装所需的包。 yum-utils 提供 yum-config-manager 实用程序，devicemapper 存储驱动程序需要 device-mapper-persistent-data 和 lvm2。\nsudo yum install -y yum-utils \\ device-mapper-persistent-data \\ lvm2 2，使用以下命令设置稳定存储库。\nsudo yum-config-manager \\ --add-repo \\ https://download.docker.com/linux/centos/docker-ce.repo 安装 `Docker CE` # 1，安装最新版本的Docker CE，或转到下一步安装特定版本\nsudo yum install docker-ce 2，要安装特定版本的Docker CE，请列出 repo中的可用版本，然后选择并安装：a. 列出并对您的仓库中可用的版本进行排序。此示例按版本号对结果进行排序，从最高到最低，并被截断：\nyum list docker-ce --showduplicates | sort -rdocker-ce.x86_64 18.09.0.ce-1.el7.centos docker-ce-stable 返回的列表取决于启用的存储库，并且与您的CentOS版本相关（在此示例中以 .el7后缀表示）。\nb. 通过其完全限定的包名称安装特定版本，包名称（docker-ce）加上版本字符串（第 2 列），直到第一个连字符为止，用连字符（ - ）分隔，例如，docker-ce-18.03.0.ce。\nsudo yum install docker-ce-\u0026lt;VERSION STRING\u0026gt; Docker 已安装但尚未启动。已创建 docker 组，但未向该组添加任何用户。\n3，启动 Docker\nsudo systemctl start docker 4，通过启动 hello-world 镜像来验证 Docker 安装并启动成功\nsudo docker run hello-world 上面的命令下载测试镜像并在容器中运行它。当容器运行时，它会打印一条提示信息并退出。\n卸载 `Docker CE` # 1，卸载 Docker 的安装包\nsudo yum remove docker-ce 2，主机上的镜像，容器，卷或自定义配置文件不会自动删除。要删除所有镜像，容器和卷：\nsudo rm -rf /var/lib/docker Maven 构建 Spring Boot 的 Docker 镜像 # 构建项目，修改配置 # 通过 https://start.spring.io/ 构建 Spring Boot 工程。\n修改 pom 文件 # \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- tag::plugin[] --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;com.spotify\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;dockerfile-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.6\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!--suppress UnresolvedMavenProperty --\u0026gt; \u0026lt;repository\u0026gt;${docker.image.prefix}/${project.artifactId}\u0026lt;/repository\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- end::plugin[] --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;com.spotify\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;docker-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;!--\u0026lt;version\u0026gt;0.4.12\u0026lt;/version\u0026gt;--\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 注意imageName一定要是符合正则[a-z0-9-_.]的，否则构建不会成功 --\u0026gt; \u0026lt;!-- 详见：https://github.com/spotify/docker-maven-plugin Invalid repository name ... only [a-z0-9-_.] are allowed--\u0026gt; \u0026lt;dockerHost\u0026gt;http://127.0.0.1:2375\u0026lt;/dockerHost\u0026gt; \u0026lt;imageName\u0026gt;docker-springboot\u0026lt;/imageName\u0026gt; \u0026lt;baseImage\u0026gt;java\u0026lt;/baseImage\u0026gt; \u0026lt;entryPoint\u0026gt;[\u0026quot;java\u0026quot;, \u0026quot;-jar\u0026quot;, \u0026quot;/${project.build.finalName}.jar\u0026quot;]\u0026lt;/entryPoint\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;targetPath\u0026gt;/\u0026lt;/targetPath\u0026gt; \u0026lt;directory\u0026gt;${project.build.directory}\u0026lt;/directory\u0026gt; \u0026lt;include\u0026gt;${project.build.finalName}.jar\u0026lt;/include\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt;\u0026lt;/build\u0026gt; 需要注意的是 \u0026lt;dockerHost\u0026gt;http://127.0.0.1:2375\u0026lt;/dockerHost\u0026gt;，如果本地有安装 docker ，直接使用本地默认即可，若未安装，需要使用远程的 docker 服务时，需要在服务器配置 docker ，具体操作请移步 Docker 远程连接。\n添加 `Dockerfile` # 注意 com.whforever.dockerspringboot.DockerSpringbootApplication 是指 Spring Boot 项目的代码入口。\nFROM openjdk:8-jdk-alpineVOLUME /tmpARG DEPENDENCY=target/dependencyCOPY ${DEPENDENCY}/BOOT-INF/lib /app/libCOPY ${DEPENDENCY}/META-INF /app/META-INFCOPY ${DEPENDENCY}/BOOT-INF/classes /appENTRYPOINT [\u0026quot;java\u0026quot;,\u0026quot;-cp\u0026quot;,\u0026quot;app:app/lib/*\u0026quot;,\u0026quot;com.whforever.dockerspringboot.DockerSpringbootApplication\u0026quot;] 构建 `Spring Boot` 的 `Docker` 镜像 # 执行 maven 命令，进行构建\nmvn clean package docker:build 执行完成之后我们就可以在远程看到刚构建好的 Spring Boot 的镜像。\ndocker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESe5f2c7e4e7c1 docker-springboot:latest \u0026quot;java -jar /docker-s…\u0026quot; 22 hours ago Up 22 hours 0.0.0.0:8080-\u0026gt;8080/tcp agitated_kilby 启动 Docker 镜像\ndocker run -p 8080:8080 docker-springboot 完美……\ndocker-run 小结 # 本文涉及的代码已经上传到 github，感兴趣的小伙伴后台回复关键字 Docker 就可获得代码地址。\n参考链接 # https://spring.io/guides/gs/spring-boot-docker/\nhttp://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html\n","date":"2018-12-03","externalUrl":null,"permalink":"/posts/docker-kuaisushangshouzhinan/","section":"文章","summary":"Docker 听其大名已久，但总是疏于操练，今天准备好好搞一下。","title":"Docker 快速上手指南","type":"posts"},{"content":"","date":"2018-12-03","externalUrl":null,"permalink":"/tags/spring-boot/","section":"标签","summary":"","title":"Spring Boot","type":"tags"},{"content":"","date":"2018-12-03","externalUrl":null,"permalink":"/tags/%E5%AE%B9%E5%99%A8/","section":"标签","summary":"","title":"容器","type":"tags"},{"content":"📌 本文原发布于掘金社区：散列表\n原文地址：haifeiWu 和他朋友们的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n做个预警，这篇文章有点硬……\n什么是散列表 # 是根据键 (Key) 而直接访问在内存存储位置的数据结构。也就是说，它通过计算一个关于键值的函数，将所需查询的数据映射到表中一个位置来访问记录，这加快了查找速度。这个映射函数称做散列函数，存放记录的数组称做散列表。\n通俗的解释 # 一个通俗的例子是，为了查找电话簿中某人的号码，可以创建一个按照人名首字母顺序排列的表（即建立人名${\\displaystyle x}$ 到首字母 ${\\displaystyle F(x)}$ 的一个函数关系），在首字母为 W 的表中查找 王 姓的电话号码，显然比直接查找就要快得多。这里使用人名作为关键字，取首字母 是这个例子中散列函数的函数法则 ${\\displaystyle F(x)}$，存放首字母的表对应散列表。关键字和函数法则理论上可以任意确定。\n基本思想 # 若关键字为 ${\\displaystyle k}$，则其值存放在 ${\\displaystyle f(k)}$ 的存储位置上。由此，不需比较便可直接取得所查记录。称这个对应关系 ${\\displaystyle f}$ 为散列函数\n散列表几个重要概念： # 散列函数、装载因子、散列冲突\n装载因子： # ${\\displaystyle \\alpha }$ = 填入表中的元素个数 / 散列表的长度\n${\\displaystyle \\alpha }$ 是散列表装满程度的标志因子。由于表长是定值，${\\displaystyle \\alpha }$ 与“填入表中的元素个数”成正比，所以，${\\displaystyle \\alpha }$ 越大，表明填入表中的元素越多，产生冲突的可能性就越大；反之，${\\displaystyle \\alpha }$ 越小，表明填入表中的元素越少，产生冲突的可能性就越小。实际上，散列表的平均查找长度是载荷因子 ${\\displaystyle \\alpha }$ 的函数，只是不同处理冲突的方法有不同的函数。\n对于开放定址法，装载因子是特别重要因素，应严格限制在 0.7-0.8 以下。超过 0.8，查表时的 CPU 缓存不命中（cache missing）按照指数曲线上升。因此，一些采用开放定址法的 hash 库，如 Java 的系统库限制了装载因子为 0.75，超过此值将 resize 散列表。\n散列冲突： # 就是指多个元素通过散列函数计算得到的散列地址是相同的。\n散列函数： # 散列函数选取原则：\n好的散列函数 = 计算简单 + 分布均匀\n数据结构中的散列函数： # 1，直接定址法：取关键字或关键字的某个线性函数值为散列地址。即${\\displaystyle hash(k)=k}$或${\\displaystyle hash(k)=a\\cdot k+b}$，其中 ${\\displaystyle a\\,b}$ 为常数。\n2，数字分析法：数字分析法通常适用于散列表中可能出现的关键字都是事先知道的情况，例如我们现在要存储某家公司员工登记表，如果用手机号作为关键字，那么我们发现抽取后面的四位数字作为散列地址是不错的选择，同理存储身份证号码时，也可以采用这样的逻辑。\n3，平方取中法：平方取中法是将关键字平方之后取中间若干位数字作为散列地址。这种方法适用于不知道关键字的分布，且数值的位数又不是很大的情况。\n4，随机数法：选择一个随机数，取关键字的随机函数值为它的散列地址，$f(key) = random(key)$\n5，除留取余法：取关键字被某个不大于散列表表长 m 的数 p 除后所得的余数为散列地址。即 ${\\displaystyle hash(k)=k\\,{\\bmod {\\,}}p}, {\\displaystyle p\\leq m}$ p为小于 m 的最大质数，所谓素数就是指只能被 1 与它本身整除的数。\n主要的散列冲突的解决办法 # 开放寻址法： # 所谓的开放定址法就是一旦发生了冲突，就去寻找下一个空的散列地址，只要散列表足够大，空的散列地址总能找到，并将记录存入其中。\n主要是有线性探查 $fi(key) = (f(key)+di) MOD m (di=1,2,…,m-1)$ 平方探查 $fi(key) = (f(key)+di) MOD m (di=1²,-1²,2²,-2²…,q²,-q²,q\u0026lt;=m/1)$\n拉链法（链地址法） 将散列到同一个存储位置的所有元素保存在一个链表中。\n再散列法： # 即在上次散列计算发生冲突时，利用该次冲突的散列函数地址产生新的散列函数地址，直到冲突不再发生。\nExample # 开放定址法 # 散列表查找的伪代码 # // 使用除留余数法 int Hash(int key) { return key % HASHSIZE; //除数一般小于等于表长的最大素数 } // 插入关键字到散列表 void InsertHash(HashTable *H, int key) { int addr; addr = Hash(key); //只是得到一个偏移地址 while( H-\u0026gt;elem[addr] != NULLKEY ) // 如果不为空，则冲突出现 { addr = (addr + 1) % HASHSIZE; // 开放定址法的线性探测 } H-\u0026gt;elem[addr] = key; } // 散列表查找关键字 int SearchHash(HashTable H, int key, int *addr) { *addr = Hash(key); while( H.elem[*addr] != key ) { *addr = (*addr + 1) % HASHSIZE; if( H.elem[*addr] == NULLKEY || *addr == Hash(key) ) //后面那个条件说明循环回到原点 { return -1; } } return 0; } Java 中的散列 # Java 中的散列冲突解决方法就是上文中提到的开放定址法。散列函数如下。\nstatic final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } 散列查找方法\npublic V get(Object key) { Node\u0026lt;K,V\u0026gt; e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node\u0026lt;K,V\u0026gt; getNode(int hash, Object key) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; first, e; int n; K k; if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (first = tab[(n - 1) \u0026amp; hash]) != null) { if (first.hash == hash \u0026amp;\u0026amp; // always check first node ((k = first.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode\u0026lt;K,V\u0026gt;)first).getTreeNode(hash, key); do { if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 散列表的插入\npublic V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node\u0026lt;K,V\u0026gt; e; K k; if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode\u0026lt;K,V\u0026gt;)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount \u0026gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size \u0026gt; threshold) resize(); afterNodeInsertion(evict); return null; } 小结 # 最近在学习数据结构的时候，复习了一下散列表的基本概念，因为散列表在我们敲代码的时候用得比较多，所以打好基础还是有必要的。\n参考链接 # 维基百科 ","date":"2018-11-23","externalUrl":null,"permalink":"/posts/sanliebiao/","section":"文章","summary":"是根据键 (Key) 而直接访问在内存存储位置的数据结构。也就是说，它通过计算一个关于键值的函数，将所需查询的数据映射到表中一个位置来访问记录，这加快了查找速度。","title":"散列表","type":"posts"},{"content":"","date":"2018-11-23","externalUrl":null,"permalink":"/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/","section":"标签","summary":"","title":"数据结构","type":"tags"},{"content":"","date":"2018-11-23","externalUrl":null,"permalink":"/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/","section":"标签","summary":"","title":"编程语言","type":"tags"},{"content":"","date":"2018-11-14","externalUrl":null,"permalink":"/tags/nginx/","section":"标签","summary":"","title":"Nginx","type":"tags"},{"content":"📌 本文原发布于掘金社区：Nginx 不停机升级 及 gzip 压缩优化\n原文地址：haifeiWu 和他朋友们的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n好久不写博客手都生了，不过这个习惯不能丢，仅以一篇水文记录一下 Nginx 不停机版本升级及配置 gzip 压缩优化网站访问体验的过程。\n缘起 # 何为水文，楼主对水文的定义就是百度一搜一大把，但是始终比较杂乱，需要自己仔细甄别才能真正解决问题，这也是楼主写这篇文章的原因，记录一下这个过程，也留给以后自己查阅，也分享给有需要的小伙伴。\n开篇 # Nginx 不停机升级 # 1. 下载稳定版本的 Nginx\nwget http://nginx.org/download/nginx-1.14.1.tar.gz 2. 编译 Nginx\n注意编译的时候不要执行 make install。\n# 解压 tar -zxvf nginx-1.14.1.tar.gz # 编译Nginx cd nginx-1.14.1 # 配置编译要加载的模块 ./configure --with-http_ssl_module # 执行编译，切记不要make install make 3. 备份原来的 nginx 脚本，替换成编译新生成的\n备份完原来的数据之后，执行下面的脚本，覆盖 ngxin 可执行程序。\ncp -rfp objs/nginx /usr/local/nginx/sbin/ 4. 执行升级\n下面的命令应该在最开始的 make 的目录下执行。\nmake upgrade 配置 Nginx 的 gzip 压缩 # 这个配置比较简单，修改 nginx.conf 文件添加如下内容即可。\ngzip on; # 开启Gzip gzip_min_length 1k; # 不压缩临界值，大于1K的才压缩 gzip_buffers 4 16k; gzip_comp_level 8; # 压缩级别 # 进行压缩的文件类型 gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; gzip_vary on; 然后重启一下 Nginx 就可以了\n./nginx -s reload 终章 # 当然除了 gzip 可以实现压缩之外还有一种 Google 爸爸加持的更强悍的压缩算法，名字叫 brotli，有时间可以再搞一下。\n","date":"2018-11-14","externalUrl":null,"permalink":"/posts/nginx-butingjishengji-ji-gzip-yasuoyouhua/","section":"文章","summary":"好久不写博客手都生了，不过这个习惯不能丢，仅以一篇水文记录一下 Nginx 不停机版本升级及配置 gzip 压缩优化网站访问体验过程。何为水文，楼主对水文的定义就是百度一搜一大把，但是始终比较杂乱，需要自己仔细甄别才能真正解决问题，这也是楼主写这篇文章的原因，记录一下这个过程…","title":"Nginx 不停机升级 及 gzip 压缩优化","type":"posts"},{"content":"","date":"2018-11-14","externalUrl":null,"permalink":"/tags/%E7%99%BE%E5%BA%A6/","section":"标签","summary":"","title":"百度","type":"tags"},{"content":"","date":"2018-11-11","externalUrl":null,"permalink":"/tags/spring/","section":"标签","summary":"","title":"Spring","type":"tags"},{"content":"📌 本文原发布于代码星冰乐：Spring 5 WebFlux 性能测试[译]\nJava 世界对反应式编程抱有很高的期望。根据 官方文档 的描述，它使程序员能够构建更具韧性，弹性，响应和消息驱动的应用程序。简而言之，它是一种更好，更快，更现代的模型，可以防止应用程序空闲。\nSpring 5 通过结合基于 Project Reactor的 Spring 反应计划，引入了一种新的响应式编程模型。但它能完成这项工作吗？\n我们研究了Spring 提供的新功能，并对它进行了一次性能测试，测试结果请往下看，我想不会让你失望的。\n注意：我们的结果可能会在几周/几个月内改变。实际上，截至目前，尚未发布真实的 Spring 样本，并且文档不完整。Spring 5 和 Spring Boot 2 仍在开发中（Spring Framework 5.0.0 RC3，Spring Boot 2.0.0.M2），Project Reactor 也在不断发展。此外，社区的反馈仍然很少（JHipster，Spring，Reddit）。\nWhat’s new in Spring 5? # Spring 框架引入了很多新功能。其中最重要的是反应式编程。\nSpring MVC and Spring WebFlux # 可能仍然有一些人试图用旧的 Spring 4 技术进行反应式编程，如果你这样做，那么你很有可能会遇到一些麻烦。Spring 5 提供了一个易于使用的新模块：spring-webflux。它与它的兄弟 spring-mvc 做同样的事情，但是它是一种响应式编程模型。让我们看看它是如何工作的吧。\nWebFlux 主要围绕两个 Project Reactor 的类：Mono 和 Flux。\nMono 是 CompletableFuture 类型的反应等价物，允许以反应方式处理单个对象。Flux 是多个对象的等价物。它们可以像 Stream 一样被处理（可以直接配合 lambda 表达式使用）。因此，你可能会看到如下所示的代码：\nreactiveService.getResults() .mergeWith(Flux.interval(100)) .map(r -\u0026gt; r * 2) .doOnNext(Service1::someObserver) .doAfterTerminate(Service2::incrementTerminate); 它们都是 Reactive Streams 规范的 Publisher 接口的实现，因此它们需要注册到订阅者，以便数据开始流动。\n幸运的是，基于注解的编程模型仍然是最新的，与 Spring MVC 的唯一区别是 REST 层的方法现在返回 Mono 或 Flux：\n@PutMapping(\u0026quot;/operations\u0026quot;) public Mono\u0026lt;Operation\u0026gt; updateOperation(@Valid @RequestBody Operation operation) throws URISyntaxException { log.debug(\u0026quot;REST request to update Operation : {}\u0026quot;, operation); return operationRepository.save(operation); } Spring 知道如何处理 Monos 或 Fluxs。它会自动将封装的对象传递给前端。\n关于与数据库的通信，Spring 5 支持 Cassandra，CouchBase，MongoDB和 Redis 的反应驱动程序，它们可以跟 Spring Data 一起使用。\n下面是操作 MongoDB 的代码例子\n@Repository public interface BankAccountRepository extends ReactiveMongoRepository\u0026lt;BankAccount,String\u0026gt; { Mono\u0026lt;BankAccount\u0026gt; getFirstByBalanceEndingWith(BigDecimal bigDecimal); Mono\u0026lt;Long\u0026gt; countByBalanceEquals(BigDecimal bigDecimal); Flux\u0026lt;BankAccount\u0026gt; findAllByIdBefore(UUID uuid); } Our tests # Why? # 反应式编程现在正在流行，当然，Pivotal 决定顺理成章地将其集成到 Spring 框架中，并承诺提供更好的性能和可扩展性。可悲的是，没有给出任何性能测试数据……\nHow? # 我们通过在生产模式下对不同的由 JHipster 生成的应用程序（MySQL，Mongo，Model……）进行压力测试（使用Gatling）。\n这些应用程序中的每一个都经过多次复制和修改，以确保我们的测试有丰富的测试数据，从而确保测试的正确性。\n例如，对于 MySQL 应用程序，我们创建了四个类似的应用程序：\n使用 Spring 4（因为你实际可以使用 JHipster 生成） 使用 Spring 5（仅迁移） 使用 Spring 5 和反应式编程（在 REST 层上） 使用 Spring 5 和反应式编程（仅在一个实体的 RestController 类上） 对于具有异步驱动程序的 Mongo，我们创建了应用程序：\n使用Spring 4（因为你实际可以使用JHipster生成） 使用Spring 5（仅迁移） 使用Spring 5和反应式编程（在REST层上） 使用Spring 5和反应式编程（仅在一个实体的RestController类上） 使用Spring 5和实体上的反应式编程一直到存储库。 Spring允许程序员配置自己的调度程序（处理反应式调用的线程池）。因此，当使用反应式编程时，仅在 REST 层（而不是实体）上，我们尝试了不同的调度程序：Schedulers.parallel()每个 CPU 核心使用一个线程，而Schedulers.elastic()动态创建线程。\n每个测试包括同时启动 Gatling 5000/10000/15000 用户，每个用户执行场景中描述的操作：\nscenario(\u0026quot;Test the Operation entity\u0026quot;) .exec(http(\u0026quot;First unauthenticated request\u0026quot;) .get(\u0026quot;/api/account\u0026quot;) .headers(headers_http) .check(status.is(401))).exitHereIfFailed .pause(5) .exec(http(\u0026quot;Authentication\u0026quot;) .post(\u0026quot;/api/authenticate\u0026quot;) .headers(headers_http_authentication) .body(StringBody(\u0026quot;\u0026quot;\u0026quot;{\u0026quot;username\u0026quot;:\u0026quot;admin\u0026quot;, \u0026quot;password\u0026quot;:\u0026quot;admin\u0026quot;}\u0026quot;\u0026quot;\u0026quot;)).asJSON .check(header.get(\u0026quot;Authorization\u0026quot;).saveAs(\u0026quot;access_token\u0026quot;))).exitHereIfFailed .pause(1) .repeat(2) { exec(http(\u0026quot;Authenticated request\u0026quot;) .get(\u0026quot;/api/account\u0026quot;) .headers(headers_http_authenticated) .check(status.is(200))) .pause(5) } .repeat(2) { exec(http(\u0026quot;Get all operations\u0026quot;) .get(\u0026quot;/api/operations\u0026quot;) .headers(headers_http_authenticated) .check(status.is(200))) .pause(5 seconds, 10 seconds) .exec(http(\u0026quot;Create new operation\u0026quot;) .post(\u0026quot;/api/operations\u0026quot;) .headers(headers_http_authenticated) .body(StringBody(\u0026quot;\u0026quot;\u0026quot;{\u0026quot;id\u0026quot;:null, \u0026quot;date\u0026quot;:\u0026quot;2020-01-01T00:00:00.000Z\u0026quot;, \u0026quot;description\u0026quot;:\u0026quot;SAMPLE_TEXT\u0026quot;, \u0026quot;amount\u0026quot;:\u0026quot;1\u0026quot;}\u0026quot;\u0026quot;\u0026quot;)).asJSON .check(status.is(201)) .check(headerRegex(\u0026quot;Location\u0026quot;, \u0026quot;(.*)\u0026quot;).saveAs(\u0026quot;new_operation_url\u0026quot;))).exitHereIfFailed .pause(5) .repeat(8) { exec(http(\u0026quot;Get created operation\u0026quot;) .get(\u0026quot;${new_operation_url}\u0026quot;) .headers(headers_http_authenticated)) .pause(3) } .exec(http(\u0026quot;Delete created operation\u0026quot;) .delete(\u0026quot;${new_operation_url}\u0026quot;) .headers(headers_http_authenticated)) .pause(5) } 然后，我们可以通过比较时间或错误/崩溃来分析这些结果。\nOur big configuration: # 机器 1 用作 Spring Boot 服务器和本地数据库：i7-4790K 4GHz - 16Go - SSD - Ubuntu 16.04 64bits 机器 2 用作加特林客户端：i7-4790K 4GHz - 16Go - SSD - Ubuntu 16.04 64bits Cisco SG100-24 24 端口千兆交换机 📷 图注：configuration\nResults # 从 5000 个用户的模拟生成了以下结果。我们还使用 10000/15000/20000 用户进行了测试，但由于错误数量很多，结果并不一致。\nWith a MySQL-based JHipster application: # （注意：下面的结果不包括场景中的暂停。）\\\n📷 图注：MySQL-based\n当用户在他的 Gatling 场景中出错时，他的模拟将停止。因此，如果存在一些错误，则请求服务器的用户较少，因此负载较低且耗时也会变化。\n错误可以有几种：超时，达到数据库连接的阈值，使用 Spring 创建/销毁 bean 的并发问题，……\n这些图表显示了用户运行 Gatling 场景所需的总时间。\\\n📷 图注：MySQL-based2 📷 图注：MySQL-based3\nWith a Mongo-based JHipster application: # 📷 图注：MySQL-based3 📷 图注：MySQL-based3 📷 图注：MySQL-based3\nRegarding the execution times # 我们可以看到，总体而言，Reactive 应用程序比“经典” Spring 应用程序慢。\n对于 MySQL，这是可以预见的，因为数据库在使用过程中设置了所有锁，并且没有官方的响应/异步驱动程序。\n对于Mongo，有一堆完整的反应组件（驱动程序，存储库，……），但即便如此，性能也会更差。\n此外，我们注意到Spring 4 和 Spring 5 之间的速度没有明显改善，即使没有添加反应式编程。\nRegarding scalability # 关于可扩展性，Reactive 应用程序可以处理比Spring4 / Spring5 应用程序更少的用户。\n实际上，我们通过使用 Gatling 模拟5000,10000,15000和20000用户注意到了这种差异。\n从10000个用户开始，我们在Reactive应用程序上有太多错误，通常超过40％的KO请求。\nConclusion # 我们的反应式应用程序没有观察到速度的提高（Gatling 的结果甚至略差）。 关于用户友好性，反应式编程不会添加大量新代码，但它肯定是一种更复杂的编码（和调试……）方式。可能需要快速复习一下 Java 8。 目前的主要问题是缺乏文档。这是我们生成测试应用程序的最大障碍，因此我们可能遗漏了一个关键点。 因此，我们建议不要过快投入反应式编程，并等待更多反馈。Spring WebFlux 尚未证明其相对于 Spring MVC 的优势。 你可以在此存储库中找到我们的代码：jhipster / webflux-jhipster。\nGatling 结果可以在每个模块根目录的 gatling-results 目录中找到。\n原文链接 # Spring 5 WebFlux: Performance tests ","date":"2018-11-11","externalUrl":null,"permalink":"/posts/spring-5-webflux--xing-neng-ce-shi---yi/","section":"文章","summary":"Java 世界对反应式编程抱有很高的期望。根据 官方文档 的描述，它使程序员能够构建更具弹性，弹性，响应和消息驱动的应用程序。简而言之，它是一种更好，更快，更现代的模型，可以防止应用程序空闲。Spring 5 通过结合基于 Proje","title":"Spring 5 WebFlux 性能测试[译]","type":"posts"},{"content":"","date":"2018-11-11","externalUrl":null,"permalink":"/tags/webflux/","section":"标签","summary":"","title":"WebFlux","type":"tags"},{"content":"","date":"2018-10-22","externalUrl":null,"permalink":"/tags/%E5%AE%89%E5%85%A8/","section":"标签","summary":"","title":"安全","type":"tags"},{"content":"📌 本文原发布于掘金社区：聊聊 volatile 关键字\n原文地址：haifeiWu 和他朋友们的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n我们知道 volatile 关键字的作用是保证变量在多线程之间的可见性，它是 java.util.concurrent 包的核心，没有 volatile 就没有这么多的并发类给我们使用。本文将简单介绍一下 volatile 这个东东。\n算法概念及其执行流程 # CAS(compare-and-swap) 是一种硬件对并发的支持，处理器中针对多处理器操作而设计的一种特殊指令，用于管理对共享数据的并发访问。\nCAS 是一种无锁非阻塞算法的实现。\nCAS 包含了 3 个操作数：\n需要读写的内存值 V、进行比较的值 A\n拟写入的新值 B\n当且仅当 V 的值等于 A 时，CAS 通过原子方式用新值更新 V 的值，否则不会执行任何操作。\nCAS 操作过程如下所示 # CAS 算法模拟 # /** * 模拟 CAS 算法 * * Created by wuhf on 2017-1-22. */ public class TestCompareAndSwap { public static void main(String[] args){ final CompareAndSwap cas = new CompareAndSwap(); for (int i = 0; i \u0026lt; 10; i++ ){ new Thread(new Runnable() { @Override public void run() { int expectValue = cas.getValue(); boolean b = cas.compareAndSet(expectValue, (int)(Math.random() * 101)); System.out.println(b); } } ).start(); } } } class CompareAndSwap{ private int value; // 获取内存值 public synchronized int getValue(){ return this.value; } // 比较 public synchronized int compareAndSwap(int expectValue,int newValue){ int oldValue = this.value; if(oldValue == expectValue){//如果期望值等于旧值 this.value = newValue; } return oldValue; } public synchronized boolean compareAndSet(int expectValue,int newValue){ return expectValue == compareAndSwap(expectValue, newValue); } } 原子变量 # 类的小工具包，支持在单个变量上解除锁的线程安全编程。事实上，此包中的类可将 volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类。\n类 AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。\nAtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操作，对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方面也引人注目，这对于普通数组来说是不受支持的。\n核心方法：boolean compareAndSet(expectedValue, updateValue)\njava.util.concurrent.atomic 包下提供了一些原子操作的常用类：\nAtomicBoolean、AtomicInteger、AtomicLong、AtomicReference AtomicIntegerArray、AtomicLongArray AtomicMarkableReference AtomicReferenceArray AtomicStampedReference 原子变量简单 Demo # /** * 一、i++ 的原子性问题：i++ 的操作实际上分为三个步骤“读-改-写” * int i = 10; * i = i++; //10 * * int temp = i; * i = i + 1; * i = temp; * 二、原子变量：在 java.util.concurrent.atomic 包下提供了一些原子变量。 * 1. volatile 保证内存可见性 * 2. CAS（Compare-And-Swap） 算法保证数据变量的原子性 * CAS 算法是硬件对于并发操作的支持 * CAS 包含了三个操作数： * 1,内存值 V * 2,预估值 A * 3,更新值 B * 当且仅当 V == A 时， V = B; 否则，不会执行任何操作。 */ public class AtomicDemo { public static void main(String[] args) { AtomicData ad = new AtomicData(); for (int i = 0; i \u0026lt; 10; i++) { new Thread(ad).start(); } } } class AtomicData implements Runnable{ // 初始化原子变量 private AtomicInteger atomicData = new AtomicInteger(0); @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getAtomicData()); } // 相当于atomicData++ public int getAtomicData(){ return atomicData.getAndIncrement(); } } 小结 # 当我们的实现可能会并发操作共享资源时，加锁可能会是最简单粗暴的方法，但是使用不慎必然会产生死锁等问题，而造成线程假死，产生重大线上问题。因此 volatile 不失为不错的选择。\n","date":"2018-10-22","externalUrl":null,"permalink":"/posts/liaoliao-volatile-guanjianzi/","section":"文章","summary":"我们知道 volatile 关键字的作用是保证变量在多线程之间的可见性，它是 java.util.concurrent 包的核心，没有 volatile 就没有这么多的并发类给我们使用。本文将简单介绍一下 volatile 这个东东。CAS(compare-and-swap)…","title":"聊聊 volatile 关键字","type":"posts"},{"content":"📌 本文原发布于代码星冰乐：安全闲扯\n这个虽然是闲扯淡的，但是看的时候请抓牢：\n我们一切以业务方案为目的， 一些关键名词和概念还是很认真。 加密签名 # 一个故事 # 这是从别人那边听来的故事，作为安全知识的入门非常有意义。\n一个国王英年早逝，留下年幼的王子，国王的弟弟就做了摄政王。 王子快 18 岁的时候，他叔叔派他做信使，给另外一个国王送信，还让侍卫长跟着。 王子晚上趁着侍卫长睡着了，偷偷打开信件，看见里面的内容是：请您杀了这个信使。 王子于是把信件修改了一下：请把您的女儿嫁给信使。 王子然后做了那个国家的驸马。过几年后，岳父也死了，他就做了国王，带兵杀回去报仇。 叔叔的反思和改进 # 暂时不讨论善恶，我们站在叔叔的立场上，看他哪些事情没做好？\n数据的隐秘：他的信件内容被别人看到了。 数据的篡改：他的邮件被人冒充了。 所以从技术层面上，我们可以给他一个有效建议，跟别的国王约定：\n针对第一个情况：他写的信，每个字母向后偏移 10 个位置——数据被加密了 针对第二个情况：商量了一个算式，每个信件，把全部字母的数字加起来，通过这个算式算出来一个新的数字，写在信件最后，这样除了他俩，没谁能伪造信——数据被签名了 PKI 里的加密和签名 # 事实上，信息传输的物理通道是不安全的，【信使】作为信息的传递人，可以轻易的偷窥别人的信息，甚至“代表”别人发言。老外的思路是认可这个现状，然后从数理逻辑上来应对这个问题。\nPublic Key Infrastructure，公开密钥体系，就是针对这个情况的。\n这个体系的特点是，算法思路和实现都可以是公开的，只要保证 Key 的安全就可以。\n这个章节里面，我们只要讲到加密和签名就可以了。\n加密 # 加密根据密钥可以粗略地分两类\n对称加密 # 对称加密，就是加密和解密的过程里面，用的 key 都是一个——我们称为 SecretKey。\nAES：一般记得这个就可以了，因为现在好像也就它是最好用的 DES、3Des：我经常在代码里面能看见的。 其它 业务跟进：在前面我们给王叔的建议，很明显是一个比较土的对称加密方案。\n非对称加密 # 非对称加密，加密和解密的密钥不同，用于加密的称为公钥（PublicKey），用于解密的称为私钥（PrivateKey）。\n业务跟进：我们的建议，国王们各自有一套公钥+私钥，私钥自己保留好，公钥发布给大家。谁想给某人写信了，就用这个人的公钥加密，即使信件半路上被人截胡了，也无法解密内容。\nRSA：最出名的，在以前非对称加密就是区分为 RSA 和其它， ECC：当前最牛的规范，rsa 也变成其它了 其它： 对称和非对称的优劣对比 # 对称加密在技术上有优势，同样的安全级别下，加密解密的速度快很多 对称加密也兼具签名功能 但是在多用户业务场景，有无法解决的问题： 两个人玩，一套 SecretKey 就够了 三个人玩，3 套 四个人玩，12 套， ……N 的组合数量。 结论：后面讨论各自适合的场景。 签名和验签 # 业务跟进：我们的建议，国王们各自有一套自己的公钥+私钥，私钥自己保留好，公钥发布给大家。谁想发邮件，用私钥对内容进行“签名”，签名的结果写在信封上；收到邮件的人，用【发件人的公钥】+【内容】+【签名】进行验签，以确保这个邮件就是那个国王发的\nJava 伪码表示如下\n签名：byte[] sign(byte[] content, PrivateKey prvKey) 验签：boolean verify(byte[] result, byte[] content, PublicKey pubKey) 小结 # 其实对称加密可以忽略，我们就记得非对称加密和签名比较重要\n私钥放在自己手里，用来解密和签名，这两个事情都明显是个人的私密行为，因此私钥 公钥发布出去，用来加密和验签，这两个事情明显是个人对外的公开行为，因此公钥 我们的“完美”方案 # 到这里，我们就可以给王叔和其他的国王们一个比较完美的 RSA 方案\n每个国王自己做一套公钥私钥 把自己的私钥保存好，把公钥送给其他的每个国王， 同时也保存好其他国王送过来的公钥，名称不能搞乱了，更不能让别人替换了 发邮件的过程如下： 把邮件的正文用自己的私钥签名，附在邮件最后 用接受者的公钥加密邮件的正文和签名 把信送过去 收邮件的过程如下： 用自己的私钥解密，获得邮件正文和签名 拿着发送者的公钥，对正文和签名进行验签 把自己的公钥送到别人那边，要包装一下，那个就是公证书\n自己的私钥也要好好的包装一下，这个就是私证书\n证书和证书链 # 证书的烦恼 # 假设国王们已经建立了证书体系，这个时候，某个国王英年早逝，他的弟弟扶持？/挟持？年幼的侄子摄政。很不幸的是，暴毙的国王把象征权力的个人私证书和其他国王的公证书都付之一炬。\n于是摄政王需要自己做一套公钥私钥，然后派信使去见每个国王，送去自己的公钥，拿回来对方的公钥。这是一个痛苦的折磨，对于所有涉及到的人：\n对方国王怎么相信你？\n突然证书说换就换，我是不是要跑一趟去求证一下？ 信使会不会有问题？\n信使是很乐意用自己的公证书来代表国王的公证书。他做两个证书，左手的代表国王 A 把公证书给国王 B，右手的代表国王 B 把公证书给国王 A，用不着第三个代表证书，他就可以作为中间人来偷看+篡改来回的信件了。 就实际情况来说，最安全的方式是，其他的国王亲自过去，验证新王，交换证书。\n但是很明显这是一个成本很巨大的工作，而且很危险。说不定回家的半路上，哪个国王头疼脑热的，也英年早逝了，然后大家又要重新跑一趟，包括那个宝座还没坐热的摄政王。\n注意：即使有了 internet，这样的场景下证书交换是不能通过网络的，因为信任问题没能解决，只能亲自跑。\n证书的本质就是信任 # 好在国王们都相信一个人——代表了神的意志的教皇。教皇说：你们也别这样跑，来回折腾。我给你们的公证书上面加上主的签名——当然也就是我代表主的签名，以后大家就只要如此如此：\n保存一个公证书，那就是主的证书，只要相信他就可以了，这个是一切证书之根； 你们所有人的公证书，都要经过主的签名 你发给别人带有签名的信件的时候，也要把你的公证书带上——记得哦，是主给你签名的公证书 当别人收到你的信件、签名、公证书时候， 他首先要根据主留给他的公证书来验签你的公证书是不是经过主认证的， 再用你的证书来验证信件的签名 更完美的方案 # 用一个证书给别的证书做签名背书，产生了证书链，就解决了证书的 N*N 问题。\nBTW：当某个国王派出军队出去征服新世界的时候，教皇一般也会派出一个大主教，授予他一个特殊的证书，这个证书是由教皇的证书签名，但是它还可以给别的证书签名。\n——这个就是多级授权。\n其它 # 摘要 MessageDigest 不是加密 # 也不是所谓的“单向加密”，就是消息摘要\nMAC # message authentication code，一般不用。\n","date":"2018-10-22","externalUrl":null,"permalink":"/posts/an-quan-xian-che/","section":"文章","summary":"这个虽然是闲扯淡的，但是看的时候请抓牢：我们一切以业务方案为目的，一些关键名词和概念还是很认真。加密签名 一个故事 这是从别人那边听来的故事，作为安全知识的入门非常有意义。1. 一个国王英年早逝，留下年幼的王子，国王的弟弟就做了摄政","title":"安全闲扯","type":"posts"},{"content":"","date":"2018-10-22","externalUrl":null,"permalink":"/tags/%E6%80%9D%E8%80%83/","section":"标签","summary":"","title":"思考","type":"tags"},{"content":"📌 本文原发布于掘金社区：Filter 设计模式编码实践\n原文地址：haifeiWu 和他朋友们的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n最近项目中遇到各种输出数据监控，数据校验等逻辑，一个个实现很是麻烦。项目是中途接手的，不是很熟悉，偶然一天发现项目中对 Filter 的使用扩展起来很是方便，所以，今天楼主来分享下，也为自己学习做个记录。下面我们从三方面来阐述。\n什么是 Filter # Filter 在设计模式里面被称为责任链设计模式，顾名思义，我们可以在这条责任链上对一组数据做不同的处理。这种类型的设计模式属于结构型模式，它结合多个标准来获得单一标准。UML 见下图，\n为什么要使用 Filter # 好处是显而易见的，它使我们的代码将请求和处理分开。请求者可以不知道是谁处理的，处理者可以不用知道请求的全貌，两者解耦，提高系统的灵活性。从而我们的代码更加简洁跟易于扩展，而不是机械重复的 Ctrl+C，Ctrl+V。当然好处还有好多，楼主就不在这里赘述了，感兴趣的小伙伴自行 Google。\n怎么用 Filter 项目中的代码实现逻辑 # 定义 Filter 接口，接口中定义数据处理的方法。\npublic interface IDataHandlerFilter { void filter(DataPackage dataPackage); } 统一数据发送端，将业务系统处理好的数据，统一发送到 Kafka。当然我们还可以实现 Filter 对数据进行其他处理。\npublic class DataSendHandlerFilter implements IDataHandlerFilter { public static final Logger log = LogManager.getLogger(DataSendHandlerFilter.class); private int logCenterType; //数据源类型 0-实时数据 1-wifi数据 private String resourceType = StringUtils.isBlank(Repository.getCityConfig().getResourceType()) ? \u0026#34;0\u0026#34; : Repository.getCityConfig().getResourceType(); public DataSendHandlerFilter() { logCenterType = Repository.getSysConfig().getLogCenterType(); //初始化kafka if (logCenterType == Constant.LogcenterType.KAFKA){ KafkaProducerHelper.init(Repository.getCityConfig().getCityId(), Repository.getSysConfig()); log.info(\u0026#34;初始化kafka\u0026#34;); } } @Override public void filter(DataPackage dataPackage) { GpsData gpsData = dataPackage.getTargetData(); /*重复数据和时间格式错误数据不发送*/ if (null != gpsData \u0026amp;\u0026amp; !gpsData.isError() \u0026amp;\u0026amp; logCenterType == Constant.LogcenterType.KAFKA) { if (gpsData.isGps()) { KafkaProducerHelper.sendData(gpsData.toGpsStr(resourceType)); } if (gpsData.isStn()) { KafkaProducerHelper.sendData(gpsData.toStnStr(resourceType)); } } } } 设置系统要使用的 Filter，根据具体业务有所不同。\npublic class HanderFilterUtil { private static List\u0026lt;IDataHandlerFilter\u0026gt; list; /** * 这个是有先后顺序的 * @return */ public static List\u0026lt;IDataHandlerFilter\u0026gt; getDefaultFilter(SysConfig sysConfig, CityConfig cityConfig){ if (null == list){ list = new ArrayList\u0026lt;\u0026gt;(); } //默认提供接收日志、重复校验、时间格式校验、属性校验、数据转发过滤器 list.add(new RepeatHandlerFilter()); list.add(new DataLogHandlerFilter()); list.add(new DataSendHandlerFilter()); // ...... return list; } } 最后我们通过调用 getDefaultFilter 方法来决定我们系统中使用哪几种 Filter 来处理数据。\n小结 # 本文中的代码不能直接运行，只是提供一种写代码的思路，小伙伴遇到此种场景可以借鉴一下。\n关注我们 ","date":"2018-09-21","externalUrl":null,"permalink":"/posts/filter-shejimoshibianmashijian/","section":"文章","summary":"最近项目中遇到各种输出数据监控，数据校验等逻辑，一个个实现很是麻烦。项目是中途接手的，不是很熟悉，偶然一天发现项目中对 Filter 的使用扩展起来很是方便，所以，今天楼主来分享下，也为自己学习做个记录。下面我们从三方面来阐述。Filter 在设计模式里面被称为责任链设计模式…","title":"Filter 设计模式编码实践","type":"posts"},{"content":"","date":"2018-09-21","externalUrl":null,"permalink":"/tags/google/","section":"标签","summary":"","title":"Google","type":"tags"},{"content":"","date":"2018-09-04","externalUrl":null,"permalink":"/tags/github/","section":"标签","summary":"","title":"GitHub","type":"tags"},{"content":"📌 本文原发布于掘金社区：造个轮子之基于 Netty 实现自己的 RPC 框架\n原文地址：haifeiWu 和他朋友们的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n服务端开发都会或多或少地涉及到 RPC 的使用，当然如果止步于会用，对自己的成长很是不利，所以楼主今天本着知其然，且知其所以然的精神来探讨一下 RPC 这个东西。\nchild-rpc 模型 # child-rpc 采用 socket 直连的方式来实现服务的远程调用，然后使用 jdk 动态代理的方式让调用者感知不到远程调用。\nchild-rpc 开箱使用 # 发布服务 # RPC 服务类要监听指定 IP 端口，设置要发布的服务的实现及其接口的引用，并指定序列化的方式，目前 child-rpc 支持 Hessian、JACKSON 两种序列化方式。\n/** * @author wuhf * @Date 2018/9/1 18:30 **/ public class ServerTest { public static void main(String[] args) { ServerConfig serverConfig = new ServerConfig(); serverConfig.setSerializer(Serializer.SerializeEnum.HESSIAN.serializer) .setPort(5201) .setInterfaceId(HelloService.class.getName()) .setRef(HelloServiceImpl.class.getName()); ServerProxy serverProxy = new ServerProxy(new NettyServer(),serverConfig); try { serverProxy.export(); while (true){ } } catch (Exception e) { e.printStackTrace(); } } } 引用服务 # RPC 客户端要连接远程 IP 端口，并注册要引用的服务，然后调用 sayHi 方法，输出结果\n/** * @author wuhf * @Date 2018/9/1 18:31 **/ public class ClientTest { public static void main(String[] args) { ClientConfig clientConfig = new ClientConfig(); clientConfig.setHost(\u0026#34;127.0.0.1\u0026#34;) .setPort(5201) .setTimeoutMillis(100000) .setSerializer(Serializer.SerializeEnum.HESSIAN.serializer); ClientProxy clientProxy = new ClientProxy(clientConfig,new NettyClient(),HelloService.class); for (int i = 0; i \u0026lt; 10; i++) { HelloService helloService = (HelloService) clientProxy.refer(); System.out.println(helloService.sayHi()); } } } 运行 # server 端输出\nclient 端输出\nchild-rpc 具体实现 # RPC 请求，响应消息实体定义 # 定义消息请求响应格式，消息类型、消息唯一 ID 和消息的 JSON 序列化字符串内容。消息唯一 ID 是用来让客户端验证服务器请求和响应是否匹配的。\n// rpc 请求 public class RpcRequest implements Serializable { private static final long serialVersionUID = -4364536436151723421L; private String requestId; private long createMillisTime; private String className; private String methodName; private Class\u0026lt;?\u0026gt;[] parameterTypes; private Object[] parameters; // set get 方法省略掉 } // rpc 响应 public class RpcResponse implements Serializable { private static final long serialVersionUID = 7329530374415722876L; private String requestId; private Throwable error; private Object result; // set get 方法省略掉 } 网络传输过程中的编码解码 # 消息编码解码使用自定义的编解码器，根据服务初始化时使用的序列化器将数据序列化成字节流，拆包的策略是设定指定长度的数据包，对 socket 粘包、拆包感兴趣的小伙伴请移步 Socket 中粘包问题浅析及其解决方案\n下面是解码器代码实现：\npublic class NettyDecoder extends ByteToMessageDecoder { private Class\u0026lt;?\u0026gt; genericClass; private Serializer serializer; public NettyDecoder(Class\u0026lt;?\u0026gt; genericClass, Serializer serializer) { this.genericClass = genericClass; this.serializer = serializer; } @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List\u0026lt;Object\u0026gt; list) throws Exception { if (byteBuf.readableBytes() \u0026lt; 4) { return; } byteBuf.markReaderIndex(); // 读取消息长度 int dataLength = byteBuf.readInt(); if (dataLength \u0026lt; 0) { channelHandlerContext.close(); } if (byteBuf.readableBytes() \u0026lt; dataLength) { byteBuf.resetReaderIndex(); return; } try { byte[] data = new byte[dataLength]; byteBuf.readBytes(data); Object object = serializer.deserialize(data,genericClass); list.add(object); } catch (Exception e) { e.printStackTrace(); } } } 下面是编码器的实现：\npublic class NettyEncoder extends MessageToByteEncoder\u0026lt;Object\u0026gt; { private Class\u0026lt;?\u0026gt; genericClass; private Serializer serializer; public NettyEncoder(Class\u0026lt;?\u0026gt; genericClass,Serializer serializer) { this.serializer = serializer; this.genericClass = genericClass; } @Override protected void encode(ChannelHandlerContext channelHandlerContext, Object object, ByteBuf byteBuf) throws Exception { if (genericClass.isInstance(object)) { byte[] data = serializer.serialize(object); byteBuf.writeInt(data.length); byteBuf.writeBytes(data); } } } RPC 业务逻辑处理 handler # server 端业务处理 handler 实现 : 主要业务逻辑是 通过 Java 的反射实现方法的调用。\npublic class NettyServerHandler extends SimpleChannelInboundHandler\u0026lt;RpcRequest\u0026gt; { private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class); @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcRequest rpcRequest) throws Exception { // invoke 通过调用反射方法获取 rpcResponse RpcResponse response = RpcInvokerHandler.invokeService(rpcRequest); channelHandlerContext.writeAndFlush(response); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { logger.error(\u0026#34;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; child-rpc provider netty server caught exception\u0026#34;, cause); ctx.close(); } } public class RpcInvokerHandler { public static Map\u0026lt;String, Object\u0026gt; serviceMap = new HashMap\u0026lt;String, Object\u0026gt;(); public static RpcResponse invokeService(RpcRequest request) throws ClassNotFoundException, IllegalAccessException, InstantiationException { Object serviceBean = serviceMap.get(request.getClassName()); RpcResponse response = new RpcResponse(); response.setRequestId(request.getRequestId()); try { Class\u0026lt;?\u0026gt; serviceClass = serviceBean.getClass(); String methodName = request.getMethodName(); Class\u0026lt;?\u0026gt;[] parameterTypes = request.getParameterTypes(); Object[] parameters = request.getParameters(); Method method = serviceClass.getMethod(methodName, parameterTypes); method.setAccessible(true); Object result = method.invoke(serviceBean, parameters); response.setResult(result); } catch (Throwable t) { t.printStackTrace(); response.setError(t); } return response; } } client 端主要业务实现是等待 server 返回响应。代码比较简单就不贴代码了，详情请看下面给出的 github 链接。\nRPC 服务端与客户端启动 # 服务端与客户端启动都是 Netty 的模板代码，因为篇幅原因就不贴出来了，感兴趣的伙伴请移步 造个轮子\u0026mdash;RPC 动手实现。\n小结 # 因为只是为了理解 RPC 的本质，所以在实现细节上还有好多没有仔细去雕琢的地方。不过 RPC 的目的就是允许像调用本地服务一样调用远程服务，对调用者透明，于是我们使用了动态代理。并使用 Netty 的 handler 发送数据和响应数据，总的来说该框架实现了简单的 RPC 调用。代码比较简单，主要是思路，以及了解 RPC 底层的实现。\n参考文章 # 造个轮子\u0026mdash;RPC 动手实现 Socket 中粘包问题浅析及其解决方案 关注我们 ","date":"2018-09-04","externalUrl":null,"permalink":"/posts/zaogelunzizhijiyu-netty-shixianzijide-rpc-kuangjia/","section":"文章","summary":"服务端开发都会或多或少的涉及到 RPC 的使用，当然如果止步于会用，对自己的成长很是不利，所以楼主今天本着知其然，且知其所以然的精神来探讨一下 RPC 这个东西。child-rpc 采用 socket 直连的方式来实现服务的远程调用，然后使用 jdk 动态代理的方式让调用者感…","title":"造个轮子之基于 Netty 实现自己的 RPC 框架","type":"posts"},{"content":"","date":"2018-08-16","externalUrl":null,"permalink":"/tags/apache-storm/","section":"标签","summary":"","title":"Apache Storm","type":"tags"},{"content":"","date":"2018-08-16","externalUrl":null,"permalink":"/tags/logstash/","section":"标签","summary":"","title":"Logstash","type":"tags"},{"content":"📌 本文原发布于掘金社区：高性能无锁队列 Disruptor 初体验\n原文地址：haifeiWu 和他朋友们的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n最近一直在研究队列的一些问题，今天楼主要分享一个高性能的队列 Disruptor。\nwhat Disruptor ? # 它是英国外汇交易公司 LMAX 开发的一个高性能队列，研发的初衷是解决内存队列的延迟问题。基于 Disruptor 开发的系统单线程能支撑每秒600万订单。\n目前，包括 Apache Storm、Log4j2 在内的很多知名项目都应用了 Disruptor 以获取高性能。在楼主公司内部使用 Disruptor 与 Netty 结合用来做 GPS 实时数据的处理，性能相当强悍。本文从实战角度来大概了解一下 Disruptor 的实现原理。\nwhy Disruptor ? # Disruptor 通过以下设计来解决队列速度慢的问题：\n环形数组结构 为了避免垃圾回收，采用数组而非链表。因为，数组对处理器的缓存机制更加友好。\\ 元素位置定位 数组长度 2^n，通过位运算，加快定位的速度。下标采取递增的形式。不用担心 index 溢出的问题。index 是 long 类型，即使 100万 QPS 的处理速度，也需要 30万年才能用完。\\ 无锁设计 每个生产者或者消费者线程，会先申请可以操作的元素在数组中的位置，申请到之后，直接在该位置写入或者读取数据。\\ 针对伪共享问题的优化 Disruptor 消除这个问题，至少对于缓存行大小是 64 字节或更少的处理器架构来说是这样的（有可能处理器的缓存行是 128 字节，那么使用 64 字节填充还是会存在伪共享问题），通过增加补全来确保 ring buffer 的序列号不会和其他东西同时存在于一个缓存行中。 how Disruptor ? # 通过上面的介绍，我们大概可以了解到 Disruptor 是一个高性能的无锁队列，那么该如何使用呢，下面楼主通过 Disruptor 实现一个简单的生产者消费者模型，介绍 Disruptor 的使用\n首先，根据 Disruptor 的事件驱动的编程模型，我们需要定义一个事件来携带数据。\npublic class DataEvent { private long value; public void set(long value) { this.value = value; } public long getValue() { return value; } } 为了让 Disruptor 为我们预先分配这些事件，我们需要构造一个 EventFactory\npublic class DataEventFactory implements EventFactory\u0026lt;DataEvent\u0026gt; { @Override public DataEvent newInstance() { return new DataEvent(); } } 一旦我们定义了事件，我们需要创建一个处理这些事件的消费者。在我们的例子中，我们要做的就是从控制台中打印出值。\npublic class DataEventHandler implements EventHandler\u0026lt;DataEvent\u0026gt; { @Override public void onEvent(DataEvent dataEvent, long l, boolean b) throws Exception { new DataEventConsumer(dataEvent); } } 接下来我们需要初始化 Disruptor，并定义一个生产者来生成消息\npublic class DisruptorManager { private final static Logger LOG = LoggerFactory.getLogger(DisruptorManager.class); /*消费者线程池*/ private static ExecutorService threadPool; private static Disruptor\u0026lt;DataEvent\u0026gt; disruptor; private static RingBuffer\u0026lt;DataEvent\u0026gt; ringBuffer; private static AtomicLong dataNum = new AtomicLong(); public static void init(EventHandler\u0026lt;DataEvent\u0026gt; eventHandler) { //初始化disruptor threadPool = Executors.newCachedThreadPool(); disruptor = new Disruptor\u0026lt;\u0026gt;(new DataEventFactory(), 8 * 1024, threadPool, ProducerType.MULTI, new BlockingWaitStrategy()); ringBuffer = disruptor.getRingBuffer(); disruptor.handleEventsWith(eventHandler); disruptor.start(); new Timer().schedule(new TimerTask() { @Override public void run() { LOG.info(\u0026#34;放入队列中数据编号{},队列剩余空间{}\u0026#34;, dataNum.get(), ringBuffer.remainingCapacity()); } }, new Date(), 60 * 1000); } /** * * @param message */ public static void putDataToQueue(long message) { if (dataNum.get() == Long.MAX_VALUE) { dataNum.set(0L); } // 往队列中加事件 long next = ringBuffer.next(); try { ringBuffer.get(next).set(message); dataNum.incrementAndGet(); } catch (Exception e) { LOG.error(\u0026#34;向RingBuffer存入数据[{}]出现异常=\u0026gt;{}\u0026#34;, message, e.getStackTrace()); } finally { ringBuffer.publish(next); } } public static void close() { threadPool.shutdown(); disruptor.shutdown(); } } 最后我们来定义一个 Main 方法来执行代码\npublic class EventMain { public static void main(String[] args) throws Exception { DisruptorManager.init(new DataEventHandler()); for (long l = 0; true; l++) { DisruptorManager.putDataToQueue(l); Thread.sleep(1000); } } } 对上面代码具体感兴趣的小伙伴请移步 github.com/haifeiWu/di…\n然后我们看下控制台打印出来的数据\n小结 # Disruptor 通过精巧的无锁设计实现了在高并发情形下的高性能。\n另外在 Log4j 2 中的异步模式采用了 Disruptor 来处理。在这里楼主遇到一个小问题，就是在使用 Log4j 2 通过 TCP 模式往 logstash 发日志数据的时候，由于网络问题导致连接中断，从而导致 Log4j 2 不停地往 ringbuffer 中写数据，ringbuffer 数据没有消费者，导致服务器内存跑满。解决方案是设置 Log4j 2 中 Disruptor 队列有界，或者换成 UDP 模式来写日志数据（如果数据不重要的话）。\n参考链接 # 高性能无锁队列 Disruptor 初体验 剖析 Disruptor:为什么会这么快？(二)神奇的缓存行填充 高性能队列——Disruptor ","date":"2018-08-16","externalUrl":null,"permalink":"/posts/gaoxingnengwusuoduilie-disruptor-chutiyan/","section":"文章","summary":"最近一直在研究队列的一些问题，今天楼主要分享一个高性能的队列 Disruptor。它是英国外汇交易公司 LMAX 开发的一个高性能队列，研发的初衷是解决内存队列的延迟问题。基于 Disruptor 开发的系统单线程能支撑每秒600万订单。目前，包括 Apache Stor…","title":"高性能无锁队列 Disruptor 初体验","type":"posts"},{"content":"📌 本文原发布于掘金社区：炮打 TCP – 关于一而再再而三的粘包拆包问题的大字报\nTCP 所谓的粘包和拆包问题，是技术圈里最奇葩的问题之一！\n一而再，再而三，就跟傻逼的中国球迷支持中国足球队一样，前赴后继。有时候同一个人多次犯同一个错误，有时候是前脚一个犯错了后脚又来一个还犯同样的错。即使是最优秀的程序员，也会在这个问题上面栽跟头，思维甚至很难转过弯，很久才能意识到自己的错误。而低水平的程序员就更不用说了，很多人到死都没有理解这个错误并解决掉，只是逃掉了而已。\n我们固然可以认为原因是某些人学艺不精，但那么多的人，其中包括无数的优秀程序员在 TCP 粘包和拆包问题上犯错误，难道我们不能说，这其实是 TCP 自身的原因吗？\n在我看来，这个问题的出现，原因就在于 TCP 协议是有原罪的 \u0026ndash; 也就是 TCP 协议所谓的“流式”协议。所以，我要炮轰 TCP！\n经过几十年的验证，除了极少数几个网络协议会用到 TCP 所谓的流式特性之外，没有任何应用协议使用流式特性。我们必须承认，所有的应用层协议都是基于报文的协议，而不是流式协议。而某些名字中带有”流（Stream）\u0026ldquo;字样的协议，如 RTP，流媒体等，其本质是无数小体积的报文按顺序拼接而成，根本就和 TCP 的流式没有任何关系！\n那么我们就可以确定，数据的本质是报文，流数据是某类报文数据的一种伪称。事实上，TCP 的流就是基于 IP 报文的。\n因为“流”是一个伪抽象的概念，所以流式协议是违反人的天性和事物的内在逻辑的。万物的本质是报文。正因为如此，”流”所引出的粘包拆包问题，就必然会一而再，再而三，大量地出现。\n炮轰之后，我们要怎么解决问题呢？\n由于 TCP 协议已经成为事实上的基础，所以淘汰掉 TCP 是不可想象的。我们要做的是，找到正确的编程代码，解决粘包拆包问题。经过无数人的探索，以及无数人一次又一次重复的愚蠢错误的反证，我发现解决 TCP 粘包和拆包问题只有一条路径，没有第二条！我断言，所有和我的解决方案不同的代码，都是错误的。\n彻底解决 TCP 粘包和拆包问题的代码架构如下：\nchar tmp[]; Buffer buffer; // 网络循环：必须在一个循环中读取网络，因为网络数据是源源不断的。 while(1){ // 从TCP流中读取不定长度的一段流数据，不能保证读到的数据是你期望的长度 tcp.read(tmp); // 将这段流数据和之前收到的流数据拼接到一起 buffer.append(tmp); // 解析循环：必须在一个循环中解析报文，应对所谓的粘包 while(1){ // 尝试解析报文 msg = parse(buffer); if(!msg){ // 报文还没有准备好，糟糕，我们遇到拆包了！跳出解析循环，继续读网络。 break; } // 将解析过的报文对应的流数据清除 buffer.remove(msg.length); // 业务处理 process(msg); } } 这段代码是终极地解决 TCP 粘包和拆包问题的代码！\n这段代码之所以正确，是因为它包含了两个循环：网络循环和解析循环。\n网络循环用于从 TCP socket 中读取流式数据，每一次读取到的数据的长度是不可预期的，也就是，读取到的数据长短不一，无法保证，这就是所谓“流式”引出的问题。\n而解析循环的功能是从拼接后的流数据中，尝试解析出多个报文。注意，是多个报文，不是一个。因为所谓的粘包问题存在，所以可能是多个，而不是一个。如果解析不成功，那说明是遇到了拆包问题，我们继续读网络数据。\n你只需要死记硬背上面的正确代码即可。不死记硬背也一样，最终你还是要得出和我相同的结论，写出和我一样的代码。那么，何不现在就死记硬背呢？\n最后，附上经典的错误代码：\ntcp.read(tmp, HEADER_LEN); header = parse_header(tmp); tcp.read(tmp, header.body_len); body = parse(tmp); 这样的代码当然是错误的，这么简单的代码怎么可能是对的？如果对了，TCP 还是 TCP 吗？\n如果你认为本文有用，请关注这个 GitHub 项目：github.com/ideawu/FUCK… 让更多人一起炮打 TCP！\n该项目还提供了模拟粘包和拆包的代码，你如果不信邪，可以写一个自己的 client 试试。\nRelated posts: # 关于 TCP 粘包和拆包的终极解答 经典的 TCP socket 读取报文错误 通过 HTTP POST 发送二进制数据 在 Linux 进行 IO 的正确姿势 使用 Channel 进行可靠传输 ","date":"2018-08-10","externalUrl":null,"permalink":"/posts/paodatcp-guanyuyierzaizaiersandezhanbaochaibaowentidedazibao/","section":"文章","summary":"TCP 所谓的粘包和拆包问题，是技术圈里最奇葩的问题之一！一而再，再而三，就跟傻逼的中国球迷支持中国足球队一样，前赴后继。有时候同一个人多次在犯同一个错误，有时候是前脚一个犯错了后脚又来一个还犯同样的错。","title":"炮打 TCP – 关于一而再再而三的粘包拆包问题的大字报","type":"posts"},{"content":"📌 本文原发布于掘金社区：Netty 源码中对 Redis 协议的实现\n原文地址：haifeiWu 的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n近期一直在做网络协议相关的工作，所以博客也就与之相关的比较多，今天楼主结合 Redis 的协议 RESP 看看在 Netty 源码中是如何实现的。\nRESP 协议 # RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议，优势在于实现非常简单，解析性能极好。\nRedis 协议将传输的结构数据分为 5 种最小单元类型，单元结束时统一加上回车换行符号\\r\\n，来表示该单元的结束。\n单行字符串 以 + 符号开头。 多行字符串 以 $ 符号开头，后跟字符串长度。 整数值 以 : 符号开头，后跟整数的字符串形式。 错误消息 以 - 符号开头。 数组 以 * 号开头，后跟数组的长度。 关于 RESP 协议的具体介绍感兴趣的小伙伴请移步楼主的另一篇文章Redis 协议规范（译文）\nNetty 中 RESP 协议的定义 # 如下面代码中所表示的，Netty 中使用对应符号的 ASCII 码来表示，感兴趣的小伙伴可以查一下 ASCII 码表来验证一下。\npublic enum RedisMessageType { // 以 + 开头的单行字符串 SIMPLE_STRING((byte)43, true), // 以 - 开头的错误信息 ERROR((byte)45, true), // 以 : 开头的整型数据 INTEGER((byte)58, true), // 以 $ 开头的多行字符串 BULK_STRING((byte)36, false), // 以 * 开头的数组 ARRAY_HEADER((byte)42, false), ARRAY((byte)42, false); private final byte value; private final boolean inline; private RedisMessageType(byte value, boolean inline) { this.value = value; this.inline = inline; } public byte value() { return this.value; } public boolean isInline() { return this.inline; } public static RedisMessageType valueOf(byte value) { switch(value) { case 36: return BULK_STRING; case 42: return ARRAY_HEADER; case 43: return SIMPLE_STRING; case 45: return ERROR; case 58: return INTEGER; default: throw new RedisCodecException(\u0026#34;Unknown RedisMessageType: \u0026#34; + value); } } } Netty 中 RESP 解码器实现 # 解码器，顾名思义，就是将服务器返回的数据根据协议反序列化成易于阅读的信息。RedisDecoder 就是根据 RESP 将服务端返回的信息反序列化出来。下面是指令的编码格式\nSET key value =\u0026gt; *3\\r\\n$5\\r\\nSET\\r\\n$1\\r\\nkey\\r\\n$1\\r\\nvalue\\r\\n 指令是一个字符串数组，编码一个字符串数组，首先需要编码数组长度*3\\r\\n。然后依次编码各个字符串参数。编码字符串首先需要编码字符串的长度$5\\r\\n。然后再编码字符串的内容 SET\\r\\n。Redis 消息以\\r\\n作为分隔符，这样设计其实挺浪费网络传输流量的，消息内容里面到处都是\\r\\n符号。但是这样的消息可读性会比较好，便于调试。RESP 协议是牺牲性能换取可读，易于实现的一个经典例子。\n指令解码器的实现，Socket 读取网络字节流的时候存在拆包问题。所谓拆包问题是指一次 Read 调用从 Socket 读到的字节数组可能只是一个完整消息的一部分。而另外一部分则需要发起另外一次 Read 调用才可能读到，甚至要发起多个 Read 调用才可以读到完整的一条消息。对于拆包问题感兴趣的小伙伴可以查看楼主的另一篇文章TCP 粘包问题浅析及其解决方案\n如果我们拿部分消息去反序列化成输入消息对象肯定是要失败的，或者说生成的消息对象是不完整的。这个时候我们需要等待下一次 Read 调用，然后将这两次 Read 调用的字节数组拼起来，尝试再一次反序列化。\n问题来了，如果一个输入消息对象很大，就可能需要多个 Read 调用和多次反序列化操作才能完整地解包出一个输入对象。那这个反序列化的过程就会重复多次。\n针对这个问题，Netty 中很巧妙地解决了这个问题，如下所示，Netty 中通过 state 属性来保存当前序列化的状态，然后下次反序列化的时候就可以从上次记录的 state 直接继续反序列化。这样就避免了重复的问题。\n// 保持当前序列化状态的字段 private RedisDecoder.State state; public RedisDecoder() { this(65536, FixedRedisMessagePool.INSTANCE); } public RedisDecoder(int maxInlineMessageLength, RedisMessagePool messagePool) { this.toPositiveLongProcessor = new RedisDecoder.ToPositiveLongProcessor(); // 默认初始化状态为，反序列化指令类型 this.state = RedisDecoder.State.DECODE_TYPE; if (maxInlineMessageLength \u0026gt; 0 \u0026amp;\u0026amp; maxInlineMessageLength \u0026lt;= 536870912) { this.maxInlineMessageLength = maxInlineMessageLength; this.messagePool = messagePool; } else { throw new RedisCodecException(\u0026#34;maxInlineMessageLength: \u0026#34; + maxInlineMessageLength + \u0026#34; (expected: \u0026lt;= \u0026#34; + 536870912 + \u0026#34;)\u0026#34;); } } // 解码器的主要业务逻辑 protected void decode(ChannelHandlerContext ctx, ByteBuf in, List\u0026lt;Object\u0026gt; out) throws Exception { try { // 循环读取信息，将信息完成的序列化 while(true) { switch(this.state) { case DECODE_TYPE: if (this.decodeType(in)) { break; } return; case DECODE_INLINE: if (this.decodeInline(in, out)) { break; } return; case DECODE_LENGTH: if (this.decodeLength(in, out)) { break; } return; case DECODE_BULK_STRING_EOL: if (this.decodeBulkStringEndOfLine(in, out)) { break; } return; case DECODE_BULK_STRING_CONTENT: if (this.decodeBulkStringContent(in, out)) { break; } return; default: throw new RedisCodecException(\u0026#34;Unknown state: \u0026#34; + this.state); } } } catch (RedisCodecException var5) { this.resetDecoder(); throw var5; } catch (Exception var6) { this.resetDecoder(); throw new RedisCodecException(var6); } } 下面的代码，是针对每种数据类型进行反序列化的具体业务逻辑。有小伙伴可能会想，没有看到解码数组类型的逻辑呢？实际上在 RESP 协议中数组就是其他类型的组合，所以完全可以循环读取，按照单个元素解码。\n// 解码消息类型 private boolean decodeType(ByteBuf in) throws Exception { if (!in.isReadable()) { return false; } else { this.type = RedisMessageType.valueOf(in.readByte()); this.state = this.type.isInline() ? RedisDecoder.State.DECODE_INLINE : RedisDecoder.State.DECODE_LENGTH; return true; } } // 解码单行字符串，错误信息，或者整型数据类型 private boolean decodeInline(ByteBuf in, List\u0026lt;Object\u0026gt; out) throws Exception { ByteBuf lineBytes = readLine(in); if (lineBytes == null) { if (in.readableBytes() \u0026gt; this.maxInlineMessageLength) { throw new RedisCodecException(\u0026#34;length: \u0026#34; + in.readableBytes() + \u0026#34; (expected: \u0026lt;= \u0026#34; + this.maxInlineMessageLength + \u0026#34;)\u0026#34;); } else { return false; } } else { out.add(this.newInlineRedisMessage(this.type, lineBytes)); this.resetDecoder(); return true; } } // 解码消息长度 private boolean decodeLength(ByteBuf in, List\u0026lt;Object\u0026gt; out) throws Exception { ByteBuf lineByteBuf = readLine(in); if (lineByteBuf == null) { return false; } else { long length = this.parseRedisNumber(lineByteBuf); if (length \u0026lt; -1L) { throw new RedisCodecException(\u0026#34;length: \u0026#34; + length + \u0026#34; (expected: \u0026gt;= \u0026#34; + -1 + \u0026#34;)\u0026#34;); } else { switch(this.type) { case ARRAY_HEADER: out.add(new ArrayHeaderRedisMessage(length)); this.resetDecoder(); return true; case BULK_STRING: if (length \u0026gt; 536870912L) { throw new RedisCodecException(\u0026#34;length: \u0026#34; + length + \u0026#34; (expected: \u0026lt;= \u0026#34; + 536870912 + \u0026#34;)\u0026#34;); } this.remainingBulkLength = (int)length; return this.decodeBulkString(in, out); default: throw new RedisCodecException(\u0026#34;bad type: \u0026#34; + this.type); } } } } // 解码多行字符串 private boolean decodeBulkString(ByteBuf in, List\u0026lt;Object\u0026gt; out) throws Exception { switch(this.remainingBulkLength) { case -1: out.add(FullBulkStringRedisMessage.NULL_INSTANCE); this.resetDecoder(); return true; case 0: this.state = RedisDecoder.State.DECODE_BULK_STRING_EOL; return this.decodeBulkStringEndOfLine(in, out); default: out.add(new BulkStringHeaderRedisMessage(this.remainingBulkLength)); this.state = RedisDecoder.State.DECODE_BULK_STRING_CONTENT; return this.decodeBulkStringContent(in, out); } } Netty 中 RESP 编码器实现 # 编码器，顾名思义，就是将对象根据 RESP 协议序列化成字节流发送到服务端。编码器的实现非常简单，不用考虑拆包等问题，就是分配一个 ByteBuf，然后将消息输出对象序列化的字节数组塞到 ByteBuf 中输出就可以了。\n下面代码中就是 encode 方法直接调用 writeRedisMessage 方法，根据消息类型进行写 buffer 操作。\n@Override protected void encode(ChannelHandlerContext ctx, RedisMessage msg, List\u0026lt;Object\u0026gt; out) throws Exception { try { writeRedisMessage(ctx.alloc(), msg, out); } catch (CodecException e) { throw e; } catch (Exception e) { throw new CodecException(e); } } private void writeRedisMessage(ByteBufAllocator allocator, RedisMessage msg, List\u0026lt;Object\u0026gt; out) { // 判断消息类型，然后调用写相应消息的方法。 if (msg instanceof InlineCommandRedisMessage) { writeInlineCommandMessage(allocator, (InlineCommandRedisMessage) msg, out); } else if (msg instanceof SimpleStringRedisMessage) { writeSimpleStringMessage(allocator, (SimpleStringRedisMessage) msg, out); } else if (msg instanceof ErrorRedisMessage) { writeErrorMessage(allocator, (ErrorRedisMessage) msg, out); } else if (msg instanceof IntegerRedisMessage) { writeIntegerMessage(allocator, (IntegerRedisMessage) msg, out); } else if (msg instanceof FullBulkStringRedisMessage) { writeFullBulkStringMessage(allocator, (FullBulkStringRedisMessage) msg, out); } else if (msg instanceof BulkStringRedisContent) { writeBulkStringContent(allocator, (BulkStringRedisContent) msg, out); } else if (msg instanceof BulkStringHeaderRedisMessage) { writeBulkStringHeader(allocator, (BulkStringHeaderRedisMessage) msg, out); } else if (msg instanceof ArrayHeaderRedisMessage) { writeArrayHeader(allocator, (ArrayHeaderRedisMessage) msg, out); } else if (msg instanceof ArrayRedisMessage) { writeArrayMessage(allocator, (ArrayRedisMessage) msg, out); } else { throw new CodecException(\u0026#34;unknown message type: \u0026#34; + msg); } } 下面代码主要是实现对应消息按照 RESP 协议 进行序列化操作，具体就是上面楼主说的，分配一个 ByteBuf，然后将消息输出对象序列化的字节数组塞到 ByteBuf 中输出即可。\nprivate static void writeInlineCommandMessage(ByteBufAllocator allocator, InlineCommandRedisMessage msg, List\u0026lt;Object\u0026gt; out) { writeString(allocator, RedisMessageType.INLINE_COMMAND, msg.content(), out); } private static void writeSimpleStringMessage(ByteBufAllocator allocator, SimpleStringRedisMessage msg, List\u0026lt;Object\u0026gt; out) { writeString(allocator, RedisMessageType.SIMPLE_STRING, msg.content(), out); } private static void writeErrorMessage(ByteBufAllocator allocator, ErrorRedisMessage msg, List\u0026lt;Object\u0026gt; out) { writeString(allocator, RedisMessageType.ERROR, msg.content(), out); } private static void writeString(ByteBufAllocator allocator, RedisMessageType type, String content, List\u0026lt;Object\u0026gt; out) { ByteBuf buf = allocator.ioBuffer(type.length() + ByteBufUtil.utf8MaxBytes(content) + RedisConstants.EOL_LENGTH); type.writeTo(buf); ByteBufUtil.writeUtf8(buf, content); buf.writeShort(RedisConstants.EOL_SHORT); out.add(buf); } private void writeIntegerMessage(ByteBufAllocator allocator, IntegerRedisMessage msg, List\u0026lt;Object\u0026gt; out) { ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH + RedisConstants.EOL_LENGTH); RedisMessageType.INTEGER.writeTo(buf); buf.writeBytes(numberToBytes(msg.value())); buf.writeShort(RedisConstants.EOL_SHORT); out.add(buf); } private void writeBulkStringHeader(ByteBufAllocator allocator, BulkStringHeaderRedisMessage msg, List\u0026lt;Object\u0026gt; out) { final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + (msg.isNull() ? RedisConstants.NULL_LENGTH : RedisConstants.LONG_MAX_LENGTH + RedisConstants.EOL_LENGTH)); RedisMessageType.BULK_STRING.writeTo(buf); if (msg.isNull()) { buf.writeShort(RedisConstants.NULL_SHORT); } else { buf.writeBytes(numberToBytes(msg.bulkStringLength())); buf.writeShort(RedisConstants.EOL_SHORT); } out.add(buf); } private static void writeBulkStringContent(ByteBufAllocator allocator, BulkStringRedisContent msg, List\u0026lt;Object\u0026gt; out) { out.add(msg.content().retain()); if (msg instanceof LastBulkStringRedisContent) { out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT)); } } private void writeFullBulkStringMessage(ByteBufAllocator allocator, FullBulkStringRedisMessage msg, List\u0026lt;Object\u0026gt; out) { if (msg.isNull()) { ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH + RedisConstants.EOL_LENGTH); RedisMessageType.BULK_STRING.writeTo(buf); buf.writeShort(RedisConstants.NULL_SHORT); buf.writeShort(RedisConstants.EOL_SHORT); out.add(buf); } else { ByteBuf headerBuf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH + RedisConstants.EOL_LENGTH); RedisMessageType.BULK_STRING.writeTo(headerBuf); headerBuf.writeBytes(numberToBytes(msg.content().readableBytes())); headerBuf.writeShort(RedisConstants.EOL_SHORT); out.add(headerBuf); out.add(msg.content().retain()); out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT)); } } /** * Write array header only without body. Use this if you want to write arrays as streaming. */ private void writeArrayHeader(ByteBufAllocator allocator, ArrayHeaderRedisMessage msg, List\u0026lt;Object\u0026gt; out) { writeArrayHeader(allocator, msg.isNull(), msg.length(), out); } /** * Write full constructed array message. */ private void writeArrayMessage(ByteBufAllocator allocator, ArrayRedisMessage msg, List\u0026lt;Object\u0026gt; out) { if (msg.isNull()) { writeArrayHeader(allocator, msg.isNull(), RedisConstants.NULL_VALUE, out); } else { writeArrayHeader(allocator, msg.isNull(), msg.children().size(), out); for (RedisMessage child : msg.children()) { writeRedisMessage(allocator, child, out); } } } private void writeArrayHeader(ByteBufAllocator allocator, boolean isNull, long length, List\u0026lt;Object\u0026gt; out) { if (isNull) { final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH + RedisConstants.EOL_LENGTH); RedisMessageType.ARRAY_HEADER.writeTo(buf); buf.writeShort(RedisConstants.NULL_SHORT); buf.writeShort(RedisConstants.EOL_SHORT); out.add(buf); } else { final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH + RedisConstants.EOL_LENGTH); RedisMessageType.ARRAY_HEADER.writeTo(buf); buf.writeBytes(numberToBytes(length)); buf.writeShort(RedisConstants.EOL_SHORT); out.add(buf); } } 小结 # 对于 Netty 源码，楼主一直是一种敬畏的态度，没想到今天竟然从另一个方面对 Netty 的冰山一角展开解读，毕竟万事开头难，有了这一次，希望之后可以更顺利，在技术成长的道路上一起加油。\n参考链接 # Redis 协议规范（译文） TCP 粘包问题浅析及其解决方案 基于 Netty 实现 Redis 协议的编码解码器 ","date":"2018-08-09","externalUrl":null,"permalink":"/posts/netty-yuanmazhongdui-redis-xieyideshixian/","section":"文章","summary":"近期一直在做网络协议相关的工作，所以博客也就与之相关的比较多，今天楼主结合 Redis 的协议 RESP 看看在 Netty 源码中是如何实现的。RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议，优势在于实现非常简单，解析性能极好。Redis 协议将传输的结…","title":"Netty 源码中对 Redis 协议的实现","type":"posts"},{"content":"📌 本文原发布于掘金社区：Redis 协议规范（译文）\n原文地址：haifeiWu 的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\nRedis 客户端使用名为 RESP（Redis 序列化协议）的协议与 Redis 服务器进行通信。虽然该协议是专为 Redis 设计的，但它也可以用作其他 CS 软件项目的通信协议。\nRESP 的设计考虑了以下几个方面：\n易于实现 快速解析 可读性高 RESP 可以序列化不同的数据类型，如整型，字符串，数组。还有一种特定的错误类型。请求将要执行的命令作为字符串数组从 Redis 客户端发送到 Redis 服务器。Redis 使用特定数据类型进行回复。\nRESP 是二进制安全的，不需要处理从一个进程传输到另一个进程的批量数据，因为它使用前缀长度来传输批量数据。\n注意： 此处概述的协议仅用于客户端 - 服务器通信。Redis Cluster 使用不同的二进制协议，以便在节点之间交换消息。\n网络层 # 客户端连接到 Redis 服务器，即创建一个到端口 6379 的 TCP 连接。虽然 RESP 在技术上是非 TCP 特定的，但在 Redis 的上下文中，协议仅用于 TCP 连接（或类似的面向流的连接，如 Unix 套接字）。\\\n请求 - 响应模型 # Redis 接受由不同参数组成的命令。收到命令后，将对其进行处理并将回复发送回客户端。这是最简单的模型，但有两个例外：\nRedis 支持流水线操作（本文档稍后介绍）。因此，客户端可以一次发送多个命令，并等待稍后的回复。 当 Redis 客户端处于 Pub/Sub 时，协议会更改语义并成为推送协议，即客户端不再需要发送命令，因为服务器会在它们接收到命令时自动向客户端发送新消息。 排除上述两个例外，Redis 协议是一个简单的请求 - 响应协议。\\\nRESP 协议描述 # RESP 协议在 Redis 1.2 中引入，但它成为与 Redis 2.0 中的 Redis 服务器通信的标准方式。这是每一个 Redis 客户端都应该实现的协议。\nRESP 实际上是一个支持以下数据类型的序列化协议：单行字符串，错误信息，整型，多行字符串和数组。RESP 在 Redis 中用作请求 - 响应协议的方式如下：\n客户端将命令作为字符串数组发送到 Redis 服务器。 服务器根据命令回复一种 RESP 类型的数据。 在 RESP 中，一些数据的类型通过它的第一个字节进行判断：\n单行回复：回复的第一个字节是 \u0026ldquo;+\u0026rdquo;\n错误信息：回复的第一个字节是 \u0026ldquo;-\u0026rdquo;\n整形数字：回复的第一个字节是 \u0026ldquo;:\u0026rdquo;\n多行字符串：回复的第一个字节是 \u0026ldquo;$\u0026rdquo;\n数组：回复的第一个字节是 \u0026ldquo;*\u0026rdquo;\n此外，RESP 能够使用稍后指定的 Bulk Strings 或 Array 的特殊变体来表示 Null 值。在 RESP 中，协议的不同部分始终以“\\ r \\ n”（CRLF）结束。\\\nRESP 单行字符串（简单字符串） # 简单字符串按以下方式编码：加号字符，后跟不能包含 CR 或 LF 字符的字符串（不允许换行），由 CRLF 终止（即“\\ r \\ n”）。\nSimple Strings 用于以最小的开销传输非二进制安全字符串。例如，许多 Redis 命令成功回复时只有“OK”，因为 RESP 单行字符串使用以下 5 个字节进行编码：\n\u0026#34;+OK\\r\\n\u0026#34; 为了发送二进制安全字符串，使用 RESP 多行字符串代替。\n当 Redis 使用 Simple String 回复时，客户端库应该向调用者返回一个字符串，该字符串从“+”之后的第一个字符开始，一直到字符串结尾，不包括最终的 CRLF 字节。\nRESP 错误信息 # RESP 具有错误的特定数据类型。实际上错误与 RESP 单行字符串完全相同，但第一个字符是减号\u0026rsquo; - \u0026lsquo;字符而不是加号。\nRESP 中单行字符串和错误之间的真正区别在于客户端将错误视为异常，组成错误类型的字符串是错误消息本身。基本格式如下：\n\u0026#34;-Error message\\r\\n\u0026#34; 错误回复仅在发生错误时发送，例如，如果您尝试对错误的数据类型执行操作，或者命令不存在等等。收到错误回复时，客户端应将异常抛出。\n以下是错误回复的示例：\n-ERR unknown command \u0026#39;foobar\u0026#39; -WRONGTYPE Operation against a key holding the wrong kind of value “ - ”之后的第一个单词，直到第一个空格或换行符，表示返回的错误类型。这只是 Redis 使用的约定，不是 RESP 错误格式的一部分。\n例如，ERR 是一般错误，而 WRONGTYPE 是一个更具体的错误，意味着客户端尝试对错误的数据类型执行操作。这称为错误前缀，是一种允许客户端理解服务器返回的错误类型的方法，而不依赖于给定的确切消息，这可能随时间而变化。\n客户端实现可以针对不同的错误返回不同类型的异常，或者可以通过直接将错误名称作为字符串提供给调用者来提供捕获错误的通用方法。\n但是，这样的功能不应该被认为是至关重要的，因为它很少有用，并且有限的客户端实现可能只返回通用的错误条件，例如 false。\nRESP 整型数据 # 此类型只是一个 CRLF 终止的字符串，表示一个以“：”字节为前缀的整数。例如“：0 \\ r \\ n”或“：1000 \\ r \\ n”是整数回复。许多 Redis 命令返回 RESP 整型，如 INCR，LLEN 和 LASTSAVE。\n返回的整数没有特殊含义，它只是 INCR 的增量编号，LASTSAVE 的 UNIX 时间等等。但是，返回的整数应保证在有符号的 64 位整数范围内。\n整数回复也被广泛使用，以便返回真或假。例如，EXISTS 或 SISMEMBER 之类的命令将返回 1 表示 true，0 表示 false。\n如果实际执行操作，其他命令（如 SADD，SREM 和 SETNX）将返回 1，否则返回 0。\n以下命令将回复整数回复：SETNX，DEL，EXISTS，INCR，INCRBY，DECR，DECRBY，DBSIZE，LASTSAVE，RENAMENX，MOVE，LLEN，SADD，SREM，SISMEMBER，SCARD。\nRESP 多行字符串 # 多行字符串用于表示长度最大为 512 MB 的单个二进制安全字符串。\n多行字符串按以下方式编码：\n一个“$”字节后跟组成字符串的字节数（一个前缀长度），由 CRLF 终止。 字符串数据。 最终的 CRLF。 所以字符串“foobar”的编码如下：\n\u0026#34;$6\\r\\nfoobar\\r\\n\u0026#34; 当只是一个空字符串时：\n\u0026#34;$0\\r\\n\\r\\n\u0026#34; RESP 多行字符串也可以使用用于表示 Null 值的特殊格式来表示值的不存在。在这种特殊格式中，长度为-1，并且没有数据，因此 Null 表示为：\n\u0026#34;$-1\\r\\n\u0026#34; 当服务器使用 Null 多行字符串回复时，客户端库 API 不应返回空字符串，而应返回 nil 对象。例如，Ruby 库应返回\u0026rsquo;nil\u0026rsquo;，而 C 库应返回 NULL（或在 reply 对象中设置特殊标志），依此类推。\nRESP 数组 # 客户端使用 RESP 数组将命令发送到 Redis 服务器。类似地，某些 Redis 命令将元素集合返回给客户端使用 RESP 数组是回复类型。一个例子是 LRANGE 命令，它返回列表的元素。\nRESP 数组使用以下格式发送：\n*字符作为第一个字节，后跟数组中的元素数作为十进制数，后跟 CRLF。 数组的每个元素的附加 RESP 类型。 所以空数组就是以下内容：\n\u0026#34;*0\\r\\n\u0026#34; 那么两个 RESP 批量字符串“foo”和“bar”的数组编码为：\n\u0026#34;*2\\r\\n$3\\r\\nfoo\\r\\n$3\\r\\nbar\\r\\n\u0026#34; 正如您在数组前面加上* CRLF 部分之后所看到的那样，组成数组的其他数据类型将一个接一个地连接起来。例如，三个整数的数组编码如下：\n\u0026#34;*3\\r\\n:1\\r\\n:2\\r\\n:3\\r\\n\u0026#34; 数组可以包含混合类型，元素不必具有相同的类型。例如，四个整数和批量字符串的列表可以编码如下：\n*5\\r\\n :1\\r\\n :2\\r\\n :3\\r\\n :4\\r\\n $6\\r\\n foobar\\r\\n 服务器发送的第一行是* 5 \\ r \\ n，以指定将跟随五个回复。然后发送构成多重回复项目的每个回复。\nNull 数组的概念也存在，并且是指定 Null 值的替代方法（通常使用 Null 多行字符串，但由于历史原因，我们有两种格式）。\n例如，当 BLPOP 命令超时时，它返回一个计数为-1 的 Null 数组，如下例所示：\n\u0026#34;*-1\\r\\n\u0026#34; 当 Redis 使用 Null 数组回复时，客户端库 API 应返回 nil 对象而不是空数组。这是区分空列表和不同条件（例如 BLPOP 命令的超时条件）所必需的。\nRESP 中可以使用数组中嵌套数组。例如，两个数组的数组编码如下：\n*2\\r\\n *3\\r\\n :1\\r\\n :2\\r\\n :3\\r\\n *2\\r\\n +Foo\\r\\n -Bar\\r\\n 第二个元素是 Null。客户端库应返回如下内容：\n[\u0026#34;foo\u0026#34;,nil,\u0026#34;bar\u0026#34;] 注意，这不是前面部分中所述的例外，而只是进一步说明协议的示例。\n发送命令到 Redis 服务端 # 既然熟悉了 RESP 序列化格式，那么编写 Redis 客户端库就很容易了。我们可以进一步讲述客户端和服务器之间的交互如何工作：\n客户端向 Redis 服务器发送仅由 Bulk Strings 组成的 RESP 数组。 Redis 服务器回复客户端，发送任何有效的 RESP 数据类型作为回复。 因此，例如，典型的交互可以是以下所示。\n客户端发送命令 LLEN mylist 以获取存储在密钥 mylist 中的列表长度，服务器返回一个 Integer 回复，如下例所示（C：是客户端，S：服务器）。\nC: *2\\r\\n C: $4\\r\\n C: LLEN\\r\\n C: $6\\r\\n C: mylist\\r\\n S: :48293\\r\\n 通常我们将协议的不同部分与换行符分开以简化，但实际的交互是客户端发送 * 2 \\ r \\ n 6 \\ r \\ nmylist \\ r \\ n 整体。\n小结 # 这是楼主第一次尝试翻译一篇技术文档，相对来说技术文档的英文阅读起来还是比较舒服的，相信有了第一次尝试，之后肯定会越来越顺利。由于楼主水平有限，文章中难免有纰漏，期望小伙伴们的指正，感谢……。\n参考链接 # Redis Protocol specification ","date":"2018-08-08","externalUrl":null,"permalink":"/posts/redisxieyiguifan-yiwen/","section":"文章","summary":"Redis 客户端使用名为 RESP（Redis 序列化协议）的协议与 Redis 服务器进行通信。虽然该协议是专为 Redis 设计的，但它可以用于其他 CS 软件项目的通讯协议。RESP 可以序列化不同的数据类型，如整型，字符串，数组。还有一种特定的错误类型。请求将要执行的命令作为字符…","title":"Redis 协议规范（译文）","type":"posts"},{"content":"","date":"2018-08-07","externalUrl":null,"permalink":"/tags/elasticsearch/","section":"标签","summary":"","title":"Elasticsearch","type":"tags"},{"content":"","date":"2018-08-07","externalUrl":null,"permalink":"/tags/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/","section":"标签","summary":"","title":"数据分析","type":"tags"},{"content":"📌 本文原发布于掘金社区：线上 ELK 集群健康值 red 状态问题排查与解决\n原文地址：haifeiWu 的博客\n博客地址：www.hchstudio.cn\n欢迎转载，转载请注明作者及出处，谢谢！\n之前一直运行正常的数据分析平台，最近一段时间没有注意到日志索引数据一直未生成，大概持续了 n 多天，当前状态：单台机器，Elasticsearch（下面称 ES）单节点(空集群),1000+ shards, 约 200G 大小。\n问题排查 # 服务器内存，CPU 状态检查 # 使用 top 查看服务器 cpu，内存等占用情况，如下图所示（当时楼主的服务器 ES 应用的 CPU 占用在 90%以上，肯定有问题）\ntop 内存占用也极高（当时楼主的 8G 内存的服务器仅剩下 150M 左右的空闲，肯定是 ES 的问题）\nfree ES 集群状态 # 查看 ES 集群健康值，发现 status 为 red，这种状态表示部分主分片不可用，楼主当前的状态是历史数据可查，但是无法生成新的 index 数据。\ncurl http://localhost:9200/_cluster/health?pretty { \u0026#34;cluster_name\u0026#34; : \u0026#34;elasticsearch\u0026#34;, \u0026#34;status\u0026#34; : \u0026#34;red\u0026#34;, \u0026#34;timed_out\u0026#34; : false, \u0026#34;number_of_nodes\u0026#34; : 1, \u0026#34;number_of_data_nodes\u0026#34; : 1, \u0026#34;active_primary_shards\u0026#34; : 663, \u0026#34;active_shards\u0026#34; : 663, \u0026#34;relocating_shards\u0026#34; : 0, \u0026#34;initializing_shards\u0026#34; : 0, \u0026#34;unassigned_shards\u0026#34; : 6, \u0026#34;delayed_unassigned_shards\u0026#34; : 0, \u0026#34;number_of_pending_tasks\u0026#34; : 0, \u0026#34;number_of_in_flight_fetch\u0026#34; : 0, \u0026#34;task_max_waiting_in_queue_millis\u0026#34; : 0, \u0026#34;active_shards_percent_as_number\u0026#34; : 99.10313901345292 } 查看每个索引的状态，发现大部分索引状态是 red，处于不可用状态，因为打开的索引数据过多，导致 ES 占用大量的 CPU，内存，使得 logstash 不可用，也就无法创建新的索引数据，从而导致数据丢失。\ncurl -XGET \u0026#34;http://localhost:9200/_cat/indices?v\u0026#34; health status index pri rep docs.count docs.deleted store.size pri.store.size red open jr-2016.12.20 3 0 red open jr-2016.12.21 3 0 red open jr-2016.12.22 3 0 red open jr-2016.12.23 3 0 red open jr-2016.12.24 3 0 red open jr-2016.12.25 3 0 red open jr-2016.12.26 3 0 red open jr-2016.12.27 3 0 ES 集群分片不可用，导致的查询失败 # 查询 ES 时抛出的异常：\n[2018-08-06 18:27:24,553][DEBUG][action.search ] [Godfrey Calthrop] All shards failed for phase: [query] [jr-2018.08.06][[jr-2018.08.06][2]] NoShardAvailableActionException[null] at org.elasticsearch.action.search.AbstractSearchAsyncAction.start(AbstractSearchAsyncAction.java:129) at org.elasticsearch.action.search.TransportSearchAction.doExecute(TransportSearchAction.java:115) at org.elasticsearch.action.search.TransportSearchAction.doExecute(TransportSearchAction.java:47) at org.elasticsearch.action.support.TransportAction.doExecute(TransportAction.java:149) at org.elasticsearch.action.support.TransportAction.execute(TransportAction.java:137) at org.elasticsearch.action.support.TransportAction.execute(TransportAction.java:85) at org.elasticsearch.client.node.NodeClient.doExecute(NodeClient.java:58) at org.elasticsearch.client.support.AbstractClient.execute(AbstractClient.java:359) at org.elasticsearch.client.FilterClient.doExecute(FilterClient.java:52) at org.elasticsearch.rest.BaseRestHandler$HeadersAndContextCopyClient.doExecute(BaseRestHandler.java:83) at org.elasticsearch.client.support.AbstractClient.execute(AbstractClient.java:359) at org.elasticsearch.client.support.AbstractClient.search(AbstractClient.java:582) at org.elasticsearch.rest.action.search.RestSearchAction.handleRequest(RestSearchAction.java:85) at org.elasticsearch.rest.BaseRestHandler.handleRequest(BaseRestHandler.java:54) at org.elasticsearch.rest.RestController.executeHandler(RestController.java:205) at org.elasticsearch.rest.RestController.dispatchRequest(RestController.java:166) at org.elasticsearch.http.HttpServer.internalDispatchRequest(HttpServer.java:128) at org.elasticsearch.http.HttpServer$Dispatcher.dispatchRequest(HttpServer.java:86) at org.elasticsearch.http.netty.NettyHttpServerTransport.dispatchRequest(NettyHttpServerTransport.java:449) at org.elasticsearch.http.netty.HttpRequestHandler.messageReceived(HttpRequestHandler.java:61) 问题解决 # 通过以上排查大概知道是历史索引数据处于 open 状态过多，从而导致 ES 的 CPU，内存占用过高而不可用。\n#关闭不需要的索引，减少内存占用 curl -XPOST \u0026#34;http://localhost:9200/index_name/_close\u0026#34; 小插曲 # 关闭非热点索引数据后，楼主的 ES 集群的健康值依然是 red 状态，楼主最后联想到索引的 red 状态可能会影响 ES 的状态，果不其然，如下所示\ncurl GET http://10.252.148.85:9200/_cluster/health?level=indices { \u0026#34;cluster_name\u0026#34;: \u0026#34;elasticsearch\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;red\u0026#34;, \u0026#34;timed_out\u0026#34;: false, \u0026#34;number_of_nodes\u0026#34;: 1, \u0026#34;number_of_data_nodes\u0026#34;: 1, \u0026#34;active_primary_shards\u0026#34;: 660, \u0026#34;active_shards\u0026#34;: 660, \u0026#34;relocating_shards\u0026#34;: 0, \u0026#34;initializing_shards\u0026#34;: 0, \u0026#34;unassigned_shards\u0026#34;: 9, \u0026#34;delayed_unassigned_shards\u0026#34;: 0, \u0026#34;number_of_pending_tasks\u0026#34;: 0, \u0026#34;number_of_in_flight_fetch\u0026#34;: 0, \u0026#34;task_max_waiting_in_queue_millis\u0026#34;: 0, \u0026#34;active_shards_percent_as_number\u0026#34;: 98.65470852017937, \u0026#34;indices\u0026#34;: { \u0026#34;jr-2018.08.06\u0026#34;: { \u0026#34;status\u0026#34;: \u0026#34;red\u0026#34;, \u0026#34;number_of_shards\u0026#34;: 3, \u0026#34;number_of_replicas\u0026#34;: 0, \u0026#34;active_primary_shards\u0026#34;: 0, \u0026#34;active_shards\u0026#34;: 0, \u0026#34;relocating_shards\u0026#34;: 0, \u0026#34;initializing_shards\u0026#34;: 0, \u0026#34;unassigned_shards\u0026#34;: 3 } } } 解决方法：删除这条索引数据（这条数据是楼主排查问题期间产生的脏数据，直接删除即可）\ncurl -XDELETE \u0026#39;http://10.252.148.85:9200/jr-2018.08.06\u0026#39; 小结 # 当 ES 处于单点时，应注意 ES 的索引状态以及服务器的监控，及时清理或者关闭不必要的索引数据，避免这种情况发生。技术成长的道路上，与你同行。\n","date":"2018-08-07","externalUrl":null,"permalink":"/posts/xianshang-elk-jiqunjiankangzhi-red-zhuangtaiwentipaichayujie/","section":"文章","summary":"之前一直运行正常的数据分析平台，最近一段时间没有注意发现日志索引数据一直未生成，大概持续了 n 多天，当前状态：单台机器，Elasticsearch（下面称 ES）单节点(空集群),1000+shrads, 约 200G 大小。查看 ES 集群健康值，发现 status 为 red，这…","title":"线上 ELK 集群健康值 red 状态问题排查与解决","type":"posts"},{"content":"","date":"2018-08-02","externalUrl":null,"permalink":"/tags/java-ee/","section":"标签","summary":"","title":"Java EE","type":"tags"},{"content":"","date":"2018-08-02","externalUrl":null,"permalink":"/tags/rxjava/","section":"标签","summary":"","title":"RxJava","type":"tags"},{"content":"","date":"2018-08-02","externalUrl":null,"permalink":"/tags/%E6%B1%82%E8%81%8C/","section":"标签","summary":"","title":"求职","type":"tags"},{"content":"📌 本文原发布于掘金社区：聊聊 JDK 非阻塞队列源码（CAS 实现）\n正如上篇文章聊聊 JDK 阻塞队列源码（ReentrantLock 实现）所说，队列在我们现实生活中随处可见，最经典的就是去银行办理业务，超市买东西排队等。今天楼主要讲的就是 JDK 中使用 CAS 算法实现的另一种安全队列。\nJDK 中的队列 # 在JDK中的队列都实现了 java.util.Queue 接口，下面就是楼主要说的无锁版本的队列实现：\n队列名字 是否加锁 数据结构 关键技术点 是否有锁 是否有界 LinkedTransferQueue 否 链表 CAS 无锁 无界 ConcurrentLinkedQueue 否 链表 CAS 无锁 无界 LinkedTransferQueue 源码分析 # LinkedTransferQueue 的原理就是通过使用原子变量 compare and swap（简称“CAS”）这种不加锁的方式来实现并发控制，LinkedTransferQueue 是一个无界的安全队列，其长度可以无限延伸，当然其带来的问题也是显而易见的。\\\n主要方法源码实现 # add：添加元素到队列里，添加成功返回 true；\\ offer：添加元素到队列里，添加成功返回 true，添加失败返回 false；\\ put：添加元素到队列里，如果容量满了会阻塞直到容量不满；\\ poll：删除队列头部元素，如果队列为空，返回 null。否则返回元素；\\ take：删除队列头部元素，如果队列为空，一直阻塞到队列有元素并删除。\\ add方法：\n\\\npublic boolean add(E e) { xfer(e, true, ASYNC, 0); return true; } offer方法：\n\\\npublic boolean offer(E e) { xfer(e, true, ASYNC, 0); return true; } poll方法：\n\\\npublic E poll() { return xfer(null, false, NOW, 0); } take方法：\n\\\npublic E take() throws InterruptedException { E e = xfer(null, false, SYNC, 0); if (e != null) return e; Thread.interrupted(); throw new InterruptedException(); } 从上面代码中可以看出，这些方法最终都指向了 xfer 方法，只不过传入的参数不同。\n/** * Implements all queuing methods. See above for explanation. * * @param e the item or null for take * @param haveData true if this is a put, else a take * @param how NOW, ASYNC, SYNC, or TIMED * @param nanos timeout in nanosecs, used only if mode is TIMED * @return an item if matched, else e * @throws NullPointerException if haveData mode but e is null */ 从源码的 doc 注释中可以知道\n第一个参数，如果是 put 类型，就是实际的值，反之就是 null。\n第二个参数，是否包含数据，put 类型就是 true，take 就是 false。\n第三个参数，执行类型，有立即返回的 NOW，有异步的 ASYNC，有阻塞的 SYNC，有带超时的 TIMED。\n第四个参数，只有在 TIMED 类型才有作用。\n接下来我们来看看 xfer 到底是何方神圣\\\nprivate E xfer(E e, boolean haveData, int how, long nanos) { if (haveData \u0026amp;\u0026amp; (e == null)) throw new NullPointerException(); Node s = null; // the node to append, if needed retry: for (;;) { // restart on append race // 从 head 开始 for (Node h = head, p = h; p != null;) { // find \u0026amp; match first node // head 的类型。 boolean isData = p.isData; // head 的数据 Object item = p.item; // item != null 有 2 种情况,一是 put 操作, 二是 take 的 itme 被修改了(匹配成功) // (itme != null) == isData 要么表示 p 是一个 put 操作, 要么表示 p 是一个还没匹配成功的 take 操作 if (item != p \u0026amp;\u0026amp; (item != null) == isData) { // 如果当前操作和 head 操作相同，就没有匹配上，结束循环，进入下面的 if 块。 if (isData == haveData) // can't match break; // 如果操作不同,匹配成功, 尝试替换 item 成功, if (p.casItem(item, e)) { // match // 更新 head for (Node q = p; q != h;) { Node n = q.next; // update by 2 unless singleton if (head == h \u0026amp;\u0026amp; casHead(h, n == null ? q : n)) { h.forgetNext(); break; } // advance and retry if ((h = head) == null || (q = h.next) == null || !q.isMatched()) break; // unless slack \u0026lt; 2 } // 唤醒原 head 线程. LockSupport.unpark(p.waiter); return LinkedTransferQueue.\u0026lt;E\u0026gt;cast(item); } } // 找下一个 Node n = p.next; p = (p != n) ? n : (h = head); // Use head if p offlist } // 如果这个操作不是立刻就返回的类型 if (how != NOW) { // No matches available // 且是第一次进入这里 if (s == null) // 创建一个 node s = new Node(e, haveData); // 尝试将 node 追加对队列尾部，并返回他的上一个节点。 Node pred = tryAppend(s, haveData); // 如果返回的是 null, 表示不能追加到 tail 节点,因为 tail 节点的模式和当前模式相反. if (pred == null) // 重来 continue retry; // lost race vs opposite mode // 如果不是异步操作(即立刻返回结果) if (how != ASYNC) // 阻塞等待匹配值 return awaitMatch(s, pred, e, (how == TIMED), nanos); } return e; // not waiting } } 代码有点长，其实逻辑很简单。就是找到 head 节点，如果 head 节点是匹配的操作，就直接赋值，如果不是，添加到队列中。\n注意：队列中永远只有一种类型的操作，要么是 put 类型，要么是 take 类型。\nConcurrentLinkedQueue 源码分析 # 与 LinkedTransferQueue 一样，ConcurrentLinkedQueue 也是采用原子变量实现的并发控制，ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列，它采用先进先出的规则对节点进行排序，当我们添加一个元素的时候，它会将该元素添加到队列的尾部，当我们获取一个元素时，它会返回队列头部的元素。它采用了“wait－free”算法来实现。\n主要方法源码实现 # add：添加元素到队列里，添加成功返回 true；\\ offer：添加元素到队列里，添加成功返回 true，添加失败返回 false；\\ put：添加元素到队列里，如果容量满了会阻塞直到容量不满；\\ poll：删除队列头部元素，如果队列为空，返回 null。否则返回元素；\\ remove：基于对象找到对应的元素，并删除。删除成功返回 true，否则返回 false；\\ add方法：\n\\\npublic boolean add(E e) { return offer(e); } offer方法：\nConcurrentLinkedQueue是无界的，所以offer永远返回 true，不能通过返回值来判断是否入队成功，\npublic boolean offer(E e) { // 校验是否为空 checkNotNull(e); //入队前，创建一个入队节点 final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;E\u0026gt;(e); //循环CAS直到入队成功。 // 1、根据tail节点定位出尾节点（last node）； // 2、将新节点置为尾节点的下一个节点， // 3、更新尾节点casTail。 for (Node\u0026lt;E\u0026gt; t = tail, p = t;;) { Node\u0026lt;E\u0026gt; q = p.next; //判断p是不是尾节点，tail节点不一定是尾节点，判断是不是尾节点的依据是该节点的next是不是null if (q == null) { // p is last node if (p.casNext(null, newNode)) { //设置P节点的下一个节点为新节点，如果p的next为null，说明p是尾节点，casNext返回true； // 如果p的next不为null，说明有其他线程更新过队列的尾节点，casNext返回false。 // Successful CAS is the linearization point // for e to become an element of this queue, // and for newNode to become \u0026quot;live\u0026quot;. if (p != t) // hop two nodes at a time casTail(t, newNode); // Failure is OK. return true; } // Lost CAS race to another thread; re-read next } else if (p == q) //p节点是null的head节点刚好被出队，更新head节点时h.lazySetNext(h)把旧的head节点指向自己 // We have fallen off list. If tail is unchanged, it // will also be off-list, in which case we need to // jump to head, from which all live nodes are always // reachable. Else the new tail is a better bet. p = (t != (t = tail)) ? t : head; else // Check for tail updates after two hops. //判断tail节点有没有被更新，如果没被更新，1）p=q：p指向p.next继续寻找尾节点; //如果被更新了，2)p=t:P赋值为新的tail节点 p = (p != t \u0026amp;\u0026amp; t != (t = tail)) ? t : q; } } poll方法：\n\\\npublic E poll() { restartFromHead: //两层循环 for (;;) { for (Node\u0026lt;E\u0026gt; h = head, p = h, q;;) { E item = p.item; if (item != null \u0026amp;\u0026amp; p.casItem(item, null)) { // Successful CAS is the linearization point // for item to be removed from this queue. if (p != h) // hop two nodes at a time updateHead(h, ((q = p.next) != null) ? q : p); return item; } //队列为空，更新head节点 else if ((q = p.next) == null) { updateHead(h, p); return null; } else if (p == q) //p节点是null的head节点刚好被出队，更新head节点时h.lazySetNext(h);把旧的head节点指向自己。 //重新从head节点开始 continue restartFromHead; else p = q; //将p执行p的下一个节点 } } } //更新head节点 final void updateHead(Node\u0026lt;E\u0026gt; h, Node\u0026lt;E\u0026gt; p) { //通过CAS将head更新为P if (h != p \u0026amp;\u0026amp; casHead(h, p)) h.lazySetNext(h);//把旧的head节点指向自己 } void lazySetNext(Node\u0026lt;E\u0026gt; val) { UNSAFE.putOrderedObject(this, nextOffset, val); } remove方法：\n\\\npublic boolean remove(Object o) { if (o != null) { Node\u0026lt;E\u0026gt; next, pred = null; // 循环CAS直到删除节点 for (Node\u0026lt;E\u0026gt; p = first(); p != null; pred = p, p = next) { boolean removed = false; E item = p.item; if (item != null) { if (!o.equals(item)) { next = succ(p); continue; } // 通过CAS删除节点 removed = p.casItem(item, null); } next = succ(p); if (pred != null \u0026amp;\u0026amp; next != null) // unlink pred.casNext(p, next); if (removed) return true; } } return false; } 小结 # 本文主要介绍了两种 CAS 算法实现的安全队列，然而在稳定性要求较高的系统中，为了防止生产者速度过快，导致内存溢出，通常是不建议选择无界队列的。当然楼主水平有限，文章中不免有纰漏，望小伙伴谅解并指出，在技术的道路上一起成长。\n参考链接 # 高性能队列——Disruptor 聊聊并发（六）ConcurrentLinkedQueue 的实现原理分析 作 者：haifeiWu\n原文链接：www.hchstudio.cn/article/201…\n版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-08-02","externalUrl":null,"permalink":"/posts/liaoliao-jdk-feizuseduilieyuanma-casshixian/","section":"文章","summary":"正如上篇文章聊聊 JDK 阻塞队列源码（ReentrantLock 实现）所说，队列在我们现实生活中队列随处可见，最经典的就是去银行办理业务，超市买东西排队等。今天楼主要讲的就是 JDK 中安全队列的另一种实现使用 CAS 算法实现的安全队列。","title":"聊聊 JDK 非阻塞队列源码（CAS 实现）","type":"posts"},{"content":"📌 本文原发布于掘金社区：聊聊 JDK 阻塞队列源码（ReentrantLock 版）\n项目中用到了一个叫做 Disruptor 的队列，今天楼主并不是要介绍 Disruptor，而是想巩固一下基础，扒一下 JDK 中的阻塞队列，听到队列相信大家对其并不陌生，在我们现实生活中队列随处可见，最经典的就是去银行办理业务等。\n当然在计算机世界中，队列是一种数据结构，队列采用的是 FIFO(first in first out)，新元素（等待进入队列的元素）总是被插入到尾部，而读取的时候总是从头部开始读取。在计算机中队列一般用来做排队(如线程池的等待排队，锁的等待排队)，用来做解耦（生产者消费者模式），异步等等。\nJDK 中的队列 # 在JDK中的队列都实现了 java.util.Queue 接口，这些队列又分为两类，一类是线程不安全的，ArrayDeque，LinkedList 等等，还有一类在java.util.concurrent包下，属于线程安全，而在我们真实的环境中，我们的机器都是多线程的，当多线程对同一个队列进行操作时，如果使用线程不安全的队列，会出现数据丢失等无法预料的问题，所以我们这个时候只能选择线程安全的队列。下面是我们今天要探讨的两个队列\n队列名字 是否加锁 数据结构 关键技术点 是否有锁 是否有界 ArrayBlockingQueue 是 数组 array ReentrantLock 有锁 有界 LinkedBlockingQueue 是 链表 ReentrantLock 有锁 有界 ArrayBlockingQueue 源码分析 # ArrayBlockingQueue 的原理就是使用一个可重入锁（ReentrantLock ）和这个锁生成的两个条件对象进行并发控制，ArrayBlockingQueue 是一个有界的阻塞队列，初始化的时候必须要指定队列长度，且指定长度之后不允许修改。\\\n成员变量属性 # /** The queued items item的集合 */ final Object[] items; /** items index for next take, poll, peek or remove 取出数据的索引 */ int takeIndex; /** items index for next put, offer, or add 添加数据的索引 */ int putIndex; /** Number of elements in the queue 队列元素的个数 */ int count; /** Main lock guarding all access 可重入的锁 */ final ReentrantLock lock; /** Condition for waiting takes 队列为空条件等待对象 */ private final Condition notEmpty; /** Condition for waiting puts 队列满条件等待对象 */ private final Condition notFull; 主要方法源码实现 # add：添加元素到队列里，添加成功返回 true，由于容量满了添加失败会抛出IllegalStateException异常；\\ offer：添加元素到队列里，添加成功返回 true，添加失败返回 false；\\ put：添加元素到队列里，如果容量满了会阻塞直到容量不满；\\ poll：删除队列头部元素，如果队列为空，返回 null。否则返回元素；\\ remove：根据传入的对象找到对应的元素，并删除。删除成功返回 true，否则返回 false；\\ take：删除队列头部元素，如果队列为空，会一直阻塞，直到队列有元素时再删除。\\ add 方法：\\\npublic boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException(\u0026quot;Queue full\u0026quot;); } offer 方法：\\\npublic boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) return false; else { insert(e); return true; } } finally { lock.unlock(); } } 我们可以看到，如果队列满了则返回 false，如果没有满则调用 insert。整个方法通过可重入锁来加锁，并最终释放锁。\n接着看一下insert方法：\n\\\nprivate void insert(E x) { items[putIndex] = x; // 元素添加到数组里 putIndex = inc(putIndex); // 放数据索引+1，当索引满了变成0 ++count; // 元素个数+1 notEmpty.signal(); // 使用条件对象notEmpty通知 } 这里insert被调用时，就会唤醒在notEmpty上等待的线程执行take操作。\\\n再看一下put方法：\\\npublic void put(E e) throws InterruptedException { checkNotNull(e); // 不允许元素为空 final ReentrantLock lock = this.lock; lock.lockInterruptibly(); // 加锁，保证调用put方法的时候只有1个线程 try { while (count == items.length) // 如果队列满了，阻塞当前线程，while用来防止假唤醒 notFull.await(); // 线程阻塞并被挂起，同时释放锁 insert(e); // 调用insert方法 } finally { lock.unlock(); // 释放锁，让其他线程可以调用put方法 } } 通过上面代码我们可以知道，add方法和offer方法不会阻塞线程，put方法如果队列满了会阻塞线程，直到有线程消费了队列里的数据才有可能被唤醒。\\\n紧接着我们看一下poll方法：\\\npublic E poll() { final ReentrantLock lock = this.lock; lock.lock(); // 加锁，保证调用poll方法的时候只有1个线程 try { return (count == 0) ? null : extract(); // 如果队列里没元素了，返回null，否则调用extract方法 } finally { lock.unlock(); // 释放锁，让其他线程可以调用poll方法 } } 看看这个extract方法，extract 翻译过来就是提取的意思：\\\nprivate E extract() { final Object[] items = this.items; E x = this.\u0026lt;E\u0026gt;cast(items[takeIndex]); // 得到取索引位置上的元素 items[takeIndex] = null; // 对应取索引上的数据清空 takeIndex = inc(takeIndex); // 取数据索引+1，当索引满了变成0 --count; // 元素个数-1 notFull.signal(); // 使用条件对象notFull通知，原理同上面的insert中 return x; // 返回元素 } 看一下take方法：\\\npublic E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); // 加锁，保证调用take方法的时候只有1个线程 try { while (count == 0) // 如果队列空，阻塞当前线程，并加入到条件对象notEmpty的等待队列里 notEmpty.await(); // 线程阻塞并被挂起，同时释放锁 return extract(); // 调用extract方法 } finally { lock.unlock(); // 释放锁，让其他线程可以调用take方法 } } remove方法：\\\npublic boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); // 加锁，保证调用remove方法的时候只有1个线程 try { for (int i = takeIndex, k = count; k \u0026gt; 0; i = inc(i), k--) { // 遍历元素 if (o.equals(items[i])) { // 两个对象相等的话 removeAt(i); // 调用removeAt方法 return true; // 删除成功，返回true } } return false; // 删除成功，返回false } finally { lock.unlock(); // 释放锁，让其他线程可以调用remove方法 } } 再看一下removeAt方法：\\\nprivate void removeAt(int i) { final Object[] items = this.items; if (i == takeIndex) { // 如果要删除数据的索引是取索引位置，直接删除取索引位置上的数据，然后取索引+1即可 items[takeIndex] = null; takeIndex = inc(takeIndex); } else { // 如果要删除数据的索引不是取索引位置，移动元素元素，更新取索引和放索引的值 for (;;) { int nexti = inc(i); if (nexti != putIndex) { items[i] = items[nexti]; i = nexti; } else { items[i] = null; putIndex = i; break; } } } --count; // 元素个数-1 notFull.signal(); // 使用条件对象notFull通知 } LinkedBlockingQueue 源码分析\\ # LinkedBlockingQueue是一个使用链表完成队列操作的阻塞队列。链表是单向链表，而不是双向链表。\n成员变量属性 # /** The capacity bound, or Integer.MAX_VALUE if none 容量大小 */ private final int capacity; /** Current number of elements 元素个数，因为有2个锁，存在竞态条件，使用AtomicInteger */ private final AtomicInteger count = new AtomicInteger(0); /** * Head of linked list. * Invariant: head.item == null * 头结点 */ private transient Node\u0026lt;E\u0026gt; head; /** * Tail of linked list. * Invariant: last.next == null * 尾节点 */ private transient Node\u0026lt;E\u0026gt; last; /** Lock held by take, poll, etc 获取元素的锁 */ private final ReentrantLock takeLock = new ReentrantLock(); /** Wait queue for waiting takes 取元素的条件对象 */ private final Condition notEmpty = takeLock.newCondition(); /** Lock held by put, offer, etc 放元素的锁 */ private final ReentrantLock putLock = new ReentrantLock(); /** Wait queue for waiting puts 添加元素的条件对象 */ private final Condition notFull = putLock.newCondition(); 主要方法源码实现 # 由于文章篇幅问题，对于LinkedBlockingQueue，我们主要分析以下几个方法：\noffer：添加元素到队列里，添加成功返回 true，添加失败返回 false；\\ put：添加元素到队列里，如果容量满了会阻塞直到容量不满；\\ poll：删除队列头部元素，如果队列为空，返回 null。否则返回元素；\\ remove：根据传入的对象找到对应的元素，并删除。删除成功返回 true，否则返回 false；\\ take：删除队列头部元素，如果队列为空，会一直阻塞，直到队列有元素时再删除。\\ offer方法：\npublic boolean offer(E e) { if (e == null) throw new NullPointerException(); // 不允许空元素 final AtomicInteger count = this.count; if (count.get() == capacity) // 如果容量满了，返回false return false; int c = -1; Node\u0026lt;E\u0026gt; node = new Node(e); // 容量没满，以新元素构造节点 final ReentrantLock putLock = this.putLock; putLock.lock(); // 放锁加锁，保证调用offer方法的时候只有1个线程 try { // 再次判断容量是否已满，因为可能取元素锁在进行消费数据，没满的话继续执行 if (count.get() \u0026lt; capacity) { enqueue(node); // 节点添加到链表尾部 c = count.getAndIncrement(); // 元素个数+1 if (c + 1 \u0026lt; capacity) // 如果容量还没满 notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程，表示可以再次往队列里面加数据 } } finally { putLock.unlock(); // 释放放锁，让其他线程可以调用offer方法 } // 由于存在放元素锁和取元素锁，这里可能取元素锁一直在消费数据，count会变化。这里的if条件表示如果队列中还有1条数据 if (c == 0) // 在拿锁的条件对象notEmpty上唤醒正在等待的1个线程，表示队列里还有1条数据，可以进行消费 signalNotEmpty(); return c \u0026gt;= 0; // 添加成功返回true，否则返回false } put方法：\\\npublic void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // 不允许空元素 int c = -1; Node\u0026lt;E\u0026gt; node = new Node(e); // 以新元素构造节点 final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); // 放锁加锁，保证调用put方法的时候只有1个线程 try { while (count.get() == capacity) { // 如果容量满了 notFull.await(); // 阻塞并挂起当前线程 } enqueue(node); // 节点添加到链表尾部 c = count.getAndIncrement(); // 元素个数+1 if (c + 1 \u0026lt; capacity) // 如果容量还没满 // 在放锁的条件对象notFull上唤醒正在等待的线程，表示可以再次往队列里面加数据了，队列还没满 notFull.signal(); } finally { putLock.unlock(); // 释放放锁，让其他线程可以调用put方法 } // 由于存在放锁和拿锁，这里可能拿锁一直在消费数据，count会变化。这里的if条件表示如果队列中还有1条数据 if (c == 0) // 在拿锁的条件对象notEmpty上唤醒正在等待的1个线程，表示队列里还有1条数据，可以进行消费 signalNotEmpty(); } poll方法：\\\npublic E poll() { final AtomicInteger count = this.count; if (count.get() == 0) // 如果元素个数为0 return null; // 返回null E x = null; int c = -1; final ReentrantLock takeLock = this.takeLock; takeLock.lock(); // 拿锁加锁，保证调用poll方法的时候只有1个线程 try { if (count.get() \u0026gt; 0) { // 判断队列里是否还有数据 x = dequeue(); // 删除头结点 c = count.getAndDecrement(); // 元素个数-1 if (c \u0026gt; 1) // 如果队列里还有元素 // 在拿锁的条件对象notEmpty上唤醒正在等待的线程，表示队列里还有数据，可以再次消费 notEmpty.signal(); } } finally { takeLock.unlock(); // 释放拿锁，让其他线程可以调用poll方法 } // 由于存在放锁和拿锁，这里可能放锁一直在添加数据，count会变化。这里的if条件表示如果队列中还可以再插入数据 if (c == capacity) // 在放锁的条件对象notFull上唤醒正在等待的1个线程，表示队列里还能再次添加数据 signalNotFull(); return x; } take方法：\\\npublic E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); // 拿锁加锁，保证调用take方法的时候只有1个线程 try { while (count.get() == 0) { // 如果队列里已经没有元素了 notEmpty.await(); // 阻塞并挂起当前线程 } x = dequeue(); // 删除头结点 c = count.getAndDecrement(); // 元素个数-1 if (c \u0026gt; 1) // 如果队列里还有元素 // 在拿锁的条件对象notEmpty上唤醒正在等待的线程，表示队列里还有数据，可以再次消费 notEmpty.signal(); } finally { takeLock.unlock(); // 释放拿锁，让其他线程可以调用take方法 } // 由于存在放锁和拿锁，这里可能放锁一直在添加数据，count会变化。这里的if条件表示如果队列中还可以再插入数据 if (c == capacity) // 在放锁的条件对象notFull上唤醒正在等待的1个线程，表示队列里还能再次添加数据 signalNotFull(); return x; } remove方法：\\\npublic boolean remove(Object o) { if (o == null) return false; fullyLock(); // remove操作要移动的位置不固定，对读锁写锁都进行加锁 try { for (Node\u0026lt;E\u0026gt; trail = head, p = trail.next; // 从链表头结点开始遍历 p != null; trail = p, p = p.next) { if (o.equals(p.item)) { // 判断是否找到对象 unlink(p, trail); // 修改节点的链接信息，同时调用notFull的signal方法 return true; } } return false; } finally { fullyUnlock(); // 2个锁解锁 } } 紧接着来看一下 fullyLock与fullyUnlock方法：\\\n/** * Locks to prevent both puts and takes. */ void fullyLock() { putLock.lock(); takeLock.lock(); } /** * Unlocks to allow both puts and takes. */ void fullyUnlock() { takeLock.unlock(); putLock.unlock(); } LinkedBlockingQueue的 take 方法在没有数据的情况下会阻塞，poll方法删除链表头结点，remove 方法删除指定的对象。\\\n需要注意的是remove方法由于要删除的数据的位置不确定，需要 2 个锁同时加锁。\\\n小结 # 文章有点长，JDK 中的阻塞队列线程安全的主要有ArrayBlockingQueue，LinkedBlockingQueue，LinkedTransferQueue，DelayQueue四种，今天楼主把ArrayBlockingQueue，LinkedBlockingQueue放在一起介绍，主要原因是这两者都是使用可重入锁 ReentrantLock实现线程安全的。\n当然二者也有很大的不同，主要是：\n1，ArrayBlockingQueue只有 1 个锁，添加数据和删除数据的时候只能有 1 个被执行，不允许并行执行。\n而LinkedBlockingQueue有 2 个锁，放元素锁和取元素锁，添加数据和删除数据是可以并行进行的，当然添加数据和删除数据的时候只能有 1 个线程各自执行。\\\n2，ArrayBlockingQueue中放入数据阻塞的时候，需要消费数据才能唤醒。\n而LinkedBlockingQueue中放入数据阻塞的时候，因为它内部有 2 个锁，可以并行执行放入数据和消费数据，不仅在消费数据的时候会唤醒因插入而阻塞的线程，同时在插入的时候如果容量还没满，也会唤醒因插入而阻塞的线程。\\\n参考链接 # 还在用阻塞队列？Disruptor 了解一下？ 作 者：haifeiWu\n原文链接：www.hchstudio.cn/article/201…\n版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-08-01","externalUrl":null,"permalink":"/posts/liaoliao-jdk-zuseduilieyuanma-reentrantlockban/","section":"文章","summary":"项目中用到了一个叫做 Disruptor 的队列，今天楼主并不是要介绍 Disruptor 而是想巩固一下基础扒一下 JDK 中的阻塞队列，听到队列相信大家对其并不陌生，在我们现实生活中队列随处可见，最经典的就是去银行办理业务等。","title":"聊聊 JDK 阻塞队列源码（ReentrantLock 版）","type":"posts"},{"content":"📌 本文原发布于掘金社区：JDK 定时任务 Timer 与 ScheduledExecutorService 排坑记录\n正在认真敲代码的楼主，突然收到数据维护系统发过来的报警邮件说楼主凌晨跑的定时任务没有成功，于是便开始了楼主今天的找坑填坑的过程。\n定时任务，关于 Timer 与 ScheduledExecutorService 的抉择 # 这事肯定会有小伙伴说了为啥不用 Quartz 啊，因为楼主的庙小啊，就几个定时任务而已 Quartz 太重了。\nTimer 存在的问题 # Timer 的主要问题在于，如果 TimerTask 抛出未检查的异常，Timer 将会产生无法预料的行为。Timer 线程并不捕获异常，所以 TimerTask 抛出的未检查的异常会终止 timer 线程，这种情况下，Timer 也不会再恢复线程的执行了；它错误地认为整个 Timer 都被取消了。此时，已经被安排但尚未执行的 TimerTask 永远不会再执行了，新的任务也不能被调度了。\n使用 ScheduledExecutorService # ScheduledExecutorService 是 JDK 1.5 之后 concurrent 包下提供的 API。ScheduledExecutorService 妥善地处理了抛出异常的任务，所以说在 JDK1.5 或更高的 JDK 中，楼主不建议使用 Timer。关于 ScheduledExecutorService 楼主的另一篇文章也有提到，感兴趣的小伙伴请移步Java 实现终止线程池中正在运行的定时任务\n产生的问题 # 上面说了一堆 Timer 与 ScheduledExecutorService 的区别，有点不着重点，现在重点来了，楼主凌晨的定时任务没有跑成功就是使用了 ScheduledExecutorService 而不是 Timer，当然倘若使用了 Timer 而导致的问题楼主也没必要说了。下面开始我们的找坑过程\n查日志 # 通过 JDK 提供的 jstack 工具获取服务的 jstack 日志，如下所示：\n2018-07-18 15:39:09 Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode): \u0026quot;Attach Listener\u0026quot; daemon prio=10 tid=0x00007f5bd0007800 nid=0x4e78 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE \u0026quot;DestroyJavaVM\u0026quot; prio=10 tid=0x00007f5bec008800 nid=0x1dd5 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE \u0026quot;Thread-4\u0026quot; prio=10 tid=0x00007f5bec4be000 nid=0x1df8 runnable [0x00007f5be88b2000] java.lang.Thread.State: RUNNABLE at java.net.PlainDatagramSocketImpl.receive0(Native Method) - locked \u0026lt;0x00000000e7e450b8\u0026gt; (a java.net.PlainDatagramSocketImpl) at java.net.AbstractPlainDatagramSocketImpl.receive(AbstractPlainDatagramSocketImpl.java:146) - locked \u0026lt;0x00000000e7e450b8\u0026gt; (a java.net.PlainDatagramSocketImpl) at java.net.DatagramSocket.receive(DatagramSocket.java:816) - locked \u0026lt;0x00000000e7e450e8\u0026gt; (a java.net.DatagramPacket) - locked \u0026lt;0x00000000e7e45110\u0026gt; (a java.net.DatagramSocket) at com.yg84.chelaile.middleware.common.remotedata.protocol.udp.UDPReceiver$1.run(UDPReceiver.java:55) at java.lang.Thread.run(Thread.java:745) \u0026quot;pool-5-thread-1\u0026quot; prio=10 tid=0x00007f5bec4ba800 nid=0x1df6 waiting on condition [0x00007f5be89b3000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for \u0026lt;0x00000000e7f1e510\u0026gt; (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1090) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:807) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) \u0026quot;kafka-producer-network-thread | producer-1\u0026quot; daemon prio=10 tid=0x00007f5bec497000 nid=0x1df2 runnable [0x00007f5be8ffe000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:79) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:87) - locked \u0026lt;0x00000000e7e45778\u0026gt; (a sun.nio.ch.Util$2) - locked \u0026lt;0x00000000e7e45788\u0026gt; (a java.util.Collections$UnmodifiableSet) - locked \u0026lt;0x00000000e7e45730\u0026gt; (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:98) at org.apache.kafka.common.network.Selector.select(Selector.java:454) at org.apache.kafka.common.network.Selector.poll(Selector.java:277) at org.apache.kafka.clients.NetworkClient.poll(NetworkClient.java:260) at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:229) at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:134) at java.lang.Thread.run(Thread.java:745) \u0026quot;AsyncLoggerConfig-1\u0026quot; daemon prio=10 tid=0x00007f5bec32a800 nid=0x1dee waiting on condition [0x00007f5bf04c2000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for \u0026lt;0x00000000e7638090\u0026gt; (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043) at com.lmax.disruptor.BlockingWaitStrategy.waitFor(BlockingWaitStrategy.java:45) at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:55) at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:123) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) \u0026quot;Service Thread\u0026quot; daemon prio=10 tid=0x00007f5bec0b3800 nid=0x1ddc runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE \u0026quot;C2 CompilerThread1\u0026quot; daemon prio=10 tid=0x00007f5bec0b1000 nid=0x1ddb waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE \u0026quot;C2 CompilerThread0\u0026quot; daemon prio=10 tid=0x00007f5bec0ae800 nid=0x1dda waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE \u0026quot;Signal Dispatcher\u0026quot; daemon prio=10 tid=0x00007f5bec0ac800 nid=0x1dd9 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE \u0026quot;Finalizer\u0026quot; daemon prio=10 tid=0x00007f5bec08c000 nid=0x1dd8 in Object.wait() [0x00007f5bf163a000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on \u0026lt;0x00000000e74a07a0\u0026gt; (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135) - locked \u0026lt;0x00000000e74a07a0\u0026gt; (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) \u0026quot;Reference Handler\u0026quot; daemon prio=10 tid=0x00007f5bec08a000 nid=0x1dd7 in Object.wait() [0x00007f5bf173b000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on \u0026lt;0x00000000e74a0848\u0026gt; (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:503) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133) - locked \u0026lt;0x00000000e74a0848\u0026gt; (a java.lang.ref.Reference$Lock) \u0026quot;VM Thread\u0026quot; prio=10 tid=0x00007f5bec085800 nid=0x1dd6 runnable \u0026quot;VM Periodic Task Thread\u0026quot; prio=10 tid=0x00007f5bec0b7000 nid=0x1ddd waiting on condition JNI global references: 165 通过 jstack 日志并没有发现线程死锁，并且看到楼主开启的 ScheduledThreadPoolExecutor 也在正常运行，并没有发现问题。\n\u0026quot;pool-5-thread-1\u0026quot; prio=10 tid=0x00007f5bec4ba800 nid=0x1df6 waiting on condition [0x00007f5be89b3000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for \u0026lt;0x00000000e7f1e510\u0026gt; (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1090) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:807) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) 扒源码 # 因为出问题的是定时任务没有执行，所以楼主打算看一下 ScheduledExecutorService 中 scheduleAtFixedRate 的代码\n/** * Creates and executes a periodic action that becomes enabled first * after the given initial delay, and subsequently with the given * period; that is executions will commence after * {@code initialDelay} then {@code initialDelay+period}, then * {@code initialDelay + 2 * period}, and so on. * If any execution of the task * encounters an exception, subsequent executions are suppressed. * Otherwise, the task will only terminate via cancellation or * termination of the executor. If any execution of this task * takes longer than its period, then subsequent executions * may start late, but will not concurrently execute. * * @param command the task to execute * @param initialDelay the time to delay first execution * @param period the period between successive executions * @param unit the time unit of the initialDelay and period parameters * @return a ScheduledFuture representing pending completion of * the task, and whose {@code get()} method will throw an * exception upon cancellation * @throws RejectedExecutionException if the task cannot be * scheduled for execution * @throws NullPointerException if command is null * @throws IllegalArgumentException if period less than or equal to zero */ public ScheduledFuture\u0026lt;?\u0026gt; scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); 果不其然，通过这个方法的 doc 中下面的这段话，大概可以知道任务遇到异常的时候，这个任务就不再执行。当然其他任务是不受影响的。\nIf any execution of the task encounters an exception, subsequent executions are suppressed. Otherwise, the task will only terminate via cancellation or termination of the executor. If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute. 复盘 # 根据楼主上面的分析过程，可以知道出问题的原因就是代码中抛出了非受检异常，下面是楼主的测试代码，代码很简单，就是使用 ScheduledExecutorService 启动两个定时任务，其中一个抛出空指针异常，线程不捕获。\npublic class TestCase { public static void main (String[] args) { ScheduledExecutorService service = Executors .newSingleThreadScheduledExecutor(); service.scheduleAtFixedRate(new TestJob(), 0, 1000, TimeUnit.MILLISECONDS); service.scheduleAtFixedRate(new TestJob2(), 0, 1000, TimeUnit.MILLISECONDS); } static class TestJob implements Runnable { int count = 0; public TestJob() { } @Override public void run() { // try { count++; System.out.println(\u0026quot;TestJob count : \u0026quot; + count); if (count == 5) { throw new NullPointerException(); } // } catch (Exception e) { // e.printStackTrace(); // } } } static class TestJob2 implements Runnable { int count = 0; public TestJob2() { } @Override public void run() { count++; System.out.println(\u0026quot;TestJob2 count : \u0026quot; + count); } } } 代码运行结果：在 TestJob 抛出异常后不再执行，TestJob2 不受影响可以继续执行。\n运行结果\n问题解决 # 问题产生的根源找到了，解决起来就比较简单了，具体代码如下所示\nstatic class TestJob implements Runnable { int count = 0; public TestJob() { } @Override public void run() { try { count++; System.out.println(\u0026quot;TestJob count : \u0026quot; + count); if (count == 5) { throw new NullPointerException(); } } catch (Exception e) { e.printStackTrace(); } } } 小结 # 总的来看这个问题也比较简单，相对来说比较隐蔽一些。当然，这跟自己平时的编码规范有很大关系。\n作 者：haifeiWu\n原文链接：www.hchstudio.cn/article/201…\n版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-07-18","externalUrl":null,"permalink":"/posts/jdk-dingshirenwu-timer-yu-scheduledexecutorservice-paikengji/","section":"文章","summary":"正在认真敲代码的楼主，突然收到数据维护系统发过来的报警邮件说楼主凌晨跑的定时任务没有成功，于是便开始了楼主今天的找坑填坑的过程。","title":"JDK 定时任务 Timer 与 ScheduledExecutorService 排坑记录","type":"posts"},{"content":"📌 本文原发布于掘金社区：TCP 粘包问题浅析及其解决方案\n最近一直在做中间件相关的东西，所以接触到的各种协议比较多，总的来说有 TCP，UDP，HTTP 等各种网络传输协议，因此楼主想先从协议最基本的 TCP 粘包问题搞起，把计算机网络这部分基础夯实一下。\nTCP 协议的简单介绍 # TCP 是面向连接的运输层协议\n简单来说，在使用 TCP 协议之前，必须先建立 TCP 连接，就是我们常说的三次握手。在数据传输完毕之后，必须释放已经建立的 TCP 连接，否则会发生不可预知的问题，造成服务的不可用状态。\n每一条 TCP 连接都是可靠连接，且只有两个端点\nTCP 连接是从 Server 端到 Client 端的点对点的连接，通过 TCP 传输数据，无差错，不重复不丢失。\nTCP 协议的通信是全双工的\nTCP 协议允许通信双方的应用程序在任何时候都能发送数据。TCP 连接的两端都设有发送缓冲区和接收缓冲区，用来临时存放双向通信的数据。发送数据时，应用程序把数据传送给 TCP 的缓冲区后，就可以做自己的事情，而 TCP 在合适的时候将数据发送出去。在接收的时候，TCP 把收到的数据放入接收缓冲区，上层应用在合适的时候读取数据。\nTCP 协议是面向字节流的\nTCP 中的流是指流入进程或者从进程中流出的字节序列。所以像 Java，golang 等高级语言在进行 TCP 通信时都需要将相应的实体序列化才能进行传输。还有就是在我们使用 Redis 做缓存的时候，都需要将放入 Redis 的数据序列化才可以，原因就是 Redis 底层是基于 TCP 协议实现的。\n**TCP 并不知道所传输的字节流的含义，TCP 并不能保证接收方应用程序和发送方应用程序所发出的数据块具有对应大小的关系（这就是 TCP 传输过程中产生的粘包问题）。**但是应用程序接收方最终收到的字节流与发送方发送的字节流是一定相同的。因此，我们在使用 TCP 协议的时候应该制定合理的粘包拆包策略。\n下图是 TCP 的协议传输的整个过程：\nTCP 面向字节流\n下面这个图是从老钱的博客里面取到的，非常生动\nTCP 传输动图\nTCP 粘包问题复现 # 理论推敲 # 如下图所示，出现的粘包问题一共有三种情况\nTCP 粘包问题\n第一种情况：\n如上图中的第一根bar所示，服务端一共读到两个数据包，每个数据包都是完整的，并没有发生粘包的问题，这种情况比较好处理，服务器只需要简单地从网络缓冲区去读就好了，每次服务端读取到的消息都是完整的，并不会出现数据不正确的情况。\n第二种情况：\n服务端仅收到一个数据包，这个数据包包含客户端发出的两条消息的完整信息，这个时候基于第一种情况的逻辑实现的服务端就蒙了，因为服务端并不能很好地处理这个数据包，甚至不能处理，这种情况其实就是 TCP 的粘包问题。\n第三种情况：\n服务端收到了两个数据包，第一个数据包只包含了第一条消息的一部分，第一条消息的后半部分和第二条消息都在第二个数据包中，或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息，第二个数据包包含了第二条消息的剩下部分，这种情况其实是发生了 TCP 拆包问题，因为一条消息被拆分在两个包里面发送了，同样上面的服务器逻辑对于这种情况是不好处理的。\n为什么会发生 TCP 粘包、拆包\n应用程序写入的数据大于套接字缓冲区大小，这将会发生拆包。\n应用程序写入数据小于套接字缓冲区大小，网卡将应用多次写入的数据发送到网络上，这将会发生粘包。\n进行 MSS（最大报文长度）大小的 TCP 分段，当 TCP 报文长度-TCP 头部长度\u0026gt;MSS 的时候将发生拆包。\n接收方不及时读取套接字缓冲区数据，这将发生粘包。\n如何处理粘包、拆包\n通常会有以下一些常用的方法：\n使用带消息头的协议、消息头存储消息开始标识及消息长度信息，服务端获取消息头的时候解析出消息长度，然后向后读取该长度的内容。\n设置定长消息，服务端每次读取既定长度的内容作为一条完整消息，当消息不够长时，空位补上固定字符。\n设置消息边界，服务端从网络流中按消息边界分离出消息内容，一般使用‘\\n’。\n更为复杂的协议，例如楼主最近接触比较多的车联网协议 808,809 协议。\nTCP 粘包拆包的代码实践 # 下面代码楼主主要演示了使用规定消息头，消息体的方式来解决 TCP 的粘包，拆包问题。\nserver 端代码： server 端代码的主要逻辑是接收客户端发送过来的消息，重新组装出消息，并打印出来。\nimport java.io.*; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; /** * @author wuhf * @Date 2018/7/16 15:50 **/ public class TestSocketServer { public static void main(String args[]) { ServerSocket serverSocket; try { serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(8089)); while (true) { Socket socket = serverSocket.accept(); new ReceiveThread(socket).start(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } static class ReceiveThread extends Thread { public static final int PACKET_HEAD_LENGTH = 2;//包头长度 private Socket socket; private volatile byte[] bytes = new byte[0]; public ReceiveThread(Socket socket) { this.socket = socket; } public byte[] mergebyte(byte[] a, byte[] b, int begin, int end) { byte[] add = new byte[a.length + end - begin]; int i = 0; for (i = 0; i \u0026lt; a.length; i++) { add[i] = a[i]; } for (int k = begin; k \u0026lt; end; k++, i++) { add[i] = b[k]; } return add; } @Override public void run() { int count = 0; while (true) { try { InputStream reader = socket.getInputStream(); if (bytes.length \u0026lt; PACKET_HEAD_LENGTH) { byte[] head = new byte[PACKET_HEAD_LENGTH - bytes.length]; int couter = reader.read(head); if (couter \u0026lt; 0) { continue; } bytes = mergebyte(bytes, head, 0, couter); if (couter \u0026lt; PACKET_HEAD_LENGTH) { continue; } } // 下面这个值请注意，一定要取2长度的字节子数组作为报文长度，你懂得 byte[] temp = new byte[0]; temp = mergebyte(temp, bytes, 0, PACKET_HEAD_LENGTH); String templength = new String(temp); int bodylength = Integer.parseInt(templength);//包体长度 if (bytes.length - PACKET_HEAD_LENGTH \u0026lt; bodylength) {//不够一个包 byte[] body = new byte[bodylength + PACKET_HEAD_LENGTH - bytes.length];//剩下应该读的字节(凑一个包) int couter = reader.read(body); if (couter \u0026lt; 0) { continue; } bytes = mergebyte(bytes, body, 0, couter); if (couter \u0026lt; body.length) { continue; } } byte[] body = new byte[0]; body = mergebyte(body, bytes, PACKET_HEAD_LENGTH, bytes.length); count++; System.out.println(\u0026quot;server receive body: \u0026quot; + count + new String(body)); bytes = new byte[0]; } catch (Exception e) { e.printStackTrace(); } } } } } **client 端代码：**客户端代码主要逻辑是组装要发送的消息，确定消息头，消息体，然后发送到服务端。\nimport java.io.*; import java.net.InetSocketAddress; import java.net.Socket; /** * @author wuhf * @Date 2018/7/16 15:45 **/ public class TestSocketClient { public static void main(String args[]) throws IOException { Socket clientSocket = new Socket(); clientSocket.connect(new InetSocketAddress(8089)); new SendThread(clientSocket).start(); } static class SendThread extends Thread { Socket socket; PrintWriter printWriter = null; public SendThread(Socket socket) { this.socket = socket; try { printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void run() { String reqMessage = \u0026quot;HelloWorld！ from clientsocket this is test half packages!\u0026quot;; for (int i = 0; i \u0026lt; 100; i++) { sendPacket(reqMessage); } if (socket != null) { try { socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public void sendPacket(String message) { try { OutputStream writer = socket.getOutputStream(); writer.write(message.getBytes()); writer.flush(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } 小结 # 最近一直在写一些框架性的博客，专门针对某些问题进行原理性的技术探讨的博客还比较少，所以楼主想着怎样能在自己学到东西的同时也可以给一同在技术这条野路子上奋斗的小伙伴们一些启发，这是楼主一直努力的方向。\n参考文章 # Netty 精粹之 TCP 粘包拆包问题 计算机网络-谢希仁-第七版 作 者：haifeiWu\n原文链接：www.hchstudio.cn/article/201…\n版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-07-16","externalUrl":null,"permalink":"/posts/tcp-zhanbaowentiqianxijiqijiejuefangan/","section":"文章","summary":"最近一直在做中间件相关的东西，所以接触到的各种协议比较多，总的来说有 TCP，UDP，HTTP 等各种网络传输协议，因此楼主想先从协议最基本的 TCP 粘包问题搞起，把计算机网络这部分基础夯实一下。","title":"TCP 粘包问题浅析及其解决方案","type":"posts"},{"content":"📌 本文原发布于掘金社区：I-team 博客全文检索 Elasticsearch 实战\n一直觉得博客缺点东西，最近还是发现了，当博客慢慢多起来的时候想要找一篇之前写的博客很是麻烦，于是作为后端开发的楼主觉得自己动手丰衣足食，也就有了这次博客全文检索功能 Elasticsearch 实战，这里还要感谢一下‘辉哥’赞助的一台服务器。\n全文检索工具选型 # 众所周知，支持全文检索的工具有很多，像 Lucene，solr，Elasticsearch 等，相比于其他的工具，显然 Elasticsearch 社区更加活跃，遇到问题相对来说也比较好解决，另外 Elasticsearch 提供的 restful 接口操作起来还是比较方便的，这也是楼主选择 Elasticsearch 的重要原因，当然 Elasticsearch 占据的内存相对来说比较大，楼主 2G 的云服务器跑起来也是捉襟见肘。\n数据迁移，从 MySQL 到 Elasticsearch # 这个功能相对来说比较简单，就是定时从 MySQL 更新数据到 Elasticsearch 中，本来楼主打算自己写一个数据迁移的工具，但是想起之前楼主做数据迁移时用到的 DataX 很是不错，看了下官方文档还是支持的，但是楼主硬是没有跑起来，原因就是楼主 2G 内存的云服务器不够使啊，DataX 光是跑起来就要 1G 多的内存，所以楼主只能另谋它法。对 DataX 感兴趣的小伙伴可以看看楼主的另一篇文章阿里离线数据同步工具 DataX 踩坑记录。\n说起可以省内存的语言，小伙伴可能会想到最近比较火的 golang，没错楼主也想到了。最后楼主使用的就是一个叫 go-mysql-elasticsearch 的工具，它是使用 golang 实现的从 MySQL 将数据迁移到 Elasticsearch 的工具。具体搭建过程楼主不在这里细说，感兴趣的小伙伴请移步go-mysql-elasticsearch，另外 Elasticsearch 环境的搭建，需要注意的就是安装 Elasticsearch 的机器内存应该大于或者等于 2G，否则可能会出现启动不起来的情况，楼主也不在这里赘述了，比较简单，请小伙伴们自行 google。\n另外需要注意的是，在使用 go-mysql-elasticsearch 的时候应该开启 MySQL 的 binlog 功能，go-mysql-elasticsearch 实现数据同步的思想就是将自己作为 MySQL 的一个 slave 挂载在 MySQL 上，这样就可以很轻松地将数据实时同步到 Elasticsearch 中，在启动 go-mysql-elasticsearch 的机器上最少应该有 MySQL client 工具，否则会启动报错。楼主的建议是跟 MySQL 部署在同一台机器上，因为 golang 耗费内存极少，并不会有太大影响。下面给出楼主同步数据时 go-mysql-elasticsearch 的配置文件：\n# MySQL address, user and password # user must have replication privilege in MySQL. my_addr = \u0026quot;127.0.0.1:3306\u0026quot; my_user = \u0026quot;root\u0026quot; my_pass = \u0026quot;******\u0026quot; my_charset = \u0026quot;utf8\u0026quot; # Set true when elasticsearch use https #es_https = false # Elasticsearch address es_addr = \u0026quot;127.0.0.1:9200\u0026quot; # Elasticsearch user and password, maybe set by shield, nginx, or x-pack es_user = \u0026quot;\u0026quot; es_pass = \u0026quot;\u0026quot; # Path to store data, like master.info, if not set or empty, # we must use this to support breakpoint resume syncing. # TODO: support other storage, like etcd. data_dir = \u0026quot;./var\u0026quot; # Inner Http status address stat_addr = \u0026quot;127.0.0.1:12800\u0026quot; # pseudo server id like a slave server_id = 1001 # mysql or mariadb flavor = \u0026quot;mysql\u0026quot; # mysqldump execution path # if not set or empty, ignore mysqldump. mysqldump = \u0026quot;mysqldump\u0026quot; # if we have no privilege to use mysqldump with --master-data, # we must skip it. #skip_master_data = false # minimal items to be inserted in one bulk bulk_size = 128 # force flush the pending requests if we don't have enough items \u0026gt;= bulk_size flush_bulk_time = \u0026quot;200ms\u0026quot; # Ignore table without primary key skip_no_pk_table = false # MySQL data source [[source]] schema = \u0026quot;billboard-blog\u0026quot; # Only below tables will be synced into Elasticsearch. tables = [\u0026quot;content\u0026quot;] # Below is for special rule mapping [[rule]] schema = \u0026quot;billboard-blog\u0026quot; table = \u0026quot;content\u0026quot; index = \u0026quot;contentindex\u0026quot; type = \u0026quot;content\u0026quot; [rule.field] title=\u0026quot;title\u0026quot; blog_desc=\u0026quot;blog_desc\u0026quot; content=\u0026quot;content\u0026quot; # Filter rule [[rule]] schema = \u0026quot;billboard-blog\u0026quot; table = \u0026quot;content\u0026quot; index = \u0026quot;contentindex\u0026quot; type = \u0026quot;content\u0026quot; # Only sync following columns filter = [\u0026quot;title\u0026quot;, \u0026quot;blog_desc\u0026quot;, \u0026quot;content\u0026quot;] # id rule [[rule]] schema = \u0026quot;billboard-blog\u0026quot; table = \u0026quot;content\u0026quot; index = \u0026quot;contentindex\u0026quot; type = \u0026quot;content\u0026quot; id = [\u0026quot;id\u0026quot;] 实现全文检索功能的服务 # 要想实现全文检索的功能并对外提供服务，web 服务必不可少，楼主使用 Spring Boot 搭建 web 服务，对 Spring Boot 感兴趣的小伙伴也可以看一下楼主的另一篇文章，使用 Spring Boot 实现博客统计服务。好了废话不多说了，请看代码\n接口实现代码，代码比较简单就是接收参数，调用 service 代码\n@ApiOperation(value=\u0026quot;全文检索接口\u0026quot;, notes=\u0026quot;\u0026quot;) @ApiImplicitParam(name = \u0026quot;searchParam\u0026quot;, value = \u0026quot;博客搜索条件（作者，描述，内容，标题）\u0026quot;, required = true, dataType = \u0026quot;String\u0026quot;) @RequestMapping(value = \u0026quot;/get_content_list_from_es\u0026quot;, method = RequestMethod.GET) public ResultCode\u0026lt;List\u0026lt;ContentsWithBLOBs\u0026gt;\u0026gt; getContentListFromEs(String searchParam) { ResultCode\u0026lt;List\u0026lt;ContentsWithBLOBs\u0026gt;\u0026gt; resultCode = new ResultCode(); try { LOGGER.info(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; method getContentListFromEs request params : {},{}，{}\u0026quot;,searchParam); resultCode = contentService.getContentListFromEs(searchParam); LOGGER.info(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; method getContentListFromEs return value : {}\u0026quot;,JSON.toJSONString(resultCode)); } catch (Exception e) { e.printStackTrace(); resultCode.setCode(Messages.API_ERROR_CODE); resultCode.setMsg(Messages.API_ERROR_MSG); } return resultCode; } service 代码实现，这里的代码主要功能就是调用 es 的工具类，对博客描述，作者，博客标题，博客内容进行全文检索。\n@Override public ResultCode\u0026lt;List\u0026lt;ContentsWithBLOBs\u0026gt;\u0026gt; getContentListFromEs(String searchParam) { ResultCode resultCode = new ResultCode(); // 校验参数，参数不能为空 if (StringUtils.isBlank(searchParam)) { LOGGER.info(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; params not be null\u0026quot;); resultCode.setMsg(Messages.INPUT_ERROR_MSG); resultCode.setCode(Messages.INPUT_ERROR_CODE); return resultCode; } String matchStr = \u0026quot;blog_desc=\u0026quot; + searchParam; List\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; result = ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr); matchStr = \u0026quot;author=\u0026quot; + searchParam; result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr)); matchStr = \u0026quot;title=\u0026quot; + searchParam; result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr)); matchStr = \u0026quot;content=\u0026quot; + searchParam; result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr)); List\u0026lt;ContentsWithBLOBs\u0026gt; data = JSON.parseArray(JSON.toJSONString(result),ContentsWithBLOBs.class); LOGGER.info(\u0026quot;es return data : {}\u0026quot;,JSON.toJSONString(result)); resultCode.setData(data); return resultCode; } 楼主用到的 es 的工具类代码实现，就是使用 es 的 Java 客户端对 es 进行检索。\n/** * 使用分词查询 * * @param index 索引名称 * @param type 类型名称,可传入多个type逗号分隔 * @param fields 需要显示的字段，逗号分隔（缺省为全部字段） * @param matchPhrase true 使用，短语精准匹配 * @param matchStr 过滤条件（xxx=111,aaa=222） * @return */ public static List\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; searchListData(String index, String type, String fields, boolean matchPhrase, String matchStr) { return searchListData(index, type, 0, 0, null, fields, null, matchPhrase, null, matchStr); } /** * 使用分词查询 * * @param index 索引名称 * @param type 类型名称,可传入多个type逗号分隔 * @param startTime 开始时间 * @param endTime 结束时间 * @param size 文档大小限制 * @param fields 需要显示的字段，逗号分隔（缺省为全部字段） * @param sortField 排序字段 * @param matchPhrase true 使用，短语精准匹配 * @param highlightField 高亮字段 * @param matchStr 过滤条件（xxx=111,aaa=222） * @return */ public static List\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; searchListData(String index, String type, long startTime, long endTime, Integer size, String fields, String sortField, boolean matchPhrase, String highlightField, String matchStr) { SearchRequestBuilder searchRequestBuilder = client.prepareSearch(index); if (StringUtils.isNotEmpty(type)) { searchRequestBuilder.setTypes(type.split(\u0026quot;,\u0026quot;)); } BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (startTime \u0026gt; 0 \u0026amp;\u0026amp; endTime \u0026gt; 0) { boolQuery.must(QueryBuilders.rangeQuery(\u0026quot;processTime\u0026quot;) .format(\u0026quot;epoch_millis\u0026quot;) .from(startTime) .to(endTime) .includeLower(true) .includeUpper(true)); } //搜索的的字段 if (StringUtils.isNotEmpty(matchStr)) { for (String s : matchStr.split(\u0026quot;,\u0026quot;)) { String[] ss = s.split(\u0026quot;=\u0026quot;); if (ss.length \u0026gt; 1) { if (matchPhrase == Boolean.TRUE) { boolQuery.must(QueryBuilders.matchPhraseQuery(s.split(\u0026quot;=\u0026quot;)[0], s.split(\u0026quot;=\u0026quot;)[1])); } else { boolQuery.must(QueryBuilders.matchQuery(s.split(\u0026quot;=\u0026quot;)[0], s.split(\u0026quot;=\u0026quot;)[1])); } } } } // 高亮（xxx=111,aaa=222） if (StringUtils.isNotEmpty(highlightField)) { HighlightBuilder highlightBuilder = new HighlightBuilder(); //highlightBuilder.preTags(\u0026quot;\u0026lt;span style='color:red' \u0026gt;\u0026quot;);//设置前缀 //highlightBuilder.postTags(\u0026quot;\u0026lt;/span\u0026gt;\u0026quot;);//设置后缀 // 设置高亮字段 highlightBuilder.field(highlightField); searchRequestBuilder.highlighter(highlightBuilder); } searchRequestBuilder.setQuery(boolQuery); if (StringUtils.isNotEmpty(fields)) { searchRequestBuilder.setFetchSource(fields.split(\u0026quot;,\u0026quot;), null); } searchRequestBuilder.setFetchSource(true); if (StringUtils.isNotEmpty(sortField)) { searchRequestBuilder.addSort(sortField, SortOrder.DESC); } if (size != null \u0026amp;\u0026amp; size \u0026gt; 0) { searchRequestBuilder.setSize(size); } //打印的内容 可以在 Elasticsearch head 和 Kibana 上执行查询 LOGGER.info(\u0026quot;\\n{}\u0026quot;, searchRequestBuilder); SearchResponse searchResponse = searchRequestBuilder.execute().actionGet(); long totalHits = searchResponse.getHits().totalHits; long length = searchResponse.getHits().getHits().length; LOGGER.info(\u0026quot;共查询到[{}]条数据,处理数据条数[{}]\u0026quot;, totalHits, length); if (searchResponse.status().getStatus() == 200) { // 解析对象 return setSearchResponse(searchResponse, highlightField); } return null; } 最后，楼主使用 postman 测试 web 服务，如下图所示：\npostman 图\n过程中遇到的坑 # IK 分词器的设置 # 这里需要注意的是，Elasticsearch 的版本一定要与 ik 分词器的版本对应，不对应的话 Elasticsearch 会报错的。\n$ ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip 接着，重新启动 Elastic，就会自动加载这个新安装的插件。\n然后，新建一个 Index，指定需要分词的字段。这一步根据数据结构而异，下面的命令只针对本文。基本上，凡是需要搜索的中文字段，都要单独设置一下。\n$ curl -X PUT 'localhost:9200/contentindex' -H 'Content-Type: application/json' -d ' { \u0026quot;mappings\u0026quot;: { \u0026quot;content\u0026quot;: { \u0026quot;properties\u0026quot;: { \u0026quot;content\u0026quot;: { \u0026quot;type\u0026quot;: \u0026quot;text\u0026quot;, \u0026quot;analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot;, \u0026quot;search_analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot; }, \u0026quot;title\u0026quot;: { \u0026quot;type\u0026quot;: \u0026quot;text\u0026quot;, \u0026quot;analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot;, \u0026quot;search_analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot; }, \u0026quot;blog_desc\u0026quot;: { \u0026quot;type\u0026quot;: \u0026quot;text\u0026quot;, \u0026quot;analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot;, \u0026quot;search_analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot; }, \u0026quot;author\u0026quot;: { \u0026quot;type\u0026quot;: \u0026quot;text\u0026quot;, \u0026quot;analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot;, \u0026quot;search_analyzer\u0026quot;: \u0026quot;ik_max_word\u0026quot; } } } } }' 上面代码中，首先新建一个名称为 contentindex 的 Index，里面有一个名称为 content 的 Type。content 有好多个字段，这里只为其中四个字段指定分词，content，title，blog_desc，author。\n这四个字段都是中文，而且类型都是文本（text），所以需要指定中文分词器，不能使用默认的英文分词器。\nMySQL binlog 的设置 # 因为楼主运行 go-mysql-elasticsearch 的时候使用的 MySQL 的客户端跟要导出数据的 MySQL server 端的版本不一致导致报错，最终在 go-mysql-elasticsearch 原作者的帮助下解决，所以一定要使用同版本的 MySQL server 与 client，因为不同版本的 MySQL 特性不一样，也就导致了 go-mysql-elasticsearch 导出数据有略微的不同。\n小结 # 整个过程相对来说比较简单，当然楼主通过这个功能的实现，也对 es 有了一个相对的认识，学习了一项新的技能，可能有的小伙伴对楼主的整个工程的代码比较感兴趣，暂时不能透露，等楼主完善好了一并贡献出来。\n参考文章 # 全文搜索引擎 Elasticsearch 入门教程 作 者：haifeiWu\n原文链接：www.hchstudio.cn/article/201…\n版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-07-12","externalUrl":null,"permalink":"/posts/i-team-bokequanwenjiansuo-elasticsearch-shizhan/","section":"文章","summary":"一直觉得博客缺点东西，最近还是发现了，当博客慢慢多起来的时候想要找一篇之前写的博客很是麻烦，于是作为后端开发的楼主觉得自己动手丰衣足食，也就有了这次博客全文检索功能 Elasticsearch 实战，这里还要感谢一下‘辉哥’赞助的一台服务器。","title":"I-team 博客全文检索 Elasticsearch 实战","type":"posts"},{"content":"","date":"2018-07-06","externalUrl":null,"permalink":"/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/","section":"标签","summary":"","title":"数据库","type":"tags"},{"content":"📌 本文原发布于掘金社区：阿里云离线同步工具 DataX 源码略读\n最近在做一些数据迁移相关工作，并最终采用了 DataX，楼主也本着知其然，也要知其所以然的精神粗略地看一看 Datax 的源码。\n代码下载 # 相信这部分作为程序员的我们，大家都知道，github 是个大家庭，所以楼主直接去 github 上扒下来就好了，项目是基于 Maven 构建的，直接使用 IDE 导入工程即可。\ngit clone git@github.com:alibaba/DataX.git 快速开始 # 这部分楼主也不多说了，因为楼主在上篇文章中已经讲清楚了，感兴趣的小伙伴请移步阿里离线数据同步工具 DataX 踩坑记录\nDataX 概览（摘抄自官网） # DataX 结构图\n设计理念\n为了解决异构数据源同步问题，DataX 将复杂的网状同步链路变成了星型数据链路，DataX 作为中间传输载体负责连接各种数据源。当需要接入一个新的数据源的时候，只需要将此数据源对接到 DataX，便能跟已有的数据源做到无缝数据同步。\n当前使用现状\nDataX 在阿里巴巴集团内被广泛使用，承担了所有大数据的离线同步业务，并已持续稳定运行了 6年之久。目前每天完成同步 8w 多道作业，每日传输数据量超过 300TB。\n源码分析 # 如上图所示，DataX 的源码大部分都是用来适配各种各样的数据源的 reader 与 writer，核心转换的 framework 就是 core 这个 model，打开这 model，我们可以直接看到一个叫 Engine 的文件\nDataX 结构图\n根据我们的代码习惯，这个就该是 DataX 的入口代码，如下所示，代码是删除异常处理的代码，很简单，就三行，我们接着看 entry 这个方法\npublic static void main(String[] args) throws Exception { int exitCode = 0; Engine.entry(args); System.exit(exitCode); } 通过代码我们知道，这个方法就是初始化一些启动参数，并调用 start 方法\\\npublic static void entry(final String[] args) throws Throwable { Options options = new Options(); options.addOption(\u0026quot;job\u0026quot;, true, \u0026quot;Job config.\u0026quot;); options.addOption(\u0026quot;jobid\u0026quot;, true, \u0026quot;Job unique id.\u0026quot;); options.addOption(\u0026quot;mode\u0026quot;, true, \u0026quot;Job runtime mode.\u0026quot;); BasicParser parser = new BasicParser(); CommandLine cl = parser.parse(options, args); String jobPath = cl.getOptionValue(\u0026quot;job\u0026quot;); // 如果用户没有明确指定jobid, 则 datax.py 会指定 jobid 默认值为-1 String jobIdString = cl.getOptionValue(\u0026quot;jobid\u0026quot;); RUNTIME_MODE = cl.getOptionValue(\u0026quot;mode\u0026quot;); Configuration configuration = ConfigParser.parse(jobPath); long jobId; if (!\u0026quot;-1\u0026quot;.equalsIgnoreCase(jobIdString)) { jobId = Long.parseLong(jobIdString); } else { // only for dsc \u0026amp; ds \u0026amp; datax 3 update String dscJobUrlPatternString = \u0026quot;/instance/(\\\\d{1,})/config.xml\u0026quot;; String dsJobUrlPatternString = \u0026quot;/inner/job/(\\\\d{1,})/config\u0026quot;; String dsTaskGroupUrlPatternString = \u0026quot;/inner/job/(\\\\d{1,})/taskGroup/\u0026quot;; List\u0026lt;String\u0026gt; patternStringList = Arrays.asList(dscJobUrlPatternString, dsJobUrlPatternString, dsTaskGroupUrlPatternString); jobId = parseJobIdFromUrl(patternStringList, jobPath); } boolean isStandAloneMode = \u0026quot;standalone\u0026quot;.equalsIgnoreCase(RUNTIME_MODE); if (!isStandAloneMode \u0026amp;\u0026amp; jobId == -1) { // 如果不是 standalone 模式，那么 jobId 一定不能为-1 throw DataXException.asDataXException(FrameworkErrorCode.CONFIG_ERROR, \u0026quot;非 standalone 模式必须在 URL 中提供有效的 jobId.\u0026quot;); } configuration.set(CoreConstant.DATAX_CORE_CONTAINER_JOB_ID, jobId); //打印vmInfo VMInfo vmInfo = VMInfo.getVmInfo(); if (vmInfo != null) { LOG.info(vmInfo.toString()); } LOG.info(\u0026quot;\\n\u0026quot; + Engine.filterJobConfiguration(configuration) + \u0026quot;\\n\u0026quot;); LOG.debug(configuration.toJSON()); ConfigurationValidate.doValidate(configuration); Engine engine = new Engine(); engine.start(configuration); } 下面代码就是获取配置文件中的配置，初始化 container 的具体类型，初始化 Job 或者 Task 的运行容器，并运行插件的 Job 或者 Task 逻辑\\\n/* check job model (job/task) first */ public void start(Configuration allConf) { // 绑定column转换信息 ColumnCast.bind(allConf); /** * 初始化PluginLoader，可以获取各种插件配置 */ LoadUtil.bind(allConf); boolean isJob = !(\u0026quot;taskGroup\u0026quot;.equalsIgnoreCase(allConf .getString(CoreConstant.DATAX_CORE_CONTAINER_MODEL))); //JobContainer会在schedule后再行进行设置和调整值 int channelNumber =0; AbstractContainer container; long instanceId; int taskGroupId = -1; if (isJob) { allConf.set(CoreConstant.DATAX_CORE_CONTAINER_JOB_MODE, RUNTIME_MODE); container = new JobContainer(allConf); instanceId = allConf.getLong( CoreConstant.DATAX_CORE_CONTAINER_JOB_ID, 0); } else { container = new TaskGroupContainer(allConf); instanceId = allConf.getLong( CoreConstant.DATAX_CORE_CONTAINER_JOB_ID); taskGroupId = allConf.getInt( CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_ID); channelNumber = allConf.getInt( CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL); } //缺省打开perfTrace boolean traceEnable = allConf.getBool(CoreConstant.DATAX_CORE_CONTAINER_TRACE_ENABLE, true); boolean perfReportEnable = allConf.getBool(CoreConstant.DATAX_CORE_REPORT_DATAX_PERFLOG, true); //standlone模式的datax shell任务不进行汇报 if(instanceId == -1){ perfReportEnable = false; } int priority = 0; try { priority = Integer.parseInt(System.getenv(\u0026quot;SKYNET_PRIORITY\u0026quot;)); }catch (NumberFormatException e){ LOG.warn(\u0026quot;prioriy set to 0, because NumberFormatException, the value is: \u0026quot;+System.getProperty(\u0026quot;PROIORY\u0026quot;)); } Configuration jobInfoConfig = allConf.getConfiguration(CoreConstant.DATAX_JOB_JOBINFO); //初始化PerfTrace PerfTrace perfTrace = PerfTrace.getInstance(isJob, instanceId, taskGroupId, priority, traceEnable); perfTrace.setJobInfo(jobInfoConfig,perfReportEnable,channelNumber); container.start(); } DataX 的 core 模块的配置文件 # 从配置文件中可以看出 DataX 要求的 JVM 的堆内存最小是 1G，这是之前楼主使用 DataX 遇到的一个小小的问题，其他的配置都是一些通用配置。\n{ \u0026quot;entry\u0026quot;: { \u0026quot;jvm\u0026quot;: \u0026quot;-Xms1G -Xmx1G\u0026quot;, \u0026quot;environment\u0026quot;: {} }, \u0026quot;common\u0026quot;: { \u0026quot;column\u0026quot;: { \u0026quot;datetimeFormat\u0026quot;: \u0026quot;yyyy-MM-dd HH:mm:ss\u0026quot;, \u0026quot;timeFormat\u0026quot;: \u0026quot;HH:mm:ss\u0026quot;, \u0026quot;dateFormat\u0026quot;: \u0026quot;yyyy-MM-dd\u0026quot;, \u0026quot;extraFormats\u0026quot;:[\u0026quot;yyyyMMdd\u0026quot;], \u0026quot;timeZone\u0026quot;: \u0026quot;GMT+8\u0026quot;, \u0026quot;encoding\u0026quot;: \u0026quot;utf-8\u0026quot; } }, \u0026quot;core\u0026quot;: { \u0026quot;dataXServer\u0026quot;: { \u0026quot;address\u0026quot;: \u0026quot;http://localhost:7001/api\u0026quot;, \u0026quot;timeout\u0026quot;: 10000, \u0026quot;reportDataxLog\u0026quot;: false, \u0026quot;reportPerfLog\u0026quot;: false }, \u0026quot;transport\u0026quot;: { \u0026quot;channel\u0026quot;: { \u0026quot;class\u0026quot;: \u0026quot;com.alibaba.datax.core.transport.channel.memory.MemoryChannel\u0026quot;, \u0026quot;speed\u0026quot;: { \u0026quot;byte\u0026quot;: -1, \u0026quot;record\u0026quot;: -1 }, \u0026quot;flowControlInterval\u0026quot;: 20, \u0026quot;capacity\u0026quot;: 512, \u0026quot;byteCapacity\u0026quot;: 67108864 }, \u0026quot;exchanger\u0026quot;: { \u0026quot;class\u0026quot;: \u0026quot;com.alibaba.datax.core.plugin.BufferedRecordExchanger\u0026quot;, \u0026quot;bufferSize\u0026quot;: 32 } }, \u0026quot;container\u0026quot;: { \u0026quot;job\u0026quot;: { \u0026quot;reportInterval\u0026quot;: 10000 }, \u0026quot;taskGroup\u0026quot;: { \u0026quot;channel\u0026quot;: 5 }, \u0026quot;trace\u0026quot;: { \u0026quot;enable\u0026quot;: \u0026quot;false\u0026quot; } }, \u0026quot;statistics\u0026quot;: { \u0026quot;collector\u0026quot;: { \u0026quot;plugin\u0026quot;: { \u0026quot;taskClass\u0026quot;: \u0026quot;com.alibaba.datax.core.statistics.plugin.task.StdoutPluginCollector\u0026quot;, \u0026quot;maxDirtyNumber\u0026quot;: 10 } } } } } 小结 # 这次楼主只是大概地梳理了一下 DataX 的代码，水平有点水，相信热爱学习的楼主，可以 day day up\n作 者：haifeiWu 原文链接：www.hchstudio.cn/article/201…版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-07-06","externalUrl":null,"permalink":"/posts/aliyunlixiantongbugongjudataxyuanmalvedu/","section":"文章","summary":"最近在做一些数据迁移相关工作，并最终采用了 DataX，楼主也本着知其然，也要知其所以然的精神粗略的看一看 Datax 的源码。","title":"阿里云离线同步工具 DataX 源码略读","type":"posts"},{"content":"","date":"2018-07-06","externalUrl":null,"permalink":"/tags/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4/","section":"标签","summary":"","title":"阿里巴巴","type":"tags"},{"content":"","date":"2018-07-04","externalUrl":null,"permalink":"/tags/oracle/","section":"标签","summary":"","title":"Oracle","type":"tags"},{"content":"📌 本文原发布于掘金社区：阿里离线数据同步工具 DataX 踩坑记录\n最近在做一些数据迁移相关工作，调研了一些工具，发现 DataX 是个不错的东西，所以安利给大家。那么 DataX 是什么呢？DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具，支持 MySQL、SQL Server、Oracle、PostgreSQL 等各种异构数据源之间高效的数据同步。\n主要功能 # DataX 本身作为数据同步框架，将不同数据源的同步抽象为从源头数据源读取数据的 Reader 插件，以及向目标端写入数据的 Writer 插件，理论上 DataX 框架可以支持任意数据源类型的数据同步工作。同时 DataX 插件体系作为一套生态系统，每接入一套新数据源，即可实现与现有数据源的互通。具体介绍请移步DataX 介绍\n系统要求 # Linux JDK(1.8 以上，推荐 1.8) Python(推荐 Python2.6.X) Apache Maven 3.x (Compile DataX) 设置 jvm 堆内存，堆内存要求大于 1g，否则会出现启动不了的情况 export JAVA_OPTS= -Xms1024m -Xmx1024m 快速开始 # 部署 DataX # 方法一、直接下载 DataX 工具包：DataX 下载地址 下载后解压至本地某个目录，进入 bin 目录，即可运行同步作业：\\\n$ cd {YOUR_DATAX_HOME}/bin $ python datax.py {YOUR_JOB.json} 方法二、下载 DataX 源码，自己编译：DataX 源码\n(1)、下载 DataX 源码：\n$ git clone git@github.com:alibaba/DataX.git (2)、通过 Maven 打包：\n$ cd {DataX_source_code_home} $ mvn -U clean package assembly:assembly -Dmaven.test.skip=true 打包成功，日志显示如下：\n[INFO] BUILD SUCCESS [INFO] ----------------------------------------------------------------- [INFO] Total time: 08:12 min [INFO] Finished at: 2018-06-05T16:26:48+08:00 [INFO] Final Memory: 133M/960M [INFO] ----------------------------------------------------------------- 打包成功后的 DataX 包位于 {DataX_source_code_home}/target/datax/datax/ ,\n生成配置文件 # 第一步、创建配置文件（JSON 格式）\n可以通过命令生成配置模板：\npython datax.py -r oraclereader -w mysqlwriter \u0026gt; oracle2mysql2.json 在 {DataX_source_code_home} 的 plugin 目录下有 DataX 支持的所有 reader 与 writer\n通过命令生成的配置模板如下所示，楼主生成的 reader 与 writer 对应的是从 oracle 读取数据，向 MySQL 写数据。\n{ \u0026quot;job\u0026quot;: { \u0026quot;content\u0026quot;: [{ \u0026quot;reader\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;oraclereader\u0026quot;, \u0026quot;parameter\u0026quot;: { \u0026quot;column\u0026quot;: [\u0026quot;*\u0026quot;], \u0026quot;connection\u0026quot;: [{ \u0026quot;jdbcUrl\u0026quot;: [\u0026quot;*\u0026quot;], \u0026quot;table\u0026quot;: [\u0026quot;tb1\u0026quot;] }], \u0026quot;password\u0026quot;: \u0026quot;***\u0026quot;, \u0026quot;username\u0026quot;: \u0026quot;***\u0026quot; } }, \u0026quot;writer\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;mysqlwriter\u0026quot;, \u0026quot;parameter\u0026quot;: { \u0026quot;column\u0026quot;: [\u0026quot;*\u0026quot;], \u0026quot;connection\u0026quot;: [{ \u0026quot;jdbcUrl\u0026quot;: \u0026quot;*\u0026quot;, \u0026quot;table\u0026quot;: [\u0026quot;tb1\u0026quot;] }], \u0026quot;password\u0026quot;: \u0026quot;**\u0026quot;, \u0026quot;preSql\u0026quot;: [], \u0026quot;session\u0026quot;: [], \u0026quot;username\u0026quot;: \u0026quot;**\u0026quot;, \u0026quot;writeMode\u0026quot;: \u0026quot;insert\u0026quot; } } }], \u0026quot;setting\u0026quot;: { \u0026quot;speed\u0026quot;: { \u0026quot;channel\u0026quot;: \u0026quot;3\u0026quot; } } } } 最后：启动 DataX\n$ cd {YOUR_DATAX_DIR_BIN} $ python datax.py ./oracle2mysql2.json 同步结束，显示日志如下：\n... 2018-06-05 11:20:25.263 [job-0] INFO JobContainer - 任务启动时刻 : 2018-06-05 11:20:15 任务结束时刻 : 2018-06-05 11:20:25 任务总计耗时 : 10s 任务平均流量 : 205B/s 记录写入速度 : 5rec/s 读出记录总数 : 50 读写失败总数 : 0 小结 # 相对来说 DataX 上手使用起来还是比较容易的，但是令楼主比较犯难的就是不能在同一个配置文件里面同时写入不同数据库的表，要想读取多张表并写入就只能单独配置。但是也解决了楼主一些问题。\n作 者：haifeiWu 原文链接：www.hchstudio.cn/article/201…版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-07-04","externalUrl":null,"permalink":"/posts/alilixianshujutongbugongju-datax-caikengjilu/","section":"文章","summary":"最近在做一些数据迁移相关工作，调研了一些工具，发现 DataX 是个不错的东西，所以安利给大家。那么 DataX 是什么呢？DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具，实现包括 MySQL、SQL Server、Oracle、PostgreSQL 等各种异构数据源的同步","title":"阿里离线数据同步工具 DataX 踩坑记录","type":"posts"},{"content":"","date":"2018-06-25","externalUrl":null,"permalink":"/tags/bower/","section":"标签","summary":"","title":"Bower","type":"tags"},{"content":"📌 本文原发布于掘金社区：golang 重构博客统计服务\n作为一个后端开发，在 Docker，etcd，k8s 等新技术不断涌现的今天，其背后的功臣 golang 在语言排行榜上持续走高，因此楼主也就开了这次使用 golang 自己开发基础功能的二次装逼之旅。\n源于 Spring Boot # 感兴趣的小伙伴可以看看楼主的上一篇，基于 Spring Boot 实现的功能，请移步使用 Spring Boot 实现博客统计服务\n实现 Redis 存储逻辑 # 选择 Redis 而没选择数据库的原因是 Redis 提供了丰富的数据结构与数据持久化策略，另外 Redis 是基于内存的，相对于数据库来说，快了不止一个数量级。而统计阅读次数的场景对接口处理的速度还是有一定的要求的，因此楼主选择了 Redis 作为阅读次数统计的 db。\n下面就是 Redis 操作的基础代码，比较简单，楼主贴一下代码，不做进一步的阐述\nRedis 操作的工具类\nfunc initRedisPool() { // 建立连接池 RedisClient = \u0026amp;redis.Pool{ // 从配置文件获取maxidle以及maxactive，取不到则用后面的默认值 MaxIdle: 1, MaxActive: 10, IdleTimeout: 180 * time.Second, Dial: func() (redis.Conn, error) { c, err := redis.Dial(\u0026quot;tcp\u0026quot;, RedisAddress) if err != nil { return nil, err } // 选择db c.Do(\u0026quot;SELECT\u0026quot;, RedisDb) return c, nil }, } } /** * 设置redis的对应key的value */ func redisSet(key string, value string) { c, err := RedisClient.Dial() if err != nil { fmt.Println(\u0026quot;Connect to redis error\u0026quot;, err) return } _, err = c.Do(\u0026quot;SET\u0026quot;, key, value) if err != nil { fmt.Println(\u0026quot;redis set failed:\u0026quot;, err) } } /** * 获取redis的对应key的value */ func redisGet(key string) (value string) { c, err := RedisClient.Dial() if err != nil { fmt.Println(\u0026quot;Connect to redis error\u0026quot;, err) return } val, err := redis.String(c.Do(\u0026quot;GET\u0026quot;, key)) if err != nil { fmt.Println(\u0026quot;redis get failed:\u0026quot;, err) return \u0026quot;\u0026quot; } else { fmt.Printf(\u0026quot;Got value is %v \\n\u0026quot;, val) return val } } /** * redis使得对应的key的值自增 */ func redisIncr(key string) (value string) { c, err := RedisClient.Dial() _, err = c.Do(\u0026quot;INCR\u0026quot;, key) if err != nil { fmt.Println(\u0026quot;incr error\u0026quot;, err.Error()) } incr, err := redis.String(c.Do(\u0026quot;GET\u0026quot;, key)) if err == nil { fmt.Println(\u0026quot;redis key after incr is : \u0026quot;, incr) } return incr } 博客阅读次数统计接口实现 # 博客阅读次数统计的基本业务逻辑就是，将每篇博客对应的 blogId 作为 Redis 的 key，而访问次数就是这个 key 所对应的 value，每访问一次该接口就要将对应的 blogId 自增一次，并返回对应的 value。这里楼主选择的 Redis 的数据结构是 String，下面是楼主实现该逻辑的主要代码：\npackage main import ( \u0026quot;encoding/json\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;github.com/garyburd/redigo/redis\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;time\u0026quot; \u0026quot;strings\u0026quot; ) const RedisAddress = \u0026quot;127.0.0.1:6379\u0026quot; const RedisDb = 0 const AllowRequestUrlH = \u0026quot;*\u0026quot; const AllowRequestUrlW = \u0026quot;*\u0026quot; const IllegalCharacters = \u0026quot;?\u0026quot; const DefaultReadCount = \u0026quot;1\u0026quot; var ( // 定义常量 RedisClient *redis.Pool ) func main() { // 初始化redis连接池 initRedisPool() // 启动web服务监听 http.HandleFunc(\u0026quot;/*-*/*/\u0026quot;, blogReadCountIncr) //设置访问的路由 err := http.ListenAndServe(\u0026quot;:9401\u0026quot;, nil) //设置监听的端口 if err != nil { log.Fatal(\u0026quot;ListenAndServe: \u0026quot;, err) } } func blogReadCountIncr(responseWriter http.ResponseWriter, request *http.Request) { // 解析参数，默认不解析 request.ParseForm() blogId := request.Form.Get(\u0026quot;blogId\u0026quot;) log.Println(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; method blogReadCountIncr exec , request params is : \u0026quot;,blogId) // 判断请求参数是否为空 if \u0026quot;\u0026quot; == blogId { result := ResultCode{ Code: 200, Msg: \u0026quot;success\u0026quot;, } ret, _ := json.Marshal(result) fmt.Fprintf(responseWriter, string(ret)) //这个写入到w的是输出到客户端的 } readCount := redisGet(blogId) if \u0026quot;\u0026quot; == readCount { // 不符合规则，直接返回 flag := strings.Index(blogId, AllowRequestUrlH) != 0 ||strings.Index(blogId, AllowRequestUrlW) != 0||strings.Contains(blogId, IllegalCharacters) if !flag { result := ResultCode{ Code: 200, Msg: \u0026quot;success\u0026quot;, } ret, _ := json.Marshal(result) fmt.Fprintf(responseWriter, string(ret)) //这个写入到w的是输出到客户端的 } redisSet(blogId, DefaultReadCount) readCount = DefaultReadCount } else { readCount = redisIncr(blogId) } log.Println(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; readCount is : \u0026quot;,readCount) result := ResultCode{ Code: 200, Msg: \u0026quot;success\u0026quot;, Data: readCount, } ret, _ := json.Marshal(result) fmt.Fprintf(responseWriter, string(ret)) //这个写入到w的是输出到客户端的 } // 结构体定义返回值 type ResultCode struct { Msg string `json:\u0026quot;msg\u0026quot;` Code int `json:\u0026quot;code\u0026quot;` Data string `json:\u0026quot;data\u0026quot;` } 实现过程中遇到的坑 # 出现的问题 # 使用 golang 原生的 JSON 工具序列化时，出现序列化失败的问题，如下所示的结构体定义，乍一看是没啥问题的，然而使用\nret, _ := json.Marshal(result) 序列化时，出现无法序列化成 JSON 串的问题，另外还不报错，这让楼主很是头疼。\\\ntype ResultCode struct { msg string `json:\u0026quot;msg\u0026quot;` code int `json:\u0026quot;code\u0026quot;` data string `json:\u0026quot;data\u0026quot;` } 问题解决 # 最终楼主通过各种姿势的排查，发现是结构体定义有问题，当定义结构体时首字母必须大写才能序列化成功，这个特点在 golang 里面很是明显，首字母小写的函数在其他文件里面是调不到的。下面给出正确的结构体定义\\\ntype ResultCode struct { Msg string `json:\u0026quot;msg\u0026quot;` Code int `json:\u0026quot;code\u0026quot;` Data string `json:\u0026quot;data\u0026quot;` } 小结 # 目前很多大佬都写过关于 golang web 的教程，如有雷同，请略过不看，本文是通过自己的亲身实战以及楼主自己踩到的坑完成的，另外本文是基于 go 内置的net/http库实现的 web 服务。\n号外 # 楼主造了一个轮子，LIGHTCONF 是一个基于 Netty 实现的配置管理平台，其核心设计目标是“为业务提供统一的配置管理服务”，可以做到开箱即用。感兴趣的给个 star 支持一下。\n基于 Netty 实现的轻量级分布式应用配置中心 作 者：haifeiWu 原文链接：www.hchstudio.cn/article/201…版权声明：非特殊声明均为本站原创作品，转载时请注明作者和原文链接。\n","date":"2018-06-25","externalUrl":null,"permalink":"/posts/golangzhonggouboketongjifuwu/","section":"文章","summary":"作为一个后端开发，在 Docker，etcd，k8s 等新技术不断涌现的今天，其背后的功臣 golang 在语言排行榜上持续走高，因此楼主也就开了这次使用 golang 自己开发的基础功能的二次装逼之旅。","title":"golang 重构博客统计服务","type":"posts"},{"content":"📌 本文原发布于掘金社区：Netty 实战之第一个应用\n作为一个正在 Java 路上摸爬滚打的小菜鸡，之前在项目中也用过 Netty,也因为 Netty 报名阿里的中间件大赛，但终究功力太浅，最终不了了之，最近工作中又遇到了 Netty 的小姐妹 Mina。此时楼主觉得 Netty 还是需要潜心深入学习一下。就这样在成为大菜鸡的路上不消停地折腾……\nNIO 简介 # Netty 是 Java 世界知名的基于 NIO 的网络框架，因此说到 Netty，介绍一下 NIO 还是有必要的。\nJava NIO 又称 Non-blocking IO,NIO 可以让你非阻塞地使用 IO,例如：当线程从通道读取数据到缓冲区时，线程还可以做其他事情。当数据被写入到缓冲区时，线程可以继续处理它。从缓冲区写入通道也类似。\nJava NIO 主要由 Channels，Buffers，Selectors 组成，虽然 Java NIO 中除此之外还有很多类和组件，但总体来说，Channel,Buffer 和 Selector 构成了核心的 API。其他组件和类主要是围绕这三者展开的。对 NIO 感兴趣的小伙伴请移步Java NIO 系列教程\nNetty 快速入门 # 一般楼主在学习一项新技术时，首先得来个“Hello，World”暖暖场。当然 Netty 也不例外，这里楼主实现一个 echo 服务器，那么 echo 是什么呢？\n就是先启动客户端，然后建立一个连接并发送一个或多个消息到服务器，其中每条消息都会被服务器原样返回给客户端。当然，这个应用程序没多大意义。但也可以帮助我们理解 Netty,以及学习 Netty 的模板代码。\n添加 Maven 依赖 # 一般开源软件在 Maven 仓库里面都可以找到，请移步Maven 仓库\\\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.netty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;netty-all\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.1.12.Final\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 代码实现 # Echo 的服务端代码实现如下，下面代码的主要逻辑是绑定端口号，启动服务，是 Netty 中常见的模板代码。\\\npublic class EchoServer { private final int port; public EchoServer(int port) { this.port = port; } public static void main(String[] args) throws Exception { // 服务器监听端口号 int port = 8080; new EchoServer(port).start(); } public void start() throws Exception { // NioEventLoopGroup是处理I/O操作的多线程事件循环 EventLoopGroup group = new NioEventLoopGroup(); try { // ServerBootstrap是一个用于设置服务器的引导类。 ServerBootstrap b = new ServerBootstrap(); b.group(group) .channel(NioServerSocketChannel.class) // 使用NioServerSocketChannel类，用于实例化新的通道以接受传入连接 .localAddress(new InetSocketAddress(port)) // 设置服务器监听端口号 .childHandler(new ChannelInitializer\u0026lt;SocketChannel\u0026gt;() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new EchoServerHandler()); // 添加请求处理 } }); // 绑定到端口和启动服务器 ChannelFuture f = b.bind().sync(); System.out.println(EchoServer.class.getName() + \u0026quot; started and listening for connections on \u0026quot; + f.channel().localAddress()); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully().sync(); } } } EchoServerHandler 实现代码，这里是使用 Netty 实现网络操作业务逻辑的主要阵地。在这里覆盖 channelRead()事件处理程序方法。每当从客户端接收到新数据时，使用该方法来接收客户端的消息。\n@Sharable public class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // 覆盖channelRead()事件处理程序方法 ByteBuf in = (ByteBuf) msg; System.out.println( \u0026quot;Server received: \u0026quot; + in.toString(CharsetUtil.UTF_8)); ctx.write(in); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { // channelRead()执行完成后，关闭channel连接 ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) .addListener(ChannelFutureListener.CLOSE); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } 客户端代码跟上面的代码大体类似，楼主就不再贴出来了，就当留个小作业吧，感兴趣的小伙伴请自行搞定。\n楼主使用 Netty 的姿势 # 楼主基于 Netty 开发了应用配置管理平台服务，实现了“为业务提供统一的配置管理服务”，可以做到开箱即用，主要功能有：\n简单易用：上手非常简单，只需要引入 Maven 依赖和一行配置即可； 在线管理：提供配置管理中心，支持在线管理配置信息； 实时推送：配置信息更新后，实时推送配置信息，项目中配置数据会实时更新并生效，不需要重启线上机器； 配置备份：配置数据会在 MySQL 中对配置信息做备份，保证配置数据的安全性； 感兴趣的小伙伴请移步楼主的 Netty 实践\n小结 # 虽然楼主经常使用到 Netty，但是很多时候对 Netty 的一些概念还是处于知其然，不知其所以然的状态，因此就萌生了重新捋一遍 Netty 实战，在有余力的情况下撸一下 Netty 的源码，并坚持写博客记录一下这个过程。由于楼主能力有限，博客中难免有不少错误之处，期望大家的建议，斧正。\n","date":"2018-06-21","externalUrl":null,"permalink":"/posts/nettyshizhanzhidiyigeyingyong/","section":"文章","summary":"作为一个正在 Java 路上摸爬滚打的小菜鸡，之前在项目中也用过 Netty，也因为 Netty 报名阿里的中间件大赛，但终究功力太浅，最终不了了之，最近工作中又遇到了 Netty 的小姐妹 Mina。此时楼主觉得 Netty 还是需要潜心深入学习一下。","title":"Netty 实战之第一个应用","type":"posts"},{"content":"📌 本文原发布于掘金社区：使用 Spring Boot 实现博客统计服务\n作为一个后端开发，在微服务，server mesh 等概念满天飞的时代，持续学习能力是不能丢的，因此楼主最近也研究好多 RPC，Netty，Spring Boot 等技术。此外，楼主博客的阅读统计功能用的是与 HEXO 相匹配的第三方的数量统计功能，也就诞生了楼主这次更换成自己开发的基础功能的装逼之旅。\n通过 SPRING INITIALIZR 生成工程 # Spring init\n如上图，通过 Spring 官方的 Spring Initial 网站生成项目，项目的目录结构如下：\n目录结构 - src -main -java -package #主函数，启动类，运行它如果运行了 Tomcat、Jetty、Undertow 等容器 -SpringbootApplication -resouces #存放静态资源 js/css/images 等 - statics #存放 html 模板文件 - templates #主要的配置文件，SpringBoot启动时候会自动加载application.yml/application.properties - application.yml #测试文件存放目录 -test # pom.xml 文件是Maven构建的基础，里面包含了我们所依赖JAR和Plugin的信息 - pom pom 依赖 \u0026lt;?xml version=\u0026quot;1.0\u0026quot; encoding=\u0026quot;UTF-8\u0026quot;?\u0026gt; \u0026lt;project xmlns=\u0026quot;http://maven.apache.org/POM/4.0.0\u0026quot; xmlns:xsi=\u0026quot;http://www.w3.org/2001/XMLSchema-instance\u0026quot; xsi:schemaLocation=\u0026quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026quot;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.*\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;*-*\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; \u0026lt;name\u0026gt;base-service\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;Demo project for Spring Boot\u0026lt;/description\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.3.RELEASE\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;project.reporting.outputEncoding\u0026gt;UTF-8\u0026lt;/project.reporting.outputEncoding\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.commons\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-lang3\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.almende.eve\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;eve-bundle-full\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; 实现 Redis 存储逻辑 # 选择 Redis 而没选择数据库的原因是 Redis 提供了丰富的数据结构与数据持久化策略，另外 Redis 是基于内存的，相对于数据库来说，快了不止一个数量级。而统计阅读次数的场景对接口处理的速度还是有一定的要求的，因此楼主选择了 Redis 作为阅读次数统计的 db。\n下面就是 Redis 操作的基础代码，比较简单，楼主贴一下代码，不做进一步的阐述\nRedis 的接口类\npublic interface RedisService { public boolean set(final String key, final String value); public String get(final String key); public String incr(final String key); } Redis 的实现类\n@Service public class RedisServiceImpl implements RedisService { @Resource private RedisTemplate\u0026lt;String, ?\u0026gt; redisTemplate; @Override public boolean set(final String key, final String value) { boolean result = redisTemplate.execute(new RedisCallback\u0026lt;Boolean\u0026gt;() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer\u0026lt;String\u0026gt; serializer = redisTemplate.getStringSerializer(); connection.set(serializer.serialize(key), serializer.serialize(value)); return true; } }); return result; } @Override public String incr(final String key) { String incr = redisTemplate.execute(new RedisCallback\u0026lt;String\u0026gt;() { @Override public String doInRedis(RedisConnection redisConnection) throws DataAccessException { RedisSerializer\u0026lt;String\u0026gt; serializer = redisTemplate.getStringSerializer(); Long value = redisConnection.incr(serializer.serialize(key)); return String.valueOf(value); } }); return incr; } @Override public String get(final String key){ String result = redisTemplate.execute(new RedisCallback\u0026lt;String\u0026gt;() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer\u0026lt;String\u0026gt; serializer = redisTemplate.getStringSerializer(); byte[] value = connection.get(serializer.serialize(key)); return serializer.deserialize(value); } }); return result; } } 博客阅读次数统计接口实现 # 博客阅读次数统计的基本业务逻辑就是，每篇博客对应的 blogId 作为 Redis 的 key，而访问次数就是这个 key 所对应的 value，每访问一次该接口就要将对应的 blogId 自增一次，并返回对应的 value。这里楼主选择的 Redis 的数据结构是 Redis 的 String，下面是楼主实现该逻辑的主要代码：\n/** * 统计博客阅读次数. * * @author wuhf * @Date 2018/6/15 15:59 **/ @RestController @RequestMapping(\u0026quot;/\u0026quot;) public class BlogReadCountController { private static String ALLOW_REQUEST_URL = \u0026quot;******\u0026quot;; private static String ILLEGAL_CHARACTERS = \u0026quot;*\u0026quot;; private static String DEFAULT_READ_COUNT = \u0026quot;1\u0026quot;; private static Logger logger = LoggerFactory.getLogger(BlogReadCountController.class); @Autowired private RedisService redisService; @ResponseBody @RequestMapping(\u0026quot;/*_*\u0026quot;) public ResultCode blogReadCountIncr(HttpServletRequest request,String blogId) { ResultCode resultCode = new ResultCode(); try { logger.info(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; method blogReadCountIncr exec , request params is : {}\u0026quot;,blogId); String readCount = redisService.get(blogId); if (StringUtils.isBlank(readCount)) { if (!blogId.startsWith(ALLOW_REQUEST_URL)||blogId.contains(ILLEGAL_CHARACTERS)) { resultCode.setCode(Messages.API_ERROR_CODE); resultCode.setMsg(Messages.API_ERROR_MSG); return resultCode; } redisService.set(blogId,DEFAULT_READ_COUNT); readCount = DEFAULT_READ_COUNT; } else { readCount = redisService.incr(blogId); } logger.info(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; readCount is : {}\u0026quot;,readCount); resultCode.setCode(Messages.SUCCESS_CODE); resultCode.setMsg(Messages.SUCCESS_MSG); resultCode.setData(readCount); return resultCode; } catch (Exception e) { e.printStackTrace(); resultCode.setCode(Messages.API_ERROR_CODE); resultCode.setMsg(Messages.API_ERROR_MSG); return resultCode; } } } 实现过程中遇到的坑 # 设置应用启动的端口号 server.port=9401 设置应用访问的 path Spring Boot 应用默认的应用访问的 path 还是 “/“，楼主在这里吃了点苦头，使用http://项目名:port访问服务，愣是访问不通，在配置文件中设置如下所示\nserver.servlet.context-path=/项目名 小结 # 目前很多大佬都写过关于 SpringBoot 的教程了，如有雷同，请略过不看，本文通过自己的亲身实战以及楼主自己踩到的坑完成的，另外本文是基于最新的 spring-boot-starter-parent：2.0.3.RELEASE 编写。\n号外 # 楼主造了一个轮子，LIGHTCONF 是一个基于 Netty 实现的配置管理平台，其核心设计目标是“为业务提供统一的配置管理服务”，可以做到开箱即用。感兴趣的给个 star 支持一下。\n基于 Netty 实现的轻量级分布式应用配置中心 ","date":"2018-06-19","externalUrl":null,"permalink":"/posts/shiyongspring-bootshixianboketongjifuwu/","section":"文章","summary":"作为一个后端开发，在微服务，server mesh 等概念满天飞的时代，持续学习能力是不能丢的。此外，楼主博客的阅读统计功能是用的是与 HEXO 相匹配的第三方的数量统计功能，也就诞生了楼主这次更换成自己开发的基础功能的装逼之旅。","title":"使用 Spring Boot 实现博客统计服务","type":"posts"},{"content":"","date":"2018-06-10","externalUrl":null,"permalink":"/tags/gitlab/","section":"标签","summary":"","title":"GitLab","type":"tags"},{"content":"","date":"2018-06-10","externalUrl":null,"permalink":"/tags/hexo/","section":"标签","summary":"","title":"Hexo","type":"tags"},{"content":"📌 本文原发布于掘金社区：I-team 博客的 gitlab-runner 持续集成实践\n作为一个略微看过 nodejs 语法，但又不懂 nodejs 的攻城狮，搭建 Hexo 环境很是麻烦，要考虑到翻墙、版本兼容等问题。于是乎，博主每换一个电脑，为了能继续发博客，都需要在新电脑上花一天时间重新搞一下 Hexo 环境，楼主感觉还是有简洁的方案来实现我一提交代码就可以自动发布博客，不需要再手动操作一波，这样岂不美哉。so，也就有了今天的经历，代码可以持续集成，博客也可以。楼主的解决方案是使用 GitLab 与 gitlab-runner 实现博客部署的持续集成，效果真的不要太好。\n持续集成工具 gitlab-runner 介绍 # gitlab-ci 的全称是 GitLab continuous integration，也就是持续集成。中心思想是每当 push 到 GitLab 的时候，都会触发一次脚本执行，然后脚本的内容包括了测试，编译，部署等一系列自定义内容。而 gitlab-runner 是 GitLab 提供的持续集成工具。\n简单地说，要让 CI 工作，可总结为以下几点：\n在仓库根目录创建一个名为.gitlab-ci.yml 的文件。 为该项目配置一个 runner 服务，楼主这里是使用 GitLab 提供的代码仓库，在自己的腾讯云服务器上运行 gitlab-runner 服务。 完成上面的步骤后，每次 push 代码到 Git 仓库，runner 就会自动开始 pipeline。 gitlab-ci 的具体部署流程如下图所示（图来自网络，侵权删）\ngitlab-runner\nHexo 博客环境迁移 # 迁移前版本控制 # 其实每个 nodejs 工程根目录下都有一个 package.json 文件，里面都包含了我们所用的插件信息，只需要我们在安装插件的时候注意加上–save，就会自动把插件信息保存到 package.json 中。\n如果目录下没有 package.json 文件也不要紧，在根目录命令行中运行 npm init 即可生成。\n博客环境安装 # 前面做好版本控制，那接下来的事情就好做了。\n备份你的代码，注意：代码中不需要包含 node_modules 文件夹了 先在新电脑中装上 nodejs 环境 由于国内安装 npm 的一些插件需要翻墙，所以这里直接用淘宝镜像：cnpm，安装方法：npm install -g cnpm –registry=registry.npm.taobao.org 安装 Hexo 客户端：cnpm install hexo-cli -g 新建博客目录：Hexo init 把你备份的代码放到此目录下，如果有重复的文件直接覆盖就行 安装 Hexo 插件：cnpm install\n就这样，新的博客环境迁移完成了，执行 hexo s 开始你新的博客征程吧！ gitlab-runner 环境搭建 # gitlab-runner 的安装 # 使用 GitLab 官网提供的下载地址太慢，所以找到了一个国内的镜像地址：\n新建 gitlab-ci-multi-runner.repo\ntouch /etc/yum.repos.d/gitlab-ci-multi-runner.repo 将以下内容写入文件\n[gitlab-ci-multi-runner] name=gitlab-ci-multi-runner baseurl=http://mirrors.tuna.tsinghua.edu.cn/gitlab-ci-multi-runner/yum/el7 repo_gpgcheck=0 gpgcheck=0 enabled=1 gpgkey=https://packages.gitlab.com/gpg.key 执行 sudo yum makecache sudo yum install gitlab-ci-multi-runner 以上是楼主在 centos 上的安装过程，其他系统版本的安装请移步gitlab-runner 其他系统版本的安装 gitlab-runner 注册到 GitLab 官网 # 在终端输入gitlab-runner register 会出现以下过程：\n[root@localhost ~]# gitlab-runner register Running in system-mode. Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/): https://gitlab.com/ Please enter the gitlab-ci token for this runner: your gitlab-ci token Please enter the gitlab-ci description for this runner: [localhost.localdomain]: my-runner Please enter the gitlab-ci tags for this runner (comma separated): your tag Whether to run untagged builds [true/false]: [false]: true Registering runner... succeeded runner=c5552857 Please enter the executor: parallels, shell, virtualbox, docker+machine, docker-ssh+machine, docker, docker-ssh, ssh, kubernetes: shell Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! 在注册过程中有两个比较重要的参数，一个是 GitLab 的 URL，另一个就是注册的 token，这两个参数可以在 GitLab 上找到，过程是Settings\u0026gt;CI/CD\u0026gt;Runners settings\u0026gt;Specific Runners，如下图所示\ngitlab-runner-settings\n另外还需要打开\ngitlab-runner-settings\n要让自己注册的 gitlab-runner 生效，还需要禁用Shared Runners\n以上过程是楼主在 centos 上操作的，其他版本请移步gitlab-runner 注册到 GitLab\n创建.gitlab-ci.yml，并放着工程的根目录下 # .gitlab-ci.yml具体配置请移步官方文档，下面给出楼主使用的**.gitlab-ci.yml**具体内容\nvariables: GIT_STRATEGY: none stages: - build_and_deploy job: stage: build_and_deploy script: - cd /opt/I-team-fly - git pull --tags origin dev - hexo clean - hexo g - hexo d only: - dev 查看 GitLab 上的构建结果 # gitlab-runner-settings\n小结 # 当然这个过程中还是要涉及到几次使用 ssh-key 来设置免密登录，楼主就不在这里赘述了，请遇到问题的小伙伴自行 Google。\n参考文章 # 基于 GitLab CI 搭建持续集成环境 GitLab 之 gitlab-ci 自动部署 GitLab CI 集成 GitLab Runner ","date":"2018-06-10","externalUrl":null,"permalink":"/posts/i-teambokedegitlab-runnerchixujichengshijian/","section":"文章","summary":"作为一个略微看过 nodejs 语法，但又不懂 nodejs 的攻城狮，搭建 Hexo 环境很是麻烦，要考虑到翻墙、版本兼容等问题。于是乎，博主每换一个电脑，为了能继续发博客，都需要在新电脑上花一天时间重新搞一下 Hexo 环境，楼主感觉还是有简洁的方案来实现我一提交代码就可以自动发布博客。","title":"I-team 博客的 gitlab-runner 持续集成实践","type":"posts"},{"content":"","date":"2018-06-10","externalUrl":null,"permalink":"/tags/node.js/","section":"标签","summary":"","title":"Node.js","type":"tags"},{"content":"","date":"2018-06-10","externalUrl":null,"permalink":"/tags/travis-ci/","section":"标签","summary":"","title":"Travis CI","type":"tags"},{"content":"","date":"2018-06-07","externalUrl":null,"permalink":"/tags/dubbo/","section":"标签","summary":"","title":"Dubbo","type":"tags"},{"content":"","date":"2018-06-07","externalUrl":null,"permalink":"/tags/grpc/","section":"标签","summary":"","title":"GRPC","type":"tags"},{"content":"📌 本文原发布于掘金社区：阿里 RPC 框架 Dubbo 初体验\n最近研究了一下阿里开源的分布式 RPC 框架 Dubbo，楼主写了一个 demo，体验了一下 Dubbo 的功能。\n快速开始 # 实际上，Dubbo 的官方文档已经提供了如何使用这个 RPC 框架的 example 代码，基于 Netty 的长连接。楼主看这个框架主要是为了在微服务，service mesh 大火的今天做一些技术储备以及了解一下分布式 RPC 框架的设计。\n当然即便是写一个 Dubbo 的 demo 也不能随便写写就好了，要认真对待说不定哪一天可以派上用场呢，下面是楼主写的代码的目录结构：\ndubboCode 图\n下面我来一一说明一下每个 model 的作用，\nmicro-service-dubbo-common：是通用工具模块，其他的 model 都需要依赖它。 micro-service-dubbo-dal：是整个项目的 dao 模块，有关数据库操作的代码都放在这里。 micro-service-dubbo-interface：是通用接口模块，专门用来声明接口，被 consumer 与 provider 同时依赖，这么做是为了项目的可拆分与分布式部署。 micro-service-dubbo-model：是公用的实体类模块，不限于数据库对应的 model，也可以放 DTO，VO 等。 micro-service-dubbo-provider：项目的服务提供者。 micro-service-dubbo-web：项目的消费者，也是直接跟前端交互的 controller 层。 另外需要在 pom 文件中添加相关依赖\n\u0026lt;!--dubbo--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;dubbo\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${dubbo.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.101tec\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;zkclient\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${zkclient_version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.zookeeper\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;zookeeper\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${zookeeper_version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.curator\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;curator-framework\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${curator_version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 接口创建 # 既然是 RPC 服务，那就需要一个接口，再有一个实现类。这里的接口定义在我们的 micro-service-dubbo-interface 模块中，具体实现是在 provider 这里创建，在楼主的项目中就是在 micro-service-dubbo-provider 中创建 DemoService 的实现。\npublic interface DemoService { String sayHello(String name); public List getUsers(); } @Service(\u0026quot;demoService\u0026quot;) public class DemoServiceImpl implements DemoService { @Override public String sayHello(String name) { System.out.println(\u0026quot;[\u0026quot; + new SimpleDateFormat(\u0026quot;HH:mm:ss\u0026quot;).format(new Date()) + \u0026quot;] Hello \u0026quot; + name + \u0026quot;, request from consumer: \u0026quot; + RpcContext .getContext().getRemoteAddress()); return \u0026quot;Hello \u0026quot; + name + \u0026quot;, response from provider: \u0026quot; + RpcContext.getContext().getLocalAddress(); } @Override public List getUsers() { List list = new ArrayList(); User u1 = new User(); u1.setName(\u0026quot;hejingyuan\u0026quot;); u1.setAge(20); u1.setSex(\u0026quot;f\u0026quot;); User u2 = new User(); u2.setName(\u0026quot;xvshu\u0026quot;); u2.setAge(21); u2.setSex(\u0026quot;m\u0026quot;); list.add(u1); list.add(u2); return list; } } 然后在 consumer 的 pom.xml 中添加对这个接口的依赖，这里的 consumer 就是 micro-service-dubbo-web。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.whforever\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;micro-service-dubbo-provider\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.whforever\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;micro-service-dubbo-interface\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 有了接口，就需要配置一下。\n接口配置 # 首先在提供方这里发布接口。创建一个 XML 文件，名为：dubbo-provider.xml。\n文件内容：\n\u0026lt;?xml version=\u0026quot;1.0\u0026quot; encoding=\u0026quot;UTF-8\u0026quot;?\u0026gt; \u0026lt;beans xmlns:xsi=\u0026quot;http://www.w3.org/2001/XMLSchema-instance\u0026quot; xmlns:dubbo=\u0026quot;http://code.alibabatech.com/schema/dubbo\u0026quot; xmlns=\u0026quot;http://www.springframework.org/schema/beans\u0026quot; xsi:schemaLocation=\u0026quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd\u0026quot;\u0026gt; \u0026lt;!-- provider's application name, used for tracing dependency relationship --\u0026gt; \u0026lt;dubbo:application name=\u0026quot;demo-provider\u0026quot;/\u0026gt; \u0026lt;!-- use multicast registry center to export service --\u0026gt; \u0026lt;dubbo:registry protocol=\u0026quot;zookeeper\u0026quot; address=\u0026quot;127.0.0.1:2181\u0026quot; /\u0026gt; \u0026lt;!-- use dubbo protocol to export service on port 20880 --\u0026gt; \u0026lt;dubbo:protocol name=\u0026quot;dubbo\u0026quot; port=\u0026quot;20880\u0026quot;/\u0026gt; \u0026lt;!-- service implementation, as same as regular local bean --\u0026gt; \u0026lt;bean id=\u0026quot;demoProviderService\u0026quot; class=\u0026quot;com.whforever.service.impl.DemoServiceImpl\u0026quot;/\u0026gt; \u0026lt;!-- declare the service interface to be exported --\u0026gt; \u0026lt;dubbo:service interface=\u0026quot;com.whforever.service.DemoService\u0026quot; ref=\u0026quot;demoProviderService\u0026quot;/\u0026gt; \u0026lt;/beans\u0026gt; 很简单，发布了一个接口，类似 Spring 的一个 bean。\n同样，在 consumer 即 micro-service-dubbo-web 的 resource 目录下，也创建一个 dubbo-consumer.xml 文件。内容稍有不同。\n\u0026lt;?xml version=\u0026quot;1.0\u0026quot; encoding=\u0026quot;UTF-8\u0026quot;?\u0026gt; \u0026lt;beans xmlns:xsi=\u0026quot;http://www.w3.org/2001/XMLSchema-instance\u0026quot; xmlns:dubbo=\u0026quot;http://code.alibabatech.com/schema/dubbo\u0026quot; xmlns=\u0026quot;http://www.springframework.org/schema/beans\u0026quot; xsi:schemaLocation=\u0026quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd\u0026quot;\u0026gt; \u0026lt;!-- consumer's application name, used for tracing dependency relationship (not a matching criterion), don't set it same as provider --\u0026gt; \u0026lt;dubbo:application name=\u0026quot;demo-consumer\u0026quot;/\u0026gt; \u0026lt;!-- use multicast registry center to discover service --\u0026gt; \u0026lt;!--\u0026lt;dubbo:registry address=\u0026quot;multicast://224.5.6.7:1234\u0026quot;/\u0026gt;--\u0026gt; \u0026lt;dubbo:registry protocol=\u0026quot;zookeeper\u0026quot; address=\u0026quot;127.0.0.1:2181\u0026quot; /\u0026gt; \u0026lt;!-- generate proxy for the remote service, then demoService can be used in the same way as the local regular interface --\u0026gt; \u0026lt;dubbo:reference id=\u0026quot;demoConsumerService\u0026quot; check=\u0026quot;false\u0026quot; interface=\u0026quot;com.whforever.service.DemoService\u0026quot;/\u0026gt; \u0026lt;/beans\u0026gt; 由此可见，这两个文件的注册发现协议是 ZooKeeper，因此在服务启动之前需要启动 ZooKeeper，具体移步ZooKeeper 注册中心安装启动\n准备测试 # 测试之前还要做点工作。\n在启动 provider 时需要一部分引导程序，请看如下代码：\\\npublic class ProviderMain { public static void main(String[] args) throws IOException { System.setProperty(\u0026quot;java.net.preferIPv4Stack\u0026quot;, \u0026quot;true\u0026quot;); ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(\u0026quot;dubbo-provider.xml\u0026quot;); context.start(); System.in.read(); // press any key to exit } } consumer 代码\\\n@Controller @RequestMapping(\u0026quot;/\u0026quot;) public class IndexController { @Autowired DemoService demoService; @RequestMapping(\u0026quot;/echo\u0026quot;) @ResponseBody public String echo() { System.out.println(\u0026quot;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;echo\u0026quot;); return JSON.toJSONString(demoService.getUsers()); } } 运行 # 先运行 provider：\\\n[06/06/18 11:56:29:029 CST] main INFO config.AbstractConfig: [DUBBO] The service ready on spring started. service: com.whforever.service.DemoService, dubbo version: 2.6.1, current host: 192.168.1.120 [06/06/18 11:56:30:030 CST] main INFO config.AbstractConfig: [DUBBO] Export dubbo service com.whforever.service.DemoService to local registry, dubbo version: 2.6.1, current host: 192.168.1.120 [06/06/18 11:56:30:030 CST] main INFO config.AbstractConfig: [DUBBO] Export dubbo service com.whforever.service.DemoService to url dubbo://192.168.1.120:20880/com.whforever.service.DemoService?anyhost=true\u0026amp;application=demo-provider\u0026amp;bind.ip=192.168.1.120\u0026amp;bind.port=20880\u0026amp;dubbo=2.6.1\u0026amp;generic=false\u0026amp;interface=com.whforever.service.DemoService\u0026amp;methods=sayHello,getUsers\u0026amp;pid=13992\u0026amp;side=provider×tamp=1528300589682, dubbo version: 2.6.1, current host: 192.168.1.120 [06/06/18 11:56:30:030 CST] main INFO config.AbstractConfig: [DUBBO] Register dubbo service com.whforever.service.DemoService url dubbo://192.168.1.120:20880/com.whforever.service.DemoService?anyhost=true\u0026amp;application=demo-provider\u0026amp;bind.ip=192.168.1.120\u0026amp;bind.port=20880\u0026amp;dubbo=2.6.1\u0026amp;generic=false\u0026amp;interface=com.whforever.service.DemoService\u0026amp;methods=sayHello,getUsers\u0026amp;pid=13992\u0026amp;side=provider×tamp=1528300589682 to registry registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider\u0026amp;dubbo=2.6.1\u0026amp;pid=13992®istry=zookeeper×tamp=1528300589673, dubbo version: 2.6.1, current host: 192.168.1.120 [06/06/18 11:56:30:030 CST] main INFO transport.AbstractServer: [DUBBO] Start NettyServer bind /0.0.0.0:20880, export /192.168.1.120:20880, dubbo version: 2.6.1, current host: 192.168.1.120 再运行 consumer：\nconsumer 图\n通过查看 Dubbo 监控中心，可以看到如下情况，具体 Dubbo 监控中心如何安装部署请移步Simple 监控中心安装\ndubboAdmin 图\n小结 # Dubbo 听其大名已久，直到最近才动手写了一些 demo，总体来看上手还是比较简单，官方也提供了比较详细的文档，社区也比较活跃。关于本篇博客中的代码，楼主已经放到了 github，感兴趣的小伙伴，请移步Dubbo 初体验 Demo 模板代码\n号外 # 楼主造了一个轮子，LIGHTCONF 是一个基于 Netty 实现的配置管理平台，其核心设计目标是“为业务提供统一的配置管理服务”，可以做到开箱即用。\n基于 Netty 实现的轻量级分布式应用配置中心 ","date":"2018-06-07","externalUrl":null,"permalink":"/posts/ali-rpc-kuangjia-dubbo-chutiyan/","section":"文章","summary":"最近研究了一下阿里开源的分布式 RPC 框架 Dubbo，楼主写了一个 demo，体验了一下 Dubbo 的功能。","title":"阿里 RPC 框架 Dubbo 初体验","type":"posts"},{"content":"📌 本文原发布于掘金社区：死磕 Java 之聊聊 HashMap 源码(基于 JDK1.8)\nHashMap 是 Java 程序员使用频率最高的数据结构之一。另外，JDK1.8 对 HashMap 底层的实现进行了优化，如引入红黑树的数据结构以及扩容的优化等等来提高性能。本文结合 JDK1.8 的源码，探讨 HashMap 的结构实现和功能原理。\nHashMap 的 UML 图 # HashMap 的 UML 图\nHashMap 的成员变量及其含义 # public class HashMap\u0026lt;K,V\u0026gt; extends AbstractMap\u0026lt;K,V\u0026gt; implements Map\u0026lt;K,V\u0026gt;, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; /** * HashMap的默认初始化大小为16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 \u0026lt;\u0026lt; 4; // aka 16 /** * HashMap的最大容量。 */ static final int MAXIMUM_CAPACITY = 1 \u0026lt;\u0026lt; 30; /** * 负载因子的大小，一般HashMap的扩容的临界点是当前HashMap的大小 \u0026gt; DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 这是JDK1.8在底层做的一个优化，当一个Entry挂载的节点超过8个，就会将当前Entry的链表结构转化为红黑树的数据结构 */ static final int TREEIFY_THRESHOLD = 8; /** * */ static final int UNTREEIFY_THRESHOLD = 6; /** * 红黑树的最大节点数 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * 是hash表中，Entry的节点. */ static class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; { final int hash; final K key; V value; Node\u0026lt;K,V\u0026gt; next; Node(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + \u0026quot;=\u0026quot; + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry\u0026lt;?,?\u0026gt; e = (Map.Entry\u0026lt;?,?\u0026gt;)o; if (Objects.equals(key, e.getKey()) \u0026amp;\u0026amp; Objects.equals(value, e.getValue())) return true; } return false; } } /* ---------------- Static utilities -------------- */ /** * 计算key的hash值。 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } /** * 这个方法时HashMap中比较实用的方法，用于计算传入值的2倍，也算是JDK源码部分的最佳实践。 */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n \u0026gt;\u0026gt;\u0026gt; 1; n |= n \u0026gt;\u0026gt;\u0026gt; 2; n |= n \u0026gt;\u0026gt;\u0026gt; 4; n |= n \u0026gt;\u0026gt;\u0026gt; 8; n |= n \u0026gt;\u0026gt;\u0026gt; 16; return (n \u0026lt; 0) ? 1 : (n \u0026gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } /* ---------------- Fields -------------- */ /** * hash表 */ transient Node\u0026lt;K,V\u0026gt;[] table; /** * 保存缓存的entrySet。 */ transient Set\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt; entrySet; /** * map中键值对的数量。 */ transient int size; /** * * 这个HashMap被结构修改的次数结构修改是那些改变HashMap中的映射数量或者修改其内部结构（例如，重新散列）的修改。 该字段用于在HashMap失败快速的Collection-views上创建迭代器。 */ transient int modCount; /** * The next size value at which to resize (capacity * load factor). * * @serial */ int threshold; /** * The load factor for the hash table. * * @serial */ final float loadFactor; } 聊聊 HashMap 的主要方法实现 # 内部实现 # 搞清楚 HashMap，首先需要知道 HashMap 是什么，即它的存储结构-字段；其次弄明白它能干什么，即它的功能实现-方法。下面我们针对这两个方面详细展开讲解。\n存储结构-字段 # 从结构实现来讲，HashMap 是数组+链表+红黑树（JDK1.8 增加了红黑树部分）实现的，如下图所示。\nHashMap 的内存结构图\n这里需要讲明白两个问题：数据底层具体存储的是什么？这样的存储方式有什么优点呢？\n\\(1\\) 从源码可知，HashMap 类中有一个非常重要的字段，就是 Node[] table，即哈希桶数组，明显它是一个 Node 的数组。我们来看 Node\n\\[JDK1.8\\]是何物。\nstatic class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; { final int hash; //用来定位数组索引位置 final K key; V value; Node\u0026lt;K,V\u0026gt; next; //链表的下一个node Node(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { ... } public final K getKey(){ ... } public final V getValue() { ... } public final String toString() { ... } public final int hashCode() { ... } public final V setValue(V newValue) { ... } public final boolean equals(Object o) { ... } } Node 是 HashMap 的一个内部类，实现了 Map.Entry 接口，本质就是一个映射(键值对)。上图中的每个黑色圆点就是一个 Node 对象。\n\\(2\\) HashMap 就是使用哈希表来存储的。哈希表为解决冲突，可以采用开放地址法和链地址法等方式，Java 中 HashMap 采用了链地址法。链地址法，简单来说，就是数组加链表的结合。在每个数组元素上都有一个链表结构，当数据被 Hash 后，得到数组下标，把数据放在对应下标元素的链表上。例如程序执行下面代码：\nmap.put(\u0026quot;name\u0026quot;,\u0026quot;makefeixiang\u0026quot;); 系统将调用”name”这个 key 的 hashCode()方法得到其 hashCode 值（该方法适用于每个 Java 对象），然后再通过 Hash 算法的后两步运算来定位该键值对的存储位置，有时两个 key 会定位到相同的位置，表示发生了 Hash 碰撞。当然 Hash 算法计算结果越分散均匀，Hash 碰撞的概率就越小，map 的存取效率就会越高。\n/** * 计算key的hash值。 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } 当然如果哈希桶数组很大，即便是较差的 hash 算法也会比较分散，有较好的效果，然而，如果哈希桶数组很小，即使好的 Hash 算法也会出现较多 hash 碰撞，因此就需要在空间成本和时间成本之间权衡，其实就是在根据实际情况确定哈希桶数组的大小，并在此基础上设计好的 hash 算法来减少 Hash 碰撞。那么通过什么方式来控制 map 使得 Hash 碰撞的概率又小，哈希桶数组（Node[] table）占用空间又少呢？答案就是好的 Hash 算法和扩容机制。\nHashMap 的扩容机制就是通过 threshold = length * Load factor 来做是否扩容的决策。也就是说，在数组定义好长度之后，负载因子越大，所能容纳的键值对个数越多。当然，负载因子也不是越大越好，JDK 设计者给出了一个相对来说比较均衡的方案，Load factor 为负载因子(默认值是 0.75)，一般我们不对这个参数做修改。\n功能实现-方法 # HashMap 的内部功能实现很多，本文主要从根据 key 获取 HashMap 数组索引、put 方法的执行、扩容、获取 HashMap 对应 key 的值等几个具有代表性的点深入展开讲解。\n1. 确定哈希桶数组索引位置 # 不管增加、删除、查找键值对，定位到哈希桶数组的索引都是很关键的第一步。HashMap 的数据结构是数组和链表或者红黑树的结合，所以我们希望这个 HashMap 里面的元素位置尽量分布均匀，使得每个位置上的元素数量只有一个，那么当我们用 hash 算法求得这个位置的时候，就可以马上找到，不用遍历链表，查询的时间复杂度也仅仅是 O(n)。我们来看看源码的实现：\n// 方法1，代码段1 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } // 当我们使用hash时，代码段2 if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) tab[i] = newNode(hash, key, value, null); 这里的 Hash 算法本质上就是三步：取 key 的 hashCode 值、高位运算、取模运算。\n2. 分析 HashMap 的 put 方法 # ①.判断键值对数组 table\n\\[i\\]是否为空或为 null，否则执行 resize()进行扩容；\n②.根据键值 key 计算 hash 值得到插入的数组索引 i，如果 table\n\\[i\\]==null，直接新建节点添加，转向⑥，如果 table\n\\[i\\]不为空，转向③；\n③.判断 table\n\\[i\\]的首个元素是否和 key 一样，如果相同直接覆盖 value，否则转向④，这里的相同指的是 hashCode 以及 equals；\n④.判断 table\n\\[i\\] 是否为 treeNode，即 table\n\\[i\\] 是否是红黑树，如果是红黑树，则直接在树中插入键值对，否则转向⑤；\n⑤.遍历 table\n\\[i\\]，判断链表长度是否大于 8，大于 8 的话把链表转换为红黑树，在红黑树中执行插入操作，否则进行链表的插入操作；遍历过程中若发现 key 已经存在直接覆盖 value 即可；\n⑥.插入成功后，判断实际存在的键值对数量 size 是否超过了最大容量 threshold，如果超过，进行扩容。\nfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; // 步骤①：tab为空则创建 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步骤②：计算index，并对null做处理 if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node\u0026lt;K,V\u0026gt; e; K k; // 步骤③：节点key存在，直接覆盖value if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) e = p; // 步骤④：判断该链为红黑树 else if (p instanceof TreeNode) e = ((TreeNode\u0026lt;K,V\u0026gt;)p).putTreeVal(this, tab, hash, key, value); // 步骤⑤：该链为链表 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //链表长度大于8转换为红黑树进行处理 if (binCount \u0026gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // key已经存在直接覆盖value if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 用来实现迭代时被修改的快速失败策略 // 步骤⑥：超过最大容量 就扩容 if (++size \u0026gt; threshold) resize(); afterNodeInsertion(evict); return null; } 3. 扩容机制的实现 # 扩容(resize)就是重新计算容量，向 HashMap 对象里不停地添加元素，当 HashMap 对象内部的数组长度 大于 DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY，HashMap 就需要扩大数组的长度，以便能装入更多的元素。方法是使用一个新的数组代替已有的容量小的数组。\n我们分析下 resize 的源码，鉴于 JDK1.8 融入了红黑树，较复杂，为了便于理解我们仍然使用 JDK1.7 的代码，本质上区别不大，具体区别后文再说。\\\nfinal Node\u0026lt;K,V\u0026gt;[] resize() { Node\u0026lt;K,V\u0026gt;[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap \u0026gt; 0) { // 超过最大值就不再扩充了 if (oldCap \u0026gt;= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 没超过最大值，就扩充为原来的2倍 else if ((newCap = oldCap \u0026lt;\u0026lt; 1) \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; oldCap \u0026gt;= DEFAULT_INITIAL_CAPACITY) newThr = oldThr \u0026lt;\u0026lt; 1; // double threshold } else if (oldThr \u0026gt; 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 计算新的resize上限 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; ft \u0026lt; (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({\u0026quot;rawtypes\u0026quot;,\u0026quot;unchecked\u0026quot;}) Node\u0026lt;K,V\u0026gt;[] newTab = (Node\u0026lt;K,V\u0026gt;[])new Node[newCap]; table = newTab; if (oldTab != null) { // 把每个bucket都移动到新的buckets中 for (int j = 0; j \u0026lt; oldCap; ++j) { Node\u0026lt;K,V\u0026gt; e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash \u0026amp; (newCap - 1)] = e; // else if (e instanceof TreeNode) ((TreeNode\u0026lt;K,V\u0026gt;)e).split(this, newTab, j, oldCap); else { // 链表优化重hash的代码块 Node\u0026lt;K,V\u0026gt; loHead = null, loTail = null; Node\u0026lt;K,V\u0026gt; hiHead = null, hiTail = null; Node\u0026lt;K,V\u0026gt; next; do { next = e.next; // 原索引 if ((e.hash \u0026amp; oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 原索引+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket里 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket里 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 4. HashMap 中根据 key 获取 value 代码实现 # 相比于上面几个，HashMap 中获取 value 相对来说就简单许多，基本逻辑就是根据 key 算出 hash 值定位到哈希桶的索引，当 key 就是当前索引的值则直接返回其对应的 value，反之用 key 去遍历 equal 该索引下的 key，直到找到位置。\\\nfinal Node\u0026lt;K,V\u0026gt; getNode(int hash, Object key) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; first, e; int n; K k; if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (first = tab[(n - 1) \u0026amp; hash]) != null) { if (first.hash == hash \u0026amp;\u0026amp; // always check first node ((k = first.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode\u0026lt;K,V\u0026gt;)first).getTreeNode(hash, key); do { if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } HashMap 的线程安全问题 # 在多线程使用场景中，应该尽量不要使用线程不安全的 HashMap，而应该使用线程安全的 ConcurrentHashMap。那么 HashMap 线程不安全的性质表现在哪里呢？下面来分析一下并发场景下使用 HashMap 可能造成死循环的问题。在 HashMap 的 resize 方法中，我们可以看到\nNode\u0026lt;K,V\u0026gt; loHead = null, loTail = null; Node\u0026lt;K,V\u0026gt; hiHead = null, hiTail = null; Node\u0026lt;K,V\u0026gt; next; do { next = e.next; if ((e.hash \u0026amp; oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); 由于楼主本人才疏学浅，具体过程就不再分析，想要了解的请移步疫苗：Java HASHMAP 的死循环\n小结 # \\(1\\) 扩容是一个特别耗性能的操作，因此初始化 HashMap 的时候给一个数值，避免 map 频繁扩容的情况发生。\n\\(2\\) 负载因子是可以修改的，但是建议一般情况下不要轻易修改。\n\\(3\\) HashMap 是线程不安全的，不要在并发的环境中使用 HashMap，建议使用 ConcurrentHashMap 或者 Collections.synchronizedMap()。\n\\(4\\) JDK1.8 引入红黑树在很大程度上优化了 HashMap 的性能。\n参考文章 # Java 8 系列之重新认识 HashMap 疫苗：Java HASHMAP 的死循环 ","date":"2018-06-04","externalUrl":null,"permalink":"/posts/sikejavazhiliaoliaohashmapyuanma-jiyujdk1-8/","section":"文章","summary":"HashMap 是 Java 程序员使用频率最高的数据结构之一。另外，JDK1.8 对 HashMap 底层的实现进行了优化，如引入红黑树的数据结构以及扩容的优化等等来提高性能。本文结合 JDK1.8 的源码，探讨 HashMap 的结构实现和功能原理。","title":"死磕 Java 之聊聊 HashMap 源码(基于 JDK1.8)","type":"posts"},{"content":"📌 本文原发布于掘金社区：聊聊 HashSet 源码\n今天聊一下 HashSet 源码，HashSet 内部基本使用 HashMap 来实现，本博客将通过以下几个方向讲解。\nHashSet 的 UML 图 # HashMap 的 UML 图\nHashSet 简介 # HashSet 数据结构 # HashSet 内部使用 HashMap 来实现，HashMap 的 key 为要存储的元素，value 为一个 Object，大致数据结构如下：\\\npublic class HashSet\u0026lt;E\u0026gt; extends AbstractSet\u0026lt;E\u0026gt; implements Set\u0026lt;E\u0026gt;, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; private transient HashMap\u0026lt;E,Object\u0026gt; map; private static final Object PRESENT = new Object(); } serialVersionUID：常量，序列化所用的 ID map：使用 HashMap 来保存 HashSet 中所有元素，并使用 transient 关键字修饰，防止被序列化，具体序列化过程，后面会有说到 PRESENT：常量，默认为 map 的 value 值 HashSet 构造函数 # public HashSet(Collection\u0026lt;? extends E\u0026gt; c) { map = new HashMap\u0026lt;E,Object\u0026gt;(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } public HashSet(int initialCapacity, float loadFactor) { map = new HashMap\u0026lt;E,Object\u0026gt;(initialCapacity, loadFactor); } HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap\u0026lt;E,Object\u0026gt;(initialCapacity, loadFactor); } 这里列举了三种构造函数\n第一种构造一个包含指定 collection 中的元素的新 set，容器大小为 collection 大小的 4/3 倍与 16 中的最大值 第二种传入初始容量和加载因子，构造一个空的 HashMap， 第三种传入初始容量、加载因子和标记，构造一个空的 LinkedHashMap，此构造函数为包访问权限，不对外公开，实际只是对 LinkedHashSet 的支持。 聊聊 HashSet 的主要方法实现 # 迭代器 # public Iterator\u0026lt;E\u0026gt; iterator() { return map.keySet().iterator(); } 返回对此 set 中元素进行迭代的迭代器。返回元素的顺序并不是特定的。底层实际调用 HashMap 的 keySet 来返回所有的 key，可见 HashSet 中的元素，只是存放在了底层 HashMap 的 key 上。\n增加元素 # public boolean add(E e) { return map.put(e, PRESENT)==null; } 底层实际将该元素作为 key 放入 HashMap。由于 HashMap 的 put()方法在添加 key-value 对时，如果新放入的 Entry 的 key 与集合中原有 Entry 的 key 相同（hashCode()返回值相等，通过 equals 比较也返回 true）,新添加的 Entry 的 value 会覆盖原来 Entry 的 value，但 key 不会有任何改变，因此如果向 HashSet 中添加一个已经存在的元素时，新添加的集合元素将不会被放入 HashMap 中，原来的元素也不会有任何改变，这也就满足了 Set 中元素不重复的特性。\n删除元素 # public boolean remove(Object o) { return map.remove(o)==PRESENT; } 如果指定元素存在于此 set 中，则将其移除。更确切地讲，如果此 set 包含一个满足(o==null ? e==null : o.equals(e))的元素 e，则将其移除。如果此 set 已包含该元素，则返回 true。底层实际调用 HashMap 的 remove 方法删除指定 Entry。\n对象拷贝 # public Object clone() { try { HashSet\u0026lt;E\u0026gt; newSet = (HashSet\u0026lt;E\u0026gt;) super.clone(); newSet.map = (HashMap\u0026lt;E, Object\u0026gt;) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(); } } 返回此 HashSet 实例的浅表副本：并没有复制这些元素本身。底层实际调用 HashMap 的 clone()方法，HashMap 的 clone()为浅拷贝，故 HashSet 的 clone 也是浅拷贝。\n聊聊 HashSet 与 HashMap 的关系 # 从上面的源码可以看出来，HashSet 与 HashMap 的关系不可谓不密切，以至于不敢相信上面的 UML 是对的。因此，对于 HashSet 而言，它是基于 HashMap 实现的，HashSet 底层使用 HashMap 来保存所有元素，因此 HashSet 源码的实现比较简单，HashSet 的相关操作，都是直接调用底层 HashMap 的相关方法完成的。\n特性小结 # 从源码来看，HashSet 无非是一个阉割版的 HashMap，所以要想明白 HashSet 的实现原理，HashMap 源码坑还是要跳的。\n对于 HashSet 中保存的对象，请注意正确重写其 equals 和 hashCode 方法，以保证放入的对象的唯一性。\nSet 是利用底层 Map 不放入重复 key 的特性来保证元素不重复的。\nHashSet 没有提供 get()方法，原因是同 HashMap 一样，Set 内部是无序的，只能通过迭代的方式获得。\n参考文章 # HashSet 源码分析（基于 JDK8） 深入 Java 集合学习系列：HashSet 的实现原理 ","date":"2018-05-29","externalUrl":null,"permalink":"/posts/liaoliaohashsetyuanma/","section":"文章","summary":"今天聊一下 HashSet 源码，HashSet 内部基本使用 HashMap 来实现，本博客将通过一下几个方向讲解。","title":"聊聊 HashSet 源码","type":"posts"},{"content":"📌 本文原发布于掘金社区：classpath* 和 classpath 使用遇到的问题\n在 Spring 配置 mybatis 的时候需要加载 mybatis 的多个相关配置文件，其中 mybatis 的 mapper 对应的 XML 通常放在其他的 jar 包中，mybatis-conf 文件通常在当前工程中，so，也就引出了今天遇到的问题，那么 classpath* 和 classpath 到底有啥区别呢？\n错误的配置与看到的异常 # 配置文件中的配置，看上去没啥问题\n\u0026lt;bean id=\u0026quot;sqlSessionFactory\u0026quot; class=\u0026quot;org.mybatis.spring.SqlSessionFactoryBean\u0026quot;\u0026gt; \u0026lt;property name=\u0026quot;dataSource\u0026quot; ref=\u0026quot;dataSourceM\u0026quot;/\u0026gt; \u0026lt;property name=\u0026quot;mapperLocations\u0026quot; value=\u0026quot;classpath*:/mappings/*.xml\u0026quot;/\u0026gt; \u0026lt;property name=\u0026quot;configLocation\u0026quot; value=\u0026quot;classpath*:/spring/mybatis-config.xml\u0026quot;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 启动服务器之后看到的异常\nCaused by: java.io.FileNotFoundException: Could not open ServletContext resource [/classpath*:/spring/mybatis-config.xml] at org.springframework.web.context.support.ServletContextResource.getInputStream(ServletContextResource.java:141) at org.mybatis.spring.SqlSessionFactoryBean.buildSqlSessionFactory(SqlSessionFactoryBean.java:358) at org.mybatis.spring.SqlSessionFactoryBean.afterPropertiesSet(SqlSessionFactoryBean.java:340) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624) ... 61 more 分析异常，解决问题\n从上面的异常可以看出来，文件很明显是找不到的，但是这是为啥呢？\n从异常中可以看出来，Spring 查找的路径是：\n然而这个路径根本不是我们想要的路径，显然是找错了地方。 但是我们配置文件给出的路径是：``` classpath*:/spring/mybatis-config.xml 我们将配置文件中下面的配置稍作修改，去掉 classpath 后面的 *\\\n\u0026lt;property name=\u0026quot;configLocation\u0026quot; value=\u0026quot;classpath*:/spring/mybatis-config.xml\u0026quot;\u0026gt;\u0026lt;/property\u0026gt; 改为：\n\u0026lt;property name=\u0026quot;configLocation\u0026quot; value=\u0026quot;classpath*:/spring/mybatis-config.xml\u0026quot;\u0026gt;\u0026lt;/property\u0026gt; 之后，启动正常，没有报错，问题解决。到这里可能有的同学会说为啥 \u0026lt;property name=\u0026quot;mapperLocations\u0026quot; value=\u0026quot;classpath*:/mappings/*.xml\u0026quot;/\u0026gt;可以用classpath*呢？原因请看下面\nclasspath* 和 classpath 的区别： # classpath* 它会搜索所有的 classpath，找到所有符合条件的文件，包括当前项目依赖的 jar 文件中的配置文件。而classpath不会到当前项目依赖的 jar 文件中去寻找。\nclasspath* 存在可移植性问题，遇到问题时，应该使用 classpath.\n一般情况下我们根本没有必要去使用 classpath*，直接使用 classpath 就好了。\n号外 # 楼主造了一个轮子，LIGHTCONF 是一个基于 Netty 实现的配置管理平台，其核心设计目标是“为业务提供统一的配置管理服务”，可以做到开箱即用。\n基于 Netty 实现的轻量级分布式应用配置中心 ","date":"2018-05-24","externalUrl":null,"permalink":"/posts/classpath-he-classpathshiyongyudaodewenti/","section":"文章","summary":"在 Spring 配置 mybatis 的时候需要加载 mybatis 的多个相关配置文件，其中 mybatis 的 mapper 对应的 XML 通常放在其他的 jar 包中，mybatis-conf 文件通常在当前工程中，so，也就引出了今天遇到的问题，那么 classpath* 和 classpath 到底","title":"classpath* 和 classpath 使用遇到的问题","type":"posts"},{"content":"","date":"2018-05-24","externalUrl":null,"permalink":"/tags/mybatis/","section":"标签","summary":"","title":"MyBatis","type":"tags"},{"content":"📌 本文原发布于掘金社区：死磕 Java 之聊聊 ThreadLocal 源码(基于 JDK1.8)\n记得在一次面试中被问到 ThreadLocal，答得马马虎虎，所以打算研究一下 ThreadLocal 的源码。\n面试官：用过 ThreadLocal 吗？\n楼主答：用过，当时使用 ThreadLocal 的时候，使用 Spring 实现横切整个 Controller 层，使用 ThreadLocal 实现了统计每次请求对应方法的执行时间，具体代码如下\n\\\npublic class ProfilerAdvice { Logger logger = Logger.getLogger(ProfilerAdvice.class); private static final ThreadLocal\u0026lt;Long\u0026gt; TIME_THREADLOCAL = new ThreadLocal\u0026lt;Long\u0026gt;(){ @Override protected Long initialValue() { return System.currentTimeMillis(); } }; public static final void begin() { TIME_THREADLOCAL.set(System.currentTimeMillis()); } public static final Long end() { return System.currentTimeMillis() - TIME_THREADLOCAL.get(); } public void requestStart() { ProfilerAdvice.begin(); } public void requestEnd(JoinPoint joinPoint) { /** * 获取被拦截的方法. */ String methodName = joinPoint.getSignature().getName(); logger.info(methodName + \u0026quot;方法请求耗时 \u0026quot;+ProfilerAdvice.end()/1000.0+\u0026quot;s\u0026quot;); } } 面试官：那 ThreadLocal 对应的数据结构是咋样的啊？\n楼主答：应该是使用哈希表实现的吧，楼主这个时候心里开始没底了，然后就没有然后……\n聊聊 JDK 源码中 ThreadLocal 的实现 # 主要方法：\nThreadLocal 的 get 方法\nThreadLocal 之 get 流程：\n1、获取当前线程 t；\n2、返回当前线程 t 的成员变量 ThreadLocalMap（以下简写 map）；\n3、map 不为 null，则获取以当前线程为 key 的 ThreadLocalMap 的 Entry（以下简写 e），如果 e 不为 null，则直接返回该 Entry 的 value；\n4、如果 map 为 null 或者 e 为 null，返回 setInitialValue()的值。setInitialValue()调用重写的 initialValue()返回新值（如果没有重写 initialValue 将返回默认值 null），并将新值存入当前线程的 ThreadLocalMap（如果当前线程没有 ThreadLocalMap，会先创建一个）。 public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings(\u0026quot;unchecked\u0026quot;) T result = (T)e.value; return result; } } return setInitialValue(); } ThreadLocal 的 set 方法\nThreadLocal 之 set 流程：\n1、获取当前线程 t；\n2、返回当前线程 t 的成员变量 ThreadLocalMap（以下简写 map）；\n3、map 不为 null，则更新以当前线程为 key 的 ThreadLocalMap，否则创建一个 ThreadLocalMap，其中当前线程 t 为 key； public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocal 主要的代码实现 # 下面代码是楼主认为 ThreadLocal 中比较重要的，也比较容易看懂的，就不再一一细说\npublic class ThreadLocal\u0026lt;T\u0026gt; { /** * ThreadLocals依赖于附加的每线程线性探测哈希映射到每个线程（Thread.threadLocals和 * inheritableThreadLocals）。 ThreadLocal对象充当键，通过threadLocalHashCode搜索。 这是一个自定义哈希码 * （仅在ThreadLocalMaps内有用），可以消除哈希冲突 * 在连续构造ThreadLocals的常见情况下 * 由相同的线程使用，同时保持良好的行为和异常情况的发生。 */ private final int threadLocalHashCode = nextHashCode(); /** * 下一个哈希码将被发出，原子更新，从零开始。 */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** * 连续生成的散列码之间的区别 , 将隐式顺序线程本地ID转换为近乎最佳的散布 * 两倍大小的表的乘法散列值。 */ private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } /** * 返回此线程局部变量的当前线程的“初始值”。 在线程首次访问带有{@link #get}方法的变量时，将调用此方法， * 除非线程先前调用了{@link #set}方法，在这种情况下，initialValue方法不会 为该线程调用。 * 通常，每个线程最多调用一次此方法，但在后续调用{@link #remove}后跟{@link #get}时可能会再次调用此方法。 * \u0026lt;p\u0026gt;这个实现只是返回{@code null}; 如果程序员希望线程局部变量的初始值不是{@code null}， * 则必须对子代码{@CodeLocal}进行子类化，并重写此方法。 通常，将使用匿名内部类。 */ protected T initialValue() { return null; } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings(\u0026quot;unchecked\u0026quot;) T result = (T)e.value; return result; } } return setInitialValue(); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } /** * SuppliedThreadLocal是JDK8新增的内部类，只是扩展了ThreadLocal的初始化值的方法而已 * ，允许使用JDK8新增的Lambda表达式赋值。需要注意的是，函数式接口Supplier不允许为null。 */ static final class SuppliedThreadLocal\u0026lt;T\u0026gt; extends ThreadLocal\u0026lt;T\u0026gt; { private final Supplier\u0026lt;? extends T\u0026gt; supplier; SuppliedThreadLocal(Supplier\u0026lt;? extends T\u0026gt; supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { return supplier.get(); } } /** * ThreadLocalMap是定制的hashMap,关于HashMap的更详细的问题请参看《聊聊HashMap源码》， * 仅用于维护当前线程的本地变量值。仅ThreadLocal类对其有操作权限， * 是Thread的私有属性。为避免占用空间较大或生命周期较长的数据常驻于内存引发一系列问题， * hash table的key是弱引用WeakReferences。当空间不足时，会清理未被引用的entry。 */ static class ThreadLocalMap { static class Entry extends WeakReference\u0026lt;ThreadLocal\u0026lt;?\u0026gt;\u0026gt; { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal\u0026lt;?\u0026gt; k, Object v) { super(k); value = v; } } /** * 构造一个新的包含初始映射,ThreadLocal映射的新映射，因此我们只在创建至少一个条目时创建一个。 */ ThreadLocalMap(ThreadLocal\u0026lt;?\u0026gt; firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode \u0026amp; (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } /** * 从给定的parentMap构造一个包含所有map的新ThreadLocal。仅由createInheritedMap调用。 * * @param parentMap the map associated with parent thread. */ private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j \u0026lt; len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings(\u0026quot;unchecked\u0026quot;) ThreadLocal\u0026lt;Object\u0026gt; key = (ThreadLocal\u0026lt;Object\u0026gt;) e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode \u0026amp; (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } } } ThreadLocal 使用 demo # public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocal\u0026lt;Integer\u0026gt; threadLocal = new ThreadLocal\u0026lt;Integer\u0026gt;(); @Override public void run() { threadLocal.set( (int) (Math.random() * 100D) ); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println(threadLocal.get()); } } public static void main(String[] args) { MyRunnable sharedRunnableInstance = new MyRunnable(); Thread thread1 = new Thread(sharedRunnableInstance); Thread thread2 = new Thread(sharedRunnableInstance); thread1.start(); thread2.start(); thread1.join(); //wait for thread 1 to terminate thread2.join(); //wait for thread 2 to terminate } } 本示例创建一个传递给两个不同线程的 MyRunnable 实例。两个线程都执行 run（）方法，从而在 ThreadLocal 实例上设置不同的值。如果对 set（）调用的访问已经同步，并且它不是 ThreadLocal 对象，则第二个线程将覆盖第一个线程设置的值。\n但是，由于它是一个 ThreadLocal 对象，因此两个线程无法看到对方的值。因此，它们设定并获得不同的值。\n小结 # ThreadLocal 特性及使用场景 1、实现单个线程单例以及单个线程上下文信息存储，比如交易 id 等\n2、实现线程安全，非线程安全的对象使用 ThreadLocal 之后就会变得线程安全，因为每个线程都会有一个对应的实例\n3、承载一些线程相关的数据，避免在方法中来回传递参数\nThreadLocal 使用过程中出现的问题 1、ThreadLocal 并未解决多线程访问共享对象的问题，如果 ThreadLocal.set()的对象是多线程共享的，那么还是涉及并发问题；\n2、会导致内存泄露么？\n有人认为 ThreadLocal 会导致内存泄露，原因如下\n首先 ThreadLocal 实例被线程的 ThreadLocalMap 实例持有，也可以看成被线程持有。\n如果应用使用了线程池，那么之前的线程实例处理完之后出于复用的目的依然存活。\n所以，ThreadLocal 设定的值被持有，导致内存泄露。\n上面的逻辑是清晰的，然而 Java 的设计者早已经想到了这个问题，ThreadLocal 并不会产生内存泄露，因为 ThreadLocalMap 在选择 key 的时候，\n并不是直接选择 ThreadLocal 实例，而是 ThreadLocal 实例的弱引用。所以实际上从 ThreadLocal 设计角度来说是不会导致内存泄露的，具体代码如下所示：\nstatic class ThreadLocalMap { static class Entry extends WeakReference\u0026lt;ThreadLocal\u0026lt;?\u0026gt;\u0026gt; { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal\u0026lt;?\u0026gt; k, Object v) { super(k); value = v; } } } 参考文章 # Java ThreadLocal 理解 Java 中的 ThreadLocal ","date":"2018-05-09","externalUrl":null,"permalink":"/posts/sikejavazhiliaoliaothreadlocalyuanma-jiyujdk1-8/","section":"文章","summary":"记得在一次面试中被问到 ThreadLocal，答得马马虎虎，所以打算研究一下 ThreadLocal 的源码 面试官：用过 ThreadLocal 吗？楼主答：用过，当时使用 ThreadLocal 的时候，使用 Spring 实现横切整个 Controller 层，使用 ThreadL…","title":"死磕 Java 之聊聊 ThreadLocal 源码(基于 JDK1.8)","type":"posts"},{"content":"📌 本文原发布于掘金社区：多应用配置管理平台 LIGHTCONF\n《多应用配置管理平台 LIGHTCONF》 # 一、简介 # 1.1 概述 # LIGHTCONF 是一个配置管理平台，其核心设计目标是“为业务提供统一的配置管理服务”。\n1.2 特性 # 1、简单易用：上手非常简单，只需要引入 Maven 依赖和一行配置即可； 2、在线管理：提供配置管理中心，支持在线管理配置信息； 3、实时推送：配置信息更新后，实时推送配置信息，项目中配置数据会实时更新并生效，不需要重启线上机器； 4、配置备份：配置数据会在 MySQL 中对配置信息做备份，保证配置数据的安全性； 1.3 背景 # why not properties\n常规项目开发过程中，通常会将配置信息放在项目 resource 目录下的 properties 文件中，配置信息通常包括：JDBC 地址配置、Redis 地址配置、活动开关、阈值配置、黑白名单……等等。使用 properties 维护配置信息将会导致以下几个问题：\n1、需要手动修改 properties 文件； 2、需要重新编译打包； 3、需要重启线上服务器 (项目集群时，更加令人崩溃) ; 4、配置生效不及时：因为流程复杂，新的配置生效需要经历比较长的时间才可以生效； 5、不同环境上线包不一致：例如 JDBC 连接，不同环境需要差异化配置； why LIGHTCONF\n1、不需要 (手动修改 properties 文件) : 在配置管理中心提供的 Web 界面中，定位到指定配置项，输入新的配置值，点击更新按钮即可； 2、不需要 (重新编译打包) : 配置更新后，实时推送新配置信息至项目中，不需要编译打包； 3、不需要 (重启线上服务器) : 配置更新后，实时推送新配置信息至项目中，实时生效，不需要重启线上机器； 4、配置生效 \u0026ldquo;非常及时\u0026rdquo; : 点击更新按钮，新的配置信息即可推送到项目中，瞬间生效，非常及时。比如一些开关类型的配置，配置变更后，将会立刻推送至项目中并生效，相对常规配置修改繁琐的流程，及时性可谓天壤之别； 项目在线预览地址 # 配置中心预览 接入 LIGHTCONF 的 Demo 项目预览 http://58.87.84.211/lightconf-admin-web/toLogin http://58.87.84.211/lightconf-sample/ 源码仓库地址 # 源码仓库地址 Release Download github.com/haifeiWu/li… Download 1.5 环境 # Maven3+ Jdk1.7+ Tomcat7+ Mysql5.5+ 二、快速入门 # 2.1 初始化“数据库” # 请下载项目源码并解压，获取 \u0026ldquo;调度数据库初始化 SQL 脚本\u0026rdquo; 并执行即可。脚本位置如下：\nlightconf/doc/db/light-conf-0.1.1V.sql 2.2 编译源码 # 解压源码，按照 Maven 格式将源码导入 IDE, 使用 Maven 进行编译即可\nlightconf-admin：配置管理中心 lightconf-core：公共依赖 lightconf-common：公共依赖 lightconf-sample: 接入 LIGHTCONF 的 Demo 项目 2.3 “配置管理中心” 项目配置 # 项目：lightconf-admin 作用：管理线上配置信息 配置文件位置：\nlightconf/lightconf-admin/lightconf-admin-web/src/main/resources/light-conf.properties 配置项目说明：\n# 配置登录lightconf的用户名，密码 light.conf.login.username=admin light.conf.login.password=123456 # mysql database setting jdbc.type=mysql jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/light-conf?useUnicode=true\u0026amp;characterEncoding=utf-8 jdbc.username=root jdbc.password=root_pwd # pool settings jdbc.pool.init=2 jdbc.pool.minIdle=3 jdbc.pool.maxActive=20 # jdbc.testSql=SELECT 'x' jdbc.testSql=SELECT 'x' FROM DUAL # 服务端启动监听端口 netty.server.port=9998 2.4 “接入 LIGHTCONF 的示例项目” 项目配置 # 项目：lightconf-sample 作用：接入LIGHTCONF的示例项目，供用户参考学习 A、引入 Maven 依赖 # \u0026lt;!-- lightconf-client --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.lightconf\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lightconf-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${project.parent.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; B、添加 LIGHTCONF 配置文件 # 可参考配置文件： lightconf/lightconf-sample/src/main/resources/light-conf.properties 配置项说明: # 连接light-conf-admin的IP地址 light.conf.host=127.0.0.1 # 连接light-conf-admin的端口号 light.conf.port=9998 ## 接入应用的uuid application.uuid=8705d6c8-bbe0-420c-9853-f780de4cb5ea C、LIGHTCONF 配置初始化\\[必须\\] # 可参考配置文件： lightconf/lightconf-sample/src/main/resources/spring/applicationcontext-light-conf.xml 配置项说明： \u0026lt;!-- ********************************* 核心配置[必须]：LIGHTCONF 配置 ********************************* --\u0026gt; \u0026lt;bean id=\u0026quot;xxlConf\u0026quot; class=\u0026quot;com.lightconf.core.spring.LightConfFactory\u0026quot; init-method=\u0026quot;init\u0026quot; destroy-method=\u0026quot;destroy\u0026quot; /\u0026gt; \u0026lt;!-- ********************************* 核心配置[必须]：LIGHTCONF netty client监听 ********************************* --\u0026gt; \u0026lt;bean id=\u0026quot;lightConfListener\u0026quot; class=\u0026quot;com.lightconf.core.listener.LightConfClientListener\u0026quot;\u0026gt;\u0026lt;/bean\u0026gt; ","date":"2018-05-07","externalUrl":null,"permalink":"/posts/duoyingyongpeizhiguanlipingtailightconf/","section":"文章","summary":"LIGHTCONF 是一个配置管理平台，其核心设计目标是“为业务提供统一的配置管理服务”。","title":"多应用配置管理平台 LIGHTCONF","type":"posts"},{"content":"📌 本文原发布于掘金社区：死磕 Java 之聊聊 LinkedList 源码(基于 JDK1.8)\n工作快一年了，近期打算研究一下 JDK 的源码，也就因此有了死磕 Java 系列\nLinkedList 是一个继承于 AbstractSequentialList 的双向链表，链表不需要 capacity 的设定，它也可以被当作堆栈、队列或双端队列来使用。 LinkedList 实现 List 接口，能对它进行队列操作，提供了相关的添加、删除、修改、遍历等功能。 LinkedList 实现 Deque 接口，即能将 LinkedList 当作双端队列使用。 LinkedList 实现了 Cloneable 接口，即覆盖了函数 clone()，能克隆。 LinkedList 实现 java.io.Serializable 接口，这意味着 LinkedList 支持序列化，能通过序列化去传输，包括网络传输与本地文件序列化。 LinkedList 是非同步的，如若要在并发情况下使用，建议选取 java.util.concurrent 包下的集合类型。 LinkedList 的 UML 图 # LinkedList 的 UML 图\nLinkedList 的成员变量及其含义 # public class LinkedList\u0026lt;E\u0026gt; extends AbstractSequentialList\u0026lt;E\u0026gt; implements List\u0026lt;E\u0026gt;, Deque\u0026lt;E\u0026gt;, Cloneable, java.io.Serializable { transient int size = 0; /** * 指向头指针的节点. * transient关键字扫盲，在实现Serilizable接口后， * 将不需要序列化的属性前添加关键字transient， * 序列化对象的时候，这个属性就不会序列化到指定的目的地中 */ transient Node\u0026lt;E\u0026gt; first; /** * 指向尾节点。 */ transient Node\u0026lt;E\u0026gt; last; /** * 构造方法的空实现。 */ public LinkedList() { } /** * 按照集合迭代器返回的顺序构造包含指定集合元素的列表。 */ public LinkedList(Collection\u0026lt;? extends E\u0026gt; c) { this(); addAll(c); } /** * 链表的节点，私有实现。 */ private static class Node\u0026lt;E\u0026gt; { E item; Node\u0026lt;E\u0026gt; next; // 链表的后继节点 Node\u0026lt;E\u0026gt; prev; // 链表的前驱节点 Node(Node\u0026lt;E\u0026gt; prev, E element, Node\u0026lt;E\u0026gt; next) { this.item = element; this.next = next; this.prev = prev; } } } 聊聊 LinkedList 的主要方法实现 # 我们主要研究一下下面的几个方法，LinkedList 其他方法都是通过调用这几个方法来实现功能，包括 LinkedList 的双端队列的方法。\n/** * 在链表头部插入元素. */ private void linkFirst(E e) { final Node\u0026lt;E\u0026gt; f = first; final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } /** * 在链表尾部插入元素. */ void linkLast(E e) { final Node\u0026lt;E\u0026gt; l = last; final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; // 链表防止并发下被修改的快速失败策略 modCount++; } /** * 在指定节点前面插入元素. */ void linkBefore(E e, Node\u0026lt;E\u0026gt; succ) { // assert succ != null; final Node\u0026lt;E\u0026gt; pred = succ.prev; final Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } /** * 移除链表的头部元素. */ private E unlinkFirst(Node\u0026lt;E\u0026gt; f) { // assert f == first \u0026amp;\u0026amp; f != null; final E element = f.item; final Node\u0026lt;E\u0026gt; next = f.next; f.item = null; f.next = null; // help GC 将元素置为空，让jvm在gc时回收资源 first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } /** * 移除链表的尾部元素. */ private E unlinkLast(Node\u0026lt;E\u0026gt; l) { // assert l == last \u0026amp;\u0026amp; l != null; final E element = l.item; final Node\u0026lt;E\u0026gt; prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; } /** * 移除某一个节点元素. */ E unlink(Node\u0026lt;E\u0026gt; x) { // assert x != null; final E element = x.item; final Node\u0026lt;E\u0026gt; next = x.next; final Node\u0026lt;E\u0026gt; prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; } /** * 通过indexOf方法来定位给定的元素，若LinkedList中存在元素则返回该元素对应的index， * 反之返回-1. */ public boolean contains(Object o) { return indexOf(o) != -1; } /** * 在链表尾部追加元素. */ public boolean add(E e) { linkLast(e); return true; } /** * 移除链表中的指定元素，分两种情况进行处理，当元素为空时与元素不为空时，时间复杂度为O(n). */ public boolean remove(Object o) { if (o == null) { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } /** * 将一个集合的所有元素追加到链表的尾部. */ public boolean addAll(Collection\u0026lt;? extends E\u0026gt; c) { return addAll(size, c); } /** * 将指定集合中的所有元素插入到该列表中，从指定位置开始。将当前在该位置的元素（如果有的话） * 和任何后续元素向右移动（增加它们的索引）。新元素将以指定集合的迭代器返回的顺序出现在列表中。 */ public boolean addAll(int index, Collection\u0026lt;? extends E\u0026gt; c) { checkPositionIndex(index); Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Node\u0026lt;E\u0026gt; pred, succ; if (index == size) { succ = null; pred = last; } else { succ = node(index); pred = succ.prev; } for (Object o : a) { @SuppressWarnings(\u0026quot;unchecked\u0026quot;) E e = (E) o; Node\u0026lt;E\u0026gt; newNode = new Node\u0026lt;\u0026gt;(pred, e, null); if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null) { last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true; } /** * 清空链表. */ public void clear() { // Clearing all of the links between nodes is \u0026quot;unnecessary\u0026quot;, but: // - helps a generational GC if the discarded nodes inhabit // more than one generation // - is sure to free memory even if there is a reachable Iterator for (Node\u0026lt;E\u0026gt; x = first; x != null; ) { Node\u0026lt;E\u0026gt; next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; size = 0; modCount++; } // Positional Access Operations 以下是位置访问操作 /** * 获取链表中对应索引的节点，时间复杂度为O(n) */ public E get(int index) { // 检查index是否越界 checkElementIndex(index); return node(index).item; } /** * 更新对应index的元素，并返回旧值，时间复杂度为O(n). */ public E set(int index, E element) { checkElementIndex(index); Node\u0026lt;E\u0026gt; x = node(index); E oldVal = x.item; x.item = element; return oldVal; } /** * 在指定索引添加元素，时间复杂度为O(1). */ public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * 移除指定索引的元素，时间复杂度为O(1). */ public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } /** * 查找指定index位置的元素. */ Node\u0026lt;E\u0026gt; node(int index) { // assert isElementIndex(index); // 在查找对应index位置的元素的时候，java开发人员做了一层优化 // 当index大于size的一半时从前向后查 // 当index小于size的一半时从后向前查 // 这样的话时间复杂度就变成了index/2 if (index \u0026lt; (size \u0026gt;\u0026gt; 1)) { Node\u0026lt;E\u0026gt; x = first; for (int i = 0; i \u0026lt; index; i++) x = x.next; return x; } else { Node\u0026lt;E\u0026gt; x = last; for (int i = size - 1; i \u0026gt; index; i--) x = x.prev; return x; } } // Search Operations /** * 查找指定元素的index，从前向后查 */ public int indexOf(Object o) { int index = 0; if (o == null) { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node\u0026lt;E\u0026gt; x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; } /** * 查找指定元素的index，从后向前查 */ public int lastIndexOf(Object o) { int index = size; if (o == null) { for (Node\u0026lt;E\u0026gt; x = last; x != null; x = x.prev) { index--; if (x.item == null) return index; } } else { for (Node\u0026lt;E\u0026gt; x = last; x != null; x = x.prev) { index--; if (o.equals(x.item)) return index; } } return -1; } 动手实现 LinkedList # 由于代码过长，故不在此处一一贴出来，感兴趣的小伙伴可以到我的 github 查看：github.com/haifeiWu/in…\n小结 # 与 ArrayList 相比，LinkedList 的长处在于当频繁地增加或者删除元素时效率会很高，但是 LinkedList 空间占用比较大，是一种典型的用空间换时间方案，在我们平时做代码优化时，这也不失为一种折中的方案。\nLinkedList 并发插入时节点覆盖的问题，就是当多个线程同时获取到相同的尾节点，并同时在此尾节点后面插入数据时，会出现数据覆盖的问题，因此在并发量大的情况下应该使用 Java 的加锁机制，或者采用 java.util.concurrent 包下的集合类型。\n","date":"2018-05-06","externalUrl":null,"permalink":"/posts/sikejavazhiliaoliaolinkedlistyuanma-jiyujdk1-8/","section":"文章","summary":"工作快一年了，近期打算研究一下 JDK 的源码，也就因此有了死磕 Java 系列","title":"死磕 Java 之聊聊 LinkedList 源码(基于 JDK1.8)","type":"posts"},{"content":"📌 本文原发布于掘金社区：聊聊 ArrayList 源码(基于 JDK1.8)\n工作快一年了，近期打算研究一下 JDK 的源码\nArrayList 是一个数组队列，相当于动态数组。与 Java 中的数组相比，它的容量能动态增长。它继承于 AbstractList，实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。 ArrayList 继承了 AbstractList，实现了 List。它是一个数组队列，提供了相关的添加、删除、修改、遍历等功能。 ArrayList 实现了 RandomAccess 接口，即提供了随机访问功能。RandomAccess 是 Java 中供 List 实现、为 List 提供快速访问功能的接口。在 ArrayList 中，我们就可以通过元素的序号快速获取元素对象；这就是快速随机访问。 ArrayList 实现了 Cloneable 接口，即覆盖了函数 clone()，能被克隆。 ArrayList 实现 java.io.Serializable 接口，这意味着 ArrayList 支持序列化，能通过序列化去传输，包括网络传输与本地文件序列化。 和 Vector 不同，ArrayList 中的操作不是线程安全的！所以，建议在单线程中才使用 ArrayList，而在多线程中建议选择 CopyOnWriteArrayList，当然 Vector 也可以，但不建议使用。 打个广告，楼主自己造的轮子，感兴趣的请点\n\\[github\\]: github.com/haifeiWu/li…\nArrayList 的 UML 图 # ArrayList 的 UML 图\nArrayList 的成员变量及其含义 # public class ArrayList\u0026lt;E\u0026gt; extends AbstractList\u0026lt;E\u0026gt; implements List\u0026lt;E\u0026gt;, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; /** * ArrayList默认初始大小为10. */ private static final int DEFAULT_CAPACITY = 10; /** * 默认的空值数组 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 当ArrayList不传参数时，elementData默认初始化成DEFAULTCAPACITY_EMPTY_ELEMENTDATA */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 实现ArrayList的object数组\u0026lt;br\u0026gt;\u0026lt;br/\u0026gt; * transient关键字扫盲，在实现Serilizable接口后，将不需要序列化的属性前添加关键字transient，序列化对象的时候，这个属性就不会序列化到指定的目的地中 */ transient Object[] elementData; // non-private to simplify nested class access /** * ArrayList的大小 */ private int size; /** * 构造一个初始容量的数组 */ public ArrayList(int initialCapacity) { if (initialCapacity \u0026gt; 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(\u0026quot;Illegal Capacity: \u0026quot;+ initialCapacity); } } /** * 初始化一个size为10的空值ArrayList. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 按照集合迭代器返回的顺序构造包含指定集合元素的列表。 */ public ArrayList(Collection\u0026lt;? extends E\u0026gt; c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } } 聊聊 ArrayList 的主要方法实现 # /** * 获取ArrayList对应下标的元素，时间复杂度为O(1). */ public E get(int index) { // 检查数组下标越界问题 rangeCheck(index); return elementData(index); } /** * 将对应下标的元素更新成现在的值 */ public E set(int index, E element) { // 检查数组下标越界问题 rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } /** * ArrayList中追加新值. */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } /** * 在ArrayList的指定下标下添加值，时间复杂度为O(n) */ public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } /** * 移除指定下标的值，时间复杂度为O(1)。 */ public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved \u0026gt; 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } /** * 移除指定值，时间复杂度为O(n)。 */ public boolean remove(Object o) { if (o == null) { for (int index = 0; index \u0026lt; size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index \u0026lt; size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } /** * Increases the capacity of this \u0026lt;tt\u0026gt;ArrayList\u0026lt;/tt\u0026gt; instance, if * necessary, to ensure that it can hold at least the number of elements * specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity \u0026gt; minExpand) { ensureExplicitCapacity(minCapacity); } } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length \u0026gt; 0) grow(minCapacity); } /** * ArrayList的最大长度是2147483639 * The maximum size of array to allocate. * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * 用于ArrayList扩容的方法，扩容的策略是int newCapacity = oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1)，即每次扩容是原来长度的1.5倍 * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1); if (newCapacity - minCapacity \u0026lt; 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE \u0026gt; 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } 动手实现 ArrayList # 由于代码过长，故不在此处一一贴出来，感兴趣的小伙伴可以到我的 github 查看\n\\[github\\]: github.com/haifeiWu/in…\n小结 # 关于 ArrayList 的源码，给出几点比较重要的总结：\n1、注意其三个不同的构造方法。无参构造方法构造的 ArrayList 的容量默认为 10，带有 Collection 参数的构造方法，将 Collection 转化为数组并赋给 ArrayList 的实现数组 elementData。\n2、注意扩充容量的方法 ensureCapacity。ArrayList 在每次增加元素（可能是 1 个，也可能是一组）时，都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时，就将新容量设置为旧容量的 1.5 倍，如果设置后的新容量还不够，则直接将新容量设置为传入的参数（也就是所需的容量），而后用 Arrays.copyof()方法将元素拷贝到新的数组（见下面的第 3点）。从中可以看出，当容量不够时，每次增加元素，都要将原来的元素拷贝到一个新的数组中，非常耗时，所以建议在事先能确定元素数量的情况下，使用 ArrayList\n3、ArrayList 的实现中大量地调用了 Arrays.copyof()和 System.arraycopy()方法。我们有必要扒一下这两个方法的代码实现。\npublic static \u0026lt;T\u0026gt; T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } 很明显调用了另一个 copyof 方法，该方法有三个参数，最后一个参数指明要转换的数据的类型，其源码如下：\npublic static \u0026lt;T,U\u0026gt; T[] copyOf(U[] original, int newLength, Class\u0026lt;? extends T[]\u0026gt; newType) { @SuppressWarnings(\u0026quot;unchecked\u0026quot;) T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } 从代码里面可以看出，copyOf 方法实际上是在其内部又创建了一个长度为 newlength 的数组，然后调用 System.arraycopy()方法，将原来数组中的元素复制到了新的数组中。\n下面来看 System.arraycopy()方法。该方法被标记了 native，调用了系统的 C/C++代码，在 JDK 中是看不到的，在这里就不再做更深入的了解，不过按照 JDK 的一贯做法，使用系统的 C/C++代码来复制数组，从效率上来说肯定是没问题的。\n4、ArrayList 基于数组实现，可以通过下标索引直接查找到指定位置的元素，因此查找效率高，但每次插入或删除元素，就要大量地移动元素，插入删除元素的效率低。\n5、在查找给定元素索引值等方法中，源码都将该元素的值分为 null 和不为 null 两种情况处理，因此看来，ArrayList 中是允许元素为 null。\n","date":"2018-05-04","externalUrl":null,"permalink":"/posts/liaoliaoarraylistyuanma-jiyujdk1-8/","section":"文章","summary":"工作快一年了，近期打算研究一下 JDK 的源码 ArrayList 是一个数组队列，相当于动态数组。与 Java 中的数组相比，它的容量能动态增长。它继承于 AbstractList，实现了 List, RandomAccess, Cloneable, java.io.Serializab…","title":"聊聊 ArrayList 源码(基于 JDK1.8)","type":"posts"},{"content":"","date":"2018-04-23","externalUrl":null,"permalink":"/tags/sql/","section":"标签","summary":"","title":"SQL","type":"tags"},{"content":"📌 本文原发布于掘金社区：复杂业务下向 MySQL 导入 30万条数据代码优化的踩坑记录\n从毕业到现在第一次接触到超过 30万条数据导入 MySQL 的场景（有点 low），就是在顺丰公司接入我司 EMM 产品时需要将 AD 中的员工数据导入 MySQL 中，因此楼主负责的模块 connector 就派上了用场。在楼主的努力下，线上数据同步代码经历了从最初的将近 16 个小时（并且还出现其他问题，等后面慢慢细说），到最终 25分钟的性能优化。\n打个广告，楼主自己造的轮子，感兴趣的请点github.com/haifeiWu/li…\n代码直接 Jenkins 打包上线 # 楼主负责的 connector 模块之前经历过的最大的数据量也仅仅是几千条，当然面对几千条数据代码也是跑得极其的快，没有啥影响，然而当第一次在顺丰的正式环境上线时，由于数据量比较大，楼主的代码又是串行执行的，事务保持的时间就相当长，也就因此出现了下面的错误信息：\nLock wait timeout exceeded; try restarting transaction 这里来说一下报这个错的解决方案，查看 MySQL 是否有锁\nshow OPEN TABLES where In_use \u0026gt; 0; 另外，在information_schema下面有三张表：INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS（解决问题方法），通过这三张表，可以更简单地监控当前的事务并分析可能存在的问题。\n比较常用的列：\ntrx_id: InnoDB 存储引擎内部唯一的事务 ID trx_status: 当前事务的状态 trx_status: 事务的开始时间 trx_requested_lock_id： 等待事务的锁 ID trx_wait_started： 事务等待的开始时间 trx_weight： 事务的权重，反应一个事务修改和锁定的行数，当发现死锁需要回滚时，权重越小的值被回滚 trx_mysql_thread_id： MySQL 中的进程 ID，与 show processlist 中的 ID 值相对应 trx_query： 事务运行的 SQL 语句 kill 进程ID，发生上面错误的根本原因在业务逻辑代码对数据库的操作无视了大数据量的情况，比如当数据量比较大时就会出现刚刚修改完这条记录，接着再次修改就会出现上述问题。\n代码优化过程 # 使用线程池，并发执行，提高效率 # 由于数据量比较大，首先想到的方法是拿到数据后将数据分拆成 n 份，由多个线程并发执行数据导入的操作。由此引出多线程的问题，在处理多线程问题时共享的数据结构像 Map，List 应该采用 jdk 提供的 concurrent 包下的数据结构，另外在涉及到操作数据库的地方应该加锁，楼主用的是 jdk 提供的 ReentrantLock。使用 jdk 自带的 jvisualvm，进行代码的监控进而找到最佳线程数，下面是监控的数据。\n下面是线程池的创建代码\nThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat(\u0026#34;demo-pool-%d\u0026#34;).build(); ExecutorService singleThreadPool = new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue\u0026lt;Runnable\u0026gt;(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); singleThreadPool.execute(()-\u0026gt; System.out.println(Thread.currentThread().getName())); singleThreadPool.shutdown(); 果不其然，在使用多线程之后，数据插入效率由原来的十几个小时降到了三个小时，但是并没有达到我们的预期效果，我们继续。\n使用 druid 监控发现问题的 SQL # 配置 druid 监控，在应用的 web.xml 添加配置，并放开/druid的拦截 \u0026lt;!-- druid 数据库监控 --\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;DruidWebStatFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;com.alibaba.druid.support.http.WebStatFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;exclusions\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;*.js,*.gif,*.jpg,*.png,*.css,*.ico,*.jsp,/druid/*,/download/*\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;sessionStatMaxCount\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;2000\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;sessionStatEnable\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;true\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;principalSessionName\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;session_user_key\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;profileEnable\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;true\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;DruidWebStatFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;DruidStatView\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;com.alibaba.druid.support.http.StatViewServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;!--\u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;allow\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;*.*.*.*\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt;--\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;!-- 允许清空统计数据 --\u0026gt; \u0026lt;param-name\u0026gt;resetEnable\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;true\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;!-- 用户名 --\u0026gt; \u0026lt;param-name\u0026gt;loginUsername\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;druid\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;!-- 密码 --\u0026gt; \u0026lt;param-name\u0026gt;loginPassword\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;druid\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;DruidStatView\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/druid/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; 配置数据库连接池 \u0026lt;!-- 数据源配置, 使用 BoneCP 数据库连接池 --\u0026gt; \u0026lt;bean id=\u0026#34;dataSource\u0026#34; class=\u0026#34;com.alibaba.druid.pool.DruidDataSource\u0026#34; init-method=\u0026#34;init\u0026#34; destroy-method=\u0026#34;close\u0026#34;\u0026gt; \u0026lt;!-- 数据源驱动类可不写，Druid默认会自动根据URL识别DriverClass --\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34; /\u0026gt; \u0026lt;!-- 基本属性 url、user、password --\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34; /\u0026gt; \u0026lt;!-- 配置初始化大小、最小、最大 --\u0026gt; \u0026lt;property name=\u0026#34;initialSize\u0026#34; value=\u0026#34;${jdbc.pool.init}\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;minIdle\u0026#34; value=\u0026#34;${jdbc.pool.minIdle}\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;maxActive\u0026#34; value=\u0026#34;${jdbc.pool.maxActive}\u0026#34; /\u0026gt; \u0026lt;!-- 配置获取连接等待超时的时间 --\u0026gt; \u0026lt;property name=\u0026#34;maxWait\u0026#34; value=\u0026#34;60000\u0026#34; /\u0026gt; \u0026lt;!-- 配置间隔多久才进行一次检测，检测需要关闭的空闲连接，单位是毫秒 --\u0026gt; \u0026lt;property name=\u0026#34;timeBetweenEvictionRunsMillis\u0026#34; value=\u0026#34;60000\u0026#34; /\u0026gt; \u0026lt;!-- 配置一个连接在池中最小生存的时间，单位是毫秒 --\u0026gt; \u0026lt;property name=\u0026#34;minEvictableIdleTimeMillis\u0026#34; value=\u0026#34;300000\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;validationQuery\u0026#34; value=\u0026#34;${jdbc.testSql}\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;testWhileIdle\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;testOnBorrow\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;testOnReturn\u0026#34; value=\u0026#34;false\u0026#34; /\u0026gt; \u0026lt;!-- 配置监控统计拦截的filters --\u0026gt; \u0026lt;property name=\u0026#34;filters\u0026#34; value=\u0026#34;stat\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; 访问http://ip/xxx/druid，输入用户名，密码登录就会看到下面的图 使用 druid 的 SQL 监控发现问题 SQL 语句，优化 SQL 通过命令查看程序的 gc 情况 # 通过命令jstat -gc pid 来查看程序的 gc 情况，下面是楼主程序数据同步完成之后的 gc 情况\n简单说明：\n这里写代码片S0C、S1C、S0U、S1U：Survivor 0/1区容量（Capacity）和使用量（Used） EC、EU：Eden区容量和使用量 OC、OU：年老代容量和使用量 PC、PU：永久代容量和使用量 YGC、YGT：年轻代GC次数和GC耗时 FGC、FGCT：Full GC次数和Full GC耗时 小结 # 通过上面的优化过程，楼主的 connector 的数据同步效率也由小时级下降到了分钟级，同步 30+万的时间可以在 25分钟内完成。经此一役，楼主算是收获满满。抱着虔诚的心态学习，不浮不躁，快乐成长。\n","date":"2018-04-23","externalUrl":null,"permalink":"/posts/fuzayewuxiaxiangmysqldaoru30wantiaoshujudaimayouhuadecaikeng/","section":"文章","summary":"另外，在 information_schema 下面有三张表：INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS（解决问题方法），通过这三张表，可以更简单地监控当前的事务并分析可能存在的问题。kill 进程 ID，发生上面错误的根本原因在业务逻辑代码…","title":"复杂业务下向 MySQL 导入 30万条数据代码优化的踩坑记录","type":"posts"},{"content":"📌 本文原发布于掘金社区：自建脚手架之配置中心\u0026ndash;LightConf 的实现\n常规项目开发过程中，通常会将配置信息放在项目 resource 目录下的 properties 文件中，配置信息通常包括：JDBC 地址配置、Redis 地址配置、活动开关等等。因此每次上线或者服务迁移的时候都要手动修改配置，并一台一台地重启服务器，甚是麻烦，且费时费力。\n于是便萌生出了使用配置中心的想法，在考察了 github 上的 apoll，xxl-conf 等开源项目后，感觉都不适合我司的应用模型，于是决定自研一套符合自己需求的配置中心，因此 LightConf 便应运而生，当然 LightConf 也借鉴了 apoll，xxl-conf 的部分代码实现。\n项目开源地址 # 在经历了一个多月的开发后，项目的第一个稳定版本终于完成，开源地址github.com/haifeiWu/li…，欢迎各路大神 star，拍砖。 LIGHTCONF 使用了 Netty 实现底层通讯，保证配置的实时生效。接入 LIGHTCONF 时只需要添加 Maven 依赖，简单配置即可马上上手，学习零成本。 一、简介 # 1.1 概述 # LIGHTCONF 是一个基于 Netty 实现的配置管理平台，其核心设计目标是“为业务提供统一的配置管理服务”，可以做到开箱即用。\n1.2 特性 # 1、简单易用：上手非常简单，只需要引入 Maven 依赖和一行配置即可； 2、在线管理：提供配置管理中心，支持在线管理配置信息； 3、实时推送：配置信息更新后，实时推送配置信息，项目中配置数据会实时更新并生效，不需要重启线上机器； 4、配置备份：配置数据会在 MySQL 中对配置信息做备份，保证配置数据的安全性； 1.3 背景 # why not properties\n常规项目开发过程中，通常会将配置信息放在项目 resource 目录下的 properties 文件中，配置信息通常包括：JDBC 地址配置、Redis 地址配置、活动开关、阈值配置、黑白名单等等。使用 properties 维护配置信息将会导致以下几个问题：\n1、需要手动修改 properties 文件； 2、需要重新编译打包； 3、需要重启线上服务器 (项目集群时，更加令人崩溃) ; 4、配置生效不及时：因为流程复杂，新的配置生效需要经历比较长的时间； 5、不同环境上线包不一致：例如 JDBC 连接，不同环境需要差异化配置； why LIGHTCONF\n1、不需要 (手动修改 properties 文件) : 在配置管理中心提供的 Web 界面中，定位到指定配置项，输入配置的新值，点击更新按钮即可； 2、不需要 (重新编译打包) : 配置更新后，实时推送新配置信息至项目中，不需要编译打包； 3、不需要 (重启线上服务器) : 配置更新后，实时推送新配置信息至项目中，实时生效，不需要重启线上机器； 4、配置生效 \u0026ldquo;非常及时\u0026rdquo; : 点击更新按钮，新的配置信息将会即刻推送到项目中，瞬间生效，非常及时。比如一些开关类型的配置，配置变更后，将会立刻推送至项目中并生效，相对常规配置修改的繁琐流程，及时性可谓天壤之别； 项目在线预览地址 # 配置中心预览 接入 LIGHTCONF 的 Demo 项目预览 http://www.whforever.cn/lightconf-admin-web/ http://www.whforever.cn/lightconf-sample/ 源码仓库地址 # 源码仓库地址 Release Download github.com/haifeiWu/li… Download 1.5 环境 # Maven3+ Jdk1.7+ Tomcat7+ Mysql5.5+ 二、快速入门 # 2.1 初始化“数据库” # 请下载项目源码并解压，获取 \u0026ldquo;调度数据库初始化 SQL 脚本\u0026rdquo; 并执行即可。脚本位置如下：\nlightconf/doc/db/light-conf-0.1.1V.sql 2.2 编译源码 # 解压源码，按照 Maven 格式将源码导入 IDE, 使用 Maven 编译即可\nlightconf-admin：配置管理中心 lightconf-core：公共依赖 lightconf-common：公共依赖 lightconf-sample: 接入 LIGHTCONF 的 Demo 项目 2.3 “配置管理中心” 项目配置 # 项目：lightconf-admin 作用：管理线上配置信息 配置文件位置：\nlightconf/lightconf-admin/lightconf-admin-web/src/main/resources/light-conf.properties 配置项说明：\n# 配置登录lightconf的用户名，密码 light.conf.login.username=admin light.conf.login.password=123456 # mysql database setting jdbc.type=mysql jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/light-conf?useUnicode=true\u0026amp;characterEncoding=utf-8 jdbc.username=root jdbc.password=root_pwd # pool settings jdbc.pool.init=2 jdbc.pool.minIdle=3 jdbc.pool.maxActive=20 # jdbc.testSql=SELECT 'x' jdbc.testSql=SELECT 'x' FROM DUAL # 服务端启动监听端口 netty.server.port=9998 2.4 “接入 LIGHTCONF 的示例项目” 项目配置 # 项目：lightconf-sample 作用：接入LIGHTCONF的示例项目，供用户参考学习 A、引入 Maven 依赖 # \u0026lt;!-- lightconf-client --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.lightconf\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lightconf-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${project.parent.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; B、添加 LIGHTCONF 配置文件 # 可参考配置文件： lightconf/lightconf-sample/src/main/resources/light-conf.properties 配置项说明: # 连接light-conf-admin的IP地址 light.conf.host=127.0.0.1 # 连接light-conf-admin的端口号 light.conf.port=9998 ## 接入应用的uuid application.uuid=8705d6c8-bbe0-420c-9853-f780de4cb5ea C、LIGHTCONF 配置初始化\\[必须\\] # 可参考配置文件： lightconf/lightconf-sample/src/main/resources/spring/applicationcontext-light-conf.xml 配置项说明： \u0026lt;!-- ********************************* 核心配置[必须]：LIGHTCONF 配置 ********************************* --\u0026gt; \u0026lt;bean id=\u0026quot;xxlConf\u0026quot; class=\u0026quot;com.lightconf.core.spring.LightConfFactory\u0026quot; init-method=\u0026quot;init\u0026quot; destroy-method=\u0026quot;destroy\u0026quot; /\u0026gt; \u0026lt;!-- ********************************* 核心配置[必须]：LIGHTCONF netty client监听 ********************************* --\u0026gt; \u0026lt;bean id=\u0026quot;lightConfListener\u0026quot; class=\u0026quot;com.lightconf.core.listener.LightConfClientListener\u0026quot;\u0026gt;\u0026lt;/bean\u0026gt; 三、配置管理中心操作指南 # 3.1、应用管理 # 系统以 “应用” 为维度进行配置隔离。可进入 \u0026ldquo;配置管理界面\u0026rdquo; 操作和维护应用，应用属性说明如下：\nUUID:每个应用拥有唯一的 UUID,作为应用标识。 应用名称：该应用的名称； light-conf-app 3.2 配置管理 # 进入\u0026quot;配置管理\u0026quot; 界面，选择应用，然后可查看和操作该应用下配置数据，同时也可以通过应用管理页面的\u0026quot;应用配置信息\u0026quot;的 button 来进入该应用的配置信息页面，如下图所示。\nlight-conf-conf 新增配置：点击 \u0026ldquo;新增配置\u0026rdquo; 按钮可添加配置数据，配置属性说明如下：\nKEY:配置的 KEY,创建时将会自动添加所属项目的 APPName 作为前缀，生成最终的 Key。可通过客户端使用最终的 Key 获取配置； 描述：该配置的描述信息； VALUE：配置的值； ","date":"2018-04-22","externalUrl":null,"permalink":"/posts/zijianjiaoshoujiazhipeizhizhongxin-lightconfdeshixian/","section":"文章","summary":"在经历了一个多月的开发后，项目的第一个稳定版本终于完成，开源地址https://github.com/haifeiWu/lightconf，欢迎各路大神star，拍砖。 LIGHTCONF 使用了 Netty 实现底层通讯，保证配置的实时生效。接入 LIGHTCONF 事只需要添加 mav…","title":"自建脚手架之配置中心--LightConf 的实现","type":"posts"},{"content":"📌 本文原发布于掘金社区：测试环境服务器硬盘塞满问题排查\n项目中出现的问题 # 文章出处\n某天下午测试环境服务器出现 tab 无法补全命令，给出的提示大概意思就是说，无可用空间无法创建临时文件，不过这次跟上次出现的问题比较像，上次服务器出现的问题，因此楼主判断可能是服务器数据盘被占满，果不其然，使用df -h命令看到服务器数据盘出现 100%被占用的情况。\n问题排查过程 # 楼主首先想到的是查看 Linux 系统中占用数据盘最大的文件，通常情况下，最有可能找出占用磁盘空间的文件或文件夹的地方，主要是 /tmp or /var or /home or /。目前没有单个命令来完成查找的工作，通常可以使用一些命令的组合来帮助您找出磁盘上比较占用空间的文件或者文件夹。主要用到下面的三个命令：\ndu : 计算出单个文件或者文件夹的磁盘空间占用。 sort : 对文件行或者标准输出行记录排序后输出。 head : 输出文件内容的前面部分。 用下面的命令组合就可以完成上述查找工作：\ndu -h / | sort -n -r | head -n 10 上述命令的含义就是查找/目录下按照大小排序占用磁盘空间最大的 10 个文件。\n如果需要输出可读性更高的内容，请使用如下命令：\ndu -hsx * | sort -rh | head -10 ok，到此为止问题华丽丽地解决了，很开心哦。\n分享一个命令的使用 # lsof -i # 在使用 Linux 系统的过程中，有时候会遇到端口被占用而导致服务无法启动的情况。比如 HTTP 使用 80 端口，但当启动 Nginx 时，却发现此端口正在使用。\n这种情况大多数是由于软件冲突、或者默认端口设置不正确导致的，此时需要查看究竟哪个进程占用了端口，来决定进一步的处理方法。\n一般情况下查看某一端口的占用情况的用法是：lsof -i:端口号 例如查看 80 端口的使用情况\nlsof -i:80 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME java 7464 root 272u IPv6 7111192 0t0 TCP 192.168.201.8:45616-\u0026gt;192.168.201.8:http (CLOSE_WAIT) nginx 7555 root 7u IPv4 7110265 0t0 TCP *:http (LISTEN) nginx 7556 nobody 7u IPv4 7110265 0t0 TCP *:http (LISTEN) java 7573 root 210u IPv6 7110330 0t0 TCP 192.168.201.8:45422-\u0026gt;192.168.201.8:http (CLOSE_WAIT) java 7602 root 140u IPv6 7111090 0t0 TCP 192.168.201.8:45412-\u0026gt;192.168.201.8:http (CLOSE_WAIT) 结束该端口的占用可以使用kill pid的方法。\n","date":"2018-03-12","externalUrl":null,"permalink":"/posts/ceshihuanjingfuwuqiyingpansaimanwentipaicha/","section":"文章","summary":"某天下午测试环境服务器出现 tab 无法补全命令，给出的提示大概意思就是说，无可用空间无法创建临时文件，不过这次跟上次出现的问题比较像，上次服务器出现的问题，因此楼主判断可能是服务器数据盘被占满，果不其然，使用 df -h 命令看到服务器数据盘出现 100%被占用的情况。楼主首先想到的…","title":"测试环境服务器硬盘塞满问题排查","type":"posts"},{"content":"📌 本文原发布于掘金社区：Java 命令之 javap 初探\n原博客地址\njavap 是 jdk 自带的一个工具，在 jdk 安装目录的/bin 下面可以找到，可以对代码反编译，也可以查看 Java 编译器生成的字节码，对代码的执行过程进行分析，了解 jvm 内部的工作。\n下面列举 javap 命令的常用 options 及其功能描述，更多功能的使用请自行 Google，楼主不做赘述。\n用法摘要 # -help 帮助 -l 输出行和变量的表 -public 只输出public方法和域 -protected 只输出public和protected类和成员 -package 只输出包，public和protected类和成员，这是默认的 -p -private 输出所有类和成员 -s 输出内部类型签名 -c 输出分解后的代码，例如，类中每一个方法内，包含java字节码的指令， -verbose 输出栈大小，方法参数的个数 -constants 输出静态final常量 实例分析 # javap 命令分解一个 class 文件，它根据 options 来决定到底输出什么。如果没有使用 options,那么 javap 将会输出该 class 文件中的包，类里的 protected 和 public 域以及类里的所有方法。javap 将会把它们输出到标准输出上。来看这个例子，先编译(javac)下面这个类。\npackage com.thundersoft.metadata.test.kafka; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; import org.junit.Test; import java.util.Arrays; import java.util.Properties; public class KafkaTest { @Test public void testProducer() { Properties props = new Properties(); props.put(\u0026#34;bootstrap.servers\u0026#34;, \u0026#34;192.168.204.30:9092\u0026#34;); props.put(\u0026#34;acks\u0026#34;, \u0026#34;all\u0026#34;); props.put(\u0026#34;retries\u0026#34;, 0); props.put(\u0026#34;batch.size\u0026#34;, 16384); props.put(\u0026#34;linger.ms\u0026#34;, 1); props.put(\u0026#34;buffer.memory\u0026#34;, 33554432); props.put(\u0026#34;key.serializer\u0026#34;, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;); props.put(\u0026#34;value.serializer\u0026#34;, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;); Producer\u0026lt;String, String\u0026gt; producer = new KafkaProducer\u0026lt;\u0026gt;(props); for(int i = 0; i \u0026lt; 100; i++) { producer.send(new ProducerRecord\u0026lt;String, String\u0026gt;(\u0026#34;my-topic\u0026#34;, Integer.toString(i), Integer.toString(i))); } producer.close(); } @Test public void testKafkaConsumer() { Properties props = new Properties(); props.put(\u0026#34;bootstrap.servers\u0026#34;, \u0026#34;192.168.204.30:9092\u0026#34;); props.put(\u0026#34;group.id\u0026#34;, \u0026#34;test\u0026#34;); props.put(\u0026#34;enable.auto.commit\u0026#34;, \u0026#34;true\u0026#34;); props.put(\u0026#34;auto.commit.interval.ms\u0026#34;, \u0026#34;1000\u0026#34;); props.put(\u0026#34;key.deserializer\u0026#34;, \u0026#34;org.apache.kafka.common.serialization.StringDeserializer\u0026#34;); props.put(\u0026#34;value.deserializer\u0026#34;, \u0026#34;org.apache.kafka.common.serialization.StringDeserializer\u0026#34;); KafkaConsumer\u0026lt;String, String\u0026gt; consumer = new KafkaConsumer\u0026lt;\u0026gt;(props); consumer.subscribe(Arrays.asList(\u0026#34;my-topic\u0026#34;)); while (true) { ConsumerRecords\u0026lt;String, String\u0026gt; records = consumer.poll(100); for (ConsumerRecord\u0026lt;String, String\u0026gt; record : records) System.out.printf(\u0026#34;offset = %s, key = %s, value = %s%n\u0026#34;, record.topic(), record.key(), record.value()); } } public static void main(String[] args) { int a = 2; int b = 3; int sum = a*b; System.out.println(sum); } } 在命令行上键入 javap KafkaTest 后，输出结果如下\npublic class com.thundersoft.metadata.test.kafka.KafkaTest { public com.thundersoft.metadata.test.kafka.KafkaTest(); public void testProducer(); public void testKafkaConsumer(); public static void main(java.lang.String[]); } 结合代码分析编译器执行过程 # 这里只关注 main 方法内部的代码逻辑，main 方法代码如下\npublic static void main(String[] args) { int a = 2; int b = 3; int sum = a*b; System.out.println(sum); } 在命令行上键入 javap -c KafkaTest 后，输出结果如下\npublic static void main(java.lang.String[]); Code: 0: iconst_2 1: istore_1 2: iconst_3 3: istore_2 4: iload_1 5: iload_2 6: imul 7: istore_3 8: getstatic #47 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3 12: invokevirtual #54 // Method java/io/PrintStream.println:(I)V 15: return 如上面代码所示，iconst_2 与 iconst_3分别代表常量 2，3。istore_1，istore_2 分别代表定义两个普通变量，iload_1，iload_2 分别表示加载 istore_1，istore_2 两个变量到数据栈中，imul 表示两个变量做乘法运算，结果赋值给变量 istore_3，最后将结果输出，程序返回。\n在分析这段简单代码的过程中，楼主发现了一个 jvm 编译命令的网站，分享出来jvm 指令。\n总结 # 楼主在上面做了一个简单的代码分析的过程，希望可以帮助到有缘人。javap 可以用于反编译和查看编译器编译后的字节码。一般用到的不多，不过平时用 javap -c 比较多，该命令用于列出每个方法所执行的 JVM 指令，用来解决比较棘手的逻辑错误是个不错的选择。另外通过字节码和源代码的对比，深入分析 Java 的编译原理及代码执行过程，解决各种 Java 原理级别的问题。\n","date":"2018-02-26","externalUrl":null,"permalink":"/posts/javaminglingzhijavapchutan/","section":"文章","summary":"下面列举 javap 命令的常用 options 及其功能描述，更多功能的使用请自行 Google，楼主不做赘述。javap 命令分解一个 class 文件，它根据 options 来决定到底输出什么。如果没有使用 options,那么 javap 将会输出该 class 文件中的包，类里的 protect…","title":"Java 命令之 javap 初探","type":"posts"},{"content":"","date":"2018-02-26","externalUrl":null,"permalink":"/tags/%E7%BC%96%E8%AF%91%E5%99%A8/","section":"标签","summary":"","title":"编译器","type":"tags"},{"content":"📌 本文原发布于掘金社区：Java 实现终止线程池中正在运行的定时任务\n源于开发 # 最近项目中遇到了一个新的需求，就是实现一个可以动态添加定时任务的功能。说到这里，有人可能会说简单啊，使用 quartz 就好了，简单粗暴。然而 quartz 框架太重了，小项目根本不好操作啊。当然，也有人会说，jdk 提供了 timer 的接口啊，完全够用啊。但是我们项目的需求完全是多线程的模型啊，而 timer 是单线程的，so，楼主最后还是选择了 jdk 的线程池。\n线程池是什么 # Java 通过 Executors 提供四种线程池，分别为：**newCachedThreadPool：**创建一个可缓存线程池，如果线程池长度超过处理需要，可灵活回收空闲线程，若无可回收，则新建线程。newFixedThreadPool : 创建一个定长线程池，可控制线程最大并发数，超出的线程会在队列中等待。newScheduledThreadPool : 创建一个定长线程池，支持定时及周期性任务执行。newSingleThreadExecutor : 创建一个单线程化的线程池，它只会用唯一的工作线程来执行任务，保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。\n楼主项目中用到的是 newScheduledThreadPool，就这些吧，再多的楼主就班门弄斧了，Google 一下，一大堆。\n线程池 service 的获取 # 楼主通过单例模式来获取线程池的 service，代码如下：\n/** * 线程池创建. * @author wuhf * @date 2018/01/16 */ public class ThreadPoolUtils { private static ScheduledExecutorService executorService; private ThreadPoolUtils() { //手动创建线程池. executorService = new ScheduledThreadPoolExecutor(10, new BasicThreadFactory.Builder().namingPattern(\u0026#34;syncdata-schedule-pool-%d\u0026#34;).daemon(true).build()); } private static class PluginConfigHolder { private final static ThreadPoolUtils INSTANCE = new ThreadPoolUtils(); } public static ThreadPoolUtils getInstance() { return PluginConfigHolder.INSTANCE; } public ScheduledExecutorService getThreadPool(){ return executorService; } } 中断某一个正在运行的线程代码实现 # 废话就不多说了，代码如下：\n/** * 中断线程池的某个任务. */ public class InterruptThread implements Runnable { private int num; public InterruptThread (int num){ this.num = num; } public static void main(String[] args) throws InterruptedException { Thread interruptThread = new Thread(new InterruptThread(1)); ScheduledFuture\u0026lt;?\u0026gt; t = ThreadPoolUtils.getInstance().getThreadPool().scheduleAtFixedRate(interruptThread,0,2, TimeUnit.SECONDS); InterruptThread interruptThread1 = new InterruptThread(2); ThreadPoolUtils.getInstance().getThreadPool().scheduleAtFixedRate(interruptThread1,0,2, TimeUnit.SECONDS); InterruptThread interruptThread2 = new InterruptThread(3); ThreadPoolUtils.getInstance().getThreadPool().scheduleAtFixedRate(interruptThread2,0,2, TimeUnit.SECONDS); Thread.sleep(5000); //终止正在运行的线程interruptThread t.cancel(true); while (true){ } } @Override public void run() { System.out.println(\u0026#34;this is a thread\u0026#34; + num); } } 踩坑记录 # 楼主在使用如下代码时，突然想到当这个定时任务需要被停止时该如何停止线程运行\nThreadPoolUtils.getInstance().getThreadPool().scheduleAtFixedRate(interruptThread,0,2, TimeUnit.SECONDS); 既然我有这样的需求，那就 Google 一下吧，找了大半圈，愣是没找到相关资料，都是一些关于 Java 线程池的深入分析。或者是全局变量啥的，并没有找到令楼主满意的解决方案。\n既然没有现成的那就扒一下 scheduleAtFixedRate 的底层源码看看是什么东西吧，果不其然我在源码中看到了 scheduleAtFixedRate 方法的具体实现，发现它的返回值是 ScheduledFuture。\npublic ScheduledFuture\u0026lt;?\u0026gt; scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (period \u0026lt;= 0) throw new IllegalArgumentException(); ScheduledFutureTask\u0026lt;Void\u0026gt; sft = new ScheduledFutureTask\u0026lt;Void\u0026gt;(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); RunnableScheduledFuture\u0026lt;Void\u0026gt; t = decorateTask(command, sft); sft.outerTask = t; delayedExecute(t); return t; } 接着往下我们再看看 ScheduledFuture 里面有什么东西吧，没有让楼主失望，看到了这个\npublic boolean cancel(boolean mayInterruptIfRunning) { boolean cancelled = super.cancel(mayInterruptIfRunning); if (cancelled \u0026amp;\u0026amp; removeOnCancel \u0026amp;\u0026amp; heapIndex \u0026gt;= 0) remove(this); return cancelled; } //从线程的运行队列中移除当前线程 public boolean remove(Runnable task) { boolean removed = workQueue.remove(task); tryTerminate(); // In case SHUTDOWN and now empty return removed; } 再往上查 super.cancel(mayInterruptIfRunning)是什么东西，我们看到了这个，\n//通过调用线程的interrupt方法终止线程运行 public boolean cancel(boolean mayInterruptIfRunning) { if (!(state == NEW \u0026amp;\u0026amp; UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) return false; try { // in case call to interrupt throws exception if (mayInterruptIfRunning) { try { Thread t = runner; if (t != null) t.interrupt(); } finally { // final state UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); } } } finally { finishCompletion(); } return true; } 到这里所有的问题都迎刃而解。\n总结一下吧 # 项目中总是会遇到比较难搞的解决方案，当 Google 不太好找时，翻一下 jdk 的源码或许也是一个不错的方法。最后宣传一下楼主的博客，博客已经迁移到腾讯云上，跟几个大学的小伙伴一起经营，内容涵盖 Java，Android 等，感兴趣的小伙伴请移步楼主跟他的小伙伴们的博客\n","date":"2018-01-17","externalUrl":null,"permalink":"/posts/javashixianzhongzhixianchengchizhongzhengzaiyunxingdedingshi/","section":"文章","summary":"最近项目中遇到了一个新的需求，就是实现一个可以动态添加定时任务的功能。说到这里，有人可能会说简单啊，使用 quartz 就好了，简单粗暴。然而 quartz 框架太重了，小项目根本不好操作啊。当然，也有人会说，jdk 提供了 timer 的接口啊，完全够用啊。但是我们项目的需求完全是多线程的…","title":"Java 实现终止线程池中正在运行的定时任务","type":"posts"},{"content":"","date":"2017-12-08","externalUrl":null,"permalink":"/tags/shell/","section":"标签","summary":"","title":"Shell","type":"tags"},{"content":"📌 本文原发布于掘金社区：线上问题解决及 shell 脚本实现自动保留最近 n 次备份记录\n项目中出现的问题 # 某天上午服务器卡顿特别严重，页面加载速度奇慢，并且某些页面刷新出现 404 的问题，就连服务器的 tab 命令的自动提示都出现了问题，楼主费了九牛二虎之力，排查服务器后发现，服务器数据盘出现 100%被占用的问题，导致该问题出现的原因是，Jenkins 每次部署服务器的时候，都会自动将上一次的 war 备份，由于开发阶段的频繁部署，最终硬盘被占满，便出现上述描述的情况。\n解决方案的实现过程 # 获取备份文件夹下的所有文件 # 根据 Google 爸爸的提示，楼主找到了下面的命令，\nfind 对应目录 -mtime +天数 -name \u0026#34;文件名\u0026#34; -exec rm -rf {} \\; 实例命令：\nfind /opt/soft/log/ -mtime +30 -name \u0026#34;*.log\u0026#34; -exec rm -rf {} \\; 说明：\n将/opt/soft/log/目录下所有 30 天前带\u0026quot;.log\u0026quot;的文件删除。\n具体参数说明如下：\nfind：Linux 的查找命令，用于查找指定条件的文件；/opt/soft/log/：想要清理的任意目录；-mtime:标准语句写法；+30:查找 30 天前的文件，这里用数字代表天数；\u0026quot; ×.log\u0026quot;:希望查找的数据类型，\u0026quot;×.jpg\u0026quot;表示查找扩展名为 jpg 的所有文件，\u0026quot;×\u0026ldquo;表示查找所有文件，这个可以灵活运用，举一反三；-exec:固定写法；rm -rf:强制删除文件，包括目录；{} ;:固定写法，一对大括号+空格+分号；\n解决问题的思路：\n当然楼主不能傻乎乎地将备份目录下的所有文件都删除掉，这样的话，备份不就失去了意义。所以换一下思路便有了下面的命令\nfind ${BAK_HOME} -mtime +1 -name \u0026#34;*:*\u0026#34; | wc -l 说明：\n获取备份目录下所有一天前带\u0026rdquo;：\u0026ldquo;的文件数量。\nfind ${BAK_HOME} -mtime +1 -name \u0026#34;*:*\u0026#34; 说明：\n获取备份目录下所有一天前带”：”的文件数量。\n到了这里我们的问题差不多就可以解决了。so，请接着往下看：\n解决方案的思路及 shell 脚本的实现 # 思路 # 目前解决该问题的方法是在原来部署脚本中添加一段脚本，实现保留最近 10 次部署的备份记录，超过 10 次的备份记录将被删除。\nshell 脚本的实现 # 逻辑很清晰，思路很明了，我就不在这里接着阐述了，谢谢大家！\n#!/bin/sh BAK_HOME=\u0026#34;/home/saveHistoryData/iam-share-8083\u0026#34; keepNum=5 fileNum=$(find ${BAK_HOME} -mtime +1 -name \u0026#34;*:*\u0026#34; | wc -l) echo \u0026#34;${fileNum}\u0026#34; for file in $(find ${BAK_HOME} -mtime +1 -name \u0026#34;*:*\u0026#34;); do if test $[fileNum] -gt $[keepNum];then rm -rf ${file} fileNum=${fileNum}-1 echo \u0026#34;delete backup file\u0026#34; else echo \u0026#34;do no thing\u0026#34; fi done ","date":"2017-12-08","externalUrl":null,"permalink":"/posts/xianshangwentijiejuejishelljiaobenshixianzidongbaoliuzuijinn/","section":"文章","summary":"某天上午服务器出现卡顿特别严重，页面加载速度奇慢，并且某些页面刷新出现 404 的问题，就连服务器的 tab 命令的自动提示都出现了问题，楼主费了九牛二虎之力，根据服务器排查发现，服务器数据盘出现 100%被占用的问题，导致该问题出现的原因是，Jenkins 每次部署服务器的时候，都会自动…","title":"线上问题解决及 shell 脚本实现自动保留最近 n 次备份记录","type":"posts"},{"content":"📌 本文原发布于掘金社区：Spring aop 实现权限管理\n问题源于项目开发 # 最近项目中需要做一个权限管理模块，按照之前同事的做法是在 controller 层的每个接口调用之前做逻辑判断，这样做也没有不妥，但是代码重复率太高，而且是体力劳动，so，便有了如题所说的使用 Spring aop 做一个切点来实现通用功能的权限管理，这样也就降低了项目后期开发的可扩展性。\n权限管理的代码实现与配置文件 # 在最小的代码修改程度上，aop 无疑是最理想的选择。项目中有各种权限复合的情况，相对来说逻辑复杂度比较高，所以一步步来。因为权限涉及到的是后端接口的调用，所以楼主选择在 controller 层代码上做切面，而切点就是 controller 中的各个方法块，对于通用访问权限，我们使用 execution 表达式进行排除。\n只读管理员权限的实现及切点选择 # 对于实现排除通用的 controller，楼主采用的是 execution 表达式逻辑运算。因为只读管理员拥有全局读权限，而对于增删改权限，楼主采用切点切入增删改的方法，so，这个时候规范的方法命名就很重要了。对于各种与只读管理员复合的管理员，我们在代码中做一下特殊判断即可。下面是 Spring aop 的配置文件的配置方法。\n\u0026lt;!--非法权限抛出异常的spring mvc的处理--\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.web.servlet.handler.SimpleMappingExceptionResolver\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;exceptionMappings\u0026#34;\u0026gt; \u0026lt;props\u0026gt; \u0026lt;prop key=\u0026#34;com.thundersoft.metadata.exception.AccessDeniedException\u0026#34;\u0026gt;forward:/auth/readOnly\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;usersPermissionsAdvice\u0026#34; class=\u0026#34;com.thundersoft.metadata.aop.UsersPermissionsAdvice\u0026#34;/\u0026gt; \u0026lt;aop:config\u0026gt; \u0026lt;!--定义切面 --\u0026gt; \u0026lt;!-- 定义切入点 (配置在com.thundersoft.metadata.web.controller下所有的类在调用之前都会被拦截) --\u0026gt; expression=\u0026#34;(execution(* com.thundersoft.metadata.web.controller.*.add*(..)) or execution(* com.thundersoft.metadata.web.controller.*.edit*(..)) or execution(* com.thundersoft.metadata.web.controller.*.del*(..)) or execution(* com.thundersoft.metadata.web.controller.*.update*(..)) or execution(* com.thundersoft.metadata.web.controller.*.insert*(..)) or execution(* com.thundersoft.metadata.web.controller.*.modif*(..))) or execution(* com.thundersoft.metadata.web.controller.*.down*(..))) and ( !execution(* com.thundersoft.metadata.web.controller.FindPasswordController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.SelfServiceController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.HomeController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.UserStatusController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.DashboardController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.MainController.*(..))))\u0026#34; id=\u0026#34;authPointCut\u0026#34;/\u0026gt; \u0026lt;!--方法被调用之前执行的 --\u0026gt; pointcut-ref=\u0026#34;authPointCut\u0026#34;/\u0026gt; \u0026lt;/aop:aspect\u0026gt; 只读管理员权限管理代码实现 # 上面说了那么多，废话不多说了，下面是对只读权限与各种复合权限进行控制的切面代码实现。\n/** * 对只读管理员以及其复合管理员进行aop拦截判断. * @param joinPoint 切入点. * @throws IOException */ public void readOnly(JoinPoint joinPoint) throws IOException { /** * 获取被拦截的方法. */ String methodName = joinPoint.getSignature().getName(); /** * 获取被拦截的对象. */ Object object = joinPoint.getTarget(); logger.info(\u0026#34;权限管理aop,方法名称{}\u0026#34; + methodName); HttpServletRequest request =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletResponse response =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); String roleFlag = GetLoginUserInfor.getLoginUserRole(request); /** * 超级管理员 */ if (PermissionsLabeled.super_Admin.equals(roleFlag)) { return; } /** * 只读管理员做数据更改权限的判断 */ if (PermissionsLabeled.reader_Admin.equals(roleFlag)) { if (methodName.contains(\u0026#34;redirectToLogout\u0026#34;)) { return; } logger.error(\u0026#34;只读管理员无操作权限!\u0026#34;); throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } /** * 部门管理员,且为只读管理员, */ if (PermissionsLabeled.dept_reader_Admin.equals(roleFlag)) { if (object instanceof DepartmentController) { return; } if (object instanceof UserController) { if (methodName.contains(\u0026#34;addAdmin\u0026#34;)) { throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } if (methodName.contains(\u0026#34;deleteAdmin\u0026#34;)) { throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } if (methodName.contains(\u0026#34;updateAdmin\u0026#34;)) { throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } return; } if (object instanceof GroupController) { return; } logger.error(\u0026#34;部门管理员,且为只读管理员无操作权限!\u0026#34;); throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } /** * 应用管理员,且为只读管理员 */ if (PermissionsLabeled.app_reader_Admin.equals(roleFlag)) { if (object instanceof AppController) { return; } if (object instanceof AppPolicyController) { return; } logger.error(\u0026#34;应用管理员,且为只读管理员无操作权限!\u0026#34;); throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } /** * 部门管理员,且为应用管理员,且为只读管理员 */ if (PermissionsLabeled.dept_app_reader_Admin.equals(roleFlag)) { if (object instanceof DepartmentController) { return; } if (object instanceof UserController) { return; } if (object instanceof GroupController) { return; } if (object instanceof AppController) { return; } if (object instanceof AppPolicyController) { return; } logger.error(\u0026#34;部门管理员,且为应用管理员,且为只读管理员无操作权限\u0026#34;); throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } } 具有专门功能的管理员权限控制的切点选择 # 因为具有专门的管理员权限比较特殊，楼主采用的方式是除了通用访问权限之外的 controller 全切，特殊情况在代码逻辑里面实现即可。配置文件代码如下：\n\u0026lt;!--定义切面 --\u0026gt; \u0026lt;!-- 定义切入点 (配置在com.thundersoft.metadata.web.controller下所有的类在调用之前都会被拦截) --\u0026gt; expression=\u0026#34;(execution(* com.thundersoft.metadata.web.controller.*.*(..)) and ( !execution(* com.thundersoft.metadata.web.controller.FindPasswordController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.SelfServiceController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.HomeController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.UserStatusController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.DashboardController.*(..)) and !execution(* com.thundersoft.metadata.web.controller.MainController.*(..))))\u0026#34; id=\u0026#34;appAuthPointCut\u0026#34;/\u0026gt; \u0026lt;!--方法被调用之前执行的 --\u0026gt; pointcut-ref=\u0026#34;appAuthPointCut\u0026#34;/\u0026gt; 权限管理的切面代码实现 # /** * 对应用管理员以及部门管理员进行aop拦截判断. * @param joinPoint 切入点. * @throws IOException */ public void appDeptAuth(JoinPoint joinPoint) throws IOException { /** * 获取被拦截的方法. */ String methodName = joinPoint.getSignature().getName(); /** * 获取被拦截的对象. */ Object object = joinPoint.getTarget(); logger.info(\u0026#34;权限管理aop,方法名称{}\u0026#34;,methodName); HttpServletRequest request =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletResponse response =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); String roleFlag = GetLoginUserInfor.getLoginUserRole(request); /** * 超级管理员 */ if (PermissionsLabeled.super_Admin.equals(roleFlag)) { return; } /** * 应用管理员做数据更改权限的判断 */ if (PermissionsLabeled.app_Admin.equals(roleFlag)) { if (object instanceof AppController) { return; } if (object instanceof AppPolicyController) { return; } logger.error(\u0026#34;应用管理员无操作权限\u0026#34;); throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } else if (PermissionsLabeled.dept_Admin.equals(roleFlag)) { if (object instanceof DepartmentController) { return; } if (object instanceof UserController) { return; } if (object instanceof GroupController) { return; } if (\u0026#34;getAllDepartments\u0026#34;.equals(methodName)) { return; } logger.error(\u0026#34;应用管理员无操作权限\u0026#34;); throw new AccessDeniedException(\u0026#34;您无权操作！\u0026#34;); } else { return; } } 自定义权限非法异常代码 # /** * @author wuhf0703@thundersoft.com * @date 2017/12/12 */ public class AccessDeniedException extends RuntimeException { /** * Constructs a \u0026lt;code\u0026gt;AccessDeniedException\u0026lt;/code\u0026gt; with the specified message. * * @param msg the detail message. */ public AccessDeniedException(String msg) { super(msg); } /** * Constructs a {@code AccessDeniedException} with the specified message and root cause. * * @param msg the detail message. * @param t root cause */ public AccessDeniedException(String msg, Throwable t) { super(msg, t); } } 博客首发链接\n","date":"2017-12-06","externalUrl":null,"permalink":"/posts/spring-aopshixianquanxianguanli/","section":"文章","summary":"最近项目中需要做一个权限管理模块，按照之前同事的做法是在 controller 层的每个接口调用之前上做逻辑判断，这样做也没有不妥，但是代码重复率太高，而且是体力劳动，so，便有了如题所说的使用 Spring aop 做一个切点来实现通用功能的权限管理，这样也就降低了项目后期开发的可扩…","title":"Spring aop 实现权限管理","type":"posts"},{"content":"","date":"2017-11-05","externalUrl":null,"permalink":"/tags/jmeter/","section":"标签","summary":"","title":"JMeter","type":"tags"},{"content":"📌 本文原发布于掘金社区：基于 JMeter 的压测工具的实现\nJMeter Web 化 # 代码说明文档\nJMeter WEB 项目使用说明文档\nJMeter Web 项目使用指南 # 项目的内网访问地址：http://10.2.250.202:9099/JmeterWEB/\n打开链接，你会看到如下界面（请大家尽量使用 chrome 浏览器）：\n在界面中选择对应的选项卡：（目前只支持 HTTP 模板，自定义脚本上传，测试相应结果两个选项卡），HTTP 模板是根据页面选择的参数生成 jmx 文件，自定义脚本是用户直接上传 jmx 脚本。\n下图是执行脚本的页面，在页面中可以选择在本地执行与在远程机执行（远程机执行是指在 3 台机器上同步执行脚本，比如你的脚本是 10 个线程，选择两台远程机再加上本机就相当于执行 30 个线程）。其他两台远程机器的 IP 是 10.2.250.203:1099，10.2.250.204:1099。\n生成的测试报告如下图所示。\n查看 Response 和 request 的数据 JMeter3.0 提供一个用于生成 HTML 页面格式图形化报告的扩展模块。该模块支持通过两种方式生成多维度图形化测试报告：在 JMeter 性能测试结束时，自动生成本次测试的 HTML 图形化报告；使用一个已有的结果文件(如 CSV 文件)来生成该次结果的 HTML 图形化报告。其默认提供的度量维度包括：\nAPDEX(Application Performance Index)指数\n聚合报告\n类似于UI上的Aggregate Report\nErrors 报告\n展示不同错误类型的数量以及百分比\n响应时间变化曲线 展示平均响应时间随时间变化情况\n类似于JMeter Plugins在UI上的jp@gc - Response Times Over Time\n数据吞吐量时间曲线\n展示每秒数据吞吐量随时间变化的情况 类似于JMeter Plugins在UI上的jp@gc - Bytes Throughput Over Time\nLatency time 变化曲线\n展示Latency time随时间变化的情况\n类似于JMeter Plugins在UI上的jp@gc - Response Latencies Over Time\n每秒点击数曲线\n类似于JMeter Plugins在UI上的jp@gc - Hits per Second\nHTTP 状态码时间分布曲线\n展示响应状态码随时间的分布情况\n类似于JMeter Plugins在UI上的jp@gc - Response Codes per Second\n事务吞吐量时间曲线(TPS)\n展示每秒处理的事务数随时间变化情况\n类似于JMeter Plugins在UI上的jp@gc - Transactions per Second\n平均响应时间与每秒请求数的关系图\n展示平均响应时间与每秒请求数(可以理解为QPS)的关系\nLatency time 与每秒请求数的关系图\n展示Latency time与每秒请求数的关系\n响应时间百分位图\n响应时间的百分位分布图\n活动线程数变化曲线\n展示测试过程中活动线程数随时间变化情况\n平均响应时间与线程数的关系图\n展示平均响应时间与线程数的关系 类似于JMeter Plugins在UI上的jp@gc - Response Times vs Threads\n柱状响应时间分布图\n展示落在各个平均响应时间区间的请求数情况\n","date":"2017-11-05","externalUrl":null,"permalink":"/posts/jiyu-jmeterdeyacegongjudeshixian/","section":"文章","summary":"在界面中选择对应的选项卡：（目前只支持 HTTP 模板，自定义脚本上传，测试相应结果两个选项卡），HTTP 模板是根据页面选择的参数生成 jmx 文件，自定义脚本是用户直接上传 jmx 脚本。下图是执行脚本的页面，在页面中可以选择在本地执行与在远程机执行（远程机执行是指在 3 台机器上同步执行…","title":"基于 JMeter 的压测工具的实现","type":"posts"},{"content":"📌 本文原发布于掘金社区：扒一扒 Spring，dom4j 实现模拟实现读取 XML\n今天 leader 提出需求，原来公司项目中读取解析 XML 文件的代码效率太低，考虑切换一种 XML 为数据封装格式与读取方式以提高效率。我这灵机一动 Spring 对 bean 的依赖注入就是读取 XML 文件，可以尝试扒一扒 Spring 的源码，来实现一个轻量级的方案。\n重构 XML 文件，向 Spring 的 XML 文件格式看齐 # 重构完成的 XML 文件格式如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans\u0026gt; \u0026lt;bean name=\u0026#34;iaminformation\u0026#34; class=\u0026#34;com.whf.readxml.model.IamConfig\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;iamUrl\u0026#34; value=\u0026#34;http:192.168.7.154:8080\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;token\u0026#34; value=\u0026#34;abcdefg\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;iamName\u0026#34; value=\u0026#34;中电科\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;sourceNumber\u0026#34; value=\u0026#34;4\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;timeSpan\u0026#34; value=\u0026#34;12\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean name=\u0026#34;plugin\u0026#34; class=\u0026#34;com.whf.readxml.model.PluginAttributes\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;name\u0026#34; value=\u0026#34;ADTest1\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;pluginType\u0026#34; value=\u0026#34;AdPlugin\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;code\u0026#34; value=\u0026#34;100001\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;ldap://192.168.7.241:389\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;adminPsw\u0026#34; value=\u0026#34;1qaz2wsx2017\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;adminName\u0026#34; value=\u0026#34;QUARKDATA\\administrator\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;adDn\u0026#34; value=\u0026#34;OU=test,DC=quarkdata,DC=com\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;securityAuthentication\u0026#34; value=\u0026#34;simple\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;staffFieldMatch\u0026#34;\u0026gt; \u0026lt;bean name=\u0026#34;staffFieldMatch\u0026#34; class=\u0026#34;com.whf.readxml.model.StaffFieldMatch\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idField\u0026#34; value=\u0026#34;objectGUID\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;userNameField\u0026#34; value=\u0026#34;sAMAccountName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;firstNameField\u0026#34; value=\u0026#34;givenName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;lastNameField\u0026#34; value=\u0026#34;sn\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;displayNameField\u0026#34; value=\u0026#34;displayName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;phoneNumberField\u0026#34; value=\u0026#34;\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;telField\u0026#34; value=\u0026#34;homePhone\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;emailField\u0026#34; value=\u0026#34;email\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;orgFieldMatch\u0026#34;\u0026gt; \u0026lt;bean name=\u0026#34;orgFieldMatch\u0026#34; class=\u0026#34;com.whf.readxml.model.OrgFieldMatch\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idField\u0026#34; value=\u0026#34;objectGUID\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;nameField\u0026#34; value=\u0026#34;name\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;displayNameField\u0026#34; value=\u0026#34;ou\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;groupFieldMatch\u0026#34;\u0026gt; \u0026lt;bean name=\u0026#34;groupFieldMatch\u0026#34; class=\u0026#34;com.whf.readxml.model.GroupFieldMatch\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idFied\u0026#34; value=\u0026#34;objectGUID\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;nameField\u0026#34; value=\u0026#34;sAMAccountName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;displayNameField\u0026#34; value=\u0026#34;displayName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;decriptionField\u0026#34; value=\u0026#34;info\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean name=\u0026#34;plugin\u0026#34; class=\u0026#34;com.whf.readxml.model.PluginAttributes\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;name\u0026#34; value=\u0026#34;LDAPTest1\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;pluginType\u0026#34; value=\u0026#34;LdapPlugin\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;code\u0026#34; value=\u0026#34;100002\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;ldap://192.168.7.245/\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;adminPsw\u0026#34; value=\u0026#34;test123456\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;adminName\u0026#34; value=\u0026#34;cn=admin,dc=thundersoft,dc=com\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;adDn\u0026#34; value=\u0026#34;dc=thundersoft,dc=com\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;securityAuthentication\u0026#34; value=\u0026#34;simple\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;staffFieldMatch\u0026#34;\u0026gt; \u0026lt;bean name=\u0026#34;staffFieldMatch\u0026#34; class=\u0026#34;com.whf.readxml.model.StaffFieldMatch\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idField\u0026#34; value=\u0026#34;gidNumber\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;userNameField\u0026#34; value=\u0026#34;uid\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;firstNameField\u0026#34; value=\u0026#34;givenName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;lastNameField\u0026#34; value=\u0026#34;sn\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;displayNameField\u0026#34; value=\u0026#34;displayName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;phoneNumberField\u0026#34; value=\u0026#34;telephoneNumber\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;telField\u0026#34; value=\u0026#34;tel\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;emailField\u0026#34; value=\u0026#34;email\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;orgFieldMatch\u0026#34;\u0026gt; \u0026lt;bean name=\u0026#34;orgFieldMatch\u0026#34; class=\u0026#34;com.whf.readxml.model.OrgFieldMatch\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idField\u0026#34; value=\u0026#34;dn\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;nameField\u0026#34; value=\u0026#34;ou\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;displayNameField\u0026#34; value=\u0026#34;orgDisplayName\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;groupFieldMatch\u0026#34;\u0026gt; \u0026lt;bean name=\u0026#34;groupFieldMatch\u0026#34; class=\u0026#34;com.whf.readxml.model.GroupFieldMatch\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idFied\u0026#34; value=\u0026#34;dn\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;nameField\u0026#34; value=\u0026#34;cn\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;displayNameField\u0026#34; value=\u0026#34;groupDisplayName\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;decriptionField\u0026#34; value=\u0026#34;description\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 看起来很眼熟的有没有，跟 Spring 的配置文件一样哦。\n扒一扒 Spring 读取 XML 文件的源码 # 手动扒了一下 Spring 读取 XML 文件的代码，由于 Spring 过于庞大，读取 Spring 的 XML 方法又按照读取不同的标签被分拆出 n 多个方法，楼主能力有限就不在这里卖弄了，不过 Spring 从配置文件把 bean 加载到 bean 工厂跟楼主下面读取 XML 文件的方式理论上是一样的。\n废话不多说，上代码：\n/** * 模拟spring依赖注入的方式读取xml文件. * @author whf * @date Aug 23, 2017 */ public class XMLBeanFactory { private String xmlName; private SAXReader reader; private Document document; /** * 构造方法. * @param xmlName xmlName. */ public XMLBeanFactory(String xmlName) { // 在构造方法中 try { this.xmlName = xmlName; reader = new SAXReader(); document = reader.read(this.getClass().getClassLoader().getResourceAsStream(xmlName)); } catch (DocumentException e) { e.printStackTrace(); } } /** * 获取相同类型的bean. * @param type bean的class type. * @return 返回相同类型的bean的list. * @throws Exception Exception. */ public \u0026lt;T\u0026gt; List\u0026lt;T\u0026gt; getBeansOfType(Class\u0026lt;T\u0026gt; type) throws Exception { List\u0026lt;T\u0026gt; objects = new ArrayList\u0026lt;\u0026gt;(); try { Element root = document.getRootElement(); List\u0026lt;Element\u0026gt; beans = root.elements(); if (beans.size() \u0026gt; 0) { for (Element bean : beans) { if (bean.attributeValue(\u0026#34;class\u0026#34;).equals(type.getName())) { T object = null; String clazz = bean.attributeValue(\u0026#34;class\u0026#34;); // 通过反射来创建对象 Class beanClass = Class.forName(clazz); object = (T) beanClass.newInstance(); List\u0026lt;Element\u0026gt; propertys = bean.elements(); if (propertys.size() \u0026gt; 0) { for (Element property : propertys) { String key = property.attributeValue(\u0026#34;name\u0026#34;); Field field = beanClass.getDeclaredField(key); field.setAccessible(true); List\u0026lt;Element\u0026gt; childBean = property.elements(); // 如果property下内嵌bean if (childBean.size() \u0026gt; 0) { Object childObject = getBean(key, property); field.set(object, childObject); } else { /* * 此属性值是一个字符串.这里单独处理int,float类型变量.如果不处理, * 会将String类型直接赋值给int类型,发生ClassCastException */ String value = property.attributeValue(\u0026#34;value\u0026#34;); // 需要对类型进行判断 if (field.getType().getName().equals(\u0026#34;int\u0026#34;)) { // 整数 int x = Integer.parseInt(value); field.set(object, x); continue; } if (field.getType().getName().equals(\u0026#34;float\u0026#34;)) { // 浮点数 float y = Float.parseFloat(value); field.set(object, y); // 注意double可以接受float类型 continue; } field.set(object, value);// 处理String类型 } } } objects.add(object); } } } } catch (Exception e) { e.printStackTrace(); } return objects; } /** * 获取property内嵌的bean. * @param name id或者bean的name * @param root 根节点. * @return 返回封装完整的bean. * @throws Exception Exception. */ public Object getBean(String name, Element root) throws Exception { Object object = null; List\u0026lt;Element\u0026gt; beans = root.elements(); if (beans.size() \u0026gt; 0) { for (Element bean : beans) { if (bean.attributeValue(\u0026#34;name\u0026#34;).equals(name)) { // 如果bean name相同则开始创建对象 String clazz = bean.attributeValue(\u0026#34;class\u0026#34;); // 通过反射来创建对象 Class beanClass = Class.forName(clazz); object = beanClass.newInstance(); List\u0026lt;Element\u0026gt; propertys = bean.elements(); if (propertys.size() \u0026gt; 0) { for (Element property : propertys) { String key = property.attributeValue(\u0026#34;name\u0026#34;); Field field = beanClass.getDeclaredField(key); field.setAccessible(true); List\u0026lt;Element\u0026gt; childBean = property.elements(); // 如果property下内嵌bean if (childBean.size() \u0026gt; 0) { field.set(object, getBean(key, property)); } if (property.attribute(\u0026#34;ref\u0026#34;) != null) { /* * 此属性的值是一个对象.这里由于直接调用getBean方法赋值给对象,返回的对象一定是Bean参数的对象, 因此强制转换不会出问题 */ String refid = property.attributeValue(\u0026#34;ref\u0026#34;); field.set(object, getBean(refid)); } else { /* * 此属性值是一个字符串.这里单独处理int,float类型变量.如果不处理,会将String类型直接赋值给int类型, * 发生ClassCastException */ String value = property.attributeValue(\u0026#34;value\u0026#34;); // 需要对类型进行判断 field.set(object, value);// 处理String类型 } } } } } } return object; } } 楼主的代码就是实现读取 XML 文件中相同类型的 bean 封装到 list 中返回。具体如何实现，看代码，注释写的很清楚了。\n文件读取的效率提升 120 多倍 # 代码完成之后，对比之前的读取 XML 的代码，比之前的效率提升了 120 多倍。\n总结一下 # 大牛都说要看开源框架的源码，收获会怎样怎样，但是对于大部分像我这样的伪码农去扒源码的时候总是一头雾水，不知所云。然而，当我们真正有需求的时候，开源代码的实现便成了一份巨大的宝藏，带着我们的目的去扒源码，有时候会有事半功倍的效果。楼主亲测有效，源码虽好，可不要贪杯哦。\n","date":"2017-08-23","externalUrl":null,"permalink":"/posts/bayibaspring-dom4jshixianmonishixianduquxml/","section":"文章","summary":"今天 leadr 提出需求，原来公司项目中读取解析 XML 文件的代码效率太低，考虑切换一种 XML 为数据封装格式与读取方式以提高效率。我这灵机一动 Spring 对 bean 的依赖注入就是读取 XML 文件，可以尝试扒一扒 Spring 的源码，来实现一个轻量级的方案。重构 XML 文件，向 sprin…","title":"扒一扒 Spring，dom4j 实现模拟实现读取 XML","type":"posts"},{"content":"📌 本文原发布于代码星冰乐：MySQL 的七种 join\n对于 SQL 的 Join，学习起来可能是比较乱的。我们知道，SQL 的 Join 语法有很多 inner 的，有 outer 的，有 left 的，有时候，对于 Select 出来的结果集是什么样子有点不是很清楚。Coding Horror 上有一篇文章（实在不清楚为什么 Coding Horror 也被墙）通过 文氏图 Venn diagrams 解释了 SQL 的 Join。\\\n建表 # 在这里呢我们先来建立两张有外键关联的表。\nCREATE DATABASE db0206; USE db0206; CREATE TABLE `db0206`.`tbl_dept`( `id` INT(11) NOT NULL AUTO_INCREMENT, `deptName` VARCHAR(30), `locAdd` VARCHAR(40), PRIMARY KEY (`id`) ) ENGINE=INNODB CHARSET=utf8; CREATE TABLE `db0206`.`tbl_emp`( `id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(20), `deptId` INT(11), PRIMARY KEY (`id`), FOREIGN KEY (`deptId`) REFERENCES `db0206`.`tb_dept`(`id`) ) ENGINE=INNODB CHARSET=utf8; /*插入数据*/ INSERT INTO tbl_dept(deptName,locAdd) VALUES('RD',11); INSERT INTO tbl_dept(deptName,locAdd) VALUES('HR',12); INSERT INTO tbl_dept(deptName,locAdd) VALUES('MK',13); INSERT INTO tbl_dept(deptName,locAdd) VALUES('MIS',14); INSERT INTO tbl_dept(deptName,locAdd) VALUES('FD',15); INSERT INTO tbl_emp(NAME,deptId) VALUES('z3',1); INSERT INTO tbl_emp(NAME,deptId) VALUES('z4',1); INSERT INTO tbl_emp(NAME,deptId) VALUES('z5',1); INSERT INTO tbl_emp(NAME,deptId) VALUES('w5',2); INSERT INTO tbl_emp(NAME,deptId) VALUES('w6',2); INSERT INTO tbl_emp(NAME,deptId) VALUES('s7',3); INSERT INTO tbl_emp(NAME,deptId) VALUES('s8',4); 文氏图与 SQL 语句的编写以及查询结果 # 内连接 # 内连接文氏图 # 📷 图注：表的内连接\n执行的 SQL 语句以及执行的查询结果 # 执行的 SQL 语句\nselect * from tbl_dept a inner join tbl_emp b on a.id=b.deptId; 查询结果\\\n左外连接 # 左外连接文氏图 # 📷 图注：左连接\n执行的 SQL 语句以及执行的查询结果 # 执行的 SQL 语句 select * from tbl_dept a left join tbl_emp b on a.id=b.deptId; 查询结果\\ 📷 图注：左外连接\n右外连接 # 右外连接文氏图 # 执行的 SQL 语句以及执行的查询结果 # 执行的 SQL 语句 select * from tbl_dept a right join tbl_emp b on a.id=b.deptId; 查询结果\\ 左连接 # 左连接文氏图 # 执行的 SQL 语句以及执行的查询结果 # 执行的 SQL 语句 elect * from tbl_dept a left join tbl_emp b on a.id=b.deptId where b.deptId is null; 查询结果 右连接 # 右连接文氏图 # 📷 图注：右连接\n执行的 SQL 语句以及执行的查询结果 # 执行的 SQL 语句 select * from tbl_dept a right join tbl_emp b on a.id=b.deptId where a.id is null; 查询结果 📷 图注：右连接\n全连接 # 全连接文氏图 # 执行的 SQL 语句以及执行的查询结果 # 执行的 SQL 语句 select * from tbl_dept a right join tbl_emp b on a.id=b.deptId union select * from tbl_dept a left join tbl_emp b on a.id=b.deptId; 查询结果\\ 📷 图注：全连接\n两张表中都没有出现的数据集 # 文氏图 # 执行的 SQL 语句以及执行的查询结果 # 执行的 SQL 语句 select * from tbl_dept a right join tbl_emp b on a.id=b.deptId where a.id is null union select * from tbl_dept a left join tbl_emp b on a.id=b.deptId where b.deptId is null; 查询结果 ","date":"2017-02-06","externalUrl":null,"permalink":"/posts/mysql--de-qi-zhong--join/","section":"文章","summary":"对于 SQL 的 Join，在学习起来可能是比较乱的。我们知道，SQL 的 Join 语法有很多 inner 的，有 outer 的，有 left 的，有时候，对于 Select 出来的结果集是什么样子有点不是很清楚。Coding Horror 上有一篇文章（实在不清楚","title":"MySQL 的七种 join","type":"posts"},{"content":" Hi, I\u0026rsquo;m haifeiWu 👋 # from coder to master……\nBackend developer based in Beijing, passionate about distributed systems, middleware and AI engineering. Currently diving deep into Go \u0026amp; Rust.\nWhat I do # 🛠️ Building distributed middleware: config centers, RPC frameworks, message push platforms 🧠 Learning \u0026amp; sharing: Go, Rust, Netty, Kafka, Redis, system design 🤖 Exploring AI engineering: MCP servers, AI gateways, sentiment monitoring tools ✍️ Writing on Juejin (64+ articles) about my practice and learning Languages \u0026amp; Tools # Go · Java · Python · Rust · TypeScript · Netty · Kafka · Redis · MySQL · Docker\nGet in touch # 📧 Email: whfstudio@163.com 🐙 GitHub: github.com/haifeiWu ✍️ Juejin: juejin.cn/user/1574156379896103 ","externalUrl":null,"permalink":"/en/about/","section":"haifeiWu","summary":"Hi, I’m haifeiWu 👋 # from coder to master……\nBackend developer based in Beijing, passionate about distributed systems, middleware and AI engineering. Currently diving deep into Go \u0026 Rust.\n","title":"About","type":"page"},{"content":"","externalUrl":null,"permalink":"/en/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"AI 网关，统一接入与管理多个 AI 服务。\nAI Gateway for unified access and management of AI services.\n","externalUrl":"https://github.com/haifeiWu/ai-gateway","permalink":"/en/projects/ai-gateway/","section":"Projects","summary":"AI 网关，统一接入与管理多个 AI 服务。\nAI Gateway for unified access and management of AI services.\n","title":"ai-gateway","type":"projects"},{"content":"A quick example of how to start using author taxonomies in your articles.\n","externalUrl":null,"permalink":"/en/authors/","section":"Authors Taxonomy Listing Example","summary":"A quick example of how to start using author taxonomies in your articles.\n","title":"Authors Taxonomy Listing Example","type":"authors"},{"content":"","externalUrl":null,"permalink":"/en/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"Netty 实现的轻量级 RPC 框架，从零手写。\nA lightweight RPC framework implemented from scratch with Netty.\n","externalUrl":"https://github.com/haifeiWu/child-rpc","permalink":"/en/projects/child-rpc/","section":"Projects","summary":"Netty 实现的轻量级 RPC 框架，从零手写。\nA lightweight RPC framework implemented from scratch with Netty.\n","title":"child-rpc","type":"projects"},{"content":"","externalUrl":null,"permalink":"/en/tags/concurrency/","section":"Tags","summary":"","title":"Concurrency","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/tags/disruptor/","section":"Tags","summary":"","title":"Disruptor","type":"tags"},{"content":"Disruptor 无锁队列学习笔记与实践。\nDisruptor lock-free queue learning notes and practice.\n","externalUrl":"https://github.com/haifeiWu/disruptor-learn","permalink":"/en/projects/disruptor-learn/","section":"Projects","summary":"Disruptor 无锁队列学习笔记与实践。\nDisruptor lock-free queue learning notes and practice.\n","title":"disruptor-learn","type":"projects"},{"content":"","externalUrl":null,"permalink":"/en/tags/distributed/","section":"Tags","summary":"","title":"Distributed","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/tags/go/","section":"Tags","summary":"","title":"Go","type":"tags"},{"content":"Backend developer in Beijing, passionate about distributed systems, middleware and AI engineering. Currently diving deep into Go \u0026amp; Rust.\nYou can find my articles in the Blog, my open-source work in Projects, and more about me in About.\n","externalUrl":null,"permalink":"/en/","section":"haifeiWu","summary":"Backend developer in Beijing, passionate about distributed systems, middleware and AI engineering. Currently diving deep into Go \u0026 Rust.\nYou can find my articles in the Blog, my open-source work in Projects, and more about me in About.\n","title":"haifeiWu","type":"page"},{"content":"Backend developer in Beijing. Passionate about distributed systems, middleware and AI engineering.\n","externalUrl":null,"permalink":"/en/authors/haifeiwu/","section":"Authors Taxonomy Listing Example","summary":"Backend developer in Beijing. Passionate about distributed systems, middleware and AI engineering.\n","title":"haifeiWu","type":"authors"},{"content":"","externalUrl":null,"permalink":"/en/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":"基于 Netty 实现的轻量级分布式应用配置中心。\nA lightweight distributed application configuration center built on Netty.\n","externalUrl":"https://github.com/haifeiWu/lightconf","permalink":"/en/projects/lightconf/","section":"Projects","summary":"基于 Netty 实现的轻量级分布式应用配置中心。\nA lightweight distributed application configuration center built on Netty.\n","title":"lightconf","type":"projects"},{"content":"","externalUrl":null,"permalink":"/en/tags/mcp/","section":"Tags","summary":"","title":"MCP","type":"tags"},{"content":"生产级 GitLab MCP Server，内置 86 个工具，让 AI 智能体（Claude、Cursor、Zed）全面操控 GitLab。\nProduction-grade GitLab MCP server with 86 tools — full GitLab control from any AI agent.\n","externalUrl":"https://github.com/haifeiWu/mcp-gitlab-server","permalink":"/en/projects/mcp-gitlab-server/","section":"Projects","summary":"生产级 GitLab MCP Server，内置 86 个工具，让 AI 智能体（Claude、Cursor、Zed）全面操控 GitLab。\n","title":"mcp-gitlab-server","type":"projects"},{"content":"","externalUrl":null,"permalink":"/en/tags/messaging/","section":"Tags","summary":"","title":"Messaging","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/tags/netty/","section":"Tags","summary":"","title":"Netty","type":"tags"},{"content":"Selected open-source projects I\u0026rsquo;ve built.\n","externalUrl":null,"permalink":"/en/projects/","section":"Projects","summary":"Selected open-source projects I’ve built.\n","title":"Projects","type":"projects"},{"content":"","externalUrl":null,"permalink":"/en/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/tags/rpc/","section":"Tags","summary":"","title":"RPC","type":"tags"},{"content":"","externalUrl":null,"permalink":"/en/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/en/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"AI 舆情监控分析工具：聚合 35 个平台热点，基于 MCP 的 AI 分析，多端推送。\nAI sentiment monitoring: aggregates hot topics from 35 platforms with MCP-based AI analysis and multi-channel push.\n","externalUrl":"https://github.com/haifeiWu/TrendRadar","permalink":"/en/projects/trendradar/","section":"Projects","summary":"AI 舆情监控分析工具：聚合 35 个平台热点，基于 MCP 的 AI 分析，多端推送。\n","title":"TrendRadar","type":"projects"},{"content":"","externalUrl":null,"permalink":"/en/tags/typescript/","section":"Tags","summary":"","title":"TypeScript","type":"tags"},{"content":"千万级消息推送平台，支持多通道统一推送。\nUnified push platform designed for 10M+ scale messaging.\n","externalUrl":"https://github.com/haifeiWu/unified-push-platform","permalink":"/en/projects/unified-push-platform/","section":"Projects","summary":"千万级消息推送平台，支持多通道统一推送。\nUnified push platform designed for 10M+ scale messaging.\n","title":"unified-push-platform","type":"projects"},{"content":"","externalUrl":null,"permalink":"/tags/%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81/","section":"标签","summary":"","title":"消息推送","type":"tags"}]